Add shell tests to bazel.

--
MOS_MIGRATED_REVID=86171408
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 0000000..ce0e8e4
--- /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()));
+  }
+}