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