diff --git a/domino/api/src/test/java/io/quarkus/domino/manifest/CyclicDependencyGraphTest.java b/domino/api/src/test/java/io/quarkus/domino/manifest/CyclicDependencyGraphTest.java new file mode 100644 index 00000000..3a0fe25e --- /dev/null +++ b/domino/api/src/test/java/io/quarkus/domino/manifest/CyclicDependencyGraphTest.java @@ -0,0 +1,153 @@ +package io.quarkus.domino.manifest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.domino.ProjectDependencyConfig; +import io.quarkus.domino.ProjectDependencyResolver; +import io.quarkus.domino.test.repo.TestArtifactRepo; +import io.quarkus.domino.test.repo.TestProject; +import io.quarkus.maven.dependency.ArtifactCoords; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import org.cyclonedx.exception.ParseException; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.parsers.JsonParser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class CyclicDependencyGraphTest { + + @TempDir + static Path testRepoDir; + static MavenArtifactResolver artifactResolver; + + @BeforeAll + static void prepareRepo() { + var testRepo = TestArtifactRepo.of(testRepoDir); + artifactResolver = testRepo.getArtifactResolver(); + + var tcnativeProject = TestProject.of("io.netty", "1.0") + .setRepoUrl("https://netty.io/tcnative") + .setTag("1.0"); + tcnativeProject.createMainModule("netty-tcnative-boringssl-static") + .addClassifier("linux-aarch_64") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-aarch_64", " 1.0")) + .addClassifier("linux-x86_64") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-x86_64", " 1.0")) + .addClassifier("osx-aarch_64") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-aarch_64", " 1.0")) + .addClassifier("osx-x86_64") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-x86_64", " 1.0")) + .addClassifier("windows-x86_64") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "windows-x86_64", " 1.0")); + testRepo.install(tcnativeProject); + + var appProject = TestProject.of("org.acme", "1.0") + .setRepoUrl("https://acme.org/app") + .setTag("1.0"); + appProject.createMainModule("acme-app") + .addDependency(ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-x86_64", "1.0")); + testRepo.install(appProject); + } + + private static ProjectDependencyConfig.Mutable newDependencyConfig() { + return ProjectDependencyConfig.builder() + .setWarnOnMissingScm(true) + .setLegacyScmLocator(true); + } + + @Test + public void dependencyGraph() { + var config = newDependencyConfig() + .setProjectArtifacts(List.of(ArtifactCoords.jar("org.acme", "acme-app", "1.0"))); + + final Bom bom; + Path output = null; + try { + output = Files.createTempFile("domino-test", "sbom"); + + ProjectDependencyResolver.builder() + .setArtifactResolver(artifactResolver) + .setDependencyConfig(config) + .addDependencyTreeVisitor(new SbomGeneratingDependencyVisitor( + SbomGenerator.builder() + .setArtifactResolver(artifactResolver) + .setOutputFile(output) + .setEnableTransformers(false), + config)) + .build() + .resolveDependencies(); + + try (BufferedReader reader = Files.newBufferedReader(output)) { + bom = new JsonParser().parse(reader); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (output != null) { + output.toFile().deleteOnExit(); + } + } + + assertDependencies(bom, ArtifactCoords.jar("org.acme", "acme-app", "1.0"), + ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-x86_64", "1.0")); + + assertDependencies(bom, ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-x86_64", "1.0"), + ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-aarch_64", "1.0"), + ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-aarch_64", "1.0"), + ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-x86_64", "1.0"), + ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "windows-x86_64", "1.0")); + + assertDependencies(bom, ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "linux-aarch_64", "1.0")); + assertDependencies(bom, ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-aarch_64", "1.0")); + assertDependencies(bom, ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "osx-x86_64", "1.0")); + assertDependencies(bom, ArtifactCoords.jar("io.netty", "netty-tcnative-boringssl-static", "windows-x86_64", "1.0")); + } + + private static void assertDependencies(Bom bom, ArtifactCoords compCoords, ArtifactCoords... expectedDeps) { + var purl = PurgingDependencyTreeVisitor.getPurl(compCoords).toString(); + Component comp = null; + for (var c : bom.getComponents()) { + if (c.getPurl().equals(purl)) { + comp = c; + break; + } + } + if (comp == null) { + fail("Failed to locate " + purl); + return; + } + Dependency dep = null; + for (var d : bom.getDependencies()) { + if (d.getRef().equals(comp.getBomRef())) { + dep = d; + break; + } + } + if (dep == null && expectedDeps.length > 0) { + fail(comp.getBomRef() + " has no dependencies manifested while expected " + expectedDeps.length); + return; + } + final List recordedDeps = dep == null ? List.of() : dep.getDependencies(); + var recordedDepPurls = new HashSet(recordedDeps.size()); + for (var d : recordedDeps) { + recordedDepPurls.add(d.getRef()); + } + var expectedDepPurls = new HashSet(expectedDeps.length); + for (var c : expectedDeps) { + expectedDepPurls.add(PurgingDependencyTreeVisitor.getPurl(c).toString()); + } + assertThat(recordedDepPurls).isEqualTo(expectedDepPurls); + } +} diff --git a/domino/api/src/test/java/io/quarkus/domino/test/repo/TestArtifactRepo.java b/domino/api/src/test/java/io/quarkus/domino/test/repo/TestArtifactRepo.java index 28b2c527..4b209d97 100644 --- a/domino/api/src/test/java/io/quarkus/domino/test/repo/TestArtifactRepo.java +++ b/domino/api/src/test/java/io/quarkus/domino/test/repo/TestArtifactRepo.java @@ -58,18 +58,29 @@ private void install(TestModule module) { } catch (IOException e) { throw new RuntimeException(e); } - try { - ModelUtils.persistModel( - artifactDir.resolve(module.getArtifactId() + "-" + module.getVersion() + "." + ArtifactCoords.TYPE_POM), - module.getModel()); - } catch (IOException e) { - throw new RuntimeException(e); - } - if (ArtifactCoords.TYPE_JAR.equals(module.getPackaging())) { - try (var fs = ZipUtils.newZip( - artifactDir.resolve(module.getArtifactId() + "-" + module.getVersion() + "." + ArtifactCoords.TYPE_JAR))) { - } catch (IOException e) { - throw new RuntimeException(e); + for (var a : module.getPublishedArtifacts()) { + if (ArtifactCoords.TYPE_JAR.equals(a.getType())) { + var name = new StringBuilder(); + name.append(module.getArtifactId()).append("-").append(module.getVersion()); + if (!a.getClassifier().isEmpty()) { + name.append("-").append(a.getClassifier()); + } + name.append(".").append(ArtifactCoords.TYPE_JAR); + try (var fs = ZipUtils.newZip(artifactDir.resolve(name.toString()))) { + } catch (IOException e) { + throw new RuntimeException(e); + } + } else if (ArtifactCoords.TYPE_POM.equals(a.getType())) { + try { + ModelUtils.persistModel( + artifactDir.resolve( + module.getArtifactId() + "-" + module.getVersion() + "." + ArtifactCoords.TYPE_POM), + module.getModel()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalStateException("Unsupported artifact type " + a); } } for (TestModule m : module.getModules()) { diff --git a/domino/api/src/test/java/io/quarkus/domino/test/repo/TestModule.java b/domino/api/src/test/java/io/quarkus/domino/test/repo/TestModule.java index 7ed2f750..2493e4e2 100644 --- a/domino/api/src/test/java/io/quarkus/domino/test/repo/TestModule.java +++ b/domino/api/src/test/java/io/quarkus/domino/test/repo/TestModule.java @@ -3,10 +3,12 @@ import io.quarkus.maven.dependency.ArtifactCoords; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import org.apache.maven.model.Dependency; import org.apache.maven.model.DependencyManagement; import org.apache.maven.model.Model; @@ -26,6 +28,7 @@ public class TestModule { private List modules = List.of(); private Map> dependenciesByScope = Map.of(); private Map> constraintsByScope = Map.of(); + private Set publishedClassifiers = Set.of(); TestModule(TestProject project, String artifactId) { this.project = Objects.requireNonNull(project); @@ -134,6 +137,10 @@ public TestModule addDependency(String groupId, String artifactId, String versio return addDependency(ArtifactCoords.jar(groupId, artifactId, version), JavaScopes.COMPILE); } + public TestModule addDependency(ArtifactCoords coords) { + return addDependency(coords, JavaScopes.COMPILE); + } + public TestModule addDependency(ArtifactCoords coords, String scope) { if (dependenciesByScope.isEmpty()) { dependenciesByScope = new LinkedHashMap<>(); @@ -183,6 +190,14 @@ public TestModule importBom(String groupId, String artifactId, String version) { return addVersionConstraint(ArtifactCoords.pom(groupId, artifactId, version), "import"); } + public TestModule addClassifier(String classifier) { + if (publishedClassifiers.isEmpty()) { + publishedClassifiers = new HashSet<>(); + } + publishedClassifiers.add(classifier); + return this; + } + public Model getModel() { var model = new Model(); model.setModelVersion("4.0.0"); @@ -224,6 +239,23 @@ public Model getModel() { return model; } + public Collection getPublishedArtifacts() { + var artifacts = new ArrayList(1 + + (ArtifactCoords.TYPE_JAR.equals(packaging) ? 1 : 0) + + publishedClassifiers.size()); + var groupId = getGroupId(); + var artifactId = getArtifactId(); + var version = getVersion(); + artifacts.add(ArtifactCoords.pom(groupId, artifactId, version)); + if (ArtifactCoords.TYPE_JAR.equals(packaging)) { + artifacts.add(ArtifactCoords.jar(groupId, artifactId, version)); + } + for (var classifier : publishedClassifiers) { + artifacts.add(ArtifactCoords.jar(groupId, artifactId, classifier, version)); + } + return artifacts; + } + private static List toModelDeps(Map> deps) { var result = new ArrayList(); for (Map.Entry> scopeDeps : deps.entrySet()) {