java-topology/test/jdk/tools/jpackage/share/ModularAppTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

336 lines
12 KiB
Java

/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import static jdk.jpackage.internal.util.MemoizingSupplier.runOnce;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.xml.xpath.XPathExpressionException;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.internal.util.TokenReplace;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.Executor;
import jdk.jpackage.test.HelloApp;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JavaAppDesc;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
/*
* @test
* @summary jpackage bundling modular app
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror ModularAppTest.java
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=ModularAppTest
*/
public final class ModularAppTest {
@Test
@ParameterSupplier
public void test(TestSpec spec) {
spec.run();
}
/**
* Test case for JDK-8233265. Adding modules in .jmod files for non-modular app
* results in unexpected jpackage failure.
*/
@Test
@Parameter("Hello!")
@Parameter("com.foo/com.foo.ModuleApp")
public void test8233265(String mainAppDesc) throws IOException {
JPackageCommand cmd = JPackageCommand.helloAppImage(mainAppDesc);
// The test should make jpackage invoke jlink.
cmd.ignoreDefaultRuntime(true);
var modulePath = Optional.ofNullable(cmd.getArgumentValue("--module-path")).map(Path::of).orElseGet(() -> {
var newModulePath = TKit.createTempDirectory("input-modules");
cmd.addArguments("--module-path", newModulePath);
return newModulePath;
});
JavaAppDesc extraModule = JavaAppDesc.parse("x.jmod:com.x/com.x.Y");
HelloApp.createBundle(extraModule, modulePath);
cmd.addArguments("--add-modules", extraModule.moduleName());
cmd.executeAndAssertHelloAppImageCreated();
}
/**
* Test case for JDK-8248254. App's module in the predefined runtime directory;
* no "--module-path" option on the command line. jpackage should find the app's
* module in the predefined runtime.
*/
@Test
@Parameter("IMAGE")
@Parameter(value = "MAC_BUNDLE", ifOS = OperatingSystem.MACOS)
public void test8248254(RuntimeType runtimeType) throws XPathExpressionException, IOException {
final var appDesc = JavaAppDesc.parse("me.mymodule/me.mymodule.Main");
new JPackageCommand()
.setDefaultAppName()
.setPackageType(PackageType.IMAGE)
.setDefaultInputOutput()
.removeArgumentWithValue("--input")
.addArguments("--module", appDesc.moduleName() + "/" + appDesc.className())
.setArgumentValue("--runtime-image", bakeModuleInRuntime(appDesc, runtimeType))
.executeAndAssertHelloAppImageCreated();
}
/**
* Test case for JDK-8261518. App's module is baked into the predefined runtime
* image with jlink; no "--module-path" option on the command line. If there is
* a non-modular jar in the current directory, jpackage used to throw.
*/
@Test
@Parameter("IMAGE")
@Parameter(value = "MAC_BUNDLE", ifOS = OperatingSystem.MACOS)
public void test8261518(RuntimeType runtimeType) throws XPathExpressionException, IOException {
final var appDesc = JavaAppDesc.parse("com.foo/com.foo.main.Aloha");
final var fooJarDir = TKit.createTempDirectory("foo");
// Create "foo.jar" in dedicated directory.
HelloApp.createBundle(JavaAppDesc.parse("foo.jar:"), fooJarDir);
new JPackageCommand()
.setDefaultAppName()
.setPackageType(PackageType.IMAGE)
.setDefaultInputOutput()
.removeArgumentWithValue("--input")
.addArguments("--module", appDesc.moduleName() + "/" + appDesc.className())
.setArgumentValue("--runtime-image", bakeModuleInRuntime(appDesc, runtimeType).toAbsolutePath())
// Run jpackage in the directory with "foo.jar"
.setDirectory(fooJarDir).useToolProvider(false)
.executeAndAssertHelloAppImageCreated();
}
private static Path bakeModuleInRuntime(JavaAppDesc appDesc, RuntimeType runtimeType) throws IOException {
final var moduleOutputDir = TKit.createTempDirectory("modules");
HelloApp.createBundle(appDesc, moduleOutputDir);
final var workDir = TKit.createTempDirectory("runtime").resolve("data");
final Path jlinkOutputDir;
switch (runtimeType) {
case IMAGE -> {
jlinkOutputDir = workDir;
}
case MAC_BUNDLE -> {
var macBundle = new MacBundle(workDir);
// Create macOS bundle structure sufficient to pass jpackage validation.
Files.createDirectories(macBundle.homeDir().getParent());
Files.createDirectories(macBundle.macOsDir());
Files.createFile(macBundle.infoPlistFile());
jlinkOutputDir = macBundle.homeDir();
}
default -> {
throw new AssertionError();
}
}
// List of modules required for the test app.
final var modules = new String[] {
"java.base",
"java.desktop",
appDesc.moduleName()
};
new Executor()
.setToolProvider(JavaTool.JLINK)
.dumpOutput()
.addArguments(
"--add-modules", String.join(",", modules),
"--output", jlinkOutputDir.toString(),
"--module-path", moduleOutputDir.resolve(appDesc.jarFileName()).toString(),
"--strip-debug",
"--no-header-files",
"--no-man-pages",
"--strip-native-commands")
.execute();
return workDir;
}
enum RuntimeType {
IMAGE,
MAC_BUNDLE,
;
}
record TestSpec(List<String> paths, String appDesc) {
TestSpec {
Objects.requireNonNull(paths);
Objects.requireNonNull(appDesc);
Objects.requireNonNull(JavaAppDesc.parse(appDesc).moduleName());
}
boolean isWithGoodPath() {
var isWithGoodPath = Slot.<Boolean>createEmpty();
paths.forEach(path -> {
ALL_TOKENS.applyTo(path, token -> {
if (token.equals(Token.GOOD_DIR.token())) {
isWithGoodPath.set(true);
}
return token;
});
});
return isWithGoodPath.find().isPresent();
}
void run() {
var emptyDir = runOnce(() -> {
return TKit.createTempDirectory("empty-dir");
});
var nonExistingDir = runOnce(() -> {
return TKit.withTempDirectory("non-existing-dir", x -> {});
});
var goodDir = runOnce(() -> {
return TKit.createTempDirectory("modules");
});
var theAppDesc = JavaAppDesc.parse(appDesc);
HelloApp.createBundle(theAppDesc, goodDir.get());
var cmd = new JPackageCommand()
.setArgumentValue("--dest", TKit.workDir().resolve("output"))
.setDefaultAppName()
.setPackageType(PackageType.IMAGE)
// Ignore runtime that can be set for all tests. Usually if default
// runtime is set, it is fake one to save time on running jlink and
// copying megabytes of data from Java home to application image.
// We need proper runtime for this test.
.ignoreDefaultRuntime(true)
.addArguments("--module", String.join("/", theAppDesc.moduleName(), theAppDesc.className()));
if (TKit.isWindows()) {
cmd.addArguments("--win-console");
}
paths.stream().map(path -> {
return ALL_TOKENS.applyTo(path, token -> {
return (switch (Token.valueOf(token.substring(2, token.length() - 2))) {
case EMPTY_DIR -> {
yield emptyDir;
}
case GOOD_DIR -> {
yield goodDir;
}
case NON_EXISTING_DIR -> {
yield nonExistingDir;
}
default -> {
throw new AssertionError();
}
}).get();
});
}).<String>mapMulti((path, acc) -> {
acc.accept("--module-path");
acc.accept(path);
}).forEach(cmd::addArgument);
if (isWithGoodPath()) {
cmd.executeAndAssertHelloAppImageCreated();
} else {
final CannedFormattedString expectedErrorMessage;
if (paths.isEmpty()) {
expectedErrorMessage = JPackageCommand.makeError(
"ERR_MissingArgument2", "--runtime-image", "--module-path");
} else {
expectedErrorMessage = JPackageCommand.makeError(
"error.no-module-in-path", theAppDesc.moduleName());
}
cmd.validateErr(expectedErrorMessage).execute(1);
}
}
private static final TokenReplace ALL_TOKENS =
new TokenReplace(Stream.of(Token.values()).map(Token::token).toArray(String[]::new));
}
public static Collection<?> test() {
var testCases = new ArrayList<TestSpec>();
for (String appDesc : List.of(
"benvenuto.jar:com.jar.foo/com.jar.foo.Hello",
"benvenuto.jmod:com.jmod.foo/com.jmod.foo.JModHello"
)) {
Stream.<Stream<String>>of(
Stream.<Token>of(Token.GOOD_DIR, Token.EMPTY_DIR, Token.NON_EXISTING_DIR).map(Token::token),
Stream.<Token>of(Token.EMPTY_DIR, Token.NON_EXISTING_DIR, Token.GOOD_DIR).map(Token::token),
Stream.<String>of(Token.GOOD_DIR.token() + "/a/b/c/d", Token.GOOD_DIR.token()),
Stream.<String>of(),
Stream.<Token>of(Token.EMPTY_DIR).map(Token::token)
).map(Stream::toList).map(paths -> {
return new TestSpec(paths, appDesc);
}).forEach(testCases::add);
}
return testCases.stream().map(v -> {
return new Object[] {v};
}).toList();
}
enum Token {
GOOD_DIR,
EMPTY_DIR,
NON_EXISTING_DIR,
;
String token() {
return makeToken(name());
}
private static String makeToken(String v) {
Objects.requireNonNull(v);
return String.format("@@%s@@", v);
}
}
}