diff options
author | 2015-02-12 13:27:06 +0000 | |
---|---|---|
committer | 2015-02-12 13:27:06 +0000 | |
commit | b0e387b18aa1d505deb85c3d27fb99fbf344fd2a (patch) | |
tree | 3faad9c437c34cc78a0f547df2751febd4a9dbc8 /src/test | |
parent | 7e73a28c3da7011a2b808f4561918bd676bc66c4 (diff) |
Add shell tests to bazel.
--
MOS_MIGRATED_REVID=86171408
Diffstat (limited to 'src/test')
13 files changed, 1861 insertions, 0 deletions
diff --git a/src/test/java/BUILD b/src/test/java/BUILD index 03b14811c3..1dfc4af8fc 100644 --- a/src/test/java/BUILD +++ b/src/test/java/BUILD @@ -166,3 +166,34 @@ java_test( "//third_party:truth", ], ) + +cc_binary( + name = "com/google/devtools/build/lib/shell/killmyself", + srcs = ["com/google/devtools/build/lib/shell/killmyself.cc"], +) + +java_test( + name = "shell_test", + srcs = glob([ + "com/google/devtools/build/lib/shell/*.java", + ]), + args = ["com.google.devtools.build.lib.AllTests"], + data = [ + ":com/google/devtools/build/lib/shell/killmyself", + "//src/main/native:libunix.dylib", + "//src/main/native:libunix.so", + ], + deps = [ + ":foundations_testutil", + ":test_runner", + ":testutil", + "//src/main/java:bazel-core", + "//src/main/java:shell", + "//third_party:guava", + "//third_party:guava-testlib", + "//third_party:jsr305", + "//third_party:junit4", + "//third_party:mockito", + "//third_party:truth", + ], +) diff --git a/src/test/java/com/google/devtools/build/lib/shell/CommandLargeInputsTest.java b/src/test/java/com/google/devtools/build/lib/shell/CommandLargeInputsTest.java new file mode 100644 index 0000000000..06d4f9a52c --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/CommandLargeInputsTest.java @@ -0,0 +1,155 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Tests the command class with large inputs + * + */ +@RunWith(JUnit4.class) +public class CommandLargeInputsTest { + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + private byte[] getRandomBytes() { + byte[] randomBytes; + final Random rand = new Random(0xdeadbeef); + randomBytes = new byte[10000]; + rand.nextBytes(randomBytes); + return randomBytes; + } + + private byte[] getAllByteValues() { + byte[] allByteValues = new byte[Byte.MAX_VALUE - Byte.MIN_VALUE]; + for(int i = 0; i < allByteValues.length; i++) { + allByteValues[i] = (byte) (i + Byte.MIN_VALUE); + } + return allByteValues; + } + + @Test + public void testCatRandomBinary() throws Exception { + final Command command = new Command(new String[] {"cat"}); + byte[] randomBytes = getRandomBytes(); + final CommandResult result = command.execute(randomBytes); + assertEquals(0, result.getTerminationStatus().getRawResult()); + TestUtil.assertArrayEquals(randomBytes, result.getStdout()); + assertEquals(0, result.getStderr().length); + } + + @Test + public void testCatRandomBinaryToOutputStream() throws Exception { + final Command command = new Command(new String[] {"cat"}); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + byte[] randomBytes = getRandomBytes(); + final CommandResult result = command.execute(randomBytes, + Command.NO_OBSERVER, out, err); + assertEquals(0, result.getTerminationStatus().getRawResult()); + TestUtil.assertArrayEquals(randomBytes, out.toByteArray()); + assertEquals(0, err.toByteArray().length); + assertOutAndErrNotAvailable(result); + } + + @Test + public void testCatRandomBinaryToErrorStream() throws Exception { + final Command command = new Command(new String[] {"/bin/sh", "-c", "cat >&2"}); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + byte[] randomBytes = getRandomBytes(); + final CommandResult result = command.execute(randomBytes, + Command.NO_OBSERVER, out, err); + assertEquals(0, result.getTerminationStatus().getRawResult()); + assertEquals(0, out.toByteArray().length); + TestUtil.assertArrayEquals(randomBytes, err.toByteArray()); + assertOutAndErrNotAvailable(result); + } + + @Test + public void testCatRandomBinaryFromInputStreamToErrorStream() + throws Exception { + final Command command = new Command(new String[] {"/bin/sh", "-c", "cat >&2"}); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + byte[] randomBytes = getRandomBytes(); + ByteArrayInputStream in = new ByteArrayInputStream(randomBytes); + + final CommandResult result = command.execute(in, + Command.NO_OBSERVER, out, err); + assertEquals(0, result.getTerminationStatus().getRawResult()); + assertEquals(0, out.toByteArray().length); + TestUtil.assertArrayEquals(randomBytes, err.toByteArray()); + assertOutAndErrNotAvailable(result); + } + + @Test + public void testStdoutInterleavedWithStdErr() throws Exception { + final Command command = new Command(new String[]{"/bin/bash", + "-c", "for i in $( seq 0 999); do (echo OUT$i >&1) && (echo ERR$i >&2); done" + }); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, err); + StringBuffer expectedOut = new StringBuffer(); + StringBuffer expectedErr = new StringBuffer(); + for (int i = 0; i < 1000; i++) { + expectedOut.append("OUT").append(Integer.toString(i)).append("\n"); + expectedErr.append("ERR").append(Integer.toString(i)).append("\n"); + } + assertEquals(expectedOut.toString(), out.toString("UTF-8")); + assertEquals(expectedErr.toString(), err.toString("UTF-8")); + } + + private void assertOutAndErrNotAvailable(final CommandResult result) { + try { + result.getStdout(); + fail(); + } catch (IllegalStateException e){} + try { + result.getStderr(); + fail(); + } catch (IllegalStateException e){} + } + + @Test + public void testCatAllByteValues() throws Exception { + final Command command = new Command(new String[] {"cat"}); + byte[] allByteValues = getAllByteValues(); + final CommandResult result = command.execute(allByteValues); + assertEquals(0, result.getTerminationStatus().getRawResult()); + assertEquals(0, result.getStderr().length); + TestUtil.assertArrayEquals(allByteValues, result.getStdout()); + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/CommandTest.java b/src/test/java/com/google/devtools/build/lib/shell/CommandTest.java new file mode 100644 index 0000000000..ce0e8e4604 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/CommandTest.java @@ -0,0 +1,692 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static com.google.devtools.build.lib.shell.TestUtil.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.testutil.TestConstants; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Unit tests for {@link Command}. This test will only succeed on Linux, + * currently, because of its non-portable nature. + */ +@RunWith(JUnit4.class) +public class CommandTest { + + private static final long LONG_TIME = 10000; + private static final long SHORT_TIME = 250; + + // Platform-independent tests ---------------------------------------------- + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + @Test + public void testIllegalArgs() throws Exception { + + try { + new Command((String[]) null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + try { + new Command(new String[] {"/bin/true", null}).execute(); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + // good + } + + try { + new Command(new String[] {"foo"}).execute(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + // good + } + + } + + @Test + public void testProcessBuilderConstructor() throws Exception { + String helloWorld = "Hello, world"; + ProcessBuilder builder = new ProcessBuilder("/bin/echo", helloWorld); + byte[] stdout = new Command(builder).execute().getStdout(); + assertEquals(helloWorld + '\n', new String(stdout, "UTF-8")); + } + + @Test + public void testMaybeUseShell() throws Exception { + String helloWorld = "Hello, world"; + byte[] stdout = new Command(new String[]{"/bin/echo", helloWorld}, + true, null, null).execute().getStdout(); + assertEquals(helloWorld + '\n', new String(stdout, "UTF-8")); + } + + @Test + public void testGetters() { + final File workingDir = new File("."); + final Map<String,String> env = Collections.singletonMap("foo", "bar"); + final String[] commandArgs = new String[] { "command" }; + final Command command = new Command(commandArgs, env, workingDir); + assertArrayEquals(commandArgs, command.getCommandLineElements()); + for (final String key : env.keySet()) { + assertEquals(env.get(key), command.getEnvironmentVariables().get(key)); + } + assertEquals(workingDir, command.getWorkingDirectory()); + } + + // Platform-dependent tests ------------------------------------------------ + + @Test + public void testSimpleCommand() throws Exception { + final Command command = new Command(new String[] {"ls"}); + final CommandResult result = command.execute(); + assertTrue(result.getTerminationStatus().success()); + assertTrue(result.getStderr().length == 0); + assertTrue(result.getStdout().length > 0); + } + + @Test + public void testArguments() throws Exception { + final Command command = new Command(new String[] {"echo", "foo"}); + checkSuccess(command.execute(), "foo\n"); + } + + @Test + public void testEnvironment() throws Exception { + final Map<String,String> env = Collections.singletonMap("FOO", "BAR"); + final Command command = new Command(new String[] {"echo", "$FOO"}, true, env, null); + checkSuccess(command.execute(), "BAR\n"); + } + + @Test + public void testWorkingDir() throws Exception { + final Command command = new Command(new String[] {"pwd"}, null, new File("/")); + checkSuccess(command.execute(), "/\n"); + } + + @Test + public void testStdin() throws Exception { + final Command command = new Command(new String[] {"grep", "bar"}); + checkSuccess(command.execute("foobarbaz".getBytes()), "foobarbaz\n"); + } + + @Test + public void testRawCommand() throws Exception { + final Command command = new Command(new String[] { "perl", + "-e", + "print 'a'x100000" }); + final CommandResult result = command.execute(); + assertTrue(result.getTerminationStatus().success()); + assertTrue(result.getStderr().length == 0); + assertTrue(result.getStdout().length > 0); + } + + @Test + public void testRawCommandWithDir() throws Exception { + final Command command = new Command(new String[] { "pwd" }, + null, + new File("/")); + final CommandResult result = command.execute(); + checkSuccess(result, "/\n"); + } + + @Test + public void testHugeOutput() throws Exception { + final Command command = new Command(new String[] {"perl", "-e", "print 'a'x100000"}); + final CommandResult result = command.execute(); + assertTrue(result.getTerminationStatus().success()); + assertEquals(0, result.getStderr().length); + assertEquals(100000, result.getStdout().length); + } + + @Test + public void testIgnoreOutput() throws Exception { + final Command command = new Command(new String[] {"perl", "-e", "print 'a'x100000"}); + final CommandResult result = command.execute(Command.NO_INPUT, null, true); + assertTrue(result.getTerminationStatus().success()); + try { + result.getStdout(); + fail("Should have thrown IllegalStateException"); + } catch (IllegalStateException ise) { + // good + } + try { + result.getStderr(); + fail("Should have thrown IllegalStateException"); + } catch (IllegalStateException ise) { + // good + } + } + + @Test + public void testNoStreamingInputForCat() throws Exception { + final Command command = new Command(new String[]{"/bin/cat"}); + ByteArrayInputStream emptyInput = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + CommandResult result = command.execute(emptyInput, + Command.NO_OBSERVER, out, err); + assertTrue(result.getTerminationStatus().success()); + assertEquals("", out.toString("UTF-8")); + assertEquals("", err.toString("UTF-8")); + } + + @Test + public void testNoInputForCat() throws Exception { + final Command command = new Command(new String[]{"/bin/cat"}); + CommandResult result = command.execute(); + assertTrue(result.getTerminationStatus().success()); + assertEquals("", new String(result.getStdout(), "UTF-8")); + assertEquals("", new String(result.getStderr(), "UTF-8")); + } + + @Test + public void testProvidedOutputStreamCapturesHelloWorld() throws Exception { + String helloWorld = "Hello, world."; + final Command command = new Command(new String[]{"/bin/echo", helloWorld}); + ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); + ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + command.execute(Command.NO_INPUT, Command.NO_OBSERVER, stdOut, stdErr); + assertEquals(helloWorld + "\n", stdOut.toString("UTF-8")); + assertEquals(0, stdErr.toByteArray().length); + } + + @Test + public void testAsynchronous() throws Exception { + final File tempFile = File.createTempFile("googlecron-test", "tmp"); + tempFile.delete(); + final Command command = new Command(new String[] {"touch", tempFile.getAbsolutePath()}); + // Shouldn't throw any exceptions: + FutureCommandResult result = + command.executeAsynchronously(Command.NO_INPUT); + result.get(); + assertTrue(tempFile.exists()); + assertTrue(result.isDone()); + tempFile.delete(); + } + + @Test + public void testAsynchronousWithKillable() throws Exception { + final Command command = new Command(new String[] {"sleep", "5"}); + final SimpleKillableObserver observer = new SimpleKillableObserver(); + FutureCommandResult result = + command.executeAsynchronously(Command.NO_INPUT, observer); + assertFalse(result.isDone()); + observer.kill(); + try { + result.get(); + } catch (AbnormalTerminationException e) { + // Expects, but does not insist on termination with a signal. + } + assertTrue(result.isDone()); + } + + @Test + public void testAsynchronousWithOutputStreams() throws Exception { + + final String helloWorld = "Hello, world."; + final Command command = new Command(new String[]{"/bin/echo", helloWorld}); + final ByteArrayInputStream emptyInput = + new ByteArrayInputStream(new byte[0]); + final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); + final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + FutureCommandResult result = command.executeAsynchronously(emptyInput, + Command.NO_OBSERVER, + stdOut, + stdErr); + result.get(); // Make sure the process actually finished + assertEquals(helloWorld + "\n", stdOut.toString("UTF-8")); + assertEquals(0, stdErr.toByteArray().length); + } + + @Test + public void testSimpleKillableObserver() throws Exception { + final Command command = new Command(new String[] {"sleep", "5"}); + final SimpleKillableObserver observer = new SimpleKillableObserver(); + new Thread() { + @Override + public void run() { + try { + command.execute(Command.NO_INPUT, observer, true); + fail(); + } catch (CommandException e) { + // Good. + checkCommandElements(e, "/bin/sh", "-c", "sleep 5"); + } + } + }.start(); + Thread.yield(); + observer.kill(); + } + + @Test + public void testTimeout() throws Exception { + // Sleep for 3 seconds, + final Command command = new Command(new String[] {"sleep", "3"}); + try { + // but timeout after 1 second + command.execute(Command.NO_INPUT, 1000L, false); + fail("Should have thrown AbnormalTerminationException"); + } catch (AbnormalTerminationException ate) { + // good + checkCommandElements(ate, "sleep", "3"); + checkATE(ate); + } + } + + @Test + public void testTimeoutDoesntFire() throws Exception { + final Command command = new Command(new String[] {"cat"}); + command.execute(new byte[]{'H', 'i', '!'}, 2000L, false); + } + + @Test + public void testCommandDoesNotExist() throws Exception { + final Command command = new Command(new String[]{"thisisnotreal"}); + try { + command.execute(); + fail(); + } catch (ExecFailedException e){ + // Good. + checkCommandElements(e, "thisisnotreal"); + } + } + + @Test + public void testNoSuchCommand() throws Exception { + final Command command = new Command(new String[] {"thisisnotreal"}); + try { + command.execute(); + fail("Should have thrown ExecFailedException"); + } catch (ExecFailedException expected) { + // good + } + } + + @Test + public void testExitCodes() throws Exception { + // 0 => success + { + String args[] = { "/bin/sh", "-c", "exit 0" }; + CommandResult result = new Command(args).execute(); + TerminationStatus status = result.getTerminationStatus(); + assertTrue(status.success()); + assertTrue(status.exited()); + assertEquals(0, status.getExitCode()); + } + + // Every exit value in range [1-255] is reported as such (except [129-191], + // which map to signals). + for (int exit : new int[] { 1, 2, 3, 127, 128, 192, 255 }) { + try { + String args[] = { "/bin/sh", "-c", "exit " + exit }; + new Command(args).execute(); + fail("Should have exited with status " + exit); + } catch (BadExitStatusException e) { + assertEquals("Process exited with status " + exit, e.getMessage()); + checkCommandElements(e, "/bin/sh", "-c", "exit " + exit); + TerminationStatus status = e.getResult().getTerminationStatus(); + assertFalse(status.success()); + assertTrue(status.exited()); + assertEquals(exit, status.getExitCode()); + assertEquals("Exit " + exit , status.toShortString()); + } + } + + // negative exit values are modulo 256: + for (int exit : new int[] { -1, -2, -3 }) { + int expected = 256 + exit; + try { + String args[] = { "/bin/sh", "-c", "exit " + exit }; + new Command(args).execute(); + fail("Should have exited with status " + expected); + } catch (BadExitStatusException e) { + assertEquals("Process exited with status " + expected, e.getMessage()); + checkCommandElements(e, "/bin/sh", "-c", "exit " + exit); + TerminationStatus status = e.getResult().getTerminationStatus(); + assertFalse(status.success()); + assertTrue(status.exited()); + assertEquals(expected, status.getExitCode()); + assertEquals("Exit " + expected, status.toShortString()); + } + } + } + + @Test + public void testFailedWithSignal() throws Exception { + // SIGHUP, SIGINT, SIGKILL, SIGTERM + for (int signal : new int[] { 1, 2, 9, 15 }) { + // Invoke a C++ program (killmyself.cc) that will die + // with the specified signal. + String killmyself = BlazeTestUtils.runfilesDir() + "/" + + TestConstants.JAVATESTS_ROOT + + "/com/google/devtools/build/lib/shell/killmyself"; + try { + String args[] = { killmyself, "" + signal }; + new Command(args).execute(); + fail("Expected signal " + signal); + } catch (AbnormalTerminationException e) { + assertEquals("Process terminated by signal " + signal, e.getMessage()); + checkCommandElements(e, killmyself, "" + signal); + TerminationStatus status = e.getResult().getTerminationStatus(); + assertFalse(status.success()); + assertFalse(status.exited()); + assertEquals(signal, status.getTerminatingSignal()); + + switch (signal) { + case 1: assertEquals("Hangup", status.toShortString()); break; + case 2: assertEquals("Interrupt", status.toShortString()); break; + case 9: assertEquals("Killed", status.toShortString()); break; + case 15: assertEquals("Terminated", status.toShortString()); break; + } + } + } + } + + @Test + public void testDestroy() throws Exception { + + // Sleep for 10 seconds, + final Command command = new Command(new String[] {"sleep", "10"}); + + // but kill it after 1 + final KillableObserver killer = new KillableObserver() { + @Override + public void startObserving(final Killable killable) { + final Thread t = new Thread() { + @Override + public void run() { + try { + Thread.sleep(1000L); + } catch (InterruptedException ie) { + // continue + } + killable.kill(); + } + }; + t.start(); + } + @Override + public void stopObserving(final Killable killable) { + // do nothing + } + }; + + try { + command.execute(Command.NO_INPUT, killer, false); + fail("Should have thrown AbnormalTerminationException"); + } catch (AbnormalTerminationException ate) { + // Good. + checkCommandElements(ate, "sleep", "10"); + checkATE(ate); + } + } + + @Test + public void testOnlyReadsPartialInput() throws Exception { + Command command = new Command(new String[] {"head", "--bytes", "500"}); + OutputStream out = new ByteArrayOutputStream(); + InputStream in = new InputStream() { + + @Override + public int read() { + return 0; // write an unbounded amount + } + + }; + + CommandResult result = command.execute(in, Command.NO_OBSERVER, out, out); + TerminationStatus status = result.getTerminationStatus(); + assertTrue(status.success()); + } + + @Test + public void testFlushing() throws Exception { + final Command command = new Command( + new String[] {"/bin/sh", "-c", "echo -n Foo; sleep 0.1; echo Bar"}); + // We run this command, passing in a special output stream + // that records when each flush() occurs. + // We test that a flush occurs after writing "Foo" + // and that another flush occurs after writing "Bar\n". + final boolean[] flushed = new boolean[8]; + OutputStream out = new OutputStream() { + private int count = 0; + @Override + public void write(int b) throws IOException { + count++; + } + @Override + public void flush() throws IOException { + flushed[count] = true; + } + }; + command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, System.err); + assertFalse(flushed[0]); + assertFalse(flushed[1]); // 'F' + assertFalse(flushed[2]); // 'o' + assertTrue(flushed[3]); // 'o' <- expect flush here. + assertFalse(flushed[4]); // 'B' + assertFalse(flushed[5]); // 'a' + assertFalse(flushed[6]); // 'r' + assertTrue(flushed[7]); // '\n' + } + + // See also InterruptibleTest. + @Test + public void testInterrupt() throws Exception { + + // Sleep for 10 seconds, + final Command command = new Command(new String[] {"sleep", "10"}); + // Easy but hacky way to let this thread "return" a result to this method + final CommandResult[] resultContainer = new CommandResult[1]; + final Thread commandThread = new Thread() { + @Override + public void run() { + try { + resultContainer[0] = command.execute(); + } catch (CommandException ce) { + fail(ce.toString()); + } + } + }; + commandThread.start(); + + Thread.sleep(1000L); + + // but interrupt it after 1 + commandThread.interrupt(); + + // should continue to wait and exit normally + commandThread.join(); + + final CommandResult result = resultContainer[0]; + assertTrue(result.getTerminationStatus().success()); + assertTrue(result.getStderr().length == 0); + assertTrue(result.getStdout().length == 0); + } + + @Test + public void testOutputStreamThrowsException() throws Exception { + OutputStream out = new OutputStream () { + @Override + public void write(int b) throws IOException { + throw new IOException(); + } + }; + Command command = new Command(new String[] {"/bin/echo", "foo"}); + try { + command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, out); + fail(); + } catch (AbnormalTerminationException e) { + // Good. + checkCommandElements(e, "/bin/echo", "foo"); + assertEquals("java.io.IOException", e.getMessage()); + } + } + + @Test + public void testOutputStreamThrowsExceptionAndCommandFails() + throws Exception { + OutputStream out = new OutputStream () { + @Override + public void write(int b) throws IOException { + throw new IOException(); + } + }; + Command command = new Command(new String[] {"cat", "/dev/thisisnotreal"}); + try { + command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, out); + fail(); + } catch (AbnormalTerminationException e) { + checkCommandElements(e, "cat", "/dev/thisisnotreal"); + TerminationStatus status = e.getResult().getTerminationStatus(); + // Subprocess either gets a SIGPIPE trying to write to our output stream, + // or it exits with failure. Both are observed, nondetermistically. + assertTrue(status.exited() + ? status.getExitCode() == 1 + : status.getTerminatingSignal() == 13); + assertTrue(e.getMessage(), + e.getMessage().endsWith("also encountered an error while attempting " + + "to retrieve output")); + } + } + + /** + * Helper to test KillableObserver classes. + */ + private class KillableTester implements Killable { + private boolean isKilled = false; + private boolean timedOut = false; + @Override + public synchronized void kill() { + isKilled = true; + notifyAll(); + } + public synchronized boolean getIsKilled() { + return isKilled; + } + public synchronized boolean getTimedOut() { + return timedOut; + } + /** + * Wait for a specified time or until the {@link #kill()} is called. + */ + public synchronized void sleepUntilKilled(final long timeoutMS) { + long nowTime = System.currentTimeMillis(); + long endTime = nowTime + timeoutMS; + while (!isKilled && !timedOut) { + long waitTime = endTime - nowTime; + if (waitTime <= 0) { + // Process has timed out, needs killing. + timedOut = true; + break; + } + try { + wait(waitTime); // Suffers "spurious wakeup", hence the while() loop. + nowTime = System.currentTimeMillis(); + } catch (InterruptedException exception) { + break; + } + } + } + } + + @Test + public void testTimeOutKillableObserverNoKill() throws Exception { + KillableTester killable = new KillableTester(); + TimeoutKillableObserver observer = new TimeoutKillableObserver(LONG_TIME); + observer.startObserving(killable); + observer.stopObserving(killable); + assertFalse(observer.hasTimedOut()); + assertFalse(killable.getIsKilled()); + } + + @Test + public void testTimeOutKillableObserverNoKillWithDelay() throws Exception { + KillableTester killable = new KillableTester(); + TimeoutKillableObserver observer = new TimeoutKillableObserver(LONG_TIME); + observer.startObserving(killable); + killable.sleepUntilKilled(SHORT_TIME); + observer.stopObserving(killable); + assertFalse(observer.hasTimedOut()); + assertFalse(killable.getIsKilled()); + } + + @Test + public void testTimeOutKillableObserverWithKill() throws Exception { + KillableTester killable = new KillableTester(); + TimeoutKillableObserver observer = new TimeoutKillableObserver(SHORT_TIME); + observer.startObserving(killable); + killable.sleepUntilKilled(LONG_TIME); + observer.stopObserving(killable); + assertTrue(observer.hasTimedOut()); + assertTrue(killable.getIsKilled()); + } + + @Test + public void testTimeOutKillableObserverWithKillZeroMillis() throws Exception { + KillableTester killable = new KillableTester(); + TimeoutKillableObserver observer = new TimeoutKillableObserver(0); + observer.startObserving(killable); + killable.sleepUntilKilled(LONG_TIME); + observer.stopObserving(killable); + assertTrue(observer.hasTimedOut()); + assertTrue(killable.getIsKilled()); + } + + private static void checkCommandElements(CommandException e, + String... expected) { + assertArrayEquals(expected, e.getCommand().getCommandLineElements()); + } + + private static void checkATE(final AbnormalTerminationException ate) { + final CommandResult result = ate.getResult(); + assertFalse(result.getTerminationStatus().success()); + } + + private static void checkSuccess(final CommandResult result, + final String expectedOutput) { + assertTrue(result.getTerminationStatus().success()); + assertTrue(result.getStderr().length == 0); + assertEquals(expectedOutput, new String(result.getStdout())); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/ConsumersTest.java b/src/test/java/com/google/devtools/build/lib/shell/ConsumersTest.java new file mode 100644 index 0000000000..d03012dac1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/ConsumersTest.java @@ -0,0 +1,172 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.devtools.build.lib.shell.Consumers.OutErrConsumers; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +@RunWith(JUnit4.class) +public class ConsumersTest { + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + private static final String SECRET_MESSAGE = "This is a secret message."; + + /** + * Tests that if an IOException occurs in an output stream, the exception + * will be recorded and thrown when we call waitForCompletion. + */ + @Test + public void testAsynchronousIOExceptionInConsumerOutputStream() { + + OutputStream out = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException(SECRET_MESSAGE); + } + }; + OutErrConsumers outErr = Consumers.createStreamingConsumers(out, out); + ByteArrayInputStream outInput = new ByteArrayInputStream(new byte[]{'a'}); + ByteArrayInputStream errInput = new ByteArrayInputStream(new byte[0]); + outErr.registerInputs(outInput, errInput, false); + try { + outErr.waitForCompletion(); + fail(); + } catch (IOException e) { + assertEquals(SECRET_MESSAGE, e.getMessage()); + } + } + + /** + * Tests that if an OutOfMemeoryError occurs in an output stream, it + * will be recorded and thrown when we call waitForCompletion. + */ + @Test + public void testAsynchronousOutOfMemoryErrorInConsumerOutputStream() { + final OutOfMemoryError error = new OutOfMemoryError(SECRET_MESSAGE); + OutputStream out = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw error; + } + }; + OutErrConsumers outErr = Consumers.createStreamingConsumers(out, out); + ByteArrayInputStream outInput = new ByteArrayInputStream(new byte[]{'a'}); + ByteArrayInputStream errInput = new ByteArrayInputStream(new byte[0]); + outErr.registerInputs(outInput, errInput, false); + try { + outErr.waitForCompletion(); + fail(); + } catch (IOException e) { + fail(); + } catch (OutOfMemoryError e) { + assertSame("OutOfMemoryError is not masked", error, e); + } + } + + /** + * Tests that if an Error occurs in an output stream, the error + * will be recorded and thrown when we call waitForCompletion. + */ + @Test + public void testAsynchronousErrorInConsumerOutputStream() { + OutputStream out = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new OutOfMemoryError(SECRET_MESSAGE); + } + }; + OutErrConsumers outErr = Consumers.createStreamingConsumers(out, out); + ByteArrayInputStream outInput = new ByteArrayInputStream(new byte[]{'a'}); + ByteArrayInputStream errInput = new ByteArrayInputStream(new byte[0]); + outErr.registerInputs(outInput, errInput, false); + try { + outErr.waitForCompletion(); + fail(); + } catch (IOException e) { + fail(); + } catch (Error e) { + assertEquals(SECRET_MESSAGE, e.getMessage()); + } + } + + /** + * Tests that if an RuntimeException occurs in an output stream, the exception + * will be recorded and thrown when we call waitForCompletion. + */ + @Test + public void testAsynchronousRuntimeExceptionInConsumerOutputStream() + throws Exception { + OutputStream out = new OutputStream() { + @Override + public void write(int b) { + throw new RuntimeException(SECRET_MESSAGE); + } + }; + OutErrConsumers outErr = Consumers.createStreamingConsumers(out, out); + ByteArrayInputStream outInput = new ByteArrayInputStream(new byte[]{'a'}); + ByteArrayInputStream errInput = new ByteArrayInputStream(new byte[0]); + outErr.registerInputs(outInput, errInput, false); + try { + outErr.waitForCompletion(); + fail(); + } catch (RuntimeException e) { + assertEquals(SECRET_MESSAGE, e.getMessage()); + } + } + + /** + * Basically tests that Consumers#silentClose(InputStream) works properly. + */ + @Test + public void testSilentlyDropIOExceptionWhenClosingInputStream() { + InputStream in = new ByteArrayInputStream(new byte[]{'a'}){ + @Override + public void close() throws IOException { + throw new IOException("Please ignore me!"); + } + }; + OutErrConsumers outErr = Consumers.createDiscardingConsumers(); + outErr.registerInputs(in, in, false); + try { + outErr.waitForCompletion(); + // yeah! + } catch (IOException e) { + fail(); // this should not throw an exception, since we're silently + // closing the output! + } + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/FutureConsumptionTest.java b/src/test/java/com/google/devtools/build/lib/shell/FutureConsumptionTest.java new file mode 100644 index 0000000000..578e046542 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/FutureConsumptionTest.java @@ -0,0 +1,106 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertTrue; + +import com.google.devtools.build.lib.shell.Consumers.OutErrConsumers; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Tests that InterruptedExceptions can't derail FutureConsumption + * instances; well, FutureConsumption is really an implementation detail, + * but we want to exercise this code, so what ... + */ +@RunWith(JUnit4.class) +public class FutureConsumptionTest { + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + private OutputStream DEV_NULL = new OutputStream() { + @Override + public void write(int b) {} + }; + + private boolean inputFinished; + + @Test + public void testFutureConsumptionIgnoresInterruptedExceptions() + throws Exception { + // Set this up so that the consumer actually have to stream stuff into + // DEV_NULL, which the discards everything. + OutErrConsumers outErr = Consumers.createStreamingConsumers(DEV_NULL, + DEV_NULL); + + inputFinished = false; + + // We keep producing input until the other thread (the main test thread) + // tells us to shut up ... + InputStream outInput = new InputStream() { + + @Override + public int read() { + if(inputFinished){ + return -1; + } + return 0; + } + + }; + ByteArrayInputStream errInput = new ByteArrayInputStream(new byte[0]); + outErr.registerInputs(outInput, errInput, false); + // OK, this is the main test thread, which we need to interrupt *while* + // it's waiting in outErr.waitForCompletion() + final Thread testThread = Thread.currentThread(); + + // go into a different thread, wait a bit, interrupt the test thread, + // wait a bit, and tell the input stream to finish. + new Thread() { + + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + testThread.interrupt(); // this is what we're testing; basic + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + inputFinished = true; + } + + }.start(); + + outErr.waitForCompletion(); + // In addition to asserting that we were interrupted, this clears the interrupt bit of the + // current thread, since Junit doesn't do it for us. This avoids the next test to run starting + // in an interrupted state. + assertTrue(Thread.interrupted()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/InterruptibleTest.java b/src/test/java/com/google/devtools/build/lib/shell/InterruptibleTest.java new file mode 100644 index 0000000000..797093f363 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/InterruptibleTest.java @@ -0,0 +1,138 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests of the interaction of Thread.interrupt and Command.execute. + * + * Read http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html + * for background material. + * + * NOTE: This test is dependent on thread timings. Under extreme machine load + * it's possible that this test could fail spuriously or intermittently. In + * that case, adjust the timing constants to increase the tolerance. + */ +@RunWith(JUnit4.class) +public class InterruptibleTest { + + private final Thread mainThread = Thread.currentThread(); + + // Interrupt main thread after 1 second. Hopefully by then /bin/sleep + // should be running. + private final Thread interrupter = new Thread() { + @Override + public void run() { + try { + Thread.sleep(1000); // 1 sec + } catch (InterruptedException e) { + throw new IllegalStateException("Unexpected interrupt!"); + } + mainThread.interrupt(); + } + }; + + private Command command; + private long before; + + @Before + public void setUp() throws Exception { + + Thread.interrupted(); // side effect: clear interrupted status + assertFalse("Unexpected interruption!", mainThread.isInterrupted()); + + this.command = new Command(new String[] { "/bin/sleep", "2" }); + this.before = System.nanoTime(); + + interrupter.start(); + } + + @After + public void tearDown() throws Exception { + interrupter.join(); + Thread.interrupted(); // Clear interrupted status, or else other tests may fail. + } + + private void assertDuration(long minMillis, long maxMillis) + throws Exception { + long after = System.nanoTime(); + long millis = (after - before) / 1000000; + if (millis < minMillis || millis > maxMillis) { + fail("Duration " + millis + "ms was not in range " + + minMillis + "-" + maxMillis + "ms"); + } + } + + /** + * Test that interrupting a thread in an "uninterruptible" Command.execute + * preserves the thread's interruptible status, and does not terminate the + * subprocess. + */ + @Test + public void testUninterruptibleCommandRunsToCompletion() throws Exception { + command.execute(); + + // Subprocess execution should be around 2000ms: + assertDuration(2000, 2500); + + // The interrupter thread should have exited about 1000ms ago. + assertFalse("Interrupter thread is still alive!", + interrupter.isAlive()); + + // The interrupter thread should have set the main thread's interrupt flag. + assertTrue("Main thread was not interrupted during command execution!", + mainThread.isInterrupted()); + } + + /** + * Test that interrupting a thread in an "interruptible" Command.execute + * causes preserves the thread's interruptible status, terminates the + * subprocess, and returns promptly. + */ + @Test + public void testInterruptibleCommand() throws Exception { + try { + command.execute(Command.NO_INPUT, + Command.NO_OBSERVER, + System.out, + System.err, + true); // => interruptible + fail("Subprocess not aborted!"); + } catch (AbnormalTerminationException e) { + assertEquals("Process terminated by signal 15", // SIGINT + e.getMessage()); + } + + // Subprocess execution should be around 1000ms: + assertDuration(1000, 1500); + + // We don't assert that the interrupter thread has exited; due to prompt + // termination it might still be running. + + // The interrupter thread should have set the main thread's interrupt flag. + assertTrue("Main thread was not interrupted during command execution!", + mainThread.isInterrupted()); + + } +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/LoadTest.java b/src/test/java/com/google/devtools/build/lib/shell/LoadTest.java new file mode 100644 index 0000000000..b49ddd9564 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/LoadTest.java @@ -0,0 +1,111 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Tests {@link Command} execution under load. + */ +@RunWith(JUnit4.class) +public class LoadTest { + + private File tempFile; + + @Before + public void setUp() throws IOException { + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + + // create a temp file + tempFile = File.createTempFile("LoadTest", "txt"); + if (tempFile.exists()) { + tempFile.delete(); + } + tempFile.deleteOnExit(); + + // write some random numbers to the file + final PrintWriter out = new PrintWriter(new FileWriter(tempFile)); + final Random r = new Random(); + for (int i = 0; i < 100; i++) { + out.println(String.valueOf(r.nextDouble())); + } + out.close(); + } + + @After + public void tearDown() throws Exception { + tempFile.delete(); + } + + @Test + public void testLoad() throws Throwable { + final Command command = new Command(new String[] {"/bin/cat", + tempFile.getAbsolutePath()}); + Thread[] threads = new Thread[10]; + List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>()); + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(new LoadThread(command, exceptions)); + } + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + if (!exceptions.isEmpty()) { + for (Throwable t : exceptions) { + t.printStackTrace(); + } + throw exceptions.get(0); + } + } + + private static final class LoadThread implements Runnable { + private final Command command; + private final List<Throwable> exception; + + private LoadThread(Command command, List<Throwable> exception) { + this.command = command; + this.exception = exception; + } + + @Override + public void run() { + try { + for (int i = 0; i < 20; i++) { + command.execute(); + } + } catch (Throwable t) { + exception.add(t); + } + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/ShellTest.java b/src/test/java/com/google/devtools/build/lib/shell/ShellTest.java new file mode 100644 index 0000000000..9258fc2309 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/ShellTest.java @@ -0,0 +1,60 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Unit tests for {@link Shell}. + */ +@RunWith(JUnit4.class) +public class ShellTest { + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + @Test + public void testPlatformShell() { + assertNotNull(Shell.getPlatformShell()); + } + + @Test + public void testLinux() { + if (!"Linux".equals(System.getProperty("os.name"))) { + return; + } + final Shell shell = Shell.getPlatformShell(); + final String[] shellified = shell.shellify("echo FOO"); + assertNotNull(shellified); + assertEquals(3, shellified.length); + assertEquals("/bin/sh", shellified[0]); + assertEquals("-c", shellified[1]); + assertEquals("echo FOO", shellified[2]); + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/ShellTests.java b/src/test/java/com/google/devtools/build/lib/shell/ShellTests.java new file mode 100644 index 0000000000..46ea6a0900 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/ShellTests.java @@ -0,0 +1,25 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import com.google.devtools.build.lib.testutil.ClasspathSuite; + +import org.junit.runner.RunWith; + +/** + * Test suite for tests for the shell package. + */ +@RunWith(ClasspathSuite.class) +public class ShellTests { +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java b/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java new file mode 100644 index 0000000000..1034561b35 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java @@ -0,0 +1,202 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static com.google.devtools.build.lib.shell.ShellUtils.prettyPrintArgv; +import static com.google.devtools.build.lib.shell.ShellUtils.shellEscape; +import static com.google.devtools.build.lib.shell.ShellUtils.tokenize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests for ShellUtils. + * + */ +@RunWith(JUnit4.class) +public class ShellUtilsTest { + + @Test + public void testShellEscape() throws Exception { + assertEquals("''", shellEscape("")); + assertEquals("foo", shellEscape("foo")); + assertEquals("'foo bar'", shellEscape("foo bar")); + assertEquals("''\\''foo'\\'''", shellEscape("'foo'")); + assertEquals("'\\'\\''foo\\'\\'''", shellEscape("\\'foo\\'")); + assertEquals("'${filename%.c}.o'", shellEscape("${filename%.c}.o")); + assertEquals("'<html!>'", shellEscape("<html!>")); + } + + @Test + public void testPrettyPrintArgv() throws Exception { + assertEquals("echo '$US' 100", + prettyPrintArgv(Arrays.asList("echo", "$US", "100"))); + } + + private void assertTokenize(String copts, String... expectedTokens) + throws Exception { + List<String> actualTokens = new ArrayList<String>(); + tokenize(actualTokens, copts); + assertEquals(Arrays.asList(expectedTokens), actualTokens); + } + + @Test + public void testTokenize() throws Exception { + assertTokenize("-DASMV", "-DASMV"); + assertTokenize("-DNO_UNDERLINE", "-DNO_UNDERLINE"); + assertTokenize("-DASMV -DNO_UNDERLINE", + "-DASMV", "-DNO_UNDERLINE"); + assertTokenize("-DDES_LONG=\"unsigned int\" -wd310", + "-DDES_LONG=unsigned int", "-wd310"); + assertTokenize("-Wno-write-strings -Wno-pointer-sign " + + "-Wno-unused-variable -Wno-pointer-to-int-cast", + "-Wno-write-strings", + "-Wno-pointer-sign", + "-Wno-unused-variable", + "-Wno-pointer-to-int-cast"); + } + + @Test + public void testTokenizeOnNestedQuotation() throws Exception { + assertTokenize("-Dfoo='foo\"bar' -Dwiz", + "-Dfoo=foo\"bar", + "-Dwiz"); + assertTokenize("-Dfoo=\"foo'bar\" -Dwiz", + "-Dfoo=foo'bar", + "-Dwiz"); + } + + @Test + public void testTokenizeOnBackslashEscapes() throws Exception { + // This would be easier to grok if we forked+exec'd a shell. + + assertTokenize("-Dfoo=\\'foo -Dbar", // \' not quoted -> ' + "-Dfoo='foo", + "-Dbar"); + assertTokenize("-Dfoo=\\\"foo -Dbar", // \" not quoted -> " + "-Dfoo=\"foo", + "-Dbar"); + assertTokenize("-Dfoo=\\\\foo -Dbar", // \\ not quoted -> \ + "-Dfoo=\\foo", + "-Dbar"); + + assertTokenize("-Dfoo='\\'foo -Dbar", // \' single quoted -> \, close quote + "-Dfoo=\\foo", + "-Dbar"); + assertTokenize("-Dfoo='\\\"foo' -Dbar", // \" single quoted -> \" + "-Dfoo=\\\"foo", + "-Dbar"); + assertTokenize("-Dfoo='\\\\foo' -Dbar", // \\ single quoted -> \\ + "-Dfoo=\\\\foo", + "-Dbar"); + + assertTokenize("-Dfoo=\"\\'foo\" -Dbar", // \' double quoted -> \' + "-Dfoo=\\'foo", + "-Dbar"); + assertTokenize("-Dfoo=\"\\\"foo\" -Dbar", // \" double quoted -> " + "-Dfoo=\"foo", + "-Dbar"); + assertTokenize("-Dfoo=\"\\\\foo\" -Dbar", // \\ double quoted -> \ + "-Dfoo=\\foo", + "-Dbar"); + } + + private void assertTokenizeFails(String copts, String expectedError) { + try { + tokenize(new ArrayList<String>(), copts); + fail(); + } catch (ShellUtils.TokenizationException e) { + assertEquals(expectedError, e.getMessage()); + } + } + + @Test + public void testTokenizeEmptyString() throws Exception { + assertTokenize(""); + } + + @Test + public void testTokenizeFailsOnUnterminatedQuotation() { + assertTokenizeFails("-Dfoo=\"bar", "unterminated quotation"); + assertTokenizeFails("-Dfoo='bar", "unterminated quotation"); + assertTokenizeFails("-Dfoo=\"b'ar", "unterminated quotation"); + } + + private void assertTokenizeIsDualToPrettyPrint(String... args) throws Exception { + List<String> in = Arrays.asList(args); + String shellCommand = prettyPrintArgv(in); + + // Assert that pretty-print is correct, i.e. dual to the actual /bin/sh + // tokenization. This test assumes no newlines in the input: + String[] execArgs = { + "/bin/sh", + "-c", + "for i in " + shellCommand + "; do echo \"$i\"; done" // tokenize, one word per line + }; + String stdout = null; + try { + stdout = new String(new Command(execArgs).execute().getStdout()); + } catch (Exception e) { + fail("/bin/sh failed:\n" + in + "\n" + shellCommand + "\n" + e.getMessage()); + } + // We can't use stdout.split("\n") here, + // because String.split() ignores trailing empty strings. + ArrayList<String> words = Lists.newArrayList(); + int index; + while ((index = stdout.indexOf("\n")) >= 0) { + words.add(stdout.substring(0, index)); + stdout = stdout.substring(index + 1); + } + assertEquals(in, words); + + // Assert that tokenize is dual to pretty-print: + List<String> out = new ArrayList<String>(); + try { + tokenize(out, shellCommand); + } finally { + if (out.isEmpty()) { // i.e. an exception + System.err.println(in); + } + } + assertEquals(in, out); + } + + @Test + public void testTokenizeIsDualToPrettyPrint() throws Exception { + // tokenize() is the inverse of prettyPrintArgv(). (However, the reverse + // is not true, since there are many ways to escape the same string, + // e.g. "foo" and 'foo'.) + + assertTokenizeIsDualToPrettyPrint("foo"); + assertTokenizeIsDualToPrettyPrint("foo bar"); + assertTokenizeIsDualToPrettyPrint("foo bar", "wiz"); + assertTokenizeIsDualToPrettyPrint("'foo'"); + assertTokenizeIsDualToPrettyPrint("\\'foo\\'"); + assertTokenizeIsDualToPrettyPrint("${filename%.c}.o"); + assertTokenizeIsDualToPrettyPrint("<html!>"); + + assertTokenizeIsDualToPrettyPrint(""); + assertTokenizeIsDualToPrettyPrint("!@#$%^&*()"); + assertTokenizeIsDualToPrettyPrint("x'y\" z"); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/TestUtil.java b/src/test/java/com/google/devtools/build/lib/shell/TestUtil.java new file mode 100644 index 0000000000..70169ba5cc --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/TestUtil.java @@ -0,0 +1,33 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static com.google.common.truth.Truth.assertThat; + +/** + * Some tiny conveniences for writing tests. + */ +class TestUtil { + + private TestUtil() {} + + public static void assertArrayEquals(byte[] expected, byte[] actual) { + assertThat(actual).isEqualTo(expected); + } + + public static void assertArrayEquals(Object[] expected, Object[] actual) { + assertThat(actual).isEqualTo(expected); + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/ToTruncatedStringTest.java b/src/test/java/com/google/devtools/build/lib/shell/ToTruncatedStringTest.java new file mode 100644 index 0000000000..90a423496a --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/ToTruncatedStringTest.java @@ -0,0 +1,76 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.shell; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Tests {@link LogUtil#toTruncatedString}. + */ +/* + * Note: The toTruncatedString method uses the platform encoding intentionally, + * so the unittest does to. Check out the comment in the implementation in + * case you're wondering why. + */ +@RunWith(JUnit4.class) +public class ToTruncatedStringTest { + + @Before + public void setUp() throws Exception { + + // enable all log statements to ensure there are no problems with + // logging code + Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); + } + + @Test + public void testTruncatingNullYieldsEmptyString() { + assertEquals("", LogUtil.toTruncatedString(null)); + } + + @Test + public void testTruncatingEmptyArrayYieldsEmptyString() { + assertEquals("", LogUtil.toTruncatedString(new byte[0])); + } + + @Test + public void testTruncatingSampleArrayYieldsTruncatedString() { + String sampleInput = "Well, there could be a lot of output, but we want " + + "to produce a useful log. A log is useful if it contains the " + + "interesting information (like what the command was), and maybe " + + "some of the output. However, too much is too much, so we just " + + "cut it after 150 bytes ..."; + String expectedOutput = "Well, there could be a lot of output, but we " + + "want to produce a useful log. A log is useful if it contains " + + "the interesting information (like what the c[... truncated. " + + "original size was 261 bytes.]"; + assertEquals(expectedOutput, + LogUtil.toTruncatedString(sampleInput.getBytes())); + } + + @Test + public void testTruncatingHelloWorldYieldsHelloWorld() { + String helloWorld = "Hello, world."; + assertEquals(helloWorld, LogUtil.toTruncatedString(helloWorld.getBytes())); + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/shell/killmyself.cc b/src/test/java/com/google/devtools/build/lib/shell/killmyself.cc new file mode 100644 index 0000000000..9c04e7a9ed --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/shell/killmyself.cc @@ -0,0 +1,60 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <stdio.h> +#include <signal.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +/* + * This process just kills itself will the signal number + * specified on the command line. + */ + +int main(int argc, char **argv) { + /* + * Parse command-line arguments. + */ + const char *progname = argv[0] ? argv[0] : "killmyself"; + if (argc != 2) { + fprintf(stderr, "%s: Usage: %s <signal-number>\n", + progname, progname); + exit(1); + } + int sig = atoi(argv[1]); + + /* + * Restore the default signal action, in case the + * parent process was ignoring the signal. + * + * This is needed because run_unittests ignores + * SIGHUP signals and this gets inherited by child + * processes. + */ + signal(sig, SIG_DFL); + + /* + * Send ourself the signal. + */ + if (raise(sig) != 0) { + fprintf(stderr, "%s: raise failed: %s", progname, + strerror(errno)); + exit(1); + } + + // We can get here if the signal was a non-fatal signal, + // e.g. SIGCONT. + exit(0); +} |