| // Copyright 2021 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.buildeventservice; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; | |
| import static com.google.devtools.build.lib.buildeventservice.BuildEventServiceModule.RUNS_PER_TEST_LIMIT; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.common.util.concurrent.Uninterruptibles; | |
| import com.google.devtools.build.lib.actions.ActionLookupData; | |
| import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions; | |
| import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; | |
| import com.google.devtools.build.lib.bugreport.BugReport; | |
| import com.google.devtools.build.lib.bugreport.Crash; | |
| import com.google.devtools.build.lib.bugreport.CrashContext; | |
| import com.google.devtools.build.lib.buildeventservice.BazelBuildEventServiceModule.BackendConfig; | |
| import com.google.devtools.build.lib.buildeventservice.BuildEventServiceModule.BuildEventFileType; | |
| import com.google.devtools.build.lib.buildeventservice.BuildEventServiceModule.BuildEventOutputStreamFactory; | |
| import com.google.devtools.build.lib.buildeventstream.AnnounceBuildEventTransportsEvent; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventServiceUploadCompleteEvent; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted.AbortReason; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.BuildFinishedId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.ConfigurationId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TargetCompletedId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventTransport; | |
| import com.google.devtools.build.lib.buildeventstream.transports.BinaryFormatFileTransport; | |
| import com.google.devtools.build.lib.buildeventstream.transports.JsonFormatFileTransport; | |
| import com.google.devtools.build.lib.buildeventstream.transports.TextFormatFileTransport; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.network.ConnectivityStatus; | |
| import com.google.devtools.build.lib.network.ConnectivityStatusProvider; | |
| import com.google.devtools.build.lib.network.NoOpConnectivityModule; | |
| import com.google.devtools.build.lib.remote.util.TracingMetadataUtils; | |
| import com.google.devtools.build.lib.runtime.BlazeModule; | |
| import com.google.devtools.build.lib.runtime.BlazeRuntime; | |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; | |
| import com.google.devtools.build.lib.runtime.NoSpawnCacheModule; | |
| import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.CommandLineSection; | |
| import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.Option; | |
| import com.google.devtools.build.lib.testutil.TestConstants; | |
| import com.google.devtools.build.lib.util.AbruptExitException; | |
| import com.google.devtools.build.lib.util.ExitCode; | |
| import com.google.devtools.build.skyframe.NotifyingHelper; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import io.grpc.ManagedChannel; | |
| import io.grpc.Metadata; | |
| import io.grpc.Server; | |
| import io.grpc.ServerInterceptors; | |
| import io.grpc.Status; | |
| import io.grpc.inprocess.InProcessChannelBuilder; | |
| import io.grpc.inprocess.InProcessServerBuilder; | |
| import io.grpc.util.MutableHandlerRegistry; | |
| import java.io.BufferedOutputStream; | |
| import java.io.File; | |
| import java.io.FileInputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.lang.Thread.UncaughtExceptionHandler; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.regex.Pattern; | |
| import javax.annotation.Nullable; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Ignore; | |
| import org.junit.Rule; | |
| import org.junit.Test; | |
| import org.junit.rules.TemporaryFolder; | |
| import org.junit.runner.RunWith; | |
| /** Tests for {@link BazelBuildEventServiceModule}. */ | |
| @RunWith(TestParameterInjector.class) | |
| public final class BazelBuildEventServiceModuleTest extends BuildIntegrationTestCase { | |
| private static final Duration WAIT_FOR_LAST_INVOCATION_TIMEOUT = Duration.ofSeconds(2); | |
| private final String fakeServerName = "fake server for " + getClass(); | |
| private final DelayingPublishBuildEventService buildEventService = | |
| new DelayingPublishBuildEventService(); | |
| private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry(); | |
| private Server fakeServer; | |
| private BazelBuildEventServiceModule besModule; | |
| private BlazeModule connectivityModule = new NoOpConnectivityModule(); | |
| @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); | |
| @Nullable private BuildEventOutputStreamFactory buildEventOutputStreamFactory; | |
| @Override | |
| protected BlazeModule getConnectivityModule() { | |
| return connectivityModule; | |
| } | |
| @Override | |
| protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception { | |
| return super.getRuntimeBuilder() | |
| .addBlazeModule( | |
| new BlazeModule() { | |
| @Override | |
| public void beforeCommand(CommandEnvironment env) { | |
| BazelBuildEventServiceModuleTest.this.events.initExternal(env.getReporter()); | |
| } | |
| }) | |
| .addBlazeModule(new NoSpawnCacheModule()) | |
| .addBlazeModule(new CredentialModule()) | |
| .addBlazeModule( | |
| new BazelBuildEventServiceModule() { | |
| @Override | |
| protected ManagedChannel newGrpcChannel(BackendConfig config) throws IOException { | |
| if (config.besBackend().equals("inprocess")) { | |
| return InProcessChannelBuilder.forName(fakeServerName).build(); | |
| } | |
| return super.newGrpcChannel(config); | |
| } | |
| @Override | |
| protected Duration getMaxWaitForPreviousInvocation() { | |
| return WAIT_FOR_LAST_INVOCATION_TIMEOUT; | |
| } | |
| @Override | |
| BuildEventOutputStreamFactory createBuildEventOutputStreamFactory( | |
| CommandEnvironment env) { | |
| return buildEventOutputStreamFactory == null | |
| ? super.createBuildEventOutputStreamFactory(env) | |
| : buildEventOutputStreamFactory; | |
| } | |
| }); | |
| } | |
| private ImmutableSet<BuildEventTransport> bepTransports; | |
| private final List<BuildEventServiceUploadCompleteEvent> besUploadCompleteEvents = | |
| new ArrayList<>(); | |
| private class BepTransportLogger { | |
| @Subscribe | |
| @SuppressWarnings("unused") | |
| public void transportsKnown(AnnounceBuildEventTransportsEvent event) { | |
| bepTransports = besModule.getBepTransports(); | |
| } | |
| } | |
| private class BuildEventServiceUploadCompleteEventListener { | |
| @Subscribe | |
| @SuppressWarnings("unused") | |
| public void onBuildEventServiceUploadComplete(BuildEventServiceUploadCompleteEvent event) { | |
| besUploadCompleteEvents.add(event); | |
| } | |
| } | |
| private ImmutableSet<BuildEventTransport> getBepTransports() { | |
| return bepTransports; | |
| } | |
| private void runBuildWithOptions(String... options) throws Exception { | |
| addOptions(options); | |
| besModule = runtimeWrapper.getRuntime().getBlazeModule(BazelBuildEventServiceModule.class); | |
| if (buildEventOutputStreamFactory != null) { | |
| besModule.setBuildEventOutputStreamFactory(buildEventOutputStreamFactory); | |
| } | |
| runtimeWrapper.newCommand(); | |
| runtimeWrapper.getSkyframeExecutor().getEventBus().register(new BepTransportLogger()); | |
| runtimeWrapper | |
| .getSkyframeExecutor() | |
| .getEventBus() | |
| .register(new BuildEventServiceUploadCompleteEventListener()); | |
| buildTarget(); | |
| } | |
| private void afterBuildCommand() throws Exception { | |
| runtimeWrapper.newCommand(); | |
| } | |
| @Override | |
| @Nullable | |
| protected UncaughtExceptionHandler createUncaughtExceptionHandler() { | |
| // Disable the crash handler since this test leaves runaway threads e.g. accessing shut down | |
| // fakeServer. | |
| return null; | |
| } | |
| @Before | |
| public void setUp() throws Exception { | |
| serviceRegistry.addService( | |
| ServerInterceptors.intercept( | |
| buildEventService, new TracingMetadataUtils.ServerHeadersInterceptor())); | |
| fakeServer = | |
| InProcessServerBuilder.forName(fakeServerName) | |
| .fallbackHandlerRegistry(serviceRegistry) | |
| .directExecutor() | |
| .build() | |
| .start(); | |
| } | |
| @After | |
| public void tearDown() throws Exception { | |
| fakeServer.shutdownNow(); | |
| fakeServer.awaitTermination(); | |
| } | |
| @Test | |
| public void testCreatesStreamerForTextFormatFileTransport() throws Exception { | |
| runBuildWithOptions("--build_event_text_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(getBepTransports()).hasSize(1); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(TextFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForBinaryFormatFileTransport() throws Exception { | |
| runBuildWithOptions("--build_event_binary_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(getBepTransports()).hasSize(1); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(BinaryFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForJsonFormatFileTransport() throws Exception { | |
| runBuildWithOptions("--build_event_json_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(getBepTransports()).hasSize(1); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(JsonFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForBesTransport() throws Exception { | |
| runBuildWithOptions("--bes_backend=does.not.exist:1234"); | |
| assertThat(getBepTransports()).hasSize(1); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(BuildEventServiceTransport.class); | |
| } | |
| @Test | |
| public void testRetryCount() throws Exception { | |
| runBuildWithOptions( | |
| "--bes_backend=does.not.exist:1234", "--experimental_build_event_upload_max_retries=3"); | |
| afterBuildCommand(); | |
| events.assertContainsError( | |
| "The Build Event Protocol upload failed: All 3 retry attempts failed"); | |
| } | |
| @Test | |
| public void testConnectivityFailureDisablesBesStreaming() throws Exception { | |
| class FailingConnectivityStatusProvider extends BlazeModule | |
| implements ConnectivityStatusProvider { | |
| @Override | |
| public ConnectivityStatus getStatus(String service) { | |
| return new ConnectivityStatus( | |
| ConnectivityStatus.Status.NO_CREDENTIALS, "forced connectivity failure"); | |
| } | |
| } | |
| connectivityModule = new FailingConnectivityStatusProvider(); | |
| reinitializeAndPreserveOptions(); | |
| addOptions("--bes_backend=does.not.exist:1234"); | |
| addOptions("--spawn_strategy=standalone"); | |
| runBuildWithOptions(); | |
| assertThat(getBepTransports()).isEmpty(); | |
| } | |
| @Test | |
| public void testCreatesStreamerForGrpcBesResultsUrl() throws Exception { | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=FULLY_ASYNC", | |
| "--bes_results_url=http://results-ui/"); | |
| assertThat(getBepTransports()).hasSize(1); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(BuildEventServiceTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForGrpcRunsPerTestTooHighDisablesStreaming() { | |
| AbruptExitException expected = | |
| assertThrows( | |
| AbruptExitException.class, | |
| () -> | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", "--runs_per_test=" + (RUNS_PER_TEST_LIMIT + 1))); | |
| assertThat(expected.getExitCode()).isEqualTo(ExitCode.COMMAND_LINE_ERROR); | |
| assertThat(getBepTransports()).isEmpty(); | |
| assertContainsError("The value of --runs_per_test"); | |
| } | |
| @Test | |
| public void testBeforeCommandGrpcReportsBesResultsUrl() throws Exception { | |
| runBuildWithOptions( | |
| "--color=no", // disable ANSI color sequences | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=FULLY_ASYNC", | |
| "--bes_results_url=http://results-ui/"); | |
| events.assertContainsEventsInOrder( | |
| "Streaming build results to: http://results-ui/", "Found 0 targets", "Found 0 targets"); | |
| } | |
| @Test | |
| public void testAfterCommandGrpcReportsBesResultsUrl() throws Exception { | |
| runBuildWithOptions( | |
| "--color=no", // disable ANSI color sequences | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=FULLY_ASYNC", | |
| "--bes_results_url=http://results-ui/"); | |
| afterBuildCommand(); | |
| events.assertContainsEventsInOrder( | |
| "Streaming build results to: http://results-ui/", | |
| "Found 0 targets", | |
| "Found 0 targets", | |
| "Streaming build results to: http://results-ui/", | |
| "Streaming build results to: http://results-ui/"); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ZERO); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=5s"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_postsEvent() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofMillis(100)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=5s"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| assertThat(besUploadCompleteEvents).hasSize(1); | |
| assertThat(besUploadCompleteEvents.get(0).duration()).isGreaterThan(Duration.ZERO); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_slowFullCloseError() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=5s"); | |
| ImmutableSet<BuildEventTransport> bepTransports = getBepTransports(); | |
| assertThat(bepTransports).hasSize(1); | |
| afterBuildCommand(); | |
| assertContainsError("The Build Event Protocol upload timed out"); | |
| for (BuildEventTransport bepTransport : bepTransports) { | |
| assertThat(bepTransport.close().isDone()).isTrue(); | |
| } | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_slowHalfCloseError() throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=5s"); | |
| afterBuildCommand(); | |
| assertContainsError("The Build Event Protocol upload timed out"); | |
| } | |
| @Test | |
| public void testAfterCommand_noWaitForUploadComplete() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ZERO); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testAfterCommand_noWaitForUploadComplete_slowFullCloseIgnored() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testAfterCommand_noWaitForUploadComplete_slowHalfCloseIgnored() throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_noWaitForUploadComplete_slowFullCloseWarning() | |
| throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed to complete in"); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_noWaitForUploadComplete_slowHalfCloseWarning() | |
| throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed to complete in"); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_noWaitForUploadComplete_besTimeout_slowFullCloseWarning() | |
| throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=1s"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed due to a network timeout"); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_noWaitForUpload_besTimeout_slowHalfCloseWarning() | |
| throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=1s"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed due to a network timeout"); | |
| } | |
| @Test | |
| public void testAfterCommand_fullyAsync() throws Exception { | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| private static String getBuildEventFileFlag( | |
| BuildEventFileType buildEventFileType, String filePath) { | |
| return switch (buildEventFileType) { | |
| case TEXT -> "--build_event_text_file=" + filePath; | |
| case JSON -> "--build_event_json_file=" + filePath; | |
| case BINARY -> "--build_event_binary_file=" + filePath; | |
| }; | |
| } | |
| private static String getBuildEventFileUploadModeFlag( | |
| BuildEventFileType buildEventFileType, String mode) { | |
| return switch (buildEventFileType) { | |
| case TEXT -> "--build_event_text_file_upload_mode=" + mode; | |
| case JSON -> "--build_event_json_file_upload_mode=" + mode; | |
| case BINARY -> "--build_event_binary_file_upload_mode=" + mode; | |
| }; | |
| } | |
| @Test | |
| public void testAfterCommand_buildEventFile_waitForUploadComplete( | |
| @TestParameter BuildEventFileType buildEventFileType) throws Exception { | |
| AtomicReference<DelayingCloseBufferedOutputStream> outRef = new AtomicReference<>(null); | |
| buildEventOutputStreamFactory = | |
| (type, filePath) -> { | |
| var out = | |
| new DelayingCloseBufferedOutputStream( | |
| Files.newOutputStream(Path.of(filePath)), Duration.ofSeconds(1)); | |
| outRef.set(out); | |
| return out; | |
| }; | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| var file = tmpFolder.newFile(); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=FULLY_ASYNC", | |
| "--bes_timeout=1s", | |
| getBuildEventFileFlag(buildEventFileType, file.getAbsolutePath()), | |
| getBuildEventFileUploadModeFlag(buildEventFileType, "wait_for_upload_complete")); | |
| afterBuildCommand(); | |
| assertThat(outRef.get().isClosed()).isTrue(); | |
| // Expect Bazel doesn't wait for uploading to bes_backend, otherwise there will be a timeout | |
| // error. | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testAfterCommand_fullyAsync_slowHalfCloseIgnored() throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_fullyAsync_slowFullCloseIgnored() throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| // TODO(b/246912214): Deflake this by fixing the threading model to match the upstream gRPC | |
| // changes in https://github.com/grpc/grpc-java/pull/9319 that affect InProcessTransport. | |
| @Ignore("b/246912214") | |
| @Test | |
| public void testBeforeSecondCommand_fullyAsync_slowHalfCloseWarning() throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed to complete in"); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_fullyAsync_besTimeout_slowFullCloseIgnored() | |
| throws Exception { | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC", "--bes_timeout=1s"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| // TODO(b/246912214): Deflake this by fixing the threading model to match the upstream gRPC | |
| // changes in https://github.com/grpc/grpc-java/pull/9319 that affect InProcessTransport. | |
| @Ignore("b/246912214") | |
| @Test | |
| public void testBeforeSecondCommand_fullyAsync_besTimeout_slowHalfCloseWarning() | |
| throws Exception { | |
| buildEventService.setDelayBeforeHalfClosingStream(Duration.ofSeconds(10)); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC", "--bes_timeout=1s"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning( | |
| "The background upload of the Build Event Protocol for the previous " | |
| + "invocation failed due to a network timeout."); | |
| } | |
| @Test | |
| public void testAfterCommandStreamerIsClosedNoWarning() throws Exception { | |
| runBuildWithOptions("--build_event_text_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(getBepTransports()).hasSize(1); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_retryableErrorEarlyInStream() | |
| throws Exception { | |
| int numRetries = 3; | |
| buildEventService.setErrorMessageAndCode("Boom8", Status.UNAVAILABLE); | |
| buildEventService.setErrorEarlyInStream(true); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--build_event_upload_max_retries=" + numRetries); | |
| afterBuildCommand(); | |
| events.assertContainsError( | |
| Pattern.compile( | |
| "The Build Event Protocol upload failed: Not retrying publishBuildEvents, no more" | |
| + " attempts left.*Boom8")); | |
| assertThat(buildEventService.getRequestsReceivedCount()).isEqualTo(numRetries + 1); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_permissionDeniedErrorEarlyInStream() | |
| throws Exception { | |
| int numRetries = 3; | |
| buildEventService.setErrorMessageAndCode("Boom15", Status.PERMISSION_DENIED); | |
| buildEventService.setErrorEarlyInStream(true); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--build_event_upload_max_retries=" + numRetries); | |
| afterBuildCommand(); | |
| events.assertContainsError( | |
| Pattern.compile( | |
| "The Build Event Protocol upload failed: Not retrying publishBuildEvents.*Boom15")); | |
| assertThat(buildEventService.getRequestsReceivedCount()).isEqualTo(1); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_invalidArgumentErrorEarlyInStream() | |
| throws Exception { | |
| int numRetries = 3; | |
| buildEventService.setErrorMessageAndCode("Boom15", Status.INVALID_ARGUMENT); | |
| buildEventService.setErrorEarlyInStream(true); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--build_event_upload_max_retries=" + numRetries); | |
| afterBuildCommand(); | |
| events.assertContainsError( | |
| Pattern.compile( | |
| "The Build Event Protocol upload failed: Not retrying publishBuildEvents.*Boom15")); | |
| assertThat(buildEventService.getRequestsReceivedCount()).isEqualTo(1); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom1"); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| events.assertContainsError("The Build Event Protocol upload failed: Boom1"); | |
| } | |
| @Test | |
| public void testAfterCommand_waitForUploadComplete_besTimeout_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom2"); | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_timeout=5s"); | |
| afterBuildCommand(); | |
| events.assertContainsError("The Build Event Protocol upload failed: Boom2"); | |
| } | |
| @Test | |
| public void testAfterCommand_noWaitForUploadComplete_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom3"); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_noWaitForUploadComplete_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom4"); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=NOWAIT_FOR_UPLOAD_COMPLETE"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertContainsWarning("The Build Event Protocol upload failed: Boom4"); | |
| } | |
| @Test | |
| public void testAfterCommand_fullyAsync_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom5"); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testBeforeSecondCommand_fullyAsync_errorOnComplete() throws Exception { | |
| buildEventService.setErrorMessage("Boom6"); | |
| runBuildWithOptions("--bes_backend=inprocess", "--bes_upload_mode=FULLY_ASYNC"); | |
| afterBuildCommand(); | |
| buildTarget(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @Test | |
| public void testCreatesStreamerForAllTransports() throws Exception { | |
| runBuildWithOptions( | |
| "--build_event_text_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--build_event_binary_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--build_event_json_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--bes_backend=does.not.exist:1234"); | |
| assertThat(getBepTransports()).hasSize(4); | |
| assertThat(getBepTransports().asList().get(0)).isInstanceOf(TextFormatFileTransport.class); | |
| assertThat(getBepTransports().asList().get(1)).isInstanceOf(BinaryFormatFileTransport.class); | |
| assertThat(getBepTransports().asList().get(2)).isInstanceOf(JsonFormatFileTransport.class); | |
| assertThat(getBepTransports().asList().get(3)).isInstanceOf(BuildEventServiceTransport.class); | |
| } | |
| @Test | |
| public void testUploaderSharing() throws Exception { | |
| runBuildWithOptions( | |
| "--build_event_text_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--build_event_binary_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--build_event_json_file=" + tmpFolder.newFile().getAbsolutePath(), | |
| "--bes_backend=does.not.exist:1234"); | |
| assertThat(getBepTransports()).hasSize(4); | |
| BuildEventArtifactUploader uploader = | |
| Iterables.getFirst(getBepTransports(), null).getUploader(); | |
| assertThat(uploader).isNotNull(); | |
| for (BuildEventTransport transport : getBepTransports()) { | |
| assertThat(uploader).isSameInstanceAs(transport.getUploader()); | |
| } | |
| } | |
| @Test | |
| public void testDoesNotCreatesStreamerWithoutTransports() throws Exception { | |
| runBuildWithOptions(); | |
| assertThat(getBepTransports()).isEmpty(); | |
| } | |
| @Test | |
| public void testKeywords() throws Exception { | |
| runBuildWithOptions(); | |
| BuildEventServiceOptions besOptions = new BuildEventServiceOptions(); | |
| besOptions.besKeywords = ImmutableList.of("keyword0", "keyword1", "keyword0"); | |
| besOptions.besSystemKeywords = ImmutableList.of("sys_keyword0", "sys_keyword1", "sys_keyword0"); | |
| assertThat(besModule.getBesKeywords("build", besOptions, null)) | |
| .containsExactly( | |
| "protocol_name=BEP", | |
| "command_name=build", | |
| "user_keyword=keyword0", | |
| "user_keyword=keyword1", | |
| "sys_keyword0", | |
| "sys_keyword1"); | |
| } | |
| @Test | |
| public void testMakeGrpcMetadata() throws Exception { | |
| runBuildWithOptions(); | |
| BuildEventServiceOptions besOptions = new BuildEventServiceOptions(); | |
| AuthAndTLSOptions authAndTLSOptions = new AuthAndTLSOptions(); | |
| besOptions.besBackend = "bes-backend"; | |
| besOptions.besProxy = "bes-proxy"; | |
| besOptions.besHeaders = | |
| ImmutableList.of( | |
| Map.entry("key1", "val1"), | |
| Map.entry("key2", "val2"), | |
| Map.entry("key3", "val3"), | |
| Map.entry("key1", "val4")); | |
| BackendConfig newConfig = BackendConfig.create(besOptions, authAndTLSOptions); | |
| Metadata metadata = BazelBuildEventServiceModule.makeGrpcMetadata(newConfig); | |
| assertThat(metadata.get(Metadata.Key.of("key1", Metadata.ASCII_STRING_MARSHALLER))) | |
| .isEqualTo("val4"); | |
| assertThat(metadata.get(Metadata.Key.of("key2", Metadata.ASCII_STRING_MARSHALLER))) | |
| .isEqualTo("val2"); | |
| assertThat(metadata.get(Metadata.Key.of("key3", Metadata.ASCII_STRING_MARSHALLER))) | |
| .isEqualTo("val3"); | |
| } | |
| @Test | |
| public void oom_firstReportedViaHandleCrash() throws Exception { | |
| testOom( | |
| () -> { | |
| OutOfMemoryError oom = new OutOfMemoryError(); | |
| // Simulates an OOM coming from GcThrashingDetector, which reports the error by calling | |
| // handleCrash. Uses keepAlive() to avoid exiting the JVM and aborting the test, then | |
| // throw the original oom to ensure control flow terminates. | |
| BugReport.handleCrash(Crash.from(oom), CrashContext.keepAlive()); | |
| throw oom; | |
| }); | |
| } | |
| @Test | |
| public void oom_firstThrownFromSkyframe() throws Exception { | |
| testOom( | |
| () -> { | |
| throw new OutOfMemoryError(); | |
| }); | |
| } | |
| private void testOom(Runnable throwOom) throws Exception { | |
| write("foo/BUILD", "genrule(name = 'gen', outs = ['gen.out'], cmd = 'touch $@')"); | |
| AtomicBoolean threwOom = new AtomicBoolean(false); | |
| getSkyframeExecutor() | |
| .getEvaluator() | |
| .injectGraphTransformerForTesting( | |
| NotifyingHelper.makeNotifyingTransformer( | |
| // To get the right configuration, some analysis has to already been done. | |
| // We're only throwing OOM here for non shareable ActionLookupData to exclude | |
| // workspace status actions, which in Skymeld mode can run without any analysis. | |
| (key, type, order, context) -> { | |
| if (key instanceof ActionLookupData | |
| && key.valueIsShareable() | |
| && !threwOom.getAndSet(true)) { | |
| throwOom.run(); | |
| } | |
| })); | |
| File buildEventBinaryFile = tmpFolder.newFile(); | |
| addOptions( | |
| "--build_event_binary_file=" + buildEventBinaryFile.getAbsolutePath(), | |
| "--oom_message=Please build fewer targets."); | |
| assertThrows(OutOfMemoryError.class, () -> buildTarget("//foo:gen")); | |
| List<BuildEvent> buildEvents = new ArrayList<>(); | |
| try (InputStream in = new FileInputStream(buildEventBinaryFile)) { | |
| BuildEvent ev; | |
| while ((ev = BuildEvent.parseDelimitedFrom(in)) != null) { | |
| buildEvents.add(ev); | |
| } | |
| } | |
| Aborted expectedAbort = | |
| Aborted.newBuilder() | |
| .setReason(AbortReason.OUT_OF_MEMORY) | |
| .setDescription(BugReport.constructOomExitMessage("Please build fewer targets.")) | |
| .build(); | |
| assertThat(buildEvents) | |
| .ignoringFields(BuildEvent.LAST_MESSAGE_FIELD_NUMBER) | |
| .containsAtLeast( | |
| BuildEvent.newBuilder() | |
| .setId( | |
| BuildEventId.newBuilder() | |
| .setBuildFinished(BuildFinishedId.getDefaultInstance())) | |
| .setAborted(expectedAbort) | |
| .build(), | |
| BuildEvent.newBuilder() | |
| .setId( | |
| BuildEventId.newBuilder() | |
| .setTargetCompleted( | |
| TargetCompletedId.newBuilder() | |
| .setLabel("//foo:gen") | |
| .setConfiguration( | |
| ConfigurationId.newBuilder() | |
| .setId( | |
| getConfiguredTarget("//foo:gen") | |
| .getConfigurationChecksum())))) | |
| .setAborted(expectedAbort) | |
| .build()); | |
| assertThat(runtimeWrapper.getCrashMessages()) | |
| .containsExactly( | |
| TestConstants.PRODUCT_NAME + " is crashing: Crashed: (java.lang.OutOfMemoryError) "); | |
| assertAndClearBugReporterStoredCrash(OutOfMemoryError.class); | |
| } | |
| @Test | |
| public void oom_besClosesAfterSpecialCaseTimeoutThrownFromSkyframe() throws Exception { | |
| // BES server-side will never finish. The test will pass simply by completing and not waiting | |
| // until the test timeout. | |
| buildEventService.setDelayBeforeClosingStream(Duration.ofHours(10)); | |
| write("foo/BUILD", "genrule(name = 'gen', outs = ['gen.out'], cmd = 'touch $@')"); | |
| AtomicBoolean threwOom = new AtomicBoolean(false); | |
| getSkyframeExecutor() | |
| .getEvaluator() | |
| .injectGraphTransformerForTesting( | |
| NotifyingHelper.makeNotifyingTransformer( | |
| (key, type, order, context) -> { | |
| if (key instanceof ActionLookupData && !threwOom.getAndSet(true)) { | |
| throw new OutOfMemoryError(); | |
| } | |
| })); | |
| addOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE", | |
| "--bes_oom_finish_upload_timeout=2s", | |
| "--oom_message=Please build fewer targets."); | |
| assertThrows(OutOfMemoryError.class, () -> buildTarget("//foo:gen")); | |
| assertThat(runtimeWrapper.getCrashMessages()) | |
| .containsExactly( | |
| TestConstants.PRODUCT_NAME + " is crashing: Crashed: (java.lang.OutOfMemoryError) "); | |
| assertAndClearBugReporterStoredCrash(OutOfMemoryError.class); | |
| } | |
| @Test | |
| public void commandLineEvents_includesFlagsFromFlagsets() throws Exception { | |
| write( | |
| "hello/BUILD", | |
| """ | |
| genrule(name = "hello", outs = ["hello.out"], cmd = "touch $@") | |
| """); | |
| write( | |
| "flag/flag_def.bzl", | |
| """ | |
| string_flag = rule( | |
| implementation = lambda ctx: [], | |
| build_setting = config.string(flag = True), | |
| ) | |
| """); | |
| write( | |
| "flag/BUILD", | |
| """ | |
| load(":flag_def.bzl", "string_flag") | |
| string_flag( | |
| name = "my_flag", | |
| build_setting_default = "default_value", | |
| ) | |
| """); | |
| writeProjectSclDefinition("test/project_proto.scl", /* alsoWriteBuildFile= */ true); | |
| write( | |
| "hello/PROJECT.scl", | |
| """ | |
| load( | |
| "//test:project_proto.scl", | |
| "buildable_unit_pb2", | |
| "project_pb2", | |
| ) | |
| project = project_pb2.Project.create( | |
| enforcement_policy = "warn", | |
| buildable_units = [ | |
| buildable_unit_pb2.BuildableUnit.create( | |
| name = "default_config", | |
| flags = ["--define=foo=bar", "--//flag:my_flag=my_value"], | |
| is_default = True, | |
| ) | |
| ], | |
| ) | |
| """); | |
| File buildEventBinaryFile = tmpFolder.newFile(); | |
| addOptions( | |
| "--enforce_project_configs", | |
| "--build_event_binary_file=" + buildEventBinaryFile.getAbsolutePath()); | |
| buildTarget("//hello:hello"); | |
| BuildEvent canonicalCommandLineEvent = null; | |
| try (InputStream in = new FileInputStream(buildEventBinaryFile)) { | |
| BuildEvent ev; | |
| while ((ev = BuildEvent.parseDelimitedFrom(in)) != null) { | |
| if (ev.hasStructuredCommandLine() | |
| && ev.getStructuredCommandLine().getCommandLineLabel().equals("canonical")) { | |
| canonicalCommandLineEvent = ev; | |
| } | |
| } | |
| } | |
| ImmutableList<CommandLineSection> sections = | |
| canonicalCommandLineEvent.getStructuredCommandLine().getSectionsList().stream() | |
| .filter(s -> s.getSectionLabel().equals("command options")) | |
| .collect(toImmutableList()); | |
| ImmutableList<String> options = | |
| sections.getFirst().getOptionList().getOptionList().stream() | |
| .map(Option::getCombinedForm) | |
| .collect(toImmutableList()); | |
| assertThat(options).contains("--define=foo=bar"); | |
| assertThat(options).contains("--//flag:my_flag=my_value"); | |
| } | |
| private static final class DelayingCloseBufferedOutputStream extends BufferedOutputStream { | |
| private final Duration delay; | |
| private final AtomicBoolean closed = new AtomicBoolean(false); | |
| DelayingCloseBufferedOutputStream(OutputStream out, Duration delay) { | |
| super(out); | |
| this.delay = delay; | |
| this.out = out; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| Uninterruptibles.sleepUninterruptibly(delay); | |
| super.close(); | |
| closed.set(true); | |
| } | |
| public boolean isClosed() { | |
| return closed.get(); | |
| } | |
| } | |
| } |