| // 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.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 static org.junit.Assume.assumeFalse; | |
| import build.bazel.remote.execution.v2.RequestMetadata; | |
| import com.google.common.base.MoreObjects; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Sets; | |
| import com.google.common.util.concurrent.Uninterruptibles; | |
| import com.google.devtools.build.lib.actions.ActionLookupData; | |
| import com.google.devtools.build.lib.analysis.util.AnalysisMock; | |
| 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.buildeventstream.BuildEventArtifactUploader; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos; | |
| 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.NamedSetOfFilesId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TargetCompletedId; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.NamedSetOfFiles; | |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.OutputGroup; | |
| 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.BlazeRuntimeWrapper; | |
| 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.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.devtools.build.v1.BuildEvent.BuildComponentStreamFinished.FinishType; | |
| import com.google.devtools.build.v1.PublishBuildEventGrpc.PublishBuildEventImplBase; | |
| import com.google.devtools.build.v1.PublishBuildToolEventStreamRequest; | |
| import com.google.devtools.build.v1.PublishBuildToolEventStreamResponse; | |
| import com.google.devtools.build.v1.PublishLifecycleEventRequest; | |
| import com.google.devtools.build.v1.StreamId; | |
| import com.google.protobuf.Empty; | |
| import io.grpc.ManagedChannel; | |
| import io.grpc.Metadata; | |
| import io.grpc.Server; | |
| import io.grpc.ServerInterceptors; | |
| import io.grpc.Status; | |
| import io.grpc.StatusRuntimeException; | |
| import io.grpc.inprocess.InProcessChannelBuilder; | |
| import io.grpc.inprocess.InProcessServerBuilder; | |
| import io.grpc.stub.StreamObserver; | |
| import io.grpc.util.MutableHandlerRegistry; | |
| import java.io.File; | |
| import java.io.FileInputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.lang.Thread.UncaughtExceptionHandler; | |
| import java.time.Duration; | |
| import java.util.ArrayDeque; | |
| import java.util.ArrayList; | |
| import java.util.Deque; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.SortedSet; | |
| import java.util.concurrent.ArrayBlockingQueue; | |
| import java.util.concurrent.BlockingQueue; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import javax.annotation.Nullable; | |
| import javax.annotation.concurrent.GuardedBy; | |
| 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; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link BazelBuildEventServiceModule}. */ | |
| @RunWith(JUnit4.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(); | |
| @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; | |
| } | |
| }); | |
| } | |
| private void runBuildWithOptions(String... options) throws Exception { | |
| addOptions(options); | |
| besModule = runtimeWrapper.getRuntime().getBlazeModule(BazelBuildEventServiceModule.class); | |
| runtimeWrapper.newCommand(); | |
| 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(besModule.getBepTransports()).hasSize(1); | |
| assertThat(besModule.getBepTransports().asList().get(0)) | |
| .isInstanceOf(TextFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForBinaryFormatFileTransport() throws Exception { | |
| runBuildWithOptions("--build_event_binary_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(besModule.getBepTransports()).hasSize(1); | |
| assertThat(besModule.getBepTransports().asList().get(0)) | |
| .isInstanceOf(BinaryFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForJsonFormatFileTransport() throws Exception { | |
| runBuildWithOptions("--build_event_json_file=" + tmpFolder.newFile().getAbsolutePath()); | |
| assertThat(besModule.getBepTransports()).hasSize(1); | |
| assertThat(besModule.getBepTransports().asList().get(0)) | |
| .isInstanceOf(JsonFormatFileTransport.class); | |
| } | |
| @Test | |
| public void testCreatesStreamerForBesTransport() throws Exception { | |
| runBuildWithOptions("--bes_backend=does.not.exist:1234"); | |
| assertThat(besModule.getBepTransports()).hasSize(1); | |
| assertThat(besModule.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(); | |
| BlazeRuntimeWrapper runtimeWrapper = | |
| new BlazeRuntimeWrapper( | |
| events, serverDirectories, directories, binTools, getRuntimeBuilder()); | |
| BazelBuildEventServiceModule besModule = | |
| runtimeWrapper.getRuntime().getBlazeModule(BazelBuildEventServiceModule.class); | |
| runtimeWrapper.addOptions("--bes_backend=does.not.exist:1234"); | |
| runtimeWrapper.addOptions("--spawn_strategy=standalone"); | |
| runtimeWrapper.executeBuild(ImmutableList.of()); | |
| assertThat(besModule.getBepTransports()).isEmpty(); | |
| } | |
| @Test | |
| public void testCreatesStreamerForGrpcBesResultsUrl() throws Exception { | |
| runBuildWithOptions( | |
| "--bes_backend=inprocess", | |
| "--bes_upload_mode=FULLY_ASYNC", | |
| "--bes_results_url=http://results-ui/"); | |
| assertThat(besModule.getBepTransports()).hasSize(1); | |
| assertThat(besModule.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(besModule.getBepTransports()).isEmpty(); | |
| assertContainsError("The value of --runs_per_test"); | |
| } | |
| @Test | |
| public void testBeforeCommandGrpcReportsBesResultsUrl() throws Exception { | |
| runBuildWithOptions( | |
| "--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( | |
| "--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_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 = besModule.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(); | |
| } | |
| @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(besModule.getBepTransports()).hasSize(1); | |
| afterBuildCommand(); | |
| events.assertNoWarningsOrErrors(); | |
| } | |
| @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(besModule.getBepTransports()).hasSize(4); | |
| assertThat(besModule.getBepTransports().asList().get(0)) | |
| .isInstanceOf(TextFormatFileTransport.class); | |
| assertThat(besModule.getBepTransports().asList().get(1)) | |
| .isInstanceOf(BinaryFormatFileTransport.class); | |
| assertThat(besModule.getBepTransports().asList().get(2)) | |
| .isInstanceOf(JsonFormatFileTransport.class); | |
| assertThat(besModule.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(besModule.getBepTransports()).hasSize(4); | |
| BuildEventArtifactUploader uploader = | |
| Iterables.getFirst(besModule.getBepTransports(), null).getUploader(); | |
| assertThat(uploader).isNotNull(); | |
| for (BuildEventTransport transport : besModule.getBepTransports()) { | |
| assertThat(uploader).isSameInstanceAs(transport.getUploader()); | |
| } | |
| } | |
| @Test | |
| public void testDoesNotCreatesStreamerWithoutTransports() throws Exception { | |
| runBuildWithOptions(); | |
| assertThat(besModule.getBepTransports()).isEmpty(); | |
| } | |
| @Test | |
| public void testKeywords() throws Exception { | |
| runBuildWithOptions(); | |
| BuildEventServiceOptions besOptions = new BuildEventServiceOptions(); | |
| besOptions.besKeywords = ImmutableList.of("keyword0", "keyword1", "keyword0"); | |
| assertThat(besModule.getBesKeywords(besOptions, null)) | |
| .containsExactly("user_keyword=keyword0", "user_keyword=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 = | |
| ImmutableMap.of("key1", "val1", "key2", "val2", "key3", "val3").entrySet().asList(); | |
| BackendConfig newConfig = BackendConfig.create(besOptions, authAndTLSOptions); | |
| Metadata metadata = BazelBuildEventServiceModule.makeGrpcMetadata(newConfig); | |
| assertThat(metadata.get(Metadata.Key.of("key1", Metadata.ASCII_STRING_MARSHALLER))) | |
| .isEqualTo("val1"); | |
| 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"); | |
| } | |
| /** Regression test for b/111653523. */ | |
| @Test | |
| public void testCoverageFileIncluded() throws Exception { | |
| assumeFalse(AnalysisMock.get().isThisBazel()); | |
| // Test aims to ensure that the TargetCompleted event for "//foo:foo_lib" includes the | |
| // "baseline_coverage.dat" file in its "baseline.lcov" output group. | |
| write("foo/BUILD", "cc_library(name = 'foo_lib', srcs = ['foo.cc'])"); | |
| write("foo/foo.cc"); | |
| File buildEventBinaryFile = tmpFolder.newFile(); | |
| addOptions( | |
| "--build_event_binary_file=" + buildEventBinaryFile.getAbsolutePath(), | |
| "--collect_code_coverage", | |
| "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE"); | |
| buildTarget("//foo:foo_lib"); | |
| // We need to wait for all events to be written to the file, which is done in #afterCommand() | |
| // if --bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE. | |
| afterBuildCommand(); | |
| List<BuildEvent> buildEvents = new ArrayList<>(); | |
| try (InputStream in = new FileInputStream(buildEventBinaryFile)) { | |
| while (in.available() > 0) { | |
| buildEvents.add(BuildEvent.parseDelimitedFrom(in)); | |
| } | |
| } | |
| // Find all the NamedSetOfFiles events and the OutputGroup named "baseline.lcov" for the | |
| // target "//foo:foo_lib". | |
| Map<String, NamedSetOfFiles> namedSets = new HashMap<>(); | |
| OutputGroup coverageOutputGroup = null; | |
| for (BuildEvent buildEvent : buildEvents) { | |
| switch (buildEvent.getId().getIdCase()) { | |
| case NAMED_SET: | |
| namedSets.put(buildEvent.getId().getNamedSet().getId(), buildEvent.getNamedSetOfFiles()); | |
| break; | |
| case TARGET_COMPLETED: | |
| if (buildEvent.getId().getTargetCompleted().getLabel().equals("//foo:foo_lib")) { | |
| for (OutputGroup outputGroup : buildEvent.getCompleted().getOutputGroupList()) { | |
| if (outputGroup.getName().equals("baseline.lcov")) { | |
| coverageOutputGroup = outputGroup; | |
| } | |
| } | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| assertThat(coverageOutputGroup).isNotNull(); | |
| BuildEventStreamProtos.File baselineCoverageFile = | |
| findFileInNamedSets(namedSets, coverageOutputGroup, "foo/foo_lib/baseline_coverage.dat"); | |
| assertThat(baselineCoverageFile).isNotNull(); | |
| } | |
| /** | |
| * Recursively walks through NamedSetOfFiles events looking for a file with a given name, starting | |
| * with the file sets in a given output group. | |
| */ | |
| @Nullable | |
| private static BuildEventStreamProtos.File findFileInNamedSets( | |
| Map<String, NamedSetOfFiles> namedSets, | |
| OutputGroup coverageOutputGroup, | |
| String fileNameToFind) { | |
| Deque<String> visit = new ArrayDeque<>(); | |
| for (NamedSetOfFilesId namedSetOfFilesId : coverageOutputGroup.getFileSetsList()) { | |
| visit.add(namedSetOfFilesId.getId()); | |
| } | |
| Set<String> seen = new HashSet<>(visit); | |
| while (!visit.isEmpty()) { | |
| String id = visit.removeFirst(); | |
| NamedSetOfFiles set = namedSets.get(id); | |
| for (BuildEventStreamProtos.File file : set.getFilesList()) { | |
| if (file.getName().equals(fileNameToFind)) { | |
| return file; | |
| } | |
| } | |
| for (NamedSetOfFilesId transitiveSet : set.getFileSetsList()) { | |
| if (seen.add(transitiveSet.getId())) { | |
| visit.addLast(transitiveSet.getId()); | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| @Test | |
| public void oom_firstReportedViaHandleCrash() throws Exception { | |
| testOom( | |
| () -> { | |
| OutOfMemoryError oom = new OutOfMemoryError(); | |
| // Simulates an OOM coming from RetainedHeapLimiter, 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( | |
| (key, type, order, context) -> { | |
| if (key instanceof ActionLookupData && !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)) { | |
| while (in.available() > 0) { | |
| buildEvents.add(BuildEvent.parseDelimitedFrom(in)); | |
| } | |
| } | |
| 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); | |
| } | |
| /** | |
| * Trivial, in-memory implementation of a PublishBuildToolEventStream handler that can have | |
| * pre-configured sleeps triggered at critical junctures. | |
| */ | |
| private static class DelayingPublishBuildStreamObserver | |
| implements StreamObserver<PublishBuildToolEventStreamRequest> { | |
| private final StreamObserver<PublishBuildToolEventStreamResponse> responseObserver; | |
| private final Duration delayBeforeClosingStream; | |
| private final Duration delayBeforeHalfClosingStream; | |
| @GuardedBy("this") | |
| private final SortedSet<Long> unackedSequenceNumbers = Sets.newTreeSet(); | |
| private final BlockingQueue<Long> ackQueue = new ArrayBlockingQueue<>(10); | |
| @GuardedBy("this") | |
| private Thread ackingThread = null; | |
| @GuardedBy("this") | |
| private StreamId streamId = null; | |
| @GuardedBy("this") | |
| private boolean finished = false; | |
| private DelayingPublishBuildStreamObserver( | |
| StreamObserver<PublishBuildToolEventStreamResponse> responseObserver, | |
| Duration delayBeforeClosingStream, | |
| Duration delayBeforeHalfClosingStream) { | |
| this.responseObserver = responseObserver; | |
| this.delayBeforeClosingStream = delayBeforeClosingStream; | |
| this.delayBeforeHalfClosingStream = delayBeforeHalfClosingStream; | |
| } | |
| /** Creates the acking thread, safely callable after the constructor finishes. */ | |
| synchronized void startAckingThread() { | |
| Preconditions.checkState(ackingThread == null, "startAckingThread() called twice"); | |
| ackingThread = new Thread(new AckingThread()); | |
| ackingThread.start(); | |
| } | |
| @Override | |
| public void onNext(PublishBuildToolEventStreamRequest req) { | |
| List<Long> longsToPut = new ArrayList<>(); | |
| synchronized (this) { | |
| if (!unackedSequenceNumbers.add(req.getOrderedBuildEvent().getSequenceNumber())) { | |
| return; // dupe, ignore | |
| } | |
| streamId = MoreObjects.firstNonNull(streamId, req.getOrderedBuildEvent().getStreamId()); | |
| if (req.getOrderedBuildEvent().getEvent().getComponentStreamFinished().getType() | |
| == FinishType.FINISH_TYPE_UNSPECIFIED) { | |
| // We did not get the final event. Ack the *previous* event, if there is a previous event. | |
| if (unackedSequenceNumbers.size() > 1) { | |
| longsToPut.add(ackLowestSequenceNumber()); | |
| } | |
| } else { | |
| Uninterruptibles.sleepUninterruptibly(delayBeforeHalfClosingStream); | |
| // final event. ack everything remaining. | |
| while (!unackedSequenceNumbers.isEmpty()) { | |
| longsToPut.add(ackLowestSequenceNumber()); | |
| } | |
| if (finished) { | |
| longsToPut.add(SENTINEL_VALUE); | |
| } | |
| } | |
| } | |
| for (Long seqNum : longsToPut) { | |
| Uninterruptibles.putUninterruptibly(ackQueue, seqNum); | |
| } | |
| } | |
| @GuardedBy("this") | |
| private Long ackLowestSequenceNumber() { | |
| Long firstUnacked = unackedSequenceNumbers.first(); | |
| unackedSequenceNumbers.remove(firstUnacked); | |
| return firstUnacked; | |
| } | |
| @Override | |
| public synchronized void onError(Throwable t) { | |
| finished = true; | |
| responseObserver.onError(t); | |
| } | |
| @Override | |
| public void onCompleted() { | |
| boolean putSentinel; | |
| synchronized (this) { | |
| finished = true; | |
| putSentinel = unackedSequenceNumbers.isEmpty(); | |
| } | |
| if (putSentinel) { | |
| Uninterruptibles.putUninterruptibly(ackQueue, SENTINEL_VALUE); | |
| } | |
| } | |
| static final Long SENTINEL_VALUE = -1L; | |
| private class AckingThread implements Runnable { | |
| @Override | |
| public void run() { | |
| while (true) { | |
| Long firstUnacked = Uninterruptibles.takeUninterruptibly(ackQueue); | |
| synchronized (DelayingPublishBuildStreamObserver.this) { | |
| if (firstUnacked.equals(SENTINEL_VALUE)) { | |
| Uninterruptibles.sleepUninterruptibly(delayBeforeClosingStream); | |
| responseObserver.onCompleted(); | |
| return; | |
| } | |
| responseObserver.onNext( | |
| PublishBuildToolEventStreamResponse.newBuilder() | |
| .setStreamId(streamId) | |
| .setSequenceNumber(firstUnacked) | |
| .build()); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Trivial implementation of {@link PublishBuildEventImplBase} that can insert sleeps at critical | |
| * junctures. | |
| */ | |
| private static final class DelayingPublishBuildEventService extends PublishBuildEventImplBase { | |
| @GuardedBy("this") | |
| private Duration delayBeforeClosingStream = Duration.ZERO; | |
| @GuardedBy("this") | |
| private Duration delayBeforeHalfClosingStream = Duration.ZERO; | |
| @GuardedBy("this") | |
| @Nullable | |
| private String errorMessage = null; | |
| /** | |
| * Synchronizing this method can lead to deadlocks -- it calls into {@link | |
| * io.grpc.inprocess.InProcessTransport} which takes a locks on itself. Opposite order of locks | |
| * happens for {@link #publishBuildToolEventStream} called while holding the lock on {@link | |
| * io.grpc.inprocess.InProcessTransport}. | |
| */ | |
| @Override | |
| public void publishLifecycleEvent( | |
| PublishLifecycleEventRequest request, StreamObserver<Empty> responseObserver) { | |
| RequestMetadata metadata = TracingMetadataUtils.fromCurrentContext(); | |
| assertThat(metadata.getToolInvocationId()).isNotEmpty(); | |
| assertThat(metadata.getCorrelatedInvocationsId()).isNotEmpty(); | |
| assertThat(metadata.getActionId()).isEqualTo("publish_lifecycle_event"); | |
| responseObserver.onNext(Empty.getDefaultInstance()); | |
| responseObserver.onCompleted(); | |
| } | |
| @Override | |
| public synchronized StreamObserver<PublishBuildToolEventStreamRequest> | |
| publishBuildToolEventStream( | |
| StreamObserver<PublishBuildToolEventStreamResponse> responseObserver) { | |
| RequestMetadata metadata = TracingMetadataUtils.fromCurrentContext(); | |
| assertThat(metadata.getToolInvocationId()).isNotEmpty(); | |
| assertThat(metadata.getCorrelatedInvocationsId()).isNotEmpty(); | |
| assertThat(metadata.getActionId()).isEqualTo("publish_build_tool_event_stream"); | |
| if (errorMessage != null) { | |
| return new ErroringPublishBuildStreamObserver(responseObserver, errorMessage); | |
| } | |
| DelayingPublishBuildStreamObserver observer = | |
| new DelayingPublishBuildStreamObserver( | |
| responseObserver, delayBeforeClosingStream, delayBeforeHalfClosingStream); | |
| observer.startAckingThread(); | |
| return observer; | |
| } | |
| synchronized void setErrorMessage(String errorMessage) { | |
| this.errorMessage = errorMessage; | |
| } | |
| synchronized void setDelayBeforeClosingStream(Duration delay) { | |
| this.delayBeforeClosingStream = delay; | |
| } | |
| synchronized void setDelayBeforeHalfClosingStream(Duration delay) { | |
| this.delayBeforeHalfClosingStream = delay; | |
| } | |
| } | |
| private static final class ErroringPublishBuildStreamObserver | |
| implements StreamObserver<PublishBuildToolEventStreamRequest> { | |
| private final StreamObserver<PublishBuildToolEventStreamResponse> responseObserver; | |
| private final String errorMessage; | |
| ErroringPublishBuildStreamObserver( | |
| StreamObserver<PublishBuildToolEventStreamResponse> responseObserver, String errorMessage) { | |
| this.responseObserver = responseObserver; | |
| this.errorMessage = errorMessage; | |
| } | |
| @Override | |
| public void onNext(PublishBuildToolEventStreamRequest value) { | |
| responseObserver.onNext( | |
| PublishBuildToolEventStreamResponse.newBuilder() | |
| .setStreamId(value.getOrderedBuildEventOrBuilder().getStreamId()) | |
| .setSequenceNumber(value.getOrderedBuildEvent().getSequenceNumber()) | |
| .build()); | |
| } | |
| @Override | |
| public void onError(Throwable t) {} | |
| @Override | |
| public void onCompleted() { | |
| responseObserver.onError( | |
| new StatusRuntimeException(Status.DATA_LOSS.withDescription(errorMessage))); | |
| } | |
| } | |
| } |