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