Add support for VirtualActionInputs to the remote cache.

RELNOTES: None
PiperOrigin-RevId: 207137932
diff --git a/src/main/java/com/google/devtools/build/lib/remote/Chunker.java b/src/main/java/com/google/devtools/build/lib/remote/Chunker.java
index fb3202c..7e960ac 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/Chunker.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/Chunker.java
@@ -22,6 +22,7 @@
 import com.google.common.io.ByteStreams;
 import com.google.devtools.build.lib.actions.ActionInput;
 import com.google.devtools.build.lib.actions.MetadataProvider;
+import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
 import com.google.devtools.build.lib.remote.util.DigestUtil;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.remoteexecution.v1test.Digest;
@@ -146,6 +147,10 @@
     this(actionInput, inputCache, execRoot, getDefaultChunkSize(), digestUtil);
   }
 
+  public Chunker(VirtualActionInput actionInput, DigestUtil digestUtil) throws IOException {
+    this(actionInput.getBytes().toByteArray(), digestUtil);
+  }
+
   public Chunker(
       ActionInput actionInput,
       MetadataProvider inputCache,
diff --git a/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
index 253ab41..c175559 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
@@ -28,6 +28,7 @@
 import com.google.devtools.build.lib.actions.ActionInput;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.MetadataProvider;
+import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.remote.Retrier.RetryException;
 import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode;
@@ -169,7 +170,11 @@
     if (!missingActionInputs.isEmpty()) {
       MetadataProvider inputFileCache = repository.getInputFileCache();
       for (ActionInput actionInput : missingActionInputs) {
-        toUpload.add(new Chunker(actionInput, inputFileCache, execRoot, digestUtil));
+        if (actionInput instanceof VirtualActionInput) {
+          toUpload.add(new Chunker((VirtualActionInput) actionInput, digestUtil));
+        } else {
+          toUpload.add(new Chunker(actionInput, inputFileCache, execRoot, digestUtil));
+        }
       }
     }
     uploader.uploadBlobs(toUpload, true);
@@ -326,22 +331,6 @@
     return digest;
   }
 
-  /**
-   * Put the file contents cache if it is not already in it. No-op if the file is already stored in
-   * cache. The given path must be a full absolute path.
-   *
-   * @return The key for fetching the file contents blob from cache.
-   */
-  Digest uploadFileContents(ActionInput input, Path execRoot, MetadataProvider inputCache)
-      throws IOException, InterruptedException {
-    Digest digest = DigestUtil.getFromInputCache(input, inputCache);
-    ImmutableSet<Digest> missing = getMissingDigests(ImmutableList.of(digest));
-    if (!missing.isEmpty()) {
-      uploader.uploadBlob(new Chunker(input, inputCache, execRoot, digestUtil), true);
-    }
-    return digest;
-  }
-
   Digest uploadBlob(byte[] blob) throws IOException, InterruptedException {
     Digest digest = digestUtil.compute(blob);
     ImmutableSet<Digest> missing = getMissingDigests(ImmutableList.of(digest));
diff --git a/src/test/java/com/google/devtools/build/lib/remote/GrpcRemoteCacheTest.java b/src/test/java/com/google/devtools/build/lib/remote/GrpcRemoteCacheTest.java
index 3c61fd1..de453b3 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/GrpcRemoteCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/GrpcRemoteCacheTest.java
@@ -28,12 +28,15 @@
 import com.google.bytestream.ByteStreamProto.WriteResponse;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.util.concurrent.ListeningScheduledExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
 import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
 import com.google.devtools.build.lib.authandtls.GoogleAuthUtils;
 import com.google.devtools.build.lib.clock.JavaClock;
+import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode;
 import com.google.devtools.build.lib.remote.util.DigestUtil;
 import com.google.devtools.build.lib.remote.util.DigestUtil.ActionKey;
 import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
@@ -43,11 +46,13 @@
 import com.google.devtools.build.lib.vfs.FileSystem;
 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.inmemoryfs.InMemoryFileSystem;
 import com.google.devtools.common.options.Options;
 import com.google.devtools.remoteexecution.v1test.Action;
 import com.google.devtools.remoteexecution.v1test.ActionCacheGrpc.ActionCacheImplBase;
 import com.google.devtools.remoteexecution.v1test.ActionResult;
+import com.google.devtools.remoteexecution.v1test.Command;
 import com.google.devtools.remoteexecution.v1test.ContentAddressableStorageGrpc.ContentAddressableStorageImplBase;
 import com.google.devtools.remoteexecution.v1test.Digest;
 import com.google.devtools.remoteexecution.v1test.Directory;
@@ -74,7 +79,10 @@
 import io.grpc.util.MutableHandlerRegistry;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -201,6 +209,98 @@
         uploader);
   }
 
+  static class StringVirtualActionInput implements VirtualActionInput {
+    private final String contents;
+    private final PathFragment execPath;
+
+    StringVirtualActionInput(String contents, PathFragment execPath) {
+      this.contents = contents;
+      this.execPath = execPath;
+    }
+
+    @Override
+    public void writeTo(OutputStream out) throws IOException {
+      out.write(contents.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public ByteString getBytes() throws IOException {
+      ByteString.Output out = ByteString.newOutput();
+      writeTo(out);
+      return out.toByteString();
+    }
+
+    @Override
+    public String getExecPathString() {
+      return execPath.getPathString();
+    }
+
+    @Override
+    public PathFragment getExecPath() {
+      return execPath;
+    }
+  }
+
+  @Test
+  public void testVirtualActionInputSupport() throws Exception {
+    GrpcRemoteCache client = newClient();
+    TreeNodeRepository treeNodeRepository =
+        new TreeNodeRepository(execRoot, fakeFileCache, DIGEST_UTIL);
+    PathFragment execPath = PathFragment.create("my/exec/path");
+    VirtualActionInput virtualActionInput = new StringVirtualActionInput("hello", execPath);
+    Digest digest = DIGEST_UTIL.compute(virtualActionInput.getBytes().toByteArray());
+    TreeNode root =
+        treeNodeRepository.buildFromActionInputs(
+            ImmutableSortedMap.of(execPath, virtualActionInput));
+
+    // Add a fake CAS that responds saying that the above virtual action input is missing
+    serviceRegistry.addService(
+        new ContentAddressableStorageImplBase() {
+          @Override
+          public void findMissingBlobs(
+              FindMissingBlobsRequest request,
+              StreamObserver<FindMissingBlobsResponse> responseObserver) {
+            responseObserver.onNext(
+                FindMissingBlobsResponse.newBuilder().addMissingBlobDigests(digest).build());
+            responseObserver.onCompleted();
+          }
+        });
+
+    // Mock a byte stream and assert that we see the virtual action input with contents 'hello'
+    AtomicBoolean writeOccurred = new AtomicBoolean();
+    serviceRegistry.addService(
+        new ByteStreamImplBase() {
+          @Override
+          public StreamObserver<WriteRequest> write(
+              final StreamObserver<WriteResponse> responseObserver) {
+            return new StreamObserver<WriteRequest>() {
+              @Override
+              public void onNext(WriteRequest request) {
+                assertThat(request.getResourceName()).contains(digest.getHash());
+                assertThat(request.getFinishWrite()).isTrue();
+                assertThat(request.getData().toStringUtf8()).isEqualTo("hello");
+                writeOccurred.set(true);
+              }
+
+              @Override
+              public void onCompleted() {
+                responseObserver.onNext(WriteResponse.newBuilder().setCommittedSize(5).build());
+                responseObserver.onCompleted();
+              }
+
+              @Override
+              public void onError(Throwable t) {
+                fail("An error occurred: " + t);
+              }
+            };
+          }
+        });
+
+    // Upload all missing inputs (that is, the virtual action input from above)
+    client.ensureInputsPresent(treeNodeRepository, execRoot, root, Command.getDefaultInstance());
+    assertThat(writeOccurred.get()).named("WriteOccurred").isTrue();
+  }
+
   @Test
   public void testDownloadEmptyBlob() throws Exception {
     GrpcRemoteCache client = newClient();