Skip to content

Commit

Permalink
Add support to prune duplicated class from classpath
Browse files Browse the repository at this point in the history
  • Loading branch information
yanhe1 committed Jan 11, 2022
1 parent ded674e commit 3296023
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 10 deletions.
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, SuiteType[] suiteTypes, Class<?>[] baseTypes,
Class<?>[] excludedBaseTypes, String classpathProperty);
Class<?>[] excludedBaseTypes, String classpathProperty, boolean excludeDuplicated);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,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 @@ -46,13 +49,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.searchInJars() && isJarFile(classRoot)) {
try {
Expand All @@ -64,14 +69,14 @@ private void gatherClassesInRoot(File classRoot, List<Class<?>> classes) {
} 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 @@ -85,11 +90,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 @@ -103,6 +109,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, SuiteType[] suiteTypes, Class<?>[] baseTypes,
Class<?>[] excludedBaseTypes, String classpathProperty) {
Class<?>[] excludedBaseTypes, String classpathProperty, boolean excludeDuplicated) {
ClassTester tester = new ClasspathSuiteTester(searchInJars, filterPatterns, suiteTypes, baseTypes, excludedBaseTypes);
return new ClasspathClassesFinder(tester, classpathProperty);
return new ClasspathClassesFinder(tester, classpathProperty, excludeDuplicated);
}

}
24 changes: 23 additions & 1 deletion src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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 @@ -47,6 +48,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 @@ -120,7 +134,7 @@ public ClasspathSuite(Class<?> suiteClass, RunnerBuilder builder, ClassesFinderF

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

private static Class<?>[] getSortedTestclasses(ClassesFinder finder) {
Expand Down Expand Up @@ -153,6 +167,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

0 comments on commit 3296023

Please sign in to comment.