| // 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 static org.junit.Assert.assertThrows; |
| |
| 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.maybePropagateUnprocessedThrowableIfInTest(); |
| } |
| |
| 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(); |
| assertThrows(OutOfMemoryError.class, BugReport::maybePropagateUnprocessedThrowableIfInTest); |
| } |
| |
| @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(); |
| } |
| } |
| } |