Remote: Fix a bug that the XML generation is executed even if test.xml is generated when build with --remote_download_minimal.

Change test.xml from BasicActionInput to Artifact before executing the spawn so its metadata can be injected.

Use a custom MetadataHandler to allow metadata injections of undeclared outputs.

Fixes #12554.

Closes #12590.

PiperOrigin-RevId: 380741230
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
index 15e1d58..34d45b3 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
@@ -25,17 +25,22 @@
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.ActionInput;
 import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact;
 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.ArtifactPathResolver;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionRequirements;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.ResourceSet;
 import com.google.devtools.build.lib.actions.SimpleSpawn;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnContinuation;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.analysis.test.TestAttempt;
 import com.google.devtools.build.lib.analysis.test.TestResult;
@@ -51,6 +56,7 @@
 import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.TestAction;
+import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileStatus;
@@ -68,6 +74,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import javax.annotation.Nullable;
 
 /** Runs TestRunnerAction actions. */
@@ -141,7 +149,7 @@
             action.getTestProperties().isPersistentTestRunner()
                 ? action.getTools()
                 : NestedSetBuilder.emptySet(Order.STABLE_ORDER),
-            ImmutableSet.copyOf(action.getSpawnOutputs()),
+            createSpawnOutputs(action),
             localResourceUsage);
     Path execRoot = actionExecutionContext.getExecRoot();
     ArtifactPathResolver pathResolver = actionExecutionContext.getPathResolver();
@@ -152,6 +160,21 @@
         action, actionExecutionContext, spawn, tmpDir, workingDirectory, execRoot);
   }
 
+  private ImmutableSet<ActionInput> createSpawnOutputs(TestRunnerAction action) {
+    ImmutableSet.Builder<ActionInput> builder = ImmutableSet.builder();
+    for (ActionInput output : action.getSpawnOutputs()) {
+      if (output.getExecPath().equals(action.getXmlOutputPath())) {
+        // HACK: Convert type of test.xml from BasicActionInput to DerivedArtifact. We want to
+        // inject metadata of test.xml if it is generated remotely and it's currently only possible
+        // to inject Artifact.
+        builder.add(createArtifactOutput(action, output.getExecPath()));
+      } else {
+        builder.add(output);
+      }
+    }
+    return builder.build();
+  }
+
   private static ImmutableList<Pair<String, Path>> renameOutputs(
       ActionExecutionContext actionExecutionContext,
       TestRunnerAction action,
@@ -301,11 +324,84 @@
       relativeTmpDir = tmpDir.asFragment();
     }
     return DEFAULT_LOCAL_POLICY.computeTestEnvironment(
-        action,
-        clientEnv,
-        getTimeout(action),
-        runfilesDir.relativeTo(execRoot),
-        relativeTmpDir);
+        action, clientEnv, getTimeout(action), runfilesDir.relativeTo(execRoot), relativeTmpDir);
+  }
+
+  static class TestMetadataHandler implements MetadataHandler {
+    private final MetadataHandler metadataHandler;
+    private final ImmutableSet<Artifact> outputs;
+    private final ConcurrentMap<Artifact, FileArtifactValue> fileMetadataMap =
+        new ConcurrentHashMap<>();
+
+    TestMetadataHandler(MetadataHandler metadataHandler, ImmutableSet<Artifact> outputs) {
+      this.metadataHandler = metadataHandler;
+      this.outputs = outputs;
+    }
+
+    @Nullable
+    @Override
+    public ActionInput getInput(String execPath) {
+      return metadataHandler.getInput(execPath);
+    }
+
+    @Nullable
+    @Override
+    public FileArtifactValue getMetadata(ActionInput input) throws IOException {
+      return metadataHandler.getMetadata(input);
+    }
+
+    @Override
+    public void setDigestForVirtualArtifact(Artifact artifact, byte[] digest) {
+      metadataHandler.setDigestForVirtualArtifact(artifact, digest);
+    }
+
+    @Override
+    public FileArtifactValue constructMetadataForDigest(
+        Artifact output, FileStatus statNoFollow, byte[] injectedDigest) throws IOException {
+      return metadataHandler.constructMetadataForDigest(output, statNoFollow, injectedDigest);
+    }
+
+    @Override
+    public ImmutableSet<TreeFileArtifact> getTreeArtifactChildren(SpecialArtifact treeArtifact) {
+      return metadataHandler.getTreeArtifactChildren(treeArtifact);
+    }
+
+    @Override
+    public TreeArtifactValue getTreeArtifactValue(SpecialArtifact treeArtifact) throws IOException {
+      return metadataHandler.getTreeArtifactValue(treeArtifact);
+    }
+
+    @Override
+    public void markOmitted(Artifact output) {
+      metadataHandler.markOmitted(output);
+    }
+
+    @Override
+    public boolean artifactOmitted(Artifact artifact) {
+      return metadataHandler.artifactOmitted(artifact);
+    }
+
+    @Override
+    public void resetOutputs(Iterable<? extends Artifact> outputs) {
+      metadataHandler.resetOutputs(outputs);
+    }
+
+    @Override
+    public void injectFile(Artifact output, FileArtifactValue metadata) {
+      if (outputs.contains(output)) {
+        metadataHandler.injectFile(output, metadata);
+      }
+      fileMetadataMap.put(output, metadata);
+    }
+
+    @Override
+    public void injectTree(SpecialArtifact output, TreeArtifactValue tree) {
+      metadataHandler.injectTree(output, tree);
+    }
+
+    public boolean fileInjected(Artifact output) {
+      return fileMetadataMap.containsKey(output);
+    }
   }
 
   private TestAttemptContinuation beginTestAttempt(
@@ -324,12 +420,26 @@
           createStreamedTestOutput(
               Reporter.outErrForReporter(actionExecutionContext.getEventHandler()), out);
     }
+
+    // We use TestMetadataHandler here mainly because the one provided by actionExecutionContext
+    // doesn't allow to inject undeclared outputs and test.xml is undeclared by the test action.
+    TestMetadataHandler testMetadataHandler = null;
+    if (actionExecutionContext.getMetadataHandler() != null) {
+      testMetadataHandler =
+          new TestMetadataHandler(
+              actionExecutionContext.getMetadataHandler(), testAction.getOutputs());
+    }
+
     long startTimeMillis = actionExecutionContext.getClock().currentTimeMillis();
     SpawnStrategyResolver resolver = actionExecutionContext.getContext(SpawnStrategyResolver.class);
     SpawnContinuation spawnContinuation;
     try {
       spawnContinuation =
-          resolver.beginExecution(spawn, actionExecutionContext.withFileOutErr(testOutErr));
+          resolver.beginExecution(
+              spawn,
+              actionExecutionContext
+                  .withFileOutErr(testOutErr)
+                  .withMetadataHandler(testMetadataHandler));
     } catch (InterruptedException e) {
       if (streamed != null) {
         streamed.close();
@@ -339,6 +449,7 @@
     }
     return new BazelTestAttemptContinuation(
         testAction,
+        testMetadataHandler,
         actionExecutionContext,
         spawn,
         resolvedPaths,
@@ -394,6 +505,12 @@
     return executionInfo.build();
   }
 
+  private static Artifact.DerivedArtifact createArtifactOutput(
+      TestRunnerAction action, PathFragment outputPath) {
+    Artifact.DerivedArtifact testLog = (Artifact.DerivedArtifact) action.getTestLog();
+    return new DerivedArtifact(testLog.getRoot(), outputPath, testLog.getArtifactOwner());
+  }
+
   /**
    * A spawn to generate a test.xml file from the test log. This is only used if the test does not
    * generate a test.xml file itself.
@@ -430,7 +547,7 @@
         /*inputs=*/ NestedSetBuilder.create(
             Order.STABLE_ORDER, action.getTestXmlGeneratorScript(), action.getTestLog()),
         /*tools=*/ NestedSetBuilder.emptySet(Order.STABLE_ORDER),
-        /*outputs=*/ ImmutableSet.of(ActionInputHelper.fromPath(action.getXmlOutputPath())),
+        /*outputs=*/ ImmutableSet.of(createArtifactOutput(action, action.getXmlOutputPath())),
         SpawnAction.DEFAULT_RESOURCE_SET);
   }
 
@@ -586,6 +703,7 @@
 
   private final class BazelTestAttemptContinuation extends TestAttemptContinuation {
     private final TestRunnerAction testAction;
+    @Nullable private final TestMetadataHandler testMetadataHandler;
     private final ActionExecutionContext actionExecutionContext;
     private final Spawn spawn;
     private final ResolvedPaths resolvedPaths;
@@ -598,6 +716,7 @@
 
     BazelTestAttemptContinuation(
         TestRunnerAction testAction,
+        @Nullable TestMetadataHandler testMetadataHandler,
         ActionExecutionContext actionExecutionContext,
         Spawn spawn,
         ResolvedPaths resolvedPaths,
@@ -608,6 +727,7 @@
         TestResultData.Builder testResultDataBuilder,
         ImmutableList<SpawnResult> spawnResults) {
       this.testAction = testAction;
+      this.testMetadataHandler = testMetadataHandler;
       this.actionExecutionContext = actionExecutionContext;
       this.spawn = spawn;
       this.resolvedPaths = resolvedPaths;
@@ -647,6 +767,7 @@
           if (!nextContinuation.isDone()) {
             return new BazelTestAttemptContinuation(
                 testAction,
+                testMetadataHandler,
                 actionExecutionContext,
                 spawn,
                 resolvedPaths,
@@ -737,6 +858,7 @@
           appendCoverageLog(coverageOutErr, fileOutErr);
           return new BazelCoveragePostProcessingContinuation(
               testAction,
+              testMetadataHandler,
               actionExecutionContext,
               spawn,
               resolvedPaths,
@@ -772,15 +894,21 @@
         throw new EnvironmentalExecException(e, Code.TEST_OUT_ERR_IO_EXCEPTION);
       }
 
+      Path xmlOutputPath = resolvedPaths.getXmlOutputPath();
+      boolean testXmlGenerated = xmlOutputPath.exists();
+      if (!testXmlGenerated && testMetadataHandler != null) {
+        testXmlGenerated =
+            testMetadataHandler.fileInjected(
+                createArtifactOutput(testAction, testAction.getXmlOutputPath()));
+      }
 
       // If the test did not create a test.xml, and --experimental_split_xml_generation is enabled,
       // then we run a separate action to create a test.xml from test.log. We do this as a spawn
       // rather than doing it locally in-process, as the test.log file may only exist remotely (when
       // remote execution is enabled), and we do not want to have to download it.
-      Path xmlOutputPath = resolvedPaths.getXmlOutputPath();
       if (executionOptions.splitXmlGeneration
           && fileOutErr.getOutputPath().exists()
-          && !xmlOutputPath.exists()) {
+          && !testXmlGenerated) {
         Spawn xmlGeneratingSpawn =
             createXmlGeneratingSpawn(testAction, spawn.getEnvironment(), spawnResults.get(0));
         SpawnStrategyResolver spawnStrategyResolver =
@@ -791,7 +919,10 @@
         try {
           SpawnContinuation xmlContinuation =
               spawnStrategyResolver.beginExecution(
-                  xmlGeneratingSpawn, actionExecutionContext.withFileOutErr(xmlSpawnOutErr));
+                  xmlGeneratingSpawn,
+                  actionExecutionContext
+                      .withFileOutErr(xmlSpawnOutErr)
+                      .withMetadataHandler(testMetadataHandler));
           return new BazelXmlCreationContinuation(
               resolvedPaths, xmlSpawnOutErr, testResultDataBuilder, spawnResults, xmlContinuation);
         } catch (InterruptedException e) {
@@ -889,6 +1020,7 @@
 
   private final class BazelCoveragePostProcessingContinuation extends TestAttemptContinuation {
     private final ResolvedPaths resolvedPaths;
+    @Nullable private final TestMetadataHandler testMetadataHandler;
     private final FileOutErr fileOutErr;
     private final Closeable streamed;
     private final TestResultData.Builder testResultDataBuilder;
@@ -900,6 +1032,7 @@
 
     BazelCoveragePostProcessingContinuation(
         TestRunnerAction testAction,
+        @Nullable TestMetadataHandler testMetadataHandler,
         ActionExecutionContext actionExecutionContext,
         Spawn spawn,
         ResolvedPaths resolvedPaths,
@@ -909,6 +1042,7 @@
         ImmutableList<SpawnResult> primarySpawnResults,
         SpawnContinuation spawnContinuation) {
       this.testAction = testAction;
+      this.testMetadataHandler = testMetadataHandler;
       this.actionExecutionContext = actionExecutionContext;
       this.spawn = spawn;
       this.resolvedPaths = resolvedPaths;
@@ -933,6 +1067,7 @@
         if (!nextContinuation.isDone()) {
           return new BazelCoveragePostProcessingContinuation(
               testAction,
+              testMetadataHandler,
               actionExecutionContext,
               spawn,
               resolvedPaths,
@@ -969,6 +1104,7 @@
 
       return new BazelTestAttemptContinuation(
           testAction,
+          testMetadataHandler,
           actionExecutionContext,
           spawn,
           resolvedPaths,