| // 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.Ignore; | |
| 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 | |
| @Ignore("b/216490090 - flaky") | |
| 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.contains("//x:y") | |
| ^ eventListener.failedTargetNames.contains("//x:fail_analysis")) | |
| .isTrue(); | |
| } | |
| } | |
| // 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 { | |
| 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 --experimental_merged_skyframe_analysis_execution and | |
| // otherwise in case of --keep_going. The version with merged phases would still build one of the | |
| // 2 conflicting targets, while the one without would stop at the end of the analysis phase and | |
| // build nothing. The overall build would still fail. | |
| @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 one and only one of the output artifacts from these 2 targets were built. | |
| assertThat((outputXY.isDirectory() && outputXYY.isFile()) ^ outputXY.isFile()).isTrue(); | |
| } 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"); | |
| } | |
| } |