| package org.apache.maven.surefire.testng; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| import org.apache.maven.surefire.booter.ProviderParameterNames; |
| import org.apache.maven.surefire.cli.CommandLineOption; |
| import org.apache.maven.surefire.report.RunListener; |
| import org.apache.maven.surefire.testng.conf.Configurator; |
| import org.apache.maven.surefire.testng.utils.FailFastEventsSingleton; |
| import org.apache.maven.surefire.testng.utils.FailFastListener; |
| import org.apache.maven.surefire.testng.utils.Stoppable; |
| import org.apache.maven.surefire.testset.TestListResolver; |
| import org.apache.maven.surefire.testset.TestSetFailedException; |
| import org.apache.maven.surefire.util.internal.StringUtils; |
| import org.testng.TestNG; |
| import org.testng.annotations.Test; |
| import org.testng.xml.XmlClass; |
| import org.testng.xml.XmlMethodSelector; |
| import org.testng.xml.XmlSuite; |
| import org.testng.xml.XmlTest; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_DEBUG; |
| import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS; |
| import static org.apache.maven.surefire.util.ReflectionUtils.instantiate; |
| import static org.apache.maven.surefire.util.ReflectionUtils.tryLoadClass; |
| import static org.apache.maven.surefire.util.internal.ConcurrencyUtils.countDownToZero; |
| |
| /** |
| * Contains utility methods for executing TestNG. |
| * |
| * @author <a href="mailto:brett@apache.org">Brett Porter</a> |
| * @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a> |
| */ |
| final class TestNGExecutor |
| { |
| /** The default name for a suite launched from the maven surefire plugin */ |
| private static final String DEFAULT_SUREFIRE_SUITE_NAME = "Surefire suite"; |
| |
| /** The default name for a test launched from the maven surefire plugin */ |
| private static final String DEFAULT_SUREFIRE_TEST_NAME = "Surefire test"; |
| |
| private static final boolean HAS_TEST_ANNOTATION_ON_CLASSPATH = |
| tryLoadClass( TestNGExecutor.class.getClassLoader(), "org.testng.annotations.Test" ) != null; |
| |
| private TestNGExecutor() |
| { |
| throw new IllegalStateException( "not instantiable constructor" ); |
| } |
| |
| @SuppressWarnings( "checkstyle:parameternumbercheck" ) |
| static void run( Iterable<Class<?>> testClasses, File testSourceDirectory, |
| Map<String, String> options, // string,string because TestNGMapConfigurator#configure() |
| RunListener reportManager, File reportsDirectory, |
| TestListResolver methodFilter, List<CommandLineOption> mainCliOptions, |
| int skipAfterFailureCount ) |
| throws TestSetFailedException |
| { |
| TestNG testng = new TestNG( true ); |
| |
| Configurator configurator = getConfigurator( options.get( "testng.configurator" ) ); |
| |
| if ( isCliDebugOrShowErrors( mainCliOptions ) ) |
| { |
| System.out.println( "Configuring TestNG with: " + configurator.getClass().getSimpleName() ); |
| } |
| |
| XmlMethodSelector groupMatchingSelector = createGroupMatchingSelector( options ); |
| XmlMethodSelector methodNameFilteringSelector = createMethodNameFilteringSelector( methodFilter ); |
| |
| Map<String, SuiteAndNamedTests> suitesNames = new HashMap<String, SuiteAndNamedTests>(); |
| |
| List<XmlSuite> xmlSuites = new ArrayList<XmlSuite>(); |
| for ( Class<?> testClass : testClasses ) |
| { |
| TestMetadata metadata = findTestMetadata( testClass ); |
| |
| SuiteAndNamedTests suiteAndNamedTests = suitesNames.get( metadata.suiteName ); |
| if ( suiteAndNamedTests == null ) |
| { |
| suiteAndNamedTests = new SuiteAndNamedTests(); |
| suiteAndNamedTests.xmlSuite.setName( metadata.suiteName ); |
| configurator.configure( suiteAndNamedTests.xmlSuite, options ); |
| xmlSuites.add( suiteAndNamedTests.xmlSuite ); |
| |
| suitesNames.put( metadata.suiteName, suiteAndNamedTests ); |
| } |
| |
| XmlTest xmlTest = suiteAndNamedTests.testNameToTest.get( metadata.testName ); |
| if ( xmlTest == null ) |
| { |
| xmlTest = new XmlTest( suiteAndNamedTests.xmlSuite ); |
| xmlTest.setName( metadata.testName ); |
| addSelector( xmlTest, groupMatchingSelector ); |
| addSelector( xmlTest, methodNameFilteringSelector ); |
| xmlTest.setXmlClasses( new ArrayList<XmlClass>() ); |
| |
| suiteAndNamedTests.testNameToTest.put( metadata.testName, xmlTest ); |
| } |
| |
| xmlTest.getXmlClasses().add( new XmlClass( testClass.getName() ) ); |
| } |
| |
| testng.setXmlSuites( xmlSuites ); |
| configurator.configure( testng, options ); |
| postConfigure( testng, testSourceDirectory, reportManager, reportsDirectory, skipAfterFailureCount, |
| extractVerboseLevel( options ) ); |
| testng.run(); |
| } |
| |
| private static boolean isCliDebugOrShowErrors( List<CommandLineOption> mainCliOptions ) |
| { |
| return mainCliOptions.contains( LOGGING_LEVEL_DEBUG ) || mainCliOptions.contains( SHOW_ERRORS ); |
| } |
| |
| private static TestMetadata findTestMetadata( Class<?> testClass ) |
| { |
| TestMetadata result = new TestMetadata(); |
| if ( HAS_TEST_ANNOTATION_ON_CLASSPATH ) |
| { |
| Test testAnnotation = findAnnotation( testClass, Test.class ); |
| if ( null != testAnnotation ) |
| { |
| if ( !StringUtils.isBlank( testAnnotation.suiteName() ) ) |
| { |
| result.suiteName = testAnnotation.suiteName(); |
| } |
| |
| if ( !StringUtils.isBlank( testAnnotation.testName() ) ) |
| { |
| result.testName = testAnnotation.testName(); |
| } |
| } |
| } |
| return result; |
| } |
| |
| private static <T extends Annotation> T findAnnotation( Class<?> clazz, Class<T> annotationType ) |
| { |
| if ( clazz == null ) |
| { |
| return null; |
| } |
| |
| T result = clazz.getAnnotation( annotationType ); |
| if ( result != null ) |
| { |
| return result; |
| } |
| |
| return findAnnotation( clazz.getSuperclass(), annotationType ); |
| } |
| |
| private static class TestMetadata |
| { |
| private String testName = DEFAULT_SUREFIRE_TEST_NAME; |
| |
| private String suiteName = DEFAULT_SUREFIRE_SUITE_NAME; |
| } |
| |
| private static class SuiteAndNamedTests |
| { |
| private XmlSuite xmlSuite = new XmlSuite(); |
| |
| private Map<String, XmlTest> testNameToTest = new HashMap<String, XmlTest>(); |
| } |
| |
| private static void addSelector( XmlTest xmlTest, XmlMethodSelector selector ) |
| { |
| if ( selector != null ) |
| { |
| xmlTest.getMethodSelectors().add( selector ); |
| } |
| } |
| |
| @SuppressWarnings( "checkstyle:magicnumber" ) |
| private static XmlMethodSelector createMethodNameFilteringSelector( TestListResolver methodFilter ) |
| throws TestSetFailedException |
| { |
| if ( methodFilter != null && !methodFilter.isEmpty() ) |
| { |
| // the class is available in the testClassPath |
| String clazzName = "org.apache.maven.surefire.testng.utils.MethodSelector"; |
| try |
| { |
| Class<?> clazz = Class.forName( clazzName ); |
| Method method = clazz.getMethod( "setTestListResolver", TestListResolver.class ); |
| method.invoke( null, methodFilter ); |
| } |
| catch ( Exception e ) |
| { |
| throw new TestSetFailedException( e.getMessage(), e ); |
| } |
| |
| XmlMethodSelector xms = new XmlMethodSelector(); |
| |
| xms.setName( clazzName ); |
| // looks to need a high value |
| xms.setPriority( 10000 ); |
| |
| return xms; |
| } |
| else |
| { |
| return null; |
| } |
| } |
| |
| @SuppressWarnings( "checkstyle:magicnumber" ) |
| private static XmlMethodSelector createGroupMatchingSelector( Map<String, String> options ) |
| throws TestSetFailedException |
| { |
| final String groups = options.get( ProviderParameterNames.TESTNG_GROUPS_PROP ); |
| final String excludedGroups = options.get( ProviderParameterNames.TESTNG_EXCLUDEDGROUPS_PROP ); |
| |
| if ( groups == null && excludedGroups == null ) |
| { |
| return null; |
| } |
| |
| // the class is available in the testClassPath |
| final String clazzName = "org.apache.maven.surefire.testng.utils.GroupMatcherMethodSelector"; |
| try |
| { |
| Class<?> clazz = Class.forName( clazzName ); |
| |
| // HORRIBLE hack, but TNG doesn't allow us to setup a method selector instance directly. |
| Method method = clazz.getMethod( "setGroups", String.class, String.class ); |
| method.invoke( null, groups, excludedGroups ); |
| } |
| catch ( Exception e ) |
| { |
| throw new TestSetFailedException( e.getMessage(), e ); |
| } |
| |
| XmlMethodSelector xms = new XmlMethodSelector(); |
| |
| xms.setName( clazzName ); |
| // looks to need a high value |
| xms.setPriority( 9999 ); |
| |
| return xms; |
| } |
| |
| static void run( List<String> suiteFiles, File testSourceDirectory, |
| Map<String, String> options, // string,string because TestNGMapConfigurator#configure() |
| RunListener reportManager, File reportsDirectory, int skipAfterFailureCount ) |
| throws TestSetFailedException |
| { |
| TestNG testng = new TestNG( true ); |
| Configurator configurator = getConfigurator( options.get( "testng.configurator" ) ); |
| configurator.configure( testng, options ); |
| postConfigure( testng, testSourceDirectory, reportManager, reportsDirectory, skipAfterFailureCount, |
| extractVerboseLevel( options ) ); |
| testng.setTestSuites( suiteFiles ); |
| testng.run(); |
| } |
| |
| private static Configurator getConfigurator( String className ) |
| { |
| try |
| { |
| return (Configurator) Class.forName( className ).newInstance(); |
| } |
| catch ( InstantiationException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| catch ( IllegalAccessException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| catch ( ClassNotFoundException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| } |
| |
| private static void postConfigure( TestNG testNG, File sourcePath, final RunListener reportManager, |
| File reportsDirectory, int skipAfterFailureCount, int verboseLevel ) |
| throws TestSetFailedException |
| { |
| // 0 (default): turn off all TestNG output |
| testNG.setVerbose( verboseLevel ); |
| |
| TestNGReporter reporter = createTestNGReporter( reportManager ); |
| testNG.addListener( (Object) reporter ); |
| |
| if ( skipAfterFailureCount > 0 ) |
| { |
| ClassLoader cl = Thread.currentThread().getContextClassLoader(); |
| testNG.addListener( instantiate( cl, "org.apache.maven.surefire.testng.utils.FailFastNotifier", |
| Object.class ) ); |
| testNG.addListener( new FailFastListener( createStoppable( reportManager, skipAfterFailureCount ) ) ); |
| } |
| |
| if ( sourcePath != null ) |
| { |
| try |
| { |
| testNG.setSourcePath( sourcePath.getCanonicalPath() ); |
| } |
| catch ( IOException e ) |
| { |
| throw new TestSetFailedException( e.getLocalizedMessage(), e ); |
| } |
| } |
| |
| testNG.setOutputDirectory( reportsDirectory.getAbsolutePath() ); |
| } |
| |
| private static Stoppable createStoppable( final RunListener reportManager, int skipAfterFailureCount ) |
| { |
| final AtomicInteger currentFaultCount = new AtomicInteger( skipAfterFailureCount ); |
| |
| return new Stoppable() |
| { |
| @Override |
| public void fireStopEvent() |
| { |
| if ( countDownToZero( currentFaultCount ) ) |
| { |
| FailFastEventsSingleton.getInstance().setSkipOnNextTest(); |
| } |
| |
| reportManager.testExecutionSkippedByUser(); |
| } |
| }; |
| } |
| |
| // If we have access to IResultListener, return a ConfigurationAwareTestNGReporter |
| // But don't cause NoClassDefFoundErrors if it isn't available; just return a regular TestNGReporter instead |
| private static TestNGReporter createTestNGReporter( RunListener reportManager ) |
| { |
| try |
| { |
| Class.forName( "org.testng.internal.IResultListener" ); |
| Class c = Class.forName( "org.apache.maven.surefire.testng.ConfigurationAwareTestNGReporter" ); |
| @SuppressWarnings( "unchecked" ) Constructor<?> ctor = c.getConstructor( RunListener.class ); |
| return (TestNGReporter) ctor.newInstance( reportManager ); |
| } |
| catch ( InvocationTargetException e ) |
| { |
| throw new RuntimeException( "Bug in ConfigurationAwareTestNGReporter", e.getCause() ); |
| } |
| catch ( ClassNotFoundException e ) |
| { |
| return new TestNGReporter( reportManager ); |
| } |
| catch ( Exception e ) |
| { |
| throw new RuntimeException( "Bug in ConfigurationAwareTestNGReporter", e ); |
| } |
| } |
| |
| private static int extractVerboseLevel( Map<String, String> options ) |
| throws TestSetFailedException |
| { |
| try |
| { |
| String verbose = options.get( "surefire.testng.verbose" ); |
| return verbose == null ? 0 : Integer.parseInt( verbose ); |
| } |
| catch ( NumberFormatException e ) |
| { |
| throw new TestSetFailedException( "Provider property 'surefire.testng.verbose' should refer to " |
| + "number -1 (debug mode), 0, 1 .. 10 (most detailed).", e ); |
| } |
| } |
| } |