From 25849adfee357bd3b3827ffcd2d2d51ff2d913d1 Mon Sep 17 00:00:00 2001 From: Andreas Gudian Date: Mon, 31 Mar 2014 22:26:07 +0200 Subject: [PATCH] #132 speed up tests by avoiding unneccessary compilations, allow parallel executions (with -DforkCount > 1 or with -Dparallel=classes -DthreadCount > 1) --- parent/pom.xml | 5 + processor/pom.xml | 9 ++ .../mapstruct/ap/testutil/MapperTestBase.java | 131 ++++++++++++++---- 3 files changed, 121 insertions(+), 24 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index 7e0e7118c..6051bcc51 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -44,6 +44,8 @@ 1.0.0 1.2 3.2.3.RELEASE + + 1 @@ -299,6 +301,9 @@ org.apache.maven.plugins maven-surefire-plugin 2.17 + + ${forkCount} + com.mycila.maven-license-plugin diff --git a/processor/pom.xml b/processor/pom.xml index 657ca26fd..eb12ede93 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -83,6 +83,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + compilation-tests-${surefire.forkNumber} + + + org.apache.maven.plugins maven-dependency-plugin diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/MapperTestBase.java b/processor/src/test/java/org/mapstruct/ap/testutil/MapperTestBase.java index 4e951d037..46f6f8de5 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/MapperTestBase.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/MapperTestBase.java @@ -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 threadsWithEnhancedClassloader = new ConcurrentHashMap(); + + private static ThreadLocal threadNumber = new ThreadLocal() { + private AtomicInteger highWaterMark = new AtomicInteger( 0 ); + + @Override + protected Integer initialValue() { + return highWaterMark.getAndIncrement(); + } + }; + + private static ThreadLocal compilationCache = new ThreadLocal() { + @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 classPath; private final List libraries; - private DiagnosticCollector 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(); - Set sourceFiles = getSourceFiles( getTestClasses( testMethod ) ); - List 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 diagnostics, Iterable sourceFiles, - List processorOptions) { + private CompilationResultHolder compile(Set> sourceClasses, List processorOptions) + throws Exception { + CompilationRequest request = new CompilationRequest( sourceClasses, processorOptions ); + + CompilationCache cache = compilationCache.get(); + if ( request.equals( cache.lastRequest ) ) { + return cache.lastResult; + } + + setupCompiler(); + DiagnosticCollector diagnostics = new DiagnosticCollector(); StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null ); - Iterable compilationUnits = fileManager.getJavaFileObjectsFromFiles( sourceFiles ); + Iterable 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 diagnostics; + private boolean compilationSuccessful; + + public CompilationResultHolder(DiagnosticCollector diagnostics, boolean compilationSuccessful) { + this.diagnostics = diagnostics; + this.compilationSuccessful = compilationSuccessful; + } + } + + private static class CompilationRequest { + private final Set> sourceClasses; + private final List processorOptions; + + public CompilationRequest(Set> sourceClasses, List 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 ); + } } }