| // 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.remote; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import static com.google.common.collect.Iterables.getOnlyElement; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth.assertWithMessage; | |
| import static com.google.devtools.build.lib.vfs.FileSystemUtils.readContent; | |
| import static com.google.devtools.build.lib.vfs.FileSystemUtils.writeContent; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.junit.Assert.assertThrows; | |
| import static org.junit.Assume.assumeFalse; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.eventbus.AllowConcurrentEvents; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.devtools.build.lib.actions.ActionExecutedEvent; | |
| import com.google.devtools.build.lib.actions.ActionStartedEvent; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.BuildFailedException; | |
| import com.google.devtools.build.lib.actions.CachedActionEvent; | |
| import com.google.devtools.build.lib.actions.FileArtifactValue; | |
| import com.google.devtools.build.lib.actions.RunningActionEvent; | |
| import com.google.devtools.build.lib.analysis.TargetCompleteEvent; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.exec.TestPolicy; | |
| import com.google.devtools.build.lib.runtime.commands.InfoCommand; | |
| import com.google.devtools.build.lib.runtime.commands.RunCommand; | |
| import com.google.devtools.build.lib.skyframe.ActionExecutionValue; | |
| import com.google.devtools.build.lib.skyframe.SkyFunctions; | |
| import com.google.devtools.build.lib.skyframe.TargetCompletionValue.TargetCompletionKey; | |
| import com.google.devtools.build.lib.skyframe.TreeArtifactValue; | |
| import com.google.devtools.build.lib.skyframe.rewinding.ActionRewoundEvent; | |
| import com.google.devtools.build.lib.testutil.TestUtils; | |
| import com.google.devtools.build.lib.util.CommandBuilder; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.util.io.RecordingOutErr; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.SymlinkTargetType; | |
| import com.google.devtools.build.skyframe.SkyFunctionName; | |
| import com.google.devtools.build.skyframe.SkyframeGraphStatsEvent; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.concurrent.CountDownLatch; | |
| import org.junit.Test; | |
| /** Base class for integration tests for BwoB. */ | |
| public abstract class BuildWithoutTheBytesIntegrationTestBase extends BuildIntegrationTestCase { | |
| // Concrete implementations should by default set the necessary flags to download minimal outputs. | |
| // These methods should override the necessary flags to download top-level outputs or all outputs. | |
| protected abstract void setDownloadToplevel(); | |
| protected abstract void setDownloadAll(); | |
| protected void setDownloadMinimal() { | |
| addOptions("--remote_download_outputs=minimal"); | |
| } | |
| protected abstract void enableActionRewinding(); | |
| protected void disableActionRewinding() { | |
| addOptions("--norewind_lost_inputs"); | |
| } | |
| protected abstract void assertOutputEquals(Path path, String expectedContent) throws Exception; | |
| protected abstract void assertOutputContains(String content, String contains) throws Exception; | |
| protected abstract void evictAllBlobs() throws Exception; | |
| protected abstract boolean hasAccessToRemoteOutputs(); | |
| protected abstract void injectFile(byte[] content); | |
| protected void waitDownloads() throws Exception { | |
| // Trigger afterCommand of modules so that downloads are waited. | |
| runtimeWrapper.newCommand(); | |
| } | |
| @Test | |
| public void outputsAreNotDownloaded() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputsDoNotExist("//:foo"); | |
| assertOutputsDoNotExist("//:foobar"); | |
| } | |
| // The trailing line break cannot be avoided with a batch tool on Windows. | |
| private static final String CAPTURED_STDOUT = | |
| OS.getCurrent() == OS.WINDOWS ? "hello stdout\r\n" : "hello stdout"; | |
| private void writeStdoutRule() throws IOException { | |
| // A rule whose only output is the captured stdout of a tool invoked via ctx.actions.run. | |
| boolean isWindows = OS.getCurrent() == OS.WINDOWS; | |
| write( | |
| "a/defs.bzl", | |
| """ | |
| def _impl(ctx): | |
| tool = ctx.actions.declare_file(ctx.label.name + "%s") | |
| ctx.actions.write( | |
| tool, | |
| %s, | |
| is_executable = True, | |
| ) | |
| out = ctx.actions.declare_file(ctx.label.name + ".out") | |
| ctx.actions.run( | |
| outputs = [], | |
| executable = tool, | |
| stdout = out, | |
| mnemonic = "Capture", | |
| ) | |
| return DefaultInfo(files = depset([out])) | |
| capture = rule(implementation = _impl) | |
| """ | |
| .formatted( | |
| isWindows ? ".bat" : ".sh", | |
| isWindows | |
| ? "\"@echo off\\necho hello stdout\\n\"" | |
| : "\"#!/bin/bash\\nprintf '%s' 'hello stdout'\\n\"")); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load(":defs.bzl", "capture") | |
| capture(name = "capture") | |
| """); | |
| } | |
| @Test | |
| public void stdoutOutput_notDownloadedUnderMinimal() throws Exception { | |
| // The stdout output is an ordinary output: under build-without-the-bytes it is not eagerly | |
| // downloaded, but its (remote) metadata is available. | |
| if (!hasAccessToRemoteOutputs()) { | |
| return; | |
| } | |
| writeStdoutRule(); | |
| buildTarget("//a:capture"); | |
| waitDownloads(); | |
| assertOnlyOutputRemoteContent("//a:capture", "capture.out", CAPTURED_STDOUT); | |
| } | |
| @Test | |
| public void stdoutOutput_downloadedWithRegex() throws Exception { | |
| // remote_download_regex matching the stdout file forces it to be downloaded, just like any | |
| // other output. | |
| writeStdoutRule(); | |
| addOptions("--remote_download_regex=.*capture\\.out$"); | |
| buildTarget("//a:capture"); | |
| waitDownloads(); | |
| assertOnlyOutputContent("//a:capture", "capture.out", CAPTURED_STDOUT); | |
| } | |
| @Test | |
| public void stdoutOutput_downloadTopLevel() throws Exception { | |
| // Requesting top-level outputs downloads the stdout output with its captured content. | |
| setDownloadToplevel(); | |
| writeStdoutRule(); | |
| buildTarget("//a:capture"); | |
| waitDownloads(); | |
| assertOnlyOutputContent("//a:capture", "capture.out", CAPTURED_STDOUT); | |
| } | |
| @Test | |
| public void disableRunfiles_buildSuccessfully() throws Exception { | |
| // Disable on Windows since it fails for unknown reasons. | |
| // TODO(chiwang): Enable it on windows. | |
| assumeFalse(OS.getCurrent() == OS.WINDOWS); | |
| write( | |
| "BUILD", | |
| "load('//test_defs:foo_test.bzl', 'foo_test')", | |
| "genrule(", | |
| " name = 'foo',", | |
| " cmd = 'echo foo > $@',", | |
| " outs = ['foo.data'],", | |
| ")", | |
| "foo_test(", | |
| " name = 'foobar',", | |
| " srcs = ['test.sh'],", | |
| " data = [':foo'],", | |
| ")"); | |
| write("test.sh"); | |
| getWorkspace().getRelative("test.sh").setExecutable(true); | |
| addOptions("--build_runfile_links", "--enable_runfiles=no"); | |
| buildTarget("//:foobar"); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo\\.txt$"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertOutputsDoNotExist("//:foobar"); | |
| // Assert that no actions have been executed for the next incremental build since nothing | |
| // changed | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| // Override out/foo.txt with the same content | |
| { | |
| var path = getOutputPath("out/foo.txt"); | |
| var isWritable = path.isWritable(); | |
| if (!isWritable) { | |
| path.setWritable(true); | |
| } | |
| writeContent(path, UTF_8, "foo\n"); | |
| if (!isWritable) { | |
| path.setWritable(false); | |
| } | |
| } | |
| buildTarget("//:foobar"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_deleteOutput_reDownload() throws Exception { | |
| // Arrange: Do a clean build and download out/foo.txt | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo\\.txt$"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertOutputsDoNotExist("//:foobar"); | |
| // Arrange: Delete out/foo.txt and do an incremental build | |
| getOutputPath("out/foo.txt").delete(); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| // Assert: out/foo.txt is re-downloaded | |
| assertThat(actionEventCollector.getActionExecutedEvents()).hasSize(1); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputDeletedAfterUnrelatedBuild_toplevelOutputIsRestored() | |
| throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'bar',", | |
| " outs = ['out/bar.txt'],", | |
| " cmd = 'echo bar > $@',", | |
| ")"); | |
| // The default minimal build leaves out/foo.txt represented only by remote metadata. The | |
| // subsequent toplevel build materializes it via the completion function without reexecuting | |
| // the generating action, so the metadata tracked for it in Skyframe remains remote. | |
| buildTarget("//:foo"); | |
| assertOutputsDoNotExist("//:foo"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| // An intervening build of an unrelated target discards the previous invocation's record of | |
| // which outputs it wanted locally. | |
| buildTarget("//:bar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/bar.txt", "bar\n"); | |
| // Delete the top-level output of the earlier build and request it again. | |
| getOutputPath("out/foo.txt").delete(); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_formerToplevelOutputDeleted_restoreAttemptedOnlyOnce() | |
| throws Exception { | |
| if (!hasAccessToRemoteOutputs()) { | |
| return; | |
| } | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| // Materialize out/foo.txt via the completion function so that the metadata tracked for it in | |
| // Skyframe remains remote, then build a target for which it is merely an intermediate output. | |
| buildTarget("//:foo"); | |
| assertOutputsDoNotExist("//:foo"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| getOutputPath("out/foo.txt").delete(); | |
| // The first incremental build reevaluates the generating action so that the current download | |
| // policy can decide whether to restore the file, but doesn't materialize it as it is no | |
| // longer a top-level output. | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(actionEventCollector.getNumActionNodesEvaluated()).isEqualTo(1); | |
| // Subsequent incremental builds trust the still-missing output and evaluate nothing. | |
| actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| assertThat(actionEventCollector.getNumActionNodesEvaluated()).isEqualTo(0); | |
| } | |
| @Test | |
| public void downloadToplevel_afterDownloadAllBuild_deletedToplevelOutputIsRestored() | |
| throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| setDownloadAll(); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| // Delete both a top-level and an intermediate output, then build with a narrower download | |
| // policy. | |
| getOutputPath("out/foo.txt").delete(); | |
| getOutputPath("out/foobar.txt").delete(); | |
| setDownloadToplevel(); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| // The top-level output must be restored; the intermediate output is not needed locally. | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_changeRegex_downloadNewMatches() throws Exception { | |
| // Arrange: Do a clean build | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| // Add the new option here because waitDownloads below will internally create a new command | |
| // which will parse the new option. | |
| addOptions("--remote_download_regex=.*foobar\\.txt$"); | |
| waitDownloads(); | |
| assertOutputsDoNotExist("//:foo"); | |
| assertOutputsDoNotExist("//:foobar"); | |
| // Arrange: Change regex | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| // Assert: out/foobar.txt is downloaded | |
| assertThat(actionEventCollector.getActionExecutedEvents()).hasSize(1); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_treeOutput_regexMatchesTreeFile() throws Exception { | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo/file-2$"); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("foo/file-2", "2"); | |
| assertOutputDoesNotExist("foo/file-1"); | |
| assertOutputDoesNotExist("foo/file-3"); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_treeOutput_regexMatchesTreeRoot() throws Exception { | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo$"); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertThat(getOutputPath("foo").exists()).isTrue(); | |
| assertOutputEquals(getOutputPath("foo/file-1"), "1"); | |
| assertOutputEquals(getOutputPath("foo/file-2"), "2"); | |
| assertOutputEquals(getOutputPath("foo/file-3"), "3"); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_treeOutput_regexMatchesEmptyTreeRoot() throws Exception { | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {},", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo$"); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertThat(getOutputPath("foo").exists()).isTrue(); | |
| } | |
| @Test | |
| public void downloadOutputsWithRegex_regexMatchParentPath_filesNotDownloaded() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'file-1',", | |
| " srcs = [],", | |
| " outs = ['foo/file-1'],", | |
| " cmd = 'echo file-1 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'file-2',", | |
| " srcs = [],", | |
| " outs = ['foo/file-2'],", | |
| " cmd = 'echo file-2 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'file-3',", | |
| " srcs = [],", | |
| " outs = ['foo/file-3'],", | |
| " cmd = 'echo file-3 > $@',", | |
| ")"); | |
| addOptions("--remote_download_regex=.*foo$"); | |
| buildTarget("//:file-1", "//:file-2", "//:file-3"); | |
| waitDownloads(); | |
| assertOutputDoesNotExist("foo/file-1"); | |
| assertOutputDoesNotExist("foo/file-2"); | |
| assertOutputDoesNotExist("foo/file-3"); | |
| } | |
| @Test | |
| public void intermediateOutputsAreInputForLocalActions_prefetchIntermediateOutputs() | |
| throws Exception { | |
| // Test that a remote-only output that's an input to a local action is downloaded lazily before | |
| // executing the local action. | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "remote", | |
| srcs = [], | |
| outs = ["remote.txt"], | |
| cmd = "echo -n remote > $@", | |
| ) | |
| genrule( | |
| name = "local", | |
| srcs = [":remote"], | |
| outs = ["local.txt"], | |
| cmd = "cat $(location :remote) > $@ && echo -n local >> $@", | |
| tags = ["no-remote"], | |
| ) | |
| """); | |
| buildTarget("//a:remote"); | |
| waitDownloads(); | |
| assertOutputsDoNotExist("//a:remote"); | |
| buildTarget("//a:local"); | |
| waitDownloads(); | |
| assertOnlyOutputContent("//a:remote", "remote.txt", "remote"); | |
| assertOnlyOutputContent("//a:local", "local.txt", "remotelocal"); | |
| } | |
| @Test | |
| public void localAction_inputSymlinkToSourceFile() throws Exception { | |
| write( | |
| "a/defs.bzl", | |
| """ | |
| def _impl(ctx): | |
| sym = ctx.actions.declare_file(ctx.label.name + ".sym") | |
| ctx.actions.symlink(output = sym, target_file = ctx.file.target) | |
| out = ctx.actions.declare_file(ctx.label.name + ".out") | |
| ctx.actions.run_shell( | |
| inputs = [sym], | |
| outputs = [out], | |
| command = "[[ hello == $(cat $1) ]] && touch $2", | |
| arguments = [sym.path, out.path], | |
| execution_requirements = {"no-remote": ""}, | |
| ) | |
| return DefaultInfo(files = depset([out])) | |
| my_rule = rule( | |
| implementation = _impl, | |
| attrs = { | |
| "target": attr.label(allow_single_file = True), | |
| }, | |
| ) | |
| """); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load(":defs.bzl", "my_rule") | |
| my_rule( | |
| name = "my", | |
| target = "src.txt", | |
| ) | |
| """); | |
| write("a/src.txt", "hello"); | |
| buildTarget("//a:my"); | |
| } | |
| @Test | |
| public void localAction_inputSymlinkToGeneratedFile() throws Exception { | |
| injectFile("hello".getBytes(UTF_8)); | |
| write( | |
| "a/defs.bzl", | |
| """ | |
| def _impl(ctx): | |
| file = ctx.actions.declare_file(ctx.label.name + ".file") | |
| # Use ctx.actions.run_shell instead of ctx.actions.write, so that it runs remotely. | |
| ctx.actions.run_shell( | |
| outputs = [file], | |
| command = "echo -n hello > $1", | |
| arguments = [file.path], | |
| ) | |
| sym = ctx.actions.declare_file(ctx.label.name + ".sym") | |
| ctx.actions.symlink(output = sym, target_file = file) | |
| out = ctx.actions.declare_file(ctx.label.name + ".out") | |
| ctx.actions.run_shell( | |
| inputs = [sym], | |
| outputs = [out], | |
| command = "[[ hello == $(cat $1) ]] && touch $2", | |
| arguments = [sym.path, out.path], | |
| execution_requirements = {"no-remote": ""}, | |
| ) | |
| return DefaultInfo(files = depset([out])) | |
| my_rule = rule(_impl) | |
| """); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load(":defs.bzl", "my_rule") | |
| my_rule(name = "my") | |
| """); | |
| buildTarget("//a:my"); | |
| } | |
| @Test | |
| public void localAction_inputSymlinkToDirectory() throws Exception { | |
| injectFile("hello".getBytes(UTF_8)); | |
| write( | |
| "a/defs.bzl", | |
| """ | |
| def _impl(ctx): | |
| dir = ctx.actions.declare_directory(ctx.label.name + ".dir") | |
| ctx.actions.run_shell( | |
| outputs = [dir], | |
| command = "mkdir -p $1/some/path && echo -n hello > $1/some/path/inside.txt", | |
| arguments = [dir.path], | |
| ) | |
| sym = ctx.actions.declare_directory(ctx.label.name + ".sym") | |
| ctx.actions.symlink(output = sym, target_file = dir) | |
| out = ctx.actions.declare_file(ctx.label.name + ".out") | |
| ctx.actions.run_shell( | |
| inputs = [sym], | |
| outputs = [out], | |
| command = "[[ hello == $(cat $1/some/path/inside.txt) ]] && touch $2", | |
| arguments = [sym.path, out.path], | |
| execution_requirements = {"no-remote": ""}, | |
| ) | |
| return DefaultInfo(files = depset([out])) | |
| my_rule = rule(_impl) | |
| """); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load(":defs.bzl", "my_rule") | |
| my_rule(name = "my") | |
| """); | |
| buildTarget("//a:my"); | |
| } | |
| @Test | |
| public void localAction_stdoutIsReported() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo my-output-message > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) && touch $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputContains(outErr.outAsLatin1(), "my-output-message"); | |
| } | |
| @Test | |
| public void localAction_stderrIsReported() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo my-error-message > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) >&2 && exit 1',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//:foobar")); | |
| assertOutputContains(outErr.errAsLatin1(), "my-error-message"); | |
| } | |
| @Test | |
| public void dynamicExecution_stdoutIsReported() throws Exception { | |
| addOptions("--internal_spawn_scheduler"); | |
| addOptions("--strategy=Genrule=dynamic"); | |
| addOptions("--experimental_local_execution_delay=9999999"); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo my-output-message > $@',", | |
| " tags = ['no-local'],", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) && touch $@',", | |
| ")"); | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputContains(outErr.outAsLatin1(), "my-output-message"); | |
| } | |
| @Test | |
| public void dynamicExecution_stderrIsReported() throws Exception { | |
| addOptions("--internal_spawn_scheduler"); | |
| addOptions("--strategy=Genrule=dynamic"); | |
| addOptions("--experimental_local_execution_delay=9999999"); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo my-error-message > $@',", | |
| " tags = ['no-local'],", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) >&2 && exit 1',", | |
| ")"); | |
| RecordingOutErr outErr = new RecordingOutErr(); | |
| this.outErr = outErr; | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//:foobar")); | |
| assertOutputContains(outErr.errAsLatin1(), "my-error-message"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputsFromAspect_notAggregated() throws Exception { | |
| setDownloadToplevel(); | |
| writeCopyAspectRule(/* aggregate= */ false); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = ['foo.in'],", | |
| " outs = ['foo.out'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['foobar.out'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| write("foo.in", "foo"); | |
| addOptions("--aspects=rules.bzl%copy_aspect", "--output_groups=+copy"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("foobar.out", "foo\nbar\n"); | |
| assertOutputDoesNotExist("foo.in.copy"); | |
| assertValidOutputFile("foo.out.copy", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputsFromAspect_aggregated() throws Exception { | |
| setDownloadToplevel(); | |
| writeCopyAspectRule(/* aggregate= */ true); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = ['foo.in'],", | |
| " outs = ['foo.out'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['foobar.out'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| write("foo.in", "foo"); | |
| addOptions("--aspects=rules.bzl%copy_aspect", "--output_groups=+copy"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("foobar.out", "foo\nbar\n"); | |
| assertValidOutputFile("foo.in.copy", "foo\n"); | |
| assertValidOutputFile("foo.out.copy", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputsFromAspect_notDownloadedIfNoOutputGroups() throws Exception { | |
| setDownloadToplevel(); | |
| writeCopyAspectRule(/* aggregate= */ true); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = ['foo.in'],", | |
| " outs = ['foo.out'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['foobar.out'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| write("foo.in", "foo"); | |
| addOptions("--aspects=rules.bzl%copy_aspect"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("foobar.out", "foo\nbar\n"); | |
| assertOutputDoesNotExist("foo.in.copy"); | |
| assertOutputDoesNotExist("foo.out.copy"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputsFromImportantOutputGroupAreDownloaded() throws Exception { | |
| setDownloadToplevel(); | |
| write( | |
| "rules.bzl", | |
| """ | |
| def _gen_impl(ctx): | |
| output = ctx.actions.declare_file(ctx.attr.name) | |
| ctx.actions.run_shell( | |
| outputs = [output], | |
| arguments = [ctx.attr.content, output.path], | |
| command = "echo $1 > $2", | |
| ) | |
| extra1 = ctx.actions.declare_file(ctx.attr.name + "1") | |
| ctx.actions.run_shell( | |
| outputs = [extra1], | |
| arguments = [ctx.attr.content, extra1.path], | |
| command = "echo $1 > $2", | |
| ) | |
| extra2 = ctx.actions.declare_file(ctx.attr.name + "2") | |
| ctx.actions.run_shell( | |
| outputs = [extra2], | |
| arguments = [ctx.attr.content, extra2.path], | |
| command = "echo $1 > $2", | |
| ) | |
| return [ | |
| DefaultInfo(files = depset([output])), | |
| OutputGroupInfo( | |
| extra1_files = depset([extra1]), | |
| extra2_files = depset([extra2]), | |
| ), | |
| ] | |
| gen = rule( | |
| implementation = _gen_impl, | |
| attrs = { | |
| "content": attr.string(mandatory = True), | |
| }, | |
| ) | |
| """); | |
| write( | |
| "BUILD", | |
| "load(':rules.bzl', 'gen')", | |
| "gen(", | |
| " name = 'foo',", | |
| " content = 'foo-content',", | |
| ")"); | |
| addOptions("--output_groups=+extra1_files"); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("foo", "foo-content\n"); | |
| assertValidOutputFile("foo1", "foo-content\n"); | |
| assertOutputDoesNotExist("foo2"); | |
| } | |
| @Test | |
| public void downloadToplevel_outputsFromHiddenOutputGroupAreNotDownloaded() throws Exception { | |
| setDownloadToplevel(); | |
| write( | |
| "rules.bzl", | |
| """ | |
| def _gen_impl(ctx): | |
| output = ctx.actions.declare_file(ctx.attr.name) | |
| ctx.actions.run_shell( | |
| outputs = [output], | |
| arguments = [ctx.attr.content, output.path], | |
| command = "echo $1 > $2", | |
| ) | |
| validation_file = ctx.actions.declare_file(ctx.attr.name + ".validation") | |
| ctx.actions.run_shell( | |
| outputs = [validation_file], | |
| arguments = [ctx.attr.content, validation_file.path], | |
| command = "echo $1 > $2", | |
| ) | |
| return [ | |
| DefaultInfo(files = depset([output])), | |
| OutputGroupInfo( | |
| _validation = depset([validation_file]), | |
| ), | |
| ] | |
| gen = rule( | |
| implementation = _gen_impl, | |
| attrs = { | |
| "content": attr.string(mandatory = True), | |
| }, | |
| ) | |
| """); | |
| write( | |
| "BUILD", | |
| "load(':rules.bzl', 'gen')", | |
| "gen(", | |
| " name = 'foo',", | |
| " content = 'foo-content',", | |
| ")"); | |
| addOptions("--output_groups=+_validation"); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("foo", "foo-content\n"); | |
| assertOutputDoesNotExist("foo.validation"); | |
| } | |
| @Test | |
| public void downloadToplevel_treeArtifacts() throws Exception { | |
| setDownloadToplevel(); | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")"); | |
| buildTarget("//:foo"); | |
| assertValidOutputFile("foo/file-1", "1"); | |
| assertValidOutputFile("foo/file-2", "2"); | |
| assertValidOutputFile("foo/file-3", "3"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| } | |
| @Test | |
| public void downloadToplevel_multipleToplevelTargets() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo1',", | |
| " srcs = [],", | |
| " outs = ['out/foo1.txt'],", | |
| " cmd = 'echo foo1 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo2',", | |
| " srcs = [],", | |
| " outs = ['out/foo2.txt'],", | |
| " cmd = 'echo foo2 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo3',", | |
| " srcs = [],", | |
| " outs = ['out/foo3.txt'],", | |
| " cmd = 'echo foo3 > $@',", | |
| ")"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo1", "//:foo2", "//:foo3"); | |
| assertValidOutputFile("out/foo1.txt", "foo1\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo1").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| assertValidOutputFile("out/foo2.txt", "foo2\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo2").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| assertValidOutputFile("out/foo3.txt", "foo3\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo3").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| } | |
| @Test | |
| public void downloadToplevel_incrementalBuild_multipleToplevelTargets() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo1',", | |
| " srcs = [],", | |
| " outs = ['out/foo1.txt'],", | |
| " cmd = 'echo foo1 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo2',", | |
| " srcs = [],", | |
| " outs = ['out/foo2.txt'],", | |
| " cmd = 'echo foo2 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo3',", | |
| " srcs = [],", | |
| " outs = ['out/foo3.txt'],", | |
| " cmd = 'echo foo3 > $@',", | |
| ")"); | |
| buildTarget("//:foo1", "//:foo2", "//:foo3"); | |
| assertOutputsDoNotExist("//:foo1"); | |
| assertThat(getMetadata("//:foo1").values().stream().allMatch(FileArtifactValue::isRemote)) | |
| .isTrue(); | |
| assertOutputsDoNotExist("//:foo2"); | |
| assertThat(getMetadata("//:foo2").values().stream().allMatch(FileArtifactValue::isRemote)) | |
| .isTrue(); | |
| assertOutputsDoNotExist("//:foo3"); | |
| assertThat(getMetadata("//:foo3").values().stream().allMatch(FileArtifactValue::isRemote)) | |
| .isTrue(); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo1", "//:foo2", "//:foo3"); | |
| assertValidOutputFile("out/foo1.txt", "foo1\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo1").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| assertValidOutputFile("out/foo2.txt", "foo2\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo2").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| assertValidOutputFile("out/foo3.txt", "foo3\n"); | |
| // TODO(chiwang): Make metadata for downloaded outputs local. | |
| // assertThat(getMetadata("//:foo3").values().stream().noneMatch(FileArtifactValue::isRemote)) | |
| // .isTrue(); | |
| } | |
| @Test | |
| public void downloadToplevel_incrementalBuild_anotherTarget() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo1',", | |
| " srcs = [':foo3'],", | |
| " outs = ['out/foo1.txt'],", | |
| " cmd = 'echo foo1 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo2',", | |
| " srcs = [],", | |
| " outs = ['out/foo2.txt'],", | |
| " cmd = 'echo foo2 > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo3',", | |
| " srcs = [],", | |
| " outs = ['out/foo3.txt'],", | |
| " cmd = 'echo foo3 > $@',", | |
| ")"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo1"); | |
| assertValidOutputFile("out/foo1.txt", "foo1\n"); | |
| assertOutputsDoNotExist("//:foo2"); | |
| assertOutputsDoNotExist("//:foo3"); | |
| buildTarget("//:foo3"); | |
| assertOutputsDoNotExist("//:foo2"); | |
| assertValidOutputFile("out/foo3.txt", "foo3\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_symlinkToGeneratedFile() throws Exception { | |
| setDownloadToplevel(); | |
| writeSymlinkRule(); | |
| write( | |
| "BUILD", | |
| "load(':symlink.bzl', 'symlink')", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "symlink(", | |
| " name = 'foo-link',", | |
| " target_artifact = ':foo',", | |
| ")"); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("out/foo.txt").asFragment()); | |
| assertValidOutputFile("foo-link", "foo\n"); | |
| // Delete link, re-plant symlink | |
| getOutputPath("foo-link").delete(); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("out/foo.txt").asFragment()); | |
| assertValidOutputFile("foo-link", "foo\n"); | |
| // Delete target, re-download it | |
| getOutputPath("out/foo.txt").delete(); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("out/foo.txt").asFragment()); | |
| assertValidOutputFile("foo-link", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_symlinkToSourceFile() throws Exception { | |
| setDownloadToplevel(); | |
| writeSymlinkRule(); | |
| write( | |
| "BUILD", | |
| "load(':symlink.bzl', 'symlink')", | |
| "symlink(", | |
| " name = 'foo-link',", | |
| " target_artifact = ':foo.txt',", | |
| ")"); | |
| write("foo.txt", "foo"); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getSourcePath("foo.txt").asFragment()); | |
| assertOnlyOutputContent("//:foo-link", "foo-link", "foo\n"); | |
| // Delete link, re-plant symlink | |
| getOutputPath("foo-link").delete(); | |
| buildTarget("//:foo-link"); | |
| assertOnlyOutputContent("//:foo-link", "foo-link", "foo\n"); | |
| } | |
| @Test | |
| public void downloadToplevel_symlinkToDirectory() throws Exception { | |
| setDownloadToplevel(); | |
| writeSymlinkRule(); | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "load(':symlink.bzl', 'symlink')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")", | |
| "symlink(", | |
| " name = 'foo-link',", | |
| " target_artifact = ':foo',", | |
| ")"); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("foo").asFragment()); | |
| assertValidOutputFile("foo-link/file-1", "1"); | |
| assertValidOutputFile("foo-link/file-2", "2"); | |
| assertValidOutputFile("foo-link/file-3", "3"); | |
| // Delete link, re-plant symlink | |
| getOutputPath("foo-link").deleteTree(); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("foo").asFragment()); | |
| assertValidOutputFile("foo-link/file-1", "1"); | |
| assertValidOutputFile("foo-link/file-2", "2"); | |
| assertValidOutputFile("foo-link/file-3", "3"); | |
| // Delete target, re-download them | |
| getOutputPath("foo").deleteTree(); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", getOutputPath("foo").asFragment()); | |
| assertValidOutputFile("foo-link/file-1", "1"); | |
| assertValidOutputFile("foo-link/file-2", "2"); | |
| assertValidOutputFile("foo-link/file-3", "3"); | |
| } | |
| @Test | |
| public void downloadToplevel_unresolvedSymlink(@TestParameter SymlinkTargetType targetType) | |
| throws Exception { | |
| Path targetPath = TestUtils.createUniqueTmpDir(null).getChild("target"); | |
| String targetPathArg = targetPath.getPathString(); | |
| String targetTypeArg = | |
| switch (targetType) { | |
| case FILE -> "file"; | |
| case DIRECTORY -> "directory"; | |
| case UNSPECIFIED -> ""; | |
| }; | |
| setDownloadToplevel(); | |
| writeSymlinkRule(); | |
| write( | |
| "BUILD", | |
| """ | |
| load(':symlink.bzl', 'symlink') | |
| symlink( | |
| name = 'foo-link', | |
| target_path = '%s', | |
| target_type = '%s', | |
| ) | |
| """ | |
| .formatted(targetPathArg, targetTypeArg)); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", targetPath.asFragment()); | |
| // Delete link, re-plant symlink | |
| getOutputPath("foo-link").delete(); | |
| buildTarget("//:foo-link"); | |
| assertSymlink("foo-link", targetPath.asFragment()); | |
| // Assert that the symlink works after planting the target. | |
| if (targetType == SymlinkTargetType.FILE) { | |
| FileSystemUtils.writeContent(targetPath, UTF_8, "hello world"); | |
| assertThat(FileSystemUtils.readContent(getOutputPath("foo-link"), UTF_8)) | |
| .isEqualTo("hello world"); | |
| } else if (targetType == SymlinkTargetType.DIRECTORY) { | |
| targetPath.createDirectory(); | |
| FileSystemUtils.writeContent(targetPath.getChild("file.txt"), UTF_8, "hello world"); | |
| assertThat(FileSystemUtils.readContent(getOutputPath("foo-link/file.txt"), UTF_8)) | |
| .isEqualTo("hello world"); | |
| } | |
| } | |
| @Test | |
| public void treeOutputsFromLocalFileSystem_works( | |
| @TestParameter({"no-remote-exec", "local"}) String executionInfo) throws Exception { | |
| // Test that tree artifact generated locally can be consumed by other actions. | |
| // See https://github.com/bazelbuild/bazel/issues/16789 | |
| // Disable remote execution so tree outputs are generated locally | |
| addOptions("--modify_execution_info=OutputDir=+" + executionInfo); | |
| setDownloadToplevel(); | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1'},", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo)/file-1 > $@ && echo bar >> $@',", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foobar.txt", "1bar\n"); | |
| } | |
| @Test | |
| public void emptyTreeConsumedByLocalAction() throws Exception { | |
| // Disable remote execution so that the empty tree artifact is prefetched. | |
| addOptions("--modify_execution_info=Genrule=+no-remote-exec"); | |
| addOptions("--verbose_failures"); | |
| setDownloadToplevel(); | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {},", // no files | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['foobar.txt'],", | |
| " cmd = 'touch $@',", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| } | |
| @Test | |
| public void multiplePackagePaths_buildsSuccessfully() throws Exception { | |
| write( | |
| "../a/src/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = [], | |
| outs = ["out/foo.txt"], | |
| cmd = "echo foo > $@", | |
| ) | |
| """); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = ['//src:foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location //src:foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| addOptions("--package_path=%workspace%:%workspace%/../a"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| } | |
| @Test | |
| public void incrementalBuild_unwritableParentDirectory_outputExists() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'unwritable',", | |
| " srcs = ['file.in'],", | |
| " outs = ['unwritable/somefile.out'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| " local = True,", | |
| ")"); | |
| write("file.in", "content"); | |
| buildTarget("//:unwritable"); | |
| getOutputPath("unwritable").setWritable(false); | |
| write("file.in", "updated content"); | |
| buildTarget("//:unwritable"); | |
| } | |
| @Test | |
| public void incrementalBuild_unwritableParentDirectory_outputDoesNotExist() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'unwritable',", | |
| " srcs = ['file.in'],", | |
| " outs = ['unwritable/somefile.out'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| " local = True,", | |
| ")"); | |
| write("file.in", "content"); | |
| buildTarget("//:unwritable"); | |
| getOutputPath("unwritable/somefile.out").delete(); | |
| getOutputPath("unwritable").setWritable(false); | |
| write("file.in", "updated content"); | |
| buildTarget("//:unwritable"); | |
| } | |
| @Test | |
| public void incrementalBuild_treeArtifacts_correctlyProducesNewTree() throws Exception { | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")"); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-4': '4'},", | |
| ")"); | |
| restartServer(); | |
| setDownloadToplevel(); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| assertValidOutputFile("foo/file-1", "1"); | |
| assertValidOutputFile("foo/file-4", "4"); | |
| assertOutputDoesNotExist("foo/file-2"); | |
| assertOutputDoesNotExist("foo/file-3"); | |
| } | |
| @Test | |
| public void incrementalBuild_restartServer_hitActionCache() throws Exception { | |
| // Prepare workspace | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| // Clean build | |
| buildTarget("//:foobar"); | |
| // all action should be executed | |
| assertThat(actionEventCollector.getActionExecutedEvents()).hasSize(3); | |
| // no outputs are staged | |
| assertOutputsDoNotExist("//:foobar"); | |
| restartServer(); | |
| actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| // Incremental build | |
| buildTarget("//:foobar"); | |
| // all actions should hit the action cache. | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| // no outputs are staged | |
| assertOutputsDoNotExist("//:foobar"); | |
| } | |
| @Test | |
| public void incrementalBuild_sourceModified_rerunActions() throws Exception { | |
| // Arrange: Prepare workspace and run a clean build | |
| write("foo.in", "foo"); | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = ['foo.in'],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'cat $(SRCS) > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| // Act: Modify source file and run an incremental build | |
| write("foo.in", "modified"); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| // Assert: All actions transitively depend on the source file are re-executed and outputs are | |
| // correct. | |
| assertValidOutputFile("out/foo.txt", "modified\n"); | |
| assertValidOutputFile("out/foobar.txt", "modified\nbar\n"); | |
| assertThat(actionEventCollector.getNumActionNodesEvaluated()).isEqualTo(2); | |
| } | |
| @Test | |
| public void incrementalBuild_intermediateOutputDeleted_nothingIsReEvaluated() throws Exception { | |
| // Arrange: Prepare workspace and run a clean build | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| // Act: Delete intermediate output and run an incremental build | |
| var fooPath = getOutputPath("out/foo.txt"); | |
| fooPath.delete(); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| // Assert: local output is deleted, skyframe should trust remote files so no nodes will be | |
| // re-evaluated. | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(actionEventCollector.getNumActionNodesEvaluated()).isEqualTo(0); | |
| } | |
| @Test | |
| public void downloadToplevel_afterInfoWithDownloadAll_doesNotReevaluateRemoteOutputs() | |
| throws Exception { | |
| if (!hasAccessToRemoteOutputs()) { | |
| return; | |
| } | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| ")"); | |
| // Leave both outputs represented only by remote metadata. | |
| buildTarget("//:foobar"); | |
| assertOutputsDoNotExist("//:foo"); | |
| assertOutputsDoNotExist("//:foobar"); | |
| // Install a previous checker with download-all, without running a build or materializing any | |
| // outputs. | |
| setDownloadAll(); | |
| addOptions("workspace"); | |
| runtimeWrapper.newCommand(InfoCommand.class); | |
| runtimeWrapper.executeCustomCommand(); | |
| setDownloadToplevel(); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| waitDownloads(); | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(actionEventCollector.getNumActionNodesEvaluated()).isEqualTo(0); | |
| } | |
| @Test | |
| public void downloadToplevel_afterDownloadAllBuildOfAnotherTarget_doesNotReexecuteActions() | |
| throws Exception { | |
| writeAppWithLibs(); | |
| setDownloadToplevel(); | |
| buildTarget("//:app"); | |
| assertValidOutputFile("out/app.txt", "lib0\nlib1\nlib2\n"); | |
| assertOutputsDoNotExist("//:lib0"); | |
| assertOutputsDoNotExist("//:lib1"); | |
| assertOutputsDoNotExist("//:lib2"); | |
| // Build an unrelated target with a broader download policy, as e.g. an IDE project generator | |
| // does, then run an incremental build without changes. | |
| setDownloadAll(); | |
| buildTarget("//:tool"); | |
| setDownloadToplevel(); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:app"); | |
| // The intermediate outputs are still trusted, so their actions hit the action cache instead of | |
| // being re-executed remotely. | |
| assertOutputsDoNotExist("//:lib0"); | |
| assertOutputsDoNotExist("//:lib1"); | |
| assertOutputsDoNotExist("//:lib2"); | |
| assertValidOutputFile("out/app.txt", "lib0\nlib1\nlib2\n"); | |
| if (hasAccessToRemoteOutputs()) { | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| } else { | |
| assertThat( | |
| actionEventCollector.getActionExecutedEvents().stream() | |
| .map(e -> e.getAction().getOwner().getLabel().toString())) | |
| .containsNoneOf("//:lib0", "//:lib1", "//:lib2"); | |
| } | |
| } | |
| @Test | |
| public void downloadToplevel_afterDownloadAllBuildOfAnotherTarget_onlyModifiedActionsRerun() | |
| throws Exception { | |
| writeAppWithLibs(); | |
| setDownloadToplevel(); | |
| buildTarget("//:app"); | |
| assertValidOutputFile("out/app.txt", "lib0\nlib1\nlib2\n"); | |
| setDownloadAll(); | |
| buildTarget("//:tool"); | |
| setDownloadToplevel(); | |
| write("lib0.in", "modified"); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:app"); | |
| // Only the actions depending on the modified source are re-executed. | |
| assertOutputsDoNotExist("//:lib1"); | |
| assertOutputsDoNotExist("//:lib2"); | |
| assertValidOutputFile("out/app.txt", "modified\nlib1\nlib2\n"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).hasSize(2); | |
| } | |
| @Test | |
| public void downloadMinimal_afterDownloadAllBuildOfAnotherTarget_doesNotReexecuteActions() | |
| throws Exception { | |
| writeAppWithLibs(); | |
| buildTarget("//:app"); | |
| assertOutputsDoNotExist("//:app"); | |
| assertOutputsDoNotExist("//:lib0"); | |
| // Build an unrelated target with a broader download policy, then run an incremental build | |
| // without changes. | |
| setDownloadAll(); | |
| buildTarget("//:tool"); | |
| setDownloadMinimal(); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:app"); | |
| // Nothing is downloaded and no action is re-executed. | |
| assertOutputsDoNotExist("//:app"); | |
| assertOutputsDoNotExist("//:lib0"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| } | |
| @Test | |
| public void incrementalBuild_fileOutputIsPrefetched_noRuns() throws Exception { | |
| // We need to download the intermediate output | |
| if (!hasAccessToRemoteOutputs()) { | |
| return; | |
| } | |
| // Arrange: Prepare workspace and run a clean build | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(getOnlyElement(getMetadata("//:foo").values()).isRemote()).isTrue(); | |
| // Act: Do an incremental build without any modifications | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| // Assert: remote file metadata has contents proxy and action node is not marked as dirty. | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| assertThat(actionEventCollector.getCachedActionEvents()).isEmpty(); | |
| var metadata = getOnlyElement(getMetadata("//:foo").values()); | |
| assertThat(metadata.isRemote()).isTrue(); | |
| assertThat(metadata.getContentsProxy()).isNotNull(); | |
| } | |
| @Test | |
| public void incrementalBuild_treeOutputIsPrefetched_noRuns() throws Exception { | |
| // We need to download the intermediate output | |
| if (!hasAccessToRemoteOutputs()) { | |
| return; | |
| } | |
| // Arrange: Prepare workspace and run a clean build | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| "load(':output_dir.bzl', 'output_dir')", | |
| "output_dir(", | |
| " name = 'foo',", | |
| " content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'},", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'echo bar >> $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("foo/file-1", "1"); | |
| assertValidOutputFile("foo/file-2", "2"); | |
| assertValidOutputFile("foo/file-3", "3"); | |
| assertValidOutputFile("out/foobar.txt", "bar\n"); | |
| assertThat(getOnlyElement(getTreeMetadata("//:foo").values()).isEntirelyRemote()).isTrue(); | |
| // Act: Do an incremental build without any modifications | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| // Assert: action node is not marked as dirty. | |
| assertValidOutputFile("foo/file-1", "1"); | |
| assertValidOutputFile("foo/file-2", "2"); | |
| assertValidOutputFile("foo/file-3", "3"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).isEmpty(); | |
| assertThat(actionEventCollector.getCachedActionEvents()).isEmpty(); | |
| } | |
| protected ImmutableMap<Artifact, FileArtifactValue> getMetadata(String target) throws Exception { | |
| var result = ImmutableMap.<Artifact, FileArtifactValue>builder(); | |
| var evaluator = getRuntimeWrapper().getSkyframeExecutor().getEvaluator(); | |
| for (var artifact : getArtifacts(target)) { | |
| var value = evaluator.getExistingValue(Artifact.key(artifact)); | |
| if (value instanceof ActionExecutionValue actionExecutionValue) { | |
| result.putAll(actionExecutionValue.getAllFileValues()); | |
| } else if (value instanceof TreeArtifactValue treeArtifactValue) { | |
| result.putAll(treeArtifactValue.getChildValues()); | |
| } | |
| } | |
| return result.buildOrThrow(); | |
| } | |
| protected FileArtifactValue getMetadata(Artifact output) throws Exception { | |
| var evaluator = getRuntimeWrapper().getSkyframeExecutor().getEvaluator(); | |
| var value = evaluator.getExistingValue(Artifact.key(output)); | |
| if (value instanceof ActionExecutionValue actionExecutionValue) { | |
| return actionExecutionValue.getAllFileValues().get(output); | |
| } else if (value instanceof TreeArtifactValue treeArtifactValue) { | |
| return treeArtifactValue.getChildValues().get(output); | |
| } | |
| return null; | |
| } | |
| protected ImmutableMap<Artifact, TreeArtifactValue> getTreeMetadata(String target) | |
| throws Exception { | |
| var result = ImmutableMap.<Artifact, TreeArtifactValue>builder(); | |
| var evaluator = getRuntimeWrapper().getSkyframeExecutor().getEvaluator(); | |
| for (var artifact : getArtifacts(target)) { | |
| var value = evaluator.getExistingValue(Artifact.key(artifact)); | |
| if (value instanceof ActionExecutionValue actionExecutionValue) { | |
| result.putAll(actionExecutionValue.getAllTreeArtifactValues()); | |
| } else if (value instanceof TreeArtifactValue treeArtifactValue) { | |
| result.put(artifact, treeArtifactValue); | |
| } | |
| } | |
| return result.buildOrThrow(); | |
| } | |
| @Test | |
| public void incrementalBuild_intermediateOutputModified_rerunGeneratingActions() | |
| throws Exception { | |
| // Arrange: Prepare workspace and run a clean build | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['out/foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo'],", | |
| " outs = ['out/foobar.txt'],", | |
| " cmd = 'cat $(location :foo) > $@ && echo bar >> $@',", | |
| " tags = ['no-remote'],", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("out/foo.txt", "foo\n"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| // Act: Modify the intermediate output and run a incremental build | |
| var fooPath = getOutputPath("out/foo.txt"); | |
| fooPath.delete(); | |
| writeAbsolute(fooPath, "modified"); | |
| ActionEventCollector actionEventCollector = new ActionEventCollector(); | |
| getRuntimeWrapper().registerSubscriber(actionEventCollector); | |
| buildTarget("//:foobar"); | |
| // Assert: the stale intermediate file should be deleted by skyframe before executing the | |
| // generating action. Since download minimal, the output didn't get downloaded. Since the input | |
| // to action :foobar didn't change, we hit the skyframe cache, so the action node didn't event | |
| // get evaluated. The input didn't get prefetched neither. | |
| assertOutputDoesNotExist("out/foo.txt"); | |
| assertValidOutputFile("out/foobar.txt", "foo\nbar\n"); | |
| assertThat(actionEventCollector.getActionExecutedEvents()).hasSize(1); | |
| assertThat(actionEventCollector.getCachedActionEvents()).isEmpty(); | |
| var executedAction = actionEventCollector.getActionExecutedEvents().get(0).getAction(); | |
| assertThat(executedAction.getPrimaryOutput().getFilename()).isEqualTo("foo.txt"); | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenPrefetchingInputFile(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| write("a/foo.in", "foo"); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| buildTarget("//a:bar"); | |
| getOutputPath("a/foo.out").delete(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out"); | |
| // Evict blobs from remote cache | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| if (actionRewinding) { | |
| // The lost input's generating action is rewound within the next build. | |
| enableActionRewinding(); | |
| } else { | |
| // The build fails because of remote cache eviction, but an incremental build without | |
| // "clean" or "shutdown" can continue. | |
| disableActionRewinding(); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| } | |
| // Act: Do an incremental build without "clean" or "shutdown" | |
| buildTarget("//a:bar"); | |
| // Assert: target was successfully built | |
| assertValidOutputFile("a/bar.out", "foo\nupdated bar\n"); | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenPrefetchingInputTree(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write("BUILD"); | |
| writeOutputDirRule(); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load("//:output_dir.bzl", "output_dir") | |
| output_dir( | |
| name = "foo.out", | |
| content_map = {"file-inside": "hello world"}, | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "( ls $(location :foo.out); cat $(location :bar.in) ) > $@", | |
| ) | |
| """); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| buildTarget("//a:bar"); | |
| getOutputPath("a/foo.out").deleteTreesBelow(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out/file-inside"); | |
| // Evict blobs from remote cache | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| if (actionRewinding) { | |
| // The lost input's generating action is rewound within the next build. | |
| enableActionRewinding(); | |
| } else { | |
| // The build fails because of remote cache eviction, but an incremental build without | |
| // "clean" or "shutdown" can continue. | |
| disableActionRewinding(); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| } | |
| // Act: Do an incremental build without "clean" or "shutdown" | |
| buildTarget("//a:bar"); | |
| // Assert: target was successfully built | |
| assertValidOutputFile("a/bar.out", "file-inside\nupdated bar\n"); | |
| } | |
| @Test | |
| public void nonDeclaredSymlinksFromLocalActions() throws Exception { | |
| write( | |
| "BUILD", | |
| "genrule(", | |
| " name = 'foo',", | |
| " srcs = [],", | |
| " outs = ['foo.txt'],", | |
| " cmd = 'echo foo > $@',", | |
| ")", | |
| "genrule(", | |
| " name = 'foo-link',", | |
| " srcs = [':foo'],", | |
| " outs = ['foo.link'],", | |
| " cmd = 'ln -s foo.txt $@',", | |
| " local = True,", | |
| ")", | |
| "genrule(", | |
| " name = 'foobar',", | |
| " srcs = [':foo-link'],", | |
| " outs = ['foobar.txt'],", | |
| " cmd = 'cat $(location :foo-link) > $@ && echo bar >> $@',", | |
| " local = True,", | |
| ")"); | |
| buildTarget("//:foobar"); | |
| assertValidOutputFile("foobar.txt", "foo\nbar\n"); | |
| } | |
| @Test | |
| public void skymeldPromoIntermediateTargetToToplevel_outputFile_downloadFile() throws Exception { | |
| // Regression test for https://github.com/bazelbuild/bazel/issues/20737. | |
| // Disable on Windows since mkfifo doesn't work there. | |
| assumeFalse(OS.getCurrent() == OS.WINDOWS); | |
| write( | |
| "BUILD", | |
| """ | |
| filegroup(name = "top", srcs = [":actual", "//slow"]) | |
| genrule(name = "proxy", srcs = [":actual"], outs = ["proxy_file"], cmd = "cp $< $@") | |
| genrule(name = "actual", srcs = [], outs = ["actual_file"], cmd = "echo ACTUAL > $@") | |
| """); | |
| getWorkspace().getRelative("slow").createDirectoryAndParents(); | |
| // Only write the content of slow/BUILD after //:proxy is built, so we can artificially delay | |
| // the analysis of //:top | |
| var unused = | |
| new CommandBuilder(System.getenv()) | |
| .addArgs("mkfifo", "slow/BUILD") | |
| .setWorkingDir(getWorkspace()) | |
| .build() | |
| .execute(); | |
| buildTarget("//:proxy"); | |
| restartServer(); | |
| getRuntimeWrapper() | |
| .registerSubscriber( | |
| new Object() { | |
| @Subscribe | |
| public void onTargetCompleted(TargetCompleteEvent event) { | |
| if (event.getLabel().toString().equals("//:proxy")) { | |
| try { | |
| write( | |
| "slow/BUILD", | |
| "filegroup(name = 'slow', visibility = ['//visibility:public'])"); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| } | |
| }); | |
| setDownloadToplevel(); | |
| buildTarget("//:top", "//:proxy"); | |
| waitDownloads(); | |
| assertValidOutputFile("actual_file", "ACTUAL\n"); | |
| } | |
| @Test | |
| public void skymeldPromoIntermediateTargetToToplevel_outputDirectory_downloadDirectory() | |
| throws Exception { | |
| // Regression test for https://github.com/bazelbuild/bazel/issues/20737. | |
| // Disable on Windows since mkfifo doesn't work there. | |
| assumeFalse(OS.getCurrent() == OS.WINDOWS); | |
| writeOutputDirRule(); | |
| write( | |
| "BUILD", | |
| """ | |
| load(':output_dir.bzl', 'output_dir') | |
| filegroup(name = "top", srcs = [":actual", "//slow"]) | |
| genrule(name = "proxy", srcs = [":actual"], outs = ["proxy_file"], cmd = "cp $</file-1 $@") | |
| output_dir( | |
| name = "actual", | |
| content_map = {'file-1': '1', 'file-2': '2', 'file-3': '3'} | |
| ) | |
| """); | |
| getWorkspace().getRelative("slow").createDirectoryAndParents(); | |
| // Only write the content of slow/BUILD after //:proxy is built, so we can artificially delay | |
| // the analysis of //:top | |
| var unused = | |
| new CommandBuilder(System.getenv()) | |
| .addArgs("mkfifo", "slow/BUILD") | |
| .setWorkingDir(getWorkspace()) | |
| .build() | |
| .execute(); | |
| buildTarget("//:proxy"); | |
| restartServer(); | |
| getRuntimeWrapper() | |
| .registerSubscriber( | |
| new Object() { | |
| @Subscribe | |
| public void onTargetCompleted(TargetCompleteEvent event) { | |
| if (event.getLabel().toString().equals("//:proxy")) { | |
| try { | |
| write( | |
| "slow/BUILD", | |
| "filegroup(name = 'slow', visibility = ['//visibility:public'])"); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| } | |
| }); | |
| setDownloadToplevel(); | |
| buildTarget("//:top", "//:proxy"); | |
| waitDownloads(); | |
| assertValidOutputFile("actual/file-1", "1"); | |
| assertValidOutputFile("actual/file-2", "2"); | |
| assertValidOutputFile("actual/file-3", "3"); | |
| } | |
| @Test | |
| public void testShareableActionOutputsAsInputs() throws Exception { | |
| write( | |
| "defs.bzl", | |
| """ | |
| def _generate_shared_file(ctx): | |
| output = ctx.actions.declare_file("shared.txt") | |
| ctx.actions.run_shell( | |
| outputs = [output], | |
| command = "echo -n 'shared content' > %s" % output.path, | |
| ) | |
| return [DefaultInfo(files=depset([output]))] | |
| generate_shared_file = rule(_generate_shared_file) | |
| """); | |
| write( | |
| "BUILD", | |
| """ | |
| load(":defs.bzl", "generate_shared_file") | |
| generate_shared_file(name = "gen1") | |
| generate_shared_file(name = "gen2") | |
| genrule( | |
| name = "consume_outputs", | |
| srcs = [":gen1", ":gen2"], | |
| outs = ["combined_output.txt"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| buildTarget("//:consume_outputs"); | |
| assertOnlyOutputRemoteContent( | |
| "//:consume_outputs", "combined_output.txt", "shared contentshared content"); | |
| assertOnlyOutputRemoteContent("//:gen1", "shared.txt", "shared content"); | |
| assertOnlyOutputRemoteContent("//:gen2", "shared.txt", "shared content"); | |
| } | |
| @Test | |
| public void runAfterBuild_keepsCompletionsOfPreviousBuild() throws Exception { | |
| writeFooAndBar(); | |
| var completionStats = new SkyframeEvaluationCollector(SkyFunctions.TARGET_COMPLETION); | |
| getRuntimeWrapper().registerSubscriber(completionStats); | |
| buildTarget("//:foo", "//:bar"); | |
| waitDownloads(); | |
| assertOutputsDoNotExist("//:foo"); | |
| assertOutputsDoNotExist("//:bar"); | |
| assertThat(completionStats.recomputed()).isEqualTo(2); | |
| // Only the build phase of the run command is exercised here; it is the same one that the build | |
| // command goes through, except for the command name recorded in the request. | |
| runtimeWrapper.newCustomCommandWithExtensions( | |
| new RunCommand(TestPolicy.EMPTY_POLICY), | |
| /* extensions= */ ImmutableList.of(), | |
| /* ignoreUserOptions= */ true); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| // The run command always needs the outputs of the target it runs, so its completion is | |
| // evaluated anew under a key of its own... | |
| assertValidOutputFile("foo.txt", "foo\n"); | |
| assertOutputsDoNotExist("//:bar"); | |
| assertThat(completionStats.recomputed()).isEqualTo(1); | |
| // ...while the completions of the preceding build are left in place. | |
| assertThat(targetCompletions()) | |
| .containsExactly("//:foo (build)", "//:bar (build)", "//:foo (run)"); | |
| buildTarget("//:foo", "//:bar"); | |
| waitDownloads(); | |
| // Since nothing was invalidated, returning to the build command leaves no completion work. | |
| assertThat(completionStats.recomputed()).isEqualTo(0); | |
| } | |
| @Test | |
| public void downloadOutputsModeChange_discardsCompletionsOfPreviousBuild() throws Exception { | |
| writeFooAndBar(); | |
| var completionStats = new SkyframeEvaluationCollector(SkyFunctions.TARGET_COMPLETION); | |
| getRuntimeWrapper().registerSubscriber(completionStats); | |
| buildTarget("//:foo", "//:bar"); | |
| // Add the new option here because waitDownloads below will internally create a new command | |
| // which will parse the new option. | |
| setDownloadToplevel(); | |
| waitDownloads(); | |
| assertThat(completionStats.recomputed()).isEqualTo(2); | |
| buildTarget("//:foo"); | |
| waitDownloads(); | |
| // A change to the outputs mode may affect any target, so all completions are discarded and | |
| // only the one requested by this build is recomputed... | |
| assertValidOutputFile("foo.txt", "foo\n"); | |
| assertThat(completionStats.recomputed()).isEqualTo(1); | |
| assertThat(targetCompletions()).containsExactly("//:foo (build)"); | |
| buildTarget("//:foo", "//:bar"); | |
| waitDownloads(); | |
| // ...leaving //:bar to be completed again by the next build. | |
| assertThat(completionStats.recomputed()).isEqualTo(1); | |
| } | |
| private void writeFooAndBar() throws IOException { | |
| write( | |
| "BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| outs = ["foo.txt"], | |
| cmd = "echo foo > $@", | |
| ) | |
| genrule( | |
| name = "bar", | |
| outs = ["bar.txt"], | |
| cmd = "echo bar > $@", | |
| ) | |
| """); | |
| } | |
| /** | |
| * Describes every target completion node in the Skyframe graph by the label it completes and the | |
| * command mode of its {@link com.google.devtools.build.lib.analysis.TopLevelArtifactContext}. | |
| * | |
| * <p>Nodes that are merely dirty are still reported; only deleted ones are missing. | |
| */ | |
| private ImmutableSet<String> targetCompletions() { | |
| return getSkyframeExecutor().getEvaluator().getValues().keySet().stream() | |
| .filter(key -> key.functionName().equals(SkyFunctions.TARGET_COMPLETION)) | |
| .map(TargetCompletionKey.class::cast) | |
| .map( | |
| key -> | |
| "%s (%s)" | |
| .formatted( | |
| key.actionLookupKey().getLabel(), | |
| key.topLevelArtifactContext().forRunCommand() ? "run" : "build")) | |
| .collect(toImmutableSet()); | |
| } | |
| protected void assertOutputsDoNotExist(String target) throws Exception { | |
| for (Artifact output : getArtifacts(target)) { | |
| assertWithMessage( | |
| "output %s for target %s should not exist", output.getExecPathString(), target) | |
| .that(output.getPath().exists()) | |
| .isFalse(); | |
| } | |
| } | |
| protected Path getSourcePath(String relativePath) { | |
| return getDirectories().getWorkspace().getRelative(relativePath); | |
| } | |
| protected Path getOutputPath(String binRelativePath) { | |
| return getTargetConfiguration().getBinDir().getRoot().getRelative(binRelativePath); | |
| } | |
| protected void assertOutputDoesNotExist(String binRelativePath) throws IOException { | |
| Path output = getOutputPath(binRelativePath); | |
| assertThat(output.exists()).isFalse(); | |
| } | |
| protected void assertOnlyOutputContent(String target, String filename, String content) | |
| throws Exception { | |
| Artifact output = getOnlyElement(getArtifacts(target)); | |
| assertThat(output.getFilename()).isEqualTo(filename); | |
| assertThat(output.getPath().exists()).isTrue(); | |
| assertOutputEquals(output.getPath(), content); | |
| } | |
| protected void assertOnlyOutputRemoteContent(String target, String filename, String content) | |
| throws Exception { | |
| Artifact output = getOnlyElement(getArtifacts(target)); | |
| assertThat(output.getFilename()).isEqualTo(filename); | |
| assertThat(output.getPath().exists()).isFalse(); | |
| var metadata = getOnlyElement(getMetadata(target).values()); | |
| assertThat(metadata.isRemote()).isTrue(); | |
| assertThat(metadata.getSize()).isEqualTo(content.length()); | |
| assertThat(metadata.getDigest()) | |
| .isEqualTo(getDigestHashFunction().getHashFunction().hashString(content, UTF_8).asBytes()); | |
| } | |
| protected void assertValidOutputFile(String binRelativePath, String content) throws Exception { | |
| Path output = getOutputPath(binRelativePath); | |
| assertOutputEquals(getOutputPath(binRelativePath), content); | |
| assertThat(output.isReadable()).isTrue(); | |
| assertThat(output.isWritable()).isFalse(); | |
| assertThat(output.isExecutable()).isTrue(); | |
| } | |
| protected void assertSymlink(String binRelativeLinkPath, PathFragment targetPath) | |
| throws Exception { | |
| // On Windows, readSymbolicLink() always returns an absolute path. | |
| if (OS.getCurrent() == OS.WINDOWS && !targetPath.isAbsolute()) { | |
| targetPath = | |
| getOutputPath(binRelativeLinkPath) | |
| .getParentDirectory() | |
| .getRelative(targetPath) | |
| .asFragment(); | |
| } | |
| Path output = getOutputPath(binRelativeLinkPath); | |
| assertThat(output.isSymbolicLink()).isTrue(); | |
| assertThat(output.readSymbolicLink()).isEqualTo(targetPath); | |
| } | |
| protected void writeSymlinkRule() throws IOException { | |
| FileSystemUtils.touchFile(getWorkspace().getRelative("BUILD")); | |
| write( | |
| "symlink.bzl", | |
| """ | |
| def _symlink_impl(ctx): | |
| if ctx.file.target_artifact and not ctx.attr.target_path: | |
| if ctx.file.target_artifact.is_directory: | |
| link = ctx.actions.declare_directory(ctx.attr.name) | |
| else: | |
| link = ctx.actions.declare_file(ctx.attr.name) | |
| ctx.actions.symlink(output = link, target_file = ctx.file.target_artifact) | |
| elif ctx.attr.target_path and not ctx.file.target_artifact: | |
| link = ctx.actions.declare_symlink(ctx.attr.name) | |
| ctx.actions.symlink( | |
| output = link, | |
| target_path = ctx.attr.target_path, | |
| target_type = ctx.attr.target_type or None, | |
| ) | |
| else: | |
| fail("exactly one of target_artifact or target_path must be set") | |
| return DefaultInfo(files = depset([link])) | |
| symlink = rule( | |
| implementation = _symlink_impl, | |
| attrs = { | |
| "target_artifact": attr.label(allow_single_file = True), | |
| "target_path": attr.string(), | |
| "target_type": attr.string(), | |
| }, | |
| ) | |
| """); | |
| } | |
| protected void writeOutputDirRule() throws IOException { | |
| write( | |
| "output_dir.bzl", | |
| """ | |
| def _output_dir_impl(ctx): | |
| out = ctx.actions.declare_directory(ctx.attr.name) | |
| args = [] | |
| for name, content in ctx.attr.content_map.items(): | |
| args.append(out.path + "/" + name) | |
| args.append(content) | |
| ctx.actions.run_shell( | |
| mnemonic = "OutputDir", | |
| outputs = [out], | |
| arguments = args, | |
| command = 'while (($#)); do echo -n "$2" > $1; shift 2; done', | |
| ) | |
| return DefaultInfo(files = depset([out])) | |
| output_dir = rule( | |
| implementation = _output_dir_impl, | |
| attrs = { | |
| "content_map": attr.string_dict(mandatory = True), | |
| }, | |
| ) | |
| """); | |
| } | |
| protected void writeAppWithLibs() throws IOException { | |
| write("lib0.in", "lib0"); | |
| write("lib1.in", "lib1"); | |
| write("lib2.in", "lib2"); | |
| write( | |
| "BUILD", | |
| """ | |
| genrule( | |
| name = "lib0", | |
| srcs = ["lib0.in"], | |
| outs = ["out/lib0.txt"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "lib1", | |
| srcs = ["lib1.in"], | |
| outs = ["out/lib1.txt"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "lib2", | |
| srcs = ["lib2.in"], | |
| outs = ["out/lib2.txt"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "app", | |
| srcs = [ | |
| ":lib0", | |
| ":lib1", | |
| ":lib2", | |
| ], | |
| outs = ["out/app.txt"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "tool", | |
| outs = ["out/tool.txt"], | |
| cmd = "echo tool > $@", | |
| ) | |
| """); | |
| } | |
| protected void writeCopyAspectRule(boolean aggregate) throws IOException { | |
| var lines = ImmutableList.<String>builder(); | |
| lines.add( | |
| "def _copy_aspect_impl(target, ctx):", | |
| " files = []", | |
| " for src in ctx.rule.files.srcs:", | |
| " dst = ctx.actions.declare_file(src.basename + '.copy')", | |
| " ctx.actions.run_shell(", | |
| " inputs = [src],", | |
| " outputs = [dst],", | |
| " command = '''", | |
| "cp $1 $2", | |
| "''',", | |
| " arguments = [src.path, dst.path],", | |
| " )", | |
| " files.append(dst)", | |
| ""); | |
| if (aggregate) { | |
| lines.add( | |
| " files = depset(", | |
| " direct = files,", | |
| " transitive = [src[OutputGroupInfo].copy for src in ctx.rule.attr.srcs if" | |
| + " OutputGroupInfo in src],", | |
| " )"); | |
| } else { | |
| lines.add(" files = depset(files)"); | |
| } | |
| lines.add( | |
| "", | |
| " return [OutputGroupInfo(copy = files)]", | |
| "", | |
| "copy_aspect = aspect(", | |
| " implementation = _copy_aspect_impl,", | |
| " attr_aspects = ['srcs'],", | |
| ")"); | |
| write("rules.bzl", lines.build().toArray(new String[0])); | |
| } | |
| /** Records how much of a single {@link SkyFunctionName} the last command made Skyframe redo. */ | |
| protected static class SkyframeEvaluationCollector { | |
| private final SkyFunctionName functionName; | |
| private int recomputed; | |
| public SkyframeEvaluationCollector(SkyFunctionName functionName) { | |
| this.functionName = functionName; | |
| } | |
| @Subscribe | |
| public void onSkyframeGraphStats(SkyframeGraphStatsEvent event) { | |
| var stats = event.getEvaluationStats(); | |
| recomputed = | |
| stats.built().getOrDefault(functionName, 0) | |
| + stats.cleaned().getOrDefault(functionName, 0); | |
| } | |
| /** | |
| * Returns how many nodes of the function the last command had to compute, whether they ended up | |
| * with a new value or were found to be unchanged. Nodes that Skyframe never had to look at are | |
| * not counted. | |
| */ | |
| public int recomputed() { | |
| return recomputed; | |
| } | |
| } | |
| protected static class ActionEventCollector { | |
| private final List<ActionExecutedEvent> actionExecutedEvents = new ArrayList<>(); | |
| private final List<CachedActionEvent> cachedActionEvents = new ArrayList<>(); | |
| @Subscribe | |
| public void onActionExecuted(ActionExecutedEvent event) { | |
| actionExecutedEvents.add(event); | |
| } | |
| @Subscribe | |
| public void onCachedAction(CachedActionEvent event) { | |
| cachedActionEvents.add(event); | |
| } | |
| public int getNumActionNodesEvaluated() { | |
| return getActionExecutedEvents().size() + getCachedActionEvents().size(); | |
| } | |
| public void clear() { | |
| this.actionExecutedEvents.clear(); | |
| this.cachedActionEvents.clear(); | |
| } | |
| public List<ActionExecutedEvent> getActionExecutedEvents() { | |
| return actionExecutedEvents; | |
| } | |
| public List<CachedActionEvent> getCachedActionEvents() { | |
| return cachedActionEvents; | |
| } | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenPrefetchingInput(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| write("a/foo.in", "foo"); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| setDownloadAll(); | |
| buildTarget("//a:bar"); | |
| waitDownloads(); | |
| var bytes = readContent(getOutputPath("a/foo.out")); | |
| var hashCode = getDigestHashFunction().getHashFunction().hashBytes(bytes); | |
| getOutputPath("a/foo.out").delete(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out"); | |
| // Act: Evict blobs from remote cache and do an incremental build | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| if (actionRewinding) { | |
| // Assert: the lost input's generating action is rewound and the build succeeds | |
| enableActionRewinding(); | |
| buildTarget("//a:bar"); | |
| assertValidOutputFile("a/bar.out", "foo\nupdated bar\n"); | |
| } else { | |
| // Assert: the build fails with exit code 39 | |
| disableActionRewinding(); | |
| var error = assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| assertThat(error).hasMessageThat().contains("Lost inputs no longer available remotely"); | |
| assertThat(error).hasMessageThat().contains("a/foo.out"); | |
| assertThat(error).hasMessageThat().contains(String.format("%s/%s", hashCode, bytes.length)); | |
| assertThat(error.getDetailedExitCode().getExitCode().getNumericExitCode()).isEqualTo(39); | |
| } | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenPrefetchingSymlinkedInput( | |
| @TestParameter boolean actionRewinding) throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| writeSymlinkRule(); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load("//:symlink.bzl", "symlink") | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| symlink( | |
| name = "symlinked_foo", | |
| target_artifact = ":foo.out", | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| ":symlinked_foo", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| write("a/foo.in", "foo"); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| setDownloadAll(); | |
| buildTarget("//a:bar"); | |
| waitDownloads(); | |
| var bytes = readContent(getOutputPath("a/foo.out")); | |
| var hashCode = getDigestHashFunction().getHashFunction().hashBytes(bytes); | |
| getOnlyElement(getArtifacts("//a:symlinked_foo")).getPath().delete(); | |
| getOutputPath("a/foo.out").delete(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out"); | |
| assertOutputsDoNotExist("//a:symlinked_foo"); | |
| // Act: Evict blobs from remote cache and do an incremental build | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| if (actionRewinding) { | |
| // Assert: the lost input's generating action is rewound and the build succeeds | |
| enableActionRewinding(); | |
| buildTarget("//a:bar"); | |
| assertValidOutputFile("a/bar.out", "foo\nupdated bar\n"); | |
| } else { | |
| // Assert: the build fails with exit code 39 | |
| disableActionRewinding(); | |
| var error = assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| assertThat(error).hasMessageThat().contains("Lost inputs no longer available remotely"); | |
| assertThat(error).hasMessageThat().contains("a/symlinked_foo"); | |
| assertThat(error).hasMessageThat().contains(String.format("%s/%s", hashCode, bytes.length)); | |
| assertThat(error.getDetailedExitCode().getExitCode().getNumericExitCode()).isEqualTo(39); | |
| } | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenUploadingInput(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| write("a/foo.in", "foo"); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| setDownloadAll(); | |
| buildTarget("//a:bar"); | |
| waitDownloads(); | |
| var bytes = readContent(getOutputPath("a/foo.out")); | |
| var hashCode = getDigestHashFunction().getHashFunction().hashBytes(bytes); | |
| getOutputPath("a/foo.out").delete(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out"); | |
| // Act: Evict blobs from remote cache and do an incremental build | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| if (actionRewinding) { | |
| // Assert: the lost input's generating action is rewound and the build succeeds | |
| enableActionRewinding(); | |
| buildTarget("//a:bar"); | |
| assertOutputsDoNotExist("//a:bar"); | |
| assertOnlyOutputRemoteContent("//a:bar", "bar.out", "foo\nupdated bar\n"); | |
| } else { | |
| // Assert: the build fails with exit code 39 | |
| disableActionRewinding(); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| var error = assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| assertThat(error).hasMessageThat().contains("Lost inputs no longer available remotely"); | |
| assertThat(error).hasMessageThat().contains(String.format("%s/%s", hashCode, bytes.length)); | |
| assertThat(error.getDetailedExitCode().getExitCode().getNumericExitCode()).isEqualTo(39); | |
| } | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenUploadingInputFile(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| srcs = ["foo.in"], | |
| outs = ["foo.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "cat $(SRCS) > $@", | |
| ) | |
| """); | |
| write("a/foo.in", "foo"); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| buildTarget("//a:bar"); | |
| getOutputPath("a/foo.out").delete(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| setDownloadToplevel(); | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out"); | |
| // Evict blobs from remote cache | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| if (actionRewinding) { | |
| // The lost input's generating action is rewound within the next build. | |
| enableActionRewinding(); | |
| } else { | |
| // The build fails because of remote cache eviction, but an incremental build without | |
| // "clean" or "shutdown" can continue. | |
| disableActionRewinding(); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| } | |
| // Act: Do an incremental build without "clean" or "shutdown" | |
| buildTarget("//a:bar"); | |
| waitDownloads(); | |
| // Assert: target was successfully built | |
| assertValidOutputFile("a/bar.out", "foo\nupdated bar\n"); | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenUploadingInputTree(@TestParameter boolean actionRewinding) | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write("BUILD"); | |
| writeOutputDirRule(); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load("//:output_dir.bzl", "output_dir") | |
| output_dir( | |
| name = "foo.out", | |
| content_map = {"file-inside": "hello world"}, | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "( ls $(location :foo.out); cat $(location :bar.in) ) > $@", | |
| ) | |
| """); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| buildTarget("//a:bar"); | |
| getOutputPath("a/foo.out").deleteTreesBelow(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, foo.out isn't downloaded | |
| setDownloadToplevel(); | |
| buildTarget("//a:bar"); | |
| assertOutputDoesNotExist("a/foo.out/file-inside"); | |
| // Evict blobs from remote cache | |
| evictAllBlobs(); | |
| write("a/bar.in", "updated bar"); | |
| if (actionRewinding) { | |
| // The lost input's generating action is rewound within the next build. | |
| enableActionRewinding(); | |
| } else { | |
| // The build fails because of remote cache eviction, but an incremental build without | |
| // "clean" or "shutdown" can continue. | |
| disableActionRewinding(); | |
| addOptions("--strategy_regexp=.*bar=local"); | |
| assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); | |
| } | |
| // Act: Do an incremental build without "clean" or "shutdown" | |
| buildTarget("//a:bar"); | |
| waitDownloads(); | |
| // Assert: target was successfully built | |
| assertValidOutputFile("a/bar.out", "file-inside\nupdated bar\n"); | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenTopLevelRequested_succeedsWithActionRewinding() | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write("BUILD"); | |
| writeOutputDirRule(); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load("//:output_dir.bzl", "output_dir") | |
| output_dir( | |
| name = "foo.out", | |
| content_map = {"file-inside": "hello world"}, | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "( ls $(location :foo.out); cat $(location :bar.in) ) > $@", | |
| ) | |
| """); | |
| write("a/bar.in", "bar"); | |
| // Populate remote cache | |
| buildTarget("//a:bar", "//a:foo.out"); | |
| getOutputPath("a/foo.out").deleteTreesBelow(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, bar.out and foo.out aren't downloaded | |
| buildTarget("//a:bar", "//a:foo.out"); | |
| assertOutputDoesNotExist("a/bar.out"); | |
| assertOutputDoesNotExist("a/foo.out/file-inside"); | |
| // Act: Do an incremental build without "clean" or "shutdown" after clearing the cache and | |
| // switching to download toplevel | |
| evictAllBlobs(); | |
| setDownloadToplevel(); | |
| enableActionRewinding(); | |
| buildTarget("//a:bar", "//a:foo.out"); | |
| // Assert: all outputs were downloaded | |
| assertValidOutputFile("a/bar.out", "file-inside\nbar\n"); | |
| assertValidOutputFile("a/foo.out/file-inside", "hello world"); | |
| } | |
| @Test | |
| public void remoteCacheEvictBlobs_whenRunfilesRequested_succeedsWithActionRewinding() | |
| throws Exception { | |
| // Arrange: Prepare workspace and populate remote cache | |
| write("BUILD"); | |
| writeOutputDirRule(); | |
| write( | |
| "native_binary.bzl", | |
| """ | |
| def _native_binary_impl(ctx): | |
| runfiles = ctx.runfiles( | |
| transitive_files = depset( | |
| transitive = [target[DefaultInfo].files for target in ctx.attr.data], | |
| ), | |
| ) | |
| runfiles = runfiles.merge_all( | |
| [target[DefaultInfo].default_runfiles for target in ctx.attr.data], | |
| ) | |
| executable = ctx.actions.declare_file(ctx.label.name) | |
| ctx.actions.symlink( | |
| output = executable, | |
| target_file = ctx.file.executable, | |
| ) | |
| return [ | |
| DefaultInfo( | |
| executable = executable, | |
| runfiles = runfiles, | |
| ), | |
| ] | |
| native_binary = rule( | |
| implementation = _native_binary_impl, | |
| attrs = { | |
| "executable": attr.label(allow_single_file = True), | |
| "data": attr.label_list(), | |
| }, | |
| executable = True, | |
| ) | |
| """); | |
| write( | |
| "a/BUILD", | |
| """ | |
| load("//:native_binary.bzl", "native_binary") | |
| load("//:output_dir.bzl", "output_dir") | |
| output_dir( | |
| name = "foo.out", | |
| content_map = {"file-inside": "hello world"}, | |
| ) | |
| genrule( | |
| name = "bar", | |
| srcs = [ | |
| "foo.out", | |
| "bar.in", | |
| ], | |
| outs = ["bar.out"], | |
| cmd = "( ls $(location :foo.out); cat $(location :bar.in) ) > $@", | |
| ) | |
| native_binary( | |
| name = "bin", | |
| executable = "bin.sh", | |
| data = [ | |
| ":foo.out", | |
| ":bar", | |
| ], | |
| ) | |
| """); | |
| write("a/bar.in", "bar"); | |
| write("a/bin.sh"); | |
| // Populate remote cache | |
| buildTarget("//a:bin"); | |
| getOutputPath("a/foo.out").deleteTreesBelow(); | |
| getOutputPath("a/bar.out").delete(); | |
| getOutputBase().getRelative("action_cache").deleteTreesBelow(); | |
| restartServer(); | |
| // Clean build, runfiles aren't downloaded | |
| buildTarget("//a:bin"); | |
| assertThat(getOutputPath("a/bin.runfiles").isDirectory()).isTrue(); | |
| assertOutputDoesNotExist("a/bar.out"); | |
| assertOutputDoesNotExist("a/foo.out/file-inside"); | |
| // Act: Do an incremental build without "clean" or "shutdown" after clearing the cache and | |
| // switching to download toplevel | |
| evictAllBlobs(); | |
| setDownloadToplevel(); | |
| enableActionRewinding(); | |
| buildTarget("//a:bin"); | |
| // Assert: all runfiles were downloaded | |
| assertValidOutputFile("a/bar.out", "file-inside\nbar\n"); | |
| assertValidOutputFile("a/foo.out/file-inside", "hello world"); | |
| } | |
| @Test | |
| public void actionRewinding_concurrentConsumersOfRewoundAction() throws Exception { | |
| enableActionRewinding(); | |
| // Arrange: Prepare workspace where action 'foo' generates two outputs: | |
| // 'foo1.out' (consumed by bar1) and 'foo2.out' (consumed concurrently by bar2). | |
| write( | |
| "a/BUILD", | |
| """ | |
| genrule( | |
| name = "foo", | |
| outs = [ | |
| "foo1.out", | |
| "foo2.out", | |
| ], | |
| cmd = "seq 1 500 > $(location foo1.out); seq 1 500 > $(location foo2.out)", | |
| ) | |
| genrule( | |
| name = "bar1", | |
| srcs = ["foo1.out"], | |
| outs = ["bar1.out"], | |
| cmd = "cat $(location foo1.out) > $@", | |
| ) | |
| genrule( | |
| name = "bar2", | |
| srcs = [ | |
| "foo2.out", | |
| "bar2.in", | |
| ], | |
| outs = ["bar2.out"], | |
| cmd = "while [ ! -f a/bar2.marker ]; do cat $(location foo2.out) > /dev/null || exit 1; sleep 0.02; done; cat $(location foo2.out) $(location bar2.in) > $@", | |
| ) | |
| """); | |
| write("a/bar2.in", "bar2"); | |
| // Clean build: build foo remotely so intermediate outputs foo1.out and foo2.out are in CAS | |
| buildTarget("//a:foo"); | |
| assertOutputDoesNotExist("a/foo1.out"); | |
| assertOutputDoesNotExist("a/foo2.out"); | |
| // Act: Run bar1 and bar2 concurrently. | |
| // When bar2 starts executing (and prefetches foo2.out), evict blobs from CAS. | |
| // bar1 starts after eviction, sees foo1.out missing from CAS, and triggers action rewinding | |
| // for foo. | |
| // When foo is rewound, foo's preparation deletes foo2.out from disk while bar2 is reading it. | |
| // Without synchronization, bar2 fails on missing foo2.out. | |
| addOptions("--strategy_regexp=.*bar=local", "--jobs=4"); | |
| CountDownLatch bar2Started = new CountDownLatch(1); | |
| CountDownLatch evictionFinished = new CountDownLatch(1); | |
| CountDownLatch bar1Rewound = new CountDownLatch(1); | |
| Path markerPath = getWorkspace().getRelative("a/bar2.marker"); | |
| runtimeWrapper.registerSubscriber( | |
| new Object() { | |
| @Subscribe | |
| @AllowConcurrentEvents | |
| public void actionStarted(ActionStartedEvent event) { | |
| String label = event.getAction().getOwner().getLabel().toString(); | |
| if (label.equals("//a:bar1")) { | |
| try { | |
| // Ensure bar2 has started and eviction has completed before bar1 attempts | |
| // prefetching | |
| bar2Started.await(); | |
| evictionFinished.await(); | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| @Subscribe | |
| @AllowConcurrentEvents | |
| public void actionRunning(RunningActionEvent event) { | |
| String label = event.getActionMetadata().getOwner().getLabel().toString(); | |
| if (label.equals("//a:bar2")) { | |
| bar2Started.countDown(); | |
| } | |
| } | |
| @Subscribe | |
| @AllowConcurrentEvents | |
| public void actionRewound(ActionRewoundEvent event) { | |
| if (event | |
| .getFailedRewoundAction() | |
| .getOwner() | |
| .getLabel() | |
| .toString() | |
| .equals("//a:bar1")) { | |
| bar1Rewound.countDown(); | |
| try { | |
| FileSystemUtils.createEmptyFile(markerPath); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| }); | |
| new Thread( | |
| () -> { | |
| try { | |
| bar2Started.await(); | |
| evictAllBlobs(); | |
| evictionFinished.countDown(); | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| throw new RuntimeException(e); | |
| } catch (Exception e) { | |
| throw new RuntimeException(e); | |
| } | |
| }) | |
| .start(); | |
| try { | |
| buildTarget("//a:bar1", "//a:bar2"); | |
| } finally { | |
| // Assert test preconditions: ensure eviction completed, bar2 was actively running, | |
| // and action rewinding was legitimately triggered for bar1. | |
| assertThat(evictionFinished.getCount()).isEqualTo(0); | |
| assertThat(bar2Started.getCount()).isEqualTo(0); | |
| assertThat(bar1Rewound.getCount()).isEqualTo(0); | |
| } | |
| waitDownloads(); | |
| // Assert: Both targets succeed | |
| assertThat(getOutputPath("a/bar1.out").exists()).isTrue(); | |
| assertThat(getOutputPath("a/bar2.out").exists()).isTrue(); | |
| } | |
| protected void restartServer() throws Exception { | |
| // Simulates a server restart | |
| createRuntimeWrapper(); | |
| } | |
| } |