/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uphyca.testing.android.suitebuilder;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import android.util.Log;
import com.android.internal.util.Predicate;
import com.uphyca.testing.android.ClassPathPackageInfo;
import com.uphyca.testing.android.ClassPathPackageInfoSource;
import com.uphyca.testing.android.PackageInfoSources;
/**
* Modified version of android.test.suitebuilder.TestGrouping.
*
*
* Represents a collection of test classes present on the classpath. You can add individual classes
* or entire packages. By default sub-packages are included recursively, but methods are
* provided to allow for arbitrary inclusion or exclusion of sub-packages. Typically a
* {@link JUnit4TestGrouping} will have only one root package, but this is not a requirement.
*
* {@hide} Not needed for 1.0 SDK.
*/
public class JUnit4TestGrouping {
private static final String LOG_TAG = "TestGrouping";
public static final Comparator<Class<?>> SORT_BY_FULLY_QUALIFIED_NAME
= new SortByFullyQualifiedName();
private SortedSet<Class<?>> testCaseClasses;
protected String firstIncludedPackage = null;
private ClassLoader classLoader;
public JUnit4TestGrouping(Comparator<Class<?>> comparator) {
testCaseClasses = new TreeSet<Class<?>>(comparator);
}
public SortedSet<Class<?>> getTestCaseClasses() {
return testCaseClasses;
}
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JUnit4TestGrouping other = (JUnit4TestGrouping) o;
if (!this.testCaseClasses.equals(other.testCaseClasses)) {
return false;
}
return this.testCaseClasses.comparator().equals(other.testCaseClasses.comparator());
}
public int hashCode() {
return testCaseClasses.hashCode();
}
/**
* Include all tests in the given packages and all their sub-packages, unless otherwise
* specified. Each of the given packages must contain at least one test class, either directly
* or in a sub-package.
*
* @param packageNames Names of packages to add.
* @return The {@link JUnit4TestGrouping} for method chaining.
*/
public JUnit4TestGrouping addPackagesRecursive(String... packageNames) {
for (String packageName : packageNames) {
List<Class<?>> addedClasses = testCaseClassesInPackage(packageName);
if (addedClasses.isEmpty()) {
Log.w(LOG_TAG, "Invalid Package: '" + packageName
+ "' could not be found or has no tests");
}
testCaseClasses.addAll(addedClasses);
if (firstIncludedPackage == null) {
firstIncludedPackage = packageName;
}
}
return this;
}
/**
* Exclude all tests in the given packages and all their sub-packages, unless otherwise
* specified.
*
* @param packageNames Names of packages to remove.
* @return The {@link JUnit4TestGrouping} for method chaining.
*/
public JUnit4TestGrouping removePackagesRecursive(String... packageNames) {
for (String packageName : packageNames) {
testCaseClasses.removeAll(testCaseClassesInPackage(packageName));
}
return this;
}
/**
* @return The first package name passed to {@link #addPackagesRecursive(String[])}, or null
* if that method was never called.
*/
public String getFirstIncludedPackage() {
return firstIncludedPackage;
}
private List<Class<?>> testCaseClassesInPackage(String packageName) {
ClassPathPackageInfoSource source = PackageInfoSources.forClassPath(classLoader);
ClassPathPackageInfo packageInfo = source.getPackageInfo(packageName);
Set<Class<?>> allClasses = packageInfo.getTopLevelClassesRecursive();
//FIXME Add nested classes to suite;
// Set<Class<?>> allNestedClasses = Sets.newHashSet();
// allNestedClasses.addAll(allClasses);
// addNestedClasses(allClasses, allNestedClasses);
// return selectTestClasses(allNestedClasses);
return selectTestClasses(allClasses);
}
private void addNestedClasses(Set<Class<?>> allClasses, Set<Class<?>> result) {
for (Class<?> c: allClasses) {
addNestedClasses(c, result);
}
}
private void addNestedClasses(Class<?> clazz, Set<Class<?>> result) {
for (Class<?> c: clazz.getClasses()) {
result.add(c);
addNestedClasses(c, result);
}
}
private List<Class<?>> selectTestClasses(Set<Class<?>> allClasses) {
List<Class<?>> testClasses = new ArrayList<Class<?>>();
for (Class<?> testClass : select(allClasses,
new TestCasePredicate())) {
testClasses.add((Class<?>) testClass);
}
return testClasses;
}
private <T> List<T> select(Collection<T> items, Predicate<T> predicate) {
ArrayList<T> selectedItems = new ArrayList<T>();
for (T item : items) {
if (predicate.apply(item)) {
selectedItems.add(item);
}
}
return selectedItems;
}
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Sort classes by their fully qualified names (i.e. with the package
* prefix).
*/
private static class SortByFullyQualifiedName
implements Comparator<Class<?>>, Serializable {
public int compare(Class<?> class1,
Class<?> class2) {
return class1.getName().compareTo(class2.getName());
}
}
private static class TestCasePredicate implements Predicate<Class<?>> {
public boolean apply(Class<?> aClass) {
//For JUnit3
if (TestCase.class.isAssignableFrom((Class<?>) aClass)) {
int modifiers = ((Class<?>) aClass).getModifiers();
return Modifier.isPublic(modifiers)
&& !Modifier.isAbstract(modifiers)
&& hasValidConstructor((Class<?>) aClass);
}
//For JUnit4
int modifiers = ((Class<?>) aClass).getModifiers();
if (!(Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers))) {
return false;
}
if (hasAnnotation(aClass, RunWith.class)) {
return true;
}
if (hasSuiteMethod(aClass)) {
return true;
}
if (hasAnnotatedMethod(aClass)) {
return true;
}
return false;
}
private boolean hasSuiteMethod(Class<?> testClass) {
//FIXME Use metadata.
try {
testClass.getMethod("suite");
} catch (NoSuchMethodException e) {
return false;
}
return true;
}
private static boolean hasAnnotation(AnnotatedElement meta, Class<? extends Annotation> annon) {
return meta.isAnnotationPresent(annon);
}
private static boolean hasAnnotatedMethod(Class<?> aClass) {
try {
//FIXME Scan superclasses's methods.
for (Method method: aClass.getMethods()) {
if (hasAnnotation(method, Test.class)) {
return true;
}
}
} catch (Exception ignore) {
} catch (LinkageError ignore) {
}
return false;
}
@SuppressWarnings("unchecked")
private boolean hasValidConstructor(java.lang.Class<?> aClass) {
// The cast below is not necessary with the Java 5 compiler, but necessary with the Java 6 compiler,
// where the return type of Class.getDeclaredConstructors() was changed
// from Constructor<T>[] to Constructor<?>[]
Constructor<? extends TestCase>[] constructors
= (Constructor<? extends TestCase>[]) aClass.getConstructors();
for (Constructor<? extends TestCase> constructor : constructors) {
if (Modifier.isPublic(constructor.getModifiers())) {
java.lang.Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length == 0 ||
(parameterTypes.length == 1 && parameterTypes[0] == String.class)) {
return true;
}
}
}
Log.i(LOG_TAG, String.format(
"TestCase class %s is missing a public constructor with no parameters " +
"or a single String parameter - skipping",
aClass.getName()));
return false;
}
}
}