blob: 9ffd40e6a94498ab8ee5b9769633f9e09c10a455 [file] [log] [blame]
// 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.buildtool;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.Subscribe;
import com.google.common.hash.HashCode;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CompletionContext;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FileStateType;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
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.NamedSetOfFiles;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.NoSpawnCacheModule;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
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;
/** Verifies TargetCompleteEvent behavior during a complete build. */
@RunWith(JUnit4.class)
public final class TargetCompleteEventTest 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();
}
/**
* Validates that TargetCompleteEvents do not keep a map of action output metadata for the
* _validation output group, which can be quite large.
*/
@Test
public void artifactsNotRetained() throws Exception {
write(
"validation_actions/defs.bzl",
"def _rule_with_implicit_outs_and_validation_impl(ctx):",
"",
" ctx.actions.write(ctx.outputs.main, \"main output\\n\")",
"",
" ctx.actions.write(ctx.outputs.implicit, \"implicit output\\n\")",
"",
" validation_output = ctx.actions.declare_file(ctx.attr.name + \".validation\")",
" # The actual tool will be created in individual tests, depending on whether",
" # validation should pass or fail.",
" ctx.actions.run(",
" outputs = [validation_output],",
" executable = ctx.executable._validation_tool,",
" arguments = [validation_output.path])",
"",
" return [",
" DefaultInfo(files = depset([ctx.outputs.main])),",
" OutputGroupInfo(_validation = depset([validation_output])),",
" ]",
"",
"",
"rule_with_implicit_outs_and_validation = rule(",
" implementation = _rule_with_implicit_outs_and_validation_impl,",
" outputs = {",
" \"main\": \"%{name}.main\",",
" \"implicit\": \"%{name}.implicit\",",
" },",
" attrs = {",
" \"_validation_tool\": attr.label(",
" allow_single_file = True,",
" default = Label(\"//validation_actions:validation_tool\"),",
" executable = True,",
" cfg = \"host\"),",
" }",
")");
write("validation_actions/validation_tool", "#!/bin/bash", "echo \"validation output\" > $1")
.setExecutable(true);
write(
"validation_actions/BUILD",
"load(",
" \":defs.bzl\",",
" \"rule_with_implicit_outs_and_validation\")",
"",
"rule_with_implicit_outs_and_validation(name = \"foo0\")");
AtomicReference<TargetCompleteEvent> targetCompleteEventRef = new AtomicReference<>();
runtimeWrapper.registerSubscriber(
new Object() {
@SuppressWarnings("unused")
@Subscribe
public void accept(TargetCompleteEvent event) {
targetCompleteEventRef.set(event);
}
});
addOptions("--experimental_run_validations");
BuildResult buildResult = buildTarget("//validation_actions:foo0");
Collection<ConfiguredTarget> successfulTargets = buildResult.getSuccessfulTargets();
ConfiguredTarget fooTarget = Iterables.getOnlyElement(successfulTargets);
// Check that the primary output, :foo0.main, has its metadata retained and the
// CompletionContext can confirm it is an output file.
Artifact main =
((RuleConfiguredTarget) fooTarget)
.getArtifactByOutputLabel(
Label.parseAbsoluteUnchecked("//validation_actions:foo0.main"));
FileStateType mainType =
targetCompleteEventRef.get().getCompletionContext().getFileArtifactValue(main).getType();
assertThat(CompletionContext.isGuaranteedToBeOutputFile(mainType)).isTrue();
// Check that the validation output, :foo0.validation, does not have its metadata retained and
// the CompletionContext cannot confirm it is an output file (even though it is).
OutputGroupInfo outputGroups = fooTarget.get(OutputGroupInfo.STARLARK_CONSTRUCTOR);
NestedSet<Artifact> validationArtifacts =
outputGroups.getOutputGroup(OutputGroupInfo.VALIDATION);
assertThat(validationArtifacts.isEmpty()).isFalse();
Artifact validationArtifact = Iterables.getOnlyElement(validationArtifacts.toList());
FileArtifactValue validationArtifactMetadata =
targetCompleteEventRef
.get()
.getCompletionContext()
.getFileArtifactValue(validationArtifact);
assertThat(validationArtifactMetadata).isNull();
}
@Test
public void digestAndLengthInBuildEventProtocol() throws Exception {
// Produces a TargetCompleteEvent in BEP and verifies that we include the output file's
// length and digest.
write(
"foo/BUILD",
"genrule(name = 'foobin', outs = ['out.txt'], cmd = 'echo -n \"Hello\" > $@')");
File buildEventBinaryFile = 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(
"--build_event_binary_file=" + buildEventBinaryFile.getAbsolutePath(),
"--bes_upload_mode=WAIT_FOR_UPLOAD_COMPLETE");
buildTarget("//foo:foobin");
// 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));
}
}
BuildEventStreamProtos.File outFile = findOutputFileInBEPStream(buildEvents);
assertThat(outFile).isNotNull();
assertThat(outFile.getLength()).isEqualTo("Hello".length());
byte[] bepDigest = BaseEncoding.base16().lowerCase().decode(outFile.getDigest());
// Try all registered hash functions and verify that one of them was used to produce the digest.
boolean foundHashFunction = false;
for (DigestHashFunction hashFunction : DigestHashFunction.getPossibleHashFunctions()) {
HashCode hashCode =
hashFunction.getHashFunction().hashString("Hello", StandardCharsets.UTF_8);
if (Arrays.equals(bepDigest, hashCode.asBytes())) {
foundHashFunction = true;
}
}
assertThat(foundHashFunction).isTrue();
}
@Nullable
private static BuildEventStreamProtos.File findOutputFileInBEPStream(
List<BuildEvent> buildEvents) {
for (BuildEvent buildEvent : buildEvents) {
if (buildEvent.getId().getIdCase() == IdCase.NAMED_SET) {
NamedSetOfFiles namedSetOfFiles = buildEvent.getNamedSetOfFiles();
for (BuildEventStreamProtos.File file : namedSetOfFiles.getFilesList()) {
if (file.getName().contains("out.txt")) {
return file;
}
}
}
}
return null;
}
}