[#3345/#3393] Support Maven 4 module source hierarchy (single + multiple modules per POM) - #3392
[#3345/#3393] Support Maven 4 module source hierarchy (single + multiple modules per POM)#3392ascheman wants to merge 7 commits into
Conversation
ea14765 to
62a1827
Compare
There was a problem hiding this comment.
Pull request overview
This pull request updates Surefire’s JPMS handling to work with Maven 4’s module source hierarchy output layout (target/{classes,test-classes}/<module>/) and to consume the compiler-generated module-info-patch.args so modular whitebox tests can run on the module path without per-module Surefire configuration.
Changes:
- Extend module descriptor detection and test class scanning to support nested build output layouts produced by Maven 4.
- Enhance forked JPMS argfile generation to read
module-info-patch.argsand forward its directives while still letting Surefire manage--add-reads/--add-modules. - Add new IT fixtures/tests covering classic module whitebox testing and Maven 4 module-source-hierarchy +
module-info-patch.mavenbehavior; default IT launcher to fork for Maven 4+.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java | Detect nested module descriptor layout, scan tests from nested test output, and choose correct patch directory for --patch-module. |
| maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfiguration.java | Read compiler-generated module-info-patch.args and incorporate supported JPMS directives into the fork argfile. |
| maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoJava7PlusTest.java | Add unit tests for nested module descriptor/layout detection edge cases. |
| maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfigurationTest.java | Add unit tests validating consumption/skipping behavior for module-info-patch.args. |
| surefire-its/src/test/java/org/apache/maven/surefire/its/fixture/MavenLauncher.java | Default to forked launcher for Maven 4+ since embedded launcher is Maven 3-specific. |
| surefire-its/src/test/java/org/apache/maven/surefire/its/ModulePathWhiteboxIT.java | New IT asserting classic single-module whitebox testing works via --patch-module. |
| surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire3345ModuleSourceHierarchyIT.java | New Maven 4-only IT asserting module-source-hierarchy + patch args support. |
| surefire-its/src/test/resources/modulepath-whitebox/pom.xml | IT fixture POM for classic module-path whitebox testing. |
| surefire-its/src/test/resources/modulepath-whitebox/src/main/java/module-info.java | IT fixture module descriptor for classic layout. |
| surefire-its/src/test/resources/modulepath-whitebox/src/main/java/com/example/Calculator.java | IT fixture main class for classic layout. |
| surefire-its/src/test/resources/modulepath-whitebox/src/main/java/com/example/internal/MathHelper.java | IT fixture internal class used for whitebox access checks. |
| surefire-its/src/test/resources/modulepath-whitebox/src/test/java/com/example/CalculatorTest.java | IT fixture test verifying exported API behavior. |
| surefire-its/src/test/resources/modulepath-whitebox/src/test/java/com/example/internal/MathHelperWhiteboxTest.java | IT fixture whitebox test verifying non-exported internal access works. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/pom.xml | New Maven 4 model-4.1.0 module-source-hierarchy fixture POM. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/main/java/module-info.java | Fixture module descriptor for Maven 4 module source hierarchy layout. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/main/java/com/example/Calculator.java | Fixture main class for module-source-hierarchy project. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/main/java/com/example/internal/MathHelper.java | Fixture internal class for module-source-hierarchy project. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/test/java/module-info-patch.maven | Fixture patch description used by compiler to generate module-info-patch.args. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/test/java/com/example/CalculatorTest.java | Fixture test in nested test output layout. |
| surefire-its/src/test/resources/surefire-3345-module-source-hierarchy/src/com.example/test/java/com/example/internal/MathHelperWhiteboxTest.java | Fixture whitebox test in nested test output layout. |
| // Check for module-info-patch.args generated by maven-compiler-plugin 4.x | ||
| File patchArgs = findModuleInfoPatchArgs(patchFile); | ||
| if (patchArgs != null) { | ||
| // Use --add-exports directives from module-info-patch.args | ||
| appendModuleInfoPatchArgs(args, patchArgs, moduleName); | ||
| } |
| * Reads the module-info-patch.args file and appends its --add-exports and --add-opens | ||
| * directives to the args builder. The --add-reads directive is handled by surefire itself | ||
| * (always adds ALL-UNNAMED) to avoid referencing modules that may be on the classpath | ||
| * rather than the module-path. |
Address Copilot review feedback on apache#3392: - Reword the inline comment and the appendModuleInfoPatchArgs Javadoc: the method forwards ALL directives from module-info-patch.args except --add-reads/--add-modules, not only --add-exports. - Sort the nested module directory candidates so the picked module no longer depends on filesystem iteration order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for doing that! I will look closely at the pull request. In the meantime, below is an observation based on the description only. It does not necessarily need an action in this pull request, maybe it is okay to process in steps and address the following point in future pull requests.
I think that it would be nice if we could remove all
|
Agreed on the direction — and you anticipated the reason for the current compromise: today surefire places test-scope dependencies (JUnit engine etc.) on the classpath, so honoring the file's Fully honoring I'm starting on that right away on a branch stacked on top of #3392/#3394 (the descriptor-driven path split from #3393 is the natural foundation), tracked under #3090 — the PR will arrive as a draft and become mergeable once these two have landed. |
| } | ||
|
|
||
| // Always auto-generate --add-opens for test packages (JUnit needs reflection access). | ||
| // module-info-patch.maven cannot use ALL-UNNAMED in add-opens, so surefire handles this. |
There was a problem hiding this comment.
I suggest to disable the auto-generation of --add-opens by default. I suggest that the default behaviour should be to use module-info-patch.maven exactly as-is. We may provide an option for enabling auto-generation of --add-opens if this is really what the user wants, but I would rather encourage developers to declare public the methods invoked by JUnit. It is not clear to me why some developers want to keep those methods private, as src/test/java classes are not distributed anyway and public makes clear that those methods are intended to be invoked from outside their package (from this point of view, declaring test methods as private or package-private seems misleading to me). Since --add-opens gives access to all private methods, not only test methods, an encapsulation argument in favour of this approach is questionable. I do not object if some developers really want to keep test methods private and rely on --add-opens, but I think that it should be opt-in rather than the default. A little bit in the spirit of "integrity by default" that Oracle is promoting.
There was a problem hiding this comment.
Correction: Surefire would still need to generate --add-exports for each non-exported package to JUnit. These --add-exports are not present in the module-info-patch.args file because they were not needed for compilation, but they are needed for execution.
If the test methods are public, --add-exports are sufficient. If the test methods are private, --add-exports need to be replaced by --add-opens (I'm not sure that we need both, let's try if we can provide only one of these options). The use of --add-exports versus --add-opens could be controlled by the configuration option suggested in above comment.
There was a problem hiding this comment.
Agreed with the direction, one empirical addition: the constraint is not only on the test methods but already on the test classes. The common JUnit 5 idiom uses package-private test classes, and --add-exports only grants access to public types — the engine then needs setAccessible (i.e. --add-opens) even for public methods in such classes, so exports-only as default would break that idiom.
This becomes actionable in the #3090 implementation (in progress, stacked on this PR and #3394), where the --add-opens generation already moved to the mojo side: I would add a configuration option along the lines of testModuleAccess = opens | exports | none. Given the JUnit 5 idiom I lean towards opens as the default — happy to follow your preference there.
There was a problem hiding this comment.
Yes — exports vs. opens becomes exactly that configuration choice in #3090, and surefire keeps generating one of the two for the test packages (they are not in module-info-patch.args since compilation does not need them, as you say).
Address desruisseaux's review comments on apache#3392: - Javadoc for createArgsFile, documenting in particular the patchFile parameter (test output directory, or its per-module subdirectory for a Maven 4 module source hierarchy build). - Correct the findModuleInfoPatchArgs documentation: there is exactly one module-info-patch.args per project, at target/test-classes/META-INF/maven/; the parent lookup exists because the patch directory is the nested per-module directory in a module source hierarchy build. - Drop the explicit charset: Files.newBufferedReader(Path) already reads UTF-8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compute the nested module directories once and pass them down instead of the isNestedModuleLayout/findNestedModuleDescriptor double invocation, switch the module-info-patch.args helpers from File to java.nio.file.Path, and create test File objects before the try block so the finally block deletes them directly. Addresses review comments by @desruisseaux on apache#3392. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The IT fixture defaults to embedded launching via Embedded3xLauncher
(maven-shared-verifier), which looks up org.apache.maven.cli.MavenCli.
Maven 4 renamed the entry point to org.apache.maven.cling.MavenCling
with a different signature, so every embedded launch against Maven 4
aborts with NoSuchMethodException before any test runs.
Detect Maven 4 by the presence of maven-api-core in ${maven.home}/lib
and default forkJvm to true in that case; Maven 3 keeps embedded mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maven 4 with maven-compiler-plugin 4.x compiles module source hierarchy projects to a nested layout (target/classes/<module>/, target/test-classes/<module>/) and emits a runtime handoff file META-INF/maven/module-info-patch.args derived from module-info-patch.maven. Surefire handled neither: - findModuleDescriptor() only looked for module-info.class at the build output root, so detection failed and execution silently fell back to the classpath. - DirectoryScanner read the module directory as a package prefix, producing doubled FQCNs and "Tests run: 0" or "Unable to create test class" failures. - The compiler-generated module-info-patch.args was ignored, dropping the --add-exports directives declared in module-info-patch.maven. Detect the nested module directory, scan its test classes, patch the module with target/test-classes/<module>, and merge the handoff file into the fork argfile. --add-reads/--add-modules lines from the file are skipped (one-line and two-line argfile forms): they may reference named modules that surefire places on the classpath; surefire keeps generating --add-reads <module>=ALL-UNNAMED, --add-opens for test packages and --add-modules ALL-MODULE-PATH itself. The nested layout is decided by the MAIN build output only (no root module-info.class plus a module subdirectory containing one): a classic modular project whose module is named after its root package (module "it", package "it") has target/test-classes/it/ as a package directory, which must not be mistaken for a nested test output and patched into the module (would break class loading of every test). Covered by unit tests and two ITs: ModulePathWhiteboxIT (classic layout, runs on Maven 3+4) and Surefire3345ModuleSourceHierarchyIT (nested layout, skips itself below Maven 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address Copilot review feedback on apache#3392: - Reword the inline comment and the appendModuleInfoPatchArgs Javadoc: the method forwards ALL directives from module-info-patch.args except --add-reads/--add-modules, not only --add-exports. - Sort the nested module directory candidates so the picked module no longer depends on filesystem iteration order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The compiler-generated handoff file is UTF-8; FileReader uses the platform default charset and could misparse non-ASCII module or package names. Addresses Copilot review feedback on apache#3394. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address desruisseaux's review comments on apache#3392: - Javadoc for createArgsFile, documenting in particular the patchFile parameter (test output directory, or its per-module subdirectory for a Maven 4 module source hierarchy build). - Correct the findModuleInfoPatchArgs documentation: there is exactly one module-info-patch.args per project, at target/test-classes/META-INF/maven/; the parent lookup exists because the patch directory is the nested per-module directory in a module source hierarchy build. - Drop the explicit charset: Files.newBufferedReader(Path) already reads UTF-8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Maven 4 module source hierarchy build may declare several Java modules in one POM; the single-module support from apache#3345 detected only the first nested module, scanned only its tests, and decided classpath-vs-module-path dependency placement with one descriptor, so the fork died at the Java Modules boot layer when a sibling module required a dependency that stayed on the classpath. - findModuleDescriptor() resolves ALL nested module descriptors; the primary module (driving scanning and --patch-module) is the first one with a nested test output directory, siblings travel as additional results in ResolvePathResultWrapper. - scanDirectories() unions the scan over every nested target/test-classes/<module>/ directory. - The classpath/module-path split is the union over all module descriptors: an element required on the module path by ANY module goes there, since the fork has a single boot layer. - The primary module only opens its own test packages; each sibling module with tests gets --patch-module, --add-reads and per-package --add-opens, passed through the existing StartupConfiguration#getJpmsArguments() channel - no change to the surefire-booter API or its serialization. Covered by a unit test for the multi-descriptor detection and the IT Surefire3393MultiModuleSourceHierarchyIT: two modules in one POM (com.example.extra requires com.example.core, core requires transitive jakarta.json to cover the boot-layer failure), whitebox tests in both modules, five tests in one execution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compute the nested module directories once and pass them down instead of the isNestedModuleLayout/findNestedModuleDescriptor double invocation, switch the module-info-patch.args helpers from File to java.nio.file.Path, and create test File objects before the try block so the finally block deletes them directly. Addresses review comments by @desruisseaux on apache#3392. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8527e26 to
bef0da5
Compare
Fixes #3345. Fixes #3393.
Note
On @desruisseaux's request (via Slack) this PR now also contains the multi-module support that previously lived in the stacked draft PR #3394 — the split created review noise without value. The commit history keeps the two steps separate (
Support Maven 4 module source hierarchy/Support multiple modules per POM).Maven 4 with
maven-compiler-plugin4.x compiles module-source-hierarchy projects(
<build><sources>with one or more<module>entries, POM model 4.1.0) to a nestedoutput layout —
target/classes/<module>/andtarget/test-classes/<module>/— andwrites a runtime handoff file
target/test-classes/META-INF/maven/module-info-patch.argsderived from
module-info-patch.maven(the non-deprecated replacement for a secondmodule-info.javain the test tree).Surefire previously handled none of this, so modular whitebox tests silently fell back to
the classpath (module detection returned null) or failed test discovery
(
DirectoryScannerinterpreted the module directory as a package prefix, producingdoubled FQCNs such as
com.example.com.example.CalculatorTest). The only workaround wasa per-module
classesDirectory/testClassesDirectory/useModulePath/argLineconfiguration hardcoding a single module name.
Changes — single module (#3345)
AbstractSurefireMojofindModuleDescriptor(...)now also looks formodule-info.classin immediatesubdirectories of the build output (
target/classes/<module>/).scanForTestClasses()scanstarget/test-classes/<module>/when the nested layoutis detected, instead of treating
<module>as a package segment.--patch-modulesource is the nestedtarget/test-classes/<module>directorywhen present.
ModularClasspathForkConfigurationcreateArgsFile(...)reads the (single, per-project) compiler-generatedmodule-info-patch.argsand passes its directives (e.g.--add-exports) through tothe forked JVM argfile.
--add-reads/--add-moduleslines from the handoff file are skipped for now (theymay reference named modules that surefire still places on the classpath); surefire
keeps generating
--add-reads <module>=ALL-UNNAMED,--add-modules ALL-MODULE-PATHand
--add-opensfor test packages itself. Removing this bridge is [SUREFIRE-1755] Put dependencies from module-info in test sources on module path #3090(implementation in progress).
surefire-itsfixture:MavenLauncherdefaults to forked JVM when the Maven undertest is 4.x (
Embedded3xLauncheronly supports the Maven 3 entry point).Changes — multiple modules per POM (#3393)
findModuleDescriptor(...)resolves ALL nested module descriptors; the primary module(driving scanning and
--patch-module) is the first one with a nested test outputdirectory, siblings travel as additional results in
ResolvePathResultWrapper.scanDirectories()unions the test-class scan over every nestedtarget/test-classes/<module>/directory.newStartupConfigWithModularPath(...): the classpath/module-path split is computed permodule descriptor and unioned — an element required on the module path by ANY module
ends up there (the fork has a single boot layer); the primary module only opens its own
test packages; each sibling module with tests gets
--patch-module,--add-readsandper-package
--add-opens, passed through the existingStartupConfiguration#getJpmsArguments()channel (no surefire-booter API change).Tests
collision, multi-module ordering), handoff-file pass-through and fallback.
ModulePathWhiteboxIT— classic single-module layout, runs under Maven 3 and 4.Surefire3345ModuleSourceHierarchyIT— model-4.1.0 module source hierarchy withmodule-info-patch.maven; skips itself under Maven 3.Surefire3393MultiModuleSourceHierarchyIT— two modules in one POM(
com.example.extrarequirescom.example.core, corerequires transitive jakarta.json), whitebox tests in both modules, 5 tests in ONE execution; skipsitself under Maven 3.
Verification (local, JDK 17.0.18-tem, macOS)
Full
maven-surefire-commonunit suite green. Real-world acceptance: a 3-modulejakarta.json/jsonb component (602 tests) runs zero-config in ONE surefire execution
(previously: silent 0-test run, or hand-written per-module executions).
Scope notes
verbatim (deps on the module path, no auto
--add-reads/--add-modules) is [SUREFIRE-1755] Put dependencies from module-info in test sources on module path #3090,in progress on top of this PR.
against 4.0.0-rc-5 and the current 4.0.x snapshot. Once Enable Maven 4 build #3352 enables a Maven 4 CI
job, they will run there too.
Following this checklist to help us incorporate your contribution quickly and easily:
mvn clean installto make sure basic checks pass. A more thorough check will be performed on your pull request automatically.