Classify the undeclared test outputs directory as a directory instead of a file.

When constructing a remote execution request, any output other than a tree artifact is classified as a (regular) file. But the undeclared outputs directory is actually an ActionInput, and promoting it to an Artifact seems a rather involved refactor. Instead, add an ActionInput#isDirectory method, similar to the preexisting ActionInput#isSymlink, and use it in RemoteExecutionService#buildCommand.

Also take the opportunity to add tests verifying that all output types are handled correctly.

Fixes #16315.

PiperOrigin-RevId: 482738939
Change-Id: I5d237efd7244cee25ed9702b593c6d0c714e3f8a
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java b/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java
index 573326a..2d26ad5 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java
@@ -41,6 +41,9 @@
    */
   PathFragment getExecPath();
 
+  /** The input is a directory. */
+  boolean isDirectory();
+
   /** The input is a symlink that is supposed to stay un-dereferenced. */
   boolean isSymlink();
 
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java b/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java
index bc3d15c..6924f04 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java
@@ -41,6 +41,11 @@
     }
 
     @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
     public int hashCode() {
       return getExecPathString().hashCode();
     }
@@ -103,6 +108,32 @@
   }
 
   /**
+   * Creates an ActionInput with just the given relative path (which must point to a directory) and
+   * no digest.
+   *
+   * @param path the relative path of the input.
+   * @return a ActionInput.
+   */
+  public static ActionInput fromPathToDirectory(PathFragment path) {
+    return new BasicActionInput() {
+      @Override
+      public String getExecPathString() {
+        return path.getPathString();
+      }
+
+      @Override
+      public PathFragment getExecPath() {
+        return path;
+      }
+
+      @Override
+      public boolean isDirectory() {
+        return true;
+      }
+    };
+  }
+
+  /**
    * Expands middleman and tree artifacts in a sequence of {@link ActionInput}s.
    *
    * <p>The constructed list never contains middleman artifacts. If {@code keepEmptyTreeArtifacts}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/CommandLines.java b/src/main/java/com/google/devtools/build/lib/actions/CommandLines.java
index 81efe6e..543e42f 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/CommandLines.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/CommandLines.java
@@ -305,6 +305,11 @@
     }
 
     @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
     public boolean isSymlink() {
       return false;
     }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java b/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java
index 8a035a9..8acbcb8 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java
@@ -55,6 +55,11 @@
     private EmptyActionInput() {}
 
     @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
     public boolean isSymlink() {
       return false;
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index 31a6744..eadd829 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -350,7 +350,7 @@
     if (testConfiguration.getZipUndeclaredTestOutputs()) {
       outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsZipPath()));
     } else {
-      outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsDir()));
+      outputs.add(ActionInputHelper.fromPathToDirectory(getUndeclaredOutputsDir()));
     }
     outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsManifestPath()));
     outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsAnnotationsPath()));
diff --git a/src/main/java/com/google/devtools/build/lib/exec/BinTools.java b/src/main/java/com/google/devtools/build/lib/exec/BinTools.java
index c91b09c..c7291e7 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/BinTools.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/BinTools.java
@@ -189,6 +189,11 @@
     }
 
     @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
     public ByteString getBytes() throws IOException {
       ByteString.Output out = ByteString.newOutput();
       writeTo(out);
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
index 1ee3d44..196247f 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
@@ -230,7 +230,7 @@
     ArrayList<String> outputDirectories = new ArrayList<>();
     for (ActionInput output : outputs) {
       String pathString = decodeBytestringUtf8(remotePathResolver.localPathToOutputPath(output));
-      if (output instanceof Artifact && ((Artifact) output).isTreeArtifact()) {
+      if (output.isDirectory()) {
         outputDirectories.add(pathString);
       } else {
         outputFiles.add(pathString);
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ActionInputMapTest.java b/src/test/java/com/google/devtools/build/lib/actions/ActionInputMapTest.java
index 62b1e2d..b3fb345 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ActionInputMapTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ActionInputMapTest.java
@@ -506,6 +506,11 @@
     }
 
     @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
     public boolean isSymlink() {
       return false;
     }
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 07fe486..118ed7e 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
@@ -347,6 +347,11 @@
       }
 
       @Override
+      public boolean isDirectory() {
+        return false;
+      }
+
+      @Override
       public boolean isSymlink() {
         return false;
       }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
index bb7ac8d..dc69fbe 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
@@ -180,6 +180,86 @@
   }
 
   @Test
+  public void buildRemoteAction_withRegularFileAsOutput() throws Exception {
+    PathFragment execPath = execRoot.getRelative("path/to/tree").asFragment();
+    Spawn spawn =
+        new SpawnBuilder("dummy")
+            .withOutput(ActionsTestUtil.createArtifactWithExecPath(artifactRoot, execPath))
+            .build();
+    FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
+    RemoteExecutionService service = newRemoteExecutionService();
+
+    RemoteAction remoteAction = service.buildRemoteAction(spawn, context);
+
+    assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly(execPath.toString());
+    assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty();
+  }
+
+  @Test
+  public void buildRemoteAction_withTreeArtifactAsOutput() throws Exception {
+    Spawn spawn =
+        new SpawnBuilder("dummy")
+            .withOutput(
+                ActionsTestUtil.createTreeArtifactWithGeneratingAction(
+                    artifactRoot, PathFragment.create("path/to/dir")))
+            .build();
+    FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
+    RemoteExecutionService service = newRemoteExecutionService();
+
+    RemoteAction remoteAction = service.buildRemoteAction(spawn, context);
+
+    assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty();
+    assertThat(remoteAction.getCommand().getOutputDirectoriesList()).containsExactly("path/to/dir");
+  }
+
+  @Test
+  public void buildRemoteAction_withUnresolvedSymlinkAsOutput() throws Exception {
+    Spawn spawn =
+        new SpawnBuilder("dummy")
+            .withOutput(
+                ActionsTestUtil.createUnresolvedSymlinkArtifactWithExecPath(
+                    artifactRoot, PathFragment.create("path/to/link")))
+            .build();
+    FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
+    RemoteExecutionService service = newRemoteExecutionService();
+
+    RemoteAction remoteAction = service.buildRemoteAction(spawn, context);
+
+    assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly("path/to/link");
+    assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty();
+  }
+
+  @Test
+  public void buildRemoteAction_withActionInputFileAsOutput() throws Exception {
+    Spawn spawn =
+        new SpawnBuilder("dummy")
+            .withOutput(ActionInputHelper.fromPath(PathFragment.create("path/to/file")))
+            .build();
+    FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
+    RemoteExecutionService service = newRemoteExecutionService();
+
+    RemoteAction remoteAction = service.buildRemoteAction(spawn, context);
+
+    assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly("path/to/file");
+    assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty();
+  }
+
+  @Test
+  public void buildRemoteAction_withActionInputDirectoryAsOutput() throws Exception {
+    Spawn spawn =
+        new SpawnBuilder("dummy")
+            .withOutput(ActionInputHelper.fromPathToDirectory(PathFragment.create("path/to/dir")))
+            .build();
+    FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
+    RemoteExecutionService service = newRemoteExecutionService();
+
+    RemoteAction remoteAction = service.buildRemoteAction(spawn, context);
+
+    assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty();
+    assertThat(remoteAction.getCommand().getOutputDirectoriesList()).containsExactly("path/to/dir");
+  }
+
+  @Test
   public void buildRemoteAction_differentiateWorkspace_generateActionSalt() throws Exception {
     Spawn spawn =
         new SpawnBuilder("dummy")
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
index ccbfd7a..7be0513 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
@@ -212,6 +212,11 @@
                   }
 
                   @Override
+                  public boolean isDirectory() {
+                    return false;
+                  }
+
+                  @Override
                   public boolean isSymlink() {
                     return false;
                   }
@@ -228,6 +233,11 @@
                   }
 
                   @Override
+                  public boolean isDirectory() {
+                    return false;
+                  }
+
+                  @Override
                   public boolean isSymlink() {
                     return false;
                   }