diff --git a/testng-core-api/src/main/java/org/testng/internal/PackageUtils.java b/testng-core-api/src/main/java/org/testng/internal/PackageUtils.java
index 65ffd2b1c3..6669fb6ab4 100644
--- a/testng-core-api/src/main/java/org/testng/internal/PackageUtils.java
+++ b/testng-core-api/src/main/java/org/testng/internal/PackageUtils.java
@@ -4,19 +4,22 @@
import java.io.File;
import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.JarURLConnection;
import java.net.URL;
-import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.Collection;
-import java.util.Enumeration;
+import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
+import java.util.Spliterator;
+import java.util.Spliterators;
import java.util.concurrent.ConcurrentLinkedDeque;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import java.util.regex.Pattern;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.testng.collections.Lists;
+import org.testng.internal.protocols.Input;
+import org.testng.internal.protocols.Processor;
+import org.testng.internal.protocols.UnhandledIOException;
/**
* Utility class that finds all the classes in a given package.
@@ -26,7 +29,6 @@
* @author Cedric Beust
*/
public class PackageUtils {
- private static final String PACKAGE_UTILS = PackageUtils.class.getSimpleName();
private static String[] testClassPaths;
/** The additional class loaders to find classes in. */
@@ -45,101 +47,36 @@ private PackageUtils() {
*/
public static String[] findClassesInPackage(
String packageName, List included, List excluded) throws IOException {
- String packageOnly = packageName;
- boolean recursive = false;
- if (packageName.endsWith(".*")) {
- packageOnly = packageName.substring(0, packageName.lastIndexOf(".*"));
- recursive = true;
+ String packageNameWithoutWildCards = packageName;
+ boolean recursive = packageName.endsWith(".*");
+ if (recursive) {
+ packageNameWithoutWildCards = packageName.substring(0, packageName.lastIndexOf(".*"));
}
- List vResult = Lists.newArrayList();
- String packageDirName = packageOnly.replace('.', '/') + (packageOnly.length() > 0 ? "/" : "");
+ String packageDirName =
+ packageNameWithoutWildCards.replace('.', '/')
+ + (packageNameWithoutWildCards.length() > 0 ? "/" : "");
+
+ Input input =
+ Input.Builder.newBuilder()
+ .forPackageWithoutWildCards(packageNameWithoutWildCards)
+ .withRecursive(recursive)
+ .include(included)
+ .exclude(excluded)
+ .withPackageName(packageName)
+ .forPackageDirectory(packageDirName)
+ .build();
- List dirs = Lists.newArrayList();
// go through additional class loaders
List allClassLoaders =
ClassHelper.appendContextualClassLoaders(Lists.newArrayList(classLoaders));
- for (ClassLoader classLoader : allClassLoaders) {
- if (null == classLoader) {
- continue;
- }
- Enumeration dirEnumeration = classLoader.getResources(packageDirName);
- while (dirEnumeration.hasMoreElements()) {
- URL dir = dirEnumeration.nextElement();
- dirs.add(dir);
- }
- }
-
- for (URL url : dirs) {
- String protocol = url.getProtocol();
- if (!matchTestClasspath(url, packageDirName, recursive)) {
- continue;
- }
-
- if ("file".equals(protocol)) {
- findClassesInDirPackage(
- packageOnly,
- included,
- excluded,
- URLDecoder.decode(url.getFile(), UTF_8),
- recursive,
- vResult);
- } else if ("jar".equals(protocol)) {
- JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
- Enumeration entries = jar.entries();
- while (entries.hasMoreElements()) {
- JarEntry entry = entries.nextElement();
- String name = entry.getName();
- if (name.startsWith("module-info") || name.startsWith("META-INF")) {
- continue;
- }
- if (name.charAt(0) == '/') {
- name = name.substring(1);
- }
- if (name.startsWith(packageDirName)) {
- int idx = name.lastIndexOf('/');
- if (idx != -1) {
- packageName = name.substring(0, idx).replace('/', '.');
- }
-
- if (recursive || packageName.equals(packageOnly)) {
- // it's not inside a deeper dir
- Utils.log(PACKAGE_UTILS, 4, "Package name is " + packageName);
- if (name.endsWith(".class") && !entry.isDirectory()) {
- String className = name.substring(packageName.length() + 1, name.length() - 6);
- Utils.log(
- PACKAGE_UTILS,
- 4,
- "Found class " + className + ", seeing it if it's included or excluded");
- includeOrExcludeClass(packageName, className, included, excluded, vResult);
- }
- }
- }
- }
- } else if ("bundleresource".equals(protocol)) {
- try {
- Class>[] params = {};
- // BundleURLConnection
- URLConnection connection = url.openConnection();
- Method thisMethod =
- url.openConnection().getClass().getDeclaredMethod("getFileURL", params);
- Object[] paramsObj = {};
- URL fileUrl = (URL) thisMethod.invoke(connection, paramsObj);
- findClassesInDirPackage(
- packageOnly,
- included,
- excluded,
- URLDecoder.decode(fileUrl.getFile(), UTF_8),
- recursive,
- vResult);
- } catch (Exception ex) {
- // ignore - probably not an Eclipse OSGi bundle
- }
- }
- }
-
- return vResult.toArray(new String[0]);
+ return allClassLoaders.stream()
+ .filter(Objects::nonNull)
+ .flatMap(asURLs(packageDirName))
+ .filter(url -> matchTestClasspath(url, packageDirName, recursive))
+ .flatMap(url -> Processor.newInstance(url.getProtocol()).process(input, url).stream())
+ .toArray(String[]::new);
}
private static String[] getTestClasspath() {
@@ -174,14 +111,25 @@ private static String[] getTestClasspath() {
return testClassPaths;
}
+ private static Function> asURLs(String packageDir) {
+ return cl -> {
+ try {
+ Iterator iterator = cl.getResources(packageDir).asIterator();
+ return StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
+ } catch (IOException e) {
+ throw new UnhandledIOException(e);
+ }
+ };
+ }
+
private static boolean matchTestClasspath(URL url, String lastFragment, boolean recursive) {
String[] classpathFragments = getTestClasspath();
if (null == classpathFragments) {
return true;
}
- String fileName = "";
- fileName = URLDecoder.decode(url.getFile(), UTF_8);
+ String fileName = URLDecoder.decode(url.getFile(), UTF_8);
for (String classpathFrag : classpathFragments) {
String path = classpathFrag + lastFragment;
@@ -195,101 +143,6 @@ private static boolean matchTestClasspath(URL url, String lastFragment, boolean
return true;
}
}
-
- return false;
- }
-
- private static void findClassesInDirPackage(
- String packageName,
- List included,
- List excluded,
- String packagePath,
- final boolean recursive,
- List classes) {
- File dir = new File(packagePath);
-
- if (!dir.exists() || !dir.isDirectory()) {
- return;
- }
-
- File[] dirfiles =
- dir.listFiles(
- file ->
- (recursive && file.isDirectory())
- || (file.getName().endsWith(".class"))
- || (file.getName().endsWith(".groovy")));
-
- Utils.log(PACKAGE_UTILS, 4, "Looking for test classes in the directory: " + dir);
- if (dirfiles == null) {
- return;
- }
- for (File file : dirfiles) {
- if (file.isDirectory()) {
- findClassesInDirPackage(
- makeFullClassName(packageName, file.getName()),
- included,
- excluded,
- file.getAbsolutePath(),
- recursive,
- classes);
- } else {
- String className = file.getName().substring(0, file.getName().lastIndexOf('.'));
- Utils.log(
- PACKAGE_UTILS,
- 4,
- "Found class " + className + ", seeing it if it's included or excluded");
- includeOrExcludeClass(packageName, className, included, excluded, classes);
- }
- }
- }
-
- private static String makeFullClassName(String pkg, String cls) {
- return pkg.length() > 0 ? pkg + "." + cls : cls;
- }
-
- private static void includeOrExcludeClass(
- String packageName,
- String className,
- List included,
- List excluded,
- List classes) {
- if (isIncluded(packageName, included, excluded)) {
- Utils.log(PACKAGE_UTILS, 4, "... Including class " + className);
- classes.add(makeFullClassName(packageName, className));
- } else {
- Utils.log(PACKAGE_UTILS, 4, "... Excluding class " + className);
- }
- }
-
- /** @return true if name should be included. */
- private static boolean isIncluded(String name, List included, List excluded) {
- boolean result;
-
- //
- // If no includes nor excludes were specified, return true.
- //
- if (included.isEmpty() && excluded.isEmpty()) {
- result = true;
- } else {
- boolean isIncluded = PackageUtils.find(name, included);
- boolean isExcluded = PackageUtils.find(name, excluded);
- if (isIncluded && !isExcluded) {
- result = true;
- } else if (isExcluded) {
- result = false;
- } else {
- result = included.isEmpty();
- }
- }
- return result;
- }
-
- private static boolean find(String name, List list) {
- for (String regexpStr : list) {
- if (Pattern.matches(regexpStr, name)) {
- return true;
- }
- }
return false;
}
}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/BundledResourceProcessor.java b/testng-core-api/src/main/java/org/testng/internal/protocols/BundledResourceProcessor.java
new file mode 100644
index 0000000000..79d0626b8c
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/BundledResourceProcessor.java
@@ -0,0 +1,43 @@
+package org.testng.internal.protocols;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLDecoder;
+import java.util.List;
+import org.testng.collections.Lists;
+
+class BundledResourceProcessor extends Processor {
+ @Override
+ public List process(Input input, URL url) {
+ return processBundledResources(
+ url,
+ input.getIncluded(),
+ input.getExcluded(),
+ input.getPackageWithoutWildCards(),
+ input.isRecursive());
+ }
+
+ private static List processBundledResources(
+ URL url,
+ List included,
+ List excluded,
+ String packageOnly,
+ boolean recursive) {
+ try {
+ Class>[] params = {};
+ // BundleURLConnection
+ URLConnection connection = url.openConnection();
+ Method thisMethod = url.openConnection().getClass().getDeclaredMethod("getFileURL", params);
+ Object[] paramsObj = {};
+ URL fileUrl = (URL) thisMethod.invoke(connection, paramsObj);
+ return findClassesInDirPackage(
+ packageOnly, included, excluded, URLDecoder.decode(fileUrl.getFile(), UTF_8), recursive);
+ } catch (Exception ex) {
+ // ignore - probably not an Eclipse OSGi bundle
+ }
+ return Lists.newArrayList();
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/FileProcessor.java b/testng-core-api/src/main/java/org/testng/internal/protocols/FileProcessor.java
new file mode 100644
index 0000000000..12b93a0e4f
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/FileProcessor.java
@@ -0,0 +1,20 @@
+package org.testng.internal.protocols;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.List;
+
+class FileProcessor extends Processor {
+
+ @Override
+ public List process(Input input, URL url) {
+ return findClassesInDirPackage(
+ input.getPackageWithoutWildCards(),
+ input.getIncluded(),
+ input.getExcluded(),
+ URLDecoder.decode(url.getFile(), UTF_8),
+ input.isRecursive());
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/Input.java b/testng-core-api/src/main/java/org/testng/internal/protocols/Input.java
new file mode 100644
index 0000000000..18fd4c23b0
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/Input.java
@@ -0,0 +1,96 @@
+package org.testng.internal.protocols;
+
+import java.util.Collections;
+import java.util.List;
+
+public class Input {
+
+ private final List included;
+ private final List excluded;
+ private final String packageWithoutWildCards;
+ private final boolean recursive;
+ private final String packageDirName;
+ private final String packageName;
+
+ private Input(Builder builder) {
+ included = Collections.unmodifiableList(builder.included);
+ excluded = Collections.unmodifiableList(builder.excluded);
+ packageWithoutWildCards = builder.packageWithoutWildCards;
+ recursive = builder.recursive;
+ packageName = builder.packageName;
+ packageDirName = builder.packageDirName;
+ }
+
+ public List getIncluded() {
+ return included;
+ }
+
+ public List getExcluded() {
+ return excluded;
+ }
+
+ public String getPackageWithoutWildCards() {
+ return packageWithoutWildCards;
+ }
+
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ public String getPackageDirName() {
+ return packageDirName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public static final class Builder {
+ private List included;
+ private List excluded;
+ private String packageWithoutWildCards;
+ private boolean recursive;
+ private String packageDirName;
+ private String packageName;
+
+ private Builder() {}
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public Builder include(List val) {
+ included = val;
+ return this;
+ }
+
+ public Builder exclude(List val) {
+ excluded = val;
+ return this;
+ }
+
+ public Builder forPackageWithoutWildCards(String val) {
+ packageWithoutWildCards = val;
+ return this;
+ }
+
+ public Builder withRecursive(boolean val) {
+ recursive = val;
+ return this;
+ }
+
+ public Builder forPackageDirectory(String val) {
+ packageDirName = val;
+ return this;
+ }
+
+ public Builder withPackageName(String val) {
+ packageName = val;
+ return this;
+ }
+
+ public Input build() {
+ return new Input(this);
+ }
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/JarProcessor.java b/testng-core-api/src/main/java/org/testng/internal/protocols/JarProcessor.java
new file mode 100644
index 0000000000..58f66eb765
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/JarProcessor.java
@@ -0,0 +1,75 @@
+package org.testng.internal.protocols;
+
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import org.testng.collections.Lists;
+import org.testng.internal.Utils;
+
+class JarProcessor extends Processor {
+ @Override
+ public List process(Input input, URL url) {
+ try {
+ return processJar(
+ url,
+ input.getIncluded(),
+ input.getExcluded(),
+ input.getPackageWithoutWildCards(),
+ input.isRecursive(),
+ input.getPackageDirName(),
+ input.getPackageName());
+ } catch (IOException e) {
+ throw new UnhandledIOException(e);
+ }
+ }
+
+ private static List processJar(
+ URL url,
+ List included,
+ List excluded,
+ String packageOnly,
+ boolean recursive,
+ String packageDirName,
+ String packageName)
+ throws IOException {
+ List vResult = Lists.newArrayList();
+ JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
+ Enumeration entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (name.startsWith("module-info") || name.startsWith("META-INF")) {
+ continue;
+ }
+ if (name.charAt(0) == '/') {
+ name = name.substring(1);
+ }
+ if (name.startsWith(packageDirName)) {
+ int idx = name.lastIndexOf('/');
+ if (idx != -1) {
+ packageName = name.substring(0, idx).replace('/', '.');
+ }
+
+ if (recursive || packageName.equals(packageOnly)) {
+ // it's not inside a deeper dir
+ Utils.log(CLS_NAME, 4, "Package name is " + packageName);
+ if (name.endsWith(".class") && !entry.isDirectory()) {
+ String className = name.substring(packageName.length() + 1, name.length() - 6);
+ Utils.log(
+ CLS_NAME,
+ 4,
+ "Found class " + className + ", seeing it if it's included or excluded");
+ List processedList =
+ includeOrExcludeClass(packageName, className, included, excluded);
+ vResult.addAll(processedList);
+ }
+ }
+ }
+ }
+ return vResult;
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/NoOpProcessor.java b/testng-core-api/src/main/java/org/testng/internal/protocols/NoOpProcessor.java
new file mode 100644
index 0000000000..8a9fb59972
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/NoOpProcessor.java
@@ -0,0 +1,12 @@
+package org.testng.internal.protocols;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+class NoOpProcessor extends Processor {
+ @Override
+ public List process(Input input, URL url) {
+ return Collections.emptyList();
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/Processor.java b/testng-core-api/src/main/java/org/testng/internal/protocols/Processor.java
new file mode 100644
index 0000000000..8c67a84d79
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/Processor.java
@@ -0,0 +1,120 @@
+package org.testng.internal.protocols;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.testng.collections.Lists;
+import org.testng.internal.Utils;
+
+public abstract class Processor {
+
+ protected static final String CLS_NAME = Processor.class.getSimpleName();
+
+ public static Processor newInstance(String protocol) {
+ Processor instance;
+ switch (protocol.toLowerCase()) {
+ case "file":
+ instance = new FileProcessor();
+ break;
+ case "jar":
+ instance = new JarProcessor();
+ break;
+ case "bundleresource":
+ instance = new BundledResourceProcessor();
+ break;
+ default:
+ instance = new NoOpProcessor();
+ }
+ return instance;
+ }
+
+ public abstract List process(Input input, URL url);
+
+ protected static List findClassesInDirPackage(
+ String packageName,
+ List included,
+ List excluded,
+ String packagePath,
+ final boolean recursive) {
+ File dir = new File(packagePath);
+
+ if (!dir.exists() || !dir.isDirectory()) {
+ return Lists.newArrayList();
+ }
+
+ File[] dirfiles =
+ dir.listFiles(
+ file ->
+ (recursive && file.isDirectory())
+ || (file.getName().endsWith(".class"))
+ || (file.getName().endsWith(".groovy")));
+
+ Utils.log(CLS_NAME, 4, "Looking for test classes in the directory: " + dir);
+ if (dirfiles == null) {
+ return Lists.newArrayList();
+ }
+ List classes = Lists.newArrayList();
+ for (File file : dirfiles) {
+ if (file.isDirectory()) {
+ List foundClasses =
+ findClassesInDirPackage(
+ makeFullClassName(packageName, file.getName()),
+ included,
+ excluded,
+ file.getAbsolutePath(),
+ recursive);
+ classes.addAll(foundClasses);
+ } else {
+ String className = file.getName().substring(0, file.getName().lastIndexOf('.'));
+ Utils.log(
+ CLS_NAME, 4, "Found class " + className + ", seeing it if it's included or excluded");
+ List processedList =
+ includeOrExcludeClass(packageName, className, included, excluded);
+ classes.addAll(processedList);
+ }
+ }
+ return classes;
+ }
+
+ private static String makeFullClassName(String pkg, String cls) {
+ return pkg.length() > 0 ? pkg + "." + cls : cls;
+ }
+
+ protected static List includeOrExcludeClass(
+ String packageName, String className, List included, List excluded) {
+ List classes = Lists.newArrayList();
+ if (isIncluded(packageName, included, excluded)) {
+ Utils.log(CLS_NAME, 4, "... Including class " + className);
+ classes.add(makeFullClassName(packageName, className));
+ } else {
+ Utils.log(CLS_NAME, 4, "... Excluding class " + className);
+ }
+ return classes;
+ }
+
+ /** @return true if name should be included. */
+ private static boolean isIncluded(String name, List included, List excluded) {
+ //
+ // If no includes nor excludes were specified, return true.
+ //
+ if (included.isEmpty() && excluded.isEmpty()) {
+ return true;
+ }
+ boolean isIncluded = find(name, included);
+ boolean isExcluded = find(name, excluded);
+ boolean result;
+ if (isIncluded && !isExcluded) {
+ result = true;
+ } else if (isExcluded) {
+ result = false;
+ } else {
+ result = included.isEmpty();
+ }
+ return result;
+ }
+
+ private static boolean find(String name, List list) {
+ return list.stream().parallel().anyMatch(each -> Pattern.matches(each, name));
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/internal/protocols/UnhandledIOException.java b/testng-core-api/src/main/java/org/testng/internal/protocols/UnhandledIOException.java
new file mode 100644
index 0000000000..2a21ff00a9
--- /dev/null
+++ b/testng-core-api/src/main/java/org/testng/internal/protocols/UnhandledIOException.java
@@ -0,0 +1,8 @@
+package org.testng.internal.protocols;
+
+public class UnhandledIOException extends RuntimeException {
+
+ public UnhandledIOException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/testng-core-api/src/main/java/org/testng/xml/XmlPackage.java b/testng-core-api/src/main/java/org/testng/xml/XmlPackage.java
index e095b91033..db5a5c2e8b 100644
--- a/testng-core-api/src/main/java/org/testng/xml/XmlPackage.java
+++ b/testng-core-api/src/main/java/org/testng/xml/XmlPackage.java
@@ -6,6 +6,7 @@
import org.testng.collections.Lists;
import org.testng.internal.PackageUtils;
import org.testng.internal.Utils;
+import org.testng.internal.protocols.UnhandledIOException;
import org.testng.reporters.XMLStringBuffer;
/** This class describes the tag <package>
in testng.xml. */
@@ -70,7 +71,7 @@ private List initializeXmlClasses() {
for (String className : classes) {
result.add(new XmlClass(className, index++, false /* don't load classes */));
}
- } catch (IOException ioex) {
+ } catch (IOException | UnhandledIOException ioex) {
Utils.log("XmlPackage", 1, ioex.getMessage());
}