View Javadoc
1   /*
2    * Copyright (C) 2010-2014 Hamburg Sud and the contributors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.aludratest.scheduler.impl;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.Enumeration;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.concurrent.atomic.AtomicLong;
33  import java.util.jar.JarEntry;
34  import java.util.jar.JarFile;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  
38  import org.aludratest.PreconditionFailedException;
39  import org.aludratest.dict.Data;
40  import org.aludratest.invoker.AludraTestMethodInvoker;
41  import org.aludratest.invoker.ErrorReportingInvoker;
42  import org.aludratest.invoker.TestInvoker;
43  import org.aludratest.scheduler.AnnotationBasedExecution;
44  import org.aludratest.scheduler.RunnerTree;
45  import org.aludratest.scheduler.RunnerTreeBuilder;
46  import org.aludratest.scheduler.TestClassFilter;
47  import org.aludratest.scheduler.node.ExecutionMode;
48  import org.aludratest.scheduler.node.RunnerGroup;
49  import org.aludratest.scheduler.node.RunnerLeaf;
50  import org.aludratest.scheduler.node.RunnerNode;
51  import org.aludratest.scheduler.util.CommonRunnerLeafAttributes;
52  import org.aludratest.testcase.AludraTestCase;
53  import org.aludratest.testcase.Parallel;
54  import org.aludratest.testcase.Sequential;
55  import org.aludratest.testcase.Suite;
56  import org.aludratest.testcase.Test;
57  import org.aludratest.testcase.data.TestCaseData;
58  import org.aludratest.testcase.data.TestDataProvider;
59  import org.codehaus.plexus.component.annotations.Component;
60  import org.codehaus.plexus.component.annotations.Requirement;
61  import org.databene.commons.BeanUtil;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  @Component(role = RunnerTreeBuilder.class, instantiationStrategy = "per-lookup")
66  public class RunnerTreeBuilderImpl implements RunnerTreeBuilder {
67  
68      private static final Logger LOGGER = LoggerFactory.getLogger(RunnerTreeBuilder.class);
69  
70      private final AtomicLong errorCount = new AtomicLong();
71  
72      private final AtomicInteger nextLeafId = new AtomicInteger();
73  
74      /** Used to trace added files and detect recursions & multi-uses of classes */
75      private Set<Class<?>> addedClasses = new HashSet<Class<?>>();
76  
77      /** Map Class -> Assertion Error for classes where an assertion failed */
78      private Map<Class<?>, String> assertionErrorClasses = new HashMap<Class<?>, String>();
79  
80      @Requirement
81      private TestDataProvider testDataProvider;
82  
83      @Override
84      public RunnerTree buildRunnerTree(Class<?> suiteOrTestClass) {
85          RunnerTree tree = new RunnerTree();
86          parseTestOrSuiteClass(suiteOrTestClass, null, tree);
87          if (!assertionErrorClasses.isEmpty()) {
88              // concatenate all exceptions
89              Iterator<Map.Entry<Class<?>, String>> iter = assertionErrorClasses.entrySet().iterator();
90              throw concatAssertionExceptions(iter, null);
91          }
92          return tree;
93      }
94  
95      @Override
96      public RunnerTree buildRunnerTree(AnnotationBasedExecution executionConfig) {
97          // find all class files matching the filter
98          List<Class<? extends AludraTestCase>> testClasses;
99  
100         File searchRoot = executionConfig.getJarOrClassRoot();
101         if (searchRoot.isDirectory()) {
102             testClasses = findMatchingClassesInFolder(searchRoot, "", executionConfig.getFilter(),
103                     executionConfig.getClassLoader());
104         }
105         else if (searchRoot.isFile()) {
106             try {
107                 testClasses = findMatchingClassesInJar(searchRoot, executionConfig.getFilter(), executionConfig.getClassLoader());
108             }
109             catch (IOException e) {
110                 throw new PreconditionFailedException("Could not search JAR file " + searchRoot.getAbsolutePath()
111                         + " for test classes", e);
112             }
113         }
114         else {
115             throw new PreconditionFailedException("Unknown file type for class root " + searchRoot.getAbsolutePath());
116         }
117 
118         RunnerTree tree = new RunnerTree();
119         tree.createRoot("All Tests", true);
120 
121         CategoryBuilder categoryBuilder;
122         if (executionConfig.getGroupingAttributes().isEmpty()) {
123             String commonPackageRoot = getCommonPackageRoot(testClasses);
124             categoryBuilder = new CategoryBuilder(commonPackageRoot);
125         }
126         else {
127             categoryBuilder = new CategoryBuilder(executionConfig.getGroupingAttributes());
128         }
129 
130         for (Class<? extends AludraTestCase> clz : testClasses) {
131             parseTestClass(clz, categoryBuilder.getParentRunnerGroup(tree, clz), tree);
132         }
133 
134         return tree;
135     }
136 
137     @SuppressWarnings("unchecked")
138     private List<Class<? extends AludraTestCase>> findMatchingClassesInFolder(File folder, String packagePrefix,
139             TestClassFilter filter, ClassLoader classLoader) {
140         List<Class<? extends AludraTestCase>> result = new ArrayList<Class<? extends AludraTestCase>>();
141         File[] children = folder.listFiles();
142         for (File file : children) {
143             if (file.isDirectory()) {
144                 result.addAll(findMatchingClassesInFolder(file,
145                         packagePrefix + ("".equals(packagePrefix) ? "" : ".") + file.getName(), filter, classLoader));
146             }
147             else if (file.isFile() && (file.getName().endsWith(".class") || file.getName().endsWith(".java"))) {
148                 String className = packagePrefix + "." + file.getName().substring(0, file.getName().lastIndexOf('.'));
149                 try {
150                     Class<?> clz;
151                     if (classLoader != null) {
152                         clz = classLoader.loadClass(className);
153                     }
154                     else {
155                         clz = Class.forName(className);
156                     }
157                     if (AludraTestCase.class.isAssignableFrom(clz) && !result.contains(clz)
158                             && filter.matches((Class<? extends AludraTestCase>) clz)) {
159                         result.add((Class<? extends AludraTestCase>) clz);
160                     }
161                 }
162                 catch (Throwable t) {
163                     // ignore that class
164                 }
165             }
166         }
167         return result;
168     }
169 
170     @SuppressWarnings("unchecked")
171     private List<Class<? extends AludraTestCase>> findMatchingClassesInJar(File jarFile, TestClassFilter filter,
172             ClassLoader classLoader)
173                     throws IOException {
174         Pattern classPattern = Pattern.compile("(.+/|)([^/]+)\\.class");
175 
176         List<Class<? extends AludraTestCase>> result = new ArrayList<Class<? extends AludraTestCase>>();
177 
178         JarFile jf = new JarFile(jarFile);
179         Enumeration<JarEntry> entries = jf.entries();
180         while (entries.hasMoreElements()) {
181             JarEntry je = entries.nextElement();
182             Matcher m = classPattern.matcher(je.getName());
183             if (m.matches()) {
184                 String pkgName = m.group(1).replace('/', '.');
185                 String className = m.group(2);
186                 if (!"".equals(pkgName)) {
187                     className = pkgName + "." + className;
188                 }
189                 try {
190                     Class<?> clz;
191                     if (classLoader != null) {
192                         clz = classLoader.loadClass(className);
193                     }
194                     else {
195                         clz = Class.forName(className);
196                     }
197                     if (AludraTestCase.class.isAssignableFrom(clz) && filter.matches((Class<? extends AludraTestCase>) clz)) {
198                         result.add((Class<? extends AludraTestCase>) clz);
199                     }
200                 }
201                 catch (Throwable t) {
202                     // ignore that class
203                 }
204             }
205         }
206 
207         return result;
208     }
209 
210     private String getCommonPackageRoot(List<Class<? extends AludraTestCase>> testClasses) {
211         if (testClasses.isEmpty()) {
212             return "";
213         }
214         String commonPrefix = testClasses.get(0).getName();
215         for (Class<?> clz : testClasses) {
216             String cn = clz.getName();
217             commonPrefix = getCommonPrefix(commonPrefix, cn);
218             if ("".equals(commonPrefix)) {
219                 // totally unequal
220                 return commonPrefix;
221             }
222         }
223 
224         if (commonPrefix.contains(".")) {
225             commonPrefix = commonPrefix.substring(0, commonPrefix.lastIndexOf('.'));
226         }
227         else {
228             return "";
229         }
230 
231         return commonPrefix;
232     }
233 
234     private String getCommonPrefix(String s1, String s2) {
235         int i;
236         for (i = 0; i < s1.length() && i < s2.length() && s1.charAt(i) == s2.charAt(i); i++)
237             ;
238         return s1.substring(0, i);
239     }
240 
241     private PreconditionFailedException concatAssertionExceptions(Iterator<Map.Entry<Class<?>, String>> iterator,
242             PreconditionFailedException cause) {
243         if (!iterator.hasNext()) {
244             return cause;
245         }
246         Map.Entry<Class<?>, String> entry = iterator.next();
247         String msg = entry.getValue() + ": " + entry.getKey().getName();
248         PreconditionFailedException ex = cause == null ? new PreconditionFailedException(msg) : new PreconditionFailedException(
249                 msg, cause);
250         return concatAssertionExceptions(iterator, ex);
251     }
252 
253     /** Parses an AludraTest test class or suite. */
254     private void parseTestOrSuiteClass(Class<?> testClass, RunnerGroup parentGroup, RunnerTree tree) {
255         // check test class type
256         if (isTestSuiteClass(testClass)) {
257             parseSuiteClass(testClass, parentGroup, tree);
258         }
259         else {
260             if (assertTestClass(testClass)) {
261                 parseTestClass(testClass, parentGroup, tree);
262             }
263         }
264     }
265 
266     private boolean isTestSuiteClass(Class<?> testClass) {
267         return (testClass.getAnnotation(Suite.class) != null);
268     }
269 
270     private boolean assertTestClass(Class<?> testClass) {
271         if ((testClass.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT) {
272             assertionErrorClasses.put(testClass, "Abstract class not suitable as test class");
273             return false;
274         }
275         else if (!AludraTestCase.class.isAssignableFrom(testClass)) {
276             assertionErrorClasses.put(testClass, "Test class does not inherit from " + AludraTestCase.class.getName());
277             return false;
278         }
279         else if (testMethodCount(testClass) == 0) {
280             assertionErrorClasses.put(testClass, "No @Test methods found in class");
281             return false;
282         }
283 
284         return true;
285     }
286 
287     private int testMethodCount(Class<?> testClass) {
288         int count = 0;
289         for (Method method : testClass.getMethods()) {
290             if (method.getAnnotation(Test.class) != null) {
291                 count++;
292             }
293         }
294         return count;
295     }
296 
297     private void checkAddTestClass(Class<?> clazz) {
298         if (addedClasses.contains(clazz)) {
299             throw new PreconditionFailedException("The class " + clazz
300                     + " is used in more than one test suite, or part of a test suite recursion.");
301         }
302         addedClasses.add(clazz);
303     }
304 
305     /** Parses an AludraTest test suite class. */
306     private void parseSuiteClass(Class<?> testClass, RunnerGroup parentGroup, RunnerTree tree) {
307         LOGGER.debug("Parsing suite class: {}", testClass.getName());
308         checkAddTestClass(testClass);
309         addedClasses.add(testClass);
310         Suite suite = testClass.getAnnotation(Suite.class);
311         if (suite == null) {
312             throw new IllegalArgumentException("Class has no @Suite annotation");
313         }
314         RunnerGroup group = createRunnerGroupForTestClass(testClass, parentGroup, tree);
315         for (Class<?> component : suite.value()) {
316             parseTestOrSuiteClass(component, group, tree);
317         }
318     }
319 
320     /** Parses an AludraTest test class. */
321     private void parseTestClass(Class<?> testClass, RunnerGroup parentGroup, RunnerTree tree) {
322         LOGGER.debug("Parsing test class: {}", testClass.getName());
323         checkAddTestClass(testClass);
324         RunnerGroup classGroup = createRunnerGroupForTestClass(testClass, parentGroup, tree);
325         for (Method method : testClass.getMethods()) {
326             parseMethod(method, testClass, classGroup, tree);
327         }
328     }
329 
330     /** Creates a {@link RunnerGroup} for an AludraTest test class */
331     private RunnerGroup createRunnerGroupForTestClass(Class<?> testClass, RunnerGroup parentGroup, RunnerTree tree) {
332         ExecutionMode mode;
333         if (testClass.getAnnotation(Parallel.class) != null) {
334             mode = ExecutionMode.PARALLEL;
335         }
336         else if (testClass.getAnnotation(Sequential.class) != null) {
337             mode = ExecutionMode.SEQUENTIAL;
338         }
339         else {
340             mode = ExecutionMode.INHERITED;
341         }
342         RunnerGroup group = tree.createGroup(testClass.getName(), mode, parentGroup);
343         return group;
344     }
345 
346     /** Parses a method */
347     private void parseMethod(Method method, Class<?> testClass, RunnerGroup classGroup, RunnerTree tree) {
348         if (method.getAnnotation(Test.class) != null) {
349             LOGGER.debug("Parsing test class method: {}", method);
350             ExecutionMode mode;
351             if (method.getAnnotation(Parallel.class) != null) {
352                 mode = ExecutionMode.PARALLEL;
353             }
354             else if (method.getAnnotation(Sequential.class) != null) {
355                 mode = ExecutionMode.SEQUENTIAL;
356             }
357             else {
358                 mode = ExecutionMode.INHERITED;
359             }
360             String methodTestSuiteName = createMethodTestSuiteName(testClass, method);
361             RunnerGroup methodGroup = tree.createGroup(methodTestSuiteName, mode, classGroup);
362             try {
363                 // iterate through method invocations
364                 List<TestCaseData> invocationParams = testDataProvider.getTestDataSets(method);
365                 for (TestCaseData data : invocationParams) {
366                     if (data.getException() == null) {
367                         createTestRunnerForMethodInvocation(method, data.getData(), data.getId(), data.isIgnored(),
368                                 data.getIgnoredReason(), methodGroup, tree);
369                     }
370                     else {
371                         createTestRunnerForErrorReporting(method, data.getException(), methodGroup, tree);
372                     }
373                 }
374             }
375             catch (Exception e) {
376                 createTestRunnerForErrorReporting(method, e, methodGroup, tree);
377             }
378         }
379     }
380 
381     /** Creates a test runner for a single method invocation */
382     private void createTestRunnerForMethodInvocation(Method method, Data[] args, String testInfo, boolean ignore,
383             String ignoredReason, RunnerGroup methodGroup, RunnerTree tree) {
384         // create log4testing TestCase
385         String invocationTestCaseName = createInvocationTestCaseName(testInfo, methodGroup.getName());
386         // Create test object
387         @SuppressWarnings("unchecked")
388         AludraTestCase testObject = BeanUtil.newInstance((Class<? extends AludraTestCase>) method.getDeclaringClass());
389         TestInvoker invoker = new AludraTestMethodInvoker(testObject, method, args);
390         createRunnerForTestInvoker(invoker, methodGroup, tree, invocationTestCaseName, ignore, ignoredReason);
391     }
392 
393     /** Creates a test runner for error reporting.
394      * @param e The exception that occurred. */
395     private void createTestRunnerForErrorReporting(Method method, Throwable e, RunnerGroup methodGroup, RunnerTree tree) {
396         LOGGER.error("createTestRunnerForErrorReporting('{}', {}, {}, ...)", new Object[] { method, e, methodGroup });
397         // create log4testing TestCase name
398         String invocationTestCaseName = createMethodTestSuiteName(method.getDeclaringClass(), method) + "_error_"
399                 + errorCount.incrementAndGet();
400         // Create test object
401         TestInvoker invoker = new ErrorReportingInvoker(method, e);
402         createRunnerForTestInvoker(invoker, methodGroup, tree, invocationTestCaseName, false, null);
403     }
404 
405     private void createRunnerForTestInvoker(TestInvoker invoker, RunnerGroup parentGroup, RunnerTree tree, String testCaseName,
406             boolean ignore, String ignoredReason) {
407         RunnerLeaf leaf = tree.addLeaf(nextLeafId.incrementAndGet(), invoker, testCaseName, parentGroup);
408         if (ignore) {
409             leaf.setAttribute(CommonRunnerLeafAttributes.IGNORE, Boolean.valueOf(ignore));
410             if (ignoredReason != null) {
411                 leaf.setAttribute(CommonRunnerLeafAttributes.IGNORE_REASON, ignoredReason);
412             }
413         }
414     }
415 
416     /** Creates a test case name for a test method. */
417     private static String createMethodTestSuiteName(Class<?> testClass, Method method) {
418         return testClass.getName() + '.' + method.getName();
419     }
420 
421     /** Creates a test case name for a test method invocation. */
422     private static String createInvocationTestCaseName(String testInfo, String methodTestSuiteName) {
423         return methodTestSuiteName + '-' + testInfo;
424     }
425 
426     private static class CategoryBuilder {
427 
428         private String removePackagePrefix;
429 
430         private List<String> categoryOrder;
431 
432         public CategoryBuilder(List<String> categoryOrder) {
433             this.categoryOrder = categoryOrder;
434         }
435 
436         public CategoryBuilder(String removePackagePrefix) {
437             this.removePackagePrefix = removePackagePrefix;
438             this.categoryOrder = Collections.emptyList();
439         }
440 
441         public RunnerGroup getParentRunnerGroup(RunnerTree tree, Class<? extends AludraTestCase> clazz) {
442             if (!categoryOrder.isEmpty()) {
443                 List<String> categories = new ArrayList<String>();
444                 StringBuilder prefix = new StringBuilder();
445                 for (String cat : categoryOrder) {
446                     String catVal = TestAttributeUtil.getTestAttributes(clazz).get(cat);
447                     if (catVal == null) {
448                         catVal = cat + " unknown";
449                     }
450                     categories.add(prefix + catVal);
451                     prefix.append(catVal).append(".");
452                 }
453                 return forceGetRunnerGroup(tree, categories);
454             }
455             else {
456                 String className = clazz.getName();
457                 if (!"".equals(removePackagePrefix) && className.startsWith(removePackagePrefix)) {
458                     className = className.substring(removePackagePrefix.length());
459                     if (className.startsWith(".")) {
460                         className = className.substring(1);
461                     }
462                 }
463 
464                 // remove class itself
465                 if (className.contains(".")) {
466                     className = className.substring(0, className.lastIndexOf('.'));
467                 }
468                 else {
469                     // no package available
470                     return tree.getRoot();
471                 }
472 
473                 List<String> groups = new ArrayList<String>();
474                 int i = 0;
475                 while (i < className.length()) {
476                     int nextIndex = className.indexOf('.', i);
477                     if (nextIndex == -1) {
478                         groups.add(className);
479                         break;
480                     }
481                     groups.add(className.substring(0, nextIndex));
482                     i = nextIndex + 1;
483                 }
484 
485                 return forceGetRunnerGroup(tree, groups);
486             }
487         }
488 
489         private RunnerGroup forceGetRunnerGroup(RunnerTree tree, List<String> pathSegments) {
490             RunnerGroup group = tree.getRoot();
491 
492             for (String seg : pathSegments) {
493                 boolean found = false;
494                 for (RunnerNode node : group.getChildren()) {
495                     if (node instanceof RunnerGroup && seg.equals(node.getName())) {
496                         group = (RunnerGroup) node;
497                         found = true;
498                         break;
499                     }
500                 }
501                 if (!found) {
502                     group = tree.createGroup(seg, ExecutionMode.PARALLEL, group);
503                 }
504             }
505 
506             return group;
507         }
508 
509     }
510 
511 }