Fix concurrent `TreeArtifactValue` builder to always inject empty tree
artifacts with archived representation.

Make sure we always add a `TreeArtifactValue` if all we set for it is the
archived representation. Currently, that is only true for
`TreeArtifactValue.BasicMultiBuilder`, not
`TreeArtifactValue.ConcurrentMultiBuilder`. Fix the concurrent builder to
inject empty tree artifacts when the archived representation is specified.

Change the `TreeArtifactValue.Builder#setArchivedRepresentation` interface to
match `TreeArtifactValue.MultiBuilder`. Add missing test coverage for setting
archived representation when using the builders.

PiperOrigin-RevId: 329323323
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
index 6ab2c81..0cf4c7b 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
@@ -297,12 +297,11 @@
         .ifPresent(
             archivedRepresentation -> {
               newTree.setArchivedRepresentation(
-                  ArchivedRepresentation.create(
-                      new ArchivedTreeArtifact(
-                          newParent,
-                          archivedRepresentation.archivedTreeFileArtifact().getRoot(),
-                          archivedRepresentation.archivedTreeFileArtifact().getExecPath()),
-                      archivedRepresentation.archivedFileValue()));
+                  new ArchivedTreeArtifact(
+                      newParent,
+                      archivedRepresentation.archivedTreeFileArtifact().getRoot(),
+                      archivedRepresentation.archivedTreeFileArtifact().getExecPath()),
+                  archivedRepresentation.archivedFileValue());
             });
 
     return newTree.build();
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 c6c6aa6..56c9dbb 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
@@ -38,7 +38,6 @@
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.actions.cache.DigestUtils;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
-import com.google.devtools.build.lib.skyframe.TreeArtifactValue.ArchivedRepresentation;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
 import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.FileStatus;
@@ -366,9 +365,7 @@
       ArchivedTreeArtifact archivedTreeArtifact =
           ArchivedTreeArtifact.create(parent, derivedPathPrefix);
       tree.setArchivedRepresentation(
-          ArchivedRepresentation.create(
-              archivedTreeArtifact,
-              constructFileArtifactValueFromFilesystem(archivedTreeArtifact)));
+          archivedTreeArtifact, constructFileArtifactValueFromFilesystem(archivedTreeArtifact));
     }
 
     return tree.build();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
index 747df12..78d2bc9 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
@@ -21,7 +21,6 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
@@ -419,16 +418,27 @@
       return this;
     }
 
-    public Builder setArchivedRepresentation(ArchivedRepresentation archivedRepresentation) {
+    public Builder setArchivedRepresentation(
+        ArchivedTreeArtifact archivedTreeArtifact, FileArtifactValue metadata) {
+      return setArchivedRepresentation(
+          ArchivedRepresentation.create(archivedTreeArtifact, metadata));
+    }
+
+    private Builder setArchivedRepresentation(ArchivedRepresentation archivedRepresentation) {
       checkState(
           this.archivedRepresentation == null,
           "Tried to add 2 archived representations for: %s",
-          archivedRepresentation);
+          parent);
       checkArgument(
-          archivedRepresentation.archivedTreeFileArtifact().getParent().equals(parent),
+          parent.equals(archivedRepresentation.archivedTreeFileArtifact().getParent()),
           "Cannot add archived representation: %s for a mismatching tree artifact: %s",
           archivedRepresentation,
           parent);
+      checkArgument(
+          !archivedRepresentation.archivedFileValue().equals(FileArtifactValue.OMITTED_FILE_MARKER),
+          "Cannot add archived representation: %s to %s because it has omitted metadata.",
+          archivedRepresentation,
+          parent);
       this.archivedRepresentation = archivedRepresentation;
       return this;
     }
@@ -507,10 +517,13 @@
           archivedRepresentations.putIfAbsent(
               archivedArtifact.getParent(),
               ArchivedRepresentation.create(archivedArtifact, metadata));
-      Preconditions.checkArgument(
+      checkArgument(
           oldValue == null,
           "Tried to add 2 archived representations for %s",
           archivedArtifact.getParent());
+      // We inject entries based on keys in the children map. Make sure a placeholder exists in case
+      // the tree artifact is otherwise empty.
+      children.computeIfAbsent(archivedArtifact.getParent(), ignored -> new ConcurrentHashMap<>());
       return this;
     }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionExecutionValueTransformSharedTreeArtifactsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionExecutionValueTransformSharedTreeArtifactsTest.java
index c2117e5..c515cd2 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionExecutionValueTransformSharedTreeArtifactsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionExecutionValueTransformSharedTreeArtifactsTest.java
@@ -228,8 +228,7 @@
           ArchivedTreeArtifact.create(treeArtifact, DERIVED_PATH_PREFIX);
       createFile(archivedArtifact.getPath());
       builder.setArchivedRepresentation(
-          ArchivedRepresentation.create(
-              archivedArtifact, FileArtifactValue.createForTesting(archivedArtifact)));
+          archivedArtifact, FileArtifactValue.createForTesting(archivedArtifact));
     }
 
     return builder.build();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java
index 5a21c1e..09ab04f 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java
@@ -14,12 +14,14 @@
 package com.google.devtools.build.lib.skyframe;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact.ArchivedTreeArtifact;
 import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
 import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType;
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
@@ -27,6 +29,7 @@
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.skyframe.TreeArtifactValue.ArchivedRepresentation;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.vfs.Dirent;
@@ -50,11 +53,13 @@
 @RunWith(JUnit4.class)
 public final class TreeArtifactValueTest {
 
+  private static final PathFragment BIN_PATH = PathFragment.create("bin");
+
   private final Scratch scratch = new Scratch();
-  private final ArtifactRoot root = ArtifactRoot.asDerivedRoot(scratch.resolve("root"), "bin");
+  private final ArtifactRoot root = ArtifactRoot.asDerivedRoot(scratch.resolve("root"), BIN_PATH);
 
   @Test
-  public void builderCreatesCorrectValue() {
+  public void createsCorrectValue() {
     SpecialArtifact parent = createTreeArtifact("bin/tree");
     TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(parent, "child1");
     TreeFileArtifact child2 = TreeFileArtifact.createTreeOutput(parent, "child2");
@@ -76,6 +81,34 @@
   }
 
   @Test
+  public void createsCorrectValueWithArchivedRepresentation() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(parent, "child1");
+    TreeFileArtifact child2 = TreeFileArtifact.createTreeOutput(parent, "child2");
+    ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+    FileArtifactValue child1Metadata = metadataWithId(1);
+    FileArtifactValue child2Metadata = metadataWithId(2);
+    FileArtifactValue archivedArtifactMetadata = metadataWithId(3);
+
+    TreeArtifactValue tree =
+        TreeArtifactValue.newBuilder(parent)
+            .putChild(child1, child1Metadata)
+            .putChild(child2, child2Metadata)
+            .setArchivedRepresentation(archivedTreeArtifact, archivedArtifactMetadata)
+            .build();
+
+    assertThat(tree.getChildren()).containsExactly(child1, child2);
+    assertThat(tree.getChildValues())
+        .containsExactly(child1, child1Metadata, child2, child2Metadata);
+    assertThat(tree.getChildPaths())
+        .containsExactly(child1.getParentRelativePath(), child2.getParentRelativePath());
+    assertThat(tree.getDigest()).isNotNull();
+    assertThat(tree.getMetadata().getDigest()).isEqualTo(tree.getDigest());
+    assertThat(tree.getArchivedRepresentation())
+        .hasValue(ArchivedRepresentation.create(archivedTreeArtifact, archivedArtifactMetadata));
+  }
+
+  @Test
   public void empty() {
     TreeArtifactValue emptyTree = TreeArtifactValue.empty();
 
@@ -87,7 +120,7 @@
   }
 
   @Test
-  public void canonicalEmptyInstance() {
+  public void createsCanonicalEmptyInstance() {
     SpecialArtifact parent = createTreeArtifact("bin/tree");
 
     TreeArtifactValue emptyTreeFromBuilder = TreeArtifactValue.newBuilder(parent).build();
@@ -96,6 +129,26 @@
   }
 
   @Test
+  public void createsCorrectEmptyValueWithArchivedRepresentation() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+    FileArtifactValue archivedArtifactMetadata = metadataWithId(1);
+
+    TreeArtifactValue tree =
+        TreeArtifactValue.newBuilder(parent)
+            .setArchivedRepresentation(archivedTreeArtifact, archivedArtifactMetadata)
+            .build();
+
+    assertThat(tree.getChildren()).isEmpty();
+    assertThat(tree.getChildValues()).isEmpty();
+    assertThat(tree.getChildPaths()).isEmpty();
+    assertThat(tree.getDigest()).isNotNull();
+    assertThat(tree.getMetadata().getDigest()).isEqualTo(tree.getDigest());
+    assertThat(tree.getArchivedRepresentation())
+        .hasValue(ArchivedRepresentation.create(archivedTreeArtifact, archivedArtifactMetadata));
+  }
+
+  @Test
   public void cannotCreateBuilderForNonTreeArtifact() {
     SpecialArtifact notTreeArtifact =
         new SpecialArtifact(
@@ -122,6 +175,19 @@
   }
 
   @Test
+  public void cannotAddArchivedRepresentationWithWrongParent() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    ArchivedTreeArtifact archivedDifferentTreeArtifact =
+        createArchivedTreeArtifact(createTreeArtifact("bin/other_tree"));
+    TreeArtifactValue.Builder builderForParent = TreeArtifactValue.newBuilder(parent);
+    FileArtifactValue metadata = metadataWithId(1);
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> builderForParent.setArchivedRepresentation(archivedDifferentTreeArtifact, metadata));
+  }
+
+  @Test
   public void cannotAddOmittedChildToBuilder() {
     SpecialArtifact parent = createTreeArtifact("bin/tree");
     TreeFileArtifact child = TreeFileArtifact.createTreeOutput(parent, "child");
@@ -134,6 +200,19 @@
   }
 
   @Test
+  public void cannotAddOmittedArchivedRepresentation() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+    TreeArtifactValue.Builder builder = TreeArtifactValue.newBuilder(parent);
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            builder.setArchivedRepresentation(
+                archivedTreeArtifact, FileArtifactValue.OMITTED_FILE_MARKER));
+  }
+
+  @Test
   public void orderIndependence() {
     SpecialArtifact parent = createTreeArtifact("bin/tree");
     TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(parent, "child1");
@@ -173,6 +252,27 @@
   }
 
   @Test
+  public void nullDigestsForArchivedRepresentation_equal() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+    FileArtifactValue metadataNoDigest = metadataWithIdNoDigest(1);
+
+    TreeArtifactValue tree1 =
+        TreeArtifactValue.newBuilder(parent)
+            .setArchivedRepresentation(archivedTreeArtifact, metadataNoDigest)
+            .build();
+    TreeArtifactValue tree2 =
+        TreeArtifactValue.newBuilder(parent)
+            .setArchivedRepresentation(archivedTreeArtifact, metadataNoDigest)
+            .build();
+
+    assertThat(metadataNoDigest.getDigest()).isNull();
+    assertThat(tree1.getDigest()).isNotNull();
+    assertThat(tree2.getDigest()).isNotNull();
+    assertThat(tree1).isEqualTo(tree2);
+  }
+
+  @Test
   public void nullDigests_notEqual() {
     SpecialArtifact parent = createTreeArtifact("bin/tree");
     TreeFileArtifact child = TreeFileArtifact.createTreeOutput(parent, "child");
@@ -192,6 +292,29 @@
   }
 
   @Test
+  public void nullDigestsForArchivedRepresentation_notEqual() {
+    SpecialArtifact parent = createTreeArtifact("bin/tree");
+    ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+    FileArtifactValue metadataNoDigest1 = metadataWithIdNoDigest(1);
+    FileArtifactValue metadataNoDigest2 = metadataWithIdNoDigest(2);
+
+    TreeArtifactValue tree1 =
+        TreeArtifactValue.newBuilder(parent)
+            .setArchivedRepresentation(archivedTreeArtifact, metadataNoDigest1)
+            .build();
+    TreeArtifactValue tree2 =
+        TreeArtifactValue.newBuilder(parent)
+            .setArchivedRepresentation(archivedTreeArtifact, metadataNoDigest2)
+            .build();
+
+    assertThat(metadataNoDigest1.getDigest()).isNull();
+    assertThat(metadataNoDigest2.getDigest()).isNull();
+    assertThat(tree1.getDigest()).isNotNull();
+    assertThat(tree2.getDigest()).isNotNull();
+    assertThat(tree1.getDigest()).isNotEqualTo(tree2.getDigest());
+  }
+
+  @Test
   public void visitTree_visitsEachChild() throws Exception {
     Path treeDir = scratch.dir("tree");
     scratch.file("tree/file1");
@@ -324,7 +447,7 @@
   public static final class MultiBuilderTest {
 
     private static final ArtifactRoot ROOT =
-        ArtifactRoot.asDerivedRoot(new InMemoryFileSystem().getPath("/root"), "bin");
+        ArtifactRoot.asDerivedRoot(new InMemoryFileSystem().getPath("/root"), BIN_PATH);
 
     private enum MultiBuilderType {
       BASIC {
@@ -352,7 +475,7 @@
     }
 
     @Test
-    public void empty() {
+    public void emptyBuilder_injectsNothing() {
       TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
 
       treeArtifacts.injectTo(results::put);
@@ -361,7 +484,7 @@
     }
 
     @Test
-    public void singleTreeArtifact() {
+    public void injectsSingleTreeArtifact() {
       TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
       SpecialArtifact parent = createTreeArtifact("bin/tree");
       TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(parent, "child1");
@@ -382,7 +505,7 @@
     }
 
     @Test
-    public void multipleTreeArtifacts() {
+    public void injectsMultipleTreeArtifacts() {
       TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
       SpecialArtifact parent1 = createTreeArtifact("bin/tree1");
       TreeFileArtifact parent1Child1 = TreeFileArtifact.createTreeOutput(parent1, "child1");
@@ -409,11 +532,86 @@
                   .build());
     }
 
+    @Test
+    public void injectsTreeArtifactWithArchivedRepresentation() {
+      TreeArtifactValue.MultiBuilder builder = multiBuilderType.newMultiBuilder();
+      SpecialArtifact parent = createTreeArtifact("bin/tree");
+      TreeFileArtifact child = TreeFileArtifact.createTreeOutput(parent, "child");
+      FileArtifactValue childMetadata = metadataWithId(1);
+      ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+      FileArtifactValue archivedTreeArtifactMetadata = metadataWithId(2);
+
+      builder
+          .putChild(child, childMetadata)
+          .setArchivedRepresentation(archivedTreeArtifact, archivedTreeArtifactMetadata)
+          .injectTo(results::put);
+
+      assertThat(results)
+          .containsExactly(
+              parent,
+              TreeArtifactValue.newBuilder(parent)
+                  .putChild(child, childMetadata)
+                  .setArchivedRepresentation(archivedTreeArtifact, archivedTreeArtifactMetadata)
+                  .build());
+    }
+
+    @Test
+    public void injectsEmptyTreeArtifactWithArchivedRepresentation() {
+      TreeArtifactValue.MultiBuilder builder = multiBuilderType.newMultiBuilder();
+      SpecialArtifact parent = createTreeArtifact("bin/tree");
+      ArchivedTreeArtifact archivedTreeArtifact = createArchivedTreeArtifact(parent);
+      FileArtifactValue metadata = metadataWithId(1);
+
+      builder.setArchivedRepresentation(archivedTreeArtifact, metadata).injectTo(results::put);
+
+      assertThat(results)
+          .containsExactly(
+              parent,
+              TreeArtifactValue.newBuilder(parent)
+                  .setArchivedRepresentation(archivedTreeArtifact, metadata)
+                  .build());
+    }
+
+    @Test
+    public void injectsTreeArtifactsWithAndWithoutArchivedRepresentation() {
+      TreeArtifactValue.MultiBuilder builder = multiBuilderType.newMultiBuilder();
+      SpecialArtifact parent1 = createTreeArtifact("bin/tree1");
+      ArchivedTreeArtifact archivedArtifact1 = createArchivedTreeArtifact(parent1);
+      FileArtifactValue archivedArtifact1Metadata = metadataWithId(1);
+      TreeFileArtifact parent1Child = TreeFileArtifact.createTreeOutput(parent1, "child");
+      FileArtifactValue parent1ChildMetadata = metadataWithId(2);
+      SpecialArtifact parent2 = createTreeArtifact("bin/tree2");
+      TreeFileArtifact parent2Child = TreeFileArtifact.createTreeOutput(parent2, "child");
+      FileArtifactValue parent2ChildMetadata = metadataWithId(3);
+
+      builder
+          .setArchivedRepresentation(archivedArtifact1, archivedArtifact1Metadata)
+          .putChild(parent1Child, parent1ChildMetadata)
+          .putChild(parent2Child, parent2ChildMetadata)
+          .injectTo(results::put);
+
+      assertThat(results)
+          .containsExactly(
+              parent1,
+              TreeArtifactValue.newBuilder(parent1)
+                  .putChild(parent1Child, parent1ChildMetadata)
+                  .setArchivedRepresentation(archivedArtifact1, metadataWithId(1))
+                  .build(),
+              parent2,
+              TreeArtifactValue.newBuilder(parent2)
+                  .putChild(parent2Child, parent2ChildMetadata)
+                  .build());
+    }
+
     private static SpecialArtifact createTreeArtifact(String execPath) {
       return TreeArtifactValueTest.createTreeArtifact(execPath, ROOT);
     }
   }
 
+  private static ArchivedTreeArtifact createArchivedTreeArtifact(SpecialArtifact specialArtifact) {
+    return ArchivedTreeArtifact.create(specialArtifact, BIN_PATH);
+  }
+
   private SpecialArtifact createTreeArtifact(String execPath) {
     return createTreeArtifact(execPath, root);
   }