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 bde7bd0a1a/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.
This commit is contained in:
Filip Hrisafov 2020-02-09 15:37:24 +01:00
parent a6b3cc364a
commit ff27b2f70d
19 changed files with 564 additions and 751 deletions

View File

@ -24,7 +24,7 @@
<org.mapstruct.gem.version>1.0.0.Alpha1</org.mapstruct.gem.version>
<!-- We can't go to 3.0.0-M2 as it has a regression. See https://issues.apache.org/jira/browse/MENFORCER-306 -->
<org.apache.maven.plugins.enforcer.version>3.0.0-M1</org.apache.maven.plugins.enforcer.version>
<org.apache.maven.plugins.surefire.version>3.0.0-M3</org.apache.maven.plugins.surefire.version>
<org.apache.maven.plugins.surefire.version>3.0.0-M4</org.apache.maven.plugins.surefire.version>
<org.apache.maven.plugins.javadoc.version>3.1.0</org.apache.maven.plugins.javadoc.version>
<org.springframework.version>4.0.3.RELEASE</org.springframework.version>
<org.eclipse.tycho.compiler-jdt.version>0.26.0</org.eclipse.tycho.compiler-jdt.version>

View File

@ -49,8 +49,13 @@
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@ -104,6 +109,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<!-- There is no compile dependency to Joda-Time; It's only required for testing the Joda conversions -->
<dependency>
<groupId>joda-time</groupId>

View File

@ -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.
* <p>
* Test classes are safe to be executed in parallel, but test methods are not safe to be executed in parallel.
* <p>
* 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.
* <p>
* 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 :
* <ul>
* <li>Processor options to be considered during compilation via
* {@link org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption ProcessorOption}.</li>
* <li>The expected compilation outcome and expected diagnostics can be specified via
* {@link org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome ExpectedCompilationOutcome}.
* If no outcome is specified, a successful compilation is assumed.</li>
* </ul>
*
* @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
};
}

View File

@ -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.
* <p>
* Test classes are safe to be executed in parallel, but test methods are not safe to be executed in parallel.
* <p>
* 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 :
* <ul>
* <li>Processor options to be considered during compilation via {@link ProcessorOption}.</li>
* <li>The expected compilation outcome and expected diagnostics can be specified via {@link ExpectedCompilationOutcome}
* . If no outcome is specified, a successful compilation is assumed.</li>
* </ul>
*
* @author Gunnar Morling
* @author Andreas Gudian
*/
public class AnnotationProcessorTestRunner extends ParentRunner<Runner> {
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<Runner> 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<Runner> 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<Runner> 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 );
}
}
}

View File

@ -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<Class<?>> sourceClasses;
private final Map<Class<?>, Class<?>> services;
private final List<String> processorOptions;
CompilationRequest(Set<Class<?>> sourceClasses, Map<Class<?>, Class<?>> services, List<String> processorOptions) {
CompilationRequest(Compiler compiler, Set<Class<?>> sourceClasses, Map<Class<?>, Class<?>> services, List<String> 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 );
}

View File

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

View File

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

View File

@ -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<String> 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<String> 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<Class<?>> getTestClasses() {
private Set<Class<?>> getTestClasses(Method testMethod, Class<?> testClass) {
Set<Class<?>> 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<?>, Class<?>> getServices() {
private Map<Class<?>, Class<?>> getServices(Method testMethod, Class<?> testClass) {
Map<Class<?>, 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<?>, Class<?>> services, WithServiceImplementations withImplementations) {
if ( withImplementations != null ) {
for ( WithServiceImplementation resource : withImplementations.value() ) {
addService( services, resource );
}
private void addServices(Map<Class<?>, Class<?>> services, List<WithServiceImplementation> 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<String> getProcessorOptions() {
List<ProcessorOption> processorOptions =
getProcessorOptions(
method.getAnnotation( ProcessorOptions.class ),
method.getAnnotation( ProcessorOption.class ) );
private List<String> getProcessorOptions(Method testMethod, Class<?> testClass) {
List<ProcessorOption> 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<String> result = new ArrayList<>( processorOptions.size() );
@ -435,17 +410,6 @@ abstract class CompilingStatement extends Statement {
return result;
}
private List<ProcessorOption> 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";

View File

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

View File

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

View File

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

View File

@ -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.
* <p>
* To add it to the test, use:
*
* <pre>
* &#064;Rule
* public GeneratedSource generatedSources = new GeneratedSource();
* &#064;RegisterExtension
* final GeneratedSource generatedSources = new GeneratedSource();
* </pre>
*
* @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> compilingStatement = new ThreadLocal<>();
private ThreadLocal<String> sourceOutputDir = new ThreadLocal<>();
private List<Class<?>> 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 {

View File

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

View File

@ -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<DiagnosticDescriptor> filterExpectedDiagnostics(List<DiagnosticDescriptor> expectedDiagnostics) {
return expectedDiagnostics;
}
@Override
protected String getPathSuffix() {
return "_jdk";
}
}

View File

@ -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<File> 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<DiagnosticDescriptor> filterExpectedDiagnostics(List<DiagnosticDescriptor> expectedDiagnostics) {
List<DiagnosticDescriptor> 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<DiagnosticDescriptor> filtered = new ArrayList<DiagnosticDescriptor>( 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";
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* This mechanism is needed since there is no way to register a custom ClassLoader for creating the test instance
* in JUnit Jupiter (see <a href="https://github.com/junit-team/junit5/issues/201">junit-test/junit5#201</a>
* for more information). Once there is support for registering a custom class loader we can simplify this.
* <p>
* This logic was heavily inspired and is really similar to the Spring Boot
* <a href="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">ModifiedClassPathExtension</a>.
*
* @author Filip Hrisafov
* @see ModifiableURLClassLoader
*/
// CHECKSTYLE:ON
public class ModifiedClassLoaderExtension implements InvocationInterceptor {
@Override
public void interceptBeforeAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept( invocation, extensionContext );
}
@Override
public void interceptBeforeEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept( invocation, extensionContext );
}
@Override
public void interceptAfterEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept( invocation, extensionContext );
}
@Override
public void interceptAfterAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept( invocation, extensionContext );
}
@Override
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> 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<Void> invocation,
ReflectiveInvocationContext<Method> 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<? extends DiscoverySelector> 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<? extends DiscoverySelector> 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<Void> 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<DiscoverySelector> discoverySelectors = new ArrayList<>();
SelectorsToRun(Class<?> testClass) {
this.testClass = testClass;
}
@Override
public void close() throws Throwable {
runTestWithModifiedClassPath( testClass, discoverySelectors );
}
}
}

View File

@ -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<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
Optional<Compiler> 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 );
}
}

View File

@ -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<Extension> getAdditionalExtensions() {
List<Extension> 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;
}
}

View File

@ -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<FrameworkMethod> getAnnotatedMethods() {
if ( null == delegate ) {
return super.getAnnotatedMethods();
}
else {
return delegate.getAnnotatedMethods();
}
}
@Override
public List<FrameworkMethod> getAnnotatedMethods(Class<? extends Annotation> annotationClass) {
if ( null == delegate ) {
return super.getAnnotatedMethods( annotationClass );
}
else {
return delegate.getAnnotatedMethods( annotationClass );
}
}
@Override
public List<FrameworkField> getAnnotatedFields() {
if ( null == delegate ) {
return super.getAnnotatedFields();
}
else {
return delegate.getAnnotatedFields();
}
}
@Override
public List<FrameworkField> getAnnotatedFields(Class<? extends Annotation> 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 extends Annotation> T getAnnotation(Class<T> annotationType) {
if ( null == delegate ) {
return super.getAnnotation( annotationType );
}
else {
return delegate.getAnnotation( annotationType );
}
}
@Override
public <T> List<T> getAnnotatedFieldValues(Object test, Class<? extends Annotation> annotationClass,
Class<T> valueClass) {
if ( null == delegate ) {
return super.getAnnotatedFieldValues( test, annotationClass, valueClass );
}
else {
return delegate.getAnnotatedFieldValues( test, annotationClass, valueClass );
}
}
@Override
public <T> List<T> getAnnotatedMethodValues(Object test, Class<? extends Annotation> annotationClass,
Class<T> 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 );
}
}
}