/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.core.test.collect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern;
import junit.framework.JUnit4TestAdapter;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.eclipse.riena.core.util.Iter;
/**
* A helper for collecting test classes.
*
* @since 5.0
*/
public final class TestCollector {
private static final String PLUS = " + "; //$NON-NLS-1$
private TestCollector() {
// utility
}
/**
* Create a {@code TestSuite} that contains all test cases in the given bundle.
*
* @param bundle
* bundle to collect all {@code TestCase}s
* @param withinPackage
* if not <code>null</code>, only {@code TestCase}s within this package are collected
* @param annotationClasses
* only {@code TestCase}s that have one of these annotations are collected
* @return
*/
public static TestSuite createTestSuiteWith(final Bundle bundle, final Package withinPackage, final Class<? extends Annotation>... annotationClasses) {
return createTestSuiteWith(bundle, withinPackage, false, annotationClasses);
}
/**
* Create a {@code TestSuite} that contains all test cases in the given bundle.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param annotationClasses
* only test cases that have one of these annotations are collected
* @return
*/
public static TestSuite createTestSuiteWithJUnit3And4(final Bundle bundle, final Package withinPackage,
final Class<? extends Annotation>... annotationClasses) {
return createTestSuiteWithJUnit3And4(bundle, withinPackage, false, annotationClasses);
}
/**
* Create a {@code TestSuite} that contains all test cases in the given bundle.
*
* @param bundle
* bundle to collect all {@code TestCase}s
* @param withinPackage
* if not <code>null</code>, only {@code TestCase}s within this package are collected
* @param annotationClasses
* only {@code TestCase}s that have one of these annotations are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @return
*/
public static TestSuite createTestSuiteWith(final Bundle bundle, final Package withinPackage, final boolean subPackages,
final Class<? extends Annotation>... annotationClasses) {
return createTestSuiteWith(bundle, withinPackage, subPackages, true, annotationClasses);
}
@SuppressWarnings("unchecked")
private static TestSuite createTestSuiteWith(final Bundle bundle, final Package withinPackage, final boolean subPackages, final boolean excludeJUnit4Tests,
final Class<? extends Annotation>... annotationClasses) {
final StringBuilder bob = new StringBuilder("Tests within bundle '").append(bundle.getSymbolicName()).append( //$NON-NLS-1$
"' and package '"); //$NON-NLS-1$
bob.append(withinPackage == null ? "all" : withinPackage.getName()).append("'").append(" recursive '").append( //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
subPackages).append("'"); //$NON-NLS-1$
bob.append(" and restricted to '"); //$NON-NLS-1$
for (final Class<? extends Annotation> annotationClass : annotationClasses) {
bob.append(annotationClass.getSimpleName()).append(PLUS);
}
if (annotationClasses.length > 0) {
bob.setLength(bob.length() - PLUS.length());
} else {
bob.append("none"); //$NON-NLS-1$
}
bob.append("'."); //$NON-NLS-1$
final TestSuite suite = new TestSuite(bob.toString());
for (final Class<?> clazz : collectWith(bundle, withinPackage, subPackages, excludeJUnit4Tests, annotationClasses)) {
if (TestCase.class.isAssignableFrom(clazz)) {
suite.addTestSuite((Class<? extends TestCase>) clazz);
} else {
suite.addTest(new JUnit4TestAdapter(clazz));
}
}
return suite;
}
/**
* Create a {@code TestSuite} that contains all test cases in the given bundle. The suite will contain JUnit3 and JUnit4 tests.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param annotationClasses
* only test cases that have one of these annotations are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @return
*/
public static TestSuite createTestSuiteWithJUnit3And4(final Bundle bundle, final Package withinPackage, final boolean subPackages,
final Class<? extends Annotation>... annotationClasses) {
return createTestSuiteWith(bundle, withinPackage, subPackages, false, annotationClasses);
}
/**
* Collect all {@code TestCase}e within the given bundle.
*
* @param bundle
* bundle to collect all {@code TestCase}s
* @param withinPackage
* if not <code>null</code>, only {@code TestCase}s within this package are collected
* @param annotationClasses
* only {@code TestCase}s that have one of these annotations are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @return
*/
public static List<Class<? extends TestCase>> collectWith(final Bundle bundle, final Package withinPackage, final boolean subPackages,
final Class<? extends Annotation>... annotationClasses) {
return convertToTestCaseList(collectWith(bundle, withinPackage, subPackages, true, annotationClasses));
}
private static List<Class<?>> collectWith(final Bundle bundle, final Package withinPackage, final boolean subPackages, final boolean excludeJUnit4Tests,
final Class<? extends Annotation>... annotationClasses) {
final List<Class<?>> testClasses = new ArrayList<Class<?>>();
for (final Class<?> testClass : collect(bundle, withinPackage, subPackages, excludeJUnit4Tests)) {
boolean collect = annotationClasses.length == 0 ? true : false;
for (final Class<? extends Annotation> annotationClass : annotationClasses) {
collect = collect || testClass.isAnnotationPresent(annotationClass);
}
if (collect) {
testClasses.add(testClass);
}
}
return testClasses;
}
/**
* Collect all test cases within the given bundle with at least one of the given annotations. This method considers JUnit3 and JUnit4 test cases.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @param annotationClasses
* only test cases that have one of these annotations are collected
* @return
*/
public static List<Class<?>> collectWithJUnit3And4(final Bundle bundle, final Package withinPackage, final boolean subPackages,
final Class<? extends Annotation>... annotationClasses) {
return collectWith(bundle, withinPackage, subPackages, false, annotationClasses);
}
/**
* Collect all unmarked {@code TestCase}s within the given bundle. This method considers JUnit3 tests only.
*
* @param bundle
* bundle to collect all {@code TestCase}s
* @param withinPackage
* if not <code>null</code>, only {@code TestCase}s within this package are collected
* @return
*/
public static List<Class<? extends TestCase>> collectUnmarked(final Bundle bundle, final Package withinPackage) {
return convertToTestCaseList(collectUnmarked(bundle, withinPackage, true));
}
private static List<Class<?>> collectUnmarked(final Bundle bundle, final Package withinPackage, final boolean excludeJUnit4Tests) {
final List<Class<?>> testClasses = new ArrayList<Class<?>>();
for (final Class<?> testClass : collect(bundle, withinPackage, true, excludeJUnit4Tests)) {
if (testClass.getAnnotations().length == 0) {
testClasses.add(testClass);
}
}
return testClasses;
}
/**
* Collect all unmarked test cases within the given bundle. This method considers JUnit3 and JUnit4 test cases.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @return
*/
public static List<Class<?>> collectUnmarkedJUnit3And4(final Bundle bundle, final Package withinPackage) {
return collectUnmarked(bundle, withinPackage, false);
}
/**
* Collect all badly named test cases within the given bundle (JUnit3 and JUnit4).
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @return
*/
public static List<Class<?>> collectBadlyNamedJUnit3And4(final Bundle bundle, final Package withinPackage) {
return collectBadlyNamed(bundle, withinPackage, false);
}
/**
* Collect all badly named {@code TestCase}e within the given bundle. This method collects JUnit3 tests only.
*
* @param bundle
* bundle to collect all {@code TestCase}s
* @param withinPackage
* if not <code>null</code>, only {@code TestCase}s within this package are collected
* @return
*/
public static List<Class<? extends TestCase>> collectBadlyNamed(final Bundle bundle, final Package withinPackage) {
return convertToTestCaseList(collectBadlyNamed(bundle, withinPackage, true));
}
private static List<Class<?>> collectBadlyNamed(final Bundle bundle, final Package withinPackage, final boolean excludeJUnit4Tests) {
final List<Class<?>> testClasses = new ArrayList<Class<?>>();
final Pattern testClassPattern = Pattern.compile(".*Test\\d*$"); //$NON-NLS-1$
for (final Class<?> testClass : collect(bundle, withinPackage, true, excludeJUnit4Tests)) {
if (!testClassPattern.matcher(testClass.getName()).matches()) {
testClasses.add(testClass);
}
}
return testClasses;
}
/**
* Collect all JUnit3 test cases within the given bundle.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @return a list of {@link TestCase}s
*/
public static List<Class<? extends TestCase>> collect(final Bundle bundle, final Package withinPackage, final boolean subPackages) {
return convertToTestCaseList(collect(bundle, withinPackage, subPackages, true));
}
/**
* @param classes
* @return
*/
@SuppressWarnings("unchecked")
private static List<Class<? extends TestCase>> convertToTestCaseList(final List<Class<?>> classes) {
final List<Class<? extends TestCase>> result = new ArrayList<Class<? extends TestCase>>();
for (final Class<?> c : classes) {
result.add((Class<TestCase>) c);
}
return result;
}
/**
* Collect all JUnit3 and JUnit4 test cases within the given bundle.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @return a list of test classes
*/
public static List<Class<?>> collectJUnit3And4(final Bundle bundle, final Package withinPackage, final boolean subPackages) {
return collect(bundle, withinPackage, subPackages, false);
}
/**
* Collect all JUnit3 and JUnit4 test cases within the given bundle.
*
* @param bundle
* bundle to collect all test cases
* @param withinPackage
* if not <code>null</code>, only test cases within this package are collected
* @param subPackages
* on <code>true</code> also collect sub-packages
* @param excludeJUnit4Tests
* <code>true</code> to scan for <strong>JUnit3 {@link TestCase}s only</strong> (support the legacy behavior)
* @return
*/
private static List<Class<?>> collect(final Bundle bundle, final Package withinPackage, final boolean subPackages, final boolean excludeJUnit4Tests) {
final List<Class<?>> result = new ArrayList<Class<?>>();
final Enumeration<URL> allClasses = bundle.findEntries("", "*.class", true); //$NON-NLS-1$ //$NON-NLS-2$
for (final URL entryURL : Iter.able(allClasses)) {
final String url = entryURL.toExternalForm();
if (url.contains("$")) { //$NON-NLS-1$
// Skip inner classes
continue;
}
final Class<?> clazz = getClass(bundle, entryURL);
if (clazz == null) {
trace("Could not get class from ", url); //$NON-NLS-1$
continue;
}
if (excludeJUnit4Tests && !TestCase.class.isAssignableFrom(clazz)) {
trace("Not a JUnit3 TestCase: ", clazz.getName()); //$NON-NLS-1$
continue;
}
final String className = clazz.getName();
if (withinPackage != null) {
if (subPackages) {
if (!(className.startsWith(withinPackage.getName()) && className.endsWith(clazz.getSimpleName()))) {
continue;
}
} else {
if (!className.equals(withinPackage.getName() + "." + clazz.getSimpleName())) { //$NON-NLS-1$
continue;
}
}
}
if (clazz.isAnnotationPresent(NonGatherableTestCase.class)) {
continue;
}
if (Modifier.isAbstract(clazz.getModifiers())) {
continue;
}
addToListIfApplicable(clazz, result);
}
return result;
}
/**
* Adds the given class to the list if it is either a JUnit3 (extends {@link TestCase}) or a JUnit4 (contains @{@link Test} annotated methods) test case.
*
* @param clazz
* the class to check
* @param testClasses
* the test cases list, to which the class will be added if it is a test case
*/
private static void addToListIfApplicable(final Class<?> clazz, final List<Class<?>> testClasses) {
if (TestCase.class.isAssignableFrom(clazz)) {
// JUnit3
testClasses.add(clazz);
} else if (isJUnit4TestCase(clazz)) {
testClasses.add(clazz);
}
}
/**
* @param clazz
* the class to check
* @return <code>true</code> if the class contains at least one method annotated with @{@link Test}
*/
private static boolean isJUnit4TestCase(final Class<?> clazz) {
for (final Method method : clazz.getMethods()) {
if (method.isAnnotationPresent(Test.class)) {
return true;
}
}
return false;
}
private static Class<?> getClass(final Bundle bundle, final URL entryURL) {
final String entry = entryURL.toExternalForm().replace(".class", "").replace('/', '.'); //$NON-NLS-1$ //$NON-NLS-2$
// Brute force detecting of how many chars we have to skip to find a class within the url
final String name = entry;
int dot = 0;
while ((dot = name.indexOf('.', dot)) != -1) {
final String className = name.substring(dot + 1);
try {
return bundle.loadClass(className);
} catch (final ClassNotFoundException e) {
dot++;
} catch (final NoClassDefFoundError e) {
dot++;
}
}
return null;
}
private static void trace(final Object... objects) {
final StringBuilder bob = new StringBuilder();
for (final Object object : objects) {
bob.append(object);
}
System.err.println(bob.toString());
}
}