8379345: jpackage: Fix issues in tests to improve their flexibility

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-06 03:59:06 +00:00
parent ad0f078669
commit a1b4ad097e
8 changed files with 159 additions and 113 deletions

View File

@ -67,6 +67,7 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import jdk.internal.util.OSVersion;
import jdk.jpackage.internal.util.MemoizingSupplier;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
@ -233,7 +234,7 @@ import jdk.jpackage.internal.util.function.ThrowingSupplier;
* An untrusted certificate can NOT be used with /usr/bin/codesign. Use
*
* <pre>
* /usr/bin/security security add-trusted-cert -k foo.keychain cert.pem
* /usr/bin/security add-trusted-cert -k foo.keychain cert.pem
* </pre>
*
* command to add trusted certificate from "cert.pem" file to "foo.keychain"
@ -440,7 +441,21 @@ public final class MacSign {
}
Keychain unlock() {
createExecutor("unlock-keychain").execute();
var exec = createExecutor("unlock-keychain");
exec.execute();
if (UnlockKeychainWithOsascript.VALUE) {
exec = Executor.of("osascript")
.addArguments(SIGN_UTILS_SCRIPT.toString(), "run-shell-script")
.addArgument(Stream.concat(
Stream.of(exec.getExecutable().orElseThrow().toString()),
exec.getAllArguments().stream()
).collect(joining(" ")));
exec.execute();
}
return this;
}
@ -576,29 +591,43 @@ public final class MacSign {
}
private static CertificateStats create(KeychainWithCertsSpec spec) {
final var allCertificates = spec.keychain().findCertificates();
final List<ResolvedCertificateRequest> allResolvedCertificateRequests = new ArrayList<>();
final Map<X509Certificate, Exception> unmappedCertificates = new HashMap<>();
withTempDirectory(workDir -> {
for (final var cert : allCertificates) {
ResolvedCertificateRequest resolvedCertificateRequest;
try {
resolvedCertificateRequest = new ResolvedCertificateRequest(cert);
} catch (RuntimeException ex) {
unmappedCertificates.put(cert, ExceptionBox.unbox(ex));
continue;
}
final Runnable workload = () -> {
final var allCertificates = spec.keychain().findCertificates();
withTempDirectory(workDir -> {
for (final var cert : allCertificates) {
ResolvedCertificateRequest resolvedCertificateRequest;
try {
resolvedCertificateRequest = new ResolvedCertificateRequest(cert);
} catch (RuntimeException ex) {
unmappedCertificates.put(cert, ExceptionBox.unbox(ex));
continue;
}
if (spec.certificateRequests().stream().anyMatch(resolvedCertificateRequest.installed()::match)) {
final var certFile = workDir.resolve(CertificateHash.of(cert).toString() + ".pem");
final var verifyStatus = verifyCertificate(resolvedCertificateRequest, spec.keychain(), certFile);
resolvedCertificateRequest = resolvedCertificateRequest.copyVerified(verifyStatus);
}
if (spec.certificateRequests().stream().anyMatch(resolvedCertificateRequest.installed()::match)) {
final var certFile = workDir.resolve(CertificateHash.of(cert).toString() + ".pem");
final var verifyStatus = verifyCertificate(resolvedCertificateRequest, spec.keychain(), certFile);
resolvedCertificateRequest = resolvedCertificateRequest.copyVerified(verifyStatus);
}
allResolvedCertificateRequests.add(resolvedCertificateRequest);
}
});
allResolvedCertificateRequests.add(resolvedCertificateRequest);
}
});
};
// Starting from some macOS version, it is no longer necessary to have the keychain
// in the list of keychains when running the "/usr/bin/security verify-cert ..." command to verify its certificate(s).
// The exact version when they relaxed this constraint is unknown, but it is still required on Catalina 10.15.7.
// On Catalina, if the keychain is not in the list of keychains, "/usr/bin/security verify-cert ..." command
// executed on the certificates of this keychain returns "untrusted" result.
if (OSVersion.current().compareTo(new OSVersion(10, 16)) < 0) {
// macOS Catalina or older
withKeychains(spec).addToSearchList().run(workload);
} else {
workload.run();
}
return new CertificateStats(allResolvedCertificateRequests,
List.copyOf(spec.certificateRequests()), unmappedCertificates);
@ -1272,7 +1301,11 @@ public final class MacSign {
for (final var quite : List.of(true, false)) {
result = security("verify-cert", "-L", "-n",
quite ? "-q" : "-v",
"-c", certFile.normalize().toString(),
// Use "-r" option to verify the certificate against itself.
// "-c" option works on newer macOS versions, but on older ones (at least on Catalina 10.15.7),
// in case there are two self-signed certificates with the same name in the given keychain,
// it links them in the certificate list and fails verification.
"-r", certFile.normalize().toString(),
"-k", keychain.name(),
"-p", resolvedCertificateRequest.installed().type().verifyPolicy()).saveOutput(!quite).executeWithoutExitCodeCheck();
if (result.getExitCode() == 0) {
@ -1463,4 +1496,10 @@ public final class MacSign {
// faketime is not a standard macOS command.
// One way to get it is with Homebrew.
private static final Path FAKETIME = Path.of(Optional.ofNullable(TKit.getConfigProperty("faketime")).orElse("/usr/local/bin/faketime"));
// Run the "/usr/bin/system unlock-keychain" command in Terminal.app if
// the current process runs in an SSH session and the OS version is macOS Catalina or older.
private static final class UnlockKeychainWithOsascript {
static final boolean VALUE = System.getenv("SSH_CONNECTION") != null && OSVersion.current().compareTo(new OSVersion(10, 16)) < 0;
}
}

View File

@ -93,8 +93,11 @@ public final class MacSignVerify {
final var exec = Executor.of(
"/usr/bin/codesign",
"-d",
"--entitlements", "-",
"--xml", path.toString()).saveOutput().dumpOutput().binaryOutput();
// `--entitlements :-` will print entitlements as XML plist in the stdout and "Executable=..." message to the stderr.
// Prefer this option combination to `--entitlements - --xml` as
// the latter doesn't work on older macOS releases (Proved unsupported on Catalina 10.15.7).
"--entitlements", ":-",
path.toString()).saveOutput().dumpOutput().binaryOutput();
final var result = exec.execute();
var xml = result.byteStdout();
if (xml.length == 0) {

View File

@ -37,7 +37,6 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
@ -606,25 +605,6 @@ public final class TKit {
return JtregSkippedExceptionClass.INSTANCE.isInstance(t);
}
public static Path createRelativePathCopy(final Path file) {
Path fileCopy = ThrowingSupplier.toSupplier(() -> {
Path localPath = createTempFile(file.getFileName());
Files.copy(file, localPath, StandardCopyOption.REPLACE_EXISTING);
return localPath;
}).get().toAbsolutePath().normalize();
final Path basePath = Path.of(".").toAbsolutePath().normalize();
try {
return basePath.relativize(fileCopy);
} catch (IllegalArgumentException ex) {
// May happen on Windows: java.lang.IllegalArgumentException: 'other' has different root
trace(String.format("Failed to relativize [%s] at [%s]", fileCopy,
basePath));
printStackTrace(ex);
}
return file;
}
public static void waitForFileCreated(Path fileToWaitFor,
Duration timeout, Duration afterCreatedTimeout) throws IOException {
waitForFileCreated(fileToWaitFor, timeout);

View File

@ -154,6 +154,7 @@ public class MainTest extends JUnitAdapter {
JPackageCommand.helloAppImage()
.ignoreDefaultVerbose(true)
.ignoreDefaultRuntime(true)
.useToolProvider(jpackageToolProviderMock)
.execute(jpackageExitCode);
}

View File

@ -200,7 +200,7 @@ public class SigningRuntimeImagePackageTest {
// This way we can test if jpackage keeps or replaces the signature of
// the predefined runtime bundle when backing it in the pkg or dmg installer.
return new SignKeyOptionWithKeychain(
SignKeyOption.Type.SIGN_KEY_USER_SHORT_NAME,
SignKeyOption.Type.SIGN_KEY_IDENTITY_APP_IMAGE,
SigningBase.StandardCertificateRequest.CODESIGN_ACME_TECH_LTD,
SigningBase.StandardKeychain.MAIN.keychain());
}

View File

@ -23,6 +23,8 @@
import static java.util.Map.entry;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import jdk.jpackage.test.Annotations.Parameter;
@ -84,7 +86,7 @@ public class FileAssociationsTest {
@Test
@Parameter("true")
@Parameter("false")
public static void test(boolean includeDescription) {
public static void test(boolean includeDescription) throws IOException {
PackageTest packageTest = new PackageTest();
// Not supported
@ -96,10 +98,8 @@ public class FileAssociationsTest {
}
fa.applyTo(packageTest);
Path icon = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "icon"
+ TKit.ICON_SUFFIX));
icon = TKit.createRelativePathCopy(icon);
var icon = TKit.createTempDirectory("icon-dir").resolve(ICON.getFileName());
Files.copy(ICON, icon);
new FileAssociations("jptest2")
.setFilename("fa2")
@ -151,4 +151,6 @@ public class FileAssociationsTest {
.addInitializer(JPackageCommand::setFakeRuntime)
.setExpectedExitCode(1);
}
private static final Path ICON = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "icon" + TKit.ICON_SUFFIX));
}

View File

@ -21,21 +21,23 @@
* questions.
*/
import static jdk.internal.util.OperatingSystem.LINUX;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static jdk.internal.util.OperatingSystem.LINUX;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.LinuxHelper;
import jdk.jpackage.test.Executor;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.LinuxHelper;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
/**
@ -93,11 +95,7 @@ public class LicenseTest {
@Test
public static void testCommon() {
PackageTest test = new PackageTest().configureHelloApp()
.addInitializer(cmd -> {
cmd.addArguments("--license-file", TKit.createRelativePathCopy(
LICENSE_FILE));
});
PackageTest test = new PackageTest().configureHelloApp().mutate(LicenseTest::setLicenseFile);
initMacDmgLicenseVerifier(test.forTypes(PackageType.MAC_DMG));
initLinuxLicenseVerifier(test.forTypes(PackageType.LINUX));
@ -127,19 +125,16 @@ public class LicenseTest {
@Test(ifOS = LINUX)
public static void testCustomDebianCopyright() {
new CustomDebianCopyrightTest().run();
new CustomDebianCopyrightTest(false).run();
}
@Test(ifOS = LINUX)
public static void testCustomDebianCopyrightSubst() {
new CustomDebianCopyrightTest().withSubstitution(true).run();
new CustomDebianCopyrightTest(true).run();
}
private static PackageTest initMacDmgLicenseVerifier(PackageTest test) {
return test
.addBundleVerifier(cmd -> {
verifyLicenseFileInDMGPackage(cmd);
});
return test.addBundleVerifier(LicenseTest::verifyLicenseFileInDMGPackage);
}
private static void verifyLicenseFileInDMGPackage(JPackageCommand cmd)
@ -179,10 +174,9 @@ public class LicenseTest {
PackageTest test = new PackageTest()
.forTypes(PackageType.LINUX)
.configureHelloApp()
.addInitializer(JPackageCommand::setFakeRuntime)
.mutate(LicenseTest::setLicenseFile)
.addInitializer(cmd -> {
cmd.setFakeRuntime();
cmd.addArguments("--license-file", TKit.createRelativePathCopy(
LICENSE_FILE));
cmd.addArguments("--install-dir", installDir);
});
@ -191,6 +185,18 @@ public class LicenseTest {
test.run();
}
private static void setLicenseFile(PackageTest test) {
var inputLicenseFile = Slot.<Path>createEmpty();
test.addRunOnceInitializer(() -> {
var dir = TKit.createTempDirectory("license-dir");
inputLicenseFile.set(dir.resolve(LICENSE_FILE.getFileName()));
Files.copy(LICENSE_FILE, inputLicenseFile.get());
}).addInitializer(cmd -> {
cmd.setArgumentValue("--license-file", inputLicenseFile.get());
});
}
private static Path rpmLicenseFile(JPackageCommand cmd) {
final Path licenseRoot = Path.of(
new Executor()
@ -296,12 +302,36 @@ public class LicenseTest {
TKit.assertPathExists(licenseFile.getParent(), false);
}
private static class CustomDebianCopyrightTest {
CustomDebianCopyrightTest() {
withSubstitution(false);
private record CustomDebianCopyrightTest(boolean withSubstitution) {
private String copyright() {
// Different values just to make easy to figure out from the test log which test was executed.
if (withSubstitution) {
return "Duke (C)";
} else {
return "Java (C)";
}
}
private List<String> licenseFileText(String copyright, String licenseText) {
private String licenseText() {
// Different values just to make easy to figure out from the test log which test was executed.
if (withSubstitution) {
return "The quick brown fox\n jumps over the lazy dog";
} else {
return "How vexingly quick daft zebras jump!";
}
}
private String name() {
// Different values just to make easy to figure out from the test log which test was executed.
if (withSubstitution) {
return "CustomDebianCopyrightWithSubst";
} else {
return "CustomDebianCopyright";
}
}
static private List<String> licenseFileText(String copyright, String licenseText) {
List<String> lines = new ArrayList<>(List.of(
String.format("Copyright=%s", copyright),
"Foo",
@ -313,28 +343,14 @@ public class LicenseTest {
private List<String> licenseFileText() {
if (withSubstitution) {
return licenseFileText("APPLICATION_COPYRIGHT",
"APPLICATION_LICENSE_TEXT");
return licenseFileText("APPLICATION_COPYRIGHT", "APPLICATION_LICENSE_TEXT");
} else {
return expectedLicenseFileText();
}
}
private List<String> expectedLicenseFileText() {
return licenseFileText(copyright, licenseText);
}
CustomDebianCopyrightTest withSubstitution(boolean v) {
withSubstitution = v;
// Different values just to make easy to figure out from the test log which test was executed.
if (v) {
copyright = "Duke (C)";
licenseText = "The quick brown fox\n jumps over the lazy dog";
} else {
copyright = "Java (C)";
licenseText = "How vexingly quick daft zebras jump!";
}
return this;
return licenseFileText(copyright(), licenseText());
}
void run() {
@ -343,20 +359,19 @@ public class LicenseTest {
.addInitializer(cmd -> {
// Create source license file.
Files.write(srcLicenseFile, List.of(
licenseText.split("\\R", -1)));
licenseText().split("\\R", -1)));
cmd.setFakeRuntime();
cmd.setArgumentValue("--name", String.format("%s%s",
withSubstitution ? "CustomDebianCopyrightWithSubst" : "CustomDebianCopyright",
cmd.name()));
cmd.setArgumentValue("--name", String.format("%s%s", name(), cmd.name()));
cmd.addArguments("--license-file", srcLicenseFile);
cmd.addArguments("--copyright", copyright);
cmd.addArguments("--resource-dir", RESOURCE_DIR);
cmd.addArguments("--copyright", copyright());
var resourceDir = TKit.createTempDirectory("resources");
cmd.addArguments("--resource-dir", resourceDir);
// Create copyright template file in a resource dir.
Files.createDirectories(RESOURCE_DIR);
Files.write(RESOURCE_DIR.resolve("copyright"),
licenseFileText());
Files.write(resourceDir.resolve("copyright"), licenseFileText());
})
.addInstallVerifier(cmd -> {
Path installedLicenseFile = linuxLicenseFile(cmd);
@ -368,12 +383,6 @@ public class LicenseTest {
})
.run();
}
private boolean withSubstitution;
private String copyright;
private String licenseText;
private final Path RESOURCE_DIR = TKit.workDir().resolve("resources");
}
private static final Path LICENSE_FILE = TKit.TEST_SRC_ROOT.resolve(

View File

@ -21,13 +21,15 @@
* questions.
*/
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Annotations.Parameters;
import java.util.List;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
@ -108,11 +110,7 @@ public class WinInstallerUiTest {
}
if (withLicense) {
test.addInitializer(cmd -> {
cmd.addArguments("--license-file", TKit.createRelativePathCopy(
TKit.TEST_SRC_ROOT.resolve(Path.of("resources",
"license.txt"))));
});
setLicenseFile(test);
}
test.run();
@ -133,7 +131,21 @@ public class WinInstallerUiTest {
cmd.setArgumentValue("--name", sb.toString());
}
private static void setLicenseFile(PackageTest test) {
var inputLicenseFile = Slot.<Path>createEmpty();
test.addRunOnceInitializer(() -> {
var dir = TKit.createTempDirectory("license-dir");
inputLicenseFile.set(dir.resolve(LICENSE_FILE.getFileName()));
Files.copy(LICENSE_FILE, inputLicenseFile.get());
}).addInitializer(cmd -> {
cmd.setArgumentValue("--license-file", inputLicenseFile.get());
});
}
private final boolean withDirChooser;
private final boolean withLicense;
private final boolean withShortcutPrompt;
private static final Path LICENSE_FILE = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "license.txt"));
}