You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2022/04/04 15:27:22 UTC

[maven-resolver] branch MRESOLVER-248-collector-bf-df-coexists updated: Resurrect replaced DF as well

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch MRESOLVER-248-collector-bf-df-coexists
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/MRESOLVER-248-collector-bf-df-coexists by this push:
     new 9572e88b Resurrect replaced DF as well
9572e88b is described below

commit 9572e88b276df9acd960aed632bfcfe857539f0c
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Apr 4 17:26:58 2022 +0200

    Resurrect replaced DF as well
    
    Along with tests
---
 .../eclipse/aether/impl/guice/AetherModule.java    |  21 +-
 .../impl/collect/DefaultDependencyCollector.java   |   6 +-
 .../impl/collect/bf/BfDependencyCollector.java     |   3 +
 .../DfDependencyCollector.java}                    | 272 ++++-----
 .../impl/collect/df/DfDependencyCycle.java         |  88 +++
 .../aether/internal/impl/collect/df/NodeStack.java | 127 ++++
 .../impl/collect/df/DfDependencyCollectorTest.java | 649 +++++++++++++++++++++
 .../impl/collect/df/DfDependencyCycleTest.java     |  44 ++
 8 files changed, 1052 insertions(+), 158 deletions(-)

diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 7d7f9f0b..e539d539 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -50,6 +50,7 @@ import org.eclipse.aether.internal.impl.checksum.Sha512ChecksumAlgorithmFactory;
 import org.eclipse.aether.internal.impl.checksum.DefaultChecksumAlgorithmFactorySelector;
 import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
 import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
+import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
 import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
 import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactorySelector;
 import org.eclipse.aether.internal.impl.synccontext.named.SimpleNamedLockFactorySelector;
@@ -139,6 +140,8 @@ public class AetherModule
                 .to( DefaultDependencyCollector.class ).in( Singleton.class );
         bind( DependencyCollectorDelegate.class ).annotatedWith( Names.named( BfDependencyCollector.NAME ) )
                 .to( BfDependencyCollector.class ).in( Singleton.class );
+        bind( DependencyCollectorDelegate.class ).annotatedWith( Names.named( DfDependencyCollector.NAME ) )
+                .to( DfDependencyCollector.class ).in( Singleton.class );
 
         bind( Deployer.class ) //
                 .to( DefaultDeployer.class ).in( Singleton.class );
@@ -220,23 +223,25 @@ public class AetherModule
 
     @Provides
     @Singleton
-    Map<String, ProvidedChecksumsSource> provideChecksumSources(
-        @Named( FileProvidedChecksumsSource.NAME ) ProvidedChecksumsSource fileProvidedChecksumSource
+    Map<String, DependencyCollectorDelegate> dependencyCollectorDelegates(
+            @Named( BfDependencyCollector.NAME ) DependencyCollectorDelegate bf,
+            @Named( DfDependencyCollector.NAME ) DependencyCollectorDelegate df
     )
     {
-        Map<String, ProvidedChecksumsSource> providedChecksumsSource = new HashMap<>();
-        providedChecksumsSource.put( FileProvidedChecksumsSource.NAME, fileProvidedChecksumSource );
+        Map<String, DependencyCollectorDelegate> providedChecksumsSource = new HashMap<>();
+        providedChecksumsSource.put( BfDependencyCollector.NAME, bf );
+        providedChecksumsSource.put( DfDependencyCollector.NAME, df );
         return providedChecksumsSource;
     }
 
     @Provides
     @Singleton
-    Map<String, DependencyCollectorDelegate> dependencyCollectorDelegates(
-            @Named( BfDependencyCollector.NAME ) DependencyCollectorDelegate bf
+    Map<String, ProvidedChecksumsSource> provideChecksumSources(
+        @Named( FileProvidedChecksumsSource.NAME ) ProvidedChecksumsSource fileProvidedChecksumSource
     )
     {
-        Map<String, DependencyCollectorDelegate> providedChecksumsSource = new HashMap<>();
-        providedChecksumsSource.put( BfDependencyCollector.NAME, bf );
+        Map<String, ProvidedChecksumsSource> providedChecksumsSource = new HashMap<>();
+        providedChecksumsSource.put( FileProvidedChecksumsSource.NAME, fileProvidedChecksumSource );
         return providedChecksumsSource;
     }
 
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java
index 1c0b6e95..7b479835 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java
@@ -32,6 +32,7 @@ import org.eclipse.aether.collection.CollectResult;
 import org.eclipse.aether.collection.DependencyCollectionException;
 import org.eclipse.aether.impl.DependencyCollector;
 import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
+import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
 import org.eclipse.aether.spi.locator.Service;
 import org.eclipse.aether.spi.locator.ServiceLocator;
 import org.eclipse.aether.util.ConfigUtils;
@@ -48,7 +49,7 @@ public class DefaultDependencyCollector
 {
     private static final String CONFIG_PROP_COLLECTOR_IMPL = "aether.collector.impl";
 
-    private static final String DEFAULT_COLLECTOR_IMPL = BfDependencyCollector.NAME;
+    private static final String DEFAULT_COLLECTOR_IMPL = DfDependencyCollector.NAME;
 
     private final Map<String, DependencyCollectorDelegate> delegates;
 
@@ -74,7 +75,10 @@ public class DefaultDependencyCollector
     {
         BfDependencyCollector bf = new BfDependencyCollector();
         bf.initService( locator );
+        DfDependencyCollector df = new DfDependencyCollector();
+        df.initService( locator );
         this.delegates.put( BfDependencyCollector.NAME, bf );
+        this.delegates.put( DfDependencyCollector.NAME, df );
     }
 
     @Override
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
index 4836d635..837af528 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
@@ -84,6 +84,9 @@ import static java.util.Objects.requireNonNull;
 import static org.eclipse.aether.internal.impl.collect.bf.BfDependencyCycle.find;
 
 /**
+ * Breadth-first {@link org.eclipse.aether.impl.DependencyCollector}
+ *
+ * @since 1.8.0
  */
 @Singleton
 @Named( BfDependencyCollector.NAME )
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java
similarity index 80%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java
index 4836d635..528fc908 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect.bf;
+package org.eclipse.aether.internal.impl.collect.df;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,11 +19,6 @@ package org.eclipse.aether.internal.impl.collect.bf;
  * under the License.
  */
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -32,7 +27,11 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Queue;
+import static java.util.Objects.requireNonNull;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
 
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositoryException;
@@ -54,7 +53,6 @@ import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.graph.Exclusion;
 import org.eclipse.aether.impl.ArtifactDescriptorReader;
-import org.eclipse.aether.impl.DependencyResolutionSkipper;
 import org.eclipse.aether.impl.RemoteRepositoryManager;
 import org.eclipse.aether.impl.VersionRangeResolver;
 import org.eclipse.aether.internal.impl.collect.CachingArtifactTypeRegistry;
@@ -80,32 +78,17 @@ import org.eclipse.aether.version.Version;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static java.util.Objects.requireNonNull;
-import static org.eclipse.aether.internal.impl.collect.bf.BfDependencyCycle.find;
-
 /**
+ * Depth-first {@link org.eclipse.aether.impl.DependencyCollector} (the "original" default).
+ *
+ * @since 1.8.0
  */
 @Singleton
-@Named( BfDependencyCollector.NAME )
-public class BfDependencyCollector
+@Named( DfDependencyCollector.NAME )
+public class DfDependencyCollector
         implements DependencyCollectorDelegate, Service
 {
-    public static final String NAME = "bf";
-
-    /**
-     * The key in the repository session's {@link RepositorySystemSession#getConfigProperties()
-     * configuration properties} used to store a {@link Boolean} flag controlling the resolver's skip mode.
-     *
-     * @since 1.8.0
-     */
-    public static final String CONFIG_PROP_USE_SKIP = "aether.dependencyCollector.useSkip";
-
-    /**
-     * The default value for {@link #CONFIG_PROP_USE_SKIP}, {@code true}.
-     *
-     * @since 1.8.0
-     */
-    public static final boolean CONFIG_PROP_USE_SKIP_DEFAULT = true;
+    public static final String NAME = "df";
 
     private static final String CONFIG_PROP_MAX_EXCEPTIONS = "aether.dependencyCollector.maxExceptions";
 
@@ -115,7 +98,7 @@ public class BfDependencyCollector
 
     private static final int CONFIG_PROP_MAX_CYCLES_DEFAULT = 10;
 
-    private static final Logger LOGGER = LoggerFactory.getLogger( BfDependencyCollector.class );
+    private static final Logger LOGGER = LoggerFactory.getLogger( DfDependencyCollector.class );
 
     private RemoteRepositoryManager remoteRepositoryManager;
 
@@ -123,13 +106,13 @@ public class BfDependencyCollector
 
     private VersionRangeResolver versionRangeResolver;
 
-    public BfDependencyCollector()
+    public DfDependencyCollector()
     {
         // enables default constructor
     }
 
     @Inject
-    BfDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
+    DfDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
                            ArtifactDescriptorReader artifactDescriptorReader,
                            VersionRangeResolver versionRangeResolver )
     {
@@ -145,20 +128,20 @@ public class BfDependencyCollector
         setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
     }
 
-    public BfDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    public DfDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
     {
         this.remoteRepositoryManager =
                 requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
         return this;
     }
 
-    public BfDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
+    public DfDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
     {
         descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
         return this;
     }
 
-    public BfDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
+    public DfDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
     {
         this.versionRangeResolver =
                 requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
@@ -173,14 +156,6 @@ public class BfDependencyCollector
         requireNonNull( request, "request cannot be null" );
         session = optimizeSession( session );
 
-        boolean useSkip = ConfigUtils.getBoolean(
-                session, CONFIG_PROP_USE_SKIP_DEFAULT, CONFIG_PROP_USE_SKIP
-        );
-        if ( useSkip )
-        {
-            LOGGER.debug( "Collector skip mode enabled" );
-        }
-
         RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
 
         CollectResult result = new CollectResult( request );
@@ -278,40 +253,23 @@ public class BfDependencyCollector
         {
             DataPool pool = new DataPool( session );
 
+            NodeStack nodes = new NodeStack();
+            nodes.push( node );
+
             DefaultDependencyCollectionContext context =
                 new DefaultDependencyCollectionContext( session, request.getRootArtifact(), root, managedDependencies );
 
             DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
 
-            Args args =
-                    new Args( session, trace, pool, context, versionContext, request,
-                            useSkip ? new DefaultDependencyResolutionSkipper()
-                                    : NeverDependencyResolutionSkipper.INSTANCE );
+            Args args = new Args( session, trace, pool, nodes, context, versionContext, request );
             Results results = new Results( result, session );
 
-            DependencySelector rootDepSelector =
-                    depSelector != null ? depSelector.deriveChildSelector( context ) : null;
-            DependencyManager rootDepManager = depManager != null ? depManager.deriveChildManager( context ) : null;
-            DependencyTraverser rootDepTraverser =
-                    depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null;
-            VersionFilter rootVerFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null;
-
-            List<DependencyNode> parents = Collections.singletonList( node );
-            for ( Dependency dependency : dependencies )
-            {
-                args.dependencyProcessingQueue.add(
-                        new BfProcessingContext( rootDepSelector, rootDepManager, rootDepTraverser,
-                                rootVerFilter, repositories, managedDependencies, parents,
-                                dependency ) );
-            }
-
-            while ( !args.dependencyProcessingQueue.isEmpty() )
-            {
-                processDependency( args, results, args.dependencyProcessingQueue.remove(), Collections.emptyList(),
-                        false );
-            }
+            process( args, results, dependencies, repositories,
+                     depSelector != null ? depSelector.deriveChildSelector( context ) : null,
+                     depManager != null ? depManager.deriveChildManager( context ) : null,
+                     depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null,
+                     verFilter != null ? verFilter.deriveChildFilter( context ) : null );
 
-            args.skipper.report();
             errorPath = results.errorPath;
         }
 
@@ -395,73 +353,90 @@ public class BfDependencyCollector
     }
 
     @SuppressWarnings( "checkstyle:parameternumber" )
-    private void processDependency( Args args, Results results, BfProcessingContext context,
+    private void process( final Args args, Results results, List<Dependency> dependencies,
+                          List<RemoteRepository> repositories, DependencySelector depSelector,
+                          DependencyManager depManager, DependencyTraverser depTraverser, VersionFilter verFilter )
+    {
+        for ( Dependency dependency : dependencies )
+        {
+            processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter,
+                               dependency );
+        }
+    }
+
+    @SuppressWarnings( "checkstyle:parameternumber" )
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency )
+    {
+
+        List<Artifact> relocations = Collections.emptyList();
+        processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, dependency,
+                           relocations, false );
+    }
+
+    @SuppressWarnings( "checkstyle:parameternumber" )
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency,
                                     List<Artifact> relocations, boolean disableVersionManagement )
     {
 
-        if ( context.depSelector != null && !context.depSelector.selectDependency( context.dependency ) )
+        if ( depSelector != null && !depSelector.selectDependency( dependency ) )
         {
             return;
         }
 
         PremanagedDependency preManaged =
-                PremanagedDependency.create( context.depManager, context.dependency, disableVersionManagement,
-                        args.premanagedState );
-        Dependency dependency = preManaged.managedDependency;
+            PremanagedDependency.create( depManager, dependency, disableVersionManagement, args.premanagedState );
+        dependency = preManaged.managedDependency;
 
         boolean noDescriptor = isLackingDescriptor( dependency.getArtifact() );
 
-        boolean traverse =
-                !noDescriptor && ( context.depTraverser == null || context.depTraverser.traverseDependency(
-                        dependency ) );
+        boolean traverse = !noDescriptor && ( depTraverser == null || depTraverser.traverseDependency( dependency ) );
 
         List<? extends Version> versions;
         VersionRangeResult rangeResult;
         try
         {
-            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, context.repositories, dependency );
+            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, repositories, dependency );
 
             rangeResult = cachedResolveRangeResult( rangeRequest, args.pool, args.session );
 
-            versions = filterVersions( dependency, rangeResult, context.verFilter, args.versionContext );
+            versions = filterVersions( dependency, rangeResult, verFilter, args.versionContext );
         }
         catch ( VersionRangeResolutionException e )
         {
-            results.addException( dependency, e, context.parents );
+            results.addException( dependency, e, args.nodes );
             return;
         }
 
-        //Resolve newer version first to maximize benefits of skipper
-        Collections.reverse( versions );
         for ( Version version : versions )
         {
             Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
             Dependency d = dependency.setArtifact( originalArtifact );
 
-            ArtifactDescriptorRequest descriptorRequest =
-                    createArtifactDescriptorRequest( args, context.repositories, d );
+            ArtifactDescriptorRequest descriptorRequest = createArtifactDescriptorRequest( args, repositories, d );
 
             final ArtifactDescriptorResult descriptorResult =
-                    noDescriptor
-                            ? new ArtifactDescriptorResult( descriptorRequest )
-                            : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session,
-                                    context.withDependency( d ), results );
-
+                getArtifactDescriptorResult( args, results, noDescriptor, d, descriptorRequest );
             if ( descriptorResult != null )
             {
                 d = d.setArtifact( descriptorResult.getArtifact() );
 
-                int cycleEntry = find( context.parents, d.getArtifact() );
+                DependencyNode node = args.nodes.top();
+
+                int cycleEntry = args.nodes.find( d.getArtifact() );
                 if ( cycleEntry >= 0 )
                 {
-                    results.addCycle( context.parents, cycleEntry, d );
-                    DependencyNode cycleNode = context.parents.get( cycleEntry );
+                    results.addCycle( args.nodes, cycleEntry, d );
+                    DependencyNode cycleNode = args.nodes.get( cycleEntry );
                     if ( cycleNode.getDependency() != null )
                     {
                         DefaultDependencyNode child =
-                                createDependencyNode( relocations, preManaged, rangeResult, version, d,
-                                        descriptorResult, cycleNode );
-                        context.getParent().getChildren().add( child );
+                            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult,
+                                                  cycleNode );
+                        node.getChildren().add( child );
                         continue;
                     }
                 }
@@ -472,8 +447,8 @@ public class BfDependencyCollector
                         originalArtifact.getGroupId().equals( d.getArtifact().getGroupId() )
                             && originalArtifact.getArtifactId().equals( d.getArtifact().getArtifactId() );
 
-                    processDependency( args, results, context.withDependency( d ), descriptorResult.getRelocations(),
-                            disableVersionManagementSubsequently );
+                    processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                       descriptorResult.getRelocations(), disableVersionManagementSubsequently );
                     return;
                 }
                 else
@@ -481,77 +456,69 @@ public class BfDependencyCollector
                     d = args.pool.intern( d.setArtifact( args.pool.intern( d.getArtifact() ) ) );
 
                     List<RemoteRepository> repos =
-                        getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
+                        getRemoteRepositories( rangeResult.getRepository( version ), repositories );
 
                     DefaultDependencyNode child =
                         createDependencyNode( relocations, preManaged, rangeResult, version, d,
                                               descriptorResult.getAliases(), repos, args.request.getRequestContext() );
 
-                    context.getParent().getChildren().add( child );
+                    node.getChildren().add( child );
 
                     boolean recurse = traverse && !descriptorResult.getDependencies().isEmpty();
                     if ( recurse )
                     {
-                        doRecurse( args, context.withDependency( d ), descriptorResult, child );
+                        doRecurse( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                   descriptorResult, child );
                     }
                 }
             }
             else
             {
+                DependencyNode node = args.nodes.top();
                 List<RemoteRepository> repos =
-                    getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
+                    getRemoteRepositories( rangeResult.getRepository( version ), repositories );
                 DefaultDependencyNode child =
                     createDependencyNode( relocations, preManaged, rangeResult, version, d, null, repos,
                                           args.request.getRequestContext() );
-                context.getParent().getChildren().add( child );
+                node.getChildren().add( child );
             }
         }
     }
 
     @SuppressWarnings( "checkstyle:parameternumber" )
-    private void doRecurse( Args args, BfProcessingContext parentContext,
+    private void doRecurse( Args args, Results results, List<RemoteRepository> repositories,
+                            DependencySelector depSelector, DependencyManager depManager,
+                            DependencyTraverser depTraverser, VersionFilter verFilter, Dependency d,
                             ArtifactDescriptorResult descriptorResult, DefaultDependencyNode child )
     {
         DefaultDependencyCollectionContext context = args.collectionContext;
-        context.set( parentContext.dependency, descriptorResult.getManagedDependencies() );
+        context.set( d, descriptorResult.getManagedDependencies() );
 
-        DependencySelector childSelector =
-                parentContext.depSelector != null ? parentContext.depSelector.deriveChildSelector( context ) : null;
-        DependencyManager childManager =
-                parentContext.depManager != null ? parentContext.depManager.deriveChildManager( context ) : null;
-        DependencyTraverser childTraverser =
-                parentContext.depTraverser != null ? parentContext.depTraverser.deriveChildTraverser( context ) : null;
-        VersionFilter childFilter =
-                parentContext.verFilter != null ? parentContext.verFilter.deriveChildFilter( context ) : null;
+        DependencySelector childSelector = depSelector != null ? depSelector.deriveChildSelector( context ) : null;
+        DependencyManager childManager = depManager != null ? depManager.deriveChildManager( context ) : null;
+        DependencyTraverser childTraverser = depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null;
+        VersionFilter childFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null;
 
         final List<RemoteRepository> childRepos =
-                args.ignoreRepos
-                        ? parentContext.repositories
-                        : remoteRepositoryManager.aggregateRepositories( args.session, parentContext.repositories,
-                        descriptorResult.getRepositories(), true );
+            args.ignoreRepos
+                ? repositories
+                : remoteRepositoryManager.aggregateRepositories( args.session, repositories,
+                                                                 descriptorResult.getRepositories(), true );
 
         Object key =
-                args.pool.toKey( parentContext.dependency.getArtifact(), childRepos, childSelector, childManager,
-                        childTraverser, childFilter );
+            args.pool.toKey( d.getArtifact(), childRepos, childSelector, childManager, childTraverser, childFilter );
 
         List<DependencyNode> children = args.pool.getChildren( key );
         if ( children == null )
         {
-            boolean skipResolution = args.skipper.skipResolution( child, parentContext.parents );
-            if ( !skipResolution )
-            {
-                List<DependencyNode> parents = new ArrayList<>( parentContext.parents.size() + 1 );
-                parents.addAll( parentContext.parents );
-                parents.add( child );
-                for ( Dependency dependency : descriptorResult.getDependencies() )
-                {
-                    args.dependencyProcessingQueue.add(
-                            new BfProcessingContext( childSelector, childManager, childTraverser, childFilter,
-                                    childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
-                }
-                args.pool.putChildren( key, child.getChildren() );
-                args.skipper.cache( child, parents );
-            }
+            args.pool.putChildren( key, child.getChildren() );
+
+            args.nodes.push( child );
+
+            process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager,
+                     childTraverser, childFilter );
+
+            args.nodes.pop();
         }
         else
         {
@@ -559,11 +526,20 @@ public class BfDependencyCollector
         }
     }
 
+    private ArtifactDescriptorResult getArtifactDescriptorResult( Args args, Results results, boolean noDescriptor,
+                                                                  Dependency d,
+                                                                  ArtifactDescriptorRequest descriptorRequest )
+    {
+        return noDescriptor
+                   ? new ArtifactDescriptorResult( descriptorRequest )
+                   : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session, d, results, args );
+
+    }
+
     private ArtifactDescriptorResult resolveCachedArtifactDescriptor( DataPool pool,
                                                                       ArtifactDescriptorRequest descriptorRequest,
-                                                                      RepositorySystemSession session,
-                                                                      BfProcessingContext context,
-                                                                      Results results )
+                                                                      RepositorySystemSession session, Dependency d,
+                                                                      Results results, Args args )
     {
         Object key = pool.toKey( descriptorRequest );
         ArtifactDescriptorResult descriptorResult = pool.getDescriptor( key, descriptorRequest );
@@ -576,7 +552,7 @@ public class BfDependencyCollector
             }
             catch ( ArtifactDescriptorException e )
             {
-                results.addException( context.dependency, e, context.parents );
+                results.addException( d, e, args.nodes );
                 pool.putDescriptor( key, e );
                 return null;
             }
@@ -731,7 +707,7 @@ public class BfDependencyCollector
 
         final DataPool pool;
 
-        final Queue<BfProcessingContext> dependencyProcessingQueue = new ArrayDeque<>( 128 );
+        final NodeStack nodes;
 
         final DefaultDependencyCollectionContext collectionContext;
 
@@ -739,11 +715,9 @@ public class BfDependencyCollector
 
         final CollectRequest request;
 
-        final DependencyResolutionSkipper skipper;
-
-        Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
+        Args( RepositorySystemSession session, RequestTrace trace, DataPool pool, NodeStack nodes,
                      DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
-                     CollectRequest request, DependencyResolutionSkipper skipper )
+                     CollectRequest request )
         {
             this.session = session;
             this.request = request;
@@ -751,9 +725,9 @@ public class BfDependencyCollector
             this.premanagedState = ConfigUtils.getBoolean( session, false, DependencyManagerUtils.CONFIG_PROP_VERBOSE );
             this.trace = trace;
             this.pool = pool;
+            this.nodes = nodes;
             this.collectionContext = collectionContext;
             this.versionContext = versionContext;
-            this.skipper = skipper;
         }
 
     }
@@ -779,7 +753,7 @@ public class BfDependencyCollector
             maxCycles = ConfigUtils.getInteger( session, CONFIG_PROP_MAX_CYCLES_DEFAULT, CONFIG_PROP_MAX_CYCLES );
         }
 
-        public void addException( Dependency dependency, Exception e, List<DependencyNode> nodes )
+        public void addException( Dependency dependency, Exception e, NodeStack nodes )
         {
             if ( maxExceptions < 0 || result.getExceptions().size() < maxExceptions )
             {
@@ -787,13 +761,13 @@ public class BfDependencyCollector
                 if ( errorPath == null )
                 {
                     StringBuilder buffer = new StringBuilder( 256 );
-                    for ( DependencyNode node : nodes )
+                    for ( int i = 0; i < nodes.size(); i++ )
                     {
                         if ( buffer.length() > 0 )
                         {
                             buffer.append( " -> " );
                         }
-                        Dependency dep = node.getDependency();
+                        Dependency dep = nodes.get( i ).getDependency();
                         if ( dep != null )
                         {
                             buffer.append( dep.getArtifact() );
@@ -809,11 +783,11 @@ public class BfDependencyCollector
             }
         }
 
-        public void addCycle( List<DependencyNode> nodes, int cycleEntry, Dependency dependency )
+        public void addCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
         {
             if ( maxCycles < 0 || result.getCycles().size() < maxCycles )
             {
-                result.addCycle( new BfDependencyCycle( nodes, cycleEntry, dependency ) );
+                result.addCycle( new DfDependencyCycle( nodes, cycleEntry, dependency ) );
             }
         }
 
@@ -935,4 +909,4 @@ public class BfDependencyCollector
 
     }
 
-}
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycle.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycle.java
new file mode 100644
index 00000000..ff8d639d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycle.java
@@ -0,0 +1,88 @@
+package org.eclipse.aether.internal.impl.collect.df;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+
+/**
+ * @see DfDependencyCollector
+ */
+final class DfDependencyCycle
+    implements DependencyCycle
+{
+
+    private final List<Dependency> dependencies;
+
+    private final int cycleEntry;
+
+    DfDependencyCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
+    {
+        // skip root node unless it actually has a dependency or is considered the cycle entry (due to its label)
+        int offset = ( cycleEntry > 0 && nodes.get( 0 ).getDependency() == null ) ? 1 : 0;
+        Dependency[] dependencies = new Dependency[nodes.size() - offset + 1];
+        for ( int i = 0, n = dependencies.length - 1; i < n; i++ )
+        {
+            DependencyNode node = nodes.get( i + offset );
+            dependencies[i] = node.getDependency();
+            // when cycle starts at root artifact as opposed to root dependency, synthesize a dependency
+            if ( dependencies[i] == null )
+            {
+                dependencies[i] = new Dependency( node.getArtifact(), null );
+            }
+        }
+        dependencies[dependencies.length - 1] = dependency;
+        this.dependencies = Collections.unmodifiableList( Arrays.asList( dependencies ) );
+        this.cycleEntry = cycleEntry;
+    }
+
+    public List<Dependency> getPrecedingDependencies()
+    {
+        return dependencies.subList( 0, cycleEntry );
+    }
+
+    public List<Dependency> getCyclicDependencies()
+    {
+        return dependencies.subList( cycleEntry, dependencies.size() );
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        int i = 0;
+        for ( Dependency dependency : dependencies )
+        {
+            if ( i++ > 0 )
+            {
+                buffer.append( " -> " );
+            }
+            buffer.append( ArtifactIdUtils.toVersionlessId( dependency.getArtifact() ) );
+        }
+        return buffer.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java
new file mode 100644
index 00000000..8266aefd
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether.internal.impl.collect.df;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * @see DfDependencyCollector
+ */
+final class NodeStack
+{
+
+    @SuppressWarnings( {"unchecked", "checkstyle:magicnumber" } )
+    // CHECKSTYLE_OFF: MagicNumber
+    private DependencyNode[] nodes = new DependencyNode[96];
+    // CHECKSTYLE_ON: MagicNumber
+
+    private int size;
+
+    public DependencyNode top()
+    {
+        if ( size <= 0 )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        return nodes[size - 1];
+    }
+
+    public void push( DependencyNode node )
+    {
+        if ( size >= nodes.length )
+        {
+            DependencyNode[] tmp = new DependencyNode[size + 64];
+            System.arraycopy( nodes, 0, tmp, 0, nodes.length );
+            nodes = tmp;
+        }
+        nodes[size++] = node;
+    }
+
+    public void pop()
+    {
+        if ( size <= 0 )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        size--;
+    }
+
+    public int find( Artifact artifact )
+    {
+        for ( int i = size - 1; i >= 0; i-- )
+        {
+            DependencyNode node = nodes[i];
+
+            Artifact a = node.getArtifact();
+            if ( a == null )
+            {
+                break;
+            }
+
+            if ( !a.getArtifactId().equals( artifact.getArtifactId() ) )
+            {
+                continue;
+            }
+            if ( !a.getGroupId().equals( artifact.getGroupId() ) )
+            {
+                continue;
+            }
+            if ( !a.getExtension().equals( artifact.getExtension() ) )
+            {
+                continue;
+            }
+            if ( !a.getClassifier().equals( artifact.getClassifier() ) )
+            {
+                continue;
+            }
+            /*
+             * NOTE: While a:1 and a:2 are technically different artifacts, we want to consider the path a:2 -> b:2 ->
+             * a:1 a cycle in the current context. The artifacts themselves might not form a cycle but their producing
+             * projects surely do. Furthermore, conflict resolution will always have to consider a:1 a loser (otherwise
+             * its ancestor a:2 would get pruned and so would a:1) so there is no point in building the sub graph of
+             * a:1.
+             */
+
+            return i;
+        }
+
+        return -1;
+    }
+
+    public int size()
+    {
+        return size;
+    }
+
+    public DependencyNode get( int index )
+    {
+        return nodes[index];
+    }
+
+    @Override
+    public String toString()
+    {
+        return Arrays.toString( nodes );
+    }
+
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..b641bbd6
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java
@@ -0,0 +1,649 @@
+package org.eclipse.aether.internal.impl.collect.df;
+
+/*
+ * 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.
+ */
+import static java.util.Objects.requireNonNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.internal.impl.IniArtifactDescriptorReader;
+import org.eclipse.aether.internal.impl.StubRemoteRepositoryManager;
+import org.eclipse.aether.internal.impl.StubVersionRangeResolver;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+import org.eclipse.aether.util.graph.manager.ClassicDependencyManager;
+import org.eclipse.aether.util.graph.manager.DefaultDependencyManager;
+import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
+import org.eclipse.aether.util.graph.manager.TransitiveDependencyManager;
+import org.eclipse.aether.util.graph.version.HighestVersionFilter;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DfDependencyCollectorTest
+{
+
+    private DfDependencyCollector collector;
+
+    private DefaultRepositorySystemSession session;
+
+    private DependencyGraphParser parser;
+
+    private RemoteRepository repository;
+
+    private IniArtifactDescriptorReader newReader( String prefix )
+    {
+        return new IniArtifactDescriptorReader( "artifact-descriptions/" + prefix );
+    }
+
+    private Dependency newDep( String coords )
+    {
+        return newDep( coords, "" );
+    }
+
+    private Dependency newDep( String coords, String scope )
+    {
+        return new Dependency( new DefaultArtifact( coords ), scope );
+    }
+
+    @Before
+    public void setup()
+    {
+        session = TestUtils.newSession();
+
+        collector = new DfDependencyCollector();
+        collector.setArtifactDescriptorReader( newReader( "" ) );
+        collector.setVersionRangeResolver( new StubVersionRangeResolver() );
+        collector.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
+
+        parser = new DependencyGraphParser( "artifact-descriptions/" );
+
+        repository = new RemoteRepository.Builder( "id", "default", "file:///" ).build();
+    }
+
+    private static void assertEqualSubtree( DependencyNode expected, DependencyNode actual )
+    {
+        assertEqualSubtree( expected, actual, new LinkedList<DependencyNode>() );
+    }
+
+    private static void assertEqualSubtree( DependencyNode expected, DependencyNode actual,
+                                            LinkedList<DependencyNode> parents )
+    {
+        assertEquals( "path: " + parents, expected.getDependency(), actual.getDependency() );
+
+        if ( actual.getDependency() != null )
+        {
+            Artifact artifact = actual.getDependency().getArtifact();
+            for ( DependencyNode parent : parents )
+            {
+                if ( parent.getDependency() != null && artifact.equals( parent.getDependency().getArtifact() ) )
+                {
+                    return;
+                }
+            }
+        }
+
+        parents.addLast( expected );
+
+        assertEquals( "path: " + parents + ", expected: " + expected.getChildren() + ", actual: "
+                          + actual.getChildren(), expected.getChildren().size(), actual.getChildren().size() );
+
+        Iterator<DependencyNode> iterator1 = expected.getChildren().iterator();
+        Iterator<DependencyNode> iterator2 = actual.getChildren().iterator();
+
+        while ( iterator1.hasNext() )
+        {
+            assertEqualSubtree( iterator1.next(), iterator2.next(), parents );
+        }
+
+        parents.removeLast();
+    }
+
+    private Dependency dep( DependencyNode root, int... coords )
+    {
+        return path( root, coords ).getDependency();
+    }
+
+    private DependencyNode path( DependencyNode root, int... coords )
+    {
+        try
+        {
+            DependencyNode node = root;
+            for ( int coord : coords )
+            {
+                node = node.getChildren().get( coord );
+            }
+
+            return node;
+        }
+        catch ( IndexOutOfBoundsException | NullPointerException e )
+        {
+            throw new IllegalArgumentException( "illegal coordinates for child", e );
+        }
+    }
+
+    @Test
+    public void testSimpleCollection()
+        throws DependencyCollectionException
+    {
+        Dependency dependency = newDep( "gid:aid:ext:ver", "compile" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+        Dependency newDependency = root.getDependency();
+
+        assertEquals( dependency, newDependency );
+        assertEquals( dependency.getArtifact(), newDependency.getArtifact() );
+
+        assertEquals( 1, root.getChildren().size() );
+
+        Dependency expect = newDep( "gid:aid2:ext:ver", "compile" );
+        assertEquals( expect, root.getChildren().get( 0 ).getDependency() );
+    }
+
+    @Test
+    public void testMissingDependencyDescription()
+    {
+        CollectRequest request =
+            new CollectRequest( newDep( "missing:description:ext:ver" ), Arrays.asList( repository ) );
+        try
+        {
+            collector.collectDependencies( session, request );
+            fail( "expected exception" );
+        }
+        catch ( DependencyCollectionException e )
+        {
+            CollectResult result = e.getResult();
+            assertSame( request, result.getRequest() );
+            assertNotNull( result.getExceptions() );
+            assertEquals( 1, result.getExceptions().size() );
+
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactDescriptorException );
+
+            assertEquals( request.getRoot(), result.getRoot().getDependency() );
+        }
+    }
+
+    @Test
+    public void testDuplicates()
+        throws DependencyCollectionException
+    {
+        Dependency dependency = newDep( "duplicate:transitive:ext:dependency" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+        Dependency newDependency = root.getDependency();
+
+        assertEquals( dependency, newDependency );
+        assertEquals( dependency.getArtifact(), newDependency.getArtifact() );
+
+        assertEquals( 2, root.getChildren().size() );
+
+        Dependency dep = newDep( "gid:aid:ext:ver", "compile" );
+        assertEquals( dep, dep( root, 0 ) );
+
+        dep = newDep( "gid:aid2:ext:ver", "compile" );
+        assertEquals( dep, dep( root, 1 ) );
+        assertEquals( dep, dep( root, 0, 0 ) );
+        assertEquals( dep( root, 1 ), dep( root, 0, 0 ) );
+    }
+
+    @Test
+    public void testEqualSubtree()
+        throws IOException, DependencyCollectionException
+    {
+        DependencyNode root = parser.parseResource( "expectedSubtreeComparisonResult.txt" );
+        Dependency dependency = root.getDependency();
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEqualSubtree( root, result.getRoot() );
+    }
+
+    @Test
+    public void testCyclicDependencies()
+        throws Exception
+    {
+        DependencyNode root = parser.parseResource( "cycle.txt" );
+        CollectRequest request = new CollectRequest( root.getDependency(), Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEqualSubtree( root, result.getRoot() );
+    }
+
+    @Test
+    public void testCyclicDependenciesBig()
+        throws Exception
+    {
+        CollectRequest request = new CollectRequest( newDep( "1:2:pom:5.50-SNAPSHOT" ), Arrays.asList( repository ) );
+        collector.setArtifactDescriptorReader( newReader( "cycle-big/" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertNotNull( result.getRoot() );
+        // we only care about the performance here, this test must not hang or run out of mem
+    }
+
+    @Test
+    public void testCyclicProjects()
+        throws Exception
+    {
+        CollectRequest request = new CollectRequest( newDep( "test:a:2" ), Arrays.asList( repository ) );
+        collector.setArtifactDescriptorReader( newReader( "versionless-cycle/" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode root = result.getRoot();
+        DependencyNode a1 = path( root, 0, 0 );
+        assertEquals( "a", a1.getArtifact().getArtifactId() );
+        assertEquals( "1", a1.getArtifact().getVersion() );
+        for ( DependencyNode child : a1.getChildren() )
+        {
+            assertNotEquals( "1", child.getArtifact().getVersion() );
+        }
+
+        assertEquals( 1, result.getCycles().size() );
+        DependencyCycle cycle = result.getCycles().get( 0 );
+        assertEquals( Arrays.asList(), cycle.getPrecedingDependencies() );
+        assertEquals( Arrays.asList( root.getDependency(), path( root, 0 ).getDependency(), a1.getDependency() ),
+                      cycle.getCyclicDependencies() );
+    }
+
+    @Test
+    public void testCyclicProjects_ConsiderLabelOfRootlessGraph()
+        throws Exception
+    {
+        Dependency dep = newDep( "gid:aid:ver", "compile" );
+        CollectRequest request =
+            new CollectRequest().addDependency( dep ).addRepository( repository ).setRootArtifact( dep.getArtifact() );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode root = result.getRoot();
+        DependencyNode a1 = root.getChildren().get( 0 );
+        assertEquals( "aid", a1.getArtifact().getArtifactId() );
+        assertEquals( "ver", a1.getArtifact().getVersion() );
+        DependencyNode a2 = a1.getChildren().get( 0 );
+        assertEquals( "aid2", a2.getArtifact().getArtifactId() );
+        assertEquals( "ver", a2.getArtifact().getVersion() );
+
+        assertEquals( 1, result.getCycles().size() );
+        DependencyCycle cycle = result.getCycles().get( 0 );
+        assertEquals( Arrays.asList(), cycle.getPrecedingDependencies() );
+        assertEquals( Arrays.asList( new Dependency( dep.getArtifact(), null ), a1.getDependency() ),
+                      cycle.getCyclicDependencies() );
+    }
+
+    @Test
+    public void testPartialResultOnError()
+        throws IOException
+    {
+        DependencyNode root = parser.parseResource( "expectedPartialSubtreeOnError.txt" );
+
+        Dependency dependency = root.getDependency();
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result;
+        try
+        {
+            result = collector.collectDependencies( session, request );
+            fail( "expected exception " );
+        }
+        catch ( DependencyCollectionException e )
+        {
+            result = e.getResult();
+
+            assertSame( request, result.getRequest() );
+            assertNotNull( result.getExceptions() );
+            assertEquals( 1, result.getExceptions().size() );
+
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactDescriptorException );
+
+            assertEqualSubtree( root, result.getRoot() );
+        }
+    }
+
+    @Test
+    public void testCollectMultipleDependencies()
+        throws DependencyCollectionException
+    {
+        Dependency root1 = newDep( "gid:aid:ext:ver", "compile" );
+        Dependency root2 = newDep( "gid:aid2:ext:ver", "compile" );
+        List<Dependency> dependencies = Arrays.asList( root1, root2 );
+        CollectRequest request = new CollectRequest( dependencies, null, Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+        assertEquals( 2, result.getRoot().getChildren().size() );
+        assertEquals( root1, dep( result.getRoot(), 0 ) );
+
+        assertEquals( 1, path( result.getRoot(), 0 ).getChildren().size() );
+        assertEquals( root2, dep( result.getRoot(), 0, 0 ) );
+
+        assertEquals( 0, path( result.getRoot(), 1 ).getChildren().size() );
+        assertEquals( root2, dep( result.getRoot(), 1 ) );
+    }
+
+    @Test
+    public void testArtifactDescriptorResolutionNotRestrictedToRepoHostingSelectedVersion()
+        throws Exception
+    {
+        RemoteRepository repo2 = new RemoteRepository.Builder( "test", "default", "file:///" ).build();
+
+        final List<RemoteRepository> repos = new ArrayList<>();
+
+        collector.setArtifactDescriptorReader( new ArtifactDescriptorReader()
+        {
+            public ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session,
+                                                                    ArtifactDescriptorRequest request )
+            {
+                repos.addAll( request.getRepositories() );
+                return new ArtifactDescriptorResult( request );
+            }
+        } );
+
+        List<Dependency> dependencies = Arrays.asList( newDep( "verrange:parent:jar:1[1,)", "compile" ) );
+        CollectRequest request = new CollectRequest( dependencies, null, Arrays.asList( repository, repo2 ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+        assertEquals( 2, repos.size() );
+        assertEquals( "id", repos.get( 0 ).getId() );
+        assertEquals( "test", repos.get( 1 ).getId() );
+    }
+
+    @Test
+    public void testManagedVersionScope()
+        throws DependencyCollectionException
+    {
+        Dependency dependency = newDep( "managed:aid:ext:ver" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        session.setDependencyManager( new ClassicDependencyManager() );
+
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+
+        assertEquals( dependency, dep( root ) );
+        assertEquals( dependency.getArtifact(), dep( root ).getArtifact() );
+
+        assertEquals( 1, root.getChildren().size() );
+        Dependency expect = newDep( "gid:aid:ext:ver", "compile" );
+        assertEquals( expect, dep( root, 0 ) );
+
+        assertEquals( 1, path( root, 0 ).getChildren().size() );
+        expect = newDep( "gid:aid2:ext:managedVersion", "managedScope" );
+        assertEquals( expect, dep( root, 0, 0 ) );
+    }
+
+    @Test
+    public void testDependencyManagement()
+        throws IOException, DependencyCollectionException
+    {
+        collector.setArtifactDescriptorReader( newReader( "managed/" ) );
+
+        DependencyNode root = parser.parseResource( "expectedSubtreeComparisonResult.txt" );
+        TestDependencyManager depMgmt = new TestDependencyManager();
+        depMgmt.add( dep( root, 0 ), "managed", null, null );
+        depMgmt.add( dep( root, 0, 1 ), "managed", "managed", null );
+        depMgmt.add( dep( root, 1 ), null, null, "managed" );
+        session.setDependencyManager( depMgmt );
+
+        // collect result will differ from expectedSubtreeComparisonResult.txt
+        // set localPath -> no dependency traversal
+        CollectRequest request = new CollectRequest( dep( root ), Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        DependencyNode node = result.getRoot();
+        assertEquals( "managed", dep( node, 0, 1 ).getArtifact().getVersion() );
+        assertEquals( "managed", dep( node, 0, 1 ).getScope() );
+
+        assertEquals( "managed", dep( node, 1 ).getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) );
+        assertEquals( "managed", dep( node, 0, 0 ).getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) );
+    }
+
+    @Test
+    public void testDependencyManagement_VerboseMode()
+        throws Exception
+    {
+        String depId = "gid:aid2:ext";
+        TestDependencyManager depMgmt = new TestDependencyManager();
+        depMgmt.version( depId, "managedVersion" );
+        depMgmt.scope( depId, "managedScope" );
+        depMgmt.optional( depId, Boolean.TRUE );
+        depMgmt.path( depId, "managedPath" );
+        depMgmt.exclusions( depId, new Exclusion( "gid", "aid", "*", "*" ) );
+        session.setDependencyManager( depMgmt );
+        session.setConfigProperty( DependencyManagerUtils.CONFIG_PROP_VERBOSE, Boolean.TRUE );
+
+        CollectRequest request = new CollectRequest().setRoot( newDep( "gid:aid:ver" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode node = result.getRoot().getChildren().get( 0 );
+        assertEquals( DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE | DependencyNode.MANAGED_OPTIONAL
+            | DependencyNode.MANAGED_PROPERTIES | DependencyNode.MANAGED_EXCLUSIONS, node.getManagedBits() );
+        assertEquals( "ver", DependencyManagerUtils.getPremanagedVersion( node ) );
+        assertEquals( "compile", DependencyManagerUtils.getPremanagedScope( node ) );
+        assertEquals( Boolean.FALSE, DependencyManagerUtils.getPremanagedOptional( node ) );
+    }
+
+    @Test
+    public void testDependencyManagement_TransitiveDependencyManager()
+            throws DependencyCollectionException, IOException
+    {
+        collector.setArtifactDescriptorReader( newReader( "managed/" ) );
+        parser = new DependencyGraphParser( "artifact-descriptions/managed/" );
+        session.setDependencyManager( new TransitiveDependencyManager() );
+        final Dependency root = newDep( "gid:root:ext:ver", "compile" );
+        CollectRequest request = new CollectRequest( root, Collections.singletonList( repository ) );
+        request.addManagedDependency( newDep( "gid:root:ext:must-retain-core-management" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        final DependencyNode expectedTree = parser.parseResource( "management-tree.txt" );
+        assertEqualSubtree( expectedTree, result.getRoot() );
+
+        // Same test for root artifact (POM) request.
+        final CollectRequest rootArtifactRequest = new CollectRequest();
+        rootArtifactRequest.setRepositories( Collections.singletonList( repository ) );
+        rootArtifactRequest.setRootArtifact( new DefaultArtifact( "gid:root:ext:ver" ) );
+        rootArtifactRequest.addDependency( newDep( "gid:direct:ext:ver", "compile" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:root:ext:must-retain-core-management" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:direct:ext:must-retain-core-management" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:transitive-1:ext:managed-by-root" ) );
+        session.setDependencyManager( new TransitiveDependencyManager() );
+        result = collector.collectDependencies( session, rootArtifactRequest );
+        assertEqualSubtree( expectedTree, toDependencyResult( result.getRoot(), "compile", null ) );
+    }
+
+    @Test
+    public void testDependencyManagement_DefaultDependencyManager()
+        throws DependencyCollectionException, IOException
+    {
+        collector.setArtifactDescriptorReader( newReader( "managed/" ) );
+        parser = new DependencyGraphParser( "artifact-descriptions/managed/" );
+        session.setDependencyManager( new DefaultDependencyManager() );
+        final Dependency root = newDep( "gid:root:ext:ver", "compile" );
+        CollectRequest request = new CollectRequest( root, Arrays.asList( repository ) );
+        request.addManagedDependency( newDep( "gid:root:ext:must-not-manage-root" ) );
+        request.addManagedDependency( newDep( "gid:direct:ext:managed-by-dominant-request" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        final DependencyNode expectedTree = parser.parseResource( "default-management-tree.txt" );
+        assertEqualSubtree( expectedTree, result.getRoot() );
+
+        // Same test for root artifact (POM) request.
+        final CollectRequest rootArtifactRequest = new CollectRequest();
+        rootArtifactRequest.setRepositories( Arrays.asList( repository ) );
+        rootArtifactRequest.setRootArtifact( new DefaultArtifact( "gid:root:ext:ver" ) );
+        rootArtifactRequest.addDependency( newDep( "gid:direct:ext:ver", "compile" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:root:ext:must-not-manage-root" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:direct:ext:managed-by-dominant-request" ) );
+        rootArtifactRequest.addManagedDependency( newDep( "gid:transitive-1:ext:managed-by-root" ) );
+        session.setDependencyManager( new DefaultDependencyManager() );
+        result = collector.collectDependencies( session, rootArtifactRequest );
+        assertEqualSubtree( expectedTree, toDependencyResult( result.getRoot(), "compile", null ) );
+    }
+
+    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.
+        assertNull( "Expected root artifact resolution result.", root.getDependency() );
+        final DefaultDependencyNode defaultNode =
+                new DefaultDependencyNode( new Dependency( root.getArtifact(), rootScope ) );
+
+        defaultNode.setChildren( root.getChildren() );
+
+        if ( optional != null )
+        {
+            defaultNode.setOptional( optional );
+        }
+
+        return defaultNode;
+    }
+
+    @Test
+    public void testVersionFilter()
+        throws Exception
+    {
+        session.setVersionFilter( new HighestVersionFilter() );
+        CollectRequest request = new CollectRequest().setRoot( newDep( "gid:aid:1" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEquals( 1, result.getRoot().getChildren().size() );
+    }
+
+    static class TestDependencyManager
+        implements DependencyManager
+    {
+
+        private Map<String, String> versions = new HashMap<>();
+
+        private Map<String, String> scopes = new HashMap<>();
+
+        private Map<String, Boolean> optionals = new HashMap<>();
+
+        private Map<String, String> paths = new HashMap<>();
+
+        private Map<String, Collection<Exclusion>> exclusions = new HashMap<>();
+
+        public void add( Dependency d, String version, String scope, String localPath )
+        {
+            String id = toKey( d );
+            version( id, version );
+            scope( id, scope );
+            path( id, localPath );
+        }
+
+        public void version( String id, String version )
+        {
+            versions.put( id, version );
+        }
+
+        public void scope( String id, String scope )
+        {
+            scopes.put( id, scope );
+        }
+
+        public void optional( String id, Boolean optional )
+        {
+            optionals.put( id, optional );
+        }
+
+        public void path( String id, String path )
+        {
+            paths.put( id, path );
+        }
+
+        public void exclusions( String id, Exclusion... exclusions )
+        {
+            this.exclusions.put( id, exclusions != null ? Arrays.asList( exclusions ) : null );
+        }
+
+        public DependencyManagement manageDependency( Dependency dependency )
+        {
+            requireNonNull( dependency, "dependency cannot be null" );
+            String id = toKey( dependency );
+            DependencyManagement mgmt = new DependencyManagement();
+            mgmt.setVersion( versions.get( id ) );
+            mgmt.setScope( scopes.get( id ) );
+            mgmt.setOptional( optionals.get( id ) );
+            String path = paths.get( id );
+            if ( path != null )
+            {
+                mgmt.setProperties( Collections.singletonMap( ArtifactProperties.LOCAL_PATH, path ) );
+            }
+            mgmt.setExclusions( exclusions.get( id ) );
+            return mgmt;
+        }
+
+        private String toKey( Dependency dependency )
+        {
+            return ArtifactIdUtils.toVersionlessId( dependency.getArtifact() );
+        }
+
+        public DependencyManager deriveChildManager( DependencyCollectionContext context )
+        {
+            requireNonNull( context, "context cannot be null" );
+            return this;
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycleTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycleTest.java
new file mode 100644
index 00000000..d9708476
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCycleTest.java
@@ -0,0 +1,44 @@
+package org.eclipse.aether.internal.impl.collect.df;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class DfDependencyCycleTest
+{
+    private static final Dependency FOO_DEPENDENCY = new Dependency( new DefaultArtifact( "group-id:foo:1.0" ), "test" );
+    private static final Dependency BAR_DEPENDENCY = new Dependency( new DefaultArtifact( "group-id:bar:1.0" ), "test" );
+
+    @Test
+    public void testToString()
+    {
+        NodeStack nodeStack = new NodeStack();
+        nodeStack.push( new DefaultDependencyNode( FOO_DEPENDENCY ) );
+        DependencyCycle cycle = new DfDependencyCycle( nodeStack, 1, BAR_DEPENDENCY );
+
+        assertEquals( "group-id:foo:jar -> group-id:bar:jar", cycle.toString() );
+    }
+}
\ No newline at end of file