From 3c706141d4f76d852baa0a75e85da639ff0aac3f Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Sun, 4 Oct 2026 20:11:20 +0000 Subject: [PATCH] Test rewinding of a lost unused_inputs_list read during input discovery When the unused_inputs_list is an input of the consuming action, it is read before execution through the action file system. With --remote_download_minimal, an evicted list surfaces as a lost input in input discovery and is recovered by rewinding the producing action. Add a build-without-the-bytes test covering both the rewinding and the invocation retry path. --- .../BuildWithoutTheBytesIntegrationTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java index 9b954dae9a6f6c..da7d301f524bdb 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java @@ -1260,4 +1260,102 @@ public void remoteTreeFilesExpiredBetweenBuilds(@TestParameter boolean actionRew // Assert: target was successfully built assertValidOutputFile("a/bar.out", "file-inside\nupdated bar\n"); } + + @Test + public void remoteCacheEvictBlobs_whenReadingUnusedInputsListDuringInputDiscovery( + @TestParameter boolean actionRewinding) throws Exception { + // Arrange: The consuming action declares a list produced by another action as both an input and + // its unused_inputs_list, so the list is read during input discovery to prune the spawn inputs + // before execution. The command records which of the declared sources made it into the remote + // input root. + write( + "a/defs.bzl", + """ + def _consume_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".out") + script = "\\n".join([ + 'out="$1"', + 'shift', + 'for f in "$@"; do', + ' if [ -e "$f" ]; then', + ' cat "$f" >> "$out"', + ' else', + ' echo "pruned $f" >> "$out"', + ' fi', + 'done', + ]) + ctx.actions.run( + mnemonic = "Consume", + inputs = ctx.files.srcs + [ctx.file.unused_inputs_list], + outputs = [out], + executable = "/bin/sh", + arguments = ["-c", script, "consume", out.path] + [f.path for f in ctx.files.srcs], + unused_inputs_list = ctx.file.unused_inputs_list, + ) + return DefaultInfo(files = depset([out])) + + consume = rule( + implementation = _consume_impl, + attrs = { + "srcs": attr.label_list(allow_files = True), + "unused_inputs_list": attr.label(allow_single_file = True), + }, + ) + """); + write( + "a/BUILD", + """ + load(":defs.bzl", "consume") + + genrule( + name = "unused_list", + outs = ["unused.list"], + cmd = "echo a/b.in > $@", + ) + + consume( + name = "consume", + srcs = [ + "a.in", + "b.in", + ], + unused_inputs_list = ":unused_list", + ) + """); + write("a/a.in", "a"); + write("a/b.in", "b"); + + // The list is pruned before execution even on a clean build without the bytes. + buildTarget("//a:consume"); + assertOnlyOutputRemoteContent("//a:consume", "consume.out", "a\npruned a/b.in\n"); + // Input discovery downloaded the list through the action file system. Delete the local copy so + // that the next read has to go to the remote cache again. + assertThat(getOutputPath("a/unused.list").delete()).isTrue(); + + // Act: Evict blobs from remote cache and invalidate only the consuming action + evictAllBlobs(); + write("a/a.in", "updated a"); + setDownloadToplevel(); + List rewoundKeys = null; + if (actionRewinding) { + enableActionRewinding(); + rewoundKeys = rewindingTestsHelper.collectOrderedRewoundKeys(); + } else { + // The build fails because the list is lost, but an incremental build without "clean" or + // "shutdown" can continue. + disableActionRewinding(); + var error = assertThrows(BuildFailedException.class, () -> buildTarget("//a:consume")); + assertThat(error).hasMessageThat().contains("Lost inputs no longer available remotely"); + assertThat(error).hasMessageThat().contains("a/unused.list"); + } + + buildTarget("//a:consume"); + waitDownloads(); + + // Assert: the list was regenerated and the consuming action ran with pruned inputs. + assertValidOutputFile("a/consume.out", "updated a\npruned a/b.in\n"); + if (actionRewinding) { + assertRewoundActions(rewoundKeys, "//a:unused_list"); + } + } }