aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test
diff options
context:
space:
mode:
authorGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-12 13:27:06 +0000
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-12 13:27:06 +0000
commitb0e387b18aa1d505deb85c3d27fb99fbf344fd2a (patch)
tree3faad9c437c34cc78a0f547df2751febd4a9dbc8 /src/test
parent7e73a28c3da7011a2b808f4561918bd676bc66c4 (diff)
Add shell tests to bazel.
-- MOS_MIGRATED_REVID=86171408
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/BUILD31
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/CommandLargeInputsTest.java155
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/CommandTest.java692
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/ConsumersTest.java172
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/FutureConsumptionTest.java106
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/InterruptibleTest.java138
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/LoadTest.java111
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/ShellTest.java60
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/ShellTests.java25
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java202
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/TestUtil.java33
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/ToTruncatedStringTest.java76
-rw-r--r--src/test/java/com/google/devtools/build/lib/shell/killmyself.cc60
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);
+}