View Javadoc
1   package org.apache.maven.surefire.testng;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.surefire.api.booter.ProviderParameterNames;
23  import org.apache.maven.surefire.api.cli.CommandLineOption;
24  import org.apache.maven.surefire.api.report.RunListener;
25  import org.apache.maven.surefire.testng.conf.Configurator;
26  import org.apache.maven.surefire.testng.utils.FailFastEventsSingleton;
27  import org.apache.maven.surefire.testng.utils.FailFastListener;
28  import org.apache.maven.surefire.testng.utils.FailFastNotifier;
29  import org.apache.maven.surefire.testng.utils.Stoppable;
30  import org.apache.maven.surefire.api.testset.TestListResolver;
31  import org.apache.maven.surefire.api.testset.TestSetFailedException;
32  import org.apache.maven.surefire.shared.utils.StringUtils;
33  import org.testng.ITestNGListener;
34  import org.testng.TestNG;
35  import org.testng.annotations.Test;
36  import org.testng.xml.XmlClass;
37  import org.testng.xml.XmlMethodSelector;
38  import org.testng.xml.XmlSuite;
39  import org.testng.xml.XmlTest;
40  
41  import java.io.File;
42  import java.lang.annotation.Annotation;
43  import java.lang.reflect.Constructor;
44  import java.lang.reflect.Method;
45  import java.util.ArrayList;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.concurrent.atomic.AtomicInteger;
50  
51  import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
52  import static org.apache.maven.surefire.api.cli.CommandLineOption.SHOW_ERRORS;
53  import static org.apache.maven.surefire.api.util.ReflectionUtils.instantiate;
54  import static org.apache.maven.surefire.api.util.ReflectionUtils.invokeSetter;
55  import static org.apache.maven.surefire.api.util.ReflectionUtils.newInstance;
56  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetConstructor;
57  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetMethod;
58  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
59  import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.runIfZeroCountDown;
60  
61  /**
62   * Contains utility methods for executing TestNG.
63   *
64   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
65   * @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a>
66   */
67  final class TestNGExecutor
68  {
69      /** The default name for a suite launched from the maven surefire plugin */
70      private static final String DEFAULT_SUREFIRE_SUITE_NAME = "Surefire suite";
71  
72      /** The default name for a test launched from the maven surefire plugin */
73      private static final String DEFAULT_SUREFIRE_TEST_NAME = "Surefire test";
74  
75      private static final boolean HAS_TEST_ANNOTATION_ON_CLASSPATH =
76              tryLoadClass( TestNGExecutor.class.getClassLoader(), "org.testng.annotations.Test" ) != null;
77  
78      // Using reflection because XmlClass.setIndex is available since TestNG 6.3
79      // XmlClass.m_index field is available since TestNG 5.13, but prior to 6.3 required invoking constructor
80      // and constructor XmlClass constructor signatures evolved over time.
81      private static final Method XML_CLASS_SET_INDEX = tryGetMethod( XmlClass.class, "setIndex", int.class );
82  
83      // For TestNG versions [5.13, 6.3) where XmlClass.setIndex is not available, invoke XmlClass(String, boolean, int)
84      // constructor. Note that XmlClass(String, boolean, int) was replaced with XmlClass(String, int) when
85      // XmlClass.setIndex already existed.
86      private static final Constructor<XmlClass> XML_CLASS_CONSTRUCTOR_WITH_INDEX =
87              tryGetConstructor( XmlClass.class, String.class, boolean.class, int.class );
88  
89      private TestNGExecutor()
90      {
91          throw new IllegalStateException( "not instantiable constructor" );
92      }
93  
94      @SuppressWarnings( "checkstyle:parameternumbercheck" )
95      static void run( Iterable<Class<?>> testClasses, String testSourceDirectory,
96                       Map<String, String> options, // string,string because TestNGMapConfigurator#configure()
97                       TestNGReporter testNGReporter, File reportsDirectory,
98                       TestListResolver methodFilter, List<CommandLineOption> mainCliOptions,
99                       int skipAfterFailureCount )
100         throws TestSetFailedException
101     {
102         TestNG testng = new TestNG( true );
103 
104         Configurator configurator = getConfigurator( options.get( "testng.configurator" ) );
105 
106         if ( isCliDebugOrShowErrors( mainCliOptions ) )
107         {
108             System.out.println( "Configuring TestNG with: " + configurator.getClass().getSimpleName() );
109         }
110 
111         XmlMethodSelector groupMatchingSelector = createGroupMatchingSelector( options );
112         XmlMethodSelector methodNameFilteringSelector = createMethodNameFilteringSelector( methodFilter );
113 
114         Map<String, SuiteAndNamedTests> suitesNames = new HashMap<>();
115 
116         List<XmlSuite> xmlSuites = new ArrayList<>();
117         for ( Class<?> testClass : testClasses )
118         {
119             TestMetadata metadata = findTestMetadata( testClass );
120 
121             SuiteAndNamedTests suiteAndNamedTests = suitesNames.get( metadata.suiteName );
122             if ( suiteAndNamedTests == null )
123             {
124                 suiteAndNamedTests = new SuiteAndNamedTests();
125                 suiteAndNamedTests.xmlSuite.setName( metadata.suiteName );
126                 configurator.configure( suiteAndNamedTests.xmlSuite, options );
127                 xmlSuites.add( suiteAndNamedTests.xmlSuite );
128 
129                 suitesNames.put( metadata.suiteName, suiteAndNamedTests );
130             }
131 
132             XmlTest xmlTest = suiteAndNamedTests.testNameToTest.get( metadata.testName );
133             if ( xmlTest == null )
134             {
135                 xmlTest = new XmlTest( suiteAndNamedTests.xmlSuite );
136                 xmlTest.setName( metadata.testName );
137                 addSelector( xmlTest, groupMatchingSelector );
138                 addSelector( xmlTest, methodNameFilteringSelector );
139                 xmlTest.setXmlClasses( new ArrayList<>() );
140 
141                 suiteAndNamedTests.testNameToTest.put( metadata.testName, xmlTest );
142             }
143 
144             xmlTest.getXmlClasses().add( newXmlClassInstance( testClass.getName(), xmlTest.getXmlClasses().size() ) );
145         }
146 
147         testng.setXmlSuites( xmlSuites );
148         configurator.configure( testng, options );
149         postConfigure( testng, testSourceDirectory, testNGReporter, reportsDirectory, skipAfterFailureCount,
150                        extractVerboseLevel( options ) );
151         testng.run();
152     }
153 
154     private static XmlClass newXmlClassInstance( String testClassName, int index )
155     {
156         // In case of parallel test execution with parallel="methods", TestNG orders test execution
157         // by XmlClass.m_index field. When unset (equal for all XmlClass instances), TestNG can
158         // invoke `@BeforeClass` setup methods on many instances, without invoking `@AfterClass`
159         // tearDown methods, thus leading to high resource consumptions when test instances
160         // allocate resources.
161         // With index set, order of setup, test and tearDown methods is reasonable, with approximately
162         // #thread-count many test classes being initialized at given point in time.
163         // Note: XmlClass.m_index field is set automatically by TestNG when it reads a suite file.
164 
165         if ( XML_CLASS_SET_INDEX != null )
166         {
167             XmlClass xmlClass = new XmlClass( testClassName );
168             invokeSetter( xmlClass, XML_CLASS_SET_INDEX, index );
169             return xmlClass;
170         }
171         if ( XML_CLASS_CONSTRUCTOR_WITH_INDEX != null )
172         {
173             boolean loadClass = true; // this is the default
174             return newInstance( XML_CLASS_CONSTRUCTOR_WITH_INDEX, testClassName, loadClass, index );
175         }
176         return new XmlClass( testClassName );
177     }
178 
179     private static boolean isCliDebugOrShowErrors( List<CommandLineOption> mainCliOptions )
180     {
181         return mainCliOptions.contains( LOGGING_LEVEL_DEBUG ) || mainCliOptions.contains( SHOW_ERRORS );
182     }
183 
184     private static TestMetadata findTestMetadata( Class<?> testClass )
185     {
186         TestMetadata result = new TestMetadata();
187         if ( HAS_TEST_ANNOTATION_ON_CLASSPATH )
188         {
189             Test testAnnotation = findAnnotation( testClass, Test.class );
190             if ( null != testAnnotation )
191             {
192                 if ( !StringUtils.isBlank( testAnnotation.suiteName() ) )
193                 {
194                     result.suiteName = testAnnotation.suiteName();
195                 }
196 
197                 if ( !StringUtils.isBlank( testAnnotation.testName() ) )
198                 {
199                     result.testName = testAnnotation.testName();
200                 }
201             }
202         }
203         return result;
204     }
205 
206     private static <T extends Annotation> T findAnnotation( Class<?> clazz, Class<T> annotationType )
207     {
208         if ( clazz == null )
209         {
210             return null;
211         }
212 
213         T result = clazz.getAnnotation( annotationType );
214         if ( result != null )
215         {
216             return result;
217         }
218 
219         return findAnnotation( clazz.getSuperclass(), annotationType );
220     }
221 
222     private static class TestMetadata
223     {
224         private String testName = DEFAULT_SUREFIRE_TEST_NAME;
225 
226         private String suiteName = DEFAULT_SUREFIRE_SUITE_NAME;
227     }
228 
229     private static class SuiteAndNamedTests
230     {
231         private final XmlSuite xmlSuite = new XmlSuite();
232 
233         private final Map<String, XmlTest> testNameToTest = new HashMap<>();
234     }
235 
236     private static void addSelector( XmlTest xmlTest, XmlMethodSelector selector )
237     {
238         if ( selector != null )
239         {
240             xmlTest.getMethodSelectors().add( selector );
241         }
242     }
243 
244     @SuppressWarnings( "checkstyle:magicnumber" )
245     private static XmlMethodSelector createMethodNameFilteringSelector( TestListResolver methodFilter )
246         throws TestSetFailedException
247     {
248         if ( methodFilter != null && !methodFilter.isEmpty() )
249         {
250             // the class is available in the testClassPath
251             String clazzName = "org.apache.maven.surefire.testng.utils.MethodSelector";
252             try
253             {
254                 Class<?> clazz = Class.forName( clazzName );
255                 Method method = clazz.getMethod( "setTestListResolver", TestListResolver.class );
256                 method.invoke( null, methodFilter );
257             }
258             catch ( Exception e )
259             {
260                 throw new TestSetFailedException( e.getMessage(), e );
261             }
262 
263             XmlMethodSelector xms = new XmlMethodSelector();
264 
265             xms.setName( clazzName );
266             // looks to need a high value
267             xms.setPriority( 10000 );
268 
269             return xms;
270         }
271         else
272         {
273             return null;
274         }
275     }
276 
277     @SuppressWarnings( "checkstyle:magicnumber" )
278     private static XmlMethodSelector createGroupMatchingSelector( Map<String, String> options )
279         throws TestSetFailedException
280     {
281         final String groups = options.get( ProviderParameterNames.TESTNG_GROUPS_PROP );
282         final String excludedGroups = options.get( ProviderParameterNames.TESTNG_EXCLUDEDGROUPS_PROP );
283 
284         if ( groups == null && excludedGroups == null )
285         {
286             return null;
287         }
288 
289         // the class is available in the testClassPath
290         final String clazzName = "org.apache.maven.surefire.testng.utils.GroupMatcherMethodSelector";
291         try
292         {
293             Class<?> clazz = Class.forName( clazzName );
294 
295             // HORRIBLE hack, but TNG doesn't allow us to setup a method selector instance directly.
296             Method method = clazz.getMethod( "setGroups", String.class, String.class );
297             method.invoke( null, groups, excludedGroups );
298         }
299         catch ( Exception e )
300         {
301             throw new TestSetFailedException( e.getMessage(), e );
302         }
303 
304         XmlMethodSelector xms = new XmlMethodSelector();
305 
306         xms.setName( clazzName );
307         // looks to need a high value
308         xms.setPriority( 9999 );
309 
310         return xms;
311     }
312 
313     static void run( List<String> suiteFiles, String testSourceDirectory,
314                      Map<String, String> options, // string,string because TestNGMapConfigurator#configure()
315                      TestNGReporter testNGReporter, File reportsDirectory, int skipAfterFailureCount )
316         throws TestSetFailedException
317     {
318         TestNG testng = new TestNG( true );
319         Configurator configurator = getConfigurator( options.get( "testng.configurator" ) );
320         configurator.configure( testng, options );
321         postConfigure( testng, testSourceDirectory, testNGReporter, reportsDirectory, skipAfterFailureCount,
322                        extractVerboseLevel( options ) );
323         testng.setTestSuites( suiteFiles );
324         testng.run();
325     }
326 
327     private static Configurator getConfigurator( String className )
328     {
329         try
330         {
331             return (Configurator) Class.forName( className ).newInstance();
332         }
333         catch ( ReflectiveOperationException e )
334         {
335             throw new RuntimeException( e );
336         }
337     }
338 
339     private static void postConfigure( TestNG testNG, String sourcePath, TestNGReporter testNGReporter,
340                                        File reportsDirectory, int skipAfterFailureCount, int verboseLevel )
341     {
342         // 0 (default): turn off all TestNG output
343         testNG.setVerbose( verboseLevel );
344         testNG.addListener( (ITestNGListener) testNGReporter );
345 
346         if ( skipAfterFailureCount > 0 )
347         {
348             ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
349             testNG.addListener( instantiate( classLoader, FailFastNotifier.class.getName(), Object.class ) );
350             testNG.addListener(
351                 new FailFastListener( createStoppable( testNGReporter.getRunListener(), skipAfterFailureCount ) ) );
352         }
353 
354         // FIXME: use classifier to decide if we need to pass along the source dir (only for JDK14)
355         if ( sourcePath != null )
356         {
357             testNG.setSourcePath( sourcePath );
358         }
359 
360         testNG.setOutputDirectory( reportsDirectory.getAbsolutePath() );
361     }
362 
363     private static Stoppable createStoppable( final RunListener reportManager, int skipAfterFailureCount )
364     {
365         final AtomicInteger currentFaultCount = new AtomicInteger( skipAfterFailureCount );
366 
367         return () ->
368         {
369             runIfZeroCountDown( () -> FailFastEventsSingleton.getInstance().setSkipOnNextTest(), currentFaultCount );
370             reportManager.testExecutionSkippedByUser();
371         };
372     }
373 
374     private static int extractVerboseLevel( Map<String, String> options )
375         throws TestSetFailedException
376     {
377         try
378         {
379             String verbose = options.get( "surefire.testng.verbose" );
380             return verbose == null ? 0 : Integer.parseInt( verbose );
381         }
382         catch ( NumberFormatException e )
383         {
384             throw new TestSetFailedException( "Provider property 'surefire.testng.verbose' should refer to "
385                                                   + "number -1 (debug mode), 0, 1 .. 10 (most detailed).", e );
386         }
387     }
388 }