diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java index af2020d445a4a6..87f8d5fb2ec284 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java +++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java @@ -198,20 +198,20 @@ public static SkyKey key(Artifact artifact) { return ((DerivedArtifact) artifact).getGeneratingActionKey(); } - public static Iterable keys(Iterable artifacts) { - return artifacts instanceof Collection collection + public static Iterable keys(Iterable artifacts) { + return artifacts instanceof Collection collection ? keys(collection) : Iterables.transform(artifacts, Artifact::key); } - public static Collection keys(Collection artifacts) { - return artifacts instanceof List list + public static Collection keys(Collection artifacts) { + return artifacts instanceof List list ? keys(list) // Use Collections2 instead of Iterables#transform to ensure O(1) size(). : Collections2.transform(artifacts, Artifact::key); } - public static List keys(List artifacts) { + public static List keys(List artifacts) { return Lists.transform(artifacts, Artifact::key); } diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestAttempt.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestAttempt.java index 9153d3e17477b1..4a7aacb72a9e17 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestAttempt.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestAttempt.java @@ -110,6 +110,10 @@ public static TestAttempt forExecutedTestResult( lastAttempt); } + /** + * Creates a test attempt result from cached test data, providing a result while indicating to + * consumers that the test did not actually execute. + */ public static TestAttempt fromCachedTestResult( TestRunnerAction testAction, TestResultData attemptData, @@ -131,6 +135,29 @@ public static TestAttempt fromCachedTestResult( lastAttempt); } + /** + * Creates a test result for rare cases where the test itself was built, but the {@link + * TestRunnerAction} could not be started by a test strategy. + * + *

This overload should be very rarely used, and in particular must not be used by an + * implementation of a {@link TestStrategy}. + */ + public static TestAttempt forUnstartableTestResult( + TestRunnerAction testAction, TestResultData attemptData) { + return new TestAttempt( + false, + testAction, + /* executionInfo= */ BuildEventStreamProtos.TestResult.ExecutionInfo.getDefaultInstance(), + /* attempt= */ 1, + attemptData.getStatus(), + attemptData.getStatusDetails(), + attemptData.getStartTimeMillisEpoch(), + attemptData.getRunDurationMillis(), + /* files= */ ImmutableList.of(), + attemptData.getWarningList(), + /* lastAttempt= */ true); + } + @VisibleForTesting public Artifact getTestStatusArtifact() { return testAction.getCacheStatusArtifact(); @@ -205,7 +232,10 @@ public ImmutableList referencedLocalFiles() { // TODO(b/199940216): Can we populate metadata for these files? localFiles.add( new LocalFile( - file.getSecond(), localFileType, /*artifact=*/ null, /*artifactMetadata=*/ null)); + file.getSecond(), + localFileType, + /* artifact= */ null, + /* artifactMetadata= */ null)); } } return localFiles.build(); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD index 8c9a1594e1fb79..7bf8943a7a049b 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD @@ -356,6 +356,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/util:TestType", "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception", "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code", + "//src/main/java/com/google/devtools/build/lib/util:exit_code", "//src/main/java/com/google/devtools/build/lib/util:heap_offset_helper", "//src/main/java/com/google/devtools/build/lib/util:resource_usage", "//src/main/java/com/google/devtools/build/lib/util:string", @@ -370,6 +371,7 @@ java_library( "//src/main/java/net/starlark/java/syntax", "//src/main/protobuf:failure_details_java_proto", "//src/main/protobuf:memory_pressure_java_proto", + "//src/main/protobuf:test_status_java_proto", "//third_party:auto_value", "//third_party:caffeine", "//third_party:flogger", diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java index 8b482faf3f803a..c31846a0f56871 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java @@ -13,17 +13,28 @@ // limitations under the License. package com.google.devtools.build.lib.skyframe; -import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.ActionLookupData; +import com.google.devtools.build.lib.actions.ActionLookupValue; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.ConfiguredTargetValue; import com.google.devtools.build.lib.analysis.TopLevelArtifactContext; +import com.google.devtools.build.lib.analysis.test.TestAttempt; import com.google.devtools.build.lib.analysis.test.TestProvider; +import com.google.devtools.build.lib.analysis.test.TestResult; +import com.google.devtools.build.lib.analysis.test.TestRunnerAction; import com.google.devtools.build.lib.cmdline.Label; -import com.google.devtools.build.skyframe.GraphTraversingHelper; +import com.google.devtools.build.lib.server.FailureDetails.Execution.Code; +import com.google.devtools.build.lib.util.DetailedExitCode; +import com.google.devtools.build.lib.util.ExitCode; +import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; +import com.google.devtools.build.lib.view.test.TestStatus.TestResultData; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; +import com.google.devtools.build.skyframe.SkyframeLookupResult; +import java.util.List; import javax.annotation.Nullable; /** @@ -39,7 +50,7 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedExcept (TestCompletionValue.TestCompletionKey) skyKey.argument(); ConfiguredTargetKey ctKey = key.configuredTargetKey(); TopLevelArtifactContext ctx = key.topLevelArtifactContext(); - if (env.getValue(TargetCompletionValue.key(ctKey, ctx, /*willTest=*/ true)) == null) { + if (env.getValue(TargetCompletionValue.key(ctKey, ctx, /* willTest= */ true)) == null) { return null; } @@ -58,13 +69,27 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedExcept } } } else { - if (GraphTraversingHelper.declareDependenciesAndCheckIfValuesMissingMaybeWithExceptions( - env, - Iterables.transform( - TestProvider.getTestStatusArtifacts(ct), - Artifact.DerivedArtifact::getGeneratingActionKey))) { + List skyKeys = Artifact.keys(TestProvider.getTestStatusArtifacts(ct)); + SkyframeLookupResult result = env.getValuesAndExceptions(skyKeys); + if (env.valuesMissing()) { return null; } + for (SkyKey actionKey : skyKeys) { + try { + if (result.getOrThrow(actionKey, ActionExecutionException.class) == null) { + return null; + } + } catch (ActionExecutionException e) { + DetailedExitCode detailedExitCode = e.getDetailedExitCode(); + if (detailedExitCode.getExitCode().equals(ExitCode.BUILD_FAILURE) + && ctValue instanceof ActionLookupValue actionLookupValue) { + postTestResultEventsForUnbuildableTestInputs( + env, (ActionLookupData) actionKey, actionLookupValue, detailedExitCode); + } else { + return null; + } + } + } } return TestCompletionValue.TEST_COMPLETION_MARKER; } @@ -73,4 +98,42 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedExcept public String extractTag(SkyKey skyKey) { return Label.print(((ConfiguredTargetKey) skyKey.argument()).getLabel()); } + + /** + * Posts events for test actions that could not run because one or more exec-configuration inputs + * common to all tests failed to build. + * + *

When we run this SkyFunction we will have already built the test executable and its inputs, + * but we might fail to build the exec-configuration attributes providing inputs to the {@link + * TestRunnerAction} such as {@code $test_runtime}, {@code $test_wrapper}, {@code + * test_setup_script} and others (see {@link + * com.google.devtools.build.lib.analysis.BaseRuleClasses.TestBaseRule#build(com.google.devtools.build.lib.packages.RuleClass.Builder, + * com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment)} where all Test-type rules + * have additional attributes added). + * + *

When these exec-configuration inputs cannot be built, we do not get to use any {@code + * TestStrategy} that is responsible for posting {@link TestAttempt} and {@link TestResult} + * events. We need to handle this case here and post minimal events indicating the test {@link + * BlazeTestStatus.FAILED_TO_BUILD FAILED_TO_BUILD}. + */ + private static void postTestResultEventsForUnbuildableTestInputs( + Environment env, + ActionLookupData actionKey, + ActionLookupValue actionLookupValue, + DetailedExitCode detailedExitCode) { + BlazeTestStatus status = BlazeTestStatus.FAILED_TO_BUILD; + if (detailedExitCode + .getFailureDetail() + .getExecution() + .getCode() + .equals(Code.ACTION_NOT_UP_TO_DATE)) { + status = BlazeTestStatus.NO_STATUS; + } + TestRunnerAction testRunnerAction = + (TestRunnerAction) actionLookupValue.getAction(actionKey.getActionIndex()); + TestResultData testData = TestResultData.newBuilder().setStatus(status).build(); + env.getListener().post(TestAttempt.forUnstartableTestResult(testRunnerAction, testData)); + env.getListener() + .post(new TestResult(testRunnerAction, testData, /* cached= */ false, detailedExitCode)); + } } diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java index 9d0744be74a18b..a8fb8d055b59f7 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java @@ -484,6 +484,50 @@ def _impl(ctx): ) """); + // Create fake, minimal implementations of test-setup.sh and test-xml-generator.sh for test + // cases that actually execute tests. Does not support coverage, interruption, signals, etc. + // For proper test execution support, the actual test-setup.sh will need to be included in the + // Java test's runfiles and copied/symlinked into the MockToolsConfig's workspace. + config + .create( + "embedded_tools/tools/test/test-setup.sh", + """ + #!/bin/bash + set -e + function is_absolute { + [[ "$1" = /* ]] || [[ "$1" =~ ^[a-zA-Z]:[/\\].* ]] + } + is_absolute "$TEST_SRCDIR" || TEST_SRCDIR="$PWD/$TEST_SRCDIR" + RUNFILES_MANIFEST_FILE="${TEST_SRCDIR}/MANIFEST" + cd ${TEST_SRCDIR} + function rlocation() { + if is_absolute "$1" ; then + # If the file path is already fully specified, simply return it. + echo "$1" + elif [[ -e "$TEST_SRCDIR/$1" ]]; then + # If the file exists in the $TEST_SRCDIR then just use it. + echo "$TEST_SRCDIR/$1" + elif [[ -e "$RUNFILES_MANIFEST_FILE" ]]; then + # If a runfiles manifest file exists then use it. + echo "$(grep "^$1 " "$RUNFILES_MANIFEST_FILE" | sed 's/[^ ]* //')" + fi + } + + EXE="${1#./}" + shift + + if is_absolute "$EXE"; then + TEST_PATH="$EXE" + else + TEST_PATH="$(rlocation $TEST_WORKSPACE/$EXE)" + fi + exec $TEST_PATH + """) + .chmod(0755); + config + .create("embedded_tools/tools/test/test-xml-generator.sh", "#!/bin/sh", "cp \"$1\" \"$2\"") + .chmod(0755); + // Use an alias package group to allow for modification at the simpler path config.create( "embedded_tools/tools/allowlists/config_feature_flag/BUILD", diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD index d17b529f9392fb..eebef3d9aba5a9 100644 --- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD +++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD @@ -559,6 +559,30 @@ java_test( ], ) +java_test( + name = "TargetSummaryEventTest", + srcs = ["TargetSummaryEventTest.java"], + data = ["//src/test/java/com/google/devtools/build/lib:embedded_scripts"], + tags = [ + "no_windows", + ], + deps = [ + "//src/main/java/com/google/devtools/build/lib:runtime", + "//src/main/java/com/google/devtools/build/lib/actions", + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper:credential_module", + "//src/main/java/com/google/devtools/build/lib/buildeventservice", + "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto", + "//src/main/java/com/google/devtools/build/lib/vfs", + "//src/test/java/com/google/devtools/build/lib/analysis/util", + "//src/test/java/com/google/devtools/build/lib/buildtool/util", + "//src/test/java/com/google/devtools/build/lib/testutil", + "//third_party:guava", + "//third_party:jsr305", + "//third_party:junit4", + "//third_party:truth", + ], +) + java_test( name = "BuildResultTestCase", srcs = ["BuildResultTestCase.java"], diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/TargetSummaryEventTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/TargetSummaryEventTest.java new file mode 100644 index 00000000000000..c02265bb6260a0 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/buildtool/TargetSummaryEventTest.java @@ -0,0 +1,224 @@ +// Copyright 2024 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.buildtool; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.BuildFailedException; +import com.google.devtools.build.lib.analysis.util.AnalysisMock; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; +import com.google.devtools.build.lib.buildeventservice.BazelBuildEventServiceModule; +import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos; +import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent; +import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.IdCase; +import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TestStatus; +import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; +import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher; +import com.google.devtools.build.lib.runtime.BlazeRuntime; +import com.google.devtools.build.lib.runtime.NoSpawnCacheModule; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration test verifying behavior of {@code + * com.google.devtools.build.lib.runtime.TargetSummaryEvent} event. + */ +@RunWith(JUnit4.class) +public class TargetSummaryEventTest extends BuildIntegrationTestCase { + + @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Before + public void stageEmbeddedTools() throws Exception { + AnalysisMock.get().setupMockToolsRepository(mockToolsConfig); + } + + @Override + protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception { + return super.getRuntimeBuilder() + .addBlazeModule(new NoSpawnCacheModule()) + .addBlazeModule(new CredentialModule()) + .addBlazeModule(new BazelBuildEventServiceModule()); + } + + private void afterBuildCommand() throws Exception { + runtimeWrapper.newCommand(); + } + + @Test + public void plainTarget_buildSuccess() throws Exception { + write("foo/BUILD", "genrule(name = 'foobin', outs = ['out.txt'], cmd = 'echo -n Hello > $@')"); + + File bep = buildTargetAndCaptureBuildEventProtocol("//foo:foobin"); + + BuildEventStreamProtos.TargetSummary summary = findTargetSummaryEventInBuildEventStream(bep); + assertThat(summary.getOverallBuildSuccess()).isTrue(); + assertThat(summary.getOverallTestStatus()).isEqualTo(TestStatus.NO_STATUS); + } + + @Test + public void plainTarget_buildFails() throws Exception { + write("foo/BUILD", "genrule(name = 'foobin', outs = ['out.txt'], cmd = 'false')"); + + File bep = buildFailingTargetAndCaptureBuildEventProtocol("//foo:foobin"); + + BuildEventStreamProtos.TargetSummary summary = findTargetSummaryEventInBuildEventStream(bep); + assertThat(summary.getOverallBuildSuccess()).isFalse(); + assertThat(summary.getOverallTestStatus()).isEqualTo(TestStatus.NO_STATUS); + } + + @Test + public void test_buildSucceeds_testSucceeds() throws Exception { + write("foo/good_test.sh", "#!/bin/bash", "true").setExecutable(true); + write("foo/BUILD", "sh_test(name = 'good_test', srcs = ['good_test.sh'])"); + + File bep = testTargetAndCaptureBuildEventProtocol("//foo:good_test"); + + BuildEventStreamProtos.TargetSummary summary = findTargetSummaryEventInBuildEventStream(bep); + assertThat(summary.getOverallBuildSuccess()).isTrue(); + assertThat(summary.getOverallTestStatus()).isEqualTo(TestStatus.PASSED); + } + + @Test + public void test_buildSucceeds_testFails() throws Exception { + write("foo/bad_test.sh", "#!/bin/bash", "false").setExecutable(true); + write("foo/BUILD", "sh_test(name = 'bad_test', srcs = ['bad_test.sh'])"); + + File bep = testTargetAndCaptureBuildEventProtocol("//foo:bad_test"); + + BuildEventStreamProtos.TargetSummary summary = findTargetSummaryEventInBuildEventStream(bep); + assertThat(summary.getOverallBuildSuccess()).isTrue(); + assertThat(summary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED); + } + + @Test + public void test_buildSucceeds_testRuntimeFailsToBuild() throws Exception { + write("foo/good_test.sh", "#!/bin/bash", "true").setExecutable(true); + write("foo/BUILD", "sh_test(name = 'good_test', srcs = ['good_test.sh'])"); + + // Hack: the path to the tools/test/BUILD file is prefixed in the Bazel tests. + String pathToToolsTestBuildPrefix = AnalysisMock.get().isThisBazel() ? "embedded_tools/" : ""; + Path toolsTestBuildPath = + mockToolsConfig.getPath(pathToToolsTestBuildPrefix + "tools/test/BUILD"); + // Delete the test-setup.sh file and introduce a broken genrule to create test-setup.sh. + mockToolsConfig.getPath(pathToToolsTestBuildPrefix + "tools/test/test-setup.sh").delete(); + String bogusTestSetupGenrule = + """ + genrule( + name = 'bogus-make-test-setup', + outs = ['test-setup.sh'], + cmd = 'false', + ) + """; + FileSystemUtils.appendIsoLatin1(toolsTestBuildPath, bogusTestSetupGenrule); + + File bep = testTargetAndCaptureBuildEventProtocol("//foo:good_test"); + + BuildEventStreamProtos.TargetSummary summary = findTargetSummaryEventInBuildEventStream(bep); + assertThat(summary.getOverallBuildSuccess()).isTrue(); + assertThat(summary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED_TO_BUILD); + } + + private File buildTargetAndCaptureBuildEventProtocol(String target) throws Exception { + File bep = tmpFolder.newFile(); + // We use WAIT_FOR_UPLOAD_COMPLETE because it's the easiest way to force the BES module to + // wait until the BEP binary file has been written. + addOptions( + "--keep_going", + "--experimental_bep_target_summary", + "--build_event_binary_file=" + bep.getAbsolutePath(), + "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE"); + buildTarget(target); + // 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(); + return bep; + } + + private File buildFailingTargetAndCaptureBuildEventProtocol(String target) throws Exception { + File bep = tmpFolder.newFile(); + // We use WAIT_FOR_UPLOAD_COMPLETE because it's the easiest way to force the BES module to + // wait until the BEP binary file has been written. + addOptions( + "--keep_going", + "--experimental_bep_target_summary", + "--build_event_binary_file=" + bep.getAbsolutePath(), + "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE"); + assertThrows(BuildFailedException.class, () -> buildTarget(target)); + // 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(); + return bep; + } + + private File testTargetAndCaptureBuildEventProtocol(String target) throws Exception { + File bep = tmpFolder.newFile(); + BlazeCommandDispatcher dispatcher = new BlazeCommandDispatcher(getRuntime()); + ImmutableList.Builder args = ImmutableList.builder(); + args.add("test", target); + args.addAll(getDefaultBlazeTestArguments()); + // We use WAIT_FOR_UPLOAD_COMPLETE because it's the easiest way to force the BES module to + // wait until the BEP binary file has been written. + args.add( + "--keep_going", + "--client_env=PATH=/bin:/usr/bin:/usr/sbin:/sbin", + "--experimental_bep_target_summary", + "--build_event_binary_file=" + bep.getAbsolutePath(), + "--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE"); + dispatcher.exec(args.build(), /* clientDescription= */ "test", outErr); + return bep; + } + + protected List getDefaultBlazeTestArguments() { + return BlazeTestUtils.makeArgs("--default_visibility=public", "--test_output=all"); + } + + private static ImmutableList parseBuildEventsFromBuildEventStream(File bep) + throws IOException { + ImmutableList.Builder buildEvents = ImmutableList.builder(); + try (InputStream in = new FileInputStream(bep)) { + BuildEvent ev; + while ((ev = BuildEvent.parseDelimitedFrom(in)) != null) { + buildEvents.add(ev); + } + } + return buildEvents.build(); + } + + @Nullable + private static BuildEventStreamProtos.TargetSummary findTargetSummaryEventInBuildEventStream( + File bep) throws IOException { + for (BuildEvent buildEvent : parseBuildEventsFromBuildEventStream(bep)) { + if (buildEvent.getId().getIdCase() == IdCase.TARGET_SUMMARY) { + return buildEvent.getTargetSummary(); + } + } + return null; + } +}