-
Notifications
You must be signed in to change notification settings - Fork 76
[GR-60258] Refactor JUnit feature #693
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
Changes from all commits
5853e8f
d041294
6ec21ea
f9755ae
66acdca
bd36fc6
faff29f
1e66e70
76ddff1
ec2a319
50a7ac9
9b4c249
f94a05e
4ce03e7
6519022
f3be7db
dd4f497
82d3fbb
3364e05
c890433
cae0ced
c3979f4
7f51603
04198fc
6f9748e
847ef48
2c2ec24
5b8d5fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -45,6 +45,8 @@ | |||||||||
import org.graalvm.nativeimage.ImageSingletons; | ||||||||||
import org.graalvm.nativeimage.hosted.Feature; | ||||||||||
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; | ||||||||||
import org.graalvm.nativeimage.hosted.RuntimeReflection; | ||||||||||
|
||||||||||
import org.junit.platform.engine.DiscoverySelector; | ||||||||||
import org.junit.platform.engine.discovery.DiscoverySelectors; | ||||||||||
import org.junit.platform.engine.discovery.UniqueIdSelector; | ||||||||||
|
@@ -57,15 +59,19 @@ | |||||||||
import org.junit.platform.launcher.core.LauncherFactory; | ||||||||||
import org.junit.platform.launcher.listeners.UniqueIdTrackingListener; | ||||||||||
|
||||||||||
import java.io.BufferedReader; | ||||||||||
import java.io.IOException; | ||||||||||
import java.io.InputStream; | ||||||||||
import java.io.InputStreamReader; | ||||||||||
import java.io.UncheckedIOException; | ||||||||||
import java.nio.file.Files; | ||||||||||
import java.nio.file.Path; | ||||||||||
import java.nio.file.Paths; | ||||||||||
import java.util.HashSet; | ||||||||||
import java.util.List; | ||||||||||
import java.util.Optional; | ||||||||||
import java.util.ServiceLoader; | ||||||||||
import java.util.Set; | ||||||||||
import java.util.function.BiConsumer; | ||||||||||
import java.util.function.Consumer; | ||||||||||
import java.util.stream.Collectors; | ||||||||||
import java.util.stream.Stream; | ||||||||||
|
@@ -74,66 +80,85 @@ | |||||||||
public final class JUnitPlatformFeature implements Feature { | ||||||||||
dnestoro marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
public final boolean debug = System.getProperty("debug") != null; | ||||||||||
|
||||||||||
private static final NativeImageConfigurationImpl nativeImageConfigImpl = new NativeImageConfigurationImpl(); | ||||||||||
private final ServiceLoader<PluginConfigProvider> extensionConfigProviders = ServiceLoader.load(PluginConfigProvider.class); | ||||||||||
|
||||||||||
public static void debug(String format, Object... args) { | ||||||||||
if (debug()) { | ||||||||||
System.out.printf("[Debug] " + format + "%n", args); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public void afterRegistration(AfterRegistrationAccess access) { | ||||||||||
extensionConfigProviders.forEach(p -> p.initialize(access.getApplicationClassLoader(), nativeImageConfigImpl)); | ||||||||||
} | ||||||||||
|
||||||||||
private static boolean debug() { | ||||||||||
return ImageSingletons.lookup(JUnitPlatformFeature.class).debug; | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public void duringSetup(DuringSetupAccess access) { | ||||||||||
forEachProvider(p -> p.onLoad(nativeImageConfigImpl)); | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public void beforeAnalysis(BeforeAnalysisAccess access) { | ||||||||||
RuntimeClassInitialization.initializeAtBuildTime(NativeImageJUnitLauncher.class); | ||||||||||
/* Before GraalVM version 22 we couldn't have classes initialized at run-time | ||||||||||
* that are also used at build-time but not added to the image heap */ | ||||||||||
if (Runtime.version().feature() <= 21) { | ||||||||||
initializeClassesForJDK21OrEarlier(); | ||||||||||
} | ||||||||||
|
||||||||||
List<Path> classpathRoots = access.getApplicationClassPath(); | ||||||||||
List<? extends DiscoverySelector> selectors = getSelectors(classpathRoots); | ||||||||||
List<? extends DiscoverySelector> selectors = getSelectors(); | ||||||||||
registerTestClassesForReflection(selectors); | ||||||||||
|
||||||||||
Launcher launcher = LauncherFactory.create(); | ||||||||||
TestPlan testplan = discoverTestsAndRegisterTestClassesForReflection(launcher, selectors); | ||||||||||
ImageSingletons.add(NativeImageJUnitLauncher.class, new NativeImageJUnitLauncher(launcher, testplan)); | ||||||||||
/* support for JUnit Vintage */ | ||||||||||
registerClassesForHamcrestSupport(access); | ||||||||||
} | ||||||||||
|
||||||||||
private List<? extends DiscoverySelector> getSelectors(List<Path> classpathRoots) { | ||||||||||
private List<? extends DiscoverySelector> getSelectors() { | ||||||||||
try { | ||||||||||
Path outputDir = Paths.get(System.getProperty(UniqueIdTrackingListener.OUTPUT_DIR_PROPERTY_NAME)); | ||||||||||
String prefix = System.getProperty(UniqueIdTrackingListener.OUTPUT_FILE_PREFIX_PROPERTY_NAME, | ||||||||||
String uniqueIdDirectoryProperty = System.getProperty(UniqueIdTrackingListener.OUTPUT_DIR_PROPERTY_NAME); | ||||||||||
if (uniqueIdDirectoryProperty == null) { | ||||||||||
throw new IllegalStateException("Cannot determine test-ids directory because junit.platform.listeners.uid.tracking.output.dir property is null"); | ||||||||||
} | ||||||||||
|
||||||||||
String uniqueIdFilePrefix = System.getProperty(UniqueIdTrackingListener.OUTPUT_FILE_PREFIX_PROPERTY_NAME, | ||||||||||
dnestoro marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
UniqueIdTrackingListener.DEFAULT_OUTPUT_FILE_PREFIX); | ||||||||||
List<UniqueIdSelector> selectors = readAllFiles(outputDir, prefix) | ||||||||||
if (uniqueIdFilePrefix == null) { | ||||||||||
throw new IllegalStateException("Cannot determine unique test-ids prefix because junit.platform.listeners.uid.tracking.output.file.prefix property is null"); | ||||||||||
} | ||||||||||
|
||||||||||
Path uniqueIdDirectory = Path.of(uniqueIdDirectoryProperty); | ||||||||||
List<UniqueIdSelector> selectors = readAllFiles(uniqueIdDirectory, uniqueIdFilePrefix) | ||||||||||
.map(DiscoverySelectors::selectUniqueId) | ||||||||||
.collect(Collectors.toList()); | ||||||||||
if (!selectors.isEmpty()) { | ||||||||||
System.out.printf( | ||||||||||
"[junit-platform-native] Running in 'test listener' mode using files matching pattern [%s*] " | ||||||||||
+ "found in folder [%s] and its subfolders.%n", | ||||||||||
prefix, outputDir.toAbsolutePath()); | ||||||||||
uniqueIdFilePrefix, uniqueIdDirectory.toAbsolutePath()); | ||||||||||
return selectors; | ||||||||||
} | ||||||||||
} catch (Exception ex) { | ||||||||||
debug("Failed to read UIDs from UniqueIdTrackingListener output files: " + ex.getMessage()); | ||||||||||
} | ||||||||||
|
||||||||||
System.out.println("[junit-platform-native] Running in 'test discovery' mode. Note that this is a fallback mode."); | ||||||||||
if (debug) { | ||||||||||
classpathRoots.forEach(entry -> debug("Selecting classpath root: " + entry)); | ||||||||||
} | ||||||||||
return DiscoverySelectors.selectClasspathRoots(new HashSet<>(classpathRoots)); | ||||||||||
throw new RuntimeException("Cannot compute test selectors from test ids."); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Use the JUnit Platform Launcher to discover tests and register classes | ||||||||||
* for reflection. | ||||||||||
* Use the JUnit Platform Launcher to register classes for reflection. | ||||||||||
*/ | ||||||||||
private TestPlan discoverTestsAndRegisterTestClassesForReflection(Launcher launcher, | ||||||||||
List<? extends DiscoverySelector> selectors) { | ||||||||||
|
||||||||||
private void registerTestClassesForReflection(List<? extends DiscoverySelector> selectors) { | ||||||||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() | ||||||||||
.selectors(selectors) | ||||||||||
.build(); | ||||||||||
|
||||||||||
Launcher launcher = LauncherFactory.create(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
TestPlan testPlan = launcher.discover(request); | ||||||||||
|
||||||||||
testPlan.getRoots().stream() | ||||||||||
.flatMap(rootIdentifier -> testPlan.getDescendants(rootIdentifier).stream()) | ||||||||||
.map(TestIdentifier::getSource) | ||||||||||
|
@@ -143,14 +168,44 @@ private TestPlan discoverTestsAndRegisterTestClassesForReflection(Launcher launc | |||||||||
.map(ClassSource.class::cast) | ||||||||||
.map(ClassSource::getJavaClass) | ||||||||||
.forEach(this::registerTestClassForReflection); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
|
||||||||||
private final Set<Class<?>> registeredClasses = new HashSet<>(); | ||||||||||
Comment on lines
+172
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should be a field. Instead, it should be possible to pass it as a paramter to
Suggested change
|
||||||||||
|
||||||||||
private boolean shouldRegisterClass(Class<?> clazz) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
/* avoid registering java internal classes */ | ||||||||||
if (ModuleLayer.boot().modules().contains(clazz.getModule())) { | ||||||||||
return false; | ||||||||||
} | ||||||||||
|
||||||||||
/* avoid loops (possible case: class B is inner class of A, and B extends A) */ | ||||||||||
if (registeredClasses.contains(clazz)) { | ||||||||||
return false; | ||||||||||
} | ||||||||||
registeredClasses.add(clazz); | ||||||||||
|
||||||||||
return testPlan; | ||||||||||
return true; | ||||||||||
Comment on lines
+182
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be simplified to the following, but your version may be more readable. return registeredClasses.add(clazz); |
||||||||||
} | ||||||||||
|
||||||||||
private void registerTestClassForReflection(Class<?> clazz) { | ||||||||||
if (!shouldRegisterClass(clazz)) { | ||||||||||
Comment on lines
190
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
debug("Registering test class for reflection: %s", clazz.getName()); | ||||||||||
nativeImageConfigImpl.registerAllClassMembersForReflection(clazz); | ||||||||||
dnestoro marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
forEachProvider(p -> p.onTestClassRegistered(clazz, nativeImageConfigImpl)); | ||||||||||
|
||||||||||
Class<?>[] declaredClasses = clazz.getDeclaredClasses(); | ||||||||||
for (Class<?> declaredClass : declaredClasses) { | ||||||||||
registerTestClassForReflection(declaredClass); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
|
||||||||||
Class<?>[] interfaces = clazz.getInterfaces(); | ||||||||||
for (Class<?> inter : interfaces) { | ||||||||||
registerTestClassForReflection(inter); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
|
||||||||||
Class<?> superClass = clazz.getSuperclass(); | ||||||||||
if (superClass != null && superClass != Object.class) { | ||||||||||
registerTestClassForReflection(superClass); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
@@ -163,16 +218,6 @@ private void forEachProvider(Consumer<PluginConfigProvider> consumer) { | |||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
public static void debug(String format, Object... args) { | ||||||||||
if (debug()) { | ||||||||||
System.out.printf("[Debug] " + format + "%n", args); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
public static boolean debug() { | ||||||||||
return ImageSingletons.lookup(JUnitPlatformFeature.class).debug; | ||||||||||
} | ||||||||||
|
||||||||||
private Stream<String> readAllFiles(Path dir, String prefix) throws IOException { | ||||||||||
return findFiles(dir, prefix).map(outputFile -> { | ||||||||||
try { | ||||||||||
|
@@ -192,4 +237,38 @@ private static Stream<Path> findFiles(Path dir, String prefix) throws IOExceptio | |||||||||
&& path.getFileName().toString().startsWith(prefix))); | ||||||||||
} | ||||||||||
|
||||||||||
private static void registerClassesForHamcrestSupport(BeforeAnalysisAccess access) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||
ClassLoader applicationLoader = access.getApplicationClassLoader(); | ||||||||||
Class<?> typeSafeMatcher = findClassOrNull(applicationLoader, "org.hamcrest.TypeSafeMatcher"); | ||||||||||
Class<?> typeSafeDiagnosingMatcher = findClassOrNull(applicationLoader, "org.hamcrest.TypeSafeDiagnosingMatcher"); | ||||||||||
if (typeSafeMatcher != null || typeSafeDiagnosingMatcher != null) { | ||||||||||
BiConsumer<DuringAnalysisAccess, Class<?>> registerMatcherForReflection = (a, c) -> RuntimeReflection.register(c.getDeclaredMethods()); | ||||||||||
if (typeSafeMatcher != null) { | ||||||||||
access.registerSubtypeReachabilityHandler(registerMatcherForReflection, typeSafeMatcher); | ||||||||||
} | ||||||||||
if (typeSafeDiagnosingMatcher != null) { | ||||||||||
access.registerSubtypeReachabilityHandler(registerMatcherForReflection, typeSafeDiagnosingMatcher); | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
private static Class<?> findClassOrNull(ClassLoader loader, String className) { | ||||||||||
try { | ||||||||||
return loader.loadClass(className); | ||||||||||
} catch (ClassNotFoundException e) { | ||||||||||
return null; | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
private static void initializeClassesForJDK21OrEarlier() { | ||||||||||
try (InputStream is = JUnitPlatformFeature.class.getResourceAsStream("/initialize-at-buildtime")) { | ||||||||||
if (is != null) { | ||||||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { | ||||||||||
br.lines().forEach(RuntimeClassInitialization::initializeAtBuildTime); | ||||||||||
} | ||||||||||
} | ||||||||||
} catch (IOException e) { | ||||||||||
throw new RuntimeException("Failed to process build time initializations for JDK 21 or earlier"); | ||||||||||
} | ||||||||||
} | ||||||||||
} |
Uh oh!
There was an error while loading. Please reload this page.