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.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, String 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, String 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, String sourcePath, final RunListener reportManager, File reportsDirectory, int skipAfterFailureCount, int verboseLevel ) { // 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 ) ) ); } // FIXME: use classifier to decide if we need to pass along the source dir (only for JDK14) if ( sourcePath != null ) { testNG.setSourcePath( sourcePath ); } 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 ); } } }