[MNG-5760] Add `-r/--resume` to automatically resume from the last failure point

Author: Martin Kanters <mkanters93@gmail.com>
This commit is contained in:
rfscholte 2020-06-20 13:55:48 +02:00
parent 628a5756d3
commit d35573e06c
17 changed files with 918 additions and 22 deletions

View File

@ -36,6 +36,9 @@ 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;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.MavenExecutionRequest;
@ -44,6 +47,7 @@ import org.apache.maven.execution.MavenSession;
import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.graph.GraphBuilder;
import org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
import org.apache.maven.lifecycle.internal.LifecycleStarter;
import org.apache.maven.model.Prerequisites;
@ -99,6 +103,12 @@ public class DefaultMaven
@Named( GraphBuilder.HINT )
private GraphBuilder graphBuilder;
@Inject
private BuildResumptionAnalyzer buildResumptionAnalyzer;
@Inject
private BuildResumptionDataRepository buildResumptionDataRepository;
@Override
public MavenExecutionResult execute( MavenExecutionRequest request )
{
@ -312,7 +322,16 @@ public class DefaultMaven
if ( session.getResult().hasExceptions() )
{
return addExceptionToResult( result, session.getResult().getExceptions().get( 0 ) );
addExceptionToResult( result, session.getResult().getExceptions().get( 0 ) );
persistResumptionData( result, session );
return result;
}
else
{
session.getAllProjects().stream()
.filter( MavenProject::isExecutionRoot )
.findFirst()
.ifPresent( buildResumptionDataRepository::removeResumptionData );
}
}
finally
@ -349,6 +368,33 @@ public class DefaultMaven
}
}
private void persistResumptionData( MavenExecutionResult result, MavenSession session )
{
boolean hasLifecycleExecutionExceptions = result.getExceptions().stream()
.anyMatch( LifecycleExecutionException.class::isInstance );
if ( hasLifecycleExecutionExceptions )
{
MavenProject rootProject = session.getAllProjects().stream()
.filter( MavenProject::isExecutionRoot )
.findFirst()
.orElseThrow( () -> new IllegalStateException( "No project in the session is execution root" ) );
buildResumptionAnalyzer.determineBuildResumptionData( result ).ifPresent( resumption ->
{
try
{
buildResumptionDataRepository.persistResumptionData( rootProject, resumption );
result.setCanResume( true );
}
catch ( BuildResumptionPersistenceException e )
{
logger.warn( "Could not persist build resumption data", e );
}
} );
}
}
public RepositorySystemSession newRepositorySession( MavenExecutionRequest request )
{
return repositorySessionFactory.newRepositorySession( request );

View File

@ -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 );
}

View File

@ -0,0 +1,54 @@
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.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;
}
}

View File

@ -0,0 +1,56 @@
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.project.MavenProject;
/**
* Instances of this interface retrieve and store data for the --resume / -r feature. This data is used to ensure newer
* builds of the same project, that have the -r command-line flag, skip successfully built projects during earlier
* invocations of Maven.
*/
public interface BuildResumptionDataRepository
{
/**
* Persists any data needed to resume the build at a later point in time, using a new Maven invocation. This method
* may also decide it is not needed or meaningful to persist such data, and return <code>false</code> to indicate
* so.
*
* @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.
*/
void persistResumptionData( final MavenProject rootProject, final BuildResumptionData buildResumptionData )
throws BuildResumptionPersistenceException;
/**
* Uses previously stored resumption data to enrich an existing execution request.
* @param request The execution request that will be enriched.
* @param rootProject The root project that is being built.
*/
void applyResumptionData( final MavenExecutionRequest request, final MavenProject rootProject );
/**
* Removes previously stored resumption data.
* @param rootProject The root project that is being built.
*/
void removeResumptionData( final MavenProject rootProject );
}

View File

@ -0,0 +1,32 @@
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.
*/
/**
* This exception will be thrown when something fails while persisting build resumption data.
* @see BuildResumptionDataRepository#persistResumptionData
*/
public class BuildResumptionPersistenceException extends Exception
{
public BuildResumptionPersistenceException( String message, Throwable cause )
{
super( message, cause );
}
}

View File

@ -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 );
}
}
}

View File

@ -0,0 +1,145 @@
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.commons.lang3.StringUtils;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Properties;
/**
* 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
public class DefaultBuildResumptionDataRepository implements BuildResumptionDataRepository
{
private static final String RESUME_PROPERTIES_FILENAME = "resume.properties";
private static final String RESUME_FROM_PROPERTY = "resumeFrom";
private static final String EXCLUDED_PROJECTS_PROPERTY = "excludedProjects";
private static final String PROPERTY_DELIMITER = ", ";
private static final Logger LOGGER = LoggerFactory.getLogger( DefaultBuildResumptionDataRepository.class );
@Override
public void persistResumptionData( MavenProject rootProject, BuildResumptionData buildResumptionData )
throws BuildResumptionPersistenceException
{
Properties properties = convertToProperties( buildResumptionData );
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 );
}
}
catch ( IOException e )
{
String message = "Could not create " + RESUME_PROPERTIES_FILENAME + " file.";
throw new BuildResumptionPersistenceException( message, e );
}
}
private Properties convertToProperties( final BuildResumptionData buildResumptionData )
{
Properties properties = new Properties();
properties.setProperty( RESUME_FROM_PROPERTY, buildResumptionData.getResumeFrom() );
String excludedProjects = String.join( PROPERTY_DELIMITER, buildResumptionData.getProjectsToSkip() );
properties.setProperty( EXCLUDED_PROJECTS_PROPERTY, excludedProjects );
return properties;
}
@Override
public void applyResumptionData( MavenExecutionRequest request, MavenProject rootProject )
{
Properties properties = loadResumptionFile( Paths.get( rootProject.getBuild().getDirectory() ) );
applyResumptionProperties( request, properties );
}
@Override
public void removeResumptionData( MavenProject rootProject )
{
Path resumeProperties = Paths.get( rootProject.getBuild().getDirectory(), RESUME_PROPERTIES_FILENAME );
try
{
Files.deleteIfExists( resumeProperties );
}
catch ( IOException e )
{
LOGGER.warn( "Could not delete {} file. ", RESUME_PROPERTIES_FILENAME, e );
}
}
private Properties loadResumptionFile( Path rootBuildDirectory )
{
Properties properties = new Properties();
Path path = Paths.get( RESUME_PROPERTIES_FILENAME ).resolve( rootBuildDirectory );
if ( !Files.exists( path ) )
{
LOGGER.warn( "The {} file does not exist. The --resume / -r feature will not work.", path );
return properties;
}
try ( Reader reader = Files.newBufferedReader( path ) )
{
properties.load( reader );
}
catch ( IOException e )
{
LOGGER.warn( "Unable to read {}. The --resume / -r feature will not work.", path );
}
return properties;
}
// This method is made package-private for testing purposes
void applyResumptionProperties( MavenExecutionRequest request, Properties properties )
{
if ( properties.containsKey( RESUME_FROM_PROPERTY ) && StringUtils.isEmpty( request.getResumeFrom() ) )
{
String propertyValue = properties.getProperty( RESUME_FROM_PROPERTY );
request.setResumeFrom( propertyValue );
LOGGER.info( "Resuming from {} due to the --resume / -r feature.", propertyValue );
}
if ( properties.containsKey( EXCLUDED_PROJECTS_PROPERTY ) )
{
String propertyValue = properties.getProperty( EXCLUDED_PROJECTS_PROPERTY );
String[] excludedProjects = propertyValue.split( PROPERTY_DELIMITER );
request.getExcludedProjects().addAll( Arrays.asList( excludedProjects ) );
LOGGER.info( "Additionally excluding projects '{}' due to the --resume / -r feature.", propertyValue );
}
}
}

View File

@ -116,6 +116,8 @@ public class DefaultMavenExecutionRequest
private List<String> excludedProjects;
private boolean resume = false;
private String resumeFrom;
private String makeBehavior;
@ -300,6 +302,12 @@ public class DefaultMavenExecutionRequest
return excludedProjects;
}
@Override
public boolean isResume()
{
return resume;
}
@Override
public String getResumeFrom()
{
@ -598,6 +606,14 @@ public class DefaultMavenExecutionRequest
return this;
}
@Override
public MavenExecutionRequest setResume()
{
resume = true;
return this;
}
@Override
public MavenExecutionRequest setResumeFrom( String project )
{

View File

@ -43,6 +43,8 @@ public class DefaultMavenExecutionResult
private final Map<MavenProject, BuildSummary> buildSummaries =
Collections.synchronizedMap( new IdentityHashMap<>() );
private boolean canResume = false;
public MavenExecutionResult setProject( MavenProject project )
{
this.project = project;
@ -108,4 +110,16 @@ public class DefaultMavenExecutionResult
{
buildSummaries.put( summary.getProject(), summary );
}
@Override
public boolean canResume()
{
return canResume;
}
@Override
public void setCanResume( boolean canResume )
{
this.canResume = canResume;
}
}

View File

@ -171,6 +171,17 @@ public interface MavenExecutionRequest
*/
List<String> getExcludedProjects();
/**
* Sets whether the build should be resumed from the data in the resume.properties file.
* @return This request, never {@code null}.
*/
MavenExecutionRequest setResume();
/**
* @return Whether the build should be resumed from the data in the resume.properties file.
*/
boolean isResume();
MavenExecutionRequest setResumeFrom( String project );
String getResumeFrom();

View File

@ -67,4 +67,19 @@ public interface MavenExecutionResult
* @param summary The build summary to add, must not be {@code null}.
*/
void addBuildSummary( BuildSummary summary );
/**
* Indicates whether or not the build could be resumed by a second invocation of Maven.
* @see BuildResumptionDataRepository
* @return <code>true</code> when it is possible to resume the build, <code>false</code> otherwise.
*/
boolean canResume();
/**
* Indicate that the build can or cannot be resumed by a second invocation of Maven.
* @param canResume <code>true</code> when it is possible to resume the build, <code>false</code> otherwise.
* @see BuildResumptionDataRepository
* @see #canResume()
*/
void setCanResume( boolean canResume );
}

View File

@ -38,6 +38,7 @@ import org.apache.maven.DefaultMaven;
import org.apache.maven.MavenExecutionException;
import org.apache.maven.ProjectCycleException;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.execution.BuildResumptionDataRepository;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.execution.ProjectDependencyGraph;
@ -73,6 +74,9 @@ public class DefaultGraphBuilder
@Inject
protected ProjectBuilder projectBuilder;
@Inject
private BuildResumptionDataRepository buildResumptionDataRepository;
@Override
public Result<ProjectDependencyGraph> build( MavenSession session )
{
@ -84,6 +88,7 @@ public class DefaultGraphBuilder
{
final List<MavenProject> projects = getProjectsForMavenReactor( session );
validateProjects( projects );
enrichRequestFromResumptionData( projects, session.getRequest() );
result = reactorDependencyGraph( session, projects );
}
@ -341,6 +346,18 @@ public class DefaultGraphBuilder
return result;
}
private void enrichRequestFromResumptionData( List<MavenProject> projects, MavenExecutionRequest request )
{
if ( request.isResume() )
{
projects.stream()
.filter( MavenProject::isExecutionRoot )
.findFirst()
.ifPresent( rootProject ->
buildResumptionDataRepository.applyResumptionData( request, rootProject ) );
}
}
private String formatProjects( List<MavenProject> projects )
{
StringBuilder projectNames = new StringBuilder();

View File

@ -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;
}
}

View File

@ -0,0 +1,78 @@
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.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
@RunWith( MockitoJUnitRunner.class )
public class DefaultBuildResumptionDataRepositoryTest
{
private final DefaultBuildResumptionDataRepository repository = new DefaultBuildResumptionDataRepository();
@Test
public void resumeFromPropertyGetsApplied()
{
MavenExecutionRequest request = new DefaultMavenExecutionRequest();
Properties properties = new Properties();
properties.setProperty( "resumeFrom", ":module-a" );
repository.applyResumptionProperties( request, properties );
assertThat( request.getResumeFrom(), is( ":module-a" ) );
}
@Test
public void resumeFromPropertyDoesNotOverrideExistingRequestParameters()
{
MavenExecutionRequest request = new DefaultMavenExecutionRequest();
request.setResumeFrom( ":module-b" );
Properties properties = new Properties();
properties.setProperty( "resumeFrom", ":module-a" );
repository.applyResumptionProperties( request, properties );
assertThat( request.getResumeFrom(), is( ":module-b" ) );
}
@Test
public void excludedProjectsFromPropertyGetsAddedToExistingRequestParameters()
{
MavenExecutionRequest request = new DefaultMavenExecutionRequest();
List<String> excludedProjects = new ArrayList<>();
excludedProjects.add( ":module-a" );
request.setExcludedProjects( excludedProjects );
Properties properties = new Properties();
properties.setProperty( "excludedProjects", ":module-b, :module-c" );
repository.applyResumptionProperties( request, properties );
assertThat( request.getExcludedProjects(), contains( ":module-a", ":module-b", ":module-c" ) );
}
}

View File

@ -83,6 +83,8 @@ public class CLIManager
public static final String FAIL_NEVER = "fn";
public static final String RESUME = "r";
public static final String RESUME_FROM = "rf";
public static final String PROJECT_LIST = "pl";
@ -134,6 +136,7 @@ public class CLIManager
options.addOption( Option.builder( FAIL_FAST ).longOpt( "fail-fast" ).desc( "Stop at first failure in reactorized builds" ).build() );
options.addOption( Option.builder( FAIL_AT_END ).longOpt( "fail-at-end" ).desc( "Only fail the build afterwards; allow all non-impacted builds to continue" ).build() );
options.addOption( Option.builder( FAIL_NEVER ).longOpt( "fail-never" ).desc( "NEVER fail the build, regardless of project result" ).build() );
options.addOption( Option.builder( RESUME ).longOpt( "resume" ).desc( "Resume reactor from the last failed project, using the resume.properties file in the build directory " ).build() );
options.addOption( Option.builder( RESUME_FROM ).longOpt( "resume-from" ).hasArg().desc( "Resume reactor from specified project" ).build() );
options.addOption( Option.builder( PROJECT_LIST ).longOpt( "projects" ).desc( "Comma-delimited list of specified reactor projects to build instead of all projects. A project can be specified by [groupId]:artifactId or by its relative path" ).hasArg().build() );
options.addOption( Option.builder( ALSO_MAKE ).longOpt( "also-make" ).desc( "If project list is specified, also build projects required by the list" ).build() );

View File

@ -48,6 +48,7 @@ import org.apache.maven.eventspy.internal.EventSpyDispatcher;
import org.apache.maven.exception.DefaultExceptionHandler;
import org.apache.maven.exception.ExceptionHandler;
import org.apache.maven.exception.ExceptionSummary;
import org.apache.maven.execution.BuildResumptionDataRepository;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.ExecutionListener;
import org.apache.maven.execution.MavenExecutionRequest;
@ -168,6 +169,8 @@ public class MavenCli
private Map<String, ConfigurationProcessor> configurationProcessors;
private BuildResumptionDataRepository buildResumptionDataRepository;
public MavenCli()
{
this( null );
@ -705,6 +708,8 @@ public class MavenCli
dispatcher = (DefaultSecDispatcher) container.lookup( SecDispatcher.class, "maven" );
buildResumptionDataRepository = container.lookup( BuildResumptionDataRepository.class );
return container;
}
@ -996,7 +1001,8 @@ public class MavenCli
if ( project == null && exception instanceof LifecycleExecutionException )
{
project = ( (LifecycleExecutionException) exception ).getProject();
LifecycleExecutionException lifecycleExecutionException = (LifecycleExecutionException) exception;
project = lifecycleExecutionException.getProject();
}
}
@ -1025,12 +1031,15 @@ public class MavenCli
}
}
if ( project != null && !project.equals( result.getTopologicallySortedProjects().get( 0 ) ) )
List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
if ( result.canResume() )
{
slf4jLogger.error( "" );
slf4jLogger.error( "After correcting the problems, you can resume the build with the command" );
slf4jLogger.error( buffer().a( " " ).strong( "mvn <args> -rf "
+ getResumeFrom( result.getTopologicallySortedProjects(), project ) ).toString() );
logBuildResumeHint( "mvn <args> -r " );
}
else if ( project != null && !project.equals( sortedProjects.get( 0 ) ) )
{
String resumeFromSelector = getResumeFromSelector( sortedProjects, project );
logBuildResumeHint( "mvn <args> -rf " + resumeFromSelector );
}
if ( MavenExecutionRequest.REACTOR_FAIL_NEVER.equals( cliRequest.request.getReactorFailureBehavior() ) )
@ -1050,32 +1059,41 @@ public class MavenCli
}
}
private void logBuildResumeHint( String resumeBuildHint )
{
slf4jLogger.error( "" );
slf4jLogger.error( "After correcting the problems, you can resume the build with the command" );
slf4jLogger.error( buffer().a( " " ).strong( resumeBuildHint ).toString() );
}
/**
* A helper method to determine the value to resume the build with {@code -rf} taking into account the
* edge case where multiple modules in the reactor have the same artifactId.
* A helper method to determine the value to resume the build with {@code -rf} taking into account the edge case
* where multiple modules in the reactor have the same artifactId.
* <p>
* {@code -rf :artifactId} will pick up the first module which matches, but when multiple modules in the
* reactor have the same artifactId, effective failed module might be later in build reactor.
* This means that developer will either have to type groupId or wait for build execution of all modules
* which were fine, but they are still before one which reported errors.
* {@code -rf :artifactId} will pick up the first module which matches, but when multiple modules in the reactor
* have the same artifactId, effective failed module might be later in build reactor.
* This means that developer will either have to type groupId or wait for build execution of all modules which
* were fine, but they are still before one which reported errors.
* <p>Then the returned value is {@code groupId:artifactId} when there is a name clash and
* {@code :artifactId} if there is no conflict.
* This method is made package-private for testing purposes.
*
* @param mavenProjects Maven projects which are part of build execution.
* @param failedProject Project which has failed.
* @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in
* general and {@code groupId:artifactId} when there is a name clash).
* @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in general
* and {@code groupId:artifactId} when there is a name clash).
*/
private String getResumeFrom( List<MavenProject> mavenProjects, MavenProject failedProject )
String getResumeFromSelector( List<MavenProject> mavenProjects, MavenProject failedProject )
{
for ( MavenProject buildProject : mavenProjects )
boolean hasOverlappingArtifactId = mavenProjects.stream()
.filter( project -> failedProject.getArtifactId().equals( project.getArtifactId() ) )
.count() > 1;
if ( hasOverlappingArtifactId )
{
if ( failedProject.getArtifactId().equals( buildProject.getArtifactId() ) && !failedProject.equals(
buildProject ) )
{
return failedProject.getGroupId() + ":" + failedProject.getArtifactId();
}
return failedProject.getGroupId() + ":" + failedProject.getArtifactId();
}
return ":" + failedProject.getArtifactId();
}
@ -1516,6 +1534,11 @@ public class MavenCli
request.setBaseDirectory( request.getPom().getParentFile() );
}
if ( commandLine.hasOption( CLIManager.RESUME ) )
{
request.setResume();
}
if ( commandLine.hasOption( CLIManager.RESUME_FROM ) )
{
request.setResumeFrom( commandLine.getOptionValue( CLIManager.RESUME_FROM ) );

View File

@ -19,6 +19,9 @@ package org.apache.maven.cli;
* under the License.
*/
import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -31,10 +34,12 @@ import static org.mockito.Mockito.times;
import java.io.File;
import java.util.Collections;
import java.util.List;
import org.apache.commons.cli.ParseException;
import org.apache.maven.Maven;
import org.apache.maven.eventspy.internal.EventSpyDispatcher;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.apache.maven.toolchain.building.ToolchainsBuildingRequest;
import org.apache.maven.toolchain.building.ToolchainsBuildingResult;
@ -346,4 +351,37 @@ public class MavenCliTest
orderdEventSpyDispatcherMock.verify(eventSpyDispatcherMock, times(1)).onEvent(any(ToolchainsBuildingResult.class));
}
@Test
public void resumeFromSelectorIsSuggestedWithoutGroupId()
{
List<MavenProject> allProjects = asList(
createMavenProject( "group", "module-a" ),
createMavenProject( "group", "module-b" ) );
MavenProject failedProject = allProjects.get( 0 );
String selector = cli.getResumeFromSelector( allProjects, failedProject );
assertThat( selector, is( ":module-a" ) );
}
@Test
public void resumeFromSelectorContainsGroupIdWhenArtifactIdIsNotUnique()
{
List<MavenProject> allProjects = asList(
createMavenProject( "group-a", "module" ),
createMavenProject( "group-b", "module" ) );
MavenProject failedProject = allProjects.get( 0 );
String selector = cli.getResumeFromSelector( allProjects, failedProject );
assertThat( selector, is( "group-a:module" ) );
}
private MavenProject createMavenProject( String groupId, String artifactId )
{
MavenProject project = new MavenProject();
project.setGroupId( groupId );
project.setArtifactId( artifactId );
return project;
}
}