From ff27b2f70d9c9b4a5d120d9a4b145941d0db2865 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sun, 9 Feb 2020 15:37:24 +0100 Subject: [PATCH] Migrate the processor test infrastructure from Junit 4 to JUnit Jupiter With JUnit Jupiter it is still not possible to set the ClassLoader for loading the test class. Therefore, use the ModifiedClassLoaderExtension (heavily inspired by the Spring Boot https://github.com/spring-projects/spring-boot/blob/bde7bd0a1a310f48fb877b9a0d4a05b8d829d6c0/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java). Once JUnit Jupiter 201 is resolved we can simplify this. With this extension we can catch all the tests in a class and then run them once the Class Extension Store gets closed with a modified ClassLoader. The CompilationCache is stored in the GlobalCache with the CompilationRequest as key. This means that even when methods are not executed in some particular order if they have same WithClasses then they would reuse the cache. --- parent/pom.xml | 2 +- processor/pom.xml | 15 +- .../mapstruct/ap/testutil/ProcessorTest.java | 52 +++++ .../runner/AnnotationProcessorTestRunner.java | 157 -------------- .../testutil/runner/CompilationRequest.java | 8 +- .../ap/testutil/runner/Compiler.java | 24 ++- .../CompilerTestEnabledOnJreCondition.java | 40 ++++ ...Statement.java => CompilingExtension.java} | 192 ++++++++--------- .../testutil/runner/DisabledOnCompiler.java | 26 --- ...nt.java => EclipseCompilingExtension.java} | 15 +- .../ap/testutil/runner/EnabledOnCompiler.java | 26 --- .../ap/testutil/runner/GeneratedSource.java | 67 +++--- .../InnerAnnotationProcessorRunner.java | 137 ------------ .../runner/Jdk11CompilingStatement.java | 37 ---- ...tement.java => JdkCompilingExtension.java} | 32 +-- .../runner/ModifiedClassLoaderExtension.java | 178 ++++++++++++++++ .../runner/ProcessorTestExtension.java | 48 +++++ .../ProcessorTestInvocationContext.java | 60 ++++++ .../testutil/runner/ReplacableTestClass.java | 199 ------------------ 19 files changed, 564 insertions(+), 751 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/ProcessorTest.java delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/AnnotationProcessorTestRunner.java create mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilerTestEnabledOnJreCondition.java rename processor/src/test/java/org/mapstruct/ap/testutil/runner/{CompilingStatement.java => CompilingExtension.java} (80%) delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/DisabledOnCompiler.java rename processor/src/test/java/org/mapstruct/ap/testutil/runner/{EclipseCompilingStatement.java => EclipseCompilingExtension.java} (93%) delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/EnabledOnCompiler.java delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/InnerAnnotationProcessorRunner.java delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/Jdk11CompilingStatement.java rename processor/src/test/java/org/mapstruct/ap/testutil/runner/{JdkCompilingStatement.java => JdkCompilingExtension.java} (80%) create mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/ModifiedClassLoaderExtension.java create mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestExtension.java create mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestInvocationContext.java delete mode 100644 processor/src/test/java/org/mapstruct/ap/testutil/runner/ReplacableTestClass.java diff --git a/parent/pom.xml b/parent/pom.xml index 3103f1c1c..6d4abbf47 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -24,7 +24,7 @@ 1.0.0.Alpha1 3.0.0-M1 - 3.0.0-M3 + 3.0.0-M4 3.1.0 4.0.3.RELEASE 0.26.0 diff --git a/processor/pom.xml b/processor/pom.xml index dfe9bdbe6..f734df3b7 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -49,8 +49,13 @@ - junit - junit + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine test @@ -104,6 +109,12 @@ test + + org.junit.platform + junit-platform-launcher + test + + joda-time diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/ProcessorTest.java b/processor/src/test/java/org/mapstruct/ap/testutil/ProcessorTest.java new file mode 100644 index 000000000..025a2ee09 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/testutil/ProcessorTest.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.testutil; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.ap.testutil.runner.Compiler; +import org.mapstruct.ap.testutil.runner.ModifiedClassLoaderExtension; +import org.mapstruct.ap.testutil.runner.ProcessorTestExtension; + +/** + * JUnit Jupiter test template for the MapStruct Processor tests. + *

+ * Test classes are safe to be executed in parallel, but test methods are not safe to be executed in parallel. + *

+ * By default this template would generate tests for the JDK and Eclipse Compiler. + * If only a single compiler is needed then specify the compiler in the value. + *

+ * The classes to be compiled for a given test method must be specified via {@link WithClasses}. In addition the + * following things can be configured optionally : + *

+ * + * @author Filip Hrisafov + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@TestTemplate +@ExtendWith(ProcessorTestExtension.class) +@ExtendWith(ModifiedClassLoaderExtension.class) +public @interface ProcessorTest { + + Compiler[] value() default { + Compiler.JDK, + Compiler.ECLIPSE + }; +} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/AnnotationProcessorTestRunner.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/AnnotationProcessorTestRunner.java deleted file mode 100644 index 6ddccf0eb..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/AnnotationProcessorTestRunner.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.List; - -import org.junit.runner.Description; -import org.junit.runner.Runner; -import org.junit.runner.manipulation.Filter; -import org.junit.runner.manipulation.NoTestsRemainException; -import org.junit.runner.notification.RunNotifier; -import org.junit.runners.BlockJUnit4ClassRunner; -import org.junit.runners.ParentRunner; -import org.mapstruct.ap.testutil.WithClasses; -import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; -import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; - -/** - * A JUnit4 runner for Annotation Processor tests. - *

- * Test classes are safe to be executed in parallel, but test methods are not safe to be executed in parallel. - *

- * The classes to be compiled for a given test method must be specified via {@link WithClasses}. In addition the - * following things can be configured optionally : - *

- * - * @author Gunnar Morling - * @author Andreas Gudian - */ -public class AnnotationProcessorTestRunner extends ParentRunner { - - private static final boolean IS_AT_LEAST_JAVA_9 = isIsAtLeastJava9(); - - private static boolean isIsAtLeastJava9() { - try { - Runtime.class.getMethod( "version" ); - return true; - } - catch ( NoSuchMethodException e ) { - return false; - } - } - - private final List runners; - - /** - * @param klass the test class - * - * @throws Exception see {@link BlockJUnit4ClassRunner#BlockJUnit4ClassRunner(Class)} - */ - public AnnotationProcessorTestRunner(Class klass) throws Exception { - super( klass ); - - runners = createRunners( klass ); - } - - @SuppressWarnings("deprecation") - private List createRunners(Class klass) throws Exception { - WithSingleCompiler singleCompiler = klass.getAnnotation( WithSingleCompiler.class ); - - if (singleCompiler != null) { - return Arrays.asList( new InnerAnnotationProcessorRunner( klass, singleCompiler.value() ) ); - } - else if ( IS_AT_LEAST_JAVA_9 ) { - // Current tycho-compiler-jdt (0.26.0) is not compatible with Java 11 - // Updating to latest version 1.3.0 fails some tests - // Once https://github.com/mapstruct/mapstruct/pull/1587 is resolved we can remove this line - return Arrays.asList( new InnerAnnotationProcessorRunner( klass, Compiler.JDK11 ) ); - } - - return Arrays.asList( - new InnerAnnotationProcessorRunner( klass, Compiler.JDK ), - new InnerAnnotationProcessorRunner( klass, Compiler.ECLIPSE ) - ); - } - - @Override - protected List getChildren() { - return runners; - } - - @Override - protected Description describeChild(Runner child) { - return child.getDescription(); - } - - @Override - protected void runChild(Runner child, RunNotifier notifier) { - child.run( notifier ); - } - - @Override - public void filter(Filter filter) throws NoTestsRemainException { - super.filter( new FilterDecorator( filter ) ); - } - - /** - * Allows to only execute selected methods, even if the executing framework is not aware of parameterized tests - * (e.g. some versions of IntelliJ, Netbeans, Eclipse). - */ - private static final class FilterDecorator extends Filter { - private final Filter delegate; - - private FilterDecorator(Filter delegate) { - this.delegate = delegate; - } - - @Override - public boolean shouldRun(Description description) { - boolean shouldRun = delegate.shouldRun( description ); - if ( !shouldRun ) { - return delegate.shouldRun( withoutParameterizedName( description ) ); - } - - return shouldRun; - } - - @Override - public String describe() { - return delegate.describe(); - } - - private Description withoutParameterizedName(Description description) { - String cleanDisplayName = removeParameter( description.getDisplayName() ); - Description cleanDescription = - Description.createSuiteDescription( - cleanDisplayName, - description.getAnnotations().toArray( new Annotation[description.getAnnotations().size()] ) ); - - for ( Description child : description.getChildren() ) { - cleanDescription.addChild( withoutParameterizedName( child ) ); - } - - return cleanDescription; - } - - private String removeParameter(String name) { - if ( name.startsWith( "[" ) ) { - return name; - } - - // remove "[compiler]" from "method[compiler](class)" - int open = name.indexOf( '[' ); - int close = name.indexOf( ']' ) + 1; - return name.substring( 0, open ) + name.substring( close ); - } - } -} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilationRequest.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilationRequest.java index 46a112da2..55230656b 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilationRequest.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilationRequest.java @@ -13,11 +13,13 @@ import java.util.Set; * Represents a compilation task for a number of sources with given processor options. */ public class CompilationRequest { + private final Compiler compiler; private final Set> sourceClasses; private final Map, Class> services; private final List processorOptions; - CompilationRequest(Set> sourceClasses, Map, Class> services, List processorOptions) { + CompilationRequest(Compiler compiler, Set> sourceClasses, Map, Class> services, List processorOptions) { + this.compiler = compiler; this.sourceClasses = sourceClasses; this.services = services; this.processorOptions = processorOptions; @@ -27,6 +29,7 @@ public class CompilationRequest { public int hashCode() { final int prime = 31; int result = 1; + result = prime * result + ( ( compiler == null ) ? 0 : compiler.hashCode() ); result = prime * result + ( ( processorOptions == null ) ? 0 : processorOptions.hashCode() ); result = prime * result + ( ( services == null ) ? 0 : services.hashCode() ); result = prime * result + ( ( sourceClasses == null ) ? 0 : sourceClasses.hashCode() ); @@ -46,7 +49,8 @@ public class CompilationRequest { } CompilationRequest other = (CompilationRequest) obj; - return processorOptions.equals( other.processorOptions ) + return compiler.equals( other.compiler ) + && processorOptions.equals( other.processorOptions ) && services.equals( other.services ) && sourceClasses.equals( other.sourceClasses ); } diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/Compiler.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/Compiler.java index d59a7ed0d..d2820c478 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/Compiler.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/Compiler.java @@ -5,10 +5,30 @@ */ package org.mapstruct.ap.testutil.runner; +import org.junit.jupiter.api.condition.JRE; + /** * @author Andreas Gudian - * + * @author Filip Hrisafov */ public enum Compiler { - JDK, JDK11, ECLIPSE; + JDK, + // Current tycho-compiler-jdt (0.26.0) is not compatible with Java 11 + // Updating to latest version 1.6.0 fails some tests + // Once https://github.com/mapstruct/mapstruct/pull/1587 is resolved we can remove the max JRE + ECLIPSE( JRE.JAVA_8 ); + + private final JRE max; + + Compiler() { + this( JRE.OTHER ); + } + + Compiler(JRE max) { + this.max = max; + } + + public JRE maxJre() { + return max; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilerTestEnabledOnJreCondition.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilerTestEnabledOnJreCondition.java new file mode 100644 index 000000000..ff9eecb7e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilerTestEnabledOnJreCondition.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.testutil.runner; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static org.mapstruct.ap.testutil.runner.ProcessorTestInvocationContext.CURRENT_VERSION; + +/** + * Every compiler is registered with it's max supported JRE that it can run on. + * This condition is used to check if the test for a particular compiler can be run with the current JRE. + * + * @author Filip Hrisafov + */ +public class CompilerTestEnabledOnJreCondition implements ExecutionCondition { + + static final ConditionEvaluationResult ENABLED_ON_CURRENT_JRE = + ConditionEvaluationResult.enabled( "Enabled on JRE version: " + System.getProperty( "java.version" ) ); + + static final ConditionEvaluationResult DISABLED_ON_CURRENT_JRE = + ConditionEvaluationResult.disabled( "Disabled on JRE version: " + System.getProperty( "java.version" ) ); + + protected final Compiler compiler; + + public CompilerTestEnabledOnJreCondition(Compiler compiler) { + this.compiler = compiler; + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + // If the max JRE is greater or equal to the current version the test is enabled + return compiler.maxJre().compareTo( CURRENT_VERSION ) >= 0 ? ENABLED_ON_CURRENT_JRE : + DISABLED_ON_CURRENT_JRE; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java similarity index 80% rename from processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java rename to processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java index ca51554f3..30aca1bf2 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java @@ -5,16 +5,14 @@ */ package org.mapstruct.ap.testutil.runner; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -25,35 +23,40 @@ import java.util.Properties; import java.util.Set; import java.util.stream.Stream; -import com.puppycrawl.tools.checkstyle.api.AutomaticBean; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.Statement; -import org.mapstruct.ap.testutil.WithClasses; -import org.mapstruct.ap.testutil.WithServiceImplementation; -import org.mapstruct.ap.testutil.WithServiceImplementations; -import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; -import org.mapstruct.ap.testutil.compilation.annotation.DisableCheckstyle; -import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; -import org.mapstruct.ap.testutil.compilation.annotation.ExpectedNote; -import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; -import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOptions; -import org.mapstruct.ap.testutil.compilation.model.CompilationOutcomeDescriptor; -import org.mapstruct.ap.testutil.compilation.model.DiagnosticDescriptor; -import org.xml.sax.InputSource; - import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import com.puppycrawl.tools.checkstyle.Checker; import com.puppycrawl.tools.checkstyle.ConfigurationLoader; import com.puppycrawl.tools.checkstyle.DefaultLogger; import com.puppycrawl.tools.checkstyle.PropertiesExpander; +import com.puppycrawl.tools.checkstyle.api.AutomaticBean; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.DisableCheckstyle; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedNote; +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.xml.sax.InputSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; +import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; +import static org.mapstruct.ap.testutil.runner.ModifiedClassLoaderExtension.isModifiedClassPathClassLoader; /** - * A JUnit4 statement that performs source generation using the annotation processor and compiles those sources. + * A JUnit Jupiter Extension that performs source generation using the annotation processor and compiles those sources. * * @author Andreas Gudian + * @author Filip Hrisafov */ -abstract class CompilingStatement extends Statement { +abstract class CompilingExtension implements BeforeEachCallback { + + static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create( new Object() ); private static final String TARGET_COMPILATION_TESTS = "/target/compilation-tests/"; @@ -67,46 +70,20 @@ abstract class CompilingStatement extends Statement { protected static final List PROCESSOR_CLASSPATH = buildProcessorClasspath(); - private final FrameworkMethod method; - private final CompilationCache compilationCache; - private final boolean runCheckstyle; - private Statement next; - private String classOutputDir; private String sourceOutputDir; private String additionalCompilerClasspath; - private CompilationRequest compilationRequest; + private final Compiler compiler; - CompilingStatement(FrameworkMethod method, CompilationCache compilationCache) { - this.method = method; - this.compilationCache = compilationCache; - this.runCheckstyle = !method.getMethod().getDeclaringClass().isAnnotationPresent( DisableCheckstyle.class ); - - this.compilationRequest = new CompilationRequest( getTestClasses(), getServices(), getProcessorOptions() ); + protected CompilingExtension(Compiler compiler) { + this.compiler = compiler; } - void setNextStatement(Statement next) { - this.next = next; - } - - @Override - public void evaluate() throws Throwable { - generateMapperImplementation(); - - GeneratedSource.setCompilingStatement( this ); - next.evaluate(); - GeneratedSource.clearCompilingStatement(); - } - - String getSourceOutputDir() { - return compilationCache.getLastSourceOutputDir(); - } - - protected void setupDirectories() { + protected void setupDirectories(Method testMethod, Class testClass) { String compilationRoot = getBasePath() + TARGET_COMPILATION_TESTS - + method.getDeclaringClass().getName() - + "/" + method.getName() + + testClass.getName() + + "/" + testMethod.getName() + getPathSuffix(); classOutputDir = compilationRoot + "/classes"; @@ -118,7 +95,9 @@ abstract class CompilingStatement extends Statement { ( (ModifiableURLClassLoader) Thread.currentThread().getContextClassLoader() ).withPath( classOutputDir ); } - protected abstract String getPathSuffix(); + protected String getPathSuffix() { + return "_" + compiler.name().toLowerCase(); + } private static List buildTestCompilationClasspath() { String[] whitelist = @@ -169,14 +148,25 @@ abstract class CompilingStatement extends Statement { return Stream.of( whitelist ).anyMatch( path::contains ); } - protected void generateMapperImplementation() throws Exception { - CompilationOutcomeDescriptor actualResult = compile(); + @Override + public void beforeEach(ExtensionContext context) throws Exception { + if ( !isModifiedClassPathClassLoader( context ) ) { + return; + } + + CompilationOutcomeDescriptor actualResult = compile( context ); + assertResult( actualResult, context ); + } + + private void assertResult(CompilationOutcomeDescriptor actualResult, ExtensionContext context) throws Exception { + Method testMethod = context.getRequiredTestMethod(); + Class testClass = context.getRequiredTestClass(); CompilationOutcomeDescriptor expectedResult = CompilationOutcomeDescriptor.forExpectedCompilationResult( - method.getAnnotation( ExpectedCompilationOutcome.class ), - method.getAnnotation( ExpectedNote.ExpectedNotes.class ), - method.getAnnotation( ExpectedNote.class ) + findAnnotation( testMethod, ExpectedCompilationOutcome.class ).orElse( null ), + findAnnotation( testMethod, ExpectedNote.ExpectedNotes.class ).orElse( null ), + findAnnotation( testMethod, ExpectedNote.class ).orElse( null ) ); if ( expectedResult.getCompilationResult() == CompilationResult.SUCCEEDED ) { @@ -195,7 +185,7 @@ abstract class CompilingStatement extends Statement { assertDiagnostics( actualResult.getDiagnostics(), expectedResult.getDiagnostics() ); assertNotes( actualResult.getNotes(), expectedResult.getNotes() ); - if ( runCheckstyle ) { + if ( !findAnnotation( testClass, DisableCheckstyle.class ).isPresent() ) { assertCheckstyleRules(); } } @@ -335,18 +325,14 @@ abstract class CompilingStatement extends Statement { * * @return A set containing the classes to be compiled for this test */ - private Set> getTestClasses() { + private Set> getTestClasses(Method testMethod, Class testClass) { Set> testClasses = new HashSet<>(); - WithClasses withClasses = method.getAnnotation( WithClasses.class ); - if ( withClasses != null ) { - testClasses.addAll( Arrays.asList( withClasses.value() ) ); - } + findAnnotation( testMethod, WithClasses.class ) + .ifPresent( withClasses -> testClasses.addAll( Arrays.asList( withClasses.value() ) ) ); - withClasses = method.getMethod().getDeclaringClass().getAnnotation( WithClasses.class ); - if ( withClasses != null ) { - testClasses.addAll( Arrays.asList( withClasses.value() ) ); - } + findAnnotation( testClass, WithClasses.class ) + .ifPresent( withClasses -> testClasses.addAll( Arrays.asList( withClasses.value() ) ) ); if ( testClasses.isEmpty() ) { throw new IllegalStateException( @@ -363,24 +349,19 @@ abstract class CompilingStatement extends Statement { * @return A map containing the package were to look for a resource (key) and the resource (value) to be compiled * for this test */ - private Map, Class> getServices() { + private Map, Class> getServices(Method testMethod, Class testClass) { Map, Class> services = new HashMap<>(); - addServices( services, method.getAnnotation( WithServiceImplementations.class ) ); - addService( services, method.getAnnotation( WithServiceImplementation.class ) ); + addServices( services, findRepeatableAnnotations( testMethod, WithServiceImplementation.class ) ); - Class declaringClass = method.getMethod().getDeclaringClass(); - addServices( services, declaringClass.getAnnotation( WithServiceImplementations.class ) ); - addService( services, declaringClass.getAnnotation( WithServiceImplementation.class ) ); + addServices( services, findRepeatableAnnotations( testClass, WithServiceImplementation.class ) ); return services; } - private void addServices(Map, Class> services, WithServiceImplementations withImplementations) { - if ( withImplementations != null ) { - for ( WithServiceImplementation resource : withImplementations.value() ) { - addService( services, resource ); - } + private void addServices(Map, Class> services, List withImplementations) { + for ( WithServiceImplementation withImplementation : withImplementations ) { + addService( services, withImplementation ); } } @@ -411,17 +392,11 @@ abstract class CompilingStatement extends Statement { * * @return A list containing the processor options to be used for this test */ - private List getProcessorOptions() { - List processorOptions = - getProcessorOptions( - method.getAnnotation( ProcessorOptions.class ), - method.getAnnotation( ProcessorOption.class ) ); + private List getProcessorOptions(Method testMethod, Class testClass) { + List processorOptions = findRepeatableAnnotations( testMethod, ProcessorOption.class ); if ( processorOptions.isEmpty() ) { - processorOptions = - getProcessorOptions( - method.getMethod().getDeclaringClass().getAnnotation( ProcessorOptions.class ), - method.getMethod().getDeclaringClass().getAnnotation( ProcessorOption.class ) ); + processorOptions = findRepeatableAnnotations( testClass, ProcessorOption.class ); } List result = new ArrayList<>( processorOptions.size() ); @@ -435,17 +410,6 @@ abstract class CompilingStatement extends Statement { return result; } - private List getProcessorOptions(ProcessorOptions options, ProcessorOption option) { - if ( options != null ) { - return Arrays.asList( options.value() ); - } - else if ( option != null ) { - return Arrays.asList( option ); - } - - return Collections.emptyList(); - } - private String asOptionString(ProcessorOption processorOption) { return String.format( "-A%s=%s", processorOption.name(), processorOption.value() ); } @@ -465,17 +429,31 @@ abstract class CompilingStatement extends Statement { return sourceFiles; } - private CompilationOutcomeDescriptor compile() - throws Exception { + private CompilationOutcomeDescriptor compile(ExtensionContext context) { + Method testMethod = context.getRequiredTestMethod(); + Class testClass = context.getRequiredTestClass(); - if ( !needsRecompilation() ) { + CompilationRequest compilationRequest = new CompilationRequest( + compiler, + getTestClasses( testMethod, testClass ), + getServices( testMethod, testClass ), + getProcessorOptions( testMethod, testClass ) + ); + + ExtensionContext.Store rootStore = context.getRoot().getStore( NAMESPACE ); + + context.getStore( NAMESPACE ).put( context.getUniqueId() + "-compilationRequest", compilationRequest ); + CompilationCache compilationCache = rootStore + .getOrComputeIfAbsent( compilationRequest, request -> new CompilationCache(), CompilationCache.class ); + + if ( !needsRecompilation( compilationRequest, compilationCache ) ) { return compilationCache.getLastResult(); } - setupDirectories(); + setupDirectories( testMethod, testClass ); compilationCache.setLastSourceOutputDir( sourceOutputDir ); - boolean needsAdditionalCompilerClasspath = prepareServices(); + boolean needsAdditionalCompilerClasspath = prepareServices( compilationRequest ); CompilationOutcomeDescriptor resultHolder; resultHolder = compileWithSpecificCompiler( @@ -503,7 +481,7 @@ abstract class CompilingStatement extends Statement { String classOutputDir, String additionalCompilerClasspath); - boolean needsRecompilation() { + boolean needsRecompilation(CompilationRequest compilationRequest, CompilationCache compilationCache) { return !compilationRequest.equals( compilationCache.getLastRequest() ); } @@ -545,7 +523,7 @@ abstract class CompilingStatement extends Statement { path.delete(); } - private boolean prepareServices() { + private boolean prepareServices(CompilationRequest compilationRequest) { if ( !compilationRequest.getServices().isEmpty() ) { String servicesDir = additionalCompilerClasspath + File.separator + "META-INF" + File.separator + "services"; diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/DisabledOnCompiler.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/DisabledOnCompiler.java deleted file mode 100644 index 41bcdb100..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/DisabledOnCompiler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * This should be used with care. - * This is similar to the JUnit 5 DisabledOnJre (once we have JUnit 5 we can replace this one) - * - * @author Filip Hrisafov - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface DisabledOnCompiler { - /** - * @return The compiler to use. - */ - Compiler value(); -} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingStatement.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingExtension.java similarity index 93% rename from processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingStatement.java rename to processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingExtension.java index a0617322e..4498e4496 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingStatement.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/EclipseCompilingExtension.java @@ -14,16 +14,16 @@ import org.codehaus.plexus.compiler.CompilerException; import org.codehaus.plexus.compiler.CompilerResult; import org.codehaus.plexus.logging.console.ConsoleLogger; import org.eclipse.tycho.compiler.jdt.JDTCompiler; -import org.junit.runners.model.FrameworkMethod; import org.mapstruct.ap.MappingProcessor; import org.mapstruct.ap.testutil.compilation.model.CompilationOutcomeDescriptor; /** - * Statement that uses the Eclipse JDT compiler to compile. + * Extension that uses the Eclipse JDT compiler to compile. * * @author Andreas Gudian + * @author Filip Hrisafov */ -class EclipseCompilingStatement extends CompilingStatement { +class EclipseCompilingExtension extends CompilingExtension { private static final List ECLIPSE_COMPILER_CLASSPATH = buildEclipseCompilerClasspath(); @@ -33,8 +33,8 @@ class EclipseCompilingStatement extends CompilingStatement { .withPaths( PROCESSOR_CLASSPATH ) .withOriginOf( ClassLoaderExecutor.class ); - EclipseCompilingStatement(FrameworkMethod method, CompilationCache compilationCache) { - super( method, compilationCache ); + EclipseCompilingExtension() { + super( Compiler.ECLIPSE ); } @Override @@ -140,9 +140,4 @@ class EclipseCompilingStatement extends CompilingStatement { return filterBootClassPath( whitelist ); } - - @Override - protected String getPathSuffix() { - return "_eclipse"; - } } diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/EnabledOnCompiler.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/EnabledOnCompiler.java deleted file mode 100644 index e297bd2c6..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/EnabledOnCompiler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * This should be used with care. - * This is similar to the JUnit 5 EnabledOnJre (once we have JUnit 5 we can replace this one) - * - * @author Filip Hrisafov - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface EnabledOnCompiler { - /** - * @return The compiler to use. - */ - Compiler value(); -} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java index f5a98bfee..616d414d1 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java @@ -13,48 +13,67 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; import org.mapstruct.ap.testutil.assertions.JavaFileAssert; import static org.assertj.core.api.Assertions.fail; +import static org.mapstruct.ap.testutil.runner.CompilingExtension.NAMESPACE; +import static org.mapstruct.ap.testutil.runner.ModifiedClassLoaderExtension.isModifiedClassPathClassLoader; /** - * A {@link TestRule} to perform assertions on generated source files. + * A {@link org.junit.jupiter.api.extension.RegisterExtension RegisterExtension} to perform assertions on generated + * source files. *

* To add it to the test, use: * *

- * @Rule
- * public GeneratedSource generatedSources = new GeneratedSource();
+ * @RegisterExtension
+ * final GeneratedSource generatedSources = new GeneratedSource();
  * 
* * @author Andreas Gudian */ -public class GeneratedSource implements TestRule { +public class GeneratedSource implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final String FIXTURES_ROOT = "fixtures/"; /** - * static ThreadLocal, as the {@link CompilingStatement} must inject itself statically for this rule to gain access - * to the statement's information. As test execution of different classes in parallel is supported. + * ThreadLocal, as the source dir must be injected for this extension to gain access + * to the compilation information. As test execution of different classes in parallel is supported. */ - private static ThreadLocal compilingStatement = new ThreadLocal<>(); + private ThreadLocal sourceOutputDir = new ThreadLocal<>(); private List> fixturesFor = new ArrayList<>(); @Override - public Statement apply(Statement base, Description description) { - return new GeneratedSourceStatement( base ); + public void beforeTestExecution(ExtensionContext context) throws Exception { + if ( !isModifiedClassPathClassLoader( context ) ) { + return; + } + CompilationRequest compilationRequest = context.getStore( NAMESPACE ) + .get( context.getUniqueId() + "-compilationRequest", CompilationRequest.class ); + setSourceOutputDir( context.getStore( NAMESPACE ) + .get( compilationRequest, CompilationCache.class ) + .getLastSourceOutputDir() ); } - static void setCompilingStatement(CompilingStatement compilingStatement) { - GeneratedSource.compilingStatement.set( compilingStatement ); + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + if ( !isModifiedClassPathClassLoader( context ) ) { + return; + } + handleFixtureComparison(); + clearSourceOutputDir(); } - static void clearCompilingStatement() { - GeneratedSource.compilingStatement.remove(); + private void setSourceOutputDir(String sourceOutputDir) { + this.sourceOutputDir.set( sourceOutputDir ); + } + + private void clearSourceOutputDir() { + this.sourceOutputDir.remove(); } /** @@ -101,21 +120,7 @@ public class GeneratedSource implements TestRule { * @return an assert for the file specified by the given path */ public JavaFileAssert forJavaFile(String path) { - return new JavaFileAssert( new File( compilingStatement.get().getSourceOutputDir() + "/" + path ) ); - } - - private class GeneratedSourceStatement extends Statement { - private final Statement next; - - private GeneratedSourceStatement(Statement next) { - this.next = next; - } - - @Override - public void evaluate() throws Throwable { - next.evaluate(); - handleFixtureComparison(); - } + return new JavaFileAssert( new File( sourceOutputDir.get() + "/" + path ) ); } private void handleFixtureComparison() throws UnsupportedEncodingException { diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/InnerAnnotationProcessorRunner.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/InnerAnnotationProcessorRunner.java deleted file mode 100644 index 4850a4870..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/InnerAnnotationProcessorRunner.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import org.junit.runners.BlockJUnit4ClassRunner; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.Statement; -import org.junit.runners.model.TestClass; - -/** - * Internal test runner that runs the tests of one class for one specific compiler implementation. - * - * @author Andreas Gudian - */ -class InnerAnnotationProcessorRunner extends BlockJUnit4ClassRunner { - static final ModifiableURLClassLoader TEST_CLASS_LOADER = new ModifiableURLClassLoader(); - private final Class klass; - private final Compiler compiler; - private final CompilationCache compilationCache; - private Class klassToUse; - private ReplacableTestClass replacableTestClass; - - /** - * @param klass the test class - * - * @throws Exception see {@link BlockJUnit4ClassRunner#BlockJUnit4ClassRunner(Class)} - */ - InnerAnnotationProcessorRunner(Class klass, Compiler compiler) throws Exception { - super( klass ); - this.klass = klass; - this.compiler = compiler; - this.compilationCache = new CompilationCache(); - } - - /** - * newly loads the class with the test class loader and sets that loader as context class loader of the thread - * - * @param klass the class to replace - * - * @return the class loaded with the test class loader - */ - private static Class replaceClassLoaderAndClass(Class klass) { - replaceContextClassLoader( klass ); - - try { - return Thread.currentThread().getContextClassLoader().loadClass( klass.getName() ); - } - catch ( ClassNotFoundException e ) { - throw new RuntimeException( e ); - } - - } - - private static void replaceContextClassLoader(Class klass) { - ModifiableURLClassLoader testClassLoader = new ModifiableURLClassLoader().withOriginOf( klass ); - - Thread.currentThread().setContextClassLoader( testClassLoader ); - } - - @Override - protected boolean isIgnored(FrameworkMethod child) { - return super.isIgnored( child ) || isIgnoredForCompiler( child ); - } - - protected boolean isIgnoredForCompiler(FrameworkMethod child) { - EnabledOnCompiler enabledOnCompiler = child.getAnnotation( EnabledOnCompiler.class ); - if ( enabledOnCompiler != null ) { - return enabledOnCompiler.value() != compiler; - } - - DisabledOnCompiler disabledOnCompiler = child.getAnnotation( DisabledOnCompiler.class ); - if ( disabledOnCompiler != null ) { - return disabledOnCompiler.value() == compiler; - } - - return false; - } - - @Override - protected TestClass createTestClass(final Class testClass) { - replacableTestClass = new ReplacableTestClass( testClass ); - return replacableTestClass; - } - - private FrameworkMethod replaceFrameworkMethod(FrameworkMethod m) { - try { - return new FrameworkMethod( - klassToUse.getDeclaredMethod( m.getName(), m.getMethod().getParameterTypes() ) ); - } - catch ( NoSuchMethodException e ) { - throw new RuntimeException( e ); - } - } - - @Override - protected Statement methodBlock(FrameworkMethod method) { - CompilingStatement statement = createCompilingStatement( method ); - if ( statement.needsRecompilation() ) { - klassToUse = replaceClassLoaderAndClass( klass ); - - replacableTestClass.replaceClass( klassToUse ); - } - - method = replaceFrameworkMethod( method ); - - Statement next = super.methodBlock( method ); - - statement.setNextStatement( next ); - - return statement; - } - - private CompilingStatement createCompilingStatement(FrameworkMethod method) { - if ( compiler == Compiler.JDK ) { - return new JdkCompilingStatement( method, compilationCache ); - } - else if ( compiler == Compiler.JDK11 ) { - return new Jdk11CompilingStatement( method, compilationCache ); - } - else { - return new EclipseCompilingStatement( method, compilationCache ); - } - } - - @Override - protected String getName() { - return "[" + compiler.name().toLowerCase() + "]"; - } - - @Override - protected String testName(FrameworkMethod method) { - return method.getName() + getName(); - } -} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/Jdk11CompilingStatement.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/Jdk11CompilingStatement.java deleted file mode 100644 index 30f12aead..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/Jdk11CompilingStatement.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import java.util.List; - -import org.junit.runners.model.FrameworkMethod; -import org.mapstruct.ap.testutil.compilation.model.DiagnosticDescriptor; - -/** - * Statement that uses the JDK compiler to compile. - * - * @author Filip Hrisafov - */ -class Jdk11CompilingStatement extends JdkCompilingStatement { - - Jdk11CompilingStatement(FrameworkMethod method, CompilationCache compilationCache) { - super( method, compilationCache ); - } - - - /** - * The JDK 11 compiler reports all ERROR diagnostics properly. Also when there are multiple per line. - */ - @Override - protected List filterExpectedDiagnostics(List expectedDiagnostics) { - return expectedDiagnostics; - } - - @Override - protected String getPathSuffix() { - return "_jdk"; - } -} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingStatement.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingExtension.java similarity index 80% rename from processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingStatement.java rename to processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingExtension.java index 454f2c3c3..ca4732371 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingStatement.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/JdkCompilingExtension.java @@ -11,7 +11,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; - import javax.annotation.processing.Processor; import javax.tools.Diagnostic.Kind; import javax.tools.DiagnosticCollector; @@ -22,17 +21,20 @@ import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import javax.tools.ToolProvider; -import org.junit.runners.model.FrameworkMethod; +import org.junit.jupiter.api.condition.JRE; import org.mapstruct.ap.MappingProcessor; import org.mapstruct.ap.testutil.compilation.model.CompilationOutcomeDescriptor; import org.mapstruct.ap.testutil.compilation.model.DiagnosticDescriptor; +import static org.mapstruct.ap.testutil.runner.ProcessorTestInvocationContext.CURRENT_VERSION; + /** - * Statement that uses the JDK compiler to compile. + * Extension that uses the JDK compiler to compile. * * @author Andreas Gudian + * @author Filip Hrisafov */ -class JdkCompilingStatement extends CompilingStatement { +class JdkCompilingExtension extends CompilingExtension { private static final List COMPILER_CLASSPATH_FILES = asFiles( TEST_COMPILATION_CLASSPATH ); @@ -40,8 +42,8 @@ class JdkCompilingStatement extends CompilingStatement { new ModifiableURLClassLoader( new FilteringParentClassLoader( "org.mapstruct." ) ) .withPaths( PROCESSOR_CLASSPATH ); - JdkCompilingStatement(FrameworkMethod method, CompilationCache compilationCache) { - super( method, compilationCache ); + JdkCompilingExtension() { + super( Compiler.JDK ); } @Override @@ -107,14 +109,20 @@ class JdkCompilingStatement extends CompilingStatement { } /** - * The JDK compiler only reports the first message of kind ERROR that is reported for one source file line, so we - * filter out the surplus diagnostics. The input list is already sorted by file name and line number, with the order - * for the diagnostics in the same line being kept at the order as given in the test. + * The JDK 8 compiler needs some special treatment for the diagnostics. + * See comment in the function. */ @Override protected List filterExpectedDiagnostics(List expectedDiagnostics) { - List filtered = new ArrayList<>( expectedDiagnostics.size() ); + if ( CURRENT_VERSION != JRE.JAVA_8 ) { + // The JDK 8+ compilers report all ERROR diagnostics properly. Also when there are multiple per line. + return expectedDiagnostics; + } + List filtered = new ArrayList( expectedDiagnostics.size() ); + // The JDK 8 compiler only reports the first message of kind ERROR that is reported for one source file line, + // so we filter out the surplus diagnostics. The input list is already sorted by file name and line number, + // with the order for the diagnostics in the same line being kept at the order as given in the test. DiagnosticDescriptor previous = null; for ( DiagnosticDescriptor diag : expectedDiagnostics ) { if ( diag.getKind() != Kind.ERROR @@ -129,8 +137,4 @@ class JdkCompilingStatement extends CompilingStatement { return filtered; } - @Override - protected String getPathSuffix() { - return "_jdk"; - } } diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/ModifiedClassLoaderExtension.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ModifiedClassLoaderExtension.java new file mode 100644 index 000000000..3ca16969c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ModifiedClassLoaderExtension.java @@ -0,0 +1,178 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.testutil.runner; + +import java.lang.reflect.Method; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; +import org.springframework.util.CollectionUtils; + +import static org.mapstruct.ap.testutil.runner.CompilingExtension.NAMESPACE; + +// CHECKSTYLE:OFF +/** + * Special extension which is responsible for making sure that the tests are run with the + * {@link ModifiableURLClassLoader}. + * Otherwise, methods and classes might not be properly shared. + *

+ * It intercepts all methods and if the test class was not loaded with the {@link ModifiableURLClassLoader} + * then registers a selector for the test case to be run once the test is done. + * The run is done by setting the Thread Context ClassLoader and manually invoking the {@link Launcher} + * for the needed tests and test templates. + * In order to be able to reuse the compilation caching we are running all tests once the current Class Extension + * Context is closed. + *

+ * This mechanism is needed since there is no way to register a custom ClassLoader for creating the test instance + * in JUnit Jupiter (see junit-test/junit5#201 + * for more information). Once there is support for registering a custom class loader we can simplify this. + *

+ * This logic was heavily inspired and is really similar to the Spring Boot + * ModifiedClassPathExtension. + * + * @author Filip Hrisafov + * @see ModifiableURLClassLoader + */ +// CHECKSTYLE:ON +public class ModifiedClassLoaderExtension implements InvocationInterceptor { + + @Override + public void interceptBeforeAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept( invocation, extensionContext ); + } + + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept( invocation, extensionContext ); + } + + @Override + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept( invocation, extensionContext ); + } + + @Override + public void interceptAfterAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept( invocation, extensionContext ); + } + + @Override + public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + if ( isModifiedClassPathClassLoader( extensionContext ) ) { + invocation.proceed(); + return; + } + invocation.skip(); + // For normal Tests the path to the Class Store is: + // method -> class + // This will most likely never be the case for a processor test. + ExtensionContext.Store store = extensionContext.getParent() + .orElseThrow( () -> new IllegalStateException( extensionContext + " has no parent store " ) ) + .getStore( NAMESPACE ); + registerSelector( extensionContext, store ); + } + + @Override + public void interceptTestTemplateMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + if ( isModifiedClassPathClassLoader( extensionContext ) ) { + invocation.proceed(); + return; + } + invocation.skip(); + // For TestTemplates the path to the Class Store is: + // method -> testTemplate -> class + ExtensionContext.Store store = extensionContext.getParent() + .flatMap( ExtensionContext::getParent ) + .orElseThrow( () -> new IllegalStateException( extensionContext + " has no parent store " ) ) + .getStore( NAMESPACE ); + registerSelector( extensionContext, store ); + } + + private void registerSelector(ExtensionContext context, ExtensionContext.Store store) { + store.getOrComputeIfAbsent( + context.getRequiredTestClass() + "-discoverySelectors", + s -> new SelectorsToRun( context.getRequiredTestClass() ), + SelectorsToRun.class + ).discoverySelectors.add( DiscoverySelectors.selectUniqueId( context.getUniqueId() ) ); + } + + private static void runTestWithModifiedClassPath(Class testClass, List selectors) + throws Throwable { + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + URLClassLoader modifiedClassLoader = + new ModifiableURLClassLoader().withOriginOf( testClass ); + Thread.currentThread().setContextClassLoader( modifiedClassLoader ); + try { + runTest( selectors ); + } + finally { + Thread.currentThread().setContextClassLoader( originalClassLoader ); + } + } + + private static void runTest(List selectors) throws Throwable { + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors( selectors ) + .build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover( request ); + SummaryGeneratingListener listener = new SummaryGeneratingListener(); + launcher.registerTestExecutionListeners( listener ); + launcher.execute( testPlan ); + TestExecutionSummary summary = listener.getSummary(); + if ( !CollectionUtils.isEmpty( summary.getFailures() ) ) { + throw summary.getFailures().get( 0 ).getException(); + } + } + + private void intercept(Invocation invocation, ExtensionContext extensionContext) throws Throwable { + if ( isModifiedClassPathClassLoader( extensionContext ) ) { + invocation.proceed(); + return; + } + invocation.skip(); + } + + static boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + ClassLoader classLoader = testClass.getClassLoader(); + return classLoader.getClass().getName().equals( ModifiableURLClassLoader.class.getName() ); + } + + static class SelectorsToRun implements ExtensionContext.Store.CloseableResource { + + private final Class testClass; + private final List discoverySelectors = new ArrayList<>(); + + SelectorsToRun(Class testClass) { + this.testClass = testClass; + } + + @Override + public void close() throws Throwable { + runTestWithModifiedClassPath( testClass, discoverySelectors ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestExtension.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestExtension.java new file mode 100644 index 000000000..ce6ede0e4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestExtension.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.testutil.runner; + +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.support.AnnotationSupport; +import org.mapstruct.ap.testutil.ProcessorTest; + +/** + * The provider of the processor tests based on the defined compilers. + * + * @author Filip Hrisafov + */ +public class ProcessorTestExtension implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return AnnotationSupport.isAnnotated( context.getTestMethod(), ProcessorTest.class ); + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + Optional withSingleCompiler = AnnotationSupport.findAnnotation( + context.getTestClass(), + WithSingleCompiler.class + ).map( WithSingleCompiler::value ); + + if ( withSingleCompiler.isPresent() ) { + return Stream.of( new ProcessorTestInvocationContext( withSingleCompiler.get() ) ); + } + + Method testMethod = context.getRequiredTestMethod(); + ProcessorTest processorTest = AnnotationSupport.findAnnotation( testMethod, ProcessorTest.class ) + .orElseThrow( () -> new RuntimeException( "Failed to get CompilerTest on " + testMethod ) ); + + return Stream.of( processorTest.value() ) + .map( ProcessorTestInvocationContext::new ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestInvocationContext.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestInvocationContext.java new file mode 100644 index 000000000..3efc9c9df --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ProcessorTestInvocationContext.java @@ -0,0 +1,60 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.testutil.runner; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; + +/** + * The template invocation processor responsible for providing the appropriate extensions for the different compilers. + * + * @author Filip Hrisafov + */ +public class ProcessorTestInvocationContext implements TestTemplateInvocationContext { + + static final JRE CURRENT_VERSION; + + static { + JRE currentVersion = JRE.OTHER; + for ( JRE jre : JRE.values() ) { + if ( jre.isCurrentVersion() ) { + currentVersion = jre; + break; + } + } + + CURRENT_VERSION = currentVersion; + } + + protected Compiler compiler; + + public ProcessorTestInvocationContext(Compiler compiler) { + this.compiler = compiler; + } + + @Override + public String getDisplayName(int invocationIndex) { + return "[" + compiler.name().toLowerCase() + "]"; + } + + @Override + public List getAdditionalExtensions() { + List extensions = new ArrayList<>(); + extensions.add( new CompilerTestEnabledOnJreCondition( compiler ) ); + if ( compiler == Compiler.JDK ) { + extensions.add( new JdkCompilingExtension() ); + } + else if ( compiler == Compiler.ECLIPSE ) { + extensions.add( new EclipseCompilingExtension() ); + } + + return extensions; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/ReplacableTestClass.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/ReplacableTestClass.java deleted file mode 100644 index 7141b7814..000000000 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/ReplacableTestClass.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.testutil.runner; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.util.List; - -import org.junit.runners.model.FrameworkField; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.TestClass; - -/** - * A {@link TestClass} where the wrapped Class can be replaced - * - * @author Andreas Gudian - */ -class ReplacableTestClass extends TestClass { - private TestClass delegate; - - /** - * @see TestClass#TestClass(Class) - */ - ReplacableTestClass(Class clazz) { - super( clazz ); - } - - /** - * @param clazz the new class - */ - void replaceClass(Class clazz) { - delegate = new TestClass( clazz ); - } - - @Override - public List getAnnotatedMethods() { - if ( null == delegate ) { - return super.getAnnotatedMethods(); - } - else { - return delegate.getAnnotatedMethods(); - } - } - - @Override - public List getAnnotatedMethods(Class annotationClass) { - if ( null == delegate ) { - return super.getAnnotatedMethods( annotationClass ); - } - else { - return delegate.getAnnotatedMethods( annotationClass ); - } - } - - @Override - public List getAnnotatedFields() { - if ( null == delegate ) { - return super.getAnnotatedFields(); - } - else { - return delegate.getAnnotatedFields(); - } - } - - @Override - public List getAnnotatedFields(Class annotationClass) { - if ( null == delegate ) { - return super.getAnnotatedFields( annotationClass ); - } - else { - return delegate.getAnnotatedFields( annotationClass ); - } - } - - @Override - public Class getJavaClass() { - if ( null == delegate ) { - return super.getJavaClass(); - } - else { - return delegate.getJavaClass(); - } - } - - @Override - public String getName() { - if ( null == delegate ) { - return super.getName(); - } - else { - return delegate.getName(); - } - } - - @Override - public Constructor getOnlyConstructor() { - if ( null == delegate ) { - return super.getOnlyConstructor(); - } - else { - return delegate.getOnlyConstructor(); - } - } - - @Override - public Annotation[] getAnnotations() { - if ( null == delegate ) { - return super.getAnnotations(); - } - else { - return delegate.getAnnotations(); - } - } - - @Override - public T getAnnotation(Class annotationType) { - if ( null == delegate ) { - return super.getAnnotation( annotationType ); - } - else { - return delegate.getAnnotation( annotationType ); - } - } - - @Override - public List getAnnotatedFieldValues(Object test, Class annotationClass, - Class valueClass) { - if ( null == delegate ) { - return super.getAnnotatedFieldValues( test, annotationClass, valueClass ); - } - else { - return delegate.getAnnotatedFieldValues( test, annotationClass, valueClass ); - } - } - - @Override - public List getAnnotatedMethodValues(Object test, Class annotationClass, - Class valueClass) { - if ( null == delegate ) { - return super.getAnnotatedMethodValues( test, annotationClass, valueClass ); - } - else { - return delegate.getAnnotatedMethodValues( test, annotationClass, valueClass ); - } - } - - @Override - public String toString() { - if ( null == delegate ) { - return super.toString(); - } - else { - return delegate.toString(); - } - } - - @Override - public boolean isPublic() { - if ( null == delegate ) { - return super.isPublic(); - } - else { - return delegate.isPublic(); - } - } - - @Override - public boolean isANonStaticInnerClass() { - if ( null == delegate ) { - return super.isANonStaticInnerClass(); - } - else { - return delegate.isANonStaticInnerClass(); - } - } - - @Override - public int hashCode() { - if ( null == delegate ) { - return super.hashCode(); - } - else { - return delegate.hashCode(); - } - } - - @Override - public boolean equals(Object obj) { - if ( null == delegate ) { - return super.equals( obj ); - } - else { - return delegate.equals( obj ); - } - } -}