| // 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(); | |
| } | |
| } | |
| } |