Skip to content

Part I - Introduce ModuleUtils with ClassFinder SPI #1061

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

Closed
wants to merge 2 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
package org.junit.jupiter.engine.discovery;

import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.platform.commons.util.ModuleUtils.findAllClassesInModule;
import static org.junit.platform.commons.util.ModuleUtils.findAllClassesOnModulePath;
import static org.junit.platform.commons.util.ReflectionUtils.findAllClassesInClasspathRoot;
import static org.junit.platform.commons.util.ReflectionUtils.findAllClassesInPackage;
import static org.junit.platform.engine.support.filter.ClasspathScanningSupport.buildClassNamePredicate;
Expand All @@ -21,11 +23,15 @@

import org.apiguardian.api.API;
import org.junit.jupiter.engine.discovery.predicates.IsScannableTestClass;
import org.junit.platform.commons.util.ClassFilter;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.discovery.ClassSelector;
import org.junit.platform.engine.discovery.ClasspathRootSelector;
import org.junit.platform.engine.discovery.MethodSelector;
import org.junit.platform.engine.discovery.ModuleFinderSelector;
import org.junit.platform.engine.discovery.ModulePathSelector;
import org.junit.platform.engine.discovery.ModuleSelector;
import org.junit.platform.engine.discovery.PackageSelector;
import org.junit.platform.engine.discovery.UniqueIdSelector;

Expand All @@ -46,11 +52,22 @@ public class DiscoverySelectorResolver {
public void resolveSelectors(EngineDiscoveryRequest request, TestDescriptor engineDescriptor) {
JavaElementsResolver javaElementsResolver = createJavaElementsResolver(engineDescriptor);
Predicate<String> classNamePredicate = buildClassNamePredicate(request);
ClassFilter classFilter = ClassFilter.of(isScannableTestClass, classNamePredicate);

request.getSelectorsByType(ClasspathRootSelector.class).forEach(selector -> {
findAllClassesInClasspathRoot(selector.getClasspathRoot(), isScannableTestClass,
classNamePredicate).forEach(javaElementsResolver::resolveClass);
});
request.getSelectorsByType(ModuleSelector.class).forEach(selector -> {
findAllClassesInModule(classFilter, selector.getModuleName()).forEach(javaElementsResolver::resolveClass);
});
request.getSelectorsByType(ModulePathSelector.class).forEach(selector -> {
findAllClassesOnModulePath(classFilter).forEach(javaElementsResolver::resolveClass);
});
request.getSelectorsByType(ModuleFinderSelector.class).forEach(selector -> {
findAllClassesOnModulePath(classFilter, selector.getClassLoader(), selector.getPaths()) //
.forEach(javaElementsResolver::resolveClass);
});
request.getSelectorsByType(PackageSelector.class).forEach(selector -> {
findAllClassesInPackage(selector.getPackageName(), isScannableTestClass, classNamePredicate).forEach(
javaElementsResolver::resolveClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static org.apiguardian.api.API.Status.INTERNAL;

import java.util.function.Predicate;

import org.apiguardian.api.API;

/**
* Scan-able class predicate holder used by reflection utilities.
*
* <h3>DISCLAIMER</h3>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 1.1
*/
@API(status = INTERNAL, since = "1.1")
public class ClassFilter {

public static ClassFilter of(Predicate<Class<?>> classPredicate, Predicate<String> namePredicate) {
Preconditions.notNull(classPredicate, "class predicate must not be null");
Preconditions.notNull(namePredicate, "name predicate must not be null");

return new ClassFilter(classPredicate, namePredicate);
}

private final Predicate<Class<?>> classPredicate;
private final Predicate<String> namePredicate;

private ClassFilter(Predicate<Class<?>> classPredicate, Predicate<String> namePredicate) {
this.classPredicate = classPredicate;
this.namePredicate = namePredicate;
}

public boolean test(Class<?> type) {
return classPredicate.test(type);
}

public boolean test(String name) {
return namePredicate.test(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static org.apiguardian.api.API.Status.INTERNAL;

import java.nio.file.Path;
import java.util.List;

import org.apiguardian.api.API;

/**
* Class finder service providing interface.
*
* <h3>DISCLAIMER</h3>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 1.1
*/
@API(status = INTERNAL, since = "1.1")
public interface ModuleClassFinder {

/**
* Return list of classes found in the named module that may contain testable methods.
*
* @param filter filter to apply to each class candidate
* @param moduleName name of the module to inspect
* @return list of test classes
*/
List<Class<?>> findAllClassesInModule(ClassFilter filter, String moduleName);

/**
* Return list of classes found on the boot module-path that may contain testable methods.
*
* @param filter filter to apply to each class candidate
* @return list of test classes
*/
List<Class<?>> findAllClassesOnModulePath(ClassFilter filter);

/**
* Return list of classes found at the given path roots that may contain testable methods.
*
* @param filter filter to apply to each class candidate
* @param parent class loader instance to used the parent class loader
* @param entries a possibly-empty array of paths to directories of modules or paths to packaged or exploded modules
* @return list of test classes
* @see <a href="http://download.java.net/java/jdk9/docs/api/java/lang/module/ModuleFinder.html">ModuleFinder</a>
*/
List<Class<?>> findAllClassesOnModulePath(ClassFilter filter, ClassLoader parent, Path... entries);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static org.apiguardian.api.API.Status.INTERNAL;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.function.Function;

import org.apiguardian.api.API;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;

/**
* Collection of utilities for working with modules.
*
* <h3>DISCLAIMER</h3>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 1.1
*/
@API(status = INTERNAL, since = "1.1")
public final class ModuleUtils {

private static final Logger logger = LoggerFactory.getLogger(ModuleUtils.class);

///CLOVER:OFF
private ModuleUtils() {
/* no-op */
}
///CLOVER:ON

/**
* Find all classes in the specified module.
*
* @param filter filter to apply to each candidate
* @param moduleName name of the module to inspect
* @return list of classes matching the passed-in criteria
*/
public static List<Class<?>> findAllClassesInModule(ClassFilter filter, String moduleName) {
Preconditions.notNull(filter, "class filter must not be null");
Preconditions.notBlank(moduleName, "module name must not be null or blank");

return find(finder -> finder.findAllClassesInModule(filter, moduleName));
}

/**
* Find all classes in all modules that are on the boot module-path.
*
* @param filter filter to apply to each candidate
* @return list of classes matching the passed-in criteria
*/
public static List<Class<?>> findAllClassesOnModulePath(ClassFilter filter) {
Preconditions.notNull(filter, "class filter must not be null");

return find(finder -> finder.findAllClassesOnModulePath(filter));
}

/**
* Find all classes in all modules that are locatable on the given path entries.
*
* @param filter filter to apply to each candidate
* @param parent class loader parent
* @return list of classes matching the passed-in criteria
*/
public static List<Class<?>> findAllClassesOnModulePath(ClassFilter filter, ClassLoader parent, Path... entries) {
Preconditions.notNull(filter, "class filter must not be null");
Preconditions.notNull(filter, "class loader must not be null");

return find(finder -> finder.findAllClassesOnModulePath(filter, parent, entries));
}

private static List<Class<?>> find(Function<ModuleClassFinder, List<Class<?>>> function) {
ClassLoader classLoader = ClassLoaderUtils.getDefaultClassLoader();
List<Class<?>> classes = new ArrayList<>();

logger.config(() -> "Loading auto-detected class finders...");
int serviceCounter = 0;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could limit the single service implementation to be our own ... by comparing the class name or some other "magic ID". Or is it okay to allow third-party implementations here? Thoughts?

for (ModuleClassFinder classFinder : ServiceLoader.load(ModuleClassFinder.class, classLoader)) {
classes.addAll(function.apply(classFinder));
serviceCounter++;
}
if (serviceCounter == 0) {
logger.warn(() -> "No module class finder service registered! No test classes found.");
}
return Collections.unmodifiableList(classes);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class AvailableOptions {
private final OptionSpec<URI> selectedUris;
private final OptionSpec<String> selectedFiles;
private final OptionSpec<String> selectedDirectories;
private final OptionSpec<Void> scanModulePath;
private final OptionSpec<String> selectedModules;
private final OptionSpec<String> selectedPackages;
private final OptionSpec<String> selectedClasses;
private final OptionSpec<String> selectedMethods;
Expand Down Expand Up @@ -150,6 +152,16 @@ class AvailableOptions {
"Select a classpath resource for test discovery. This option can be repeated.") //
.withRequiredArg();

// --- Java Platform Module System -------------------------------------

scanModulePath = parser.acceptsAll(asList("scan-modulepath", "scan-module-path"), //
"EXPERIMENTAL: Scan all modules on the module-path for test discovery.");

selectedModules = parser.acceptsAll(asList("o", "select-module"), //
"EXPERIMENTAL: Select single module for test discovery. This option can be repeated.") //
.withRequiredArg() //
.describedAs("module name");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add support in the Vintage engine?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh? Vintage should support both new selectors. If it doesn't then something was lost in rebasing to master.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, missed that! LGTM!

// --- Filters ---------------------------------------------------------

includeClassNamePattern = parser.acceptsAll(asList("n", "include-classname"),
Expand Down Expand Up @@ -217,6 +229,8 @@ CommandLineOptions toCommandLineOptions(OptionSet detectedOptions) {
result.setSelectedUris(detectedOptions.valuesOf(this.selectedUris));
result.setSelectedFiles(detectedOptions.valuesOf(this.selectedFiles));
result.setSelectedDirectories(detectedOptions.valuesOf(this.selectedDirectories));
result.setScanModulepath(detectedOptions.has(this.scanModulePath));
result.setSelectedModules(detectedOptions.valuesOf(this.selectedModules));
result.setSelectedPackages(detectedOptions.valuesOf(this.selectedPackages));
result.setSelectedClasses(detectedOptions.valuesOf(this.selectedClasses));
result.setSelectedMethods(detectedOptions.valuesOf(this.selectedMethods));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ public class CommandLineOptions {
private boolean scanClasspath;
private List<Path> selectedClasspathEntries = emptyList();

private boolean scanModulepath;

private List<URI> selectedUris = emptyList();
private List<String> selectedFiles = emptyList();
private List<String> selectedDirectories = emptyList();
private List<String> selectedModules = emptyList();
private List<String> selectedPackages = emptyList();
private List<String> selectedClasses = emptyList();
private List<String> selectedMethods = emptyList();
Expand Down Expand Up @@ -92,6 +95,14 @@ public void setScanClasspath(boolean scanClasspath) {
this.scanClasspath = scanClasspath;
}

public boolean isScanModulepath() {
return scanModulepath;
}

public void setScanModulepath(boolean scanModulepath) {
this.scanModulepath = scanModulepath;
}

public Details getDetails() {
return details;
}
Expand Down Expand Up @@ -132,6 +143,14 @@ public void setSelectedDirectories(List<String> selectedDirectories) {
this.selectedDirectories = selectedDirectories;
}

public List<String> getSelectedModules() {
return selectedModules;
}

public void setSelectedModules(List<String> selectedModules) {
this.selectedModules = selectedModules;
}

public List<String> getSelectedPackages() {
return selectedPackages;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -55,6 +56,9 @@ private List<? extends DiscoverySelector> createDiscoverySelectors(CommandLineOp
"Scanning the classpath and using explicit selectors at the same time is not supported");
return createClasspathRootSelectors(options);
}
if (options.isScanModulepath()) {
return Collections.singletonList(DiscoverySelectors.selectModulePath());
}
return createExplicitDiscoverySelectors(options);
}

Expand All @@ -77,6 +81,7 @@ private List<DiscoverySelector> createExplicitDiscoverySelectors(CommandLineOpti
options.getSelectedUris().stream().map(DiscoverySelectors::selectUri).forEach(selectors::add);
options.getSelectedFiles().stream().map(DiscoverySelectors::selectFile).forEach(selectors::add);
options.getSelectedDirectories().stream().map(DiscoverySelectors::selectDirectory).forEach(selectors::add);
options.getSelectedModules().stream().map(DiscoverySelectors::selectModule).forEach(selectors::add);
options.getSelectedPackages().stream().map(DiscoverySelectors::selectPackage).forEach(selectors::add);
options.getSelectedClasses().stream().map(DiscoverySelectors::selectClass).forEach(selectors::add);
options.getSelectedMethods().stream().map(DiscoverySelectors::selectMethod).forEach(selectors::add);
Expand Down
Loading