( 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 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 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 extends Annotation> 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 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 getAnnotation(Class annotationType) {
- if ( null == delegate ) {
- return super.getAnnotation( annotationType );
- }
- else {
- return delegate.getAnnotation( annotationType );
- }
- }
-
- @Override
- public List getAnnotatedFieldValues(Object test, Class extends Annotation> 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 extends Annotation> 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 );
- }
- }
-}