/** * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) * and/or other contributors as indicated by the @authors tag. See the * copyright.txt file in the distribution for a full listing of all * contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mapstruct.itest.testutil.runner; import static org.apache.maven.it.util.ResourceExtractor.extractResourceToDestination; import static org.apache.maven.shared.utils.io.FileUtils.copyURLToFile; import static org.apache.maven.shared.utils.io.FileUtils.deleteDirectory; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import org.apache.maven.it.Verifier; import org.junit.internal.AssumptionViolatedException; import org.junit.internal.runners.model.EachTestNotifier; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.ParentRunner; import org.junit.runners.model.InitializationError; import org.mapstruct.itest.testutil.runner.ProcessorSuite.CommandLineEnhancer; import org.mapstruct.itest.testutil.runner.ProcessorSuite.ProcessorType; import org.mapstruct.itest.testutil.runner.ProcessorSuiteRunner.ProcessorTestCase; import org.mapstruct.itest.testutil.runner.xml.Toolchains; import org.mapstruct.itest.testutil.runner.xml.Toolchains.ProviderDescription; /** * Runner for processor integration tests. Requires the annotation {@link ProcessorSuite} on the test class. * * @author Andreas Gudian */ public class ProcessorSuiteRunner extends ParentRunner<ProcessorTestCase> { /** * System property for specifying the location of the toolchains.xml file */ public static final String SYS_PROP_TOOLCHAINS_FILE = "processorIntegrationTest.toolchainsFile"; /** * System property to enable remote debugging of the processor execution in the integration test */ public static final String SYS_PROP_DEBUG = "processorIntegrationTest.debug"; private static final File TOOLCHAINS_FILE = getToolchainsFile(); private static final Collection<ProcessorType> ENABLED_PROCESSOR_TYPES = detectSupportedTypes( TOOLCHAINS_FILE ); public static final class ProcessorTestCase { private final String baseDir; private final ProcessorType processor; private final boolean ignored; private final Constructor<? extends CommandLineEnhancer> cliEnhancerConstructor; public ProcessorTestCase(String baseDir, ProcessorType processor, Constructor<? extends CommandLineEnhancer> cliEnhancerConstructor) { this.baseDir = baseDir; this.processor = processor; this.cliEnhancerConstructor = cliEnhancerConstructor; this.ignored = !ENABLED_PROCESSOR_TYPES.contains( processor ); } } private final List<ProcessorTestCase> methods; /** * @param clazz the test class * @throws InitializationError in case the initialization fails */ public ProcessorSuiteRunner(Class<?> clazz) throws InitializationError { super( clazz ); ProcessorSuite suite = clazz.getAnnotation( ProcessorSuite.class ); if ( null == suite ) { throw new InitializationError( "The test class must be annotated with " + ProcessorSuite.class.getName() ); } if ( suite.processorTypes().length == 0 ) { throw new InitializationError( "ProcessorSuite#processorTypes must not be empty" ); } Constructor<? extends CommandLineEnhancer> cliEnhancerConstructor = null; if ( suite.commandLineEnhancer() != CommandLineEnhancer.class ) { try { cliEnhancerConstructor = suite.commandLineEnhancer().getConstructor(); } catch ( NoSuchMethodException e ) { throw new InitializationError( suite.commandLineEnhancer().getName() + " does not have a default constructor." ); } catch ( SecurityException e ) { throw new InitializationError( e ); } } methods = initializeTestCases( suite, cliEnhancerConstructor ); } private List<ProcessorTestCase> initializeTestCases(ProcessorSuite suite, Constructor<? extends CommandLineEnhancer> cliEnhancerConstructor) { List<ProcessorType> types = new ArrayList<ProcessorType>(); for ( ProcessorType compiler : suite.processorTypes() ) { if ( compiler.getIncluded().length > 0 ) { types.addAll( Arrays.asList( compiler.getIncluded() ) ); } else { types.add( compiler ); } } List<ProcessorTestCase> result = new ArrayList<ProcessorTestCase>( types.size() ); for ( ProcessorType type : types ) { result.add( new ProcessorTestCase( suite.baseDir(), type, cliEnhancerConstructor ) ); } return result; } @Override protected List<ProcessorTestCase> getChildren() { return methods; } @Override protected Description describeChild(ProcessorTestCase child) { return Description.createTestDescription( getTestClass().getJavaClass(), child.processor.name().toLowerCase() ); } @Override protected void runChild(ProcessorTestCase child, RunNotifier notifier) { Description description = describeChild( child ); EachTestNotifier testNotifier = new EachTestNotifier( notifier, description ); if ( child.ignored ) { testNotifier.fireTestIgnored(); } else { try { testNotifier.fireTestStarted(); doExecute( child, description ); } catch ( AssumptionViolatedException e ) { testNotifier.fireTestIgnored(); } catch ( StoppedByUserException e ) { throw e; } catch ( Throwable e ) { testNotifier.addFailure( e ); } finally { testNotifier.fireTestFinished(); } } } private void doExecute(ProcessorTestCase child, Description description) throws Exception { File destination = extractTest( child, description ); PrintStream originalOut = System.out; final Verifier verifier; if ( Boolean.getBoolean( SYS_PROP_DEBUG ) ) { if ( child.processor.getToolchain() == null ) { // when not using toolchains for a test, then the compiler is executed within the Maven JVM. So make // sure we fork a new JVM for that, and let that new JVM use the command 'mvnDebug' instead of 'mvn' verifier = new Verifier( destination.getCanonicalPath(), null, true, true ); verifier.setDebugJvm( true ); } else { verifier = new Verifier( destination.getCanonicalPath() ); verifier.addCliOption( "-Pdebug-forked-javac" ); } } else { verifier = new Verifier( destination.getCanonicalPath() ); } List<String> goals = new ArrayList<String>( 3 ); goals.add( "clean" ); try { configureToolchains( child, verifier, goals, originalOut ); configureProcessor( child, verifier ); verifier.addCliOption( "-Dcompiler-source-target-version=" + child.processor.getSourceTargetVersion() ); if ( "1.8".equals( child.processor.getSourceTargetVersion() ) || "1.9".equals( child.processor.getSourceTargetVersion() ) ) { verifier.addCliOption( "-Dmapstruct-artifact-id=mapstruct-jdk8" ); } else { verifier.addCliOption( "-Dmapstruct-artifact-id=mapstruct" ); } if ( Boolean.getBoolean( SYS_PROP_DEBUG ) ) { originalOut.print( "Processor Integration Test: " ); originalOut.println( "Listening for transport dt_socket at address: 8000 (in some seconds)" ); } goals.add( "test" ); addAdditionalCliArguments( child, verifier ); originalOut.println( "executing " + child.processor.name().toLowerCase() ); verifier.executeGoals( goals ); verifier.verifyErrorFreeLog(); } finally { verifier.resetStreams(); } } private void addAdditionalCliArguments(ProcessorTestCase child, final Verifier verifier) throws Exception { if ( child.cliEnhancerConstructor != null ) { CommandLineEnhancer enhancer = child.cliEnhancerConstructor.newInstance(); Collection<String> additionalArgs = enhancer.getAdditionalCommandLineArguments( child.processor ); if ( additionalArgs != null ) { for ( String arg : additionalArgs ) { verifier.addCliOption( arg ); } } } } private void configureProcessor(ProcessorTestCase child, Verifier verifier) { if ( child.processor.getCompilerId() != null ) { verifier.addCliOption( "-Pgenerate-via-compiler-plugin" ); verifier.addCliOption( "-Dcompiler-id=" + child.processor.getCompilerId() ); } else { verifier.addCliOption( "-Pgenerate-via-processor-plugin" ); } } private void configureToolchains(ProcessorTestCase child, Verifier verifier, List<String> goals, PrintStream originalOut) { if ( child.processor.getToolchain() != null ) { verifier.addCliOption( "--toolchains" ); verifier.addCliOption( TOOLCHAINS_FILE.getPath().replace( '\\', '/' ) ); Toolchain toolchain = child.processor.getToolchain(); verifier.addCliOption( "-Dtoolchain-jdk-vendor=" + toolchain.getVendor() ); verifier.addCliOption( "-Dtoolchain-jdk-version=" + toolchain.getVersionRangeString() ); goals.add( "toolchains:toolchain" ); } } private File extractTest(ProcessorTestCase child, Description description) throws IOException { File tempDirBase = new File( "target/tmp", description.getClassName() ).getCanonicalFile(); if ( !tempDirBase.exists() ) { tempDirBase.mkdirs(); } File parentPom = new File( tempDirBase, "pom.xml" ); copyURLToFile( getClass().getResource( "/pom.xml" ), parentPom ); File tempDir = new File( tempDirBase, description.getMethodName() ); deleteDirectory( tempDir ); return extractResourceToDestination( getClass(), "/" + child.baseDir, tempDir, true ); } private static File getToolchainsFile() { String specifiedToolchainsFile = System.getProperty( SYS_PROP_TOOLCHAINS_FILE ); if ( null != specifiedToolchainsFile ) { try { File canonical = new File( specifiedToolchainsFile ).getCanonicalFile(); if ( canonical.exists() ) { return canonical; } // check the path relative to the parent directory (allows specifying a path relative to the top-level // aggregator module) canonical = new File( "..", specifiedToolchainsFile ).getCanonicalFile(); if ( canonical.exists() ) { return canonical; } } catch ( IOException e ) { return null; } } // use default location String defaultPath = System.getProperty( "user.home" ) + System.getProperty( "file.separator" ) + ".m2"; return new File( defaultPath, "toolchains.xml" ); } private static Collection<ProcessorType> detectSupportedTypes(File toolchainsFile) { Toolchains toolchains; try { if ( toolchainsFile.exists() ) { Unmarshaller unmarshaller = JAXBContext.newInstance( Toolchains.class ).createUnmarshaller(); toolchains = (Toolchains) unmarshaller.unmarshal( toolchainsFile ); } else { toolchains = null; } } catch ( JAXBException e ) { toolchains = null; } Collection<ProcessorType> supported = new HashSet<>(); for ( ProcessorType type : ProcessorType.values() ) { if ( isSupported( type, toolchains ) ) { supported.add( type ); } } return supported; } private static boolean isSupported(ProcessorType type, Toolchains toolchains) { if ( type.getToolchain() == null ) { return true; } if ( toolchains == null ) { return false; } Toolchain required = type.getToolchain(); for ( Toolchains.Toolchain tc : toolchains.getToolchains() ) { if ( "jdk".equals( tc.getType() ) ) { ProviderDescription desc = tc.getProviderDescription(); if ( desc != null && required.getVendor().equals( desc.getVendor() ) && desc.getVersion() != null && desc.getVersion().startsWith( required.getVersionMinInclusive() ) ) { return true; } } } return false; } }