1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
75 private Set<Class<?>> addedClasses = new HashSet<Class<?>>();
76
77
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
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
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
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
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
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
254 private void parseTestOrSuiteClass(Class<?> testClass, RunnerGroup parentGroup, RunnerTree tree) {
255
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
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
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
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
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
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
382 private void createTestRunnerForMethodInvocation(Method method, Data[] args, String testInfo, boolean ignore,
383 String ignoredReason, RunnerGroup methodGroup, RunnerTree tree) {
384
385 String invocationTestCaseName = createInvocationTestCaseName(testInfo, methodGroup.getName());
386
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
394
395 private void createTestRunnerForErrorReporting(Method method, Throwable e, RunnerGroup methodGroup, RunnerTree tree) {
396 LOGGER.error("createTestRunnerForErrorReporting('{}', {}, {}, ...)", new Object[] { method, e, methodGroup });
397
398 String invocationTestCaseName = createMethodTestSuiteName(method.getDeclaringClass(), method) + "_error_"
399 + errorCount.incrementAndGet();
400
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
417 private static String createMethodTestSuiteName(Class<?> testClass, Method method) {
418 return testClass.getName() + '.' + method.getName();
419 }
420
421
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
465 if (className.contains(".")) {
466 className = className.substring(0, className.lastIndexOf('.'));
467 }
468 else {
469
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 }