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