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()); }