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.booter.ProviderParameterNames;
23  import org.apache.maven.surefire.cli.CommandLineOption;
24  import org.apache.maven.surefire.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.Stoppable;
29  import org.apache.maven.surefire.testset.TestListResolver;
30  import org.apache.maven.surefire.testset.TestSetFailedException;
31  import org.apache.maven.surefire.util.internal.StringUtils;
32  import org.testng.TestNG;
33  import org.testng.annotations.Test;
34  import org.testng.xml.XmlClass;
35  import org.testng.xml.XmlMethodSelector;
36  import org.testng.xml.XmlSuite;
37  import org.testng.xml.XmlTest;
38  
39  import java.io.File;
40  import java.lang.annotation.Annotation;
41  import java.lang.reflect.Constructor;
42  import java.lang.reflect.InvocationTargetException;
43  import java.lang.reflect.Method;
44  import java.util.ArrayList;
45  import java.util.HashMap;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.concurrent.atomic.AtomicInteger;
49  
50  import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
51  import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS;
52  import static org.apache.maven.surefire.util.ReflectionUtils.instantiate;
53  import static org.apache.maven.surefire.util.ReflectionUtils.tryLoadClass;
54  import static org.apache.maven.surefire.util.internal.ConcurrencyUtils.countDownToZero;
55  
56  /**
57   * Contains utility methods for executing TestNG.
58   *
59   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
60   * @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a>
61   */
62  final class TestNGExecutor
63  {
64      /** The default name for a suite launched from the maven surefire plugin */
65      private static final String DEFAULT_SUREFIRE_SUITE_NAME = "Surefire suite";
66  
67      /** The default name for a test launched from the maven surefire plugin */
68      private static final String DEFAULT_SUREFIRE_TEST_NAME = "Surefire test";
69  
70      private static final boolean HAS_TEST_ANNOTATION_ON_CLASSPATH =
71              tryLoadClass( TestNGExecutor.class.getClassLoader(), "org.testng.annotations.Test" ) != null;
72  
73      private TestNGExecutor()
74      {
75          throw new IllegalStateException( "not instantiable constructor" );
76      }
77  
78      @SuppressWarnings( "checkstyle:parameternumbercheck" )
79      static void run( Iterable<Class<?>> testClasses, String testSourceDirectory,
80                              Map<String, String> options, // string,string because TestNGMapConfigurator#configure()
81                              RunListener reportManager, File reportsDirectory,
82                              TestListResolver methodFilter, List<CommandLineOption> mainCliOptions,
83                              int skipAfterFailureCount )
84          throws TestSetFailedException
85      {
86          TestNG testng = new TestNG( true );
87  
88          Configurator configurator = getConfigurator( options.get( "testng.configurator" ) );
89  
90          if ( isCliDebugOrShowErrors( mainCliOptions ) )
91          {
92              System.out.println( "Configuring TestNG with: " + configurator.getClass().getSimpleName() );
93          }
94  
95          XmlMethodSelector groupMatchingSelector = createGroupMatchingSelector( options );
96          XmlMethodSelector methodNameFilteringSelector = createMethodNameFilteringSelector( methodFilter );
97  
98          Map<String, SuiteAndNamedTests> suitesNames = new HashMap<>();
99  
100         List<XmlSuite> xmlSuites = new ArrayList<>();
101         for ( Class<?> testClass : testClasses )
102         {
103             TestMetadata metadata = findTestMetadata( testClass );
104 
105             SuiteAndNamedTests suiteAndNamedTests = suitesNames.get( metadata.suiteName );
106             if ( suiteAndNamedTests == null )
107             {
108                 suiteAndNamedTests = new SuiteAndNamedTests();
109                 suiteAndNamedTests.xmlSuite.setName( metadata.suiteName );
110                 configurator.configure( suiteAndNamedTests.xmlSuite, options );
111                 xmlSuites.add( suiteAndNamedTests.xmlSuite );
112 
113                 suitesNames.put( metadata.suiteName, suiteAndNamedTests );
114             }
115 
116             XmlTest xmlTest = suiteAndNamedTests.testNameToTest.get( metadata.testName );
117             if ( xmlTest == null )
118             {
119                 xmlTest = new XmlTest( suiteAndNamedTests.xmlSuite );
120                 xmlTest.setName( metadata.testName );
121                 addSelector( xmlTest, groupMatchingSelector );
122                 addSelector( xmlTest, methodNameFilteringSelector );
123                 xmlTest.setXmlClasses( new ArrayList<XmlClass>() );
124 
125                 suiteAndNamedTests.testNameToTest.put( metadata.testName, xmlTest );
126             }
127 
128             xmlTest.getXmlClasses().add( new XmlClass( testClass.getName() ) );
129         }
130 
131         testng.setXmlSuites( xmlSuites );
132         configurator.configure( testng, options );
133         postConfigure( testng, testSourceDirectory, reportManager, reportsDirectory, skipAfterFailureCount,
134                        extractVerboseLevel( options ) );
135         testng.run();
136     }
137 
138     private static boolean isCliDebugOrShowErrors( List<CommandLineOption> mainCliOptions )
139     {
140         return mainCliOptions.contains( LOGGING_LEVEL_DEBUG ) || mainCliOptions.contains( SHOW_ERRORS );
141     }
142 
143     private static TestMetadata findTestMetadata( Class<?> testClass )
144     {
145         TestMetadata result = new TestMetadata();
146         if ( HAS_TEST_ANNOTATION_ON_CLASSPATH )
147         {
148             Test testAnnotation = findAnnotation( testClass, Test.class );
149             if ( null != testAnnotation )
150             {
151                 if ( !StringUtils.isBlank( testAnnotation.suiteName() ) )
152                 {
153                     result.suiteName = testAnnotation.suiteName();
154                 }
155 
156                 if ( !StringUtils.isBlank( testAnnotation.testName() ) )
157                 {
158                     result.testName = testAnnotation.testName();
159                 }
160             }
161         }
162         return result;
163     }
164 
165     private static <T extends Annotation> T findAnnotation( Class<?> clazz, Class<T> annotationType )
166     {
167         if ( clazz == null )
168         {
169             return null;
170         }
171 
172         T result = clazz.getAnnotation( annotationType );
173         if ( result != null )
174         {
175             return result;
176         }
177 
178         return findAnnotation( clazz.getSuperclass(), annotationType );
179     }
180 
181     private static class TestMetadata
182     {
183         private String testName = DEFAULT_SUREFIRE_TEST_NAME;
184 
185         private String suiteName = DEFAULT_SUREFIRE_SUITE_NAME;
186     }
187 
188     private static class SuiteAndNamedTests
189     {
190         private XmlSuite xmlSuite = new XmlSuite();
191 
192         private Map<String, XmlTest> testNameToTest = new HashMap<>();
193     }
194 
195     private static void addSelector( XmlTest xmlTest, XmlMethodSelector selector )
196     {
197         if ( selector != null )
198         {
199             xmlTest.getMethodSelectors().add( selector );
200         }
201     }
202 
203     @SuppressWarnings( "checkstyle:magicnumber" )
204     private static XmlMethodSelector createMethodNameFilteringSelector( TestListResolver methodFilter )
205         throws TestSetFailedException
206     {
207         if ( methodFilter != null && !methodFilter.isEmpty() )
208         {
209             // the class is available in the testClassPath
210             String clazzName = "org.apache.maven.surefire.testng.utils.MethodSelector";
211             try
212             {
213                 Class<?> clazz = Class.forName( clazzName );
214                 Method method = clazz.getMethod( "setTestListResolver", TestListResolver.class );
215                 method.invoke( null, methodFilter );
216             }
217             catch ( Exception e )
218             {
219                 throw new TestSetFailedException( e.getMessage(), e );
220             }
221 
222             XmlMethodSelector xms = new XmlMethodSelector();
223 
224             xms.setName( clazzName );
225             // looks to need a high value
226             xms.setPriority( 10000 );
227 
228             return xms;
229         }
230         else
231         {
232             return null;
233         }
234     }
235 
236     @SuppressWarnings( "checkstyle:magicnumber" )
237     private static XmlMethodSelector createGroupMatchingSelector( Map<String, String> options )
238         throws TestSetFailedException
239     {
240         final String groups = options.get( ProviderParameterNames.TESTNG_GROUPS_PROP );
241         final String excludedGroups = options.get( ProviderParameterNames.TESTNG_EXCLUDEDGROUPS_PROP );
242 
243         if ( groups == null && excludedGroups == null )
244         {
245             return null;
246         }
247 
248         // the class is available in the testClassPath
249         final String clazzName = "org.apache.maven.surefire.testng.utils.GroupMatcherMethodSelector";
250         try
251         {
252             Class<?> clazz = Class.forName( clazzName );
253 
254             // HORRIBLE hack, but TNG doesn't allow us to setup a method selector instance directly.
255             Method method = clazz.getMethod( "setGroups", String.class, String.class );
256             method.invoke( null, groups, excludedGroups );
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( 9999 );
268 
269         return xms;
270     }
271 
272     static void run( List<String> suiteFiles, String testSourceDirectory,
273                             Map<String, String> options, // string,string because TestNGMapConfigurator#configure()
274                             RunListener reportManager, File reportsDirectory, int skipAfterFailureCount )
275         throws TestSetFailedException
276     {
277         TestNG testng = new TestNG( true );
278         Configurator configurator = getConfigurator( options.get( "testng.configurator" ) );
279         configurator.configure( testng, options );
280         postConfigure( testng, testSourceDirectory, reportManager, reportsDirectory, skipAfterFailureCount,
281                        extractVerboseLevel( options ) );
282         testng.setTestSuites( suiteFiles );
283         testng.run();
284     }
285 
286     private static Configurator getConfigurator( String className )
287     {
288         try
289         {
290             return (Configurator) Class.forName( className ).newInstance();
291         }
292         catch ( ReflectiveOperationException e )
293         {
294             throw new RuntimeException( e );
295         }
296     }
297 
298     private static void postConfigure( TestNG testNG, String sourcePath, final RunListener reportManager,
299                                        File reportsDirectory, int skipAfterFailureCount, int verboseLevel )
300     {
301         // 0 (default): turn off all TestNG output
302         testNG.setVerbose( verboseLevel );
303 
304         TestNGReporter reporter = createTestNGReporter( reportManager );
305         testNG.addListener( (Object) reporter );
306 
307         if ( skipAfterFailureCount > 0 )
308         {
309             ClassLoader cl = Thread.currentThread().getContextClassLoader();
310             testNG.addListener( instantiate( cl, "org.apache.maven.surefire.testng.utils.FailFastNotifier",
311                                              Object.class ) );
312             testNG.addListener( new FailFastListener( createStoppable( reportManager, skipAfterFailureCount ) ) );
313         }
314 
315         // FIXME: use classifier to decide if we need to pass along the source dir (only for JDK14)
316         if ( sourcePath != null )
317         {
318             testNG.setSourcePath( sourcePath );
319         }
320 
321         testNG.setOutputDirectory( reportsDirectory.getAbsolutePath() );
322     }
323 
324     private static Stoppable createStoppable( final RunListener reportManager, int skipAfterFailureCount )
325     {
326         final AtomicInteger currentFaultCount = new AtomicInteger( skipAfterFailureCount );
327 
328         return new Stoppable()
329         {
330             @Override
331             public void fireStopEvent()
332             {
333                 if ( countDownToZero( currentFaultCount ) )
334                 {
335                     FailFastEventsSingleton.getInstance().setSkipOnNextTest();
336                 }
337 
338                 reportManager.testExecutionSkippedByUser();
339             }
340         };
341     }
342 
343     // If we have access to IResultListener, return a ConfigurationAwareTestNGReporter
344     // But don't cause NoClassDefFoundErrors if it isn't available; just return a regular TestNGReporter instead
345     private static TestNGReporter createTestNGReporter( RunListener reportManager )
346     {
347         try
348         {
349             Class.forName( "org.testng.internal.IResultListener" );
350             Class c = Class.forName( "org.apache.maven.surefire.testng.ConfigurationAwareTestNGReporter" );
351             @SuppressWarnings( "unchecked" ) Constructor<?> ctor = c.getConstructor( RunListener.class );
352             return (TestNGReporter) ctor.newInstance( reportManager );
353         }
354         catch ( InvocationTargetException e )
355         {
356             throw new RuntimeException( "Bug in ConfigurationAwareTestNGReporter", e.getCause() );
357         }
358         catch ( ClassNotFoundException e )
359         {
360             return new TestNGReporter( reportManager );
361         }
362         catch ( Exception e )
363         {
364             throw new RuntimeException( "Bug in ConfigurationAwareTestNGReporter", e );
365         }
366     }
367 
368     private static int extractVerboseLevel( Map<String, String> options )
369         throws TestSetFailedException
370     {
371         try
372         {
373             String verbose = options.get( "surefire.testng.verbose" );
374             return verbose == null ? 0 : Integer.parseInt( verbose );
375         }
376         catch ( NumberFormatException e )
377         {
378             throw new TestSetFailedException( "Provider property 'surefire.testng.verbose' should refer to "
379                                                   + "number -1 (debug mode), 0, 1 .. 10 (most detailed).", e );
380         }
381     }
382 }