blob: 9a6937c06a9f1e8e5b2a59fefbf9d76cbb983249 [file] [log] [blame]
// Copyright 2020 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.runtime;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.eventbus.Subscribe;
import com.google.common.testing.GcFinalization;
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.bugreport.BugReporter;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.profiler.MemoryProfiler;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.runtime.CommandDispatcher.LockingMode;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.BuildProgress;
import com.google.devtools.build.lib.server.FailureDetails.BuildProgress.Code;
import com.google.devtools.build.lib.server.FailureDetails.Crash;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.Spawn;
import com.google.devtools.build.lib.testutil.MoreAsserts;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.testutil.TestThread;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests {@link BlazeCommandDispatcher}. */
@RunWith(JUnit4.class)
public final class BlazeCommandDispatcherTest {
private final Scratch scratch = new Scratch();
private BlazeRuntime runtime;
private final RecordingOutErr outErr = new RecordingOutErr();
private final FooCommand foo = new FooCommand();
private final BarCommand bar = new BarCommand();
private Map<String, String> clientEnv;
private AbruptExitException errorOnAfterCommand;
@Before
public void initializeRuntime() throws Exception {
initializeRuntimeInternal();
}
@After
public void cleanUp() {
BugReport.maybePropagateLastCrashIfInTest();
}
private void initializeRuntimeInternal(BlazeModule... additionalModules) throws Exception {
String productName = TestConstants.PRODUCT_NAME;
ServerDirectories serverDirectories =
new ServerDirectories(
scratch.dir("install_base"), scratch.dir("output_base"), scratch.dir("user_root"));
// no ConfiguredTargetFactory is needed for testing command dispatch
BlazeRuntime.Builder builder =
new BlazeRuntime.Builder()
.setFileSystem(scratch.getFileSystem())
.setServerDirectories(serverDirectories)
.setProductName(productName)
.setStartupOptionsProvider(
OptionsParser.builder().optionsClasses(BlazeServerStartupOptions.class).build())
.addBlazeModule(
new BlazeModule() {
@Override
public void beforeCommand(CommandEnvironment env) {
clientEnv = env.getClientEnv();
}
@Override
public void afterCommand() throws AbruptExitException {
if (errorOnAfterCommand != null) {
throw errorOnAfterCommand;
}
}
});
for (BlazeModule module : additionalModules) {
builder.addBlazeModule(module);
}
this.runtime = builder.build();
BlazeDirectories directories =
new BlazeDirectories(
serverDirectories,
scratch.dir("scratch"),
/* defaultSystemJavabase= */ null,
productName);
runtime.initWorkspace(directories, /*binTools=*/null);
errorOnAfterCommand = null;
}
@After
public void stopProfilers() throws Exception {
// Needs to be done because we are simulating crashes but keeping the jvm alive.
Profiler.instance().stop();
MemoryProfiler.instance().stop();
}
/** Options for {@link FooCommand}. */
public static class FooOptions extends OptionsBase {
@Option(
name = "success",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "true"
)
public boolean exitStatus;
@Option(
name = "stdout",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = ""
)
public String stdout;
@Option(
name = "stderr",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = ""
)
public String stderr;
}
@Command(name = "foo", options = {FooOptions.class},
shortDescription = "", help = "")
private static class FooCommand implements BlazeCommand {
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
FooOptions fooOptions = options.getOptions(FooOptions.class);
env.getReporter().getOutErr().printOut(fooOptions.stdout);
env.getReporter().getOutErr().printErr(fooOptions.stderr);
if (fooOptions.exitStatus) {
return BlazeCommandResult.success();
} else {
return BlazeCommandResult.failureDetail(
FailureDetail.newBuilder()
.setSpawn(Spawn.newBuilder().setCode(Spawn.Code.NON_ZERO_EXIT))
.build());
}
}
}
@Command(name = "bar", shortDescription = "", help = "")
private static class BarCommand implements BlazeCommand {
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
env.getReporter().getOutErr().printOut("Hello, bar.\n");
return BlazeCommandResult.success();
}
}
private abstract static class AnsiTestingCommand implements BlazeCommand {
public static final String ANSI_CODE = "\u001B[34mFoo";
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
OutErr outErr = env.getReporter().getOutErr();
try {
env.getReporter().switchToAnsiAllowingHandler();
byte[] ansiBytes = ANSI_CODE.getBytes(US_ASCII);
env.getReporter().handle(Event.of(EventKind.STDOUT, null, ansiBytes));
outErr.getOutputStream().flush();
outErr.getErrorStream().flush();
} catch (IOException e) {
return BlazeCommandResult.failureDetail(
FailureDetail.newBuilder().setCrash(Crash.getDefaultInstance()).build());
}
return BlazeCommandResult.success();
}
}
@Command(name = "binary", binaryStdOut = true, shortDescription = "", help = "")
private static class BinaryCommand extends AnsiTestingCommand {
// Same logic as AsciiCommand, but binary.
}
@Command(name = "ascii", shortDescription = "", help = "")
private static class AsciiCommand extends AnsiTestingCommand {
// Same logic as BinaryCommand, but not binary.
}
@Test
public void testOutErrorAndExitStatus() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
String[] args = {"foo", "--stdout=Hello, out.",
"--stderr=Hello, err.", "--success=false"};
BlazeCommandResult result = dispatch.exec(Arrays.asList(args), "test", outErr);
assertThat(outErr.outAsLatin1()).isEqualTo("Hello, out.");
assertThat(outErr.errAsLatin1()).isEqualTo("Hello, err.");
assertThat(result.getExitCode()).isEqualTo(ExitCode.BUILD_FAILURE);
}
@Test
public void testExecReportsHardCrashStatus() throws Exception {
CommandCompleteRecordingCommand crashCommand =
new CommandCompleteRecordingCommand(
() -> {
throw new OutOfMemoryError("oom message");
});
runtime.overrideCommands(ImmutableList.of(crashCommand));
BlazeCommandDispatcher dispatch =
new BlazeCommandDispatcher(runtime, BugReporter.defaultInstance());
BlazeCommandResult directResult =
dispatch.exec(ImmutableList.of("testcommand"), "clientdesc", outErr);
// Crashes from main thread don't interrupt main thread.
assertThat(Thread.currentThread().isInterrupted()).isFalse();
CommandCompleteEvent commandCompleteEvent =
crashCommand.commandCompleteEvent.get(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS);
DetailedExitCode exitCode = commandCompleteEvent.getExitCode();
assertThat(exitCode.getExitCode()).isEqualTo(ExitCode.OOM_ERROR);
assertThat(exitCode.getFailureDetail()).isNotNull();
FailureDetails.Crash crash = exitCode.getFailureDetail().getCrash();
assertThat(crash.getCode()).isEqualTo(FailureDetails.Crash.Code.CRASH_OOM);
assertThat(crash.getCausesCount()).isEqualTo(1);
assertThat(crash.getCauses(0).getMessage()).isEqualTo("oom message");
assertThat(crash.getCauses(0).getStackTrace(0)).contains("BlazeCommandDispatcherTest.java");
assertThat(directResult.getExitCode()).isEqualTo(ExitCode.OOM_ERROR);
assertThat(directResult.shutdown()).isTrue();
assertThat(BugReport.getAndResetLastCrashingThrowableIfInTest())
.isInstanceOf(OutOfMemoryError.class);
}
@Test
public void crashPreventsNewCommand() throws Exception {
CountDownLatch commandStarted = new CountDownLatch(1);
BlazeCommand hangingCommand =
new CommandCompleteRecordingCommand(
() -> {
commandStarted.countDown();
try {
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
} catch (InterruptedException e) {
return BlazeCommandResult.detailedExitCode(
DetailedExitCode.of(
FailureDetail.newBuilder()
.setInterrupted(
FailureDetails.Interrupted.newBuilder()
.setCode(FailureDetails.Interrupted.Code.INTERRUPTED))
.build()));
}
throw new IllegalStateException("Should have been interrupted");
});
CountDownLatch crashStarted = new CountDownLatch(1);
CountDownLatch waitToFinishOnCrash = new CountDownLatch(1);
initializeRuntimeInternal(
new BlazeModule() {
@Override
public void blazeShutdownOnCrash(DetailedExitCode exitCode) {
crashStarted.countDown();
Uninterruptibles.awaitUninterruptibly(waitToFinishOnCrash);
}
});
runtime.overrideCommands(ImmutableList.of(hangingCommand));
BlazeCommandDispatcher dispatch =
new BlazeCommandDispatcher(runtime, BugReporter.defaultInstance());
AtomicReference<BlazeCommandResult> directResult = new AtomicReference<>();
TestThread commandThread =
new TestThread(
() ->
directResult.set(
dispatch.exec(ImmutableList.of("testcommand"), "clientdesc", outErr)));
commandThread.start();
assertThat(commandStarted.await(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue();
DetailedExitCode crashExitCode =
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage("crash oom message")
.setCrash(Crash.newBuilder().setCode(Crash.Code.CRASH_OOM))
.build());
TestThread crashThread = new TestThread(() -> runtime.cleanUpForCrash(crashExitCode));
crashThread.start();
assertThat(crashStarted.await(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue();
commandThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
assertThat(directResult.get().getDetailedExitCode()).isSameInstanceAs(crashExitCode);
RecordingOutErr recordingOutErr = new RecordingOutErr();
String errorMessage = TestConstants.PRODUCT_NAME + " is crashing: crash oom message";
assertThat(
dispatch
.exec(ImmutableList.of("testcommand"), "clientdesc", recordingOutErr)
.getDetailedExitCode()
.getFailureDetail())
.isEqualTo(
FailureDetails.FailureDetail.newBuilder()
.setCommand(
FailureDetails.Command.newBuilder()
.setCode(FailureDetails.Command.Code.PREVIOUSLY_SHUTDOWN))
.setMessage(errorMessage)
.build());
assertThat(recordingOutErr.errAsLatin1()).contains(errorMessage);
assertThat(crashThread.isAlive()).isTrue();
waitToFinishOnCrash.countDown();
crashThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
}
@Test
public void testExecReportsStatus() throws Exception {
FailureDetail failureDetail =
FailureDetail.newBuilder()
.setSpawn(Spawn.newBuilder().setCode(Spawn.Code.NON_ZERO_EXIT))
.build();
CommandCompleteRecordingCommand crashCommand =
new CommandCompleteRecordingCommand(() -> BlazeCommandResult.failureDetail(failureDetail));
runtime.overrideCommands(ImmutableList.of(crashCommand));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult directResult =
dispatch.exec(ImmutableList.of("testcommand"), "clientdesc", outErr);
CommandCompleteEvent commandCompleteEvent =
crashCommand.commandCompleteEvent.get(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS);
assertThat(commandCompleteEvent.getExitCode()).isEqualTo(DetailedExitCode.of(failureDetail));
assertThat(directResult.shutdown()).isFalse();
}
@Test
public void testClientEnv() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
String[] args = {"foo", "--client_env=V1=val1", "--client_env=V2=", "--client_env=V3=val3"};
dispatch.exec(Arrays.asList(args), "test", outErr);
assertThat(clientEnv).containsExactly("V1", "val1", "V2", "", "V3", "val3");
}
@Test
public void testClientEnvEmpty() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
String[] args = {"foo"};
dispatch.exec(Arrays.asList(args), "test", outErr);
assertThat(clientEnv).isEmpty();
}
@Test
public void testAfterCommandCanModifyExitStatus() throws Exception {
DetailedExitCode detailedExitCode =
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage("afterCommandError")
.setBuildProgress(
BuildProgress.newBuilder().setCode(Code.BES_UPLOAD_LOCAL_FILE_ERROR))
.build());
errorOnAfterCommand = new AbruptExitException(detailedExitCode);
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult result =
dispatch.exec(Arrays.asList("foo", "--success=true"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.TRANSIENT_BUILD_EVENT_SERVICE_UPLOAD_ERROR);
assertThat(result.getDetailedExitCode()).isEqualTo(detailedExitCode);
assertThat(outErr.errAsLatin1()).contains("afterCommandError");
}
@Test
public void testMultipleCommands() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo, bar));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
dispatch.exec(asList("foo", "--stdout=Hello, foo."), "test", outErr);
assertThat(outErr.outAsLatin1()).isEqualTo("Hello, foo.");
outErr.reset();
dispatch.exec(asList("bar"), "test", outErr);
assertThat(outErr.outAsLatin1()).isEqualTo("Hello, bar.\n");
}
@Command(name = "block", help = "", shortDescription = "")
private static class BlockCommand implements BlazeCommand {
private final CountDownLatch waitLatch = new CountDownLatch(1);
private final CountDownLatch started = new CountDownLatch(1);
void unblock() {
waitLatch.countDown();
}
void awaitRunning() throws InterruptedException {
started.await();
}
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
started.countDown();
try {
waitLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Should not have been interrupted");
}
return BlazeCommandResult.success();
}
}
@Test
public void testConcurrentCommandsWaitForLock() throws Exception {
BlockCommand blockCommand = new BlockCommand();
runtime.overrideCommands(ImmutableList.of(bar, blockCommand));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime, /*serverPid=*/ 42);
Thread blockCommandThread =
new TestThread(
() ->
dispatch.exec(ImmutableList.of("block"), "blocking client", new RecordingOutErr()));
TestThread blockedCommandThread =
new TestThread(
() ->
dispatch.exec(
InvocationPolicy.getDefaultInstance(),
ImmutableList.of("bar"),
outErr,
LockingMode.WAIT,
"test client",
runtime.getClock().currentTimeMillis(),
/*startupOptionsTaggedWithBazelRc=*/ Optional.empty(),
/*commandExtensions=*/ ImmutableList.of()));
try {
blockCommandThread.start();
blockCommand.awaitRunning();
blockedCommandThread.start();
while (!outErr.errAsLatin1().contains("Another command")) {
Thread.sleep(100);
}
assertThat(outErr.errAsLatin1())
.contains(
"Another command (blocking client) is running. Waiting for it to complete on the"
+ " server (server_pid=42)...");
} finally {
blockCommand.unblock();
// We don't care what happened on the threads, don't assert state to make sure we join both.
blockCommandThread.join();
blockedCommandThread.join();
}
}
@Test
public void testDetectsInvalidCommandLineOptions() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult result = dispatch.exec(asList("foo", "--invalid"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.COMMAND_LINE_ERROR);
assertThat(outErr.errAsLatin1()).contains("Unrecognized option: --invalid\n");
}
@Test
public void testReportsCommandNotFound() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult result = dispatch.exec(asList("baz"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.COMMAND_LINE_ERROR);
assertThat(outErr.errAsLatin1())
.matches("Command 'baz' not found. Try '(blaze|bazel) help'.\n");
}
@Test
public void testProvidesHelpWhenNoCommandSpecified() throws Exception {
@Command(name = "help", shortDescription = "", help = "")
class HelpCommand implements BlazeCommand {
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
env.getReporter().getOutErr().printOutLn("This is the help message.");
return BlazeCommandResult.success();
}
}
runtime.overrideCommands(ImmutableList.of(new HelpCommand()));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult result = dispatch.exec(ImmutableList.of(), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.SUCCESS);
assertThat(outErr.outAsLatin1()).isEqualTo("This is the help message.\n");
}
@Test
public void testOptionsDefaults() throws Exception {
List<String> blazercOpts =
ImmutableList.of(
"--rc_source=/home/jrluser/.blazerc",
"--default_override=0:foo=--stdout",
"--default_override=0:foo=stdout",
"--default_override=0:foo=--stderr",
"--default_override=0:foo=stderr",
"--announce_rc");
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
List<String> cmdLine = Lists.newArrayList("foo");
cmdLine.addAll(blazercOpts);
BlazeCommandResult result = dispatch.exec(cmdLine, "test", outErr);
assertThat(outErr.outAsLatin1()).isEqualTo("stdout");
// TODO(bazel-team): Fix inconsistent line breaks that make the regex match necessary.
assertThat(outErr.errAsLatin1())
.matches(
"INFO: Reading rc options for 'foo' from /home/jrluser/.blazerc:\\s+"
+ " 'foo' options: --stdout stdout --stderr stderr\\s+"
+ "stderr");
assertThat(result.getExitCode()).isEqualTo(ExitCode.SUCCESS);
// Explicit options override those from config file:
result = dispatch.exec(asList("foo", "--success=false"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.BUILD_FAILURE);
}
@Test
public void testIllegalOptions() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
BlazeCommandResult result = dispatch.exec(
asList("foo", "--not_a_valid_option"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.COMMAND_LINE_ERROR);
}
@Command(name = "wiz", inherits = {FooCommand.class}, shortDescription = "", help = "")
private static class WizCommand extends FooCommand {}
@Test
public void testInheritanceOfOptionDefaults() throws Exception {
// "foo" options in ~/.blazerc should apply to "wiz" too...
List<String> blazercOpts =
ImmutableList.of(
"--rc_source=/home/jrluser/.blazerc",
"--default_override=0:foo=--stdout",
"--default_override=0:foo=stdout",
"--default_override=0:foo=--stderr",
"--default_override=0:foo=stderr",
"--announce_rc");
runtime.overrideCommands(ImmutableList.of(foo, new WizCommand()));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
List<String> cmdLine = Lists.newArrayList("wiz");
cmdLine.addAll(blazercOpts);
dispatch.exec(cmdLine, "test", outErr);
assertThat(outErr.outAsLatin1()).isEqualTo("stdout");
// TODO(bazel-team): Fix inconsistent line breaks that make the regex match necessary.
assertThat(outErr.errAsLatin1())
.matches(
"INFO: Reading rc options for 'wiz' from /home/jrluser/.blazerc:\\s+"
+ " Inherited 'foo' options: --stdout stdout --stderr stderr\\s+"
+ "stderr");
}
@Test
public void testBinaryCommandOutput() throws Exception {
runtime.overrideCommands(ImmutableList.of(new BinaryCommand()));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
final String ansiEscapedString = AnsiTestingCommand.ANSI_CODE;
// Binary commands do not remove ANSI control codes.
BlazeCommandResult result = dispatch.exec(
asList("binary", "--color=no"), "test", outErr);
String out = outErr.outAsLatin1();
String err = outErr.errAsLatin1();
MoreAsserts.assertExitCode(ExitCode.SUCCESS.getNumericExitCode(),
result.getExitCode().getNumericExitCode(), out, err);
assertThat(out).contains(ansiEscapedString);
}
@Test
public void testAsciiCommandOutput() throws Exception {
runtime.overrideCommands(ImmutableList.of(new AsciiCommand()));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
final String ansiEscapedString = AnsiTestingCommand.ANSI_CODE;
// ASCII commands remove ANSI control codes.
BlazeCommandResult result = dispatch.exec(asList("ascii", "--color=no"),
"test", outErr);
String out = outErr.outAsLatin1();
String err = outErr.errAsLatin1();
MoreAsserts.assertExitCode(ExitCode.SUCCESS.getNumericExitCode(),
result.getExitCode().getNumericExitCode(), out, err);
assertThat(out).doesNotContain(ansiEscapedString);
}
@Test
public void testWaitingForTimestampGranularityMonitor() throws Exception {
runtime.overrideCommands(ImmutableList.of(foo));
BlazeCommandDispatcher dispatch = new BlazeCommandDispatcher(runtime);
for (int i = 0; i < 3; i++) {
BlazeCommandResult result = dispatch.exec(Arrays.asList("foo"), "test", outErr);
assertThat(result.getExitCode()).isEqualTo(ExitCode.SUCCESS);
}
assertThat(outErr.outAsLatin1()).isEmpty();
for (String line : outErr.errAsLatin1().split("\n")) {
assertThat(line)
.containsMatch(
"^|Blaze waited .* to avoid potential file system timestamp granularity issues");
}
}
/**
* Regression test for b/136003907.
*
* <p>Tests that even if {@link System#out} or {@link System#err} are read and retained during the
* lifetime of a command (which we cannot prevent, since they are public), there is no memory leak
* of {@link CommandEnvironment#getReporter}.
*/
@Test
public void noMemoryLeakOfReporterThroughSystemOutErr() throws Exception {
@Command(name = "retain_out_err", shortDescription = "", help = "")
final class SystemOutErrRetainingCommand implements BlazeCommand {
private final PrintStream defaultStdout = System.out;
private final PrintStream defaultStderr = System.err;
private WeakReference<Reporter> reporterRef;
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
PrintStream overriddenStdout = System.out;
assertThat(overriddenStdout).isNotNull();
assertThat(overriddenStdout).isNotEqualTo(defaultStdout);
PrintStream overriddenStderr = System.err;
assertThat(overriddenStderr).isNotNull();
assertThat(overriddenStderr).isNotEqualTo(defaultStderr);
Reporter reporter = env.getReporter();
assertThat(reporter).isNotNull();
reporterRef = new WeakReference<>(env.getReporter());
return BlazeCommandResult.success();
}
}
SystemOutErrRetainingCommand cmd = new SystemOutErrRetainingCommand();
runtime.overrideCommands(ImmutableList.of(cmd));
BlazeCommandDispatcher dispatcher = new BlazeCommandDispatcher(runtime);
dispatcher.exec(ImmutableList.of("retain_out_err"), "test", outErr);
GcFinalization.awaitClear(cmd.reporterRef);
}
@Command(
name = "testcommand",
shortDescription = "",
help = "")
private static class CommandCompleteRecordingCommand implements BlazeCommand {
private final SettableFuture<CommandCompleteEvent> commandCompleteEvent =
SettableFuture.create();
private final Supplier<BlazeCommandResult> resultSupplier;
private CommandCompleteRecordingCommand(Supplier<BlazeCommandResult> resultSupplier) {
this.resultSupplier = resultSupplier;
}
@Subscribe
public void onCommandComplete(CommandCompleteEvent commandComplete) {
commandCompleteEvent.set(commandComplete);
}
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
env.getEventBus().register(this);
return resultSupplier.get();
}
}
}