blob: 34287808433fe6d9f7f90fe570860ca951940bff [file] [log] [blame]
// Copyright 2015 The Bazel Authors. 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;
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 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()) {
assertThat(command.getEnvironmentVariables()).containsEntry(key, env.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());
assertEquals(0, result.getStderr().length);
assertThat(result.getStdout().length).isGreaterThan(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[] {"/bin/sh", "-c", "echo $FOO"}, 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());
assertEquals(0, result.getStderr().length);
assertThat(result.getStdout().length).isGreaterThan(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());
assertThat(out.toString("UTF-8")).isEmpty();
assertThat(err.toString("UTF-8")).isEmpty();
}
@Test
public void testNoInputForCat() throws Exception {
final Command command = new Command(new String[]{"/bin/cat"});
CommandResult result = command.execute();
assertTrue(result.getTerminationStatus().success());
assertThat(new String(result.getStdout(), "UTF-8")).isEmpty();
assertThat(new String(result.getStderr(), "UTF-8")).isEmpty();
}
@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, "sleep", "5");
}
}
}.start();
// We're racing against the actual startup of the other command. Wait for 10ms so it can start.
Thread.sleep(10);
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) {
assertThat(e).hasMessage("Process exited with status " + exit);
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/bash", "-c", "exit " + exit };
new Command(args).execute();
fail("Should have exited with status " + expected);
} catch (BadExitStatusException e) {
assertThat(e).hasMessage("Process exited with status " + expected);
checkCommandElements(e, "/bin/bash", "-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) {
assertThat(e).hasMessage("Process terminated by signal " + signal);
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 {
// -c == --bytes, but -c also works on Darwin.
Command command = new Command(new String[] {"head", "-c", "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(
// On darwin, /bin/sh does not support -n for the echo builtin.
new String[] {"/bin/bash", "-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());
assertEquals(0, result.getStderr().length);
assertEquals(0, result.getStdout().length);
}
@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");
assertThat(e).hasMessage("java.io.IOException");
}
}
@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;
}
/**
* 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());
assertEquals(0, result.getStderr().length);
assertEquals(expectedOutput, new String(result.getStdout()));
}
}