You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by rf...@apache.org on 2020/06/20 09:33:42 UTC

[maven] 29/31: Split off BuildResumptionAnalyzer from BuildResumptionDataRepository

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

rfscholte pushed a commit to branch MNG-5760
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 6faa4cd12a2b2478c63e88d34a62637c7f75e653
Author: Maarten Mulders <ma...@infosupport.com>
AuthorDate: Wed Jun 17 20:24:28 2020 +0200

    Split off BuildResumptionAnalyzer from BuildResumptionDataRepository
---
 .../main/java/org/apache/maven/DefaultMaven.java   |  32 ++--
 .../maven/execution/BuildResumptionAnalyzer.java   |  36 ++++
 .../maven/execution/BuildResumptionData.java       |  55 ++++++
 .../execution/BuildResumptionDataRepository.java   |   4 +-
 .../execution/DefaultBuildResumptionAnalyzer.java  | 162 ++++++++++++++++++
 .../DefaultBuildResumptionDataRepository.java      | 190 +++------------------
 .../DefaultBuildResumptionAnalyzerTest.java        | 150 ++++++++++++++++
 .../DefaultBuildResumptionDataRepositoryTest.java  | 132 +-------------
 8 files changed, 453 insertions(+), 308 deletions(-)

diff --git a/maven-core/src/main/java/org/apache/maven/DefaultMaven.java b/maven-core/src/main/java/org/apache/maven/DefaultMaven.java
index ad5aad7..167cb7b 100644
--- a/maven-core/src/main/java/org/apache/maven/DefaultMaven.java
+++ b/maven-core/src/main/java/org/apache/maven/DefaultMaven.java
@@ -36,6 +36,7 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.maven.artifact.ArtifactUtils;
+import org.apache.maven.execution.BuildResumptionAnalyzer;
 import org.apache.maven.execution.BuildResumptionDataRepository;
 import org.apache.maven.execution.BuildResumptionPersistenceException;
 import org.apache.maven.execution.DefaultMavenExecutionResult;
@@ -103,6 +104,9 @@ public class DefaultMaven
     private GraphBuilder graphBuilder;
 
     @Inject
+    private BuildResumptionAnalyzer buildResumptionAnalyzer;
+
+    @Inject
     private BuildResumptionDataRepository buildResumptionDataRepository;
 
     @Override
@@ -371,21 +375,23 @@ public class DefaultMaven
 
         if ( hasLifecycleExecutionExceptions )
         {
-            session.getAllProjects().stream()
+            MavenProject rootProject = session.getAllProjects().stream()
                     .filter( MavenProject::isExecutionRoot )
                     .findFirst()
-                    .ifPresent( rootProject ->
-                    {
-                        try
-                        {
-                            boolean persistenceResult = buildResumptionDataRepository.persistResumptionData( result, rootProject );
-                            result.setCanResume( persistenceResult );
-                        }
-                        catch ( BuildResumptionPersistenceException e )
-                        {
-                            logger.warn( "Could not persist build resumption data", e );
-                        }
-                    } );
+                    .orElseThrow( () -> new IllegalStateException( "No project in the session is execution root" ) );
+
+            buildResumptionAnalyzer.determineBuildResumptionData( result ).ifPresent( resumption ->
+            {
+                try
+                {
+                    boolean canResume = buildResumptionDataRepository.persistResumptionData( rootProject, resumption );
+                    result.setCanResume( canResume );
+                }
+                catch ( BuildResumptionPersistenceException e )
+                {
+                    logger.warn( "Could not persist build resumption data", e );
+                }
+            } );
         }
     }
 
diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java
new file mode 100644
index 0000000..1778946
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java
@@ -0,0 +1,36 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.Optional;
+
+/**
+ * Instances of this class are responsible for determining whether it makes sense to "resume" a build (i.e., using
+ * the {@code --resume} flag.
+ */
+public interface BuildResumptionAnalyzer
+{
+    /**
+     * Construct an instance of {@link BuildResumptionData} based on the outcome of the current Maven build.
+     * @param result Outcome of the current Maven build.
+     * @return A {@link BuildResumptionData} instance or {@link Optional#empty()} if resuming the build is not possible.
+     */
+    Optional<BuildResumptionData> determineBuildResumptionData( final MavenExecutionResult result );
+}
diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java
new file mode 100644
index 0000000..9330929
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java
@@ -0,0 +1,55 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.Collections;
+import java.util.List;
+
+/**
+ * This class holds the information required to enable resuming a Maven build with {@code --resume}.
+ */
+public class BuildResumptionData
+{
+    /**
+     * The project where the next build could resume from.
+     */
+    private final String resumeFrom;
+
+    /**
+     * List of projects to skip if the build would be resumed from {@link #resumeFrom}.
+     */
+    private final List<String> projectsToSkip;
+
+    public BuildResumptionData ( final String resumeFrom, final List<String> projectsToSkip )
+    {
+        this.resumeFrom = resumeFrom;
+        this.projectsToSkip = projectsToSkip;
+    }
+
+    public String getResumeFrom()
+    {
+        return this.resumeFrom;
+    }
+
+    public List<String> getProjectsToSkip()
+    {
+        return this.projectsToSkip;
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java
index 8733819..b8a3b22 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java
@@ -33,12 +33,12 @@ public interface BuildResumptionDataRepository
      * may also decide it is not needed or meaningful to persist such data, and return <code>false</code> to indicate
      * so.
      *
-     * @param result The result of the current Maven invocation.
      * @param rootProject The root project that is being built.
+     * @param buildResumptionData Information needed to resume the build.
      * @throws BuildResumptionPersistenceException When an error occurs while persisting data.
      * @return Whether any data was persisted.
      */
-    boolean persistResumptionData( final MavenExecutionResult result, final MavenProject rootProject )
+    boolean persistResumptionData( final MavenProject rootProject, final BuildResumptionData buildResumptionData )
             throws BuildResumptionPersistenceException;
 
     /**
diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java
new file mode 100644
index 0000000..3d100dc
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java
@@ -0,0 +1,162 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.apache.maven.lifecycle.LifecycleExecutionException;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static java.util.Comparator.comparing;
+
+/**
+ * Default implementation of {@link BuildResumptionAnalyzer}.
+ */
+@Named
+@Singleton
+public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultBuildResumptionAnalyzer.class );
+
+    @Override
+    public Optional<BuildResumptionData> determineBuildResumptionData( final MavenExecutionResult result )
+    {
+        final List<MavenProject> failedProjects = getFailedProjectsInOrder( result );
+
+        if ( failedProjects.isEmpty() )
+        {
+            LOGGER.info( "No failed projects found, resuming the build would not make sense." );
+            return Optional.empty();
+        }
+
+        final MavenProject resumeFromProject = failedProjects.get( 0 );
+
+        if ( isFailedProjectFirstInBuild( result, resumeFromProject ) )
+        {
+            LOGGER.info( "The first module in the build failed, resuming the build would not make sense." );
+            return Optional.empty();
+        }
+
+        final String resumeFromSelector = resumeFromProject.getGroupId() + ":" + resumeFromProject.getArtifactId();
+        final List<String> projectsToSkip = determineProjectsToSkip( result, failedProjects, resumeFromProject );
+
+        return Optional.of( new BuildResumptionData( resumeFromSelector, projectsToSkip ) );
+    }
+
+    private boolean isFailedProjectFirstInBuild( final MavenExecutionResult result, final MavenProject failedProject )
+    {
+        final List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
+        return sortedProjects.indexOf( failedProject ) == 0;
+    }
+
+    private List<MavenProject> getFailedProjectsInOrder( MavenExecutionResult result )
+    {
+        List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
+
+        return result.getExceptions().stream()
+                .filter( LifecycleExecutionException.class::isInstance )
+                .map( LifecycleExecutionException.class::cast )
+                .map( LifecycleExecutionException::getProject )
+                .sorted( comparing( sortedProjects::indexOf ) )
+                .collect( Collectors.toList() );
+    }
+
+    /**
+     * Projects after the first failed project could have succeeded by using -T or --fail-at-end.
+     * These projects can be skipped from later builds.
+     * This is not the case these projects are dependent on one of the failed projects.
+     * @param result The result of the Maven build.
+     * @param failedProjects The list of failed projects in the build.
+     * @param resumeFromProject The project where the build will be resumed with in the next run.
+     * @return A list of projects which can be skipped in a later build.
+     */
+    private List<String> determineProjectsToSkip( MavenExecutionResult result,
+                                                  List<MavenProject> failedProjects,
+                                                  MavenProject resumeFromProject )
+    {
+        List<MavenProject> allProjects = result.getTopologicallySortedProjects();
+        int resumeFromProjectIndex = allProjects.indexOf( resumeFromProject );
+        List<MavenProject> remainingProjects = allProjects.subList( resumeFromProjectIndex + 1, allProjects.size() );
+
+        List<GroupArtifactPair> failedProjectsGAList = failedProjects.stream()
+                .map( GroupArtifactPair::new )
+                .collect( Collectors.toList() );
+
+        return remainingProjects.stream()
+                .filter( project -> result.getBuildSummary( project ) instanceof BuildSuccess )
+                .filter( project -> hasNoDependencyOnProjects( project, failedProjectsGAList ) )
+                .map( project -> project.getGroupId() + ":" + project.getArtifactId() )
+                .collect( Collectors.toList() );
+    }
+
+    private boolean hasNoDependencyOnProjects( MavenProject project, List<GroupArtifactPair> projectsGAs )
+    {
+        return project.getDependencies().stream()
+                .map( GroupArtifactPair::new )
+                .noneMatch( projectsGAs::contains );
+    }
+
+    private static class GroupArtifactPair
+    {
+        private final String groupId;
+        private final String artifactId;
+
+        GroupArtifactPair( MavenProject project )
+        {
+            this.groupId = project.getGroupId();
+            this.artifactId = project.getArtifactId();
+        }
+
+        GroupArtifactPair( Dependency dependency )
+        {
+            this.groupId = dependency.getGroupId();
+            this.artifactId = dependency.getArtifactId();
+        }
+
+        @Override
+        public boolean equals( Object o )
+        {
+            if ( this == o )
+            {
+                return true;
+            }
+            if ( o == null || getClass() != o.getClass() )
+            {
+                return false;
+            }
+            GroupArtifactPair that = (GroupArtifactPair) o;
+            return Objects.equals( groupId, that.groupId ) && Objects.equals( artifactId, that.artifactId );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash( groupId, artifactId );
+        }
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
index 7e647d7..e7965b3 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
@@ -20,8 +20,6 @@ package org.apache.maven.execution;
  */
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.maven.lifecycle.LifecycleExecutionException;
-import org.apache.maven.model.Dependency;
 import org.apache.maven.project.MavenProject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -35,17 +33,11 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
 import java.util.Properties;
-import java.util.stream.Collectors;
-
-import static java.util.Comparator.comparing;
 
 /**
- * This implementation of {@link BuildResumptionDataRepository} persists information in a properties file. The file is stored
- * in the build output directory under the Maven execution root.
+ * This implementation of {@link BuildResumptionDataRepository} persists information in a properties file. The file is
+ * stored in the build output directory under the Maven execution root.
  */
 @Named
 @Singleton
@@ -58,151 +50,58 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData
     private static final Logger LOGGER = LoggerFactory.getLogger( DefaultBuildResumptionDataRepository.class );
 
     @Override
-    public boolean persistResumptionData( MavenExecutionResult result, MavenProject rootProject )
+    public boolean persistResumptionData( MavenProject rootProject, BuildResumptionData buildResumptionData )
             throws BuildResumptionPersistenceException
     {
-        Properties properties = determineResumptionProperties( result );
-
-        if ( properties.isEmpty() )
-        {
-            LOGGER.debug( "Will not create {} file: nothing to resume from", RESUME_PROPERTIES_FILENAME );
-            return false;
-        }
-
-        return writeResumptionFile( rootProject, properties );
-    }
-
-    @Override
-    public void applyResumptionData( MavenExecutionRequest request, MavenProject rootProject )
-    {
-        Properties properties = loadResumptionFile( Paths.get( rootProject.getBuild().getDirectory() ) );
-        applyResumptionProperties( request, properties );
-    }
+        Properties properties = convertToProperties( buildResumptionData );
 
-    @Override
-    public void removeResumptionData( MavenProject rootProject )
-    {
         Path resumeProperties = Paths.get( rootProject.getBuild().getDirectory(), RESUME_PROPERTIES_FILENAME );
         try
         {
-            Files.deleteIfExists( resumeProperties );
+            Files.createDirectories( resumeProperties.getParent() );
+            try ( Writer writer = Files.newBufferedWriter( resumeProperties ) )
+            {
+                properties.store( writer, null );
+            }
         }
         catch ( IOException e )
         {
-            LOGGER.warn( "Could not delete {} file. ", RESUME_PROPERTIES_FILENAME, e );
+            String message = "Could not create " + RESUME_PROPERTIES_FILENAME + " file.";
+            throw new BuildResumptionPersistenceException( message, e );
         }
+
+        return true;
     }
 
-    // This method is made package-private for testing purposes
-    Properties determineResumptionProperties( MavenExecutionResult result )
+    private Properties convertToProperties( final BuildResumptionData buildResumptionData )
     {
         Properties properties = new Properties();
-
-        List<MavenProject> failedProjects = getFailedProjectsInOrder( result );
-        if ( !failedProjects.isEmpty() )
-        {
-            MavenProject resumeFromProject = failedProjects.get( 0 );
-            Optional<String> resumeFrom = getResumeFrom( result, resumeFromProject );
-            List<String> projectsToSkip = determineProjectsToSkip( result, failedProjects, resumeFromProject );
-
-            resumeFrom.ifPresent( value -> properties.setProperty( RESUME_FROM_PROPERTY, value ) );
-            if ( !projectsToSkip.isEmpty() ) {
-                String excludedProjects = String.join( PROPERTY_DELIMITER, projectsToSkip );
-                properties.setProperty( EXCLUDED_PROJECTS_PROPERTY, excludedProjects );
-            }
-        }
-        else
-        {
-            LOGGER.warn( "Could not create {} file: no failed projects found", RESUME_PROPERTIES_FILENAME );
-        }
+        properties.setProperty( RESUME_FROM_PROPERTY, buildResumptionData.getResumeFrom() );
+        String excludedProjects = String.join( PROPERTY_DELIMITER, buildResumptionData.getProjectsToSkip() );
+        properties.setProperty( EXCLUDED_PROJECTS_PROPERTY, excludedProjects );
 
         return properties;
     }
 
-    private List<MavenProject> getFailedProjectsInOrder( MavenExecutionResult result )
-    {
-        List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
-
-        return result.getExceptions().stream()
-                .filter( LifecycleExecutionException.class::isInstance )
-                .map( LifecycleExecutionException.class::cast )
-                .map( LifecycleExecutionException::getProject )
-                .sorted( comparing( sortedProjects::indexOf ) )
-                .collect( Collectors.toList() );
-    }
-
-    /**
-     * Determine the project where the next build can be resumed from.
-     * If the failed project is the first project of the build,
-     * it does not make sense to use --resume-from, so the result will be empty.
-     * @param result The result of the Maven build.
-     * @param failedProject The first failed project of the build.
-     * @return An optional containing the resume-from suggestion.
-     */
-    private Optional<String> getResumeFrom( MavenExecutionResult result, MavenProject failedProject )
-    {
-        List<MavenProject> allSortedProjects = result.getTopologicallySortedProjects();
-        if ( !allSortedProjects.get( 0 ).equals( failedProject ) )
-        {
-            return Optional.of( failedProject.getGroupId() + ":" + failedProject.getArtifactId() );
-        }
-
-        return Optional.empty();
-    }
-
-    /**
-     * Projects after the first failed project could have succeeded by using -T or --fail-at-end.
-     * These projects can be skipped from later builds.
-     * This is not the case these projects are dependent on one of the failed projects.
-     * @param result The result of the Maven build.
-     * @param failedProjects The list of failed projects in the build.
-     * @param resumeFromProject The project where the build will be resumed with in the next run.
-     * @return A list of projects which can be skipped in a later build.
-     */
-    private List<String> determineProjectsToSkip( MavenExecutionResult result, List<MavenProject> failedProjects,
-                                                  MavenProject resumeFromProject )
-    {
-        List<MavenProject> allProjects = result.getTopologicallySortedProjects();
-        int resumeFromProjectIndex = allProjects.indexOf( resumeFromProject );
-        List<MavenProject> remainingProjects = allProjects.subList( resumeFromProjectIndex + 1, allProjects.size() );
-
-        List<GroupArtifactPair> failedProjectsGAList = failedProjects.stream()
-                .map( GroupArtifactPair::new )
-                .collect( Collectors.toList() );
-
-        return remainingProjects.stream()
-                .filter( project -> result.getBuildSummary( project ) instanceof BuildSuccess )
-                .filter( project -> hasNoDependencyOnProjects( project, failedProjectsGAList ) )
-                .map( project -> project.getGroupId() + ":" + project.getArtifactId() )
-                .collect( Collectors.toList() );
-    }
-
-    private boolean hasNoDependencyOnProjects( MavenProject project, List<GroupArtifactPair> projectsGAs )
+    @Override
+    public void applyResumptionData( MavenExecutionRequest request, MavenProject rootProject )
     {
-        return project.getDependencies().stream()
-                .map( GroupArtifactPair::new )
-                .noneMatch( projectsGAs::contains );
+        Properties properties = loadResumptionFile( Paths.get( rootProject.getBuild().getDirectory() ) );
+        applyResumptionProperties( request, properties );
     }
 
-    private boolean writeResumptionFile( MavenProject rootProject, Properties properties )
-            throws BuildResumptionPersistenceException
+    @Override
+    public void removeResumptionData( MavenProject rootProject )
     {
         Path resumeProperties = Paths.get( rootProject.getBuild().getDirectory(), RESUME_PROPERTIES_FILENAME );
         try
         {
-            Files.createDirectories( resumeProperties.getParent() );
-            try ( Writer writer = Files.newBufferedWriter( resumeProperties ) )
-            {
-                properties.store( writer, null );
-            }
+            Files.deleteIfExists( resumeProperties );
         }
         catch ( IOException e )
         {
-            String message = "Could not create " + RESUME_PROPERTIES_FILENAME + " file.";
-            throw new BuildResumptionPersistenceException( message, e );
+            LOGGER.warn( "Could not delete {} file. ", RESUME_PROPERTIES_FILENAME, e );
         }
-
-        return true;
     }
 
     private Properties loadResumptionFile( Path rootBuildDirectory )
@@ -245,43 +144,4 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData
             LOGGER.info( "Additionally excluding projects '{}' due to the --resume / -r feature.", propertyValue );
         }
     }
-
-    private static class GroupArtifactPair
-    {
-        private final String groupId;
-        private final String artifactId;
-
-        GroupArtifactPair( MavenProject project )
-        {
-            this.groupId = project.getGroupId();
-            this.artifactId = project.getArtifactId();
-        }
-
-        GroupArtifactPair( Dependency dependency )
-        {
-            this.groupId = dependency.getGroupId();
-            this.artifactId = dependency.getArtifactId();
-        }
-
-        @Override
-        public boolean equals( Object o )
-        {
-            if ( this == o )
-            {
-                return true;
-            }
-            if ( o == null || getClass() != o.getClass() )
-            {
-                return false;
-            }
-            GroupArtifactPair that = (GroupArtifactPair) o;
-            return Objects.equals( groupId, that.groupId ) && Objects.equals( artifactId, that.artifactId );
-        }
-
-        @Override
-        public int hashCode()
-        {
-            return Objects.hash( groupId, artifactId );
-        }
-    }
 }
diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java
new file mode 100644
index 0000000..59a8e63
--- /dev/null
+++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java
@@ -0,0 +1,150 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.apache.maven.lifecycle.LifecycleExecutionException;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+
+public class DefaultBuildResumptionAnalyzerTest
+{
+    private final DefaultBuildResumptionAnalyzer analyzer = new DefaultBuildResumptionAnalyzer();
+
+    private MavenExecutionResult executionResult;
+
+    @Before
+    public void before() {
+        executionResult = new DefaultMavenExecutionResult();
+    }
+
+    @Test
+    public void resumeFromGetsDetermined()
+    {
+        MavenProject projectA = createSucceededMavenProject( "A" );
+        MavenProject projectB = createFailedMavenProject( "B" );
+        executionResult.setTopologicallySortedProjects( asList( projectA, projectB ) );
+
+        Optional<BuildResumptionData> result = analyzer.determineBuildResumptionData( executionResult );
+
+        assertThat( result.isPresent(), is( true ) );
+        assertThat( result.get().getResumeFrom(), is( "test:B" ) );
+    }
+
+    @Test
+    public void resumeFromIsIgnoredWhenFirstProjectFails()
+    {
+        MavenProject projectA = createFailedMavenProject( "A" );
+        MavenProject projectB = createMavenProject( "B" );
+        executionResult.setTopologicallySortedProjects( asList( projectA, projectB ) );
+
+        Optional<BuildResumptionData> result = analyzer.determineBuildResumptionData( executionResult );
+
+        assertThat( result.isPresent(), is( false ) );
+    }
+
+    @Test
+    public void projectsSucceedingAfterFailedProjectsAreExcluded()
+    {
+        MavenProject projectA = createSucceededMavenProject( "A" );
+        MavenProject projectB = createFailedMavenProject( "B" );
+        MavenProject projectC = createSucceededMavenProject( "C" );
+        executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) );
+
+        Optional<BuildResumptionData> result = analyzer.determineBuildResumptionData( executionResult );
+
+        assertThat( result.isPresent(), is( true ) );
+        assertThat( result.get().getProjectsToSkip(), contains( "test:C" ) );
+    }
+
+    @Test
+    public void projectsDependingOnFailedProjectsAreNotExcluded()
+    {
+        MavenProject projectA = createSucceededMavenProject( "A" );
+        MavenProject projectB = createFailedMavenProject( "B" );
+        MavenProject projectC = createSucceededMavenProject( "C" );
+        projectC.setDependencies( singletonList( toDependency( projectB ) ) );
+        executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) );
+
+        Optional<BuildResumptionData> result = analyzer.determineBuildResumptionData( executionResult );
+
+        assertThat( result.isPresent(), is( true ) );
+        assertThat( result.get().getProjectsToSkip().isEmpty(), is( true ) );
+    }
+
+    @Test
+    public void projectsFailingAfterAnotherFailedProjectAreNotExcluded()
+    {
+        MavenProject projectA = createSucceededMavenProject( "A" );
+        MavenProject projectB = createFailedMavenProject( "B" );
+        MavenProject projectC = createSucceededMavenProject( "C" );
+        MavenProject projectD = createFailedMavenProject( "D" );
+        executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC, projectD ) );
+
+        Optional<BuildResumptionData> result = analyzer.determineBuildResumptionData( executionResult );
+
+        assertThat( result.isPresent(), is( true ) );
+        assertThat( result.get().getResumeFrom(), is( "test:B" ) );
+        assertThat( result.get().getProjectsToSkip(), contains( "test:C" ) );
+        assertThat( result.get().getProjectsToSkip(), not( contains( "test:D" ) ) );
+    }
+
+    private MavenProject createMavenProject( String artifactId )
+    {
+        MavenProject project = new MavenProject();
+        project.setGroupId( "test" );
+        project.setArtifactId( artifactId );
+        return project;
+    }
+
+    private Dependency toDependency(MavenProject mavenProject )
+    {
+        Dependency dependency = new Dependency();
+        dependency.setGroupId( mavenProject.getGroupId() );
+        dependency.setArtifactId( mavenProject.getArtifactId() );
+        dependency.setVersion( mavenProject.getVersion() );
+        return dependency;
+    }
+
+    private MavenProject createSucceededMavenProject( String artifactId )
+    {
+        MavenProject project = createMavenProject( artifactId );
+        executionResult.addBuildSummary( new BuildSuccess( project, 0 ) );
+        return project;
+    }
+
+    private MavenProject createFailedMavenProject( String artifactId )
+    {
+        MavenProject project = createMavenProject( artifactId );
+        executionResult.addBuildSummary( new BuildFailure( project, 0, new Exception() ) );
+        executionResult.addException( new LifecycleExecutionException( "", project ) );
+        return project;
+    }
+}
\ No newline at end of file
diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
index 279fb11..415b946 100644
--- a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
+++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
@@ -19,10 +19,6 @@ package org.apache.maven.execution;
  * under the License.
  */
 
-import org.apache.maven.lifecycle.LifecycleExecutionException;
-import org.apache.maven.model.Dependency;
-import org.apache.maven.project.MavenProject;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -31,8 +27,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
 
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.is;
@@ -40,93 +34,7 @@ import static org.hamcrest.Matchers.is;
 @RunWith( MockitoJUnitRunner.class )
 public class DefaultBuildResumptionDataRepositoryTest
 {
-    private final DefaultBuildResumptionDataRepository buildResumer = new DefaultBuildResumptionDataRepository();
-
-    private MavenExecutionResult result;
-
-    @Before
-    public void before() {
-        result = new DefaultMavenExecutionResult();
-    }
-
-    @Test
-    public void resumeFromGetsDetermined()
-    {
-        MavenProject projectA = createSucceededMavenProject( "A" );
-        MavenProject projectB = createFailedMavenProject( "B" );
-        result.setTopologicallySortedProjects( asList( projectA, projectB ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.get( "resumeFrom" ), is( "test:B" ) );
-    }
-
-    @Test
-    public void resumeFromIsIgnoredWhenFirstProjectFails()
-    {
-        MavenProject projectA = createFailedMavenProject( "A" );
-        MavenProject projectB = createMavenProject( "B" );
-        result.setTopologicallySortedProjects( asList( projectA, projectB ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.containsKey( "resumeFrom" ), is(false) );
-    }
-
-    @Test
-    public void projectsSucceedingAfterFailedProjectsAreExcluded()
-    {
-        MavenProject projectA = createSucceededMavenProject( "A" );
-        MavenProject projectB = createFailedMavenProject( "B" );
-        MavenProject projectC = createSucceededMavenProject( "C" );
-        result.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.get( "excludedProjects" ), is("test:C") );
-    }
-
-    @Test
-    public void projectsDependingOnFailedProjectsAreNotExcluded()
-    {
-        MavenProject projectA = createSucceededMavenProject( "A" );
-        MavenProject projectB = createFailedMavenProject( "B" );
-        MavenProject projectC = createSucceededMavenProject( "C" );
-        projectC.setDependencies( singletonList( toDependency( projectB ) ) );
-        result.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.containsKey( "excludedProjects" ), is(false) );
-    }
-
-    @Test
-    public void projectsFailingAfterAnotherFailedProjectAreNotExcluded()
-    {
-        MavenProject projectA = createSucceededMavenProject( "A" );
-        MavenProject projectB = createFailedMavenProject( "B" );
-        MavenProject projectC = createSucceededMavenProject( "C" );
-        MavenProject projectD = createFailedMavenProject( "D" );
-        result.setTopologicallySortedProjects( asList( projectA, projectB, projectC, projectD ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.get( "resumeFrom" ), is("test:B") );
-        assertThat( properties.get( "excludedProjects" ), is("test:C") );
-    }
-
-    @Test
-    public void multipleExcludedProjectsAreCommaSeparated()
-    {
-        MavenProject projectA = createFailedMavenProject( "A" );
-        MavenProject projectB = createSucceededMavenProject( "B" );
-        MavenProject projectC = createSucceededMavenProject( "C" );
-        result.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) );
-
-        Properties properties = buildResumer.determineResumptionProperties( result );
-
-        assertThat( properties.get( "excludedProjects" ), is( "test:B, test:C" ) );
-    }
+    private final DefaultBuildResumptionDataRepository repository = new DefaultBuildResumptionDataRepository();
 
     @Test
     public void resumeFromPropertyGetsApplied()
@@ -135,7 +43,7 @@ public class DefaultBuildResumptionDataRepositoryTest
         Properties properties = new Properties();
         properties.setProperty( "resumeFrom", ":module-a" );
 
-        buildResumer.applyResumptionProperties( request, properties );
+        repository.applyResumptionProperties( request, properties );
 
         assertThat( request.getResumeFrom(), is( ":module-a" ) );
     }
@@ -148,7 +56,7 @@ public class DefaultBuildResumptionDataRepositoryTest
         Properties properties = new Properties();
         properties.setProperty( "resumeFrom", ":module-a" );
 
-        buildResumer.applyResumptionProperties( request, properties );
+        repository.applyResumptionProperties( request, properties );
 
         assertThat( request.getResumeFrom(), is( ":module-b" ) );
     }
@@ -163,40 +71,8 @@ public class DefaultBuildResumptionDataRepositoryTest
         Properties properties = new Properties();
         properties.setProperty( "excludedProjects", ":module-b, :module-c" );
 
-        buildResumer.applyResumptionProperties( request, properties );
+        repository.applyResumptionProperties( request, properties );
 
         assertThat( request.getExcludedProjects(), contains( ":module-a", ":module-b", ":module-c" ) );
     }
-
-    private MavenProject createMavenProject( String artifactId )
-    {
-        MavenProject project = new MavenProject();
-        project.setGroupId( "test" );
-        project.setArtifactId( artifactId );
-        return project;
-    }
-
-    private Dependency toDependency( MavenProject mavenProject )
-    {
-        Dependency dependency = new Dependency();
-        dependency.setGroupId( mavenProject.getGroupId() );
-        dependency.setArtifactId( mavenProject.getArtifactId() );
-        dependency.setVersion( mavenProject.getVersion() );
-        return dependency;
-    }
-
-    private MavenProject createSucceededMavenProject( String artifactId )
-    {
-        MavenProject project = createMavenProject( artifactId );
-        result.addBuildSummary( new BuildSuccess( project, 0 ) );
-        return project;
-    }
-
-    private MavenProject createFailedMavenProject( String artifactId )
-    {
-        MavenProject project = createMavenProject( artifactId );
-        result.addBuildSummary( new BuildFailure( project, 0, new Exception() ) );
-        result.addException( new LifecycleExecutionException( "", project ) );
-        return project;
-    }
 }