| // Copyright 2022 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.ImmutableSet.toImmutableSet; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.devtools.build.lib.testutil.TestConstants.WORKSPACE_NAME; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Sets; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.devtools.build.lib.actions.BuildFailedException; | |
| import com.google.devtools.build.lib.analysis.AnalysisPhaseCompleteEvent; | |
| import com.google.devtools.build.lib.analysis.ViewCreationFailedException; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.skyframe.SkymeldModule; | |
| import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelEntityAnalysisConcludedEvent; | |
| import com.google.devtools.build.lib.util.io.RecordingOutErr; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.skyframe.NodeEntry.LifecycleState; | |
| import com.google.devtools.build.skyframe.SkyKey; | |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| 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.Collections; | |
| import java.util.List; | |
| import java.util.logging.Level; | |
| import java.util.logging.Logger; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** Integration tests for project Skymeld: interleaving Skyframe's analysis and execution phases. */ | |
| @RunWith(TestParameterInjector.class) | |
| public class SkymeldBuildIntegrationTest extends BuildIntegrationTestCase { | |
| private EventsSubscriber eventsSubscriber; | |
| @Before | |
| public void setUp() { | |
| addOptions("--experimental_merged_skyframe_analysis_execution"); | |
| this.eventsSubscriber = new EventsSubscriber(); | |
| runtimeWrapper.registerSubscriber(eventsSubscriber); | |
| } | |
| /** A simple rule that has srcs, deps and writes these attributes to its output. */ | |
| private void writeMyRuleBzl() throws IOException { | |
| write( | |
| "foo/my_rule.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])) | |
| my_rule = rule( | |
| implementation = _impl, | |
| attrs = { | |
| "srcs": attr.label_list(allow_files = True), | |
| "deps": attr.label_list(providers = ["DefaultInfo"]), | |
| }, | |
| ) | |
| """); | |
| } | |
| private void writeAnalysisFailureAspectBzl() throws IOException { | |
| write( | |
| "foo/aspect.bzl", | |
| """ | |
| def _aspect_impl(target, ctx): | |
| malformed | |
| analysis_err_aspect = aspect(implementation = _aspect_impl) | |
| """); | |
| } | |
| private void writeExecutionFailureAspectBzl() throws IOException { | |
| write( | |
| "foo/aspect.bzl", | |
| """ | |
| def _aspect_impl(target, ctx): | |
| output = ctx.actions.declare_file("aspect_output") | |
| ctx.actions.run_shell( | |
| outputs = [output], | |
| command = "false", | |
| ) | |
| return [OutputGroupInfo( | |
| files = depset([output]), | |
| )] | |
| execution_err_aspect = aspect(implementation = _aspect_impl) | |
| """); | |
| } | |
| private void writeEnvironmentRules(String... defaults) throws Exception { | |
| StringBuilder defaultsBuilder = new StringBuilder(); | |
| for (String defaultEnv : defaults) { | |
| defaultsBuilder.append("'").append(defaultEnv).append("', "); | |
| } | |
| write( | |
| "buildenv/BUILD", | |
| "environment_group(", | |
| " name = 'group',", | |
| " environments = [':one', ':two'],", | |
| " defaults = [" + defaultsBuilder + "])", | |
| "environment(name = 'one')", | |
| "environment(name = 'two')"); | |
| } | |
| @CanIgnoreReturnValue | |
| private Path assertSingleOutputBuilt(String target) throws Exception { | |
| Path singleOutput = Iterables.getOnlyElement(getArtifacts(target)).getPath(); | |
| assertThat(singleOutput.isFile()).isTrue(); | |
| return singleOutput; | |
| } | |
| @Test | |
| public void nobuild_warning() throws Exception { | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| addOptions("--nobuild"); | |
| RecordingOutErr recordedOutput = divertInfoLogToOutErr(); | |
| BuildResult result = buildTarget("//foo:foo"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| assertThat(recordedOutput.errAsLatin1()) | |
| .containsMatch( | |
| "--experimental_merged_skyframe_analysis_execution is incompatible with --nobuild" | |
| + " and will be ignored"); | |
| } | |
| @Test | |
| public void multiTargetBuild_success() throws Exception { | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "bar", | |
| srcs = ["bar.in"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| write("foo/bar.in"); | |
| BuildResult result = buildTarget("//foo:foo", "//foo:bar"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| assertSingleOutputBuilt("//foo:bar"); | |
| assertThat(getLabelsOfAnalyzedTargets()).containsExactly("//foo:foo", "//foo:bar"); | |
| assertThat(getLabelsOfBuiltTargets()).containsExactly("//foo:foo", "//foo:bar"); | |
| assertThat(eventsSubscriber.getTopLevelEntityAnalysisConcludedEvents()).hasSize(2); | |
| assertSingleAnalysisPhaseCompleteEventWithLabels("//foo:foo", "//foo:bar"); | |
| assertThat(directories.getOutputPath(WORKSPACE_NAME).getRelative("build-info.txt").isFile()) | |
| .isTrue(); | |
| assertThat( | |
| directories.getOutputPath(WORKSPACE_NAME).getRelative("build-changelist.txt").isFile()) | |
| .isTrue(); | |
| } | |
| @Test | |
| public void multiTargetNullIncrementalBuild_success() throws Exception { | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "bar", | |
| srcs = ["bar.in"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| write("foo/bar.in"); | |
| // First build, ignored. | |
| buildTarget("//foo:foo", "//foo:bar"); | |
| BuildResult result = buildTarget("//foo:foo", "//foo:bar"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| assertSingleOutputBuilt("//foo:bar"); | |
| assertThat(directories.getOutputPath(WORKSPACE_NAME).getRelative("build-info.txt").isFile()) | |
| .isTrue(); | |
| assertThat( | |
| directories.getOutputPath(WORKSPACE_NAME).getRelative("build-changelist.txt").isFile()) | |
| .isTrue(); | |
| } | |
| @Test | |
| public void sequentialBuilds_verifyNodesAreDone(@TestParameter boolean mergedAnalysisExecution) | |
| throws Exception { | |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); | |
| write("hello/x.txt", "x"); | |
| write( | |
| "hello/BUILD", | |
| """ | |
| genrule( | |
| name = "target", | |
| srcs = ["x.txt"], | |
| outs = ["out"], | |
| cmd = "cat $< > $@", | |
| ) | |
| genrule( | |
| name = "target2", | |
| srcs = ["x.txt"], | |
| outs = ["out2"], | |
| cmd = "cat $< > $@", | |
| ) | |
| """); | |
| buildTarget("//hello:target"); | |
| assertThat(getSkyframeExecutor().getEvaluator().getDoneValues()) | |
| .containsExactlyEntriesIn(getSkyframeExecutor().getEvaluator().getValues()); | |
| buildTarget("//hello:target2"); | |
| if (mergedAnalysisExecution) { | |
| // BuildDriverKey of the previous build with be marked dirty from its child BUILD_ID dep. | |
| // However, only the new BuildDriverKey will be evaluated and marked done. | |
| SkyKey dirtyKey = | |
| Iterables.getOnlyElement( | |
| Sets.difference( | |
| getSkyframeExecutor().getEvaluator().getValues().keySet(), | |
| getSkyframeExecutor().getEvaluator().getDoneValues().keySet())); | |
| assertThat(dirtyKey.getCanonicalName()) | |
| .contains( | |
| "BUILD_DRIVER:BuildDriverKey of ActionLookupKey:" | |
| + " ConfiguredTargetKey{label=//hello:target"); | |
| assertThat( | |
| getSkyframeExecutor() | |
| .getEvaluator() | |
| .getInMemoryGraph() | |
| .getIfPresent(dirtyKey) | |
| .getLifecycleState()) | |
| .isEqualTo(LifecycleState.CHECK_DEPENDENCIES); | |
| } else { | |
| // This doesn't happen for non-Skymeld builds. | |
| assertThat(getSkyframeExecutor().getEvaluator().getDoneValues()) | |
| .containsExactlyEntriesIn(getSkyframeExecutor().getEvaluator().getValues()); | |
| } | |
| } | |
| @Test | |
| public void aspectAnalysisFailure_consistentWithNonSkymeld( | |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); | |
| writeMyRuleBzl(); | |
| writeAnalysisFailureAspectBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| addOptions("--aspects=//foo:aspect.bzl%analysis_err_aspect", "--output_groups=files"); | |
| if (keepGoing) { | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); | |
| } else { | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:foo")); | |
| } | |
| events.assertContainsError("compilation of module 'foo/aspect.bzl' failed"); | |
| } | |
| @Test | |
| public void aspectExecutionFailure_consistentWithNonSkymeld( | |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); | |
| writeMyRuleBzl(); | |
| writeExecutionFailureAspectBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| addOptions("--aspects=//foo:aspect.bzl%execution_err_aspect", "--output_groups=files"); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); | |
| events.assertContainsError( | |
| "Action foo/aspect_output failed: (Exit 1): bash failed: error executing Action command"); | |
| } | |
| @Test | |
| public void targetExecutionFailure_consistentWithNonSkymeld( | |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "execution_failure", | |
| srcs = ["missing"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| assertThrows( | |
| BuildFailedException.class, () -> buildTarget("//foo:foo", "//foo:execution_failure")); | |
| if (keepGoing) { | |
| assertSingleOutputBuilt("//foo:foo"); | |
| } | |
| events.assertContainsError( | |
| "Action foo/execution_failure.out failed: missing input file '//foo:missing'"); | |
| } | |
| @Test | |
| public void targetAnalysisFailure_consistentWithNonSkymeld( | |
| @TestParameter boolean keepGoing, @TestParameter boolean mergedAnalysisExecution) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution); | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "analysis_failure", | |
| srcs = ["foo.in"], | |
| deps = [":missing"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| if (keepGoing) { | |
| assertThrows( | |
| BuildFailedException.class, () -> buildTarget("//foo:foo", "//foo:analysis_failure")); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| } else { | |
| assertThrows( | |
| ViewCreationFailedException.class, | |
| () -> buildTarget("//foo:foo", "//foo:analysis_failure")); | |
| } | |
| events.assertContainsError("rule '//foo:missing' does not exist"); | |
| } | |
| // Regression test for https://github.com/bazelbuild/bazel/issues/20443 | |
| @Test | |
| public void testKeepGoingWarningContainsDetails() throws Exception { | |
| addOptions("--keep_going"); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| constraint_setting(name = "incompatible_setting") | |
| constraint_value( | |
| name = "incompatible", | |
| constraint_setting = ":incompatible_setting", | |
| visibility = ["//visibility:public"], | |
| ) | |
| cc_library( | |
| name = "foo", | |
| srcs = ["foo.cc"], | |
| target_compatible_with = ["//foo:incompatible"], | |
| ) | |
| """); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); | |
| events.assertContainsWarning( | |
| "errors encountered while analyzing target '//foo:foo', it will not be built."); | |
| // The details. | |
| events.assertContainsWarning("Dependency chain:"); | |
| } | |
| @Test | |
| public void analysisAndExecutionFailure_keepGoing_bothReported() throws Exception { | |
| addOptions("--keep_going"); | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "execution_failure", | |
| srcs = ["missing"], | |
| ) | |
| my_rule( | |
| name = "analysis_failure", | |
| srcs = ["foo.in"], | |
| deps = [":missing"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| assertThrows( | |
| BuildFailedException.class, | |
| () -> buildTarget("//foo:analysis_failure", "//foo:execution_failure")); | |
| events.assertContainsError( | |
| "Action foo/execution_failure.out failed: missing input file '//foo:missing'"); | |
| events.assertContainsError("rule '//foo:missing' does not exist"); | |
| assertThat(getLabelsOfAnalyzedTargets()).contains("//foo:execution_failure"); | |
| assertThat(getLabelsOfBuiltTargets()).isEmpty(); | |
| } | |
| @Test | |
| public void symlinkPlantedLocalAction_success() throws Exception { | |
| addOptions("--spawn_strategy=standalone"); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cp $< $@", | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| BuildResult result = buildTarget("//foo:foo"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| } | |
| @Test | |
| public void symlinksPlanted() throws Exception { | |
| Path execroot = directories.getExecRoot(directories.getWorkspace().getBaseName()); | |
| writeMyRuleBzl(); | |
| Path fooDir = | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """) | |
| .getParentDirectory(); | |
| write("foo/foo.in"); | |
| Path unusedDir = write("unused/dummy").getParentDirectory(); | |
| // Before the build: no symlink. | |
| assertThat(execroot.getRelative("foo").exists()).isFalse(); | |
| buildTarget("//foo:foo"); | |
| // After the build: symlinks to the source directory, even unused packages. | |
| assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir); | |
| assertThat(execroot.getRelative("unused").resolveSymbolicLinks()).isEqualTo(unusedDir); | |
| } | |
| @Test | |
| public void symlinksPlantedExceptProductNamePrefixAndIgnoredPaths() throws Exception { | |
| String productName = getRuntime().getProductName(); | |
| Path execroot = directories.getExecRoot(directories.getWorkspace().getBaseName()); | |
| writeMyRuleBzl(); | |
| Path fooDir = | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """) | |
| .getParentDirectory(); | |
| write("foo/foo.in"); | |
| Path unusedDir = write("unused/dummy").getParentDirectory(); | |
| write(".bazelignore", "ignored"); | |
| write("ignored/dummy"); | |
| write(productName + "-dir/dummy"); | |
| // Before the build: no symlink. | |
| assertThat(execroot.getRelative("foo").exists()).isFalse(); | |
| buildTarget("//foo:foo"); | |
| // After the build: symlinks to the source directory, even unused packages, except for those | |
| // in the .bazelignore file and those with the bazel- prefix. | |
| assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir); | |
| assertThat(execroot.getRelative("unused").resolveSymbolicLinks()).isEqualTo(unusedDir); | |
| assertThat(execroot.getRelative("ignored").exists()).isFalse(); | |
| assertThat(execroot.getRelative(productName + "-dir").exists()).isFalse(); | |
| } | |
| @Test | |
| public void symlinksReplantedEachBuild() throws Exception { | |
| Path execroot = directories.getExecRoot(directories.getWorkspace().getBaseName()); | |
| writeMyRuleBzl(); | |
| Path fooDir = | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """) | |
| .getParentDirectory(); | |
| write("foo/foo.in"); | |
| Path unusedDir = write("unused/dummy").getParentDirectory(); | |
| buildTarget("//foo:foo"); | |
| // After the 1st build: symlinks to the source directory, even unused packages. | |
| assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir); | |
| assertThat(execroot.getRelative("unused").resolveSymbolicLinks()).isEqualTo(unusedDir); | |
| unusedDir.deleteTree(); | |
| buildTarget("//foo:foo"); | |
| // After the 2nd build: symlink to unusedDir is gone, since the package itself was deleted. | |
| assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir); | |
| assertThat(execroot.getRelative("unused").exists()).isFalse(); | |
| } | |
| @Test | |
| public void targetAnalysisFailure_skymeld_correctAnalysisEvents(@TestParameter boolean keepGoing) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "analysis_failure", | |
| srcs = ["foo.in"], | |
| deps = [":missing"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| if (keepGoing) { | |
| assertThrows( | |
| BuildFailedException.class, () -> buildTarget("//foo:foo", "//foo:analysis_failure")); | |
| assertThat(eventsSubscriber.getTopLevelEntityAnalysisConcludedEvents()).hasSize(2); | |
| assertSingleAnalysisPhaseCompleteEventWithLabels("//foo:foo"); | |
| } else { | |
| assertThrows( | |
| ViewCreationFailedException.class, | |
| () -> buildTarget("//foo:foo", "//foo:analysis_failure")); | |
| assertThat(eventsSubscriber.getAnalysisPhaseCompleteEvents()).isEmpty(); | |
| } | |
| } | |
| @Test | |
| public void aspectAnalysisFailure_skymeld_correctAnalysisEvents(@TestParameter boolean keepGoing) | |
| throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| writeMyRuleBzl(); | |
| writeAnalysisFailureAspectBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| addOptions("--aspects=//foo:aspect.bzl%analysis_err_aspect", "--output_groups=files"); | |
| if (keepGoing) { | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); | |
| assertThat(eventsSubscriber.getTopLevelEntityAnalysisConcludedEvents()).hasSize(2); | |
| assertSingleAnalysisPhaseCompleteEventWithLabels("//foo:foo"); | |
| } else { | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:foo")); | |
| assertThat(eventsSubscriber.getAnalysisPhaseCompleteEvents()).isEmpty(); | |
| } | |
| events.assertContainsError("compilation of module 'foo/aspect.bzl' failed"); | |
| } | |
| @Test | |
| public void targetSkipped_skymeld_correctAnalysisEvents(@TestParameter boolean keepGoing) | |
| throws Exception { | |
| writeEnvironmentRules(); | |
| addOptions("--keep_going=" + keepGoing); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| sh_library( | |
| name = "good_bar", | |
| srcs = ["bar.sh"], | |
| compatible_with = ["//buildenv:one"], | |
| ) | |
| sh_library( | |
| name = "bad_bar", | |
| srcs = ["bar.sh"], | |
| compatible_with = ["//buildenv:two"], | |
| ) | |
| """); | |
| write("foo/bar.sh"); | |
| addOptions("--target_environment=//buildenv:one"); | |
| if (keepGoing) { | |
| assertThrows( | |
| BuildFailedException.class, () -> buildTarget("//foo:good_bar", "//foo:bad_bar")); | |
| assertThat(eventsSubscriber.getTopLevelEntityAnalysisConcludedEvents()).hasSize(2); | |
| assertThat(eventsSubscriber.getAnalysisPhaseCompleteEvents()).hasSize(1); | |
| AnalysisPhaseCompleteEvent analysisPhaseCompleteEvent = | |
| Iterables.getOnlyElement(eventsSubscriber.getAnalysisPhaseCompleteEvents()); | |
| assertThat(analysisPhaseCompleteEvent.getTimeInMs()).isGreaterThan(0); | |
| assertThat(getLabelsOfAnalyzedTargets(analysisPhaseCompleteEvent)) | |
| .containsExactly("//foo:good_bar", "//foo:bad_bar"); | |
| } else { | |
| assertThrows( | |
| ViewCreationFailedException.class, () -> buildTarget("//foo:good_bar", "//foo:bad_bar")); | |
| assertThat(eventsSubscriber.getAnalysisPhaseCompleteEvents()).isEmpty(); | |
| } | |
| } | |
| @Test | |
| public void targetWithNoConfiguration_success() throws Exception { | |
| write("foo/BUILD", "exports_files(['bar.txt'])"); | |
| write("foo/bar.txt", "This is just a test file to pretend to build."); | |
| BuildResult result = buildTarget("//foo:bar.txt"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| } | |
| @Test | |
| public void explain_ignoreSkymeldWithWarning() throws Exception { | |
| addOptions("--explain=/dev/null"); | |
| write("foo/BUILD", "genrule(name = 'foo', outs = ['foo.out'], cmd = 'touch $@')"); | |
| RecordingOutErr recordedOutput = divertInfoLogToOutErr(); | |
| BuildResult buildResult = buildTarget("//foo"); | |
| assertThat(buildResult.getSuccess()).isTrue(); | |
| assertThat(recordedOutput.errAsLatin1()) | |
| .containsMatch( | |
| "--experimental_merged_skyframe_analysis_execution is incompatible with --explain" | |
| + " and will be ignored."); | |
| } | |
| @Test | |
| public void multiplePackagePath_ignoreSkymeldWithWarning() throws Exception { | |
| write("foo/BUILD", "genrule(name = 'foo', outs = ['foo.out'], cmd = 'touch $@')"); | |
| write("otherroot/bar/BUILD", "genrule(name = 'bar', outs = ['bar.out'], cmd = 'touch $@')"); | |
| addOptions("--package_path=%workspace%:otherroot"); | |
| RecordingOutErr recordedOutput = divertInfoLogToOutErr(); | |
| BuildResult buildResult = buildTarget("//foo", "//bar"); | |
| assertThat(buildResult.getSuccess()).isTrue(); | |
| assertThat(recordedOutput.errAsLatin1()) | |
| .containsMatch( | |
| "--experimental_merged_skyframe_analysis_execution is incompatible with multiple" | |
| + " --package_path.*and its value will be ignored."); | |
| } | |
| // Regression test for b/245919888. | |
| @Test | |
| public void outputFileRemoved_regeneratedWithIncrementalBuild() throws Exception { | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| BuildResult result = buildTarget("//foo:foo"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| Path fooOut = assertSingleOutputBuilt("//foo:foo"); | |
| fooOut.delete(); | |
| BuildResult incrementalBuild = buildTarget("//foo:foo"); | |
| assertThat(incrementalBuild.getSuccess()).isTrue(); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| } | |
| // Regression test for b/245922900. | |
| @Test | |
| public void executionFailure_discardAnalysisCache_doesNotCrash() throws Exception { | |
| addOptions("--experimental_merged_skyframe_analysis_execution", "--discard_analysis_cache"); | |
| writeExecutionFailureAspectBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| cc_library( | |
| name = "foo", | |
| srcs = ["foo.cc"], | |
| deps = [":bar"], | |
| ) | |
| cc_library( | |
| name = "bar", | |
| srcs = ["bar.cc"], | |
| ) | |
| """); | |
| write("foo/foo.cc"); | |
| write("foo/bar.cc"); | |
| addOptions("--aspects=//foo:aspect.bzl%execution_err_aspect", "--output_groups=files"); | |
| // Verify that the build did not crash. | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); | |
| events.assertContainsError( | |
| "Action foo/aspect_output failed: (Exit 1): bash failed: error executing Action command"); | |
| } | |
| @Test | |
| public void targetCycle_doesNotCrash() throws Exception { | |
| write( | |
| "a/BUILD", | |
| """ | |
| alias( | |
| name = "a", | |
| actual = ":b", | |
| ) | |
| alias( | |
| name = "b", | |
| actual = ":c", | |
| ) | |
| alias( | |
| name = "c", | |
| actual = ":a", | |
| ) | |
| filegroup( | |
| name = "d", | |
| srcs = [":c"], | |
| ) | |
| """); | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//a:d")); | |
| events.assertContainsError("cycle in dependency graph"); | |
| } | |
| @Test | |
| public void analysisOverlapPercentageSanityCheck_success() throws Exception { | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "bar", | |
| srcs = ["bar.in"], | |
| ) | |
| my_rule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| write("foo/bar.in"); | |
| addOptions("--experimental_skymeld_analysis_overlap_percentage=5"); | |
| BuildResult result = buildTarget("//foo:foo", "//foo:bar"); | |
| assertThat(result.getSuccess()).isTrue(); | |
| assertSingleOutputBuilt("//foo:foo"); | |
| assertSingleOutputBuilt("//foo:bar"); | |
| assertThat(getLabelsOfAnalyzedTargets()).containsExactly("//foo:foo", "//foo:bar"); | |
| assertThat(getLabelsOfBuiltTargets()).containsExactly("//foo:foo", "//foo:bar"); | |
| assertThat(eventsSubscriber.getTopLevelEntityAnalysisConcludedEvents()).hasSize(2); | |
| assertSingleAnalysisPhaseCompleteEventWithLabels("//foo:foo", "//foo:bar"); | |
| } | |
| // Regression test for b/277783687. | |
| @Test | |
| public void targetAnalysisFailureNullBuild_correctErrorsPropagated( | |
| @TestParameter boolean keepGoing) throws Exception { | |
| addOptions("--keep_going=" + keepGoing); | |
| writeMyRuleBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//foo:my_rule.bzl", "my_rule") | |
| my_rule( | |
| name = "analysis_failure", | |
| srcs = ["foo.in"], | |
| deps = [":missing"], | |
| ) | |
| """); | |
| write("foo/foo.in"); | |
| if (keepGoing) { | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:analysis_failure")); | |
| } else { | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:analysis_failure")); | |
| } | |
| events.assertContainsError( | |
| "in deps attribute of my_rule rule //foo:analysis_failure: rule '//foo:missing' does not" | |
| + " exist"); | |
| // Null build | |
| if (keepGoing) { | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:analysis_failure")); | |
| } else { | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:analysis_failure")); | |
| } | |
| events.assertContainsError( | |
| "in deps attribute of my_rule rule //foo:analysis_failure: rule '//foo:missing' does not" | |
| + " exist"); | |
| } | |
| // Regression test for b/300391729. | |
| @Test | |
| public void executionFailure_keepGoing_doesNotSpamWarnings() throws Exception { | |
| addOptions("--keep_going"); | |
| writeExecutionFailureAspectBzl(); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| cc_library( | |
| name = "foo", | |
| srcs = ["foo.cc"], | |
| deps = [":bar"], | |
| ) | |
| cc_library( | |
| name = "bar", | |
| srcs = ["bar.cc"], | |
| ) | |
| """); | |
| write("foo/foo.cc"); | |
| write("foo/bar.cc"); | |
| addOptions("--aspects=//foo:aspect.bzl%execution_err_aspect", "--output_groups=files"); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo/...")); | |
| // No warnings. | |
| events.assertNoWarnings(); | |
| } | |
| // Regression test for b/301289073. | |
| @Test | |
| public void conflictCheck_doesNotTimeout() throws Exception { | |
| addOptions("--keep_going"); | |
| write( | |
| "foo/BUILD", | |
| """ | |
| BASE_SIZE = 500 | |
| TOP_SIZE = 100 | |
| genrule( | |
| name = "base_0", | |
| outs = ["base_0.txt"], | |
| cmd = "touch $@", | |
| ) | |
| [genrule( | |
| name = "base_%s" % x, | |
| srcs = ["base_%s.txt" % (x - 1)], | |
| outs = ["base_%s.txt" % x], | |
| cmd = "touch $@", | |
| ) for x in range(1, BASE_SIZE)] | |
| [genrule( | |
| name = "level_%s" % y, | |
| srcs = ["base_%s.txt" % ( | |
| x, | |
| ) for x in range(0, BASE_SIZE)], | |
| outs = ["level_%s.txt" % y], | |
| cmd = "touch $@", | |
| ) for y in range(0, TOP_SIZE)] | |
| genrule( | |
| name = "conflict", | |
| outs = ["conflict"], | |
| cmd = "touch $@", | |
| ) | |
| """); | |
| write( | |
| "foo/conflict/BUILD", | |
| """ | |
| genrule( | |
| name = "conflict", | |
| outs = ["conflict"], | |
| cmd = "touch $@", | |
| ) | |
| """); | |
| // Building a set of targets with recursive dependencies that would trivially finish in time | |
| // with memoization and time out without. | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo/...")); | |
| events.assertContainsError("is a prefix of the other"); | |
| } | |
| private void assertSingleAnalysisPhaseCompleteEventWithLabels(String... labels) { | |
| assertThat(eventsSubscriber.getAnalysisPhaseCompleteEvents()).hasSize(1); | |
| AnalysisPhaseCompleteEvent analysisPhaseCompleteEvent = | |
| Iterables.getOnlyElement(eventsSubscriber.getAnalysisPhaseCompleteEvents()); | |
| assertThat(analysisPhaseCompleteEvent.getTimeInMs()).isGreaterThan(0); | |
| assertThat(getLabelsOfAnalyzedTargets(analysisPhaseCompleteEvent)) | |
| .containsExactlyElementsIn(labels); | |
| } | |
| private static ImmutableSet<String> getLabelsOfAnalyzedTargets(AnalysisPhaseCompleteEvent event) { | |
| return event.getTopLevelTargets().stream() | |
| .map(x -> x.getOriginalLabel().getCanonicalForm()) | |
| .collect(toImmutableSet()); | |
| } | |
| private RecordingOutErr divertInfoLogToOutErr() { | |
| // Divert output into recorder: | |
| RecordingOutErr recordedOutput = new RecordingOutErr(); | |
| this.outErr = recordedOutput; | |
| divertLogging( | |
| Level.INFO, outErr, ImmutableList.of(Logger.getLogger(SkymeldModule.class.getName()))); | |
| return recordedOutput; | |
| } | |
| private static final class EventsSubscriber { | |
| private final List<TopLevelEntityAnalysisConcludedEvent> topLevelEntityAnalysisConcludedEvents = | |
| Collections.synchronizedList(new ArrayList<>()); | |
| private final List<AnalysisPhaseCompleteEvent> analysisPhaseCompleteEvents = | |
| Collections.synchronizedList(new ArrayList<>()); | |
| EventsSubscriber() {} | |
| @Subscribe | |
| void recordTopLevelEntityAnalysisConcludedEvent(TopLevelEntityAnalysisConcludedEvent event) { | |
| topLevelEntityAnalysisConcludedEvents.add(event); | |
| } | |
| @Subscribe | |
| void recordAnalysisPhaseCompleteEvent(AnalysisPhaseCompleteEvent event) { | |
| analysisPhaseCompleteEvents.add(event); | |
| } | |
| public List<TopLevelEntityAnalysisConcludedEvent> getTopLevelEntityAnalysisConcludedEvents() { | |
| return topLevelEntityAnalysisConcludedEvents; | |
| } | |
| public List<AnalysisPhaseCompleteEvent> getAnalysisPhaseCompleteEvents() { | |
| return analysisPhaseCompleteEvents; | |
| } | |
| } | |
| } |