Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature to handle duplicated test classes #22

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ Negation expressions are preceded by a "!". In the previous example all tests th

You can have as many positive and negative filters as you like. The positve filters still work disjunctively whereas the negated filters will subtract all matching tests after the maximum set of tests to run has been determined. Having only negated filters starts with the full set of tests.

#### Duplicated Classes

In some occasional cases, there may be multiple same / duplicated test classes in your classpath, which will cause some unexpected and vulnerable behavior to your test suites (e.g. using [build-helper-maven-plugin](https://www.mojohaus.org/build-helper-maven-plugin/add-test-source-mojo.html) to compile).

You can use the following annotation to prune duplicated cases in your suite: only one of the same classes is kept (the first one being scanned with the same full classname):

```java
import org.junit.extensions.cpsuite.ClasspathSuite.*;
...
@ExcludeDuplicated(true)
public class MySuite...
```

#### Abstract Test Classes

[ClasspathSuite](http://johanneslink.net/projects/cpsuite.jsp) solves another problem (bug?) in the JUnit 4 integration of Eclipse 3.2: test classes derived from an abstract test class which do not have test methods of their own are being ignored by Eclipse's test runner. When using `RunWith(ClasspathSuite.class)` you will catch those test classes as well.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

public interface ClassesFinderFactory {
ClassesFinder create(boolean searchInJars, String[] filterPatterns, String[] classpathFilterPatterns, SuiteType[] suiteTypes,
Class<?>[] baseTypes, Class<?>[] excludedBaseTypes, String classpathProperty);
Class<?>[] baseTypes, Class<?>[] excludedBaseTypes, String classpathProperty, boolean excludeDuplicated);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Utility class to find classes within the class path, both inside and outside
Expand All @@ -28,9 +30,12 @@ public class ClasspathClassesFinder implements ClassesFinder

private final String classpathProperty;

public ClasspathClassesFinder(ClassTester tester, String classpathProperty) {
private final boolean excludeDuplicated;

public ClasspathClassesFinder(ClassTester tester, String classpathProperty, boolean excludeDuplicated) {
this.tester = tester;
this.classpathProperty = classpathProperty;
this.excludeDuplicated = excludeDuplicated;
}

public List<Class<?>> find() {
Expand All @@ -50,13 +55,15 @@ private List<Class<?>> findClassesInClasspath(String classPath) {

private List<Class<?>> findClassesInRoots(List<String> roots) {
List<Class<?>> classes = new ArrayList<Class<?>>(100);
Set<String> classNames = null; // by default no hashmap for efficiency
if (excludeDuplicated) classNames = new HashSet<String>(100);
for (String root : roots) {
gatherClassesInRoot(new File(root), classes);
gatherClassesInRoot(new File(root), classes, classNames);
}
return classes;
}

private void gatherClassesInRoot(File classRoot, List<Class<?>> classes) {
private void gatherClassesInRoot(File classRoot, List<Class<?>> classes, Set<String> classNames) {
Iterable<String> relativeFilenames = new NullIterator<String>();
if (tester.acceptClassRoot(classRoot.getAbsolutePath())) {
if (tester.searchInJars() && isJarFile(classRoot)) {
Expand All @@ -71,14 +78,14 @@ else if (classRoot.isDirectory()) {
relativeFilenames = new RecursiveFilenameIterator(classRoot);
}
}
gatherClasses(classes, relativeFilenames);
gatherClasses(classes, relativeFilenames, classNames);
}

private boolean isJarFile(File classRoot) {
return classRoot.getName().endsWith(".jar") || classRoot.getName().endsWith(".JAR");
}

private void gatherClasses(List<Class<?>> classes, Iterable<String> filenamesIterator) {
private void gatherClasses(List<Class<?>> classes, Iterable<String> filenamesIterator, Set<String> classNames) {
for (String fileName : filenamesIterator) {
if (!isClassFile(fileName)) {
continue;
Expand All @@ -92,11 +99,12 @@ private void gatherClasses(List<Class<?>> classes, Iterable<String> filenamesIte
}
try {
Class<?> clazz = Class.forName(className, false, getClass().getClassLoader());
if (clazz == null || clazz.isLocalClass() || clazz.isAnonymousClass()) {
if (clazz == null || clazz.isLocalClass() || clazz.isAnonymousClass() || hasDuplicatedClassName(classNames, clazz)) {
continue;
}
if (tester.acceptClass(clazz)) {
classes.add(clazz);
addClassName(classNames, clazz.getName());
}
} catch (ClassNotFoundException cnfe) {
// ignore not instantiable classes
Expand All @@ -110,6 +118,16 @@ private void gatherClasses(List<Class<?>> classes, Iterable<String> filenamesIte
}
}

private void addClassName(Set<String> classNames, String className) {
if (classNames != null) { // only check if hashmap initialized
classNames.add(className);
}
}

private boolean hasDuplicatedClassName(Set<String> classNames, Class<?> clazz) {
return classNames != null && classNames.contains(clazz.getName());
}

private boolean isInnerClass(String className) {
return className.contains("$");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
public class ClasspathFinderFactory implements ClassesFinderFactory
{
public ClassesFinder create(boolean searchInJars, String[] filterPatterns, String[] classpathFilterPatterns, SuiteType[] suiteTypes, Class<?>[] baseTypes,
Class<?>[] excludedBaseTypes, String classpathProperty) {
Class<?>[] excludedBaseTypes, String classpathProperty, boolean excludeDuplicated) {
ClassTester tester = new ClasspathSuiteTester(searchInJars, filterPatterns, classpathFilterPatterns, suiteTypes, baseTypes, excludedBaseTypes);
return new ClasspathClassesFinder(tester, classpathProperty);
return new ClasspathClassesFinder(tester, classpathProperty, excludeDuplicated);
}

}
25 changes: 24 additions & 1 deletion src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
public class ClasspathSuite extends Suite {

private static final boolean DEFAULT_INCLUDE_JARS = false;
private static final boolean DEFAULT_EXCLUDE_DUP = false;
private static final SuiteType[] DEFAULT_SUITE_TYPES = new SuiteType[] { SuiteType.TEST_CLASSES };
private static final Class<?>[] DEFAULT_BASE_TYPES = new Class<?>[] { Object.class };
private static final Class<?>[] DEFAULT_EXCLUDED_BASES_TYPES = new Class<?>[0];
Expand Down Expand Up @@ -68,6 +69,19 @@ public class ClasspathSuite extends Suite {
public boolean value();
}

/**
* The <code>ExcludeDuplicated</code> annotation specifies if duplicated
* classes should be excluded or not (true means only one class is kept,
* other ones with duplicated class names are pruned from results).
* If the annotation is missing or set to false, duplicated classes are
* not being checked.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ExcludeDuplicated {
boolean value();
}

/**
* The <code>SuiteTypes</code> annotation specifies which types of tests
* will be included in the test run. You can choose one or more from
Expand Down Expand Up @@ -141,7 +155,8 @@ public ClasspathSuite(Class<?> suiteClass, RunnerBuilder builder, ClassesFinderF

private static ClassesFinder createFinder(Class<?> suiteClass, ClassesFinderFactory finderFactory) {
return finderFactory.create(getSearchInJars(suiteClass), getClassnameFilters(suiteClass), getClasspathFilters(suiteClass),
getSuiteTypes(suiteClass), getBaseTypes(suiteClass), getExcludedBaseTypes(suiteClass), getClasspathProperty(suiteClass));
getSuiteTypes(suiteClass), getBaseTypes(suiteClass), getExcludedBaseTypes(suiteClass), getClasspathProperty(suiteClass),
getExcludeDuplicated(suiteClass));
}

private static Class<?>[] getSortedTestclasses(ClassesFinder finder) {
Expand Down Expand Up @@ -182,6 +197,14 @@ private static boolean getSearchInJars(Class<?> suiteClass) {
return includeJarsAnnotation.value();
}

private static boolean getExcludeDuplicated(Class<?> suiteClass) {
ExcludeDuplicated excludeDuplicatedAnnotation = suiteClass.getAnnotation(ExcludeDuplicated.class);
if (excludeDuplicatedAnnotation == null) {
return DEFAULT_EXCLUDE_DUP;
}
return excludeDuplicatedAnnotation.value();
}

private static SuiteType[] getSuiteTypes(Class<?> suiteClass) {
SuiteTypes suiteTypesAnnotation = suiteClass.getAnnotation(SuiteTypes.class);
if (suiteTypesAnnotation == null) {
Expand Down