blob: 6b97e8cbab1ec162bc0c6a4dc1a14b6785a9849c [file] [log] [blame]
// 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.collect.MoreCollectors.toOptional;
import static com.google.common.truth.Truth.assertThat;
import static java.util.stream.Collectors.joining;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.UserExecException;
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.BuildEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.IdCase;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TargetSummary;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TestStatus;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TestSummary;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.exec.SpawnExecException;
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.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
import com.google.devtools.build.lib.server.FailureDetails.TestAction;
import com.google.devtools.build.lib.skyframe.rewinding.RewindingTestsHelper;
import com.google.devtools.build.lib.testutil.ActionEventRecorder;
import com.google.devtools.build.lib.testutil.SpawnController.ExecResult;
import com.google.devtools.build.lib.testutil.SpawnInputUtils;
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.NoSuchElementException;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.After;
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 final class TargetSummaryEventTest extends BuildIntegrationTestCase {
private static final SpawnResult FAILED_RESULT =
new SpawnResult.Builder()
.setStatus(SpawnResult.Status.NON_ZERO_EXIT)
.setExitCode(1)
.setFailureDetail(
FailureDetail.newBuilder()
.setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.NON_ZERO_EXIT))
.build())
.setRunnerName("remote")
.build();
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
@Rule public final TemporaryFolder tmpFolder = new TemporaryFolder();
private final ActionEventRecorder actionEventRecorder = new ActionEventRecorder();
private final RewindingTestsHelper helper = new RewindingTestsHelper(this, actionEventRecorder);
@Before
public void stageEmbeddedTools() throws Exception {
AnalysisMock.get().setupMockToolsRepository(mockToolsConfig);
}
@After
public void verifyAllSpawnShimsConsumed() {
helper.verifyAllSpawnShimsConsumed();
}
@Override
protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
return super.getRuntimeBuilder()
.addBlazeModule(new NoSpawnCacheModule())
.addBlazeModule(new CredentialModule())
.addBlazeModule(new BazelBuildEventServiceModule())
.addBlazeModule(helper.makeControllableActionStrategyModule("standalone"));
}
@Override
protected void setupOptions() throws Exception {
super.setupOptions();
addOptions("--spawn_strategy=standalone", "--test_strategy=standalone");
runtimeWrapper.registerSubscriber(actionEventRecorder);
}
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");
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");
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",
"load('//test_defs:foo_test.bzl', 'foo_test')",
"foo_test(name = 'good_test', srcs = ['good_test.sh'])");
File bep = testTargetAndCaptureBuildEventProtocol("//foo:good_test");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.PASSED);
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary.getOverallStatus()).isEqualTo(TestStatus.PASSED);
}
@Test
public void test_buildSucceeds_testFails() throws Exception {
write("foo/bad_test.sh", "#!/bin/bash", "false").setExecutable(true);
write(
"foo/BUILD",
"load('//test_defs:foo_test.bzl', 'foo_test')",
"foo_test(name = 'bad_test', srcs = ['bad_test.sh'])");
File bep = testTargetAndCaptureBuildEventProtocol("//foo:bad_test");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED);
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary.getOverallStatus()).isEqualTo(TestStatus.FAILED);
}
@Test
public void test_buildSucceeds_testRuntimeFailsToBuild() throws Exception {
write("foo/good_test.sh", "#!/bin/bash", "true").setExecutable(true);
write(
"foo/BUILD",
"load('//test_defs:foo_test.bzl', 'foo_test')",
"foo_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");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED_TO_BUILD);
// TODO: b/186996003 - TestSummary is a child of TargetComplete and should be posted.
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary).isNull();
}
@Test
public void test_testActionThrowsExecException() throws Exception {
addOptions("--rewind_lost_inputs");
write(
"foo/BUILD",
"""
load("//test_defs:foo_test.bzl", "foo_test")
foo_test(name = "test", srcs = ["test.sh"], tags = ["cpu:invalid"])
""");
write("foo/test.sh", "#!/bin/bash", "true").setExecutable(true);
helper.addSpawnShim(
"Testing //foo:test",
(spawn, context) ->
ExecResult.ofException(
new UserExecException(
FailureDetail.newBuilder()
.setMessage("Invalid cpu tag: 'cpu:invalid'")
.setTestAction(
TestAction.newBuilder().setCode(TestAction.Code.INVALID_CPU_TAG))
.build())));
File bep = testTargetAndCaptureBuildEventProtocol("//foo:test");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED_TO_BUILD);
// TODO: b/186996003 - TestSummary is a child of TargetComplete and should be posted.
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary).isNull();
}
@Test
public void test_testActionLosesInput_rewindingSucceeds() throws Exception {
addOptions("--rewind_lost_inputs");
write(
"foo/BUILD",
"""
load("//test_defs:foo_test.bzl", "foo_test")
foo_test(name = "test", srcs = ["test.sh"], data = [":lost"])
genrule(name = "lost", outs = ["lost.out"], cmd = "echo lost > $@")
""");
write("foo/test.sh", "#!/bin/bash", "true").setExecutable(true);
helper.addSpawnShim(
"Testing //foo:test",
(spawn, context) -> {
Artifact lost = SpawnInputUtils.getRunfilesArtifactWithName(spawn, context, "lost.out");
return helper.createLostInputsExecException(context, lost);
});
File bep = testTargetAndCaptureBuildEventProtocol("//foo:test");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.PASSED);
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary.getOverallStatus()).isEqualTo(TestStatus.PASSED);
assertThat(ImmutableMultiset.copyOf(helper.getExecutedSpawnDescriptions()))
.hasCount("Executing genrule //foo:lost", 2);
}
@Test
public void test_testActionLosesInput_flakyActionFailsAfterRewind() throws Exception {
addOptions("--rewind_lost_inputs");
write(
"foo/BUILD",
"""
load("//test_defs:foo_test.bzl", "foo_test")
foo_test(name = "test", srcs = ["test.sh"], data = [":flaky_lost"])
genrule(name = "flaky_lost", outs = ["flaky_lost.out"], cmd = "echo flaky_lost > $@")
""");
write("foo/test.sh", "#!/bin/bash", "true").setExecutable(true);
helper.addSpawnShim(
"Testing //foo:test",
(spawn, context) -> {
helper.addSpawnShim(
"Executing genrule //foo:flaky_lost",
(spawn2, context2) ->
ExecResult.ofException(
new SpawnExecException(
"Flaky action failure",
FAILED_RESULT,
/* forciblyRunRemotely= */ false,
/* catastrophe= */ false)));
Artifact flakyLost =
SpawnInputUtils.getRunfilesArtifactWithName(spawn, context, "flaky_lost.out");
return helper.createLostInputsExecException(context, flakyLost);
});
File bep = testTargetAndCaptureBuildEventProtocol("//foo:test");
TargetSummary targetSummary = findTargetSummaryEventInBuildEventStream(bep);
assertThat(targetSummary.getOverallBuildSuccess()).isTrue();
assertThat(targetSummary.getOverallTestStatus()).isEqualTo(TestStatus.FAILED_TO_BUILD);
// TODO: b/186996003 - TestSummary is a child of TargetComplete and should be posted.
TestSummary testSummary = findTestSummaryEventInBuildEventStream(bep);
assertThat(testSummary).isNull();
assertThat(ImmutableMultiset.copyOf(helper.getExecutedSpawnDescriptions()))
.hasCount("Executing genrule //foo:flaky_lost", 2);
}
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<String> args = ImmutableList.builder();
args.add("test", target);
args.addAll(runtimeWrapper.getOptions());
// 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(
"--default_visibility=public",
"--test_output=all",
"--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;
}
private static ImmutableList<BuildEvent> parseBuildEventsFromBuildEventStream(File bep)
throws IOException {
ImmutableList.Builder<BuildEvent> buildEvents = ImmutableList.builder();
try (InputStream in = new FileInputStream(bep)) {
BuildEvent ev;
while ((ev = BuildEvent.parseDelimitedFrom(in)) != null) {
buildEvents.add(ev);
}
}
return buildEvents.build();
}
private static TargetSummary findTargetSummaryEventInBuildEventStream(File bep)
throws IOException {
ImmutableList<BuildEvent> events = parseBuildEventsFromBuildEventStream(bep);
Optional<TargetSummary> targetSummary =
events.stream()
.filter(e -> e.getId().getIdCase() == IdCase.TARGET_SUMMARY)
.map(BuildEvent::getTargetSummary)
.collect(toOptional());
if (targetSummary.isEmpty()) {
logger.atSevere().log(
"No TargetSummary event found, dumping BEP:\n%s",
events.stream().map(BuildEvent::toString).collect(joining("\n")));
throw new NoSuchElementException("No TargetSummary event found, see test log for full BEP");
}
return targetSummary.get();
}
@Nullable
private static TestSummary findTestSummaryEventInBuildEventStream(File bep) throws IOException {
return parseBuildEventsFromBuildEventStream(bep).stream()
.filter(e -> e.getId().getIdCase() == IdCase.TEST_SUMMARY)
.map(BuildEvent::getTestSummary)
.collect(toOptional())
.orElse(null);
}
}