support in-memory outputs in spawn result

split out from #7778

Closes #7791.

PiperOrigin-RevId: 239638546
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
index c17183b..e6b56c7 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.shell.TerminationStatus;
+import com.google.protobuf.ByteString;
 import java.io.InputStream;
 import java.time.Duration;
 import java.util.Locale;
@@ -226,6 +227,8 @@
     private final Optional<Long> numInvoluntaryContextSwitches;
     private final boolean cacheHit;
     private final String failureMessage;
+    private final ActionInput inMemoryOutputFile;
+    private final ByteString inMemoryContents;
 
     SimpleSpawnResult(Builder builder) {
       this.exitCode = builder.exitCode;
@@ -243,6 +246,8 @@
       this.numInvoluntaryContextSwitches = builder.numInvoluntaryContextSwitches;
       this.cacheHit = builder.cacheHit;
       this.failureMessage = builder.failureMessage;
+      this.inMemoryOutputFile = builder.inMemoryOutputFile;
+      this.inMemoryContents = builder.inMemoryContents;
     }
 
     @Override
@@ -358,6 +363,15 @@
       }
       return messagePrefix + " failed" + reason + explanation;
     }
+
+    @Nullable
+    @Override
+    public InputStream getInMemoryOutput(ActionInput output) {
+      if (inMemoryOutputFile != null && inMemoryOutputFile.equals(output)) {
+        return inMemoryContents.newInput();
+      }
+      return null;
+    }
   }
 
   /**
@@ -377,6 +391,9 @@
     private Optional<Long> numInvoluntaryContextSwitches = Optional.empty();
     private boolean cacheHit;
     private String failureMessage = "";
+    /* Invariant: Either both have a value or both are null. */
+    private ActionInput inMemoryOutputFile;
+    private ByteString inMemoryContents;
 
     public SpawnResult build() {
       Preconditions.checkArgument(!runnerName.isEmpty());
@@ -470,5 +487,11 @@
       this.failureMessage = failureMessage;
       return this;
     }
+
+    public Builder setInMemoryOutput(ActionInput outputFile, ByteString contents) {
+      this.inMemoryOutputFile = Preconditions.checkNotNull(outputFile);
+      this.inMemoryContents = Preconditions.checkNotNull(contents);
+      return this;
+    }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java b/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
index 6dccb47..6bedfc9 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
@@ -15,6 +15,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.devtools.build.lib.actions.SpawnResult.Status;
+import com.google.protobuf.ByteString;
 import java.time.Duration;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -50,4 +52,22 @@
     assertThat(r.getDetailMessage("", "", false, false))
         .contains("(failed due to timeout.)");
   }
+
+  @Test
+  public void inMemoryContents() throws Exception {
+    ActionInput output = ActionInputHelper.fromPath("/foo/bar");
+    ByteString contents = ByteString.copyFromUtf8("hello world");
+
+    SpawnResult r =
+        new SpawnResult.Builder()
+            .setStatus(Status.SUCCESS)
+            .setExitCode(0)
+            .setRunnerName("test")
+            .setInMemoryOutput(output, contents)
+            .build();
+
+    assertThat(ByteString.readFrom(r.getInMemoryOutput(output))).isEqualTo(contents);
+    assertThat(r.getInMemoryOutput(null)).isEqualTo(null);
+    assertThat(r.getInMemoryOutput(ActionInputHelper.fromPath("/does/not/exist"))).isEqualTo(null);
+  }
 }