Skip to content

[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

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

dnestoro
Copy link
Collaborator

@dnestoro dnestoro commented Feb 12, 2025

Problem:

  • Currently, we are running test discovery and generate test plan at buildtime.
  • Then, we are storing launcher and test plan in the ImageSingleton, in order to run it at runtime.
  • This approach requires many classes to be initialized at buildtime.
  • The negative effect of this approach is that we have to maintain multiple lists of classes that should be initialized at buildtime (here, here, and here).

Idea:

  • Move test plan generation and tests execution to runtime.

Solution:

  • PREREQUISITE: We are using test-ids generated before test compilation to discover tests (currently on master as well)
  • We should run test discovery at buildtime to only collect classes needed for reflection registration
  • Then at runtime, we will have everything necessary for reflection, and the only thing left is to run test discovery with test-ids to find tests and execute them
  • Test-ids location should be passed to the NativeImageJUnitLauncher class, so we simply set systemProperty with this location in order to read it at runtime (we do this automatically for both gradle and maven)

This PR:

  • removes hardcoded lists for initializeAtBuildtime (here, here, and here)
  • removes launcher and testPlan from ImageSingletons (here) => therefore we don't need classes to be initialized at buildtime
  • computes testPlan at runtime

In addition:

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Feb 12, 2025
Copy link
Collaborator

@melix melix left a comment

Choose a reason for hiding this comment

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

Thanks David. Can you give a description of the changes that this makes? For example you mention that the tests are discovered at runtime but in practice you are still using the test-ids.

There are changes which need to be reverted on the samples: build plugins versions and maven repositories.

@dnestoro
Copy link
Collaborator Author

Thanks David. Can you give a description of the changes that this makes? For example you mention that the tests are discovered at runtime but in practice you are still using the test-ids.

There are changes which need to be reverted on the samples: build plugins versions and maven repositories.

Hey @melix the PR is not ready yet (it is in draft state). I will update PR description and revert unnecessary changes after I finish testing.

@dnestoro dnestoro force-pushed the dnestoro/refactor-junit-feature branch 3 times, most recently from 29c75fe to b80fd65 Compare February 21, 2025 01:56
@olpaw
Copy link
Member

olpaw commented Feb 24, 2025

Please clean up the commit history: https://github.com/graalvm/native-build-tools/pull/693/commits

E.g. fold
[Fix checkstyle errors] commits into the commit that introduced the style error.

@@ -12,7 +12,7 @@ mavenResolver = "1.9.22"
graalvm = "23.0.2"
openjson = "1.0.13"
junitPlatform = "1.10.0"
junitJupiter = "5.10.0"
junitJupiter = "5.11.0"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I updated this in order to provide support for @FieldSource annotation introduced in 5.11.0 (see this)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please note that we recently released JUnit 5.12.

https://github.com/junit-team/junit5/releases/tag/r5.12.0

So, it's probably a good idea to upgrade to that. I can create a separate GitHub issue for that if you like.

Copy link
Collaborator

@sbrannen sbrannen Feb 24, 2025

Choose a reason for hiding this comment

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

As a side note, JUnit 5.12 includes native-image.properties files (see release notes).

All affected JAR files now include native-image.properties files that contain the --initialize-at-build-time option to avoid breakages in GraalVM projects when updating to newer versions of JUnit.

So, that might affect your refactoring effort.

Copy link
Collaborator

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.

As a side note, JUnit 5.12 includes native-image.properties files (see release notes).

@dnestoro , that means we have to use --exclude-config ... to ensure we do not get those --initialize-at-build-time options for your new implementation that does not require any initialize-at-build-time classes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If anything is incomplete in the out-of-the-box properties files in the JUnit 5 JARs, please let us (the JUnit Team) know, so that we can address that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hey @sbrannen, I tried JUnit 5.12 with both my implementation and the current master. My findings are the following:

  • If I bump JUnit version from 5.10 to 5.12 image build command contains multiple lines like:
-H:ClassInitialization@jar:.../.gradle/caches/modules-2/files-2.1/org.junit.platform/junit-platform-reporting/.../META-INF/native-image/org.junit.platform/junit-platform-reporting/native-image.properties+api=org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener...
  • So native-image.properties are really provided to the native image
  • But if I remove our own initializeAtBuildtime (either on master or in my implementation) I am facing the following error:
Fatal error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: An object of type 'org.junit.platform.commons.support.scanning.DefaultClasspathScanner' was found in the image heap. This type, however, is marked for initialization at image run time for the following reason: classes are initialized at run time by default.
This is not allowed for correctness reasons: All objects that are stored in the image heap must be initialized at build time.
...
  • So, it looks like properties files provided by JUnit are not complete

Copy link
Collaborator

Choose a reason for hiding this comment

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

@marcphilipp, please take a look. Thanks!

Choose a reason for hiding this comment

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

I doubt that's the only class that's missing. I could copy the list of classes from the hard-coded lists in the feature implementation but I'd rather extend our existing tests so it exercises all relevant code.

But if I remove our own initializeAtBuildtime (either on master or in my implementation) I am facing the following error

@dnestoro What do I need run to reproduce this locally?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am so confused now... I tried to reproduce this error again, but now everything works correctly on master (while on my branch it fails with different errors depending on the GraalVM version I use). I will double check what happened here (maybe something got cached) because I am 100% sure that it was failing on master as well.

@marcphilipp @sbrannen Sorry for the false alarm. I will investigate this deeper to see if something is still missing on master and give you a reproducer if something fails.

@dnestoro dnestoro force-pushed the dnestoro/refactor-junit-feature branch 2 times, most recently from 2448625 to e5d1f01 Compare February 25, 2025 16:46
@dnestoro dnestoro marked this pull request as ready for review February 26, 2025 11:41
@fniephaus
Copy link
Member

While not so concerning for testing, how much bigger do such test images get when launcher and testPlan are no longer ImageSingletons? Bigger or smaller? :)

Copy link
Collaborator

@melix melix left a comment

Choose a reason for hiding this comment

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

I didn't finish the review but left a few comments. I'm still confused and would appreciate a few more details:

  • you are saying that we should discover the test ids at runtime, however, we still run the tests in JVM mode to get the test ids?
  • if the test ids are not needed anymore, maybe we should change the name of the parameters which are used

import java.nio.file.Files

class JUnitFunctionalTests extends AbstractFunctionalTest {
@Unroll("test if JUint support works with various annotations, reflection and resources")
Copy link
Collaborator

Choose a reason for hiding this comment

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

:nit: this is not an unrolled test. I guess you just wanted a display name, in which case you can put this directly in the test method name (also, there's a typo in JUnit)

@dnestoro
Copy link
Collaborator Author

dnestoro commented Feb 26, 2025

While not so concerning for testing, how much bigger do such test images get when launcher and testPlan are no longer ImageSingletons? Bigger or smaller? :)

On a first look, the tests image size gets around 5% of increase. But I will run more tests and provide more accurate information soon

@dnestoro
Copy link
Collaborator Author

dnestoro commented Feb 26, 2025

@melix

  • you are saying that we should discover the test ids at runtime, however, we still run the tests in JVM mode to get the test ids?
  • if the test ids are not needed anymore, maybe we should change the name of the parameters which are used

No, I am just saying that we should use test ids to compute test plan (to discover it with launcher.discover(request)) at runtime. With that approach, we don't have to compute testPlan at buildtime and store it in ImageSingletons in order to use it at runtime (as we currently do on master)

@dnestoro dnestoro changed the title Refactor JUnit feature [GR-60258] Refactor JUnit feature Mar 3, 2025
@dnestoro
Copy link
Collaborator Author

dnestoro commented Mar 3, 2025

@fniephaus after deeper investigation, I came to the following results regarding image size:

  • On an image created from a single test:

    • master: 25.47MB
    • branch: 27.29MB
    • diff: 1.82MB (6.6691% bigger image size)
  • On an image created from 400 tests:

    • master: 26.58MB
    • branch: 27.88MB
    • diff: 1.3MB (4,6628% bigger image size)
  • On an image created from 1500 tests:

    • master: 28.96MB
    • branch: 29.31MB
    • diff: 0.35MB (1.1941% bigger image size)
  • On an image created from 2800 tests, the master and the branch are equal

  • On an image, created from 10 000 tests:

    • master: 48.81MB
    • branch: 42.39MB
    • diff: -6.42MB (13.15% smaller image size)

The more tests, the more benefits we get!

@dnestoro dnestoro requested review from sbrannen and melix March 3, 2025 14:09
@fniephaus
Copy link
Member

fniephaus commented Mar 3, 2025

Thanks for checking, @dnestoro!

@dnestoro dnestoro force-pushed the dnestoro/refactor-junit-feature branch from 9637662 to c890433 Compare April 16, 2025 10:42
@dnestoro dnestoro marked this pull request as ready for review April 16, 2025 11:35
@dnestoro dnestoro requested a review from olpaw April 16, 2025 11:35
@dnestoro dnestoro self-assigned this Apr 23, 2025
olpaw
olpaw previously approved these changes Apr 28, 2025
Copy link

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

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

LGTM 👍

(I lack permissions to formally approve)

Copy link
Member

@fniephaus fniephaus left a comment

Choose a reason for hiding this comment

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

Thank you, @dnestoro!

Copy link

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

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

Good catch on the recursive type declarations! 👍

Comment on lines +172 to +173

private final Set<Class<?>> registeredClasses = new HashSet<>();

Choose a reason for hiding this comment

The 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 registerTestClassForReflection and shouldRegisterClass.

Suggested change
private final Set<Class<?>> registeredClasses = new HashSet<>();

LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectors)
.build();

Launcher launcher = LauncherFactory.create();

Choose a reason for hiding this comment

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

Suggested change
Launcher launcher = LauncherFactory.create();
Set<Class<?>> registeredClasses = new HashSet<>();
Launcher launcher = LauncherFactory.create();

@@ -143,14 +168,44 @@ private TestPlan discoverTestsAndRegisterTestClassesForReflection(Launcher launc
.map(ClassSource.class::cast)
.map(ClassSource::getJavaClass)
.forEach(this::registerTestClassForReflection);

Choose a reason for hiding this comment

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

Suggested change
.forEach(this::registerTestClassForReflection);
.forEach(clazz -> registerTestClassForReflection(clazz, registeredClasses));


private final Set<Class<?>> registeredClasses = new HashSet<>();

private boolean shouldRegisterClass(Class<?> clazz) {

Choose a reason for hiding this comment

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

Suggested change
private boolean shouldRegisterClass(Class<?> clazz) {
private boolean shouldRegisterClass(Class<?> clazz, Set<Class<?>> registeredClasses) {

Comment on lines +182 to +187
if (registeredClasses.contains(clazz)) {
return false;
}
registeredClasses.add(clazz);

return testPlan;
return true;

Choose a reason for hiding this comment

The 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);

Comment on lines 190 to +191
private void registerTestClassForReflection(Class<?> clazz) {
if (!shouldRegisterClass(clazz)) {

Choose a reason for hiding this comment

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

Suggested change
private void registerTestClassForReflection(Class<?> clazz) {
if (!shouldRegisterClass(clazz)) {
private void registerTestClassForReflection(Class<?> clazz, Set<Class<?>> registeredClasses) {
if (!shouldRegisterClass(clazz, registeredClasses)) {


Class<?>[] declaredClasses = clazz.getDeclaredClasses();
for (Class<?> declaredClass : declaredClasses) {
registerTestClassForReflection(declaredClass);

Choose a reason for hiding this comment

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

Suggested change
registerTestClassForReflection(declaredClass);
registerTestClassForReflection(declaredClass, registeredClasses);


Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> inter : interfaces) {
registerTestClassForReflection(inter);

Choose a reason for hiding this comment

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

Suggested change
registerTestClassForReflection(inter);
registerTestClassForReflection(inter, registeredClasses);

for (Class<?> inter : interfaces) {
registerTestClassForReflection(inter);
}

Class<?> superClass = clazz.getSuperclass();
if (superClass != null && superClass != Object.class) {
registerTestClassForReflection(superClass);

Choose a reason for hiding this comment

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

Suggested change
registerTestClassForReflection(superClass);
registerTestClassForReflection(superClass, registeredClasses);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants