Skip to content

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin.surefire;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;

/**
* The runtime handoff file {@code META-INF/maven/module-info-patch.args} written by
* maven-compiler-plugin 4.x from {@code module-info-patch.maven}. It contains the Java
* Modules options the compiler used for the test compilation (one option per line, value
* either on the same line separated by whitespace or on the following line).
*
* @since 3.6.0
*/
public final class ModuleInfoPatchArgsFile {
public static final String RELATIVE_PATH = "META-INF/maven/module-info-patch.args";

private final File file;
private final List<String> lines;
private final Set<String> addedModules;

private ModuleInfoPatchArgsFile(File file, List<String> lines, Set<String> addedModules) {
this.file = file;
this.lines = lines;
this.addedModules = addedModules;
}

/**
* Loads the handoff file from the test output directory.
*
* @param testClassesDirectory the test output directory (e.g. target/test-classes)
* @return the parsed file, or null if it does not exist
* @throws IOException if the file exists but cannot be read
*/
public static ModuleInfoPatchArgsFile load(File testClassesDirectory) throws IOException {
if (testClassesDirectory == null || !testClassesDirectory.isDirectory()) {
return null;
}
return parse(new File(testClassesDirectory, RELATIVE_PATH));
}

/**
* Parses the given handoff file.
*
* @param file the module-info-patch.args file
* @return the parsed file, or null if it does not exist
* @throws IOException if the file exists but cannot be read
*/
public static ModuleInfoPatchArgsFile parse(File file) throws IOException {
if (file == null || !file.isFile()) {
return null;
}

List<String> lines = new ArrayList<>();
Set<String> addedModules = new LinkedHashSet<>();
try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
lines.add(line);
}
}
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
String value = optionValue(line, "--add-modules", lines, i);
if (value != null) {
for (String module : value.split(",")) {
String name = module.trim();
if (!name.isEmpty()) {
addedModules.add(name);
}
}
}
}
return new ModuleInfoPatchArgsFile(file, unmodifiableList(lines), unmodifiableSet(addedModules));
}

/**
* The value of the given option at index {@code i}, supporting both the same-line form
* ({@code --add-modules a,b}) and the two-line argfile form (value on the next line).
*
* @return the option value, or null if the line is not the given option
*/
private static String optionValue(String line, String option, List<String> lines, int i) {
if (line.equals(option)) {
return i + 1 < lines.size() ? lines.get(i + 1) : null;
}
if (line.startsWith(option) && Character.isWhitespace(line.charAt(option.length()))) {
return line.substring(option.length()).trim();
}
return null;
}

public File getFile() {
return file;
}

/**
* @return all non-empty, non-comment lines of the file, in order
*/
public List<String> getLines() {
return lines;
}

/**
* Module names listed by the file's {@code --add-modules} directives — with
* {@code module-info-patch.maven}'s {@code add-modules TEST-MODULE-PATH} these are the
* test-scope dependencies that belong on the module path.
*
* @return the module names, in file order
*/
public Set<String> getAddedModules() {
return addedModules;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,32 @@
*/
package org.apache.maven.plugin.surefire;

import java.util.List;

import org.codehaus.plexus.languages.java.jpms.ResolvePathResult;

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;

/**
* Wraps {@link ResolvePathResult} and place marker.
*/
final class ResolvePathResultWrapper {
private final ResolvePathResult resolvePathResult;
private final boolean isMainModuleDescriptor;
private final List<ResolvePathResult> additionalResults;

ResolvePathResultWrapper(ResolvePathResult resolvePathResult, boolean isMainModuleDescriptor) {
this(resolvePathResult, isMainModuleDescriptor, emptyList());
}

ResolvePathResultWrapper(
ResolvePathResult resolvePathResult,
boolean isMainModuleDescriptor,
List<ResolvePathResult> additionalResults) {
this.resolvePathResult = resolvePathResult;
this.isMainModuleDescriptor = isMainModuleDescriptor;
this.additionalResults = additionalResults;
}

ResolvePathResult getResolvePathResult() {
Expand All @@ -42,4 +56,15 @@ ResolvePathResult getResolvePathResult() {
boolean isMainModuleDescriptor() {
return isMainModuleDescriptor;
}

/**
* Descriptors of further Java modules beyond the primary one, present when a Maven 4
* module source hierarchy build produces several modules under one build output directory
* ({@code target/classes/<module>/}).
*
* @return additional module descriptors, empty for single-module or flat layouts
*/
List<ResolvePathResult> getAdditionalResults() {
return unmodifiableList(additionalResults);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.maven.plugin.surefire.ModuleInfoPatchArgsFile;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Commandline;
import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
Expand Down Expand Up @@ -127,6 +130,23 @@ protected void resolveClasspath(
}
}

/**
* Writes the Java argument file for the forked JVM: module path, classpath,
* {@code --patch-module} for the module under test and the related module options.
*
* @param moduleName the name of the module under test (from the main module descriptor)
* @param modulePath the module path elements
* @param classPath the classpath elements
* @param packages the test packages to open for reflective access
* @param patchFile the directory with the compiled test classes patched into the module
* under test: the test output directory itself, or one of its per-module
* subdirectories for a Maven 4 module source hierarchy build
* @param startClassName the fork's main class
* @param isMainDescriptor whether the module descriptor stems from the main build output
* @param providerJpmsArguments additional Java Modules arguments required by the provider
* @return the created argument file
* @throws IOException if the file cannot be written
*/
@Nonnull
File createArgsFile(
@Nonnull String moduleName,
Expand Down Expand Up @@ -174,6 +194,7 @@ File createArgsFile(
args.append('"').append(NL);
}

ModuleInfoPatchArgsFile patchArgs = null;
if (isMainDescriptor) {
args.append("--patch-module")
.append(NL)
Expand All @@ -184,27 +205,56 @@ File createArgsFile(
.append('"')
.append(NL);

for (String pkg : packages) {
args.append("--add-opens")
// module-info-patch.args generated by maven-compiler-plugin 4.x
Path patchArgsPath = findModuleInfoPatchArgs(patchFile);
if (patchArgsPath != null) {
patchArgs = ModuleInfoPatchArgsFile.parse(patchArgsPath.toFile());
}
if (patchArgs != null) {
// #3090: the developer-controlled handoff file is the
// single source of truth — pass it through verbatim. Its --add-modules
// dependencies are named modules on the module path (moved there by
// AbstractSurefireMojo), so the directives resolve in the boot layer.
for (String line : patchArgs.getLines()) {
args.append(line).append(NL);
}
// the patched module itself must still be a resolution root
args.append("--add-modules").append(NL).append(moduleName).append(NL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe replace the first NL by a space for consistency with the options added above. The goal is to make the file not only machine readable, but also human readable. I think that it is a little bit easier to understand when we have one option (including its value) per line.

}

if (patchArgs == null) {
// Without module-info-patch.args, auto-generate --add-opens for test
// packages (JUnit needs reflection access; module-info-patch.maven cannot
// express ALL-UNNAMED opens) and --add-reads. With the file present these
// arrive as pass-through arguments from AbstractSurefireMojo, which knows
// the modules moved to the module path.
for (String pkg : packages) {
args.append("--add-opens")
.append(NL)
.append(moduleName)
.append('/')
.append(pkg)
.append('=')
.append("ALL-UNNAMED")
.append(NL);
}

args.append("--add-reads")
.append(NL)
.append(moduleName)
.append('/')
.append(pkg)
.append('=')
.append("ALL-UNNAMED")
.append(NL);
}
}

args.append("--add-reads")
if (patchArgs == null) {
args.append("--add-modules")
.append(NL)
.append(moduleName)
.append('=')
.append("ALL-UNNAMED")
.append("ALL-MODULE-PATH")
.append(NL);
}

args.append("--add-modules").append(NL).append("ALL-MODULE-PATH").append(NL);

for (String[] entries : providerJpmsArguments) {
for (String entry : entries) {
args.append(entry).append(NL);
Expand All @@ -224,4 +274,34 @@ File createArgsFile(
return surefireArgs;
}
}

/**
* Locates the module-info-patch.args file generated by maven-compiler-plugin 4.x.
* There is exactly one such file per project, at
* {@code target/test-classes/META-INF/maven/module-info-patch.args}. The patch
* directory passed in is either the test output directory itself (classic layout) or
* one of its per-module subdirectories (Maven 4 module source hierarchy), which is why
* the file is looked up in the given directory and in its parent.
*
* @param patchFile the patch directory: the test output directory or a nested module directory
* @return the module-info-patch.args file, or null if not found
*/
@Nullable
private static Path findModuleInfoPatchArgs(File patchFile) {
Path patchDir = patchFile.toPath();
// classic layout: patchDir == target/test-classes
Path argsFile = patchDir.resolve("META-INF/maven/module-info-patch.args");
if (Files.isRegularFile(argsFile)) {
return argsFile;
}
// module source hierarchy: patchDir == target/test-classes/<module>
Path parent = patchDir.getParent();
if (parent != null) {
argsFile = parent.resolve("META-INF/maven/module-info-patch.args");
if (Files.isRegularFile(argsFile)) {
return argsFile;
}
}
return null;
}
}
Loading
Loading