blob: 0543e07f8feb4f0471c0fb09cd5268e6369e55b0 [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.common.truth.Truth.assertWithMessage;
import static com.google.devtools.build.lib.shell.TestUtil.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.testutil.BlazeTestUtils;
import com.google.devtools.build.lib.testutil.TestConstants;
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.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* 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 {
// Platform-independent tests ----------------------------------------------
@Before
public final void configureLogger() 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 {
assertThrows(NullPointerException.class, () -> new Command(null));
assertThrows(
NullPointerException.class, () -> new Command(new String[] {"/bin/true", null}).execute());
Command r = new Command(new String[] {"foo"});
assertThrows(
NullPointerException.class,
() -> r.executeAsync((InputStream) null, Command.KILL_SUBPROCESS_ON_INTERRUPT).get());
}
@Test
public void testGetters() {
File workingDir = new File(".");
Map<String, String> env = Collections.singletonMap("foo", "bar");
String[] commandArgs = new String[] { "command" };
Command command = new Command(commandArgs, env, workingDir);
assertArrayEquals(commandArgs, command.getCommandLineElements());
for (String key : env.keySet()) {
assertThat(command.getEnvironmentVariables()).containsEntry(key, env.get(key));
}
assertThat(command.getWorkingDirectory()).isEqualTo(workingDir);
}
// Platform-dependent tests ------------------------------------------------
@Test
public void testSimpleCommand() throws Exception {
Command command = new Command(new String[] {"ls"});
CommandResult result = command.execute();
assertThat(result.getTerminationStatus().success()).isTrue();
assertThat(result.getStderr()).isEmpty();
assertThat(result.getStdout().length).isGreaterThan(0);
}
@Test
public void testArguments() throws Exception {
Command command = new Command(new String[] {"echo", "foo"});
checkSuccess(command.execute(), "foo\n");
}
@Test
public void testEnvironment() throws Exception {
Map<String, String> env = Collections.singletonMap("FOO", "BAR");
Command command = new Command(new String[] {"/bin/sh", "-c", "echo $FOO"}, env, null);
checkSuccess(command.execute(), "BAR\n");
}
@Test
public void testWorkingDir() throws Exception {
Command command = new Command(new String[] {"pwd"}, null, new File("/"));
checkSuccess(command.execute(), "/\n");
}
@Test
public void testStdin() throws Exception {
Command command = new Command(new String[] {"grep", "bar"});
InputStream in = new ByteArrayInputStream("foobarbaz".getBytes());
checkSuccess(
command.executeAsync(in, Command.KILL_SUBPROCESS_ON_INTERRUPT).get(),
"foobarbaz\n");
}
@Test
public void testRawCommand() throws Exception {
Command command = new Command(new String[] { "perl", "-e", "print 'a'x100000" });
CommandResult result = command.execute();
assertThat(result.getTerminationStatus().success()).isTrue();
assertThat(result.getStderr()).isEmpty();
assertThat(result.getStdout().length).isGreaterThan(0);
}
@Test
public void testRawCommandWithDir() throws Exception {
Command command = new Command(new String[] { "pwd" }, null, new File("/"));
CommandResult result = command.execute();
checkSuccess(result, "/\n");
}
@Test
public void testHugeOutput() throws Exception {
Command command = new Command(new String[] {"perl", "-e", "print 'a'x100000"});
CommandResult result = command.execute();
assertThat(result.getTerminationStatus().success()).isTrue();
assertThat(result.getStderr()).isEmpty();
assertThat(result.getStdout()).hasLength(100000);
}
@Test
public void testNoStreamingInputForCat() throws Exception {
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
.executeAsync(emptyInput, out, err, Command.KILL_SUBPROCESS_ON_INTERRUPT)
.get();
assertThat(result.getTerminationStatus().success()).isTrue();
assertThat(out.toString("UTF-8")).isEmpty();
assertThat(err.toString("UTF-8")).isEmpty();
}
@Test
public void testNoInputForCat() throws Exception {
Command command = new Command(new String[]{"/bin/cat"});
CommandResult result = command.execute();
assertThat(result.getTerminationStatus().success()).isTrue();
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.";
Command command = new Command(new String[]{"/bin/echo", helloWorld});
ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
ByteArrayOutputStream stdErr = new ByteArrayOutputStream();
command.execute(stdOut, stdErr);
assertThat(stdOut.toString("UTF-8")).isEqualTo(helloWorld + "\n");
assertThat(stdErr.toByteArray()).isEmpty();
}
@Test
public void testAsynchronous() throws Exception {
File tempFile = File.createTempFile("googlecron-test", "tmp");
tempFile.delete();
Command command = new Command(new String[] {"touch", tempFile.getAbsolutePath()});
FutureCommandResult result = command.executeAsync();
result.get();
assertThat(tempFile.exists()).isTrue();
assertThat(result.isDone()).isTrue();
tempFile.delete();
}
@Test
public void testAsynchronousWithOutputStreams() throws Exception {
String helloWorld = "Hello, world.";
Command command = new Command(new String[]{"/bin/echo", helloWorld});
ByteArrayInputStream emptyInput = new ByteArrayInputStream(new byte[0]);
ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
ByteArrayOutputStream stdErr = new ByteArrayOutputStream();
FutureCommandResult result =
command.executeAsync(emptyInput, stdOut, stdErr, /* killSubprocessOnInterrupt= */ false);
result.get(); // Make sure the process actually finished
assertThat(stdOut.toString("UTF-8")).isEqualTo(helloWorld + "\n");
assertThat(stdErr.toByteArray()).isEmpty();
}
@Test
public void testTimeout() throws Exception {
// Sleep for 3 seconds, but timeout after 1 second.
Command command = new Command(new String[] {"sleep", "3"}, null, null, Duration.ofSeconds(1));
AbnormalTerminationException ate =
assertThrows(AbnormalTerminationException.class, () -> command.execute());
checkCommandElements(ate, "sleep", "3");
checkATE(ate);
}
@Test
public void testTimeoutDoesntFire() throws Exception {
Command command = new Command(new String[] {"cat"}, null, null, Duration.ofSeconds(2));
InputStream in = new ByteArrayInputStream(new byte[]{'H', 'i', '!'});
command.executeAsync(in, Command.KILL_SUBPROCESS_ON_INTERRUPT).get();
}
@Test
public void testCommandDoesNotExist() throws Exception {
Command command = new Command(new String[]{"thisisnotreal"});
ExecFailedException e = assertThrows(ExecFailedException.class, () -> command.execute());
checkCommandElements(e, "thisisnotreal");
}
@Test
public void testNoSuchCommand() throws Exception {
final Command command = new Command(new String[] {"thisisnotreal"});
assertThrows(ExecFailedException.class, () -> command.execute());
}
@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();
assertThat(status.success()).isTrue();
assertThat(status.exited()).isTrue();
assertThat(status.getExitCode()).isEqualTo(0);
}
// 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 }) {
String[] args = {"/bin/sh", "-c", "exit " + exit};
BadExitStatusException e =
assertThrows(
"Should have exited with status " + exit,
BadExitStatusException.class,
() -> new Command(args).execute());
assertThat(e).hasMessageThat().isEqualTo("Process exited with status " + exit);
checkCommandElements(e, "/bin/sh", "-c", "exit " + exit);
TerminationStatus status = e.getResult().getTerminationStatus();
assertThat(status.success()).isFalse();
assertThat(status.exited()).isTrue();
assertThat(status.getExitCode()).isEqualTo(exit);
assertThat(status.toShortString()).isEqualTo("Exit " + exit);
}
// negative exit values are modulo 256:
for (int exit : new int[] { -1, -2, -3 }) {
int expected = 256 + exit;
String[] args = {"/bin/bash", "-c", "exit " + exit};
BadExitStatusException e =
assertThrows(
"Should have exited with status " + expected,
BadExitStatusException.class,
() -> new Command(args).execute());
assertThat(e).hasMessageThat().isEqualTo("Process exited with status " + expected);
checkCommandElements(e, "/bin/bash", "-c", "exit " + exit);
TerminationStatus status = e.getResult().getTerminationStatus();
assertThat(status.success()).isFalse();
assertThat(status.exited()).isTrue();
assertThat(status.getExitCode()).isEqualTo(expected);
assertThat(status.toShortString()).isEqualTo("Exit " + expected);
}
}
@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";
String[] args = {killmyself, "" + signal};
AbnormalTerminationException e =
assertThrows(
"Expected signal " + signal,
AbnormalTerminationException.class,
() -> new Command(args).execute());
assertThat(e).hasMessageThat().isEqualTo("Process terminated by signal " + signal);
checkCommandElements(e, killmyself, "" + signal);
TerminationStatus status = e.getResult().getTerminationStatus();
assertThat(status.success()).isFalse();
assertThat(status.exited()).isFalse();
assertThat(status.getTerminatingSignal()).isEqualTo(signal);
switch (signal) {
case 1: assertThat(status.toShortString()).isEqualTo("Hangup"); break;
case 2: assertThat(status.toShortString()).isEqualTo("Interrupt"); break;
case 9: assertThat(status.toShortString()).isEqualTo("Killed"); break;
case 15: assertThat(status.toShortString()).isEqualTo("Terminated"); break;
default: // fall out
}
}
}
@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.executeAsync(in, out, out, Command.KILL_SUBPROCESS_ON_INTERRUPT).get();
TerminationStatus status = result.getTerminationStatus();
assertThat(status.success()).isTrue();
}
@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".
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(out, System.err);
assertThat(flushed[0]).isFalse();
assertThat(flushed[1]).isFalse(); // 'F'
assertThat(flushed[2]).isFalse(); // 'o'
assertThat(flushed[3]).isTrue(); // 'o' <- expect flush here.
assertThat(flushed[4]).isFalse(); // 'B'
assertThat(flushed[5]).isFalse(); // 'a'
assertThat(flushed[6]).isFalse(); // 'r'
assertThat(flushed[7]).isTrue(); // '\n'
}
@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"});
AbnormalTerminationException e =
assertThrows(AbnormalTerminationException.class, () -> command.execute(out, out));
checkCommandElements(e, "/bin/echo", "foo");
assertThat(e).hasMessageThat().isEqualTo("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"});
AbnormalTerminationException e =
assertThrows(AbnormalTerminationException.class, () -> command.execute(out, out));
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.
assertThat(status.exited() ? status.getExitCode() == 1 : status.getTerminatingSignal() == 13)
.isTrue();
assertWithMessage(e.getMessage())
.that(
e.getMessage()
.endsWith("also encountered an error while attempting " + "to retrieve output"))
.isTrue();
}
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();
assertThat(result.getTerminationStatus().success()).isFalse();
}
private static void checkSuccess(final CommandResult result,
final String expectedOutput) {
assertThat(result.getTerminationStatus().success()).isTrue();
assertThat(result.getStderr()).isEmpty();
assertThat(new String(result.getStdout())).isEqualTo(expectedOutput);
}
@Test
public void testRelativePath() throws Exception {
Command command = new Command(new String[]{"relative/path/to/binary"},
ImmutableMap.<String, String>of(),
new File("/working/directory"));
assertThat(command.getCommandLineElements()[0])
.isEqualTo("/working/directory/relative/path/to/binary");
}
}