Repository: smithy-lang/smithy
Pull request #1979 (2023-09-15 13:16:17)

Add Sha1 checksum to ResolvedArtifacts
### Description of changes:
Re-adds functionality removed in https://github.com/smithy-lang/smithy/pull/1978. 

#### Updates from previous version 
This updated version pulls the sha1 checksum from the `.sha1` files in the maven cache rather than computing the sha for each artifact. If a `.sha1` checksum file can't be found then the `sha1` checksum is computed using the Checksum algorithm provided by the maven resolver.

#### Overview 
As in https://github.com/smithy-lang/smithy/pull/1978, the sha is saved to the cache file as a property of the resolved artifact. 
Example of the new cache file:
```
{"version":"1.0","artifacts":{"software.amazon.smithy:smithy-aws-traits:1.37.0":{"path":"/Users/hpm/.m2/repository/software/amazon/smithy/smithy-aws-traits/1.37.0/smithy-aws-traits-1.37.0.jar","sha1":"723b00f4473c0f3f308b57b2c95b32fad669904c68d8e287606304d841cd52ff"}}}
```

This sha1 checksum will be used to validate artifacts against locked dependencies as part of implementing lock files for the smithy cli.

A version is also added to the cache file to allow the CLI to invalidate and re-write cache file versions that are not compatible with the current implementation used by the CLI.

#### Simple Benchmarking of dependency resolution time
For a `smithy-build.json` with 6 dependencies
**Without Cache**
Before - 94.25ms +- 1.9ms (1sigma, 15 runs)
After - 99.5ms +- 3.0ms (1sigma, 15 runs)

**With Cache**
Before - 36ms +- 1.1ms (1sigma, 15 runs)
After - 37ms +- 0.5ms (1sigma, 15 runs)


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java
new file mode 100644
index 000000000..d87ccad88
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.dependencies;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+final class DependencyUtils {
+    private DependencyUtils() {}
+
+    /**
+     * Computes the sha1 digest of a file.
+     *
+     * @param path Path to file to compute hash for.
+     * @return sha1 digest string.
+     * @throws UncheckedIOException if the specified file could not be read.
+     */
+    public static String computeSha1(Path path) {
+        try (InputStream in = Files.newInputStream(path)) {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            try (DigestInputStream din = new DigestInputStream(in, md)) {
+                byte[] buf = new byte[1024 * 32];
+                int n;
+                do {
+                    n = din.read(buf);
+                } while (n > 0);
+            }
+            StringBuilder sb = new StringBuilder();
+            for (byte b : md.digest()) {
+                int decimal = (int) b & 0xff;
+                String hex = Integer.toHexString(decimal);
+                if (hex.length() == 1) {
+                    sb.append('0');
+                }
+                sb.append(hex);
+            }
+            return sb.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/FileCacheResolver.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/FileCacheResolver.java
index 3c439f7e0..49bcc8298 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/FileCacheResolver.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/FileCacheResolver.java
@@ -21,7 +21,6 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -40,6 +39,7 @@ import software.amazon.smithy.model.node.ObjectNode;
  * given reference point in time.
  */
 public final class FileCacheResolver implements DependencyResolver {
+    private static final String CURRENT_CACHE_FILE_VERSION = "1.0";
 
     // This is hard-coded for now to 1 day, and it can become an environment variable in the future if needed.
     private static final Duration SMITHY_MAVEN_TTL = Duration.parse("P1D");
@@ -113,17 +113,28 @@ public final class FileCacheResolver implements DependencyResolver {
             throw new DependencyResolverException("Error loading dependency cache file from " + location, e);
         }
 
-        List<ResolvedArtifact> result = new ArrayList<>(node.getStringMap().size());
-        for (Map.Entry<String, Node> entry : node.getStringMap().entrySet()) {
-            Path artifactLocation = Paths.get(entry.getValue().expectStringNode().getValue());
-            long lastModifiedOfArtifact = artifactLocation.toFile().lastModified();
+        // If the version of the cache file does not match the current version or does not exist
+        // invalidate it so we can replace it with a more recent version.
+        if (!node.containsMember("version")
+                || !CURRENT_CACHE_FILE_VERSION.equals(node.expectStringMember("version").getValue())
+        ) {
+            LOGGER.fine(() -> "Invalidating dependency cache: cache file uses old version");
+            invalidate();
+            return Collections.emptyList();
+        }
+
+        ObjectNode artifactNode = node.expectObjectMember("artifacts");
+        List<ResolvedArtifact> result = new ArrayList<>(artifactNode.getStringMap().size());
+        for (Map.Entry<String, Node> entry : artifactNode.getStringMap().entrySet()) {
+            ResolvedArtifact artifact = ResolvedArtifact.fromCoordinateNode(entry.getKey(), entry.getValue());
+            long lastModifiedOfArtifact = artifact.getLastModified();
             // Invalidate the cache if the JAR file was updated since the cache was created.
             if (lastModifiedOfArtifact == 0 || lastModifiedOfArtifact > cacheLastModifiedMillis) {
-                LOGGER.fine(() -> "Invalidating dependency cache: artifact is newer than cache: " + artifactLocation);
+                LOGGER.fine(() -> "Invalidating dependency cache: artifact is newer than cache: " + artifact.getPath());
                 invalidate();
                 return Collections.emptyList();
             }
-            result.add(ResolvedArtifact.fromCoordinates(artifactLocation, entry.getKey()));
+            result.add(artifact);
         }
 
         return result;
@@ -139,11 +150,13 @@ public final class FileCacheResolver implements DependencyResolver {
         try {
             Files.createDirectories(parent);
             ObjectNode.Builder builder = Node.objectNodeBuilder();
+            builder.withMember("version", CURRENT_CACHE_FILE_VERSION);
+            ObjectNode.Builder artifactNodeBuilder = Node.objectNodeBuilder();
             for (ResolvedArtifact artifact : result) {
-                builder.withMember(artifact.getCoordinates(), artifact.getPath().toString());
+                artifactNodeBuilder.withMember(artifact.getCoordinates(), artifact.toNode());
             }
-            ObjectNode objectNode = builder.build();
-            Files.write(filePath, Node.printJson(objectNode).getBytes(StandardCharsets.UTF_8));
+            builder.withMember("artifacts", artifactNodeBuilder.build());
+            Files.write(filePath, Node.printJson(builder.build()).getBytes(StandardCharsets.UTF_8));
         } catch (IOException e) {
             throw new DependencyResolverException("Unable to write classpath cache file: " + e.getMessage(), e);
         }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/MavenDependencyResolver.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/MavenDependencyResolver.java
index 86c75ad07..f9715251a 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/MavenDependencyResolver.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/MavenDependencyResolver.java
@@ -141,7 +141,7 @@ public final class MavenDependencyResolver implements DependencyResolver {
         for (ArtifactResult result : results) {
             Artifact artifact = result.getArtifact();
             artifacts.add(new ResolvedArtifact(artifact.getFile().toPath(), artifact.getGroupId(),
-                                               artifact.getArtifactId(), artifact.getVersion()));
+                    artifact.getArtifactId(), artifact.getVersion()));
         }
         return artifacts;
     }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/ResolvedArtifact.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/ResolvedArtifact.java
index ab42f1bf9..2f52d20e0 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/ResolvedArtifact.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/ResolvedArtifact.java
@@ -15,30 +15,59 @@
 
 package software.amazon.smithy.cli.dependencies;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Objects;
+import org.eclipse.aether.util.ChecksumUtils;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.node.ToNode;
 
 /**
  * An artifact resolved from a repository that provides the path on disk where the artifact
  * was downloaded, and the coordinates of the artifact.
  */
-public final class ResolvedArtifact {
+public final class ResolvedArtifact implements ToNode {
+    private static final String SHA_SUM_MEMBER_NAME = "sha1";
+    private static final String PATH_MEMBER_NAME = "path";
+    private static final String CHECKSUM_FILE_EXTENSION = ".sha1";
     private final Path path;
     private final String coordinates;
     private final String groupId;
     private final String artifactId;
     private final String version;
+    private final String shaSum;
 
     public ResolvedArtifact(Path path, String groupId, String artifactId, String version) {
-        this(path, groupId + ':' + artifactId + ':' + version, groupId, artifactId, version);
+        this(path, groupId + ':' + artifactId + ':' + version, groupId, artifactId, version, null);
     }
 
-    private ResolvedArtifact(Path path, String coordinates, String groupId, String artifactId, String version) {
+    private ResolvedArtifact(Path path, String coordinates, String groupId, String artifactId,
+                             String version, String shaSum
+    ) {
         this.coordinates = Objects.requireNonNull(coordinates);
         this.path = Objects.requireNonNull(path);
         this.groupId = groupId;
         this.artifactId = artifactId;
         this.version = version;
+        this.shaSum = shaSum != null ? shaSum : getOrComputeSha(path);
+    }
+
+    private static String getOrComputeSha(Path path) {
+        File artifactFile = path.toFile();
+        File checksumFile = new File(artifactFile.getParent(), artifactFile.getName() + CHECKSUM_FILE_EXTENSION);
+        try {
+            if (checksumFile.exists()) {
+                return ChecksumUtils.read(checksumFile);
+            } else {
+                return DependencyUtils.computeSha1(path);
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
     }
 
     /**
@@ -50,11 +79,23 @@ public final class ResolvedArtifact {
      * @throws DependencyResolverException if the provided coordinates are invalid.
      */
     public static ResolvedArtifact fromCoordinates(Path location, String coordinates) {
-        String[] parts = coordinates.split(":");
-        if (parts.length != 3) {
-            throw new DependencyResolverException("Invalid Maven coordinates: " + coordinates);
-        }
-        return new ResolvedArtifact(location, coordinates, parts[0], parts[1], parts[2]);
+        String[] parts = parseCoordinates(coordinates);
+        return new ResolvedArtifact(location, coordinates, parts[0], parts[1], parts[2], null);
+    }
+
+    /**
+     * Creates a resolved artifact from a Maven coordinates string and Node.
+     *
+     * @param coordinates Maven coordinates (e.g., group:artifact:version).
+     * @param node Node containing the resolved artifact data.
+     * @return Returns the created artifact
+     */
+    public static ResolvedArtifact fromCoordinateNode(String coordinates, Node node) {
+        String[] parts = parseCoordinates(coordinates);
+        ObjectNode objectNode = node.expectObjectNode();
+        String shaSum = objectNode.expectMember(SHA_SUM_MEMBER_NAME).expectStringNode().getValue();
+        Path location = Paths.get(objectNode.expectMember(PATH_MEMBER_NAME).expectStringNode().getValue());
+        return new ResolvedArtifact(location, coordinates, parts[0], parts[1], parts[2], shaSum);
     }
 
     /**
@@ -96,14 +137,28 @@ public final class ResolvedArtifact {
         return version;
     }
 
+    /**
+     * @return Get the sha1 digest of the artifact.
+     */
+    public String getShaSum() {
+        return shaSum;
+    }
+
+    /**
+     * @return Get the last time the artifact was modified.
+     */
+    public long getLastModified() {
+        return path.toFile().lastModified();
+    }
+
     @Override
     public String toString() {
-        return "{path=" + path + ", coordinates='" + coordinates + "'}";
+        return "{path=" + path + ", coordinates='" + coordinates + ", sha1='" + shaSum + "'}";
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(coordinates, path);
+        return Objects.hash(coordinates, path, shaSum);
     }
 
     @Override
@@ -114,6 +169,24 @@ public final class ResolvedArtifact {
             return false;
         }
         ResolvedArtifact artifact = (ResolvedArtifact) o;
-        return path.equals(artifact.path) && coordinates.equals(artifact.coordinates);
+        return path.equals(artifact.path)
+                && shaSum.equals(artifact.shaSum)
+                && coordinates.equals(artifact.coordinates);
+    }
+
+    @Override
+    public Node toNode() {
+        return Node.objectNodeBuilder()
+                .withMember(PATH_MEMBER_NAME, path.toString())
+                .withMember(SHA_SUM_MEMBER_NAME, shaSum)
+                .build();
+    }
+
+    private static String[] parseCoordinates(String coordinates) {
+        String[] parts = coordinates.split(":");
+        if (parts.length != 3) {
+            throw new DependencyResolverException("Invalid Maven coordinates: " + coordinates);
+        }
+        return parts;
     }
 }


Test patch:
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
index 40bb424a8..add3af72e 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
@@ -154,8 +154,9 @@ public class MavenResolverTest {
             String cacheContents = result.getFile("build", "smithy", "classpath.json");
 
             ObjectNode node = Node.parse(cacheContents).expectObjectNode();
-            String location = node.expectStringMember("software.amazon.smithy:smithy-aws-traits:"
-                                                      + TEST_VERSION).getValue();
+            ObjectNode artifacts = node.expectObjectMember("artifacts");
+            String location = artifacts.expectObjectMember("software.amazon.smithy:smithy-aws-traits:"
+                    + TEST_VERSION).expectStringMember("path").getValue();
 
             // Set the lastModified of the JAR to the current time, which is > than the time of the config file,
             // so the cache is invalided.
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/DependencyUtilsTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/DependencyUtilsTest.java
new file mode 100644
index 000000000..1eb403979
--- /dev/null
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/DependencyUtilsTest.java
@@ -0,0 +1,20 @@
+package software.amazon.smithy.cli.dependencies;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class DependencyUtilsTest {
+    @Test
+    public void computesCorrectShaHash() throws URISyntaxException {
+        // Expected sha was computed using the `sha1sum` command line utility
+        String expectedSha = "543d822a441db77f32309650a1b3c1510c9be392";
+        String shaHash = DependencyUtils.computeSha1(Paths.get(Objects.requireNonNull(
+                getClass().getResource("sha-test.txt")).toURI()));
+        assertEquals(shaHash, expectedSha);
+    }
+}
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
index 8ef15ebbe..264ed7e95 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
@@ -11,6 +11,7 @@ import java.util.List;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.build.model.MavenRepository;
+import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.utils.ListUtils;
 
 public class FilterCliVersionResolverTest {
@@ -52,17 +53,17 @@ public class FilterCliVersionResolverTest {
             @Override
             public List<ResolvedArtifact> resolve() {
                 return Arrays.asList(
-                    ResolvedArtifact.fromCoordinates(Paths.get("/a"), "software.amazon.smithy:smithy-model:1.25.0"),
-                    ResolvedArtifact.fromCoordinates(Paths.get("/b"), "software.amazon.smithy:smithy-utils:1.25.0"),
-                    ResolvedArtifact.fromCoordinates(Paths.get("/c"), "software.amazon.smithy:smithy-other:1.25.0"),
-                    ResolvedArtifact.fromCoordinates(Paths.get("/d"), "software.amazon.foo:foo-other:1.0.0")
+                    ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-model:1.25.0", getNodeForPath("/a")),
+                    ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-utils:1.25.0", getNodeForPath("/b")),
+                    ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-other:1.25.0", getNodeForPath("/c")),
+                    ResolvedArtifact.fromCoordinateNode("software.amazon.foo:foo-other:1.0.0", getNodeForPath("/d"))
                 );
             }
         });
 
         assertThat(filter.resolve(), contains(
-            ResolvedArtifact.fromCoordinates(Paths.get("/c"), "software.amazon.smithy:smithy-other:1.25.0"),
-            ResolvedArtifact.fromCoordinates(Paths.get("/d"), "software.amazon.foo:foo-other:1.0.0")
+            ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-other:1.25.0", getNodeForPath("/c")),
+            ResolvedArtifact.fromCoordinateNode("software.amazon.foo:foo-other:1.0.0", getNodeForPath("/d"))
         ));
     }
 
@@ -81,11 +82,17 @@ public class FilterCliVersionResolverTest {
 
             @Override
             public List<ResolvedArtifact> resolve() {
-                return ListUtils.of(ResolvedArtifact.fromCoordinates(Paths.get("/a"),
-                                                                     "software.amazon.smithy:smithy-model:1.27.0"));
+                return ListUtils.of(ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-model:1.27.0", getNodeForPath("/a")));
             }
         });
 
         Assertions.assertThrows(DependencyResolverException.class, filter::resolve);
     }
+
+    private static Node getNodeForPath(String pathStr) {
+        return Node.objectNodeBuilder()
+                .withMember("path", pathStr)
+                .withMember("sha1", "sum")
+                .build();
+    }
 }
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/ResolvedArtifactTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/ResolvedArtifactTest.java
index eb4ea7b40..c8727035f 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/ResolvedArtifactTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/ResolvedArtifactTest.java
@@ -3,15 +3,19 @@ package software.amazon.smithy.cli.dependencies;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Objects;
+
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.node.Node;
 
 public class ResolvedArtifactTest {
     @Test
-    public void loadsFromCoordinates() {
-        Path path = Paths.get("/a");
+    public void loadsFromCoordinates() throws URISyntaxException {
+        Path path =  Paths.get(Objects.requireNonNull(getClass().getResource("test.txt")).toURI());
         String coordinates = "com.foo:baz-bam:1.2.0";
         ResolvedArtifact artifact = ResolvedArtifact.fromCoordinates(path, coordinates);
 
@@ -20,11 +24,12 @@ public class ResolvedArtifactTest {
         assertThat(artifact.getGroupId(), equalTo("com.foo"));
         assertThat(artifact.getArtifactId(), equalTo("baz-bam"));
         assertThat(artifact.getVersion(), equalTo("1.2.0"));
+        assertThat(artifact.getShaSum(), equalTo("bbe99d58c301c90f0ea9354275468f3c84de710e"));
     }
 
     @Test
-    public void createsCoordinatesStringFromParts() {
-        Path path = Paths.get("/a");
+    public void createsCoordinatesStringFromParts() throws URISyntaxException {
+        Path path =  Paths.get(Objects.requireNonNull(getClass().getResource("test.txt")).toURI());
         ResolvedArtifact artifact = new ResolvedArtifact(path, "com.foo", "baz-bam", "1.2.0");
 
         assertThat(artifact.getPath(), equalTo(path));
@@ -32,23 +37,43 @@ public class ResolvedArtifactTest {
         assertThat(artifact.getGroupId(), equalTo("com.foo"));
         assertThat(artifact.getArtifactId(), equalTo("baz-bam"));
         assertThat(artifact.getVersion(), equalTo("1.2.0"));
+        assertThat(artifact.getShaSum(), equalTo("bbe99d58c301c90f0ea9354275468f3c84de710e"));
     }
 
     @Test
     public void validatesCoordinatesNotTooManyParts() {
-        Path path = Paths.get("/a");
         String coordinates = "com.foo:baz-bam:1.2.0:boo";
 
         Assertions.assertThrows(DependencyResolverException.class,
-                                () -> ResolvedArtifact.fromCoordinates(path, coordinates));
+                                () -> ResolvedArtifact.fromCoordinateNode(coordinates, getNode()));
     }
 
     @Test
     public void validatesCoordinatesEnoughParts() {
-        Path path = Paths.get("/a");
         String coordinates = "com.foo:baz-bam";
 
         Assertions.assertThrows(DependencyResolverException.class,
-                                () -> ResolvedArtifact.fromCoordinates(path, coordinates));
+                                () -> ResolvedArtifact.fromCoordinateNode(coordinates,getNode()));
+    }
+
+    @Test
+    public void createsArtifactFromNode() {
+        Path path =  Paths.get("/a");
+        ResolvedArtifact artifact = ResolvedArtifact.fromCoordinateNode("com.foo:baz-bam:1.2.0",
+                        getNode());
+
+        assertThat(artifact.getPath(), equalTo(path));
+        assertThat(artifact.getCoordinates(), equalTo("com.foo:baz-bam:1.2.0"));
+        assertThat(artifact.getGroupId(), equalTo("com.foo"));
+        assertThat(artifact.getArtifactId(), equalTo("baz-bam"));
+        assertThat(artifact.getVersion(), equalTo("1.2.0"));
+        assertThat(artifact.getShaSum(), equalTo("sum"));
+    }
+
+    public Node getNode() {
+        return Node.objectNodeBuilder()
+                .withMember("path", "/a")
+                .withMember("sha1", "sum")
+                .build();
     }
 }
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/sha-test.txt b/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/sha-test.txt
new file mode 100644
index 000000000..8058eda3a
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/sha-test.txt
@@ -0,0 +1,1 @@
+Tests sha digest.
\ No newline at end of file
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/test.txt b/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/test.txt
new file mode 100644
index 000000000..390298abd
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/dependencies/test.txt
@@ -0,0 +1,1 @@
+testing testing testing
\ No newline at end of file