Do not assume files are remote for MetadataInjector methods.

PiperOrigin-RevId: 314837801
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java
index 6e06891..fb37eb8 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java
@@ -18,6 +18,8 @@
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.MetadataProvider;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import java.io.IOException;
 
 /**
  * Retrieves {@link FileArtifactValue} of {@link Artifact}s, and inserts virtual metadata as well.
@@ -34,6 +36,17 @@
   /** Sets digest for virtual artifacts (e.g. middlemen). {@code digest} must not be null. */
   void setDigestForVirtualArtifact(Artifact artifact, byte[] digest);
 
+  /**
+   * Constructs a {@link FileArtifactValue} for the given output whose digest is known.
+   *
+   * <p>This call does not inject the returned metadata. It should be injected with a followup call
+   * to {@link #injectFile} or {@link #injectDirectory} as appropriate.
+   *
+   * <p>chmod will not be called on the output.
+   */
+  FileArtifactValue constructMetadataForDigest(
+      Artifact output, FileStatus statNoFollow, byte[] digest) throws IOException;
+
   /** Retrieves the artifacts inside the TreeArtifact, without injecting its digest. */
   ImmutableSet<TreeFileArtifact> getExpandedOutputs(Artifact artifact);
 
@@ -46,8 +59,7 @@
 
   /**
    * Discards all known output artifact metadata, presumably because outputs will be modified. May
-   * only be called before any metadata is injected using {@link #injectDigest} or {@link
-   * #markOmitted};
+   * only be called before any metadata is injected.
    *
    * <p>Must be called at most once on any specific instance.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
index a944cae..ae9cec4 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
@@ -16,29 +16,31 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
-import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
-import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import java.util.Map;
 
 /** Supports metadata injection of action outputs into skyframe. */
 public interface MetadataInjector {
 
   /**
-   * Injects metadata of a file that is stored remotely.
+   * Injects the metadata of a file.
+   *
+   * <p>This can be used to save filesystem operations when the metadata is already known.
    *
    * @param output a regular output file
    * @param metadata the remote file metadata
    */
-  void injectRemoteFile(Artifact output, RemoteFileArtifactValue metadata);
+  void injectFile(Artifact output, FileArtifactValue metadata);
 
   /**
-   * Injects the metadata of a tree artifact whose contents are stored remotely.
+   * Injects the metadata of a tree artifact.
+   *
+   * <p>This can be used to save filesystem operations when the metadata is already known.
    *
    * @param output an output directory {@linkplain Artifact#isTreeArtifact tree artifact}
    * @param children the metadata of the files stored in the directory
    */
-  void injectRemoteDirectory(
-      SpecialArtifact output, Map<TreeFileArtifact, RemoteFileArtifactValue> children);
+  void injectDirectory(SpecialArtifact output, Map<TreeFileArtifact, FileArtifactValue> children);
 
   /**
    * Marks an {@link Artifact} as intentionally omitted.
@@ -47,9 +49,4 @@
    * action depends on) from a remote system.
    */
   void markOmitted(Artifact output);
-
-  /**
-   * Injects provided digest into the metadata handler, simultaneously caching lstat() data as well.
-   */
-  void injectDigest(Artifact output, FileStatus statNoFollow, byte[] digest);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
index a7a0bdb..0c881f4 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
@@ -44,6 +44,7 @@
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.actions.cache.MetadataInjector;
@@ -614,7 +615,7 @@
                 + "--experimental_remote_download_outputs=minimal");
       }
       SpecialArtifact parent = (SpecialArtifact) output;
-      ImmutableMap.Builder<TreeFileArtifact, RemoteFileArtifactValue> childMetadata =
+      ImmutableMap.Builder<TreeFileArtifact, FileArtifactValue> childMetadata =
           ImmutableMap.builderWithExpectedSize(directory.files.size());
       for (FileMetadata file : directory.files()) {
         TreeFileArtifact child =
@@ -627,7 +628,7 @@
                 actionId);
         childMetadata.put(child, value);
       }
-      metadataInjector.injectRemoteDirectory(parent, childMetadata.build());
+      metadataInjector.injectDirectory(parent, childMetadata.build());
     } else {
       FileMetadata outputMetadata = metadata.file(execRoot.getRelative(output.getExecPathString()));
       if (outputMetadata == null) {
@@ -635,7 +636,7 @@
         // SkyFrame will make sure to fail.
         return;
       }
-      metadataInjector.injectRemoteFile(
+      metadataInjector.injectFile(
           output,
           new RemoteFileArtifactValue(
               DigestUtil.toBinaryDigest(outputMetadata.digest()),
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
index 1a65a9c..ed3404f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
@@ -29,7 +29,6 @@
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.ArtifactPathResolver;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
-import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.FileStateType;
 import com.google.devtools.build.lib.actions.FileStateValue;
 import com.google.devtools.build.lib.actions.FilesetManifest;
@@ -375,7 +374,8 @@
   }
 
   @Override
-  public void injectDigest(Artifact output, FileStatus statNoFollow, byte[] digest) {
+  public FileArtifactValue constructMetadataForDigest(
+      Artifact output, FileStatus statNoFollow, byte[] digest) throws IOException {
     Preconditions.checkState(executionMode.get());
     Preconditions.checkState(!output.isSymlink());
     Preconditions.checkNotNull(digest);
@@ -383,37 +383,26 @@
     // We have to add the artifact to injectedFiles before calling constructFileArtifactValue to
     // avoid duplicate chmod calls.
     store.injectedFiles().add(output);
-    try {
-      // This call may make an extra call to Path#getFastDigest to see if the digest is readily
-      // available, even though we have the injected digest. This is necessary though, because
-      // otherwise this FileArtifactValue will not compare equal to another one created for the same
-      // file without an injected digest, because the other one will be missing its digest.
-      FileArtifactValue fileMetadata =
-          constructFileArtifactValue(
-              output, FileStatusWithDigestAdapter.adapt(statNoFollow), digest);
-      store.putArtifactData(output, fileMetadata);
-    } catch (IOException e) {
-      // Do nothing - we just failed to inject metadata. Real error handling will be done later,
-      // when somebody tries to access that file.
-    }
+
+    // TODO(jhorvitz): This unnecessarily calls getFastDigest even though we know the digest.
+    return constructFileArtifactValue(
+        output, FileStatusWithDigestAdapter.adapt(statNoFollow), digest);
   }
 
   @Override
-  public void injectRemoteFile(Artifact output, RemoteFileArtifactValue metadata) {
+  public void injectFile(Artifact output, FileArtifactValue metadata) {
     Preconditions.checkArgument(
         isKnownOutput(output), "%s is not a declared output of this action", output);
     Preconditions.checkArgument(
-        !output.isTreeArtifact(),
-        "injectRemoteFile must not be called on TreeArtifacts: %s",
-        output);
+        !output.isTreeArtifact(), "injectFile must not be called on TreeArtifacts: %s", output);
     Preconditions.checkState(
         executionMode.get(), "Tried to inject %s outside of execution", output);
     store.injectOutputData(output, metadata);
   }
 
   @Override
-  public void injectRemoteDirectory(
-      SpecialArtifact output, Map<TreeFileArtifact, RemoteFileArtifactValue> children) {
+  public void injectDirectory(
+      SpecialArtifact output, Map<TreeFileArtifact, FileArtifactValue> children) {
     Preconditions.checkArgument(
         isKnownOutput(output), "%s is not a declared output of this action", output);
     Preconditions.checkArgument(output.isTreeArtifact(), "output must be a tree artifact");
diff --git a/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java b/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
index 29b5eb0..9e12a2a 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
@@ -53,14 +53,12 @@
 import com.google.devtools.build.lib.actions.BuildConfigurationEvent;
 import com.google.devtools.build.lib.actions.Executor;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
-import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.MutableActionGraph;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.actions.PackageRootResolver;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissDetail;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
-import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeMetadataHandlerBase;
 import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
 import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate;
 import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate.OutputPathMapper;
@@ -971,18 +969,19 @@
     }
 
     @Override
-    public void injectDigest(Artifact output, FileStatus statNoFollow, byte[] digest) {
+    public FileArtifactValue constructMetadataForDigest(
+        Artifact output, FileStatus statNoFollow, byte[] digest) {
       throw new UnsupportedOperationException();
     }
 
     @Override
-    public void injectRemoteFile(Artifact output, RemoteFileArtifactValue metadata) {
+    public void injectFile(Artifact output, FileArtifactValue metadata) {
       throw new UnsupportedOperationException();
     }
 
     @Override
-    public void injectRemoteDirectory(
-        SpecialArtifact treeArtifact, Map<TreeFileArtifact, RemoteFileArtifactValue> children) {
+    public void injectDirectory(
+        SpecialArtifact treeArtifact, Map<TreeFileArtifact, FileArtifactValue> children) {
       throw new UnsupportedOperationException();
     }
 
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java
index e95fab7..4808159 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java
@@ -50,6 +50,7 @@
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.cache.MetadataInjector;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
@@ -929,8 +930,8 @@
 
     // assert
     assertThat(inMemoryOutput).isNull();
-    verify(injector).injectRemoteFile(eq(a1), remoteFileMatchingDigest(d1));
-    verify(injector).injectRemoteFile(eq(a2), remoteFileMatchingDigest(d2));
+    verify(injector).injectFile(eq(a1), remoteFileMatchingDigest(d1));
+    verify(injector).injectFile(eq(a2), remoteFileMatchingDigest(d2));
 
     Path outputBase = artifactRoot.getRoot().asPath();
     assertThat(outputBase.readdir(Symlinks.NOFOLLOW)).isEmpty();
@@ -992,13 +993,13 @@
     // assert
     assertThat(inMemoryOutput).isNull();
 
-    Map<TreeFileArtifact, RemoteFileArtifactValue> m =
+    Map<TreeFileArtifact, FileArtifactValue> m =
         ImmutableMap.of(
             TreeFileArtifact.createTreeOutput(dir, "file1"),
             new RemoteFileArtifactValue(toBinaryDigest(d1), d1.getSizeBytes(), 1, "action-id"),
             TreeFileArtifact.createTreeOutput(dir, "a/file2"),
             new RemoteFileArtifactValue(toBinaryDigest(d2), d2.getSizeBytes(), 1, "action-id"));
-    verify(injector).injectRemoteDirectory(eq(dir), eq(m));
+    verify(injector).injectDirectory(eq(dir), eq(m));
 
     Path outputBase = artifactRoot.getRoot().asPath();
     assertThat(outputBase.readdir(Symlinks.NOFOLLOW)).isEmpty();
@@ -1144,8 +1145,8 @@
     assertThat(inMemoryOutput.getContents()).isEqualTo(expectedContents);
     assertThat(inMemoryOutput.getOutput()).isEqualTo(a1);
     // The in memory file also needs to be injected as an output
-    verify(injector).injectRemoteFile(eq(a1), remoteFileMatchingDigest(d1));
-    verify(injector).injectRemoteFile(eq(a2), remoteFileMatchingDigest(d2));
+    verify(injector).injectFile(eq(a1), remoteFileMatchingDigest(d1));
+    verify(injector).injectFile(eq(a2), remoteFileMatchingDigest(d2));
 
     Path outputBase = artifactRoot.getRoot().asPath();
     assertThat(outputBase.readdir(Symlinks.NOFOLLOW)).isEmpty();
@@ -1181,7 +1182,7 @@
     // assert
     assertThat(inMemoryOutput).isNull();
     // The in memory file metadata also should not have been injected.
-    verify(injector, never()).injectRemoteFile(eq(a1), remoteFileMatchingDigest(d1));
+    verify(injector, never()).injectFile(eq(a1), remoteFileMatchingDigest(d1));
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
index e77a62c..ca3d511 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
@@ -364,7 +364,7 @@
     assertThat(handler.getMetadata(artifact).getSize()).isEqualTo(10);
 
     // Inject a remote file of size 42.
-    handler.injectRemoteFile(
+    handler.injectFile(
         artifact, new RemoteFileArtifactValue(new byte[] {1, 2, 3}, 42, 0, "ultimate-answer"));
     assertThat(handler.getMetadata(artifact).getSize()).isEqualTo(42);
 
@@ -391,7 +391,7 @@
 
     byte[] digest = new byte[] {1, 2, 3};
     int size = 10;
-    handler.injectRemoteFile(
+    handler.injectFile(
         artifact, new RemoteFileArtifactValue(digest, size, /*locationIndex=*/ 1, "action-id"));
 
     FileArtifactValue v = handler.getMetadata(artifact);
@@ -428,8 +428,8 @@
     RemoteFileArtifactValue child1Value = new RemoteFileArtifactValue(new byte[] {1, 2, 3}, 5, 1);
     RemoteFileArtifactValue child2Value = new RemoteFileArtifactValue(new byte[] {4, 5, 6}, 10, 1);
 
-    handler.injectRemoteFile(child1, child1Value);
-    handler.injectRemoteFile(child2, child2Value);
+    handler.injectFile(child1, child1Value);
+    handler.injectFile(child2, child2Value);
 
     FileArtifactValue treeMetadata = handler.getMetadata(treeArtifact);
     FileArtifactValue child1Metadata = handler.getMetadata(child1);
@@ -464,12 +464,12 @@
         new RemoteFileArtifactValue(new byte[] {1, 2, 3}, 5, 1, "foo");
     RemoteFileArtifactValue barValue =
         new RemoteFileArtifactValue(new byte[] {4, 5, 6}, 10, 1, "bar");
-    Map<TreeFileArtifact, RemoteFileArtifactValue> children =
+    Map<TreeFileArtifact, FileArtifactValue> children =
         ImmutableMap.of(
             TreeFileArtifact.createTreeOutput(treeArtifact, "foo"), fooValue,
             TreeFileArtifact.createTreeOutput(treeArtifact, "bar"), barValue);
 
-    handler.injectRemoteDirectory(treeArtifact, children);
+    handler.injectDirectory(treeArtifact, children);
 
     FileArtifactValue value = handler.getMetadata(treeArtifact);
     assertThat(value).isNotNull();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
index 2552a36..4a11445 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
@@ -39,6 +39,7 @@
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.MetadataProvider;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
@@ -557,15 +558,8 @@
     assertThat(errors.get(1).getMessage()).contains("not all outputs were created or valid");
   }
 
-  // This is more a smoke test than anything, because it turns out that:
-  // 1) there is no easy way to turn fast digests on/off for these test cases, and
-  // 2) injectDigest() doesn't really complain if you inject bad digests or digests
-  // for nonexistent files. Instead some weird error shows up down the line.
-  // In fact, there are no tests for injectDigest anywhere in the codebase.
-  // So all we're really testing here is that injectDigest() doesn't throw a weird exception.
-  // TODO(bazel-team): write real tests for injectDigest, here and elsewhere.
   @Test
-  public void digestInjection() throws Exception {
+  public void constructMetadataForDigest() throws Exception {
     SpecialArtifact out = createTreeArtifact("output");
     Action action =
         new SimpleTestAction(out) {
@@ -578,10 +572,26 @@
 
             MetadataHandler md = actionExecutionContext.getMetadataHandler();
             FileStatus stat = child1.getPath().stat(Symlinks.NOFOLLOW);
-            md.injectDigest(child1, stat, Hashing.sha256().hashString("one", UTF_8).asBytes());
+            FileArtifactValue metadata1 =
+                md.constructMetadataForDigest(
+                    child1, stat, Hashing.sha256().hashString("one", UTF_8).asBytes());
 
             stat = child2.getPath().stat(Symlinks.NOFOLLOW);
-            md.injectDigest(child2, stat, Hashing.sha256().hashString("two", UTF_8).asBytes());
+            FileArtifactValue metadata2 =
+                md.constructMetadataForDigest(
+                    child2, stat, Hashing.sha256().hashString("two", UTF_8).asBytes());
+
+            // The metadata will not be equal to reading from the filesystem since the filesystem
+            // won't have the digest. However, we should be able to detect that nothing could have
+            // been modified.
+            assertThat(
+                    metadata1.couldBeModifiedSince(
+                        FileArtifactValue.createForTesting(child1.getPath())))
+                .isFalse();
+            assertThat(
+                    metadata2.couldBeModifiedSince(
+                        FileArtifactValue.createForTesting(child2.getPath())))
+                .isFalse();
           }
         };
 
@@ -610,8 +620,7 @@
 
             actionExecutionContext
                 .getMetadataHandler()
-                .injectRemoteDirectory(
-                    out, ImmutableMap.of(child1, remoteFile1, child2, remoteFile2));
+                .injectDirectory(out, ImmutableMap.of(child1, remoteFile1, child2, remoteFile2));
           }
         };