#132 speed up tests by avoiding unneccessary compilations, allow parallel executions (with -DforkCount > 1 or with -Dparallel=classes -DthreadCount > 1)

This commit is contained in:
Andreas Gudian 2014-03-31 22:26:07 +02:00 committed by Gunnar Morling
parent da1050ab34
commit 25849adfee
3 changed files with 121 additions and 24 deletions

View File

@ -44,6 +44,8 @@
<com.jolira.hickory.version>1.0.0</com.jolira.hickory.version>
<org.apache.maven.plugins.enforcer.version>1.2</org.apache.maven.plugins.enforcer.version>
<org.springframework.version>3.2.3.RELEASE</org.springframework.version>
<forkCount>1</forkCount>
</properties>
<licenses>
@ -299,6 +301,9 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.17</version>
<configuration>
<forkCount>${forkCount}</forkCount>
</configuration>
</plugin>
<plugin>
<groupId>com.mycila.maven-license-plugin</groupId>

View File

@ -83,6 +83,15 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<mapper.test.output.dir>compilation-tests-${surefire.forkNumber}</mapper.test.output.dir>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>

View File

@ -33,7 +33,10 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
@ -49,7 +52,6 @@ import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutco
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption;
import org.mapstruct.ap.testutil.compilation.model.CompilationOutcomeDescriptor;
import org.mapstruct.ap.testutil.compilation.model.DiagnosticDescriptor;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
/**
@ -72,7 +74,28 @@ public abstract class MapperTestBase {
private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
private static final DiagnosticDescriptorComparator COMPARATOR = new DiagnosticDescriptorComparator();
private static volatile boolean enhancedClassloader = false;
private static final String TARGET_COMPILATION_TESTS = "/target/" + System.getProperty(
"mapper.test.output.dir",
"compilation-tests"
) + "_";
private static Map<Integer, Integer> threadsWithEnhancedClassloader = new ConcurrentHashMap<Integer, Integer>();
private static ThreadLocal<Integer> threadNumber = new ThreadLocal<Integer>() {
private AtomicInteger highWaterMark = new AtomicInteger( 0 );
@Override
protected Integer initialValue() {
return highWaterMark.getAndIncrement();
}
};
private static ThreadLocal<CompilationCache> compilationCache = new ThreadLocal<CompilationCache>() {
@Override
protected CompilationCache initialValue() {
return new CompilationCache();
}
};
private JavaCompiler compiler;
private String sourceDir;
@ -80,21 +103,21 @@ public abstract class MapperTestBase {
private String sourceOutputDir;
private List<File> classPath;
private final List<String> libraries;
private DiagnosticCollector<JavaFileObject> diagnostics;
public MapperTestBase() {
this.libraries = Arrays.asList( "mapstruct.jar", "guava.jar", "javax.inject.jar" );
}
@BeforeClass
public void setup() throws Exception {
protected void setupCompiler() throws Exception {
compiler = ToolProvider.getSystemJavaCompiler();
String basePath = getBasePath();
Integer i = threadNumber.get();
sourceDir = basePath + "/src/test/java";
classOutputDir = basePath + "/target/compilation-tests/classes";
sourceOutputDir = basePath + "/target/compilation-tests/generated-sources/mapping";
classOutputDir = basePath + TARGET_COMPILATION_TESTS + i + "/classes";
sourceOutputDir = basePath + TARGET_COMPILATION_TESTS + i + "/generated-sources/mapping";
String testDependenciesDir = basePath + "/target/test-dependencies/";
@ -106,7 +129,7 @@ public abstract class MapperTestBase {
createOutputDirs();
// TODO #140 Is there a better way to do this?
if ( !enhancedClassloader ) {
if ( !threadsWithEnhancedClassloader.containsKey( i ) ) {
// we need to make sure that the the generated classes are loaded by the same classloader as the test has
// been loaded already. Otherwise some tests won't work.
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
@ -114,22 +137,20 @@ public abstract class MapperTestBase {
Method method = clazz.getDeclaredMethod( "addURL", new Class[] { URL.class } );
method.setAccessible( true );
method.invoke( classLoader, new File( classOutputDir ).toURI().toURL() );
enhancedClassloader = true;
threadsWithEnhancedClassloader.put( i, i );
}
}
@BeforeMethod
public void generateMapperImplementation(Method testMethod) {
diagnostics = new DiagnosticCollector<JavaFileObject>();
Set<File> sourceFiles = getSourceFiles( getTestClasses( testMethod ) );
List<String> processorOptions = getProcessorOptions( testMethod );
boolean compilationSuccessful = compile( diagnostics, sourceFiles, processorOptions );
public void generateMapperImplementation(Method testMethod) throws Exception {
CompilationResultHolder compilationResult =
compile( getTestClasses( testMethod ), getProcessorOptions( testMethod ) );
CompilationOutcomeDescriptor actualResult = CompilationOutcomeDescriptor.forResult(
sourceDir,
compilationSuccessful,
diagnostics.getDiagnostics()
compilationResult.compilationSuccessful,
compilationResult.diagnostics.getDiagnostics()
);
CompilationOutcomeDescriptor expectedResult = CompilationOutcomeDescriptor.forExpectedCompilationResult(
testMethod.getAnnotation( ExpectedCompilationOutcome.class )
@ -137,7 +158,9 @@ public abstract class MapperTestBase {
if ( expectedResult.getCompilationResult() == CompilationResult.SUCCEEDED ) {
assertThat( actualResult.getCompilationResult() )
.describedAs( "Compilation failed. Diagnostics: " + diagnostics.getDiagnostics() )
.describedAs(
"Compilation failed. Diagnostics: " + compilationResult.diagnostics.getDiagnostics()
)
.isEqualTo( CompilationResult.SUCCEEDED );
}
else {
@ -260,11 +283,21 @@ public abstract class MapperTestBase {
return sourceFiles;
}
private boolean compile(DiagnosticCollector<JavaFileObject> diagnostics, Iterable<File> sourceFiles,
List<String> processorOptions) {
private CompilationResultHolder compile(Set<Class<?>> sourceClasses, List<String> processorOptions)
throws Exception {
CompilationRequest request = new CompilationRequest( sourceClasses, processorOptions );
CompilationCache cache = compilationCache.get();
if ( request.equals( cache.lastRequest ) ) {
return cache.lastResult;
}
setupCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles( sourceFiles );
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjectsFromFiles( getSourceFiles( sourceClasses ) );
try {
fileManager.setLocation( StandardLocation.CLASS_PATH, classPath );
@ -285,7 +318,11 @@ public abstract class MapperTestBase {
);
task.setProcessors( Arrays.asList( new MappingProcessor() ) );
return task.call();
CompilationResultHolder resultHolder = new CompilationResultHolder( diagnostics, task.call() );
cache.lastRequest = request;
cache.lastResult = resultHolder;
return resultHolder;
}
private String getBasePath() {
@ -345,7 +382,53 @@ public abstract class MapperTestBase {
}
}
private boolean isJdk6() {
return ( Integer.parseInt( System.getProperty( "java.version" ).split( "\\." )[1]) == 6 );
private static class CompilationCache {
private CompilationRequest lastRequest;
private CompilationResultHolder lastResult;
}
private static class CompilationResultHolder {
private DiagnosticCollector<JavaFileObject> diagnostics;
private boolean compilationSuccessful;
public CompilationResultHolder(DiagnosticCollector<JavaFileObject> diagnostics, boolean compilationSuccessful) {
this.diagnostics = diagnostics;
this.compilationSuccessful = compilationSuccessful;
}
}
private static class CompilationRequest {
private final Set<Class<?>> sourceClasses;
private final List<String> processorOptions;
public CompilationRequest(Set<Class<?>> sourceClasses, List<String> processorOptions) {
this.sourceClasses = sourceClasses;
this.processorOptions = processorOptions;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ( ( processorOptions == null ) ? 0 : processorOptions.hashCode() );
result = prime * result + ( ( sourceClasses == null ) ? 0 : sourceClasses.hashCode() );
return result;
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
CompilationRequest other = (CompilationRequest) obj;
return processorOptions.equals( other.processorOptions ) && sourceClasses.equals( other.sourceClasses );
}
}
}