diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java index 1e5bab32a..f947f8543 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java @@ -35,6 +35,7 @@ public static void main(String[] args) throws Exception { GetDirectDependencies.main(args); GetDependencyTree.main(args); GetDependencyHierarchy.main(args); + DependencyHierarchyWithRanges.main(args); ResolveArtifact.main(args); ResolveTransitiveDependencies.main(args); ReverseDependencyTree.main(args); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java new file mode 100644 index 000000000..19820f59f --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.resolver.examples; + +import java.io.File; +import java.util.Collections; + +import org.apache.maven.resolver.examples.util.Booter; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.resolution.ArtifactDescriptorRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; + +/** + * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view. Artifact in this + * test is not "plain" one as is original "demo" {@link GetDependencyHierarchy}, but specially crafted for case + * described in MRESOLVER-345. + * + * @see MRESOLVER-345 + */ +public class DependencyHierarchyWithRanges { + + /** + * Main. + */ + public static void main(String[] args) throws Exception { + System.out.println("------------------------------------------------------------"); + System.out.println(DependencyHierarchyWithRanges.class.getSimpleName()); + + RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args)); + + DefaultRepositorySystemSession session = Booter.newRepositorySystemSession(system); + + session.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_IGNORE); // to not bother with checksums + session.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + session.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + + // this artifact is in "remote" repository in src/main/remote-repository + Artifact artifact = new DefaultArtifact("org.apache.maven.resolver.demo.mresolver345:a:1.0"); + + File remoteRepoBasedir = new File("src/main/remote-repository"); + + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(artifact); + descriptorRequest.setRepositories(Collections.singletonList(new RemoteRepository.Builder( + "remote", "default", remoteRepoBasedir.toURI().toASCIIString()) + .build())); + ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); + + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(descriptorResult.getArtifact()); + collectRequest.setDependencies(descriptorResult.getDependencies()); + collectRequest.setManagedDependencies(descriptorResult.getManagedDependencies()); + collectRequest.setRepositories(descriptorRequest.getRepositories()); + + CollectResult collectResult = system.collectDependencies(session, collectRequest); + + collectResult.getRoot().accept(Booter.DUMPER_SOUT); + } +} diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java index 3d6441f51..3a4504d74 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java @@ -19,7 +19,6 @@ package org.apache.maven.resolver.examples; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.artifact.Artifact; @@ -67,6 +66,6 @@ public static void main(String[] args) throws Exception { CollectResult collectResult = system.collectDependencies(session, collectRequest); - collectResult.getRoot().accept(new ConsoleDependencyGraphDumper()); + collectResult.getRoot().accept(Booter.DUMPER_SOUT); } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java index e47129cb5..77042e40e 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java @@ -19,7 +19,6 @@ package org.apache.maven.resolver.examples; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -54,6 +53,6 @@ public static void main(String[] args) throws Exception { CollectResult collectResult = system.collectDependencies(session, collectRequest); - collectResult.getRoot().accept(new ConsoleDependencyGraphDumper()); + collectResult.getRoot().accept(Booter.DUMPER_SOUT); } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java index a5a01f76d..bf1d37954 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java @@ -20,9 +20,10 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; @@ -40,6 +41,7 @@ import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.DependencyRequest; import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; import org.eclipse.aether.util.repository.AuthenticationBuilder; @@ -121,8 +123,13 @@ public void deploy(Artifact artifact, Artifact pom, String remoteRepository) thr } private void displayTree(DependencyNode node, StringBuilder sb) { - ByteArrayOutputStream os = new ByteArrayOutputStream(1024); - node.accept(new ConsoleDependencyGraphDumper(new PrintStream(os))); - sb.append(os.toString()); + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os, true, StandardCharsets.UTF_8.name()); + node.accept(new DependencyGraphDumper(ps::println)); + sb.append(os); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java index 93ade9c4f..2744650b6 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java @@ -28,6 +28,7 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; /** * A helper to boot the repository system and a repository system session. @@ -39,6 +40,8 @@ public class Booter { public static final String SISU = "sisu"; + public static final DependencyGraphDumper DUMPER_SOUT = new DependencyGraphDumper(System.out::println); + public static String selectFactory(String[] args) { if (args == null || args.length == 0) { return SERVICE_LOCATOR; diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom new file mode 100644 index 000000000..918e0eb18 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom @@ -0,0 +1,40 @@ + + + + 4.0.0 + + org.apache.maven.resolver.demo.mresolver345 + a + 1.0 + + + + org.apache.maven.resolver.demo.mresolver345 + b + 1.0 + + + org.apache.maven.resolver.demo.mresolver345 + c + [1.0, 3.0] + + + + diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom new file mode 100644 index 000000000..02805ea95 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + + org.apache.maven.resolver.demo.mresolver345 + b + 1.0 + + + + org.apache.maven.resolver.demo.mresolver345 + c + [1.0, 3.0] + + + + diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom new file mode 100644 index 000000000..a875fe07a --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.apache.maven.resolver.demo.mresolver345 + c + 1.0 + + diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom new file mode 100644 index 000000000..9f961ae0d --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.apache.maven.resolver.demo.mresolver345 + c + 2.0 + + diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom new file mode 100644 index 000000000..44f9af58c --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.apache.maven.resolver.demo.mresolver345 + c + 3.0 + + diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml new file mode 100644 index 000000000..7d3618d73 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml @@ -0,0 +1,33 @@ + + + + org.apache.maven.resolver.demo.mresolver345 + c + + 3.0 + 3.0 + + 1.0 + 2.0 + 3.0 + + 20230320074804 + + diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java index 5cf4d7f23..89a2291e6 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java @@ -497,6 +497,23 @@ public void testDependencyManagement_DefaultDependencyManager() throws Dependenc assertEqualSubtree(expectedTree, toDependencyResult(result.getRoot(), "compile", null)); } + @Test + public void testTransitiveDepsUseRangesDirtyTree() throws DependencyCollectionException, IOException { + // Note: DF depends on version order (ultimately the order of versions as returned by VersionRangeResolver + // that in case of Maven, means order as in maven-metadata.xml + // BF on the other hand explicitly sorts versions from range in descending order + // + // Hence, the "dirty tree" of two will not match. + DependencyNode root = parser.parseResource(getTransitiveDepsUseRangesDirtyTreeResource()); + Dependency dependency = root.getDependency(); + CollectRequest request = new CollectRequest(dependency, singletonList(repository)); + + CollectResult result = collector.collectDependencies(session, request); + assertEqualSubtree(root, result.getRoot()); + } + + protected abstract String getTransitiveDepsUseRangesDirtyTreeResource(); + private DependencyNode toDependencyResult( final DependencyNode root, final String rootScope, final Boolean optional) { // Make the root artifact resultion result a dependency resolution result for the subtree check. diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java index 353030fdd..870d2ef61 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java @@ -59,10 +59,13 @@ public static List parameters() { protected void setupCollector() { session.setConfigProperty(BfDependencyCollector.CONFIG_PROP_SKIPPER, useSkipper); - collector = new BfDependencyCollector(); - collector.setArtifactDescriptorReader(newReader("")); - collector.setVersionRangeResolver(new StubVersionRangeResolver()); - collector.setRemoteRepositoryManager(new StubRemoteRepositoryManager()); + collector = new BfDependencyCollector( + new StubRemoteRepositoryManager(), newReader(""), new StubVersionRangeResolver()); + } + + @Override + protected String getTransitiveDepsUseRangesDirtyTreeResource() { + return "transitiveDepsUseRangesDirtyTreeResult_BF.txt"; } private Dependency newDep(String coords, String scope, Collection exclusions) { diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java index f5e808b66..f07fdc335 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java @@ -28,9 +28,12 @@ public class DfDependencyCollectorTest extends DependencyCollectorDelegateTestSupport { @Override protected void setupCollector() { - collector = new DfDependencyCollector(); - collector.setArtifactDescriptorReader(newReader("")); - collector.setVersionRangeResolver(new StubVersionRangeResolver()); - collector.setRemoteRepositoryManager(new StubRemoteRepositoryManager()); + collector = new DfDependencyCollector( + new StubRemoteRepositoryManager(), newReader(""), new StubVersionRangeResolver()); + } + + @Override + protected String getTransitiveDepsUseRangesDirtyTreeResource() { + return "transitiveDepsUseRangesDirtyTreeResult_DF.txt"; } } diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt new file mode 100644 index 000000000..b1711a4bd --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt @@ -0,0 +1,8 @@ +transitiveDepsUseRangesDirtyTree:aid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:bid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +| \- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +\- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt new file mode 100644 index 000000000..ef148ca9a --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt @@ -0,0 +1,8 @@ +transitiveDepsUseRangesDirtyTree:aid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:bid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +| \- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +\- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini new file mode 100644 index 000000000..ed282a9c1 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini @@ -0,0 +1,3 @@ +[dependencies] +transitiveDepsUseRangesDirtyTree:bid:ext:1 +transitiveDepsUseRangesDirtyTree:cid:ext:[1,3] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini new file mode 100644 index 000000000..e9a5d76b2 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini @@ -0,0 +1,2 @@ +[dependencies] +transitiveDepsUseRangesDirtyTree:cid:ext:[1,3] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini new file mode 100644 index 000000000..61a252c23 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini new file mode 100644 index 000000000..61a252c23 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini new file mode 100644 index 000000000..61a252c23 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java index 7289c9bc1..084f2d666 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java @@ -18,26 +18,17 @@ */ package org.eclipse.aether.util.graph.transformer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; +import java.util.*; import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; -import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.artifact.ArtifactIdUtils; import static java.util.Objects.requireNonNull; @@ -66,9 +57,66 @@ public final class ConflictResolver implements DependencyGraphTransformer { /** * The key in the repository session's {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties() * configuration properties} used to store a {@link Boolean} flag controlling the transformer's verbose mode. + * Accepted values are {@link Boolean} type, {@link String} type (where "true" would be interpreted as {@code true} + * or {@link Verbosity} enum instances. */ public static final String CONFIG_PROP_VERBOSE = "aether.conflictResolver.verbose"; + /** + * The enum representing verbosity levels of conflict resolver. + * + * @since 1.9.8 + */ + public enum Verbosity { + /** + * Verbosity level to be used in all "common" resolving use cases (ie. dependencies to build class path). The + * {@link ConflictResolver} in this mode will trim down the graph to the barest minimum: will not leave + * any conflicting node in place, hence no conflicts will be present in transformed graph either. + */ + NONE, + + /** + * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The + * {@link ConflictResolver} in this mode will remove any redundant collected nodes, in turn it will leave one + * with recorded conflicting information. This mode corresponds to "classic verbose" mode when + * {@link #CONFIG_PROP_VERBOSE} was set to {@code true}. Obviously, the resulting dependency tree is not + * suitable for artifact resolution unless a filter is employed to exclude the duplicate dependencies. + */ + STANDARD, + + /** + * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The + * {@link ConflictResolver} in this mode will not remove any collected node, in turn it will record on all + * eliminated nodes the conflicting information. Obviously, the resulting dependency tree is not suitable + * for artifact resolution unless a filter is employed to exclude the duplicate dependencies. + */ + FULL + } + + /** + * Helper method that uses {@link RepositorySystemSession} and {@link #CONFIG_PROP_VERBOSE} key to figure out + * current {@link Verbosity}: if {@link Boolean} or {@code String} found, returns {@link Verbosity#STANDARD} + * or {@link Verbosity#NONE}, depending on value (string is parsed with {@link Boolean#parseBoolean(String)} + * for {@code true} or {@code false} correspondingly. This is to retain "existing" behavior, where the config + * key accepted only these values. + * Since 1.9.8 release, this key may contain {@link Verbosity} enum instance as well, in which case that instance + * is returned. + * This method never returns {@code null}. + */ + private static Verbosity getVerbosity(RepositorySystemSession session) { + final Object verbosityValue = session.getConfigProperties().get(CONFIG_PROP_VERBOSE); + if (verbosityValue instanceof Boolean) { + return (Boolean) verbosityValue ? Verbosity.STANDARD : Verbosity.NONE; + } else if (verbosityValue instanceof String) { + return Boolean.parseBoolean(verbosityValue.toString()) ? Verbosity.STANDARD : Verbosity.NONE; + } else if (verbosityValue instanceof Verbosity) { + return (Verbosity) verbosityValue; + } else if (verbosityValue != null) { + throw new IllegalArgumentException("Unsupported Verbosity configuration: " + verbosityValue); + } + return Verbosity.NONE; + } + /** * The key in the dependency node's {@link DependencyNode#getData() custom data} under which a reference to the * {@link DependencyNode} which has won the conflict is stored. @@ -173,14 +221,14 @@ public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransfo DependencyNode winner = ctx.winner.node; state.scopeSelector.selectScope(ctx); - if (state.verbose) { + if (Verbosity.NONE != state.verbosity) { winner.setData( NODE_DATA_ORIGINAL_SCOPE, winner.getDependency().getScope()); } winner.setScope(ctx.scope); state.optionalitySelector.selectOptionality(ctx); - if (state.verbose) { + if (Verbosity.NONE != state.verbosity) { winner.setData( NODE_DATA_ORIGINAL_OPTIONALITY, winner.getDependency().isOptional()); @@ -232,11 +280,12 @@ private boolean gatherConflictItems(DependencyNode node, State state) throws Rep return true; } - private void removeLosers(State state) { + private static void removeLosers(State state) { ConflictItem winner = state.conflictCtx.winner; + String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact()); List previousParent = null; ListIterator childIt = null; - boolean conflictVisualized = false; + HashSet toRemoveIds = new HashSet<>(); for (ConflictItem item : state.items) { if (item == winner) { continue; @@ -244,30 +293,78 @@ private void removeLosers(State state) { if (item.parent != previousParent) { childIt = item.parent.listIterator(); previousParent = item.parent; - conflictVisualized = false; } while (childIt.hasNext()) { DependencyNode child = childIt.next(); if (child == item.node) { - if (state.verbose && !conflictVisualized && item.parent != winner.parent) { - conflictVisualized = true; - DependencyNode loser = new DefaultDependencyNode(child); - loser.setData(NODE_DATA_WINNER, winner.node); - loser.setData( - NODE_DATA_ORIGINAL_SCOPE, loser.getDependency().getScope()); - loser.setData( - NODE_DATA_ORIGINAL_OPTIONALITY, - loser.getDependency().isOptional()); - loser.setScope(item.getScopes().iterator().next()); - loser.setChildren(Collections.emptyList()); - childIt.set(loser); - } else { + // NONE: just remove it and done + if (Verbosity.NONE == state.verbosity) { childIt.remove(); + break; + } + + // STANDARD: doing extra bookkeeping to select "which nodes to remove" + if (Verbosity.STANDARD == state.verbosity) { + String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); + // if two IDs are equal, it means "there is nearest", not conflict per se. + // In that case we do NOT allow this child to be removed (but remove others) + // and this keeps us safe from iteration (and in general, version) ordering + // as we explicitly leave out ID that is "nearest found" state. + // + // This tackles version ranges mostly, where ranges are turned into list of + // several nodes in collector (as many were discovered, ie. from metadata), and + // old code would just "mark" the first hit as conflict, and remove the rest, + // even if rest could contain "more suitable" version, that is not conflicting/diverging. + // This resulted in verbose mode transformed tree, that was misrepresenting things + // for dependency convergence calculations: it represented state like parent node + // depends on "wrong" version (diverge), while "right" version was present (but removed) + // as well, as it was contained in parents version range. + if (!Objects.equals(winnerArtifactId, childArtifactId)) { + toRemoveIds.add(childArtifactId); + } } + + // FULL: just record the facts + DependencyNode loser = new DefaultDependencyNode(child); + loser.setData(NODE_DATA_WINNER, winner.node); + loser.setData( + NODE_DATA_ORIGINAL_SCOPE, loser.getDependency().getScope()); + loser.setData( + NODE_DATA_ORIGINAL_OPTIONALITY, + loser.getDependency().isOptional()); + loser.setScope(item.getScopes().iterator().next()); + loser.setChildren(Collections.emptyList()); + childIt.set(loser); + item.node = loser; break; } } } + + // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care + if (Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) { + previousParent = null; + for (ConflictItem item : state.items) { + if (item == winner) { + continue; + } + if (item.parent != previousParent) { + childIt = item.parent.listIterator(); + previousParent = item.parent; + } + while (childIt.hasNext()) { + DependencyNode child = childIt.next(); + if (child == item.node) { + String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); + if (toRemoveIds.contains(childArtifactId) && item.parent.size() > 1) { + childIt.remove(); + } + break; + } + } + } + } + // there might still be losers beneath the winner (e.g. in case of cycles) // those will be nuked during future graph walks when we include the winner in the recursion } @@ -361,7 +458,7 @@ final class State { /** * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts. */ - final boolean verbose; + final Verbosity verbosity; /** * A mapping from conflict id to winner node, helps to recognize nodes that have their effective @@ -458,7 +555,7 @@ final class State { DependencyGraphTransformationContext context) throws RepositoryException { this.conflictIds = conflictIds; - verbose = ConfigUtils.getBoolean(context.getSession(), false, CONFIG_PROP_VERBOSE); + this.verbosity = getVerbosity(context.getSession()); potentialAncestorIds = new HashSet<>(conflictIdCount * 2); resolvedIds = new HashMap<>(conflictIdCount * 2); items = new ArrayList<>(256); @@ -734,7 +831,8 @@ public static final class ConflictItem { // only for debugging/toString() to help identify the parent node(s) final Artifact artifact; - final DependencyNode node; + // is mutable as removeLosers will mutate it (if Verbosity==STANDARD) + DependencyNode node; int depth; diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java index 97b3342a3..6293dda08 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java @@ -156,7 +156,9 @@ protected boolean setVisited(DependencyNode node) { return visitedNodes.put(node, Boolean.TRUE) == null; } + @Override public abstract boolean visitEnter(DependencyNode node); + @Override public abstract boolean visitLeave(DependencyNode node); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java index 0da5649fa..ab1406e8e 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java @@ -66,6 +66,7 @@ protected DependencyNode clone(DependencyNode node) { return new DefaultDependencyNode(node); } + @Override public final boolean visitEnter(DependencyNode node) { boolean recurse = true; @@ -90,6 +91,7 @@ public final boolean visitEnter(DependencyNode node) { return recurse; } + @Override public final boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java similarity index 75% rename from maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java rename to maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java index e94ae2c32..60a12727f 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.resolver.examples.util; +package org.eclipse.aether.util.graph.visitor; -import java.io.PrintStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.function.Consumer; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.graph.Dependency; @@ -31,25 +31,27 @@ import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import static java.util.Objects.requireNonNull; + /** - * A dependency visitor that dumps the graph to the console. + * A dependency visitor that dumps the graph to any {@link Consumer}. Meant for diagnostic and testing, as + * it may output the graph to standard output, error or even some logging interface. + * + * @since 1.9.8 */ -public class ConsoleDependencyGraphDumper implements DependencyVisitor { +public class DependencyGraphDumper implements DependencyVisitor { - private final PrintStream out; + private final Consumer consumer; private final List childInfos = new ArrayList<>(); - public ConsoleDependencyGraphDumper() { - this(null); - } - - public ConsoleDependencyGraphDumper(PrintStream out) { - this.out = (out != null) ? out : System.out; + public DependencyGraphDumper(Consumer consumer) { + this.consumer = requireNonNull(consumer); } + @Override public boolean visitEnter(DependencyNode node) { - out.println(formatIndentation() + formatNode(node)); + consumer.accept(formatIndentation() + formatNode(node)); childInfos.add(new ChildInfo(node.getChildren().size())); return true; } @@ -84,19 +86,24 @@ private String formatNode(DependencyNode node) { buffer.append(" (scope managed from ").append(premanaged).append(")"); } DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER); - if (winner != null && !ArtifactIdUtils.equalsId(a, winner.getArtifact())) { - Artifact w = winner.getArtifact(); - buffer.append(" (conflicts with "); - if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) { - buffer.append(w.getVersion()); + if (winner != null) { + if (ArtifactIdUtils.equalsId(a, winner.getArtifact())) { + buffer.append(" (nearer exists)"); } else { - buffer.append(w); + Artifact w = winner.getArtifact(); + buffer.append(" (conflicts with "); + if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) { + buffer.append(w.getVersion()); + } else { + buffer.append(w); + } + buffer.append(")"); } - buffer.append(")"); } return buffer.toString(); } + @Override public boolean visitLeave(DependencyNode node) { if (!childInfos.isEmpty()) { childInfos.remove(childInfos.size() - 1); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java index 16a851028..202c2598e 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java @@ -26,7 +26,7 @@ /** * A dependency visitor that delegates to another visitor if nodes match a filter. Note that in case of a mismatching - * node, the children of that node are still visisted and presented to the filter. + * node, the children of that node are still visited and presented to the filter. */ public final class FilteringDependencyVisitor implements DependencyVisitor { @@ -69,6 +69,7 @@ public DependencyFilter getFilter() { return filter; } + @Override public boolean visitEnter(DependencyNode node) { boolean accept = filter == null || filter.accept(node, parents); @@ -83,6 +84,7 @@ public boolean visitEnter(DependencyNode node) { } } + @Override public boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java index 119323851..d0576ca90 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java @@ -85,6 +85,7 @@ public List> getPaths() { return paths; } + @Override public boolean visitEnter(DependencyNode node) { boolean accept = filter == null || filter.accept(node, parents); @@ -106,6 +107,7 @@ public boolean visitEnter(DependencyNode node) { return !hasDuplicateNodeInParent; } + @Override public boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java index b8ba0dae5..09dad67cc 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java @@ -42,11 +42,7 @@ public boolean visitEnter(DependencyNode node) { visits.push(visited); - if (visited) { - return false; - } - - return true; + return !visited; } @Override diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java index f8370b1a3..07841049c 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java @@ -21,7 +21,7 @@ import org.eclipse.aether.graph.DependencyNode; /** - * Generates a sequence of dependency nodes from a dependeny graph by traversing the graph in preorder. This visitor + * Generates a sequence of dependency nodes from a dependency graph by traversing the graph in preorder. This visitor * visits each node exactly once regardless how many paths within the dependency graph lead to the node such that the * resulting node sequence is free of duplicates. */ diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java index 94cc382ea..44b3759ca 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java @@ -50,6 +50,7 @@ public TreeDependencyVisitor(DependencyVisitor visitor) { visits = new Stack<>(); } + @Override public boolean visitEnter(DependencyNode node) { boolean visited = visitedNodes.put(node, Boolean.TRUE) != null; @@ -62,6 +63,7 @@ public boolean visitEnter(DependencyNode node) { return visitor.visitEnter(node); } + @Override public boolean visitLeave(DependencyNode node) { Boolean visited = visits.pop(); diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java index 01b288000..0486e6698 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.List; +import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.DefaultDependencyNode; @@ -30,9 +31,13 @@ import org.eclipse.aether.internal.test.util.TestUtils; import org.eclipse.aether.internal.test.util.TestVersion; import org.eclipse.aether.internal.test.util.TestVersionConstraint; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -77,6 +82,266 @@ public void versionClash() throws RepositoryException { assertSame(baz1Node, fooNode.getChildren().get(1)); } + @Test + public void versionClashForkedStandardVerbose() throws RepositoryException { + + // root -> impl1 -> api:1 + // |----> impl2 -> api:2 + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode impl1 = makeDependencyNode("some-group", "impl1", "1.0"); + DependencyNode impl2 = makeDependencyNode("some-group", "impl2", "1.0"); + DependencyNode api1 = makeDependencyNode("some-group", "api", "1.1"); + DependencyNode api2 = makeDependencyNode("some-group", "api", "1.0"); + + root.setChildren(mutableList(impl1, impl2)); + impl1.setChildren(mutableList(api1)); + impl2.setChildren(mutableList(api2)); + + DependencyNode transformedNode = versionRangeClash(root, ConflictResolver.Verbosity.STANDARD); + + assertSame(root, transformedNode); + assertEquals(2, root.getChildren().size()); + assertSame(impl1, root.getChildren().get(0)); + assertSame(impl2, root.getChildren().get(1)); + assertEquals(1, impl1.getChildren().size()); + assertSame(api1, impl1.getChildren().get(0)); + assertEquals(1, impl2.getChildren().size()); + assertConflictedButSameAsOriginal(api2, impl2.getChildren().get(0)); + } + + @Test + public void versionRangeClashAscOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashAscOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashAscOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(1)); + assertSame(c2, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(1)); + } + + @Test + public void versionRangeClashDescOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashDescOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashDescOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(1)); + } + + @Test + public void versionRangeClashMixedOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashMixedOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashMixedOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(1)); + } + + /** + * Conflict resolver in case of conflict replaces {@link DependencyNode} instances with copies to keep them + * stateful on different levels of graph and records conflict data. This method assert that two nodes do represent + * same dependency (same GAV, scope, optionality), but that original is not conflicted while current is. + */ + private void assertConflictedButSameAsOriginal(DependencyNode original, DependencyNode current) { + assertNotSame(original, current); + assertEquals( + original.getDependency().getArtifact(), current.getDependency().getArtifact()); + assertEquals( + original.getDependency().getScope(), current.getDependency().getScope()); + assertEquals( + original.getDependency().getOptional(), current.getDependency().getOptional()); + assertNull(original.getData().get(ConflictResolver.NODE_DATA_WINNER)); + assertNotNull(current.getData().get(ConflictResolver.NODE_DATA_WINNER)); + } + + private static final DependencyGraphDumper DUMPER_SOUT = new DependencyGraphDumper(System.out::println); + + /** + * Performs a verbose conflict resolution on passed in root. + */ + private DependencyNode versionRangeClash(DependencyNode root, ConflictResolver.Verbosity verbosity) + throws RepositoryException { + ConflictResolver resolver = makeDefaultResolver(); + + System.out.println(); + System.out.println("Input node:"); + root.accept(DUMPER_SOUT); + + DefaultRepositorySystemSession session = TestUtils.newSession(); + session.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, verbosity); + DependencyNode transformedRoot = resolver.transformGraph(root, TestUtils.newTransformationContext(session)); + + System.out.println(); + System.out.println("Transformed node:"); + transformedRoot.accept(DUMPER_SOUT); + + return transformedRoot; + } + @Test public void derivedScopeChange() throws RepositoryException { ConflictResolver resolver = makeDefaultResolver();