blob: 71ba213b5af13803bd8b3fc5c3a6d2d287135941 [file] [log] [blame]
// Copyright 2020 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 com.google.devtools.build.lib.testutil.MoreAsserts.assertNoEvents;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.MutableActionGraph;
import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TargetCompletedId;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
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.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.vfs.Path;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for action conflicts. */
@RunWith(TestParameterInjector.class)
public class OutputArtifactConflictTest extends BuildIntegrationTestCase {
static class AnalysisFailureEventListener extends BlazeModule {
private final List<TargetCompletedId> eventIds = new ArrayList<>();
private final List<String> failedTargetNames = new ArrayList<>();
@Override
public void beforeCommand(CommandEnvironment env) {
env.getEventBus().register(this);
}
@Subscribe
public void onAnalysisFailure(AnalysisFailureEvent event) {
eventIds.add(event.getEventId().getTargetCompleted());
failedTargetNames.add(event.getFailedTarget().getLabel().toString());
}
}
private final AnalysisFailureEventListener eventListener = new AnalysisFailureEventListener();
@Override
protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
return super.getRuntimeBuilder().addBlazeModule(eventListener);
}
private void writeConflictBzl() throws IOException {
write(
"foo/conflict.bzl",
"def _conflict_impl(ctx):",
" inputs = depset(",
" ctx.files.srcs, transitive = [dep[DefaultInfo].files for dep in ctx.attr.deps])",
" conflict_output = ctx.actions.declare_file('conflict_output')",
" other = ctx.actions.declare_file('other' + ctx.attr.name)",
" ctx.actions.run_shell(",
" inputs = inputs,",
" outputs = [conflict_output, other],",
" command = 'touch %s %s' % (conflict_output.path, other.path)",
" )",
" return [DefaultInfo(files = depset([conflict_output, other]))]",
"",
"my_rule = rule(",
" implementation=_conflict_impl,",
" attrs = {",
" 'srcs': attr.label_list(allow_files = True),",
" 'deps': attr.label_list(providers = [DefaultInfo]),",
" }",
")");
}
/**
* Builds the provided targets and asserts expected exceptions.
*
* @return the exit code extracted from the failure detail.
*/
private Code assertThrowsExceptionWhenBuildingTargets(boolean keepGoing, String... targets) {
FailureDetail failureDetail =
keepGoing
? assertThrows(BuildFailedException.class, () -> buildTarget(targets))
.getDetailedExitCode()
.getFailureDetail()
: assertThrows(ViewCreationFailedException.class, () -> buildTarget(targets))
.getFailureDetail();
return Preconditions.checkNotNull(failureDetail).getAnalysis().getCode();
}
@Test
public void testArtifactPrefix(
@TestParameter boolean keepGoing,
@TestParameter boolean modifyBuildFile,
@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
if (modifyBuildFile) {
write("x/BUILD", "genrule(name = 'y', outs = ['not_y'], cmd = 'touch $@')");
buildTarget("//x:y", "//x/y:y");
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
} else {
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
buildTarget("//x/y:y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y:y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
if (keepGoing) {
assertThat(eventListener.failedTargetNames).containsExactly("//x:y", "//x/y:y");
} else {
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x/y:y");
}
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
}
@Test
public void testAspectArtifactSharesPrefixWithTargetArtifact(
@TestParameter boolean keepGoing,
@TestParameter boolean modifyBuildFile,
@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
if (modifyBuildFile) {
write("x/BUILD", "genrule(name = 'y', outs = ['y.out'], cmd = 'touch $@')");
} else {
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
}
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write(
"x/aspect.bzl",
"def _aspect_impl(target, ctx):",
" if not getattr(ctx.rule.attr, 'outs', None):",
" return struct(output_groups = {})",
" conflict_outputs = list()",
" for out in ctx.rule.attr.outs:",
" if out.name[1:] == '.bad':",
" aspect_out = ctx.actions.declare_file(out.name[:1])",
" conflict_outputs.append(aspect_out)",
" cmd = 'echo %s > %s' % (out.name, aspect_out.path)",
" ctx.actions.run_shell(",
" outputs = [aspect_out],",
" command = cmd,",
" )",
" return [OutputGroupInfo(",
" files = depset(conflict_outputs)",
" )]",
"",
"my_aspect = aspect(implementation = _aspect_impl)");
if (modifyBuildFile) {
buildTarget("//x/y", "//x:y");
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
} else {
buildTarget("//x/y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
// As we have --output_groups=file, the CTs won't actually be built. Only the
// AnalysisFailureEvent from Aspect(//x:y) is expected even though there are 2 conflicting
// actions.
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.failedTargetNames).containsExactly("//x:y");
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
}
@Test
public void testAspectArtifactPrefix(
@TestParameter boolean keepGoing,
@TestParameter boolean modifyBuildFile,
@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
if (modifyBuildFile) {
write(
"x/BUILD",
"genrule(name = 'y', outs = ['y.out'], cmd = 'touch $@')",
"genrule(name = 'ydir', outs = ['y.dir'], cmd = 'touch $@')");
} else {
write(
"x/BUILD",
"genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')",
"genrule(name = 'ydir', outs = ['y.dir'], cmd = 'touch $@')");
}
write(
"x/aspect.bzl",
"def _aspect_impl(target, ctx):",
" if not getattr(ctx.rule.attr, 'outs', None):",
" return struct(output_groups = {})",
" conflict_outputs = list()",
" for out in ctx.rule.attr.outs:",
" if out.name[1:] == '.bad':",
" aspect_out = ctx.actions.declare_file(out.name[:1])",
" conflict_outputs.append(aspect_out)",
" cmd = 'echo %s > %s' % (out.name, aspect_out.path)",
" ctx.actions.run_shell(",
" outputs = [aspect_out],",
" command = cmd,",
" )",
" elif out.name[1:] == '.dir':",
" aspect_out = ctx.actions.declare_file(out.name[:1] + '/' + out.name)",
" conflict_outputs.append(aspect_out)",
" out_dir = aspect_out.path[:len(aspect_out.path) - len(out.name) + 1]",
" cmd = 'mkdir %s && echo %s > %s' % (out_dir, out.name, aspect_out.path)",
" ctx.actions.run_shell(",
" outputs = [aspect_out],",
" command = cmd,",
" )",
" return [OutputGroupInfo(",
" files = depset(conflict_outputs)",
" )]",
"",
"my_aspect = aspect(implementation = _aspect_impl)");
if (modifyBuildFile) {
buildTarget("//x:y", "//x:ydir");
write(
"x/BUILD",
"genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')",
"genrule(name = 'ydir', outs = ['y.dir'], cmd = 'touch $@')");
} else {
buildTarget("//x:y");
}
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x:ydir", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("bin/x/y' (belonging to //x:y)");
events.assertContainsError("bin/x/y/y.dir' (belonging to //x:ydir)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
if (keepGoing) {
assertThat(eventListener.failedTargetNames).containsExactly("//x:y", "//x:ydir");
} else {
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x:ydir");
}
}
@Test
public void testInvalidatedConflict(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
write("foo/BUILD", "load('//foo:conflict.bzl', 'my_rule')", "my_rule(name = 'first')");
events.clear();
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
}
@Test
public void testNewTargetConflict(
@TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--keep_going=" + keepGoing);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(keepGoing, "//foo:first", "//foo:second");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ACTION_CONFLICT);
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
assertThat(eventListener.failedTargetNames).hasSize(1);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void testTwoOverlappingBuildsHasNoConflict(
@TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--keep_going=" + keepGoing);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
// Verify that together they fail, even though no new targets have been analyzed
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(keepGoing, "//foo:first", "//foo:second");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ACTION_CONFLICT);
events.clear();
// Verify that they still don't fail individually, so no state remains
buildTarget("//foo:first");
events.assertNoWarningsOrErrors();
buildTarget("//foo:second");
events.assertNoWarningsOrErrors();
}
@Test
public void testFailingTargetsDoNotCauseActionConflicts(
@TestParameter boolean mergedAnalysisExecution) throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
write(
"x/bad_rule.bzl",
"def _impl(ctx):",
" return list().this_method_does_not_exist()",
"bad_rule = rule(_impl, attrs = {'deps': attr.label_list()})");
write(
"x/BUILD",
"load('//x:bad_rule.bzl', 'bad_rule')",
"cc_binary(name = 'y', srcs = ['y.cc'], malloc = '//base:system_malloc')",
"bad_rule(name = 'bad', deps = [':y'])");
write("x/y/y.cc", "");
write("x/y/BUILD", "cc_library(name = 'y', srcs=['y.cc'])");
write("x/y.cc", "int main() { return 0; }");
runtimeWrapper.addOptions("--keep_going");
try {
buildTarget("//x:y", "//x/y");
fail();
} catch (ViewCreationFailedException e) {
fail("Unexpected artifact prefix conflict: " + e);
} catch (BuildFailedException e) {
// Expected.
}
}
// Regression test for b/184944522.
@Test
public void testConflictErrorAndAnalysisError(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
write("x/BUILD", "sh_library(name = 'x', deps = ['//y:y'])");
write("y/BUILD", "sh_library(name = 'y', visibility = ['//visibility:private'])");
addOptions("--keep_going");
assertThrows(
BuildFailedException.class, () -> buildTarget("//x:x", "//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
// When targets have conflicting artifacts, one of them "wins" and is successfully built. All
// of the other targets with conflicting artifacts fail.
assertThat(eventListener.failedTargetNames).contains("//x:x");
assertThat(eventListener.failedTargetNames).hasSize(2);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
// Verify that an aspect whose analysis is unfinished doesn't fail the conflict reporting process.
@Test
public void testConflictErrorAndUnfinishedAspectAnalysis_mergedAnalysisExecution(
@TestParameter boolean keepGoing) throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution");
addOptions("--keep_going=" + keepGoing);
write(
"x/aspect.bzl",
"def _aspect_impl(target, ctx):",
" if not getattr(ctx.rule.attr, 'outs', None):",
" return struct(output_groups = {})",
" conflict_outputs = list()",
" for out in ctx.rule.attr.outs:",
" if out.name[1:] == '.bad':",
" aspect_out = ctx.actions.declare_file(out.name[:1])",
" conflict_outputs.append(aspect_out)",
" cmd = 'echo %s > %s' % (out.name, aspect_out.path)",
" ctx.actions.run_shell(",
" outputs = [aspect_out],",
" command = cmd,",
" )",
" return [OutputGroupInfo(",
" files = depset(conflict_outputs)",
" )]",
"",
"my_aspect = aspect(implementation = _aspect_impl)");
write(
"x/BUILD",
"genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')",
"sh_library(name = 'fail_analysis', deps = ['//private:y'])");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write("private/BUILD", "sh_library(name = 'y', visibility = ['//visibility:private'])");
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(
keepGoing, "//x/y:y", "//x:y", "//x:fail_analysis");
if (keepGoing) {
assertThat(errorCode).isEqualTo(Code.NOT_ALL_TARGETS_ANALYZED);
events.assertContainsError(
"One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
events.assertContainsError("Analysis of target '//x:fail_analysis' failed");
assertThat(eventListener.failedTargetNames).containsExactly("//x:y", "//x:fail_analysis");
} else {
assertThat(errorCode)
.isAnyOf(Code.ARTIFACT_PREFIX_CONFLICT, Code.CONFIGURED_VALUE_CREATION_FAILED);
assertThat(eventListener.failedTargetNames).containsAnyOf("//x:y", "//x:fail_analysis");
}
}
// This test is documenting current behavior more than enforcing a contract: it might be ok for
// Bazel to suppress the error message about an action conflict, since the relevant actions are
// not run in this build. However, that might cause problems for users who aren't immediately
// alerted when they introduce an action conflict. We already skip exhaustive checks for action
// conflicts in the name of performance and that has prompted complaints, so suppressing actual
// conflicts seems like a bad idea.
//
// While this test is written with aspects, any actions that generate conflicting outputs but
// aren't run would exhibit this behavior.
@Test
public void unusedActionsStillConflict() throws Exception {
// TODO(b/245923465) Limitation with Skymeld.
addOptions("--noexperimental_merged_skyframe_analysis_execution");
write(
"foo/aspect.bzl",
"def _aspect1_impl(target, ctx):",
" outfile = ctx.actions.declare_file('aspect.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for aspect 1',",
" command = 'echo \"1\" > ' + outfile.path,",
" )",
" return [OutputGroupInfo(files1 = [outfile])]",
"",
"def _aspect2_impl(target, ctx):",
" outfile = ctx.actions.declare_file('aspect.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for aspect 2',",
" command = 'echo \"2\" > ' + outfile.path,",
" )",
" return [OutputGroupInfo(files2 = [outfile])]",
"",
"def _rule_impl(ctx):",
" outfile = ctx.actions.declare_file('file.out')",
" ctx.actions.run_shell(",
" outputs = [outfile],",
" progress_message = 'Action for target',",
" command = 'touch ' + outfile.path,",
" )",
" return [DefaultInfo(files = depset([outfile]))]",
"aspect1 = aspect(implementation = _aspect1_impl)",
"aspect2 = aspect(implementation = _aspect2_impl)",
"",
"bad_rule = rule(implementation = _rule_impl, attrs = {'deps' : attr.label_list(aspects ="
+ " [aspect1, aspect2])})");
write(
"foo/BUILD",
"load('//foo:aspect.bzl', 'bad_rule')",
"sh_library(name = 'dep', srcs = ['dep.sh'])",
"bad_rule(name = 'foo', deps = [':dep'])");
addOptions("--keep_going");
// If Bazel decides to permit this scenario, the build should succeed instead of throwing here.
BuildFailedException buildFailedException =
assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
assertThat(buildFailedException)
.hasMessageThat()
.contains("command succeeded, but not all targets were analyzed");
// We successfully built the output file despite the supposed failure.
Iterable<Artifact> artifacts = getArtifacts("//foo:foo");
assertThat(artifacts).hasSize(1);
assertThat(Iterables.getOnlyElement(artifacts).getPath().exists()).isTrue();
assertThat(
buildFailedException.getDetailedExitCode().getFailureDetail().getAnalysis().getCode())
.isEqualTo(FailureDetails.Analysis.Code.NOT_ALL_TARGETS_ANALYZED);
events.assertContainsError("file 'foo/aspect.out' is generated by these conflicting actions:");
events.assertContainsError(
Pattern.compile(
"Aspects: \\[//foo:aspect.bzl%aspect[12]], \\[//foo:aspect.bzl%aspect[12]]"));
}
@Test
public void testMultipleConflictErrors(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
addOptions("--keep_going");
assertThrows(
BuildFailedException.class,
() -> buildTarget("//x/y", "//x:y", "//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
// When targets have conflicting artifacts, one of them "wins" and is successfully built. All
// of the other targets with conflicting artifacts fail.
assertThat(eventListener.failedTargetNames).containsAtLeast("//x:y", "//x/y:y");
assertThat(eventListener.failedTargetNames).hasSize(3);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void repeatedConflictBuild(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
ViewCreationFailedException e =
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(e)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(MutableActionGraph.ActionConflictException.class);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
eventListener.failedTargetNames.clear();
e =
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
assertThat(e)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(MutableActionGraph.ActionConflictException.class);
assertThat(eventListener.failedTargetNames).containsAnyOf("//foo:first", "//foo:second");
}
@Test
public void testConflictAfterNullBuild(
@TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution)
throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--aspects=//x:aspect.bzl%my_aspect", "--output_groups=files");
addOptions("--keep_going=" + keepGoing);
write("x/BUILD", "genrule(name = 'y', outs = ['y.out'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
write(
"x/aspect.bzl",
"def _aspect_impl(target, ctx):",
" if not getattr(ctx.rule.attr, 'outs', None):",
" return struct(output_groups = {})",
" conflict_outputs = list()",
" for out in ctx.rule.attr.outs:",
" if out.name[1:] == '.bad':",
" aspect_out = ctx.actions.declare_file(out.name[:1])",
" conflict_outputs.append(aspect_out)",
" cmd = 'echo %s > %s' % (out.name, aspect_out.path)",
" ctx.actions.run_shell(",
" outputs = [aspect_out],",
" command = cmd,",
" )",
" return [OutputGroupInfo(",
" files = depset(conflict_outputs)",
" )]",
"",
"my_aspect = aspect(implementation = _aspect_impl)");
// First build: no conflict expected.
buildTarget("//x/y", "//x:y");
// Null build
buildTarget("//x/y", "//x:y");
assertNoEvents(events.errors());
assertThat(eventListener.failedTargetNames).isEmpty();
// Modify BUILD file to introduce a conflict.
write("x/BUILD", "genrule(name = 'y', outs = ['y.bad'], cmd = 'touch $@')");
Code errorCode = assertThrowsExceptionWhenBuildingTargets(keepGoing, "//x/y", "//x:y");
assertThat(errorCode)
.isEqualTo(keepGoing ? Code.NOT_ALL_TARGETS_ANALYZED : Code.ARTIFACT_PREFIX_CONFLICT);
events.assertContainsError("One of the output paths '" + TestConstants.PRODUCT_NAME + "-out/");
events.assertContainsError("/bin/x/y/whatever' (belonging to //x/y:y)");
events.assertContainsError("/bin/x/y' (belonging to //x:y)");
events.assertContainsError("is a prefix of the other");
assertThat(events.errors()).hasSize(1);
assertThat(eventListener.failedTargetNames).containsExactly("//x:y");
assertThat(eventListener.eventIds.get(0).getAspect()).isEqualTo("//x:aspect.bzl%my_aspect");
}
// There exists a discrepancy between skymeld and noskymeld modes in case of --keep_going.
// noskymeld: bazel would stop at the end of the analysis phase and build nothing.
// skymeld: we either finish building one of the 2 conflicting artifacts, or none at all.
//
// The overall build would still fail in both cases.
@Test
public void testTwoConflictingTargets_keepGoing_behaviorDifferences(
@TestParameter boolean mergedAnalysisExecution) throws Exception {
addOptions("--keep_going");
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
write("x/BUILD", "genrule(name = 'y', outs = ['y'], cmd = 'touch $@')");
write("x/y/BUILD", "genrule(name = 'y', outs = ['whatever'], cmd = 'touch $@')");
Code errorCode =
assertThrowsExceptionWhenBuildingTargets(/*keepGoing=*/ true, "//x:y", "//x/y:y");
Path outputXY = Iterables.getOnlyElement(getArtifacts("//x:y")).getPath();
Path outputXYY = Iterables.getOnlyElement(getArtifacts("//x/y:y")).getPath();
if (mergedAnalysisExecution) {
// Verify that these 2 conflicting artifacts can't both exist.
assertThat(outputXYY.isFile() && outputXY.isFile()).isFalse();
} else {
// Verify that none of the output artifacts were built.
assertThat(outputXY.exists()).isFalse();
assertThat(outputXYY.exists()).isFalse();
}
assertThat(errorCode).isEqualTo(Code.NOT_ALL_TARGETS_ANALYZED);
}
@Test
public void dependencyHasConflict_keepGoing_bothTopLevelTargetsFail(
@TestParameter boolean mergedAnalysisExecution) throws Exception {
addOptions("--keep_going");
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
writeConflictBzl();
write(
"foo/dummy.bzl",
"def _path(file):",
" return file.path",
"def _impl(ctx):",
" inputs = depset(",
" ctx.files.srcs, transitive = [dep[DefaultInfo].files for dep in ctx.attr.deps])",
" output = ctx.actions.declare_file(ctx.attr.name + '.out')",
" command = 'echo $@ > %s' % (output.path)",
" args = ctx.actions.args()",
" args.add_all(inputs, map_each=_path)",
" ctx.actions.run_shell(",
" inputs = inputs,",
" outputs = [output],",
" command = command,",
" arguments = [args]",
" )",
" return [DefaultInfo(files = depset([output]))]",
"",
"dummy = rule(",
" implementation = _impl,",
" attrs = {",
" 'srcs': attr.label_list(allow_files = True),",
" 'deps': attr.label_list(providers = [DefaultInfo]),",
" }",
")");
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"load('//foo:dummy.bzl', 'dummy')",
"my_rule(name = 'conflict_first')",
"my_rule(name = 'conflict_second', deps = [':conflict_first'])",
"dummy(name = 'top_level_a', deps = [':conflict_second'])",
"dummy(name = 'top_level_b', deps = [':conflict_second'])");
assertThrows(
BuildFailedException.class, () -> buildTarget("//foo:top_level_a", "//foo:top_level_b"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
assertThat(eventListener.failedTargetNames)
.containsExactly("//foo:top_level_a", "//foo:top_level_b");
}
@Test
public void conflict_noTrackIncrementalState_detected(
@TestParameter boolean mergedAnalysisExecution) throws Exception {
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--notrack_incremental_state");
writeConflictBzl();
write(
"foo/BUILD",
"load('//foo:conflict.bzl', 'my_rule')",
"my_rule(name = 'first')",
"my_rule(name = 'second')");
assertThrows(
ViewCreationFailedException.class, () -> buildTarget("//foo:first", "//foo:second"));
events.assertContainsError(
"file 'foo/conflict_output' is generated by these conflicting actions:");
}
private void setupStrictConflictChecksTest() throws IOException {
write(
"foo/conflict.bzl",
"def _impl(ctx):",
" dir = ctx.actions.declare_directory(ctx.label.name + '.dir')",
" file = ctx.actions.declare_file(ctx.label.name + '.dir/file.txt')",
" ctx.actions.run_shell(",
" outputs = [dir, file],",
" command = 'mkdir -p $1 && touch $2',",
" arguments = [dir.path, file.path],",
" )",
" return [DefaultInfo(files = depset([dir, file]))]",
"",
"my_rule = rule(implementation = _impl)");
write("foo/BUILD", "load(':conflict.bzl', 'my_rule')", "my_rule(name = 'bar')");
}
@Test
public void laxFollowedByStrictConflictChecks(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
setupStrictConflictChecksTest();
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--noincompatible_strict_conflict_checks");
buildTarget("//foo:bar");
assertNoEvents(events.errors());
addOptions("--incompatible_strict_conflict_checks");
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar"));
events.assertContainsError("One of the output paths");
events.assertContainsError("is a prefix of the other");
}
@Test
public void strictFollowedByLaxConflictChecks(@TestParameter boolean mergedAnalysisExecution)
throws Exception {
setupStrictConflictChecksTest();
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
addOptions("--incompatible_strict_conflict_checks");
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar"));
events.assertContainsError("One of the output paths");
events.assertContainsError("is a prefix of the other");
events.clear();
addOptions("--noincompatible_strict_conflict_checks");
buildTarget("//foo:bar");
assertNoEvents(events.errors());
}
}