From 3296023323eafb2404782f2c328800bd95af0e18 Mon Sep 17 00:00:00 2001 From: yanhe1 Date: Tue, 11 Jan 2022 12:11:57 +0800 Subject: [PATCH] Add support to prune duplicated class from classpath --- README.md | 13 +++++++++ .../cpsuite/ClassesFinderFactory.java | 2 +- .../cpsuite/ClasspathClassesFinder.java | 28 +++++++++++++++---- .../cpsuite/ClasspathFinderFactory.java | 4 +-- .../extensions/cpsuite/ClasspathSuite.java | 24 +++++++++++++++- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0655300..4ac308d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/main/java/org/junit/extensions/cpsuite/ClassesFinderFactory.java b/src/main/java/org/junit/extensions/cpsuite/ClassesFinderFactory.java index cef29b8..523cb97 100644 --- a/src/main/java/org/junit/extensions/cpsuite/ClassesFinderFactory.java +++ b/src/main/java/org/junit/extensions/cpsuite/ClassesFinderFactory.java @@ -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); } diff --git a/src/main/java/org/junit/extensions/cpsuite/ClasspathClassesFinder.java b/src/main/java/org/junit/extensions/cpsuite/ClasspathClassesFinder.java index fe15cb1..134819d 100644 --- a/src/main/java/org/junit/extensions/cpsuite/ClasspathClassesFinder.java +++ b/src/main/java/org/junit/extensions/cpsuite/ClasspathClassesFinder.java @@ -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> find() { @@ -46,13 +49,15 @@ private List> findClassesInClasspath(String classPath) { private List> findClassesInRoots(List roots) { List> classes = new ArrayList>(100); + Set classNames = null; // by default no hashmap for efficiency + if (excludeDuplicated) classNames = new HashSet(100); for (String root : roots) { - gatherClassesInRoot(new File(root), classes); + gatherClassesInRoot(new File(root), classes, classNames); } return classes; } - private void gatherClassesInRoot(File classRoot, List> classes) { + private void gatherClassesInRoot(File classRoot, List> classes, Set classNames) { Iterable relativeFilenames = new NullIterator(); if (tester.searchInJars() && isJarFile(classRoot)) { try { @@ -64,14 +69,14 @@ private void gatherClassesInRoot(File classRoot, List> 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> classes, Iterable filenamesIterator) { + private void gatherClasses(List> classes, Iterable filenamesIterator, Set classNames) { for (String fileName : filenamesIterator) { if (!isClassFile(fileName)) { continue; @@ -85,11 +90,12 @@ private void gatherClasses(List> classes, Iterable 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 @@ -103,6 +109,16 @@ private void gatherClasses(List> classes, Iterable filenamesIte } } + private void addClassName(Set classNames, String className) { + if (classNames != null) { // only check if hashmap initialized + classNames.add(className); + } + } + + private boolean hasDuplicatedClassName(Set classNames, Class clazz) { + return classNames != null && classNames.contains(clazz.getName()); + } + private boolean isInnerClass(String className) { return className.contains("$"); } diff --git a/src/main/java/org/junit/extensions/cpsuite/ClasspathFinderFactory.java b/src/main/java/org/junit/extensions/cpsuite/ClasspathFinderFactory.java index 2db1c77..7a42ace 100644 --- a/src/main/java/org/junit/extensions/cpsuite/ClasspathFinderFactory.java +++ b/src/main/java/org/junit/extensions/cpsuite/ClasspathFinderFactory.java @@ -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); } } diff --git a/src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java b/src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java index 4431933..cbb2110 100644 --- a/src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java +++ b/src/main/java/org/junit/extensions/cpsuite/ClasspathSuite.java @@ -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]; @@ -47,6 +48,19 @@ public class ClasspathSuite extends Suite { public boolean value(); } + /** + * The ExcludeDuplicated 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 SuiteTypes annotation specifies which types of tests * will be included in the test run. You can choose one or more from @@ -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) { @@ -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) {