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