Make Artifact an abstract class, and break out a DerivedArtifact subclass. This will facilitate future refactorings in which a DerivedArtifact will have an ActionLookupData "owner", while a SourceArtifact will have a Label owner (or an ArtifactOwner, depending on how lazy I am).

I verified using the test in ArtifactTest, modified to use dummy classes, that derived artifacts don't use any more memory with this refactoring than they did before.

PiperOrigin-RevId: 249535371
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
index 4b693bd..5efea6a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -104,7 +104,7 @@
  */
 @Immutable
 @AutoCodec
-public class Artifact
+public abstract class Artifact
     implements FileType.HasFileType,
         ActionInput,
         FileApi,
@@ -138,7 +138,7 @@
         } else if (b == null) {
           return 1;
         } else {
-          int result = a.rootRelativePath.compareTo(b.rootRelativePath);
+          int result = a.getRootRelativePath().compareTo(b.getRootRelativePath());
           if (result == 0) {
             // Use the full exec path as a fallback if the root-relative paths are the same, thus
             // avoiding problems when ImmutableSortedMaps are switched from EXEC_PATH_COMPARATOR.
@@ -223,12 +223,12 @@
   /** A Predicate that evaluates to true if the Artifact is not a middleman artifact. */
   public static final Predicate<Artifact> MIDDLEMAN_FILTER = input -> !input.isMiddlemanArtifact();
 
-  private static final Interner<Artifact> ARTIFACT_INTERNER = BlazeInterners.newWeakInterner();
+  private static final Interner<DerivedArtifact> ARTIFACT_INTERNER =
+      BlazeInterners.newWeakInterner();
 
   private final int hashCode;
   private final ArtifactRoot root;
   private final PathFragment execPath;
-  private final PathFragment rootRelativePath;
   private final ArtifactOwner owner;
 
   /**
@@ -250,54 +250,17 @@
               + ")");
     }
     PathFragment rootExecPath = root.getExecPath();
-    Artifact artifact = new Artifact(
-        root,
-        rootExecPath.isEmpty() ? rootRelativePath : rootExecPath.getRelative(rootRelativePath),
-        rootRelativePath,
-        owner);
-    if (artifact.isSourceArtifact()) {
-      return artifact;
+    if (rootExecPath.isEmpty()) {
+      return new Artifact.SourceArtifact(root, rootRelativePath, owner);
     } else {
-      return ARTIFACT_INTERNER.intern(artifact);
-    }
-  }
-
-  /**
-   * Constructs an artifact for the specified path, root and execPath. The root must be an ancestor
-   * of path, and execPath must be a non-absolute tail of path. Outside of testing, this method
-   * should only be called by ArtifactFactory. The ArtifactOwner may be null.
-   *
-   * <p>In a source Artifact, the path tail after the root will be identical to the execPath, but
-   * the root will be orthogonal to execRoot.
-   *
-   * <pre>
-   *  [path] == [/root/][execPath]
-   * </pre>
-   *
-   * <p>In a derived Artifact, the execPath will overlap with part of the root, which in turn will
-   * be below the execRoot.
-   *
-   * <pre>
-   *  [path] == [/root][pathTail] == [/execRoot][execPath] == [/execRoot][rootPrefix][pathTail]
-   * </pre>
-   */
-  @VisibleForTesting
-  public Artifact(ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) {
-    this(
-        Preconditions.checkNotNull(root),
-        Preconditions.checkNotNull(execPath, "Null execPath not allowed (root %s", root),
-        root.getExecPath().isEmpty() ? execPath : execPath.relativeTo(root.getExecPath()),
-        Preconditions.checkNotNull(owner));
-    if (execPath.isAbsolute() != root.getRoot().isAbsolute()) {
-      throw new IllegalArgumentException(
-          execPath + ": illegal execPath for " + execPath + " (root: " + root + ")");
+      return ARTIFACT_INTERNER.intern(
+          new Artifact.DerivedArtifact(root, rootExecPath.getRelative(rootRelativePath), owner));
     }
   }
 
   private Artifact(
       ArtifactRoot root,
       PathFragment execPath,
-      PathFragment rootRelativePath,
       ArtifactOwner owner) {
     Preconditions.checkNotNull(root);
     if (execPath.isEmpty()) {
@@ -310,54 +273,30 @@
     this.hashCode = execPath.hashCode();
     this.root = root;
     this.execPath = execPath;
-    this.rootRelativePath = rootRelativePath;
     this.owner = Preconditions.checkNotNull(owner);
   }
 
-  /**
-   * Constructs an artifact for the specified path, root and execPath. The root must be an ancestor
-   * of path, and execPath must be a non-absolute tail of path. Should only be called for testing.
-   *
-   * <p>In a source Artifact, the path tail after the root will be identical to the execPath, but
-   * the root will be orthogonal to execRoot.
-   * <pre>
-   *  [path] == [/root/][execPath]
-   * </pre>
-   *
-   * <p>In a derived Artifact, the execPath will overlap with part of the root, which in turn will
-   * be below of the execRoot.
-   * <pre>
-   *  [path] == [/root][pathTail] == [/execRoot][execPath] == [/execRoot][rootPrefix][pathTail]
-   * <pre>
-   */
+  /** An artifact corresponding to a file in the output tree, generated by an {@link Action}. */
   @VisibleForTesting
-  public Artifact(ArtifactRoot root, PathFragment execPath) {
-    this(root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE);
-  }
+  public static class DerivedArtifact extends Artifact {
+    private final PathFragment rootRelativePath;
 
-  /**
-   * Constructs a source or derived Artifact for the specified path and specified root. The root
-   * must be an ancestor of the path.
-   */
-  @VisibleForTesting // Only exists for testing.
-  public Artifact(Path path, ArtifactRoot root) {
-    this(
-        root,
-        root.getExecPath().getRelative(root.getRoot().relativize(path)),
-        ArtifactOwner.NullArtifactOwner.INSTANCE);
-  }
+    @VisibleForTesting
+    public DerivedArtifact(ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) {
+      super(root, execPath, owner);
+      Preconditions.checkState(
+          !root.getExecPath().isEmpty(), "Derived root has no exec path: %s, %s", root, execPath);
+      this.rootRelativePath = execPath.relativeTo(root.getExecPath());
+    }
 
-  /** Constructs a source or derived Artifact for the specified root-relative path and root. */
-  @VisibleForTesting // Only exists for testing.
-  public Artifact(PathFragment rootRelativePath, ArtifactRoot root) {
-    this(
-        root,
-        root.getExecPath().getRelative(rootRelativePath),
-        ArtifactOwner.NullArtifactOwner.INSTANCE);
+    @Override
+    public PathFragment getRootRelativePath() {
+      return rootRelativePath;
+    }
   }
 
   public final Path getPath() {
-    return root.getRoot().getRelative(rootRelativePath);
+    return root.getRoot().getRelative(getRootRelativePath());
   }
 
   public boolean hasParent() {
@@ -531,6 +470,11 @@
     public boolean equals(Object other) {
       return other instanceof SourceArtifact && equalsWithoutOwner((SourceArtifact) other);
     }
+
+    @Override
+    public PathFragment getRootRelativePath() {
+      return getExecPath();
+    }
   }
 
   /**
@@ -555,7 +499,7 @@
   @Immutable
   @VisibleForTesting
   @AutoCodec
-  public static final class SpecialArtifact extends Artifact {
+  public static final class SpecialArtifact extends DerivedArtifact {
     private final SpecialArtifactType type;
 
     @VisibleForSerialization
@@ -617,7 +561,7 @@
    */
   @Immutable
   @AutoCodec
-  public static final class TreeFileArtifact extends Artifact {
+  public static final class TreeFileArtifact extends DerivedArtifact {
     private final Artifact parentTreeArtifact;
     private final PathFragment parentRelativePath;
 
@@ -671,19 +615,17 @@
   }
 
   /**
-   * Returns the relative path to this artifact relative to its root.  (Useful
-   * when deriving output filenames from input files, etc.)
+   * Returns the relative path to this artifact relative to its root. (Useful when deriving output
+   * filenames from input files, etc.)
    */
-  public final PathFragment getRootRelativePath() {
-    return rootRelativePath;
-  }
+  public abstract PathFragment getRootRelativePath();
 
   /**
    * For targets in external repositories, this returns the path the artifact live at in the
    * runfiles tree. For local targets, it returns the rootRelativePath.
    */
   public final PathFragment getRunfilesPath() {
-    PathFragment relativePath = rootRelativePath;
+    PathFragment relativePath = getRootRelativePath();
     if (relativePath.startsWith(LabelConstants.EXTERNAL_PATH_PREFIX)) {
       // Turn external/repo/foo into ../repo/foo.
       relativePath = relativePath.relativeTo(LabelConstants.EXTERNAL_PATH_PREFIX);
@@ -719,7 +661,7 @@
   public final String prettyPrint() {
     // toDetailString would probably be more useful to users, but lots of tests rely on the
     // current values.
-    return rootRelativePath.toString();
+    return getRootRelativePath().toString();
   }
 
   @SuppressWarnings("EqualsGetClass") // Distinct classes of Artifact are never equal.
@@ -770,13 +712,13 @@
   public final String toDetailString() {
     if (isSourceArtifact()) {
       // Source Artifact: relPath == execPath, & real path is not under execRoot
-      return "[" + root + "]" + rootRelativePath;
+      return "[" + root + "]" + getRootRelativePathString();
     } else {
       // Derived Artifact: path and root are under execRoot
       //
       // TODO(blaze-team): this is misleading because execution_root isn't unique. Dig the
       // workspace name out and print that also.
-      return "[[<execution_root>]" + root.getExecPath() + "]" + rootRelativePath;
+      return "[[<execution_root>]" + root.getExecPath() + "]" + getRootRelativePathString();
     }
   }
 
@@ -1015,9 +957,9 @@
   @Override
   public void repr(SkylarkPrinter printer) {
     if (isSourceArtifact()) {
-      printer.append("<source file " + rootRelativePath + ">");
+      printer.append("<source file " + getRootRelativePathString() + ">");
     } else {
-      printer.append("<generated file " + rootRelativePath + ">");
+      printer.append("<generated file " + getRootRelativePathString() + ">");
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
index f04ebcd..30ea236 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
@@ -319,7 +319,7 @@
     if (type == null) {
       return root.isSourceRoot()
           ? new Artifact.SourceArtifact(root, execPath, owner)
-          : new Artifact(root, execPath, owner);
+          : new Artifact.DerivedArtifact(root, execPath, owner);
     } else {
       return new Artifact.SpecialArtifact(root, execPath, owner, type);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 38e7710..3558a4b 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -83,12 +83,6 @@
             return compareLists((SkylarkList) o1, (SkylarkList) o2);
           }
 
-          if (!(o1.getClass().isAssignableFrom(o2.getClass())
-              || o2.getClass().isAssignableFrom(o1.getClass()))) {
-            throw new ComparisonException(
-                "Cannot compare " + getDataTypeName(o1) + " with " + getDataTypeName(o2));
-          }
-
           if (o1 instanceof ClassObject) {
             throw new ComparisonException("Cannot compare structs");
           }
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
index 23d5661..39b49a2 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissDetail;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeArtifactResolverBase;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeMetadataHandlerBase;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil.MissDetailsBuilder;
@@ -281,7 +282,7 @@
             FileSystem fileSystem = getPrimaryOutput().getPath().getFileSystem();
             Path path = fileSystem.getPath("/input");
             ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(fileSystem.getPath("/")));
-            return ImmutableList.of(new Artifact(path, root));
+            return ImmutableList.of(ActionsTestUtil.createArtifact(root, path));
           }
         };
     runAction(action);  // Not cached so recorded as different deps.
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
index b0a050e..2020379 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
@@ -65,30 +65,59 @@
     Path bogusDir = scratch.file("/exec/dir/bogus");
     assertThrows(
         IllegalArgumentException.class,
-        () -> new Artifact(ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir)));
+        () ->
+            ActionsTestUtil.createArtifactWithExecPath(
+                ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir)));
+  }
+
+  private static long getUsedMemory() {
+    System.gc();
+    System.gc();
+    System.runFinalization();
+    System.gc();
+    System.gc();
+    return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+  }
+
+  @Test
+  public void testMemoryUsage() throws IOException {
+    ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo")));
+    PathFragment aPath = PathFragment.create("src/a");
+    int arrSize = 1 << 20;
+    Object[] arr = new Object[arrSize];
+    long usedMemory = getUsedMemory();
+    for (int i = 0; i < arrSize; i++) {
+      arr[i] = ActionsTestUtil.createArtifactWithExecPath(root, aPath);
+    }
+    assertThat((getUsedMemory() - usedMemory) / arrSize).isAtMost(34L);
   }
 
   @Test
   public void testEquivalenceRelation() throws Exception {
     PathFragment aPath = PathFragment.create("src/a");
     PathFragment bPath = PathFragment.create("src/b");
-    assertThat(new Artifact(aPath, rootDir)).isEqualTo(new Artifact(aPath, rootDir));
-    assertThat(new Artifact(bPath, rootDir)).isEqualTo(new Artifact(bPath, rootDir));
-    assertThat(new Artifact(aPath, rootDir).equals(new Artifact(bPath, rootDir))).isFalse();
+    assertThat(ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, aPath))
+        .isEqualTo(ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, aPath));
+    assertThat(ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, bPath))
+        .isEqualTo(ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, bPath));
+    assertThat(
+            ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, aPath)
+                .equals(ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, bPath)))
+        .isFalse();
   }
 
   @Test
-  public void testEmptyLabelIsNone() throws Exception {
-    Artifact artifact = new Artifact(PathFragment.create("src/a"), rootDir);
+  public void testEmptyLabelIsNone() {
+    Artifact artifact = ActionsTestUtil.createArtifact(rootDir, "src/a");
     assertThat(artifact.getOwnerLabel()).isNull();
   }
 
   @Test
-  public void testComparison() throws Exception {
+  public void testComparison() {
     PathFragment aPath = PathFragment.create("src/a");
     PathFragment bPath = PathFragment.create("src/b");
-    Artifact aArtifact = new Artifact(aPath, rootDir);
-    Artifact bArtifact = new Artifact(bPath, rootDir);
+    Artifact aArtifact = ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, aPath);
+    Artifact bArtifact = ActionsTestUtil.createArtifactWithRootRelativePath(rootDir, bPath);
     assertThat(Artifact.EXEC_PATH_COMPARATOR.compare(aArtifact, bArtifact)).isEqualTo(-1);
     assertThat(Artifact.EXEC_PATH_COMPARATOR.compare(aArtifact, aArtifact)).isEqualTo(0);
     assertThat(Artifact.EXEC_PATH_COMPARATOR.compare(bArtifact, bArtifact)).isEqualTo(0);
@@ -98,7 +127,7 @@
   @Test
   public void testRootPrefixedExecPath_normal() throws IOException {
     Path f1 = scratch.file("/exec/root/dir/file.ext");
-    Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir));
+    Artifact a1 = ActionsTestUtil.createArtifactWithExecPath(rootDir, f1.relativeTo(execDir));
     assertThat(Artifact.asRootPrefixedExecPath(a1)).isEqualTo("root:dir/file.ext");
   }
 
@@ -106,14 +135,19 @@
   public void testRootPrefixedExecPath_noRoot() throws IOException {
     Path f1 = scratch.file("/exec/dir/file.ext");
     Artifact a1 =
-        new Artifact(f1.relativeTo(execDir), ArtifactRoot.asSourceRoot(Root.fromPath(execDir)));
+        ActionsTestUtil.createArtifactWithExecPath(
+            ArtifactRoot.asSourceRoot(Root.fromPath(execDir)), f1.relativeTo(execDir));
     assertThat(Artifact.asRootPrefixedExecPath(a1)).isEqualTo(":dir/file.ext");
   }
 
   @Test
   public void testRootPrefixedExecPath_nullRootDir() throws IOException {
     Path f1 = scratch.file("/exec/dir/file.ext");
-    assertThrows(NullPointerException.class, () -> new Artifact(null, f1.relativeTo(execDir)));
+    assertThrows(
+        NullPointerException.class,
+        () ->
+            new Artifact.DerivedArtifact(
+                null, f1.relativeTo(execDir), ArtifactOwner.NullArtifactOwner.INSTANCE));
   }
 
   @Test
@@ -121,9 +155,9 @@
     Path f1 = scratch.file("/exec/root/dir/file1.ext");
     Path f2 = scratch.file("/exec/root/dir/dir/file2.ext");
     Path f3 = scratch.file("/exec/root/dir/dir/dir/file3.ext");
-    Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir));
-    Artifact a2 = new Artifact(rootDir, f2.relativeTo(execDir));
-    Artifact a3 = new Artifact(rootDir, f3.relativeTo(execDir));
+    Artifact a1 = ActionsTestUtil.createArtifactWithExecPath(rootDir, f1.relativeTo(execDir));
+    Artifact a2 = ActionsTestUtil.createArtifactWithExecPath(rootDir, f2.relativeTo(execDir));
+    Artifact a3 = ActionsTestUtil.createArtifactWithExecPath(rootDir, f3.relativeTo(execDir));
     List<String> strings = new ArrayList<>();
     Artifact.addRootPrefixedExecPaths(Lists.newArrayList(a1, a2, a3), strings);
     assertThat(strings).containsExactly(
@@ -135,10 +169,11 @@
   @Test
   public void testGetFilename() throws Exception {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo")));
-    Artifact javaFile = new Artifact(scratch.file("/foo/Bar.java"), root);
-    Artifact generatedHeader = new Artifact(scratch.file("/foo/bar.proto.h"), root);
-    Artifact generatedCc = new Artifact(scratch.file("/foo/bar.proto.cc"), root);
-    Artifact aCPlusPlusFile = new Artifact(scratch.file("/foo/bar.cc"), root);
+    Artifact javaFile = ActionsTestUtil.createArtifact(root, scratch.file("/foo/Bar.java"));
+    Artifact generatedHeader =
+        ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar.proto.h"));
+    Artifact generatedCc = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar.proto.cc"));
+    Artifact aCPlusPlusFile = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar.cc"));
     assertThat(JavaSemantics.JAVA_SOURCE.matches(javaFile.getFilename())).isTrue();
     assertThat(CppFileTypes.CPP_HEADER.matches(generatedHeader.getFilename())).isTrue();
     assertThat(CppFileTypes.CPP_SOURCE.matches(generatedCc.getFilename())).isTrue();
@@ -148,7 +183,7 @@
   @Test
   public void testGetExtension() throws Exception {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo")));
-    Artifact javaFile = new Artifact(scratch.file("/foo/Bar.java"), root);
+    Artifact javaFile = ActionsTestUtil.createArtifact(root, scratch.file("/foo/Bar.java"));
     assertThat(javaFile.getExtension()).isEqualTo("java");
   }
 
@@ -161,13 +196,12 @@
   private List<Artifact> getFooBarArtifacts(MutableActionGraph actionGraph, boolean collapsedList)
       throws Exception {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo")));
-    Artifact aHeader1 = new Artifact(scratch.file("/foo/bar1.h"), root);
-    Artifact aHeader2 = new Artifact(scratch.file("/foo/bar2.h"), root);
-    Artifact aHeader3 = new Artifact(scratch.file("/foo/bar3.h"), root);
-    Artifact middleman =
-        new Artifact(
-            PathFragment.create("middleman"),
-            ArtifactRoot.middlemanRoot(scratch.dir("/foo"), scratch.dir("/foo/out")));
+    Artifact aHeader1 = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar1.h"));
+    Artifact aHeader2 = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar2.h"));
+    Artifact aHeader3 = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar3.h"));
+    ArtifactRoot middleRoot =
+        ArtifactRoot.middlemanRoot(scratch.dir("/foo"), scratch.dir("/foo/out"));
+    Artifact middleman = ActionsTestUtil.createArtifact(middleRoot, "middleman");
     actionGraph.registerAction(new MiddlemanAction(ActionsTestUtil.NULL_ACTION_OWNER,
         ImmutableList.of(aHeader1, aHeader2, aHeader3), middleman, "desc",
         MiddlemanType.AGGREGATING_MIDDLEMAN));
@@ -279,7 +313,7 @@
   @Test
   public void testRootRelativePathIsSameAsExecPath() throws Exception {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo")));
-    Artifact a = new Artifact(scratch.file("/foo/bar1.h"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, scratch.file("/foo/bar1.h"));
     assertThat(a.getRootRelativePath()).isSameInstanceAs(a.getExecPath());
   }
 
@@ -287,32 +321,30 @@
   public void testToDetailString() throws Exception {
     Path execRoot = scratch.getFileSystem().getPath("/execroot/workspace");
     Artifact a =
-        new Artifact(
-            ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/execroot/workspace/b")),
-            PathFragment.create("b/c"));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/execroot/workspace/b")), "c");
     assertThat(a.toDetailString()).isEqualTo("[[<execution_root>]b]c");
   }
 
   @Test
-  public void testWeirdArtifact() throws Exception {
+  public void testWeirdArtifact() {
     Path execRoot = scratch.getFileSystem().getPath("/");
     assertThrows(
         IllegalArgumentException.class,
         () ->
-            new Artifact(
+            ActionsTestUtil.createArtifactWithExecPath(
                 ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a")), PathFragment.create("c")));
   }
 
   @Test
   public void testCodec() throws Exception {
+    ArtifactRoot anotherRoot =
+        ArtifactRoot.asDerivedRoot(scratch.getFileSystem().getPath("/"), scratch.dir("/src"));
     new SerializationTester(
-            new Artifact(PathFragment.create("src/a"), rootDir),
-            new Artifact(
-                PathFragment.create("src/b"), ArtifactRoot.asSourceRoot(Root.fromPath(execDir))),
-            new Artifact(
-                ArtifactRoot.asDerivedRoot(
-                    scratch.getFileSystem().getPath("/"), scratch.dir("/src")),
-                PathFragment.create("src/c"),
+            ActionsTestUtil.createArtifact(rootDir, "src/a"),
+            new Artifact.DerivedArtifact(
+                anotherRoot,
+                anotherRoot.getExecPath().getRelative("src/c"),
                 new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar"))))
         .addDependency(FileSystem.class, scratch.getFileSystem())
         .runTests();
@@ -363,9 +395,9 @@
   @Test
   public void testDirnameInExecutionDir() throws Exception {
     Artifact artifact =
-        new Artifact(
-            scratch.file("/foo/bar.txt"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/foo"))),
+            scratch.file("/foo/bar.txt"));
 
     assertThat(artifact.getDirname()).isEqualTo(".");
   }
@@ -382,16 +414,17 @@
   @Test
   public void testIsSourceArtifact() throws Exception {
     assertThat(
-            new Artifact(
+            new Artifact.SourceArtifact(
                     ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/"))),
-                    PathFragment.create("src/foo.cc"))
+                    PathFragment.create("src/foo.cc"),
+                    ArtifactOwner.NullArtifactOwner.INSTANCE)
                 .isSourceArtifact())
         .isTrue();
     assertThat(
-            new Artifact(
-                    scratch.file("/genfiles/aaa/bar.out"),
+            ActionsTestUtil.createArtifact(
                     ArtifactRoot.asDerivedRoot(
-                        scratch.dir("/genfiles"), scratch.dir("/genfiles/aaa")))
+                        scratch.dir("/genfiles"), scratch.dir("/genfiles/aaa")),
+                    scratch.file("/genfiles/aaa/bar.out"))
                 .isSourceArtifact())
         .isFalse();
   }
@@ -400,7 +433,8 @@
   public void testGetRoot() throws Exception {
     Path execRoot = scratch.getFileSystem().getPath("/");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/newRoot"));
-    assertThat(new Artifact(scratch.file("/newRoot/foo"), root).getRoot()).isEqualTo(root);
+    assertThat(ActionsTestUtil.createArtifact(root, scratch.file("/newRoot/foo")).getRoot())
+        .isEqualTo(root);
   }
 
   @Test
@@ -409,8 +443,10 @@
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/newRoot"));
     ArtifactOwner firstOwner = () -> Label.parseAbsoluteUnchecked("//bar:bar");
     ArtifactOwner secondOwner = () -> Label.parseAbsoluteUnchecked("//foo:foo");
-    Artifact derived1 = new Artifact(root, PathFragment.create("newRoot/shared"), firstOwner);
-    Artifact derived2 = new Artifact(root, PathFragment.create("newRoot/shared"), secondOwner);
+    Artifact derived1 =
+        new Artifact.DerivedArtifact(root, PathFragment.create("newRoot/shared"), firstOwner);
+    Artifact derived2 =
+        new Artifact.DerivedArtifact(root, PathFragment.create("newRoot/shared"), secondOwner);
     ArtifactRoot sourceRoot = ArtifactRoot.asSourceRoot(Root.fromPath(root.getRoot().asPath()));
     Artifact source1 = new SourceArtifact(sourceRoot, PathFragment.create("shared"), firstOwner);
     Artifact source2 = new SourceArtifact(sourceRoot, PathFragment.create("shared"), secondOwner);
@@ -438,8 +474,8 @@
   }
 
   private Artifact createDirNameArtifact() throws Exception {
-    return new Artifact(
-        scratch.file("/aaa/bbb/ccc/ddd"),
-        ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/"))));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/"))),
+        scratch.file("/aaa/bbb/ccc/ddd"));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/actions/CompositeRunfilesSupplierTest.java b/src/test/java/com/google/devtools/build/lib/actions/CompositeRunfilesSupplierTest.java
index 3ebc001..eff483c 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/CompositeRunfilesSupplierTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/CompositeRunfilesSupplierTest.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.testutil.Scratch;
@@ -152,19 +153,15 @@
   private static Map<PathFragment, Artifact> mkMappings(ArtifactRoot rootDir, String... paths) {
     ImmutableMap.Builder<PathFragment, Artifact> builder = ImmutableMap.builder();
     for (String path : paths) {
-      builder.put(PathFragment.create(path), mkArtifact(rootDir, path));
+      builder.put(PathFragment.create(path), ActionsTestUtil.createArtifact(rootDir, path));
     }
     return builder.build();
   }
 
-  private static Artifact mkArtifact(ArtifactRoot rootDir, String path) {
-    return new Artifact(PathFragment.create(path), rootDir);
-  }
-
   private static NestedSet<Artifact> mkArtifacts(ArtifactRoot rootDir, String... paths) {
     NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
     for (String path : paths) {
-      builder.add(mkArtifact(rootDir, path));
+      builder.add(ActionsTestUtil.createArtifact(rootDir, path));
     }
     return builder.build();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
index f387863..21ba9fc 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
@@ -24,6 +24,7 @@
 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;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
 import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.VectorArg;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -34,7 +35,6 @@
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.LazyString;
 import com.google.devtools.build.lib.vfs.PathFragment;
-import com.google.devtools.build.lib.vfs.Root;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
@@ -58,9 +58,9 @@
   @Before
   public void createArtifacts() throws Exception  {
     scratch = new Scratch();
-    rootDir = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/exec/root")));
-    artifact1 = new Artifact(scratch.file("/exec/root/dir/file1.txt"), rootDir);
-    artifact2 = new Artifact(scratch.file("/exec/root/dir/file2.txt"), rootDir);
+    rootDir = ArtifactRoot.asDerivedRoot(scratch.dir("/exec/root"), scratch.dir("/exec/root/dir"));
+    artifact1 = ActionsTestUtil.createArtifact(rootDir, scratch.file("/exec/root/dir/file1.txt"));
+    artifact2 = ActionsTestUtil.createArtifact(rootDir, scratch.file("/exec/root/dir/file2.txt"));
   }
 
   @Test
@@ -912,9 +912,9 @@
     assertThat(commandLine.arguments())
         .containsExactly(
             "--argOne",
-            "myArtifact/treeArtifact1/children/child1",
+            "dir/myArtifact/treeArtifact1/children/child1",
             "--argTwo",
-            "myArtifact/treeArtifact2/children/child2")
+            "dir/myArtifact/treeArtifact2/children/child2")
         .inOrder();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java b/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
index 48a99c0..46d5bf6 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ExecutableSymlinkActionTest.java
@@ -19,6 +19,7 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.DummyExecutor;
 import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
 import com.google.devtools.build.lib.exec.SingleBuildFileCache;
@@ -77,8 +78,8 @@
     Path outputFile = outputRoot.getRoot().getRelative("some-output");
     FileSystemUtils.createEmptyFile(inputFile);
     inputFile.setExecutable(/*executable=*/true);
-    Artifact input = new Artifact(inputFile, inputRoot);
-    Artifact output = new Artifact(outputFile, outputRoot);
+    Artifact input = ActionsTestUtil.createArtifact(inputRoot, inputFile);
+    Artifact output = ActionsTestUtil.createArtifact(outputRoot, outputFile);
     SymlinkAction action = SymlinkAction.toExecutable(NULL_ACTION_OWNER, input, output, "progress");
     ActionResult actionResult = action.execute(createContext());
     assertThat(actionResult.spawnResults()).isEmpty();
@@ -89,8 +90,9 @@
   public void testFailIfInputIsNotAFile() throws Exception {
     Path dir = inputRoot.getRoot().getRelative("some-dir");
     FileSystemUtils.createDirectoryAndParents(dir);
-    Artifact input = new Artifact(dir, inputRoot);
-    Artifact output = new Artifact(outputRoot.getRoot().getRelative("some-output"), outputRoot);
+    Artifact input = ActionsTestUtil.createArtifact(inputRoot, dir);
+    Artifact output =
+        ActionsTestUtil.createArtifact(outputRoot, outputRoot.getRoot().getRelative("some-output"));
     SymlinkAction action = SymlinkAction.toExecutable(NULL_ACTION_OWNER, input, output, "progress");
     ActionExecutionException e =
         assertThrows(ActionExecutionException.class, () -> action.execute(createContext()));
@@ -102,8 +104,9 @@
     Path file = inputRoot.getRoot().getRelative("some-file");
     FileSystemUtils.createEmptyFile(file);
     file.setExecutable(/*executable=*/false);
-    Artifact input = new Artifact(file, inputRoot);
-    Artifact output = new Artifact(outputRoot.getRoot().getRelative("some-output"), outputRoot);
+    Artifact input = ActionsTestUtil.createArtifact(inputRoot, file);
+    Artifact output =
+        ActionsTestUtil.createArtifact(outputRoot, outputRoot.getRoot().getRelative("some-output"));
     SymlinkAction action = SymlinkAction.toExecutable(NULL_ACTION_OWNER, input, output, "progress");
     ActionExecutionException e =
         assertThrows(ActionExecutionException.class, () -> action.execute(createContext()));
@@ -119,8 +122,9 @@
     Path file = inputRoot.getRoot().getRelative("some-file");
     FileSystemUtils.createEmptyFile(file);
     file.setExecutable(/*executable=*/ false);
-    Artifact input = new Artifact(file, inputRoot);
-    Artifact output = new Artifact(outputRoot.getRoot().getRelative("some-output"), outputRoot);
+    Artifact input = ActionsTestUtil.createArtifact(inputRoot, file);
+    Artifact output =
+        ActionsTestUtil.createArtifact(outputRoot, outputRoot.getRoot().getRelative("some-output"));
     SymlinkAction action = SymlinkAction.toExecutable(NULL_ACTION_OWNER, input, output, "progress");
     new SerializationTester(action)
         .addDependency(FileSystem.class, scratch.getFileSystem())
diff --git a/src/test/java/com/google/devtools/build/lib/actions/FailActionTest.java b/src/test/java/com/google/devtools/build/lib/actions/FailActionTest.java
index ae2ec55..d991b7c 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/FailActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/FailActionTest.java
@@ -18,6 +18,7 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.testutil.Scratch;
 import java.util.Collection;
 import java.util.Collections;
@@ -43,9 +44,9 @@
   public final void setUp() throws Exception  {
     errorMessage = "An error just happened.";
     anOutput =
-        new Artifact(
-            scratch.file("/out/foo"),
-            ArtifactRoot.asDerivedRoot(scratch.dir("/"), scratch.dir("/out")));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asDerivedRoot(scratch.dir("/"), scratch.dir("/out")),
+            scratch.file("/out/foo"));
     outputs = ImmutableList.of(anOutput);
     failAction = new FailAction(NULL_ACTION_OWNER, outputs, errorMessage);
     actionGraph.registerAction(failAction);
diff --git a/src/test/java/com/google/devtools/build/lib/actions/MapBasedActionGraphTest.java b/src/test/java/com/google/devtools/build/lib/actions/MapBasedActionGraphTest.java
index 5ec0d4a..080493c 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/MapBasedActionGraphTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/MapBasedActionGraphTest.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil.UncheckedActionConflictException;
 import com.google.devtools.build.lib.actions.util.TestAction;
 import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
@@ -45,13 +46,14 @@
     Path execRoot = fileSystem.getPath("/");
     Path root = fileSystem.getPath("/root");
     Path path = root.getRelative("foo");
-    Artifact output = new Artifact(path, ArtifactRoot.asDerivedRoot(execRoot, root));
+    Artifact output =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asDerivedRoot(execRoot, root), path);
     Action action = new TestAction(TestAction.NO_EFFECT,
         ImmutableSet.<Artifact>of(), ImmutableSet.of(output));
     actionGraph.registerAction(action);
     actionGraph.unregisterAction(action);
     path = root.getRelative("bar");
-    output = new Artifact(path, ArtifactRoot.asDerivedRoot(execRoot, root));
+    output = ActionsTestUtil.createArtifact(ArtifactRoot.asDerivedRoot(execRoot, root), path);
     Action action2 = new TestAction(TestAction.NO_EFFECT,
         ImmutableSet.<Artifact>of(), ImmutableSet.of(output));
     actionGraph.registerAction(action);
@@ -65,7 +67,8 @@
     Path execRoot = fileSystem.getPath("/");
     Path root = fileSystem.getPath("/root");
     Path path = root.getRelative("/root/foo");
-    Artifact output = new Artifact(path, ArtifactRoot.asDerivedRoot(execRoot, root));
+    Artifact output =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asDerivedRoot(execRoot, root), path);
     Action action = new TestAction(TestAction.NO_EFFECT,
         ImmutableSet.<Artifact>of(), ImmutableSet.of(output));
     actionGraph.registerAction(action);
@@ -93,7 +96,7 @@
       Path execRoot = fileSystem.getPath("/");
       Path root = fileSystem.getPath("/root");
       Path path = root.getRelative("foo");
-      output = new Artifact(path, ArtifactRoot.asDerivedRoot(execRoot, root));
+      output = ActionsTestUtil.createArtifact(ArtifactRoot.asDerivedRoot(execRoot, root), path);
       allActions.add(new TestAction(
           TestAction.NO_EFFECT, ImmutableSet.<Artifact>of(), ImmutableSet.of(output)));
     }
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 6600f2a..5ad450c 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
@@ -205,6 +205,25 @@
     };
   }
 
+  public static Artifact createArtifact(ArtifactRoot root, Path path) {
+    return createArtifactWithRootRelativePath(root, root.getRoot().relativize(path));
+  }
+
+  public static Artifact createArtifact(ArtifactRoot root, String path) {
+    return createArtifactWithRootRelativePath(root, PathFragment.create(path));
+  }
+
+  public static Artifact createArtifactWithRootRelativePath(
+      ArtifactRoot root, PathFragment rootRelativePath) {
+    PathFragment execPath = root.getExecPath().getRelative(rootRelativePath);
+    return createArtifactWithExecPath(root, execPath);
+  }
+
+  public static Artifact createArtifactWithExecPath(ArtifactRoot root, PathFragment execPath) {
+    return root.isSourceRoot()
+        ? new Artifact.SourceArtifact(root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE)
+        : new Artifact.DerivedArtifact(root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE);
+  }
 
   /**
    * {@link SkyFunction.Environment} that internally makes a full Skyframe evaluate call for the
@@ -267,10 +286,22 @@
     }
   }
 
+  static class NullArtifactOwner implements ArtifactOwner {
+    private NullArtifactOwner() {}
+
+    @Override
+    public Label getLabel() {
+      return NULL_LABEL;
+    }
+  }
+
+  @AutoCodec public static final ArtifactOwner NULL_ARTIFACT_OWNER = new NullArtifactOwner();
+
   public static final Artifact DUMMY_ARTIFACT =
-      new Artifact(
+      new Artifact.SourceArtifact(
+          ArtifactRoot.asSourceRoot(Root.absoluteRoot(new InMemoryFileSystem())),
           PathFragment.create("/dummy"),
-          ArtifactRoot.asSourceRoot(Root.absoluteRoot(new InMemoryFileSystem())));
+          NULL_ARTIFACT_OWNER);
 
   public static final ActionOwner NULL_ACTION_OWNER =
       ActionOwner.create(
@@ -284,17 +315,6 @@
           null,
           null);
 
-  static class NullArtifactOwner implements ArtifactOwner {
-    private NullArtifactOwner() {}
-
-    @Override
-    public Label getLabel() {
-      return NULL_LABEL;
-    }
-  }
-
-  @AutoCodec public static final ArtifactOwner NULL_ARTIFACT_OWNER = new NullArtifactOwner();
-
   /** An unchecked exception class for action conflicts. */
   public static class UncheckedActionConflictException extends RuntimeException {
     public UncheckedActionConflictException(ActionConflictException e) {
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
index bdc969c..99a2290 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/LocationFunctionTest.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.LocationExpander.LocationFunction;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -190,12 +191,12 @@
   private static Artifact makeArtifact(String path) {
     FileSystem fs = new InMemoryFileSystem();
     if (path.startsWith("/exec/out")) {
-      return new Artifact(
-          fs.getPath(path),
-          ArtifactRoot.asDerivedRoot(fs.getPath("/exec"), fs.getPath("/exec/out")));
+      return ActionsTestUtil.createArtifact(
+          ArtifactRoot.asDerivedRoot(fs.getPath("/exec"), fs.getPath("/exec/out")),
+          fs.getPath(path));
     } else {
-      return new Artifact(
-          fs.getPath(path), ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/exec"))));
+      return ActionsTestUtil.createArtifact(
+          ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/exec"))), fs.getPath(path));
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/RunfilesSupplierImplTest.java b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesSupplierImplTest.java
index 33e3daf..c015d97 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/RunfilesSupplierImplTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesSupplierImplTest.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -33,20 +34,13 @@
 /** Tests for RunfilesSupplierImpl */
 @RunWith(JUnit4.class)
 public class RunfilesSupplierImplTest {
-
-  private Path execRoot;
   private ArtifactRoot rootDir;
-  private ArtifactRoot middlemanRoot;
 
   @Before
   public final void setRoot() throws IOException {
     Scratch scratch = new Scratch();
-    execRoot = scratch.getFileSystem().getPath("/");
+    Path execRoot = scratch.getFileSystem().getPath("/");
     rootDir = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/fake/root/dont/matter"));
-
-    Path middlemanExecPath = scratch.dir("/still/fake/root/dont/matter");
-    middlemanRoot =
-        ArtifactRoot.middlemanRoot(middlemanExecPath, middlemanExecPath.getChild("subdir"));
   }
 
   @Test
@@ -68,7 +62,7 @@
 
   @Test
   public void testGetManifestsWhenSupplied() {
-    Artifact manifest = new Artifact(PathFragment.create("manifest"), rootDir);
+    Artifact manifest = ActionsTestUtil.createArtifact(rootDir, "manifest");
     RunfilesSupplier underTest =
         new RunfilesSupplierImpl(PathFragment.create("ignored"), Runfiles.EMPTY, manifest);
     assertThat(underTest.getManifests()).containsExactly(manifest);
@@ -81,7 +75,7 @@
   private static List<Artifact> mkArtifacts(ArtifactRoot rootDir, String... paths) {
     ImmutableList.Builder<Artifact> builder = ImmutableList.builder();
     for (String path : paths) {
-      builder.add(new Artifact(PathFragment.create(path), rootDir));
+      builder.add(ActionsTestUtil.createArtifact(rootDir, path));
     }
     return builder.build();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java
index 457e3e5..70aacf6 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
@@ -53,10 +54,9 @@
     Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
     PathFragment pathA = PathFragment.create("a");
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("x"), root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "x");
     obscuringMap.put(pathA, artifactA);
-    obscuringMap.put(PathFragment.create("a/b"), new Artifact(PathFragment.create("c/b"),
-        root));
+    obscuringMap.put(PathFragment.create("a/b"), ActionsTestUtil.createArtifact(root, "c/b"));
     assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
         .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
     checkWarning();
@@ -67,10 +67,9 @@
     Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
     PathFragment pathA = PathFragment.create("a");
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("x"), root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "x");
     obscuringMap.put(pathA, artifactA);
-    obscuringMap.put(PathFragment.create("a/b/c"), new Artifact(PathFragment.create("b/c"),
-                                                         root));
+    obscuringMap.put(PathFragment.create("a/b/c"), ActionsTestUtil.createArtifact(root, "b/c"));
     assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
         .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
     checkWarning();
@@ -81,11 +80,9 @@
     Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
     PathFragment pathA = PathFragment.create("a");
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("a"),
-                                          root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a");
     obscuringMap.put(pathA, artifactA);
-    obscuringMap.put(PathFragment.create("a/b"), new Artifact(PathFragment.create("c/b"),
-        root));
+    obscuringMap.put(PathFragment.create("a/b"), ActionsTestUtil.createArtifact(root, "c/b"));
     assertThat(Runfiles.filterListForObscuringSymlinks(null, null, obscuringMap).entrySet())
         .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
   }
@@ -95,11 +92,9 @@
     Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
     PathFragment pathA = PathFragment.create("a");
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("a"),
-                                          root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a");
     obscuringMap.put(pathA, artifactA);
-    obscuringMap.put(PathFragment.create("a/b"), new Artifact(PathFragment.create("a/b"),
-                                                         root));
+    obscuringMap.put(PathFragment.create("a/b"), ActionsTestUtil.createArtifact(root, "a/b"));
 
     assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
         .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
@@ -111,12 +106,10 @@
     Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
     PathFragment pathA = PathFragment.create("a");
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("a"),
-                                          root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a");
     obscuringMap.put(pathA, artifactA);
     PathFragment pathBC = PathFragment.create("b/c");
-    Artifact artifactBC = new Artifact(PathFragment.create("a/b"),
-                                       root);
+    Artifact artifactBC = ActionsTestUtil.createArtifact(root, "a/b");
     obscuringMap.put(pathBC, artifactBC);
     assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap)
         .entrySet()).containsExactly(Maps.immutableEntry(pathA, artifactA),
@@ -144,8 +137,8 @@
   public void testPutCatchesConflict() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
-    Artifact artifactC = new Artifact(PathFragment.create("c"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
+    Artifact artifactC = ActionsTestUtil.createArtifact(root, "c");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     Runfiles.ConflictChecker checker =
@@ -161,8 +154,8 @@
   public void testPutReportsError() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
-    Artifact artifactC = new Artifact(PathFragment.create("c"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
+    Artifact artifactC = ActionsTestUtil.createArtifact(root, "c");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     // Same as above but with ERROR not WARNING
@@ -179,7 +172,7 @@
   public void testPutCatchesConflictBetweenNullAndNotNull() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     Runfiles.ConflictChecker checker =
@@ -194,7 +187,7 @@
   public void testPutCatchesConflictBetweenNotNullAndNull() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     // Same as above but opposite order
@@ -210,8 +203,8 @@
   public void testPutIgnoresConflict() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
-    Artifact artifactC = new Artifact(PathFragment.create("c"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
+    Artifact artifactC = ActionsTestUtil.createArtifact(root, "c");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     Runfiles.ConflictChecker checker =
@@ -226,8 +219,8 @@
   public void testPutIgnoresConflictNoListener() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
-    Artifact artifactC = new Artifact(PathFragment.create("c"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
+    Artifact artifactC = ActionsTestUtil.createArtifact(root, "c");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     Runfiles.ConflictChecker checker =
@@ -242,8 +235,8 @@
   public void testPutIgnoresSameArtifact() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathA = PathFragment.create("a");
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
-    Artifact artifactB2 = new Artifact(PathFragment.create("b"), root);
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
+    Artifact artifactB2 = ActionsTestUtil.createArtifact(root, "b");
     assertThat(artifactB2).isEqualTo(artifactB);
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
@@ -275,8 +268,8 @@
     PathFragment pathA = PathFragment.create("a");
     PathFragment pathB = PathFragment.create("b");
     PathFragment pathC = PathFragment.create("c");
-    Artifact artifactA = new Artifact(PathFragment.create("a"), root);
-    Artifact artifactB = new Artifact(PathFragment.create("b"), root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a");
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b");
     Map<PathFragment, Artifact> map = new LinkedHashMap<>();
 
     Runfiles.ConflictChecker checker =
@@ -328,7 +321,7 @@
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment workspaceName = PathFragment.create("wsname");
     PathFragment pathB = PathFragment.create("external/repo/b");
-    Artifact artifactB = new Artifact(pathB, root);
+    Artifact artifactB = ActionsTestUtil.createArtifactWithRootRelativePath(root, pathB);
 
     Runfiles.ManifestBuilder builder = new Runfiles.ManifestBuilder(workspaceName, true);
 
@@ -349,7 +342,7 @@
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment workspaceName = PathFragment.create("wsname");
     PathFragment pathB = PathFragment.create("external/repo/b");
-    Artifact artifactB = new Artifact(pathB, root);
+    Artifact artifactB = ActionsTestUtil.createArtifactWithRootRelativePath(root, pathB);
 
     Runfiles.ManifestBuilder builder = new Runfiles.ManifestBuilder(workspaceName, false);
 
@@ -372,8 +365,9 @@
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
     PathFragment pathB = PathFragment.create("repo/b");
     PathFragment externalPathB = LabelConstants.EXTERNAL_PACKAGE_NAME.getRelative(pathB);
-    Artifact artifactB = new Artifact(pathB, root);
-    Artifact artifactExternalB = new Artifact(externalPathB, root);
+    Artifact artifactB = ActionsTestUtil.createArtifactWithRootRelativePath(root, pathB);
+    Artifact artifactExternalB =
+        ActionsTestUtil.createArtifactWithRootRelativePath(root, externalPathB);
 
     Runfiles.ManifestBuilder builder = new Runfiles.ManifestBuilder(
         PathFragment.EMPTY_FRAGMENT, false);
@@ -394,8 +388,8 @@
   @Test
   public void testMergeWithSymlinks() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("a/target"), root);
-    Artifact artifactB = new Artifact(PathFragment.create("b/target"), root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a/target");
+    Artifact artifactB = ActionsTestUtil.createArtifact(root, "b/target");
     PathFragment sympathA = PathFragment.create("a/symlink");
     PathFragment sympathB = PathFragment.create("b/symlink");
     Runfiles runfilesA = new Runfiles.Builder("TESTING")
@@ -413,7 +407,7 @@
   @Test
   public void testMergeEmptyWithNonEmpty() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifactA = new Artifact(PathFragment.create("a/target"), root);
+    Artifact artifactA = ActionsTestUtil.createArtifact(root, "a/target");
     Runfiles runfilesB = new Runfiles.Builder("TESTING").addArtifact(artifactA).build();
     assertThat(Runfiles.EMPTY.merge(runfilesB)).isSameInstanceAs(runfilesB);
     assertThat(runfilesB.merge(Runfiles.EMPTY)).isSameInstanceAs(runfilesB);
@@ -423,7 +417,7 @@
   public void testOnlyExtraMiddlemenNotConsideredEmpty() {
     ArtifactRoot root =
         ArtifactRoot.middlemanRoot(scratch.resolve("execroot"), scratch.resolve("execroot/out"));
-    Artifact mm = new Artifact(PathFragment.create("a-middleman"), root);
+    Artifact mm = ActionsTestUtil.createArtifact(root, "a-middleman");
     Runfiles runfiles = new Runfiles.Builder("TESTING").addLegacyExtraMiddleman(mm).build();
     assertThat(runfiles.isEmpty()).isFalse();
   }
@@ -432,8 +426,8 @@
   public void testMergingExtraMiddlemen() {
     ArtifactRoot root =
         ArtifactRoot.middlemanRoot(scratch.resolve("execroot"), scratch.resolve("execroot/out"));
-    Artifact mm1 = new Artifact(PathFragment.create("middleman-1"), root);
-    Artifact mm2 = new Artifact(PathFragment.create("middleman-2"), root);
+    Artifact mm1 = ActionsTestUtil.createArtifact(root, "middleman-1");
+    Artifact mm2 = ActionsTestUtil.createArtifact(root, "middleman-2");
     Runfiles runfiles1 = new Runfiles.Builder("TESTING").addLegacyExtraMiddleman(mm1).build();
     Runfiles runfiles2 = new Runfiles.Builder("TESTING").addLegacyExtraMiddleman(mm2).build();
     Runfiles runfilesMerged =
@@ -445,7 +439,7 @@
   @Test
   public void testGetEmptyFilenames() {
     ArtifactRoot root = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.resolve("/workspace")));
-    Artifact artifact = new Artifact(PathFragment.create("my-artifact"), root);
+    Artifact artifact = ActionsTestUtil.createArtifact(root, "my-artifact");
     Runfiles runfiles =
         new Runfiles.Builder("TESTING")
             .addArtifact(artifact)
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelperTest.java b/src/test/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelperTest.java
index 141982d..68cd008 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelperTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelperTest.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper.ArtifactsInOutputGroup;
 import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper.ArtifactsToBuild;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -124,6 +125,6 @@
   }
 
   private Artifact newArtifact() {
-    return new Artifact(path.getRelative(Integer.toString(artifactIdx++)), root);
+    return ActionsTestUtil.createArtifact(root, path.getRelative(Integer.toString(artifactIdx++)));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
index 62bdf44..2bdaed0 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
@@ -381,7 +381,7 @@
   }
 
   private Artifact createDerivedArtifact(String rootRelativePath) {
-    return new Artifact(PathFragment.create(rootRelativePath), root);
+    return ActionsTestUtil.createArtifact(root, rootRelativePath);
   }
 
   private CustomCommandLine createSimpleCommandLineTemplate(
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java
index e6a6ac5..58bd02c 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ServerDirectories;
 import com.google.devtools.build.lib.events.StoredEventHandler;
@@ -86,9 +87,9 @@
     outputRoot =
         ArtifactRoot.asDerivedRoot(scratch.dir("/workspace"), scratch.dir("/workspace/out"));
     Path input = scratch.overwriteFile("/workspace/input.txt", StandardCharsets.UTF_8, template);
-    inputArtifact = new Artifact(input, workspace);
+    inputArtifact = ActionsTestUtil.createArtifact(workspace, input);
     output = scratch.resolve("/workspace/out/destination.txt");
-    outputArtifact = new Artifact(output, outputRoot);
+    outputArtifact = ActionsTestUtil.createArtifact(outputRoot, output);
   }
 
   private TemplateExpansionAction create() {
@@ -118,8 +119,9 @@
 
   @Test
   public void testKeySameIfSame() throws Exception {
-    Artifact outputArtifact2 = new Artifact(scratch.resolve("/workspace/out/destination.txt"),
-        outputRoot);
+    Artifact outputArtifact2 =
+        ActionsTestUtil.createArtifact(
+            outputRoot, scratch.resolve("/workspace/out/destination.txt"));
     TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER,
          outputArtifact, Template.forString(TEMPLATE),
          ImmutableList.of(Substitution.of("%key%", "foo")), false);
@@ -132,8 +134,9 @@
 
   @Test
   public void testKeyDiffersForSubstitution() throws Exception {
-    Artifact outputArtifact2 = new Artifact(scratch.resolve("/workspace/out/destination.txt"),
-        outputRoot);
+    Artifact outputArtifact2 =
+        ActionsTestUtil.createArtifact(
+            outputRoot, scratch.resolve("/workspace/out/destination.txt"));
     TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER,
          outputArtifact, Template.forString(TEMPLATE),
          ImmutableList.of(Substitution.of("%key%", "foo")), false);
@@ -146,8 +149,9 @@
 
   @Test
   public void testKeyDiffersForExecutable() throws Exception {
-    Artifact outputArtifact2 = new Artifact(scratch.resolve("/workspace/out/destination.txt"),
-        outputRoot);
+    Artifact outputArtifact2 =
+        ActionsTestUtil.createArtifact(
+            outputRoot, scratch.resolve("/workspace/out/destination.txt"));
     TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER,
          outputArtifact, Template.forString(TEMPLATE),
          ImmutableList.of(Substitution.of("%key%", "foo")), false);
@@ -160,8 +164,9 @@
 
   @Test
   public void testKeyDiffersForTemplates() throws Exception {
-    Artifact outputArtifact2 = new Artifact(scratch.resolve("/workspace/out/destination.txt"),
-        outputRoot);
+    Artifact outputArtifact2 =
+        ActionsTestUtil.createArtifact(
+            outputRoot, scratch.resolve("/workspace/out/destination.txt"));
     TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER,
          outputArtifact, Template.forString(TEMPLATE),
          ImmutableList.of(Substitution.of("%key%", "foo")), false);
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
index 0cc5116..51c03bd 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
@@ -47,7 +47,6 @@
 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.Root;
 import com.google.devtools.build.lib.vfs.util.FileSystems;
 import java.io.IOException;
@@ -237,11 +236,11 @@
     testRoot.deleteTreesBelow();
     executorService = Executors.newCachedThreadPool();
     inputArtifact =
-        new Artifact(
-            PathFragment.create("input.txt"), ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "input.txt");
     outputArtifact =
-        new Artifact(
-            PathFragment.create("output.txt"), ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "output.txt");
     outErr = new FileOutErr(testRoot.getRelative("stdout"), testRoot.getRelative("stderr"));
     actionExecutionContext =
         ActionsTestUtil.createContext(
diff --git a/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
index e6e026c..10a5ca7 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.SpawnResult.Status;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.exec.Protos.Digest;
 import com.google.devtools.build.lib.exec.Protos.EnvironmentVariable;
 import com.google.devtools.build.lib.exec.Protos.File;
@@ -215,7 +216,7 @@
                     .build()));
     when(actionExecutionContext.getMetadataProvider()).thenReturn(mock(MetadataProvider.class));
 
-    Artifact input = new Artifact(scratch.file("/execroot/foo", "1"), rootDir);
+    Artifact input = ActionsTestUtil.createArtifact(rootDir, scratch.file("/execroot/foo", "1"));
     scratch.file("/execroot/out1", "123");
     scratch.file("/execroot/out2", "123");
     Spawn spawn =
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
index a61ed0a..fb5e5ad 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java
@@ -37,6 +37,7 @@
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
 import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.Runfiles;
 import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
 import com.google.devtools.build.lib.exec.util.FakeActionInputFileCache;
@@ -84,9 +85,9 @@
   @Test
   public void testRunfilesSingleFile() throws Exception {
     Artifact artifact =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
     RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
     FakeActionInputFileCache mockCache = new FakeActionInputFileCache();
@@ -105,9 +106,9 @@
   @Test
   public void testRunfilesDirectoryStrict() {
     Artifact artifact =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
     RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
     FakeActionInputFileCache mockCache = new FakeActionInputFileCache();
@@ -130,9 +131,9 @@
   @Test
   public void testRunfilesDirectoryNonStrict() throws Exception {
     Artifact artifact =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
     RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
     FakeActionInputFileCache mockCache = new FakeActionInputFileCache();
@@ -149,13 +150,13 @@
   @Test
   public void testRunfilesTwoFiles() throws Exception {
     Artifact artifact1 =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Artifact artifact2 =
-        new Artifact(
-            fs.getPath("/root/dir/baz"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/baz"));
     Runfiles runfiles =
         new Runfiles.Builder("workspace").addArtifact(artifact1).addArtifact(artifact2).build();
     RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
@@ -181,9 +182,9 @@
   @Test
   public void testRunfilesSymlink() throws Exception {
     Artifact artifact =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Runfiles runfiles =
         new Runfiles.Builder("workspace")
             .addSymlink(PathFragment.create("symlink"), artifact)
@@ -205,9 +206,9 @@
   @Test
   public void testRunfilesRootSymlink() throws Exception {
     Artifact artifact =
-        new Artifact(
-            fs.getPath("/root/dir/file"),
-            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(fs.getPath("/root"))),
+            fs.getPath("/root/dir/file"));
     Runfiles runfiles =
         new Runfiles.Builder("workspace")
             .addRootSymlink(PathFragment.create("symlink"), artifact)
@@ -316,15 +317,15 @@
     inputMappings = expander.getInputMapping(spawn, artifactExpander, ArtifactPathResolver.IDENTITY,
         fakeCache, true);
     assertThat(inputMappings).hasSize(2);
-    assertThat(inputMappings).containsEntry(PathFragment.create("treeArtifact/file1"), file1);
-    assertThat(inputMappings).containsEntry(PathFragment.create("treeArtifact/file2"), file2);
+    assertThat(inputMappings).containsEntry(PathFragment.create("out/treeArtifact/file1"), file1);
+    assertThat(inputMappings).containsEntry(PathFragment.create("out/treeArtifact/file2"), file2);
   }
 
   private SpecialArtifact createTreeArtifact(String relPath) throws IOException {
-    Path outputDir = fs.getPath("/root");
-    Path outputPath = execRoot.getRelative(relPath);
+    Path outputDir = execRoot.getRelative("out");
+    Path outputPath = outputDir.getRelative(relPath);
     outputPath.createDirectoryAndParents();
-    ArtifactRoot derivedRoot = ArtifactRoot.asSourceRoot(Root.fromPath(outputDir));
+    ArtifactRoot derivedRoot = ArtifactRoot.asDerivedRoot(execRoot, outputDir);
     return new SpecialArtifact(
         derivedRoot,
         derivedRoot.getExecPath().getRelative(derivedRoot.getRoot().relativize(outputPath)),
diff --git a/src/test/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCacheTests.java b/src/test/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCacheTests.java
index 341e1cb..5716372 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCacheTests.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCacheTests.java
@@ -51,6 +51,7 @@
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.cache.MetadataInjector;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.remote.AbstractRemoteActionCache.UploadManifest;
 import com.google.devtools.build.lib.remote.options.RemoteOptions;
@@ -754,8 +755,8 @@
             .addOutputFiles(OutputFile.newBuilder().setPath("outputs/file2").setDigest(d2))
             .build();
 
-    Artifact a1 = new Artifact(PathFragment.create("file1"), artifactRoot);
-    Artifact a2 = new Artifact(PathFragment.create("file2"), artifactRoot);
+    Artifact a1 = ActionsTestUtil.createArtifact(artifactRoot, "file1");
+    Artifact a2 = ActionsTestUtil.createArtifact(artifactRoot, "file2");
 
     MetadataInjector injector = mock(MetadataInjector.class);
 
@@ -945,8 +946,8 @@
             .addOutputFiles(OutputFile.newBuilder().setPath("outputs/file1").setDigest(d1))
             .addOutputFiles(OutputFile.newBuilder().setPath("outputs/file2").setDigest(d2))
             .build();
-    Artifact a1 = new Artifact(PathFragment.create("file1"), artifactRoot);
-    Artifact a2 = new Artifact(PathFragment.create("file2"), artifactRoot);
+    Artifact a1 = ActionsTestUtil.createArtifact(artifactRoot, "file1");
+    Artifact a2 = ActionsTestUtil.createArtifact(artifactRoot, "file2");
     MetadataInjector injector = mock(MetadataInjector.class);
     // a1 should be provided as an InMemoryOutput
     PathFragment inMemoryOutputPathFragment = a1.getPath().relativeTo(execRoot);
diff --git a/src/test/java/com/google/devtools/build/lib/remote/BUILD b/src/test/java/com/google/devtools/build/lib/remote/BUILD
index aa18d2c..d35bd3e 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/remote/BUILD
@@ -41,6 +41,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/protobuf:remote_execution_log_java_proto",
+        "//src/test/java/com/google/devtools/build/lib:actions_testutil",
         "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
         "//src/test/java/com/google/devtools/build/lib:test_runner",
         "//src/test/java/com/google/devtools/build/lib:testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionFileSystemTest.java
index 40b6f81..1953e17 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionFileSystemTest.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 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.clock.JavaClock;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.FileSystem;
@@ -137,7 +138,7 @@
   private Artifact createRemoteArtifact(
       String pathFragment, String contents, ActionInputMap inputs) {
     Path p = outputRoot.getRoot().asPath().getRelative(pathFragment);
-    Artifact a = new Artifact(p, outputRoot);
+    Artifact a = ActionsTestUtil.createArtifact(outputRoot, p);
     byte[] b = contents.getBytes(StandardCharsets.UTF_8);
     HashCode h = HASH_FUNCTION.getHashFunction().hashBytes(b);
     FileArtifactValue f =
@@ -151,7 +152,7 @@
       throws IOException {
     Path p = outputRoot.getRoot().asPath().getRelative(pathFragment);
     FileSystemUtils.writeContent(p, StandardCharsets.UTF_8, contents);
-    Artifact a = new Artifact(p, outputRoot);
+    Artifact a = ActionsTestUtil.createArtifact(outputRoot, p);
     inputs.putWithNoDepOwner(a, FileArtifactValue.create(a.getPath(), /* isShareable= */ true));
     return a;
   }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
index b564754..b822a4c 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
@@ -32,6 +32,7 @@
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.MetadataProvider;
 import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.remote.options.RemoteOptions;
 import com.google.devtools.build.lib.remote.util.DigestUtil;
@@ -161,7 +162,7 @@
     // arrange
     Path p = execRoot.getRelative(artifactRoot.getExecPath()).getRelative("file1");
     FileSystemUtils.writeContent(p, StandardCharsets.UTF_8, "hello world");
-    Artifact a = new Artifact(p, artifactRoot);
+    Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p);
     FileArtifactValue f = FileArtifactValue.create(a);
     MetadataProvider metadataProvider = new StaticMetadataProvider(ImmutableMap.of(a, f));
     AbstractRemoteActionCache remoteCache =
@@ -204,7 +205,7 @@
       Map<ActionInput, FileArtifactValue> metadata,
       Map<Digest, ByteString> cacheEntries) {
     Path p = artifactRoot.getRoot().getRelative(pathFragment);
-    Artifact a = new Artifact(p, artifactRoot);
+    Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p);
     byte[] b = contents.getBytes(StandardCharsets.UTF_8);
     HashCode h = HASH_FUNCTION.getHashFunction().hashBytes(b);
     FileArtifactValue f =
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
index 17f6be6..e3699d1 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
@@ -60,6 +60,7 @@
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.SpawnResult.Status;
 import com.google.devtools.build.lib.actions.cache.MetadataInjector;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventKind;
@@ -910,7 +911,8 @@
     options.remoteOutputsMode = RemoteOutputsMode.TOPLEVEL;
 
     ArtifactRoot outputRoot = ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("outs"));
-    Artifact topLevelOutput = new Artifact(outputRoot.getRoot().getRelative("foo.bin"), outputRoot);
+    Artifact topLevelOutput =
+        ActionsTestUtil.createArtifact(outputRoot, outputRoot.getRoot().getRelative("foo.bin"));
 
     ActionResult succeededAction = ActionResult.newBuilder().setExitCode(0).build();
     when(cache.getCachedActionResult(any(ActionKey.class))).thenReturn(succeededAction);
diff --git a/src/test/java/com/google/devtools/build/lib/remote/merkletree/BUILD b/src/test/java/com/google/devtools/build/lib/remote/merkletree/BUILD
index 150cda4..cf6592d 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/merkletree/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/remote/merkletree/BUILD
@@ -23,6 +23,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
+        "//src/test/java/com/google/devtools/build/lib:actions_testutil",
         "//src/test/java/com/google/devtools/build/lib:test_runner",
         "//src/test/java/com/google/devtools/build/lib:testutil",
         "//src/test/java/com/google/devtools/build/lib/remote/util",
diff --git a/src/test/java/com/google/devtools/build/lib/remote/merkletree/InputTreeTest.java b/src/test/java/com/google/devtools/build/lib/remote/merkletree/InputTreeTest.java
index 5f7663a..a8a143a 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/merkletree/InputTreeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/merkletree/InputTreeTest.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.remote.merkletree.InputTree.DirectoryNode;
 import com.google.devtools.build.lib.remote.merkletree.InputTree.FileNode;
@@ -147,7 +148,7 @@
     ActionInput buzz = ActionInputHelper.fromPath(buzzPath.relativeTo(execRoot));
     metadata.put(buzz, FileArtifactValue.createShareable(buzzPath));
 
-    Artifact dir = new Artifact(dirPath, artifactRoot);
+    Artifact dir = ActionsTestUtil.createArtifact(artifactRoot, dirPath);
     sortedInputs.put(dirPath.relativeTo(execRoot), dir);
     metadata.put(dir, FileArtifactValue.createShareable(dirPath));
 
@@ -202,7 +203,7 @@
     Path p = execRoot.getRelative(path);
     p.getParentDirectory().createDirectoryAndParents();
     FileSystemUtils.writeContentAsLatin1(p, content);
-    Artifact a = new Artifact(p, artifactRoot);
+    Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p);
 
     sortedInputs.put(PathFragment.create(path), a);
     metadata.put(a, FileArtifactValue.create(a));
diff --git a/src/test/java/com/google/devtools/build/lib/remote/merkletree/MerkleTreeTest.java b/src/test/java/com/google/devtools/build/lib/remote/merkletree/MerkleTreeTest.java
index b00ea4f..6f790dc 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/merkletree/MerkleTreeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/merkletree/MerkleTreeTest.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.remote.util.DigestUtil;
 import com.google.devtools.build.lib.remote.util.StaticMetadataProvider;
@@ -145,7 +146,7 @@
     Path p = execRoot.getRelative(path);
     Preconditions.checkNotNull(p.getParentDirectory()).createDirectoryAndParents();
     FileSystemUtils.writeContentAsLatin1(p, content);
-    Artifact a = new Artifact(p, artifactRoot);
+    Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p);
 
     sortedInputs.put(PathFragment.create(path), a);
     metadata.put(a, FileArtifactValue.create(a));
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
index 6443170..2452c80 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
@@ -213,7 +213,7 @@
 
   private Artifact getArtifact(String subdir, String pathString) {
     Path path = fileSystem.getPath("/" + subdir + "/" + pathString);
-    return new Artifact(
+    return new Artifact.SourceArtifact(
         root, root.getExecPath().getRelative(root.getRoot().relativize(path)), OWNER);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeaturesTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeaturesTest.java
index 8297391..2e810ea 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeaturesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeaturesTest.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Multimap;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.ActionConfig;
 import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.ExpansionException;
@@ -136,7 +137,8 @@
     Path outputRoot = execRoot.getRelative("out");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, outputRoot);
     try {
-      return new Artifact(scratch.overwriteFile(outputRoot.getRelative(s).toString()), root);
+      return ActionsTestUtil.createArtifact(
+          root, scratch.overwriteFile(outputRoot.getRelative(s).toString()));
     } catch (IOException e) {
       throw new RuntimeException(e);
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CompileCommandLineTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CompileCommandLineTest.java
index dba5802..7f6c4b7 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CompileCommandLineTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CompileCommandLineTest.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.rules.cpp.CcCommon.CoptsFilter;
@@ -53,7 +54,8 @@
     Path outputRoot = execRoot.getRelative("root");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, outputRoot);
     try {
-      return new Artifact(scratch.overwriteFile(outputRoot.getRelative(s).toString()), root);
+      return ActionsTestUtil.createArtifact(
+          root, scratch.overwriteFile(outputRoot.getRelative(s).toString()));
     } catch (IOException e) {
       throw new RuntimeException(e);
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
index f36d4b8..80f2e94 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
@@ -702,10 +702,9 @@
                 ruleContext,
                 ruleContext,
                 ruleContext.getLabel(),
-                new Artifact(
-                    PathFragment.create(outputPath),
-                    getTargetConfiguration()
-                        .getBinDirectory(ruleContext.getRule().getRepository())),
+                ActionsTestUtil.createArtifact(
+                    getTargetConfiguration().getBinDirectory(ruleContext.getRule().getRepository()),
+                    outputPath),
                 ruleContext.getConfiguration(),
                 toolchain,
                 toolchain.getFdoContext(),
@@ -732,7 +731,7 @@
   }
 
   public Artifact getOutputArtifact(String relpath) {
-    return new Artifact(
+    return ActionsTestUtil.createArtifactWithExecPath(
         getTargetConfiguration().getBinDirectory(RepositoryName.MAIN),
         getTargetConfiguration().getBinFragment().getRelative(relpath));
   }
@@ -742,7 +741,8 @@
     Path outputRoot = execRoot.getRelative("out");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, outputRoot);
     try {
-      return new Artifact(scratch.overwriteFile(outputRoot.getRelative(s).toString()), root);
+      return ActionsTestUtil.createArtifact(
+          root, scratch.overwriteFile(outputRoot.getRelative(s).toString()));
     } catch (IOException e) {
       throw new RuntimeException(e);
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkActionTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkActionTest.java
index bbeb21e..5cde05f 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkActionTest.java
@@ -22,13 +22,13 @@
 import com.google.devtools.build.lib.actions.ActionKeyContext;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.DummyExecutor;
 import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
 import com.google.devtools.build.lib.util.Fingerprint;
 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.Symlinks;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,17 +46,17 @@
   public void testDifferentOrderSameActionKey() throws Exception {
     Path includePath = rootDirectory.getRelative("out");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(rootDirectory, includePath);
-    Artifact a = new Artifact(PathFragment.create("a"), root);
-    Artifact b = new Artifact(PathFragment.create("b"), root);
-    Artifact c = new Artifact(PathFragment.create("c"), root);
-    Artifact d = new Artifact(PathFragment.create("d"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, "a");
+    Artifact b = ActionsTestUtil.createArtifact(root, "b");
+    Artifact c = ActionsTestUtil.createArtifact(root, "c");
+    Artifact d = ActionsTestUtil.createArtifact(root, "d");
     CreateIncSymlinkAction action1 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b, c, d), includePath);
     // Can't reuse the artifacts here; that would lead to DuplicateArtifactException.
-    a = new Artifact(PathFragment.create("a"), root);
-    b = new Artifact(PathFragment.create("b"), root);
-    c = new Artifact(PathFragment.create("c"), root);
-    d = new Artifact(PathFragment.create("d"), root);
+    a = ActionsTestUtil.createArtifact(root, "a");
+    b = ActionsTestUtil.createArtifact(root, "b");
+    c = ActionsTestUtil.createArtifact(root, "c");
+    d = ActionsTestUtil.createArtifact(root, "d");
     CreateIncSymlinkAction action2 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(c, d, a, b), includePath);
 
@@ -67,13 +67,13 @@
   public void testDifferentTargetsDifferentActionKey() throws Exception {
     Path includePath = rootDirectory.getRelative("out");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(rootDirectory, includePath);
-    Artifact a = new Artifact(PathFragment.create("a"), root);
-    Artifact b = new Artifact(PathFragment.create("b"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, "a");
+    Artifact b = ActionsTestUtil.createArtifact(root, "b");
     CreateIncSymlinkAction action1 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b), includePath);
     // Can't reuse the artifacts here; that would lead to DuplicateArtifactException.
-    a = new Artifact(PathFragment.create("a"), root);
-    b = new Artifact(PathFragment.create("c"), root);
+    a = ActionsTestUtil.createArtifact(root, "a");
+    b = ActionsTestUtil.createArtifact(root, "c");
     CreateIncSymlinkAction action2 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b), includePath);
 
@@ -84,13 +84,13 @@
   public void testDifferentSymlinksDifferentActionKey() throws Exception {
     Path includePath = rootDirectory.getRelative("out");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(rootDirectory, includePath);
-    Artifact a = new Artifact(PathFragment.create("a"), root);
-    Artifact b = new Artifact(PathFragment.create("b"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, "a");
+    Artifact b = ActionsTestUtil.createArtifact(root, "b");
     CreateIncSymlinkAction action1 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b), includePath);
     // Can't reuse the artifacts here; that would lead to DuplicateArtifactException.
-    a = new Artifact(PathFragment.create("c"), root);
-    b = new Artifact(PathFragment.create("b"), root);
+    a = ActionsTestUtil.createArtifact(root, "c");
+    b = ActionsTestUtil.createArtifact(root, "b");
     CreateIncSymlinkAction action2 =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b), includePath);
 
@@ -103,8 +103,8 @@
     outputDir.createDirectory();
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(rootDirectory, outputDir);
     Path symlink = rootDirectory.getRelative("out/a");
-    Artifact a = new Artifact(symlink, root);
-    Artifact b = new Artifact(PathFragment.create("b"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, symlink);
+    Artifact b = ActionsTestUtil.createArtifact(root, "b");
     CreateIncSymlinkAction action = new CreateIncSymlinkAction(NULL_ACTION_OWNER,
         ImmutableMap.of(a, b), outputDir);
     action.execute(makeDummyContext());
@@ -137,8 +137,8 @@
     outputDir.createDirectory();
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(rootDirectory, outputDir);
     Path symlink = rootDirectory.getRelative("out/subdir/a");
-    Artifact a = new Artifact(symlink, root);
-    Artifact b = new Artifact(PathFragment.create("b"), root);
+    Artifact a = ActionsTestUtil.createArtifact(root, symlink);
+    Artifact b = ActionsTestUtil.createArtifact(root, "b");
     CreateIncSymlinkAction action =
         new CreateIncSymlinkAction(NULL_ACTION_OWNER, ImmutableMap.of(a, b), outputDir);
     Path extra = rootDirectory.getRelative("out/extra");
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/HeaderDiscoveryTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/HeaderDiscoveryTest.java
index 594b294..9c621f7 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/HeaderDiscoveryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/HeaderDiscoveryTest.java
@@ -66,7 +66,8 @@
         .shouldValidateInclusions()
         .setAction(new ActionsTestUtil.NullAction())
         .setPermittedSystemIncludePrefixes(ImmutableList.of())
-        .setSourceFile(new Artifact(derivedRoot.getRelative("foo.cc"), artifactRoot))
+        .setSourceFile(
+            ActionsTestUtil.createArtifact(artifactRoot, derivedRoot.getRelative("foo.cc")))
         .setDependencies(dependencies)
         .setAllowedDerivedinputs(includedHeaders)
         .build()
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
index a368a36..50614c4 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
@@ -25,7 +25,9 @@
 import com.google.devtools.build.lib.actions.Artifact;
 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.ArtifactOwner;
 import com.google.devtools.build.lib.actions.ArtifactPathResolver;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
@@ -120,7 +122,7 @@
     List<Artifact> expectedHeaders =
         ImmutableList.of(getSourceArtifact("objc/a.h"), getTreeArtifact("tree/dir"));
     HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders));
-    writeToHeadersListFile(action, "objc/a.h", "tree/dir/c.h");
+    writeToHeadersListFile(action, "objc/a.h", "out/tree/dir/c.h");
 
     Iterable<Artifact> headersFound = determineAdditionalInputs(headerThinning, action);
     assertThat(headersFound).containsExactlyElementsIn(expectedHeaders);
@@ -206,7 +208,12 @@
   }
 
   private Artifact getTreeArtifact(String name) {
-    Artifact treeArtifactBase = getSourceArtifact(name);
+    Artifact treeArtifactBase =
+        getDerivedArtifact(
+            PathFragment.create(name),
+            ArtifactRoot.asDerivedRoot(
+                directories.getExecRoot(), directories.getExecRoot().getChild("out")),
+            ArtifactOwner.NullArtifactOwner.INSTANCE);
     return new SpecialArtifact(
         treeArtifactBase.getRoot(),
         treeArtifactBase.getExecPath(),
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/J2ObjcSourceTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/J2ObjcSourceTest.java
index 1304d64..ff38b55 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/J2ObjcSourceTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/J2ObjcSourceTest.java
@@ -19,6 +19,7 @@
 import com.google.common.testing.EqualsTester;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.Path;
@@ -74,6 +75,6 @@
   }
 
   private Artifact getArtifactForTest(String path) throws Exception {
-    return new Artifact(PathFragment.create(path), rootDir);
+    return ActionsTestUtil.createArtifact(rootDir, path);
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcProviderTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcProviderTest.java
index f5ae192..b4e96a2 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcProviderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcProviderTest.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.rules.objc.ObjcProvider.Key;
@@ -55,9 +56,8 @@
   }
 
   private static Artifact createArtifact(String path) {
-    return new Artifact(
-        PathFragment.create(path),
-        ArtifactRoot.asSourceRoot(Root.absoluteRoot(new InMemoryFileSystem())));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.absoluteRoot(new InMemoryFileSystem())), path);
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
index 331d969..aefb20d 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
@@ -429,7 +429,7 @@
   }
 
   private Artifact artifact(String ownerLabel, String path) {
-    return new Artifact(
+    return new Artifact.SourceArtifact(
         root,
         root.getExecPath().getRelative(path),
         new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel)));
@@ -437,7 +437,7 @@
 
   /** Creates a dummy artifact with the given path, that actually resides in /out/<path>. */
   private Artifact derivedArtifact(String ownerLabel, String path) {
-    return new Artifact(
+    return new Artifact.DerivedArtifact(
         derivedRoot,
         derivedRoot.getExecPath().getRelative(path),
         new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel)));
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
index caabb08..e18a2ba 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
@@ -207,6 +207,7 @@
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/test/java/com/google/devtools/build/lib:actions_testutil",
         "//src/test/java/com/google/devtools/build/lib:testutil",
         "//third_party:guava",
         "//third_party:junit4",
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PyStructUtilsTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PyStructUtilsTest.java
index 21f8cb8..8b141e8 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/PyStructUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PyStructUtilsTest.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
@@ -29,7 +30,6 @@
 import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
 import com.google.devtools.build.lib.testutil.MoreAsserts.ThrowingRunnable;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -47,8 +47,8 @@
   @Before
   public void setUp() {
     this.dummyArtifact =
-        new Artifact(
-            PathFragment.create("dummy"), ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)));
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)), "dummy");
   }
 
   /**
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
index 6da3ca7..be5cc35 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
@@ -672,7 +672,8 @@
 
   private Artifact makeArtifact(String pathString) {
     Path path = outputBase.getRelative(PathFragment.create(pathString));
-    return new Artifact(path, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path);
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
index 2e26c06..dac992e 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
@@ -34,6 +34,7 @@
 import com.google.devtools.build.lib.actions.RunningActionEvent;
 import com.google.devtools.build.lib.actions.ScanningActionEvent;
 import com.google.devtools.build.lib.actions.SchedulingActionEvent;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadProgressEvent;
 import com.google.devtools.build.lib.buildeventstream.AnnounceBuildEventTransportsEvent;
@@ -149,7 +150,8 @@
 
   private Action mockAction(String progressMessage, String primaryOutput) {
     Path path = outputBase.getRelative(PathFragment.create(primaryOutput));
-    Artifact artifact = new Artifact(path, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    Artifact artifact =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path);
 
     Action action = Mockito.mock(Action.class);
     when(action.getProgressMessage()).thenReturn(progressMessage);
@@ -555,7 +557,8 @@
 
     ManualClock clock = new ManualClock();
     Path path = outputBase.getRelative(PathFragment.create(primaryOutput));
-    Artifact artifact = new Artifact(path, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    Artifact artifact =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path);
     Action action = mockAction("Some random action", primaryOutput);
     when(action.getOwner()).thenReturn(Mockito.mock(ActionOwner.class));
     when(action.getPrimaryOutput()).thenReturn(artifact);
@@ -581,7 +584,8 @@
 
     ManualClock clock = new ManualClock();
     Path path = outputBase.getRelative(PathFragment.create(primaryOutput));
-    Artifact artifact = new Artifact(path, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    Artifact artifact =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path);
     Action action = mockAction("Some random action", primaryOutput);
     when(action.getOwner()).thenReturn(Mockito.mock(ActionOwner.class));
     when(action.getPrimaryOutput()).thenReturn(artifact);
@@ -616,14 +620,16 @@
     LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true);
 
     Path path1 = outputBase.getRelative(PathFragment.create(primaryOutput1));
-    Artifact artifact1 = new Artifact(path1, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    Artifact artifact1 =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path1);
     Action action1 = mockAction("First random action", primaryOutput1);
     when(action1.getOwner()).thenReturn(Mockito.mock(ActionOwner.class));
     when(action1.getPrimaryOutput()).thenReturn(artifact1);
     stateTracker.actionStarted(new ActionStartedEvent(action1, clock.nanoTime()));
 
     Path path2 = outputBase.getRelative(PathFragment.create(primaryOutput2));
-    Artifact artifact2 = new Artifact(path2, ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)));
+    Artifact artifact2 =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path2);
     Action action2 = mockAction("First random action", primaryOutput1);
     when(action2.getOwner()).thenReturn(Mockito.mock(ActionOwner.class));
     when(action2.getPrimaryOutput()).thenReturn(artifact2);
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
index bdf3b18..5fc2f17 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 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.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
@@ -76,7 +77,7 @@
   @Test
   public void withArtifactInput() throws Exception {
     PathFragment path = PathFragment.create("src/a");
-    Artifact artifact = new Artifact(path, sourceRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(sourceRoot, path);
     FileArtifactValue metadata =
         FileArtifactValue.createNormalFile(
             new byte[] {1, 2, 3}, /*proxy=*/ null, 10L, /*isShareable=*/ true);
@@ -95,7 +96,7 @@
   @Test
   public void withUnknownSourceArtifactAndNoMissingArtifactsAllowed() throws Exception {
     PathFragment path = PathFragment.create("src/a");
-    Artifact artifact = new Artifact(path, sourceRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(sourceRoot, path);
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
         map,
@@ -112,7 +113,7 @@
   @Test
   public void withUnknownSourceArtifact() throws Exception {
     PathFragment path = PathFragment.create("src/a");
-    Artifact artifact = new Artifact(path, sourceRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(sourceRoot, path);
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
         map,
@@ -127,7 +128,7 @@
   @Test
   public void withUnknownOutputArtifactMissingAllowed() throws Exception {
     PathFragment path = PathFragment.create("foo/bar");
-    Artifact artifact = new Artifact(path, outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(outputRoot, path);
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
         map,
@@ -142,7 +143,7 @@
   @Test
   public void withUnknownOutputArtifactStatsFile() throws Exception {
     scratch.file("/output/bin/foo/bar", "not empty");
-    Artifact artifact = new Artifact(PathFragment.create("foo/bar"), outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifact(outputRoot, "foo/bar");
     assertThat(artifact.getPath().exists()).isTrue();
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
@@ -157,7 +158,7 @@
 
   @Test
   public void withUnknownOutputArtifactStatsFileFailsWithException() throws Exception {
-    Artifact artifact = new Artifact(PathFragment.create("foo/bar"), outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifact(outputRoot, "foo/bar");
     assertThat(artifact.getPath().exists()).isFalse();
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
@@ -173,7 +174,7 @@
   @Test
   public void withUnknownOutputArtifactMissingDisallowed() throws Exception {
     PathFragment path = PathFragment.create("foo/bar");
-    Artifact artifact = new Artifact(path, outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(outputRoot, path);
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler = new ActionMetadataHandler(
         map,
@@ -245,7 +246,7 @@
   public void resettingOutputs() throws Exception {
     scratch.file("/output/bin/foo/bar", "not empty");
     PathFragment path = PathFragment.create("foo/bar");
-    Artifact artifact = new Artifact(path, outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(outputRoot, path);
     ActionInputMap map = new ActionInputMap(1);
     ActionMetadataHandler handler =
         new ActionMetadataHandler(
@@ -272,7 +273,7 @@
   @Test
   public void injectRemoteArtifactMetadata() throws Exception {
     PathFragment path = PathFragment.create("foo/bar");
-    Artifact artifact = new Artifact(path, outputRoot);
+    Artifact artifact = ActionsTestUtil.createArtifactWithRootRelativePath(outputRoot, path);
     ActionMetadataHandler handler =
         new ActionMetadataHandler(
             /* inputArtifactData= */ new ActionInputMap(0),
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
index 7b25f59..f6f8a8b 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
@@ -322,19 +322,15 @@
   }
 
   private Artifact createSourceArtifact(String path) {
-    return new Artifact.SourceArtifact(
-        ArtifactRoot.asSourceRoot(Root.fromPath(root)),
-        PathFragment.create(path),
-        ArtifactOwner.NullArtifactOwner.INSTANCE);
+    return ActionsTestUtil.createArtifactWithExecPath(
+        ArtifactRoot.asSourceRoot(Root.fromPath(root)), PathFragment.create(path));
   }
 
   private Artifact createDerivedArtifact(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
     Artifact output =
-        new Artifact(
-            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
-            execPath,
-            ALL_OWNER);
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER);
     actions.add(new DummyAction(ImmutableList.<Artifact>of(), output));
     return output;
   }
@@ -342,7 +338,8 @@
   private Artifact createMiddlemanArtifact(String path) {
     ArtifactRoot middlemanRoot =
         ArtifactRoot.middlemanRoot(middlemanPath, middlemanPath.getRelative("out"));
-    return new Artifact(middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER);
+    return new Artifact.DerivedArtifact(
+        middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER);
   }
 
   private SpecialArtifact createDerivedTreeArtifactWithAction(String path) {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
index 66d3ddf..166403d 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
@@ -32,6 +32,7 @@
 import com.google.devtools.build.lib.actions.FilesetTraversalParams;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
 import com.google.devtools.build.lib.actions.FilesetTraversalParamsFactory;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ServerDirectories;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -139,8 +140,8 @@
   }
 
   private Artifact getSourceArtifact(String path) throws Exception {
-    return new Artifact(
-        PathFragment.create(path), ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)), path);
   }
 
   private Artifact createSourceArtifact(String path) throws Exception {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
index 931719a..1a9da7a 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
@@ -37,6 +37,7 @@
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.FileStateValue;
 import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.TestAction;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ServerDirectories;
@@ -623,8 +624,8 @@
   private Artifact createDerivedArtifact(String relPath) throws IOException {
     Path outputPath = fs.getPath("/bin");
     outputPath.createDirectory();
-    return new Artifact(
-        outputPath.getRelative(relPath), ArtifactRoot.asDerivedRoot(fs.getPath("/"), outputPath));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asDerivedRoot(fs.getPath("/"), outputPath), outputPath.getRelative(relPath));
   }
 
   private SpecialArtifact createTreeArtifact(String relPath) throws IOException {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
index 42399c0..7ab9ea3 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
@@ -41,6 +41,7 @@
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversalRoot;
 import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.ServerDirectories;
@@ -187,14 +188,13 @@
   }
 
   private Artifact sourceArtifact(String path) {
-    return new Artifact(
-        PathFragment.create(path), ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)), path);
   }
 
   private Artifact sourceArtifactUnderPackagePath(String path, String packagePath) {
-    return new Artifact(
-        PathFragment.create(path),
-        ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory.getRelative(packagePath))));
+    return ActionsTestUtil.createArtifact(
+        ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory.getRelative(packagePath))), path);
   }
 
   private SpecialArtifact treeArtifact(String path) {
@@ -216,9 +216,8 @@
   private Artifact derivedArtifact(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
     Artifact output =
-        new Artifact(
-            ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")),
-            execPath);
+        ActionsTestUtil.createArtifactWithExecPath(
+            ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")), execPath);
     return output;
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
index ca08220..98ec1e7 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
@@ -38,7 +38,6 @@
 import com.google.devtools.build.lib.actions.ActionResult;
 import com.google.devtools.build.lib.actions.Actions;
 import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.ArtifactOwner;
 import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.ArtifactSkyKey;
 import com.google.devtools.build.lib.actions.BasicActionLookupValue;
@@ -54,6 +53,7 @@
 import com.google.devtools.build.lib.actions.cache.ActionCache;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.DummyExecutor;
 import com.google.devtools.build.lib.actions.util.InjectedActionLookupKey;
 import com.google.devtools.build.lib.actions.util.TestAction;
@@ -371,10 +371,8 @@
 
   private static Artifact createSourceArtifact(FileSystem fs, String name) {
     Path root = fs.getPath(TestUtils.tmpDir());
-    return new Artifact.SourceArtifact(
-        ArtifactRoot.asSourceRoot(Root.fromPath(root)),
-        PathFragment.create(name),
-        ArtifactOwner.NullArtifactOwner.INSTANCE);
+    return ActionsTestUtil.createArtifactWithExecPath(
+        ArtifactRoot.asSourceRoot(Root.fromPath(root)), PathFragment.create(name));
   }
 
   protected Artifact createDerivedArtifact(String name) {
@@ -384,7 +382,7 @@
   Artifact createDerivedArtifact(FileSystem fs, String name) {
     Path execRoot = fs.getPath(TestUtils.tmpDir());
     PathFragment execPath = PathFragment.create("out").getRelative(name);
-    return new Artifact(
+    return new Artifact.DerivedArtifact(
         ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")),
         execPath,
         ACTION_LOOKUP_KEY);