diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java new file mode 100644 index 00000000000..433db6fc6f6 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java @@ -0,0 +1,894 @@ +/* + * Copyright (c) 2025, 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. + */ +package jdk.jpackage.test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.internal.util.function.ThrowingRunnable; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.PackageTest.PackageHandlers; +import jdk.jpackage.test.RunnablePackageTest.Action; + +public class PackageTestTest extends JUnitAdapter { + + private interface Verifiable { + void verify(); + } + + private static class CallbackFactory { + + CallbackFactory(int tickCount) { + this.tickCount = tickCount; + } + + CountingInstaller createInstaller(int exitCode) { + return new CountingInstaller(tickCount, exitCode); + } + + CountingConsumer createUninstaller() { + return new CountingConsumer(tickCount, "uninstall"); + } + + CountingUnpacker createUnpacker() { + return new CountingUnpacker(tickCount); + } + + CountingConsumer createInitializer() { + return new CountingConsumer(tickCount, "init"); + } + + CountingRunnable createRunOnceInitializer() { + return new CountingRunnable(tickCount, "once-init"); + } + + CountingConsumer createInstallVerifier() { + return new CountingConsumer(tickCount, "on-install"); + } + + CountingConsumer createUninstallVerifier() { + return new CountingConsumer(tickCount, "on-uninstall"); + } + + CountingConsumer createBundleVerifier() { + return new CountingConsumer(tickCount, "on-bundle"); + } + + CountingBundleVerifier createBundleVerifier(int jpackageExitCode) { + return new CountingBundleVerifier(tickCount, jpackageExitCode); + } + + private final int tickCount; + } + + private final static int ERROR_EXIT_CODE_JPACKAGE = 35; + private final static int ERROR_EXIT_CODE_INSTALL = 27; + + private final static CallbackFactory NEVER = new CallbackFactory(0); + private final static CallbackFactory ONCE = new CallbackFactory(1); + private final static CallbackFactory TWICE = new CallbackFactory(2); + + enum BundleVerifier { + ONCE_SUCCESS(ONCE), + ONCE_FAIL(ONCE), + NEVER(PackageTestTest.NEVER), + ONCE_SUCCESS_EXIT_CODE(ONCE, 0), + ONCE_FAIL_EXIT_CODE(ONCE, ERROR_EXIT_CODE_JPACKAGE), + NEVER_EXIT_CODE(PackageTestTest.NEVER, 0); + + BundleVerifier(CallbackFactory factory) { + specSupplier = () -> new BundleVerifierSpec(Optional.of(factory.createBundleVerifier()), Optional.empty()); + } + + BundleVerifier(CallbackFactory factory, int jpackageExitCode) { + specSupplier = () -> new BundleVerifierSpec(Optional.empty(), + Optional.of(factory.createBundleVerifier(jpackageExitCode))); + } + + BundleVerifierSpec spec() { + return specSupplier.get(); + } + + private final Supplier specSupplier; + } + + private static class TickCounter implements Verifiable { + + TickCounter(int expectedTicks) { + this.expectedTicks = expectedTicks; + } + + void tick() { + ticks++; + } + + @Override + public void verify() { + switch (expectedTicks) { + case 0 -> { + TKit.assertEquals(expectedTicks, ticks, String.format("%s: never called", this)); + } + case 1 -> { + TKit.assertEquals(expectedTicks, ticks, String.format("%s: called once", this)); + } + case 2 -> { + TKit.assertEquals(expectedTicks, ticks, String.format("%s: called twice", this)); + } + default -> { + TKit.assertEquals(expectedTicks, ticks, toString()); + } + } + } + + protected int tickCount() { + return ticks; + } + + protected static String getDescription(TickCounter o) { + return "tk=" + o.expectedTicks; + } + + private int ticks; + protected final int expectedTicks; + } + + private static class CountingConsumer extends TickCounter implements ThrowingConsumer { + + @Override + public void accept(JPackageCommand cmd) { + tick(); + } + + @Override + public String toString() { + return String.format("%s(%s)", label, TickCounter.getDescription(this)); + } + + CountingConsumer(int expectedTicks, String label) { + super(expectedTicks); + this.label = Objects.requireNonNull(label); + } + + private final String label; + } + + private static class CountingRunnable extends TickCounter implements ThrowingRunnable { + + @Override + public void run() { + tick(); + } + + @Override + public String toString() { + return String.format("%s(%s)", label, TickCounter.getDescription(this)); + } + + CountingRunnable(int expectedTicks, String label) { + super(expectedTicks); + this.label = Objects.requireNonNull(label); + } + + private final String label; + } + + private static class CountingBundleVerifier extends TickCounter implements ThrowingBiConsumer { + + @Override + public void accept(JPackageCommand cmd, Executor.Result result) { + tick(); + jpackageExitCode = result.exitCode(); + } + + @Override + public void verify() { + super.verify(); + if (expectedTicks > 0) { + TKit.assertEquals(expectedJPackageExitCode, jpackageExitCode, String.format("%s: run jpackage", this)); + } + } + + @Override + public String toString() { + return String.format("on-bundle-ex(exit=%d, %s)", expectedJPackageExitCode, TickCounter.getDescription(this)); + } + + CountingBundleVerifier(int expectedTicks, int expectedJPackageExitCode) { + super(expectedTicks); + this.expectedJPackageExitCode = expectedJPackageExitCode; + } + + private int jpackageExitCode; + private final int expectedJPackageExitCode; + } + + private final static class CountingInstaller extends TickCounter implements Function { + + @Override + public Integer apply(JPackageCommand cmd) { + tick(); + return exitCode; + } + + @Override + public String toString() { + return String.format("install(exit=%d, %s)", exitCode, TickCounter.getDescription(this)); + } + + CountingInstaller(int expectedTicks, int exitCode) { + super(expectedTicks); + this.exitCode = exitCode; + } + + private final int exitCode; + } + + private static class CountingUnpacker extends TickCounter implements BiFunction { + + @Override + public Path apply(JPackageCommand cmd, Path path) { + tick(); + try { + Files.createDirectories(path.resolve("mockup-installdir")); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + unpackPaths.add(path); + return path; + } + + @Override + public String toString() { + return String.format("unpack(%s)", TickCounter.getDescription(this)); + } + + CountingUnpacker(int expectedTicks) { + super(expectedTicks); + } + + List unpackPaths() { + return unpackPaths; + } + + private final List unpackPaths = new ArrayList<>(); + } + + record BundleVerifierSpec(Optional verifier, Optional verifierWithExitCode) { + BundleVerifierSpec { + if (verifier.isPresent() == verifierWithExitCode.isPresent()) { + throw new IllegalArgumentException(); + } + } + + Verifiable apply(PackageTest test) { + verifier.ifPresent(test::addBundleVerifier); + verifierWithExitCode.ifPresent(test::addBundleVerifier); + return verifier.map(Verifiable.class::cast).orElseGet(verifierWithExitCode::orElseThrow); + } + + @Override + public String toString() { + return verifier.map(Verifiable.class::cast).orElseGet(verifierWithExitCode::orElseThrow).toString(); + } + } + + record PackageHandlersSpec(CountingInstaller installer, CountingConsumer uninstaller, + Optional unpacker, int installExitCode) { + + PackageHandlers createPackageHandlers(Consumer verifiableAccumulator) { + List.of(installer, uninstaller).forEach(verifiableAccumulator::accept); + unpacker.ifPresent(verifiableAccumulator::accept); + return new PackageHandlers(installer, uninstaller::accept, unpacker); + } + } + + record TestSpec(PackageType type, PackageHandlersSpec handlersSpec, + List initializers, List bundleVerifierSpecs, + List installVerifiers, List uninstallVerifiers, + int expectedJPackageExitCode, int actualJPackageExitCode, List actions) { + + PackageTest createTest(Consumer verifiableAccumulator) { + return createTest(handlersSpec.createPackageHandlers(verifiableAccumulator)); + } + + PackageTest createTest(PackageHandlers handlers) { + return new PackageTest().jpackageFactory(() -> { + return new JPackageCommand() { + @Override + public Path outputBundle() { + return outputDir().resolve("mockup-bundle" + super.packageType().getSuffix()); + } + + @Override + public PackageType packageType() { + return null; + } + + @Override + JPackageCommand assertAppLayout() { + return this; + } + + @Override + JPackageCommand createImmutableCopy() { + return this; + } + + @Override + public void verifyIsOfType(PackageType ... types) { + } + + @Override + public String getPrintableCommandLine() { + return "'mockup jpackage'"; + } + + @Override + public Executor.Result execute(int expectedExitCode) { + final var outputBundle = outputBundle(); + try { + Files.createDirectories(outputBundle.getParent()); + if (actualJPackageExitCode == 0) { + Files.createFile(outputBundle); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return new Executor.Result(actualJPackageExitCode, null, + this::getPrintableCommandLine).assertExitCodeIs(expectedExitCode); + } + }; + }).setExpectedExitCode(expectedJPackageExitCode) + .setExpectedInstallExitCode(handlersSpec.installExitCode) + .isPackageTypeSupported(type -> true) + .forTypes().packageHandlers(handlers); + } + + void configureInitializers(PackageTest test, Consumer verifiableAccumulator) { + for (final var initializer : initializers) { + verifiableAccumulator.accept(initializer); + test.addInitializer(initializer); + } + } + + void configureBundleVerifiers(PackageTest test, Consumer verifiableAccumulator) { + for (final var verifierSpec : bundleVerifierSpecs) { + verifiableAccumulator.accept(verifierSpec.apply(test)); + } + } + + void configureInstallVerifiers(PackageTest test, Consumer verifiableAccumulator) { + for (final var verifier : installVerifiers) { + verifiableAccumulator.accept(verifier); + test.addInstallVerifier(verifier); + } + } + + void configureUninstallVerifiers(PackageTest test, Consumer verifiableAccumulator) { + for (final var verifier : uninstallVerifiers) { + verifiableAccumulator.accept(verifier); + test.addUninstallVerifier(verifier); + } + } + + void run(PackageTest test) { + final boolean expectedSuccess = (expectedJPackageExitCode == actualJPackageExitCode); + + TKit.assertAssert(expectedSuccess, () -> { + test.run(actions.toArray(Action[]::new)); + }); + } + + List run() { + return run(Optional.empty()); + } + + List run(Consumer customConfigure) { + return run(Optional.of(customConfigure)); + } + + private List run(Optional> customConfigure) { + final List verifiers = new ArrayList<>(); + + final var test = createTest(verifiers::add); + test.forTypes(type); + configureInitializers(test, verifiers::add); + configureBundleVerifiers(test, verifiers::add); + configureInstallVerifiers(test, verifiers::add); + configureUninstallVerifiers(test, verifiers::add); + customConfigure.ifPresent(callback -> callback.accept(test)); + run(test); + verifiers.forEach(Verifiable::verify); + return verifiers; + } + } + + private final static class TestSpecBuilder { + + TestSpecBuilder type(PackageType v) { + type = Objects.requireNonNull(v); + return this; + } + + TestSpecBuilder install(CallbackFactory v) { + install = Objects.requireNonNull(v); + return this; + } + + TestSpecBuilder uninstall(CallbackFactory v) { + uninstall = Objects.requireNonNull(v); + return this; + } + + TestSpecBuilder unpack(CallbackFactory v) { + unpack = v; + return this; + } + + TestSpecBuilder installExitCode(int v) { + installExitCode = v; + return this; + } + + TestSpecBuilder jpackageExitCode(int v) { + return expectedJPackageExitCode(v).actualJPackageExitCode(v); + } + + TestSpecBuilder expectedJPackageExitCode(int v) { + expectedJPackageExitCode = v; + return this; + } + + TestSpecBuilder actualJPackageExitCode(int v) { + actualJPackageExitCode = v; + return this; + } + + TestSpecBuilder addActions(Action... v) { + actions.addAll(List.of(v)); + return this; + } + + TestSpecBuilder actions(Action... v) { + actions.clear(); + return addActions(v); + } + + TestSpecBuilder doCreateAndUnpack() { + actions(Action.CREATE_AND_UNPACK); + install(NEVER); + uninstall(NEVER); + if (willHaveBundle()) { + overrideNonNullUnpack(ONCE); + } else { + overrideNonNullUnpack(NEVER); + } + initializers(ONCE); + if (expectedJPackageExitCode != actualJPackageExitCode) { + bundleVerifiers(BundleVerifier.NEVER.spec()); + } else if (expectedJPackageExitCode == 0) { + bundleVerifiers(BundleVerifier.ONCE_SUCCESS.spec()); + bundleVerifiers(BundleVerifier.ONCE_SUCCESS_EXIT_CODE.spec()); + } else { + bundleVerifiers(BundleVerifier.ONCE_FAIL.spec()); + if (expectedJPackageExitCode == ERROR_EXIT_CODE_JPACKAGE) { + bundleVerifiers(BundleVerifier.ONCE_FAIL_EXIT_CODE.spec()); + } + } + uninstallVerifiers(NEVER); + if (willVerifyUnpack()) { + installVerifiers(ONCE); + } else { + installVerifiers(NEVER); + } + return this; + } + + TestSpecBuilder doCreateUnpackInstallUninstall() { + actions(Action.CREATE, Action.UNPACK, Action.VERIFY_INSTALL, Action.INSTALL, + Action.VERIFY_INSTALL, Action.UNINSTALL, Action.VERIFY_UNINSTALL); + initializers(ONCE); + uninstallVerifiers(NEVER); + if (willHaveBundle()) { + overrideNonNullUnpack(ONCE); + install(ONCE); + if (installExitCode == 0) { + uninstall(ONCE); + uninstallVerifiers(ONCE); + } else { + uninstall(NEVER); + } + } else { + overrideNonNullUnpack(NEVER); + install(NEVER); + uninstall(NEVER); + } + + if (expectedJPackageExitCode != actualJPackageExitCode) { + bundleVerifiers(BundleVerifier.NEVER.spec()); + installVerifiers(NEVER); + } else if (expectedJPackageExitCode == 0) { + bundleVerifiers(BundleVerifier.ONCE_SUCCESS.spec()); + bundleVerifiers(BundleVerifier.ONCE_SUCCESS_EXIT_CODE.spec()); + if (installExitCode == 0) { + if (willVerifyUnpack()) { + installVerifiers(TWICE); + } else { + installVerifiers(ONCE); + } + } else { + if (willVerifyUnpack()) { + installVerifiers(ONCE); + } else { + installVerifiers(NEVER); + } + } + } else { + bundleVerifiers(BundleVerifier.ONCE_FAIL.spec()); + if (expectedJPackageExitCode == ERROR_EXIT_CODE_JPACKAGE) { + bundleVerifiers(BundleVerifier.ONCE_FAIL_EXIT_CODE.spec()); + } + installVerifiers(NEVER); + } + return this; + } + + TestSpecBuilder addInitializers(CallbackFactory... v) { + initializers.addAll(List.of(v)); + return this; + } + + TestSpecBuilder addBundleVerifiers(BundleVerifierSpec... v) { + bundleVerifiers.addAll(List.of(v)); + return this; + } + + TestSpecBuilder addInstallVerifiers(CallbackFactory... v) { + installVerifiers.addAll(List.of(v)); + return this; + } + + TestSpecBuilder addUninstallVerifiers(CallbackFactory... v) { + uninstallVerifiers.addAll(List.of(v)); + return this; + } + + TestSpecBuilder initializers(CallbackFactory... v) { + initializers.clear(); + return addInitializers(v); + } + + TestSpecBuilder bundleVerifiers(BundleVerifierSpec... v) { + bundleVerifiers.clear(); + return addBundleVerifiers(v); + } + + TestSpecBuilder installVerifiers(CallbackFactory... v) { + installVerifiers.clear(); + return addInstallVerifiers(v); + } + + TestSpecBuilder uninstallVerifiers(CallbackFactory... v) { + uninstallVerifiers.clear(); + return addUninstallVerifiers(v); + } + + TestSpec create() { + final var handlersSpec = new PackageHandlersSpec( + install.createInstaller(installExitCode), uninstall.createUninstaller(), + Optional.ofNullable(unpack).map(CallbackFactory::createUnpacker), installExitCode); + return new TestSpec(type, handlersSpec, + initializers.stream().map(CallbackFactory::createInitializer).toList(), + bundleVerifiers, + installVerifiers.stream().map(CallbackFactory::createInstallVerifier).toList(), + uninstallVerifiers.stream().map(CallbackFactory::createUninstallVerifier).toList(), + expectedJPackageExitCode, + actualJPackageExitCode, actions); + } + + boolean willVerifyCreate() { + return actions.contains(Action.CREATE) && actualJPackageExitCode == 0 && expectedJPackageExitCode == actualJPackageExitCode; + } + + boolean willHaveBundle() { + return !actions.contains(Action.CREATE) || willVerifyCreate(); + } + + boolean willVerifyUnpack() { + return actions.contains(Action.UNPACK) && willHaveBundle() && unpack != null; + } + + boolean willVerifyInstall() { + return (actions.contains(Action.INSTALL) && installExitCode == 0) && willHaveBundle(); + } + + private void overrideNonNullUnpack(CallbackFactory v) { + if (unpack != null) { + unpack(v); + } + } + + private PackageType type = PackageType.LINUX_RPM; + private CallbackFactory install = NEVER; + private CallbackFactory uninstall = NEVER; + private CallbackFactory unpack = NEVER; + private int installExitCode; + private final List initializers = new ArrayList<>(); + private final List bundleVerifiers = new ArrayList<>(); + private final List installVerifiers = new ArrayList<>(); + private final List uninstallVerifiers = new ArrayList<>(); + private int expectedJPackageExitCode; + private int actualJPackageExitCode; + private final List actions = new ArrayList<>(); + } + + @Test + @ParameterSupplier + public void test(TestSpec spec) { + spec.run(); + } + + public static List test() { + List data = new ArrayList<>(); + + for (boolean withUnpack : List.of(false, true)) { + for (int actualJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) { + for (int expectedJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) { + data.add(new TestSpecBuilder() + .unpack(withUnpack ? ONCE : null) + .actualJPackageExitCode(actualJPackageExitCode) + .expectedJPackageExitCode(expectedJPackageExitCode) + .doCreateAndUnpack().create()); + } + } + } + + for (boolean withUnpack : List.of(false, true)) { + for (int installExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) { + for (int actualJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_JPACKAGE)) { + for (int expectedJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_JPACKAGE)) { + data.add(new TestSpecBuilder() + .unpack(withUnpack ? ONCE : null) + .installExitCode(installExitCode) + .actualJPackageExitCode(actualJPackageExitCode) + .expectedJPackageExitCode(expectedJPackageExitCode) + .doCreateUnpackInstallUninstall().create()); + } + } + } + } + + data.add(new TestSpecBuilder() + .actions(Action.VERIFY_INSTALL, Action.UNINSTALL, Action.VERIFY_INSTALL, Action.VERIFY_UNINSTALL) + .uninstall(ONCE) + .initializers(ONCE) + .bundleVerifiers(BundleVerifier.NEVER.spec()) + .installVerifiers(TWICE) + .uninstallVerifiers(ONCE) + .create()); + + return data.stream().map(v -> { + return new Object[] {v}; + }).toList(); + } + + @Test + @ParameterSupplier + public void testDisableInstallerUninstaller(TestSpec spec, boolean disableInstaller, boolean disableUninstaller) { + spec.run(test -> { + if (disableInstaller) { + test.disablePackageInstaller(); + } + if (disableUninstaller) { + test.disablePackageUninstaller(); + } + }); + } + + public static List testDisableInstallerUninstaller() { + List data = new ArrayList<>(); + + for (boolean disableInstaller : List.of(true, false)) { + for (boolean disableUninstaller : List.of(true, false)) { + if (disableInstaller || disableUninstaller) { + final var builder = new TestSpecBuilder().doCreateUnpackInstallUninstall(); + if (disableInstaller) { + builder.install(NEVER); + } + if (disableUninstaller) { + builder.uninstall(NEVER); + } + data.add(new Object[] { builder.create(), disableInstaller, disableUninstaller }); + } + } + } + + return data; + } + + private static List getUnpackPaths(Collection verifiers) { + return verifiers.stream() + .filter(CountingUnpacker.class::isInstance) + .map(CountingUnpacker.class::cast) + .map(CountingUnpacker::unpackPaths) + .reduce((x , y) -> { + throw new UnsupportedOperationException(); + }).orElseThrow(); + } + + @Test + public void testUnpackTwice() { + final var testSpec = new TestSpecBuilder() + .actions(Action.CREATE, Action.UNPACK, Action.VERIFY_INSTALL, Action.UNPACK, Action.VERIFY_INSTALL) + .unpack(TWICE) + .initializers(ONCE) + .installVerifiers(TWICE) + .create(); + + final var unpackPaths = getUnpackPaths(testSpec.run()); + + TKit.assertEquals(2, unpackPaths.size(), "Check the bundle was unpacked in different directories"); + + unpackPaths.forEach(dir -> { + TKit.assertTrue(dir.startsWith(TKit.workDir()), "Check unpack directory is inside of the test work directory"); + }); + } + + @Test + public void testDeleteUnpackDirs() { + final int unpackActionCount = 4; + final var testSpec = new TestSpecBuilder() + .actions(Action.UNPACK, Action.UNPACK, Action.UNPACK, Action.UNPACK) + .unpack(new CallbackFactory(unpackActionCount) { + @Override + CountingUnpacker createUnpacker() { + return new CountingUnpacker(unpackActionCount) { + @Override + public Path apply(JPackageCommand cmd, Path path) { + switch (tickCount()) { + case 0 -> { + } + + case 2 -> { + path = path.resolve("foo"); + } + + case 1, 3 -> { + try { + path = Files.createTempDirectory("jpackage-test"); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + default -> { + throw new IllegalStateException(); + } + } + return super.apply(cmd, path); + } + }; + } + }) + .initializers(ONCE) + .create(); + + final var unpackPaths = getUnpackPaths(testSpec.run()); + + TKit.assertEquals(unpackActionCount, unpackPaths.size(), "Check the bundle was unpacked in different directories"); + + // Unpack directories within the test work directory must exist. + TKit.assertDirectoryExists(unpackPaths.get(0)); + TKit.assertDirectoryExists(unpackPaths.get(2)); + + // Unpack directories outside of the test work directory must be deleted. + TKit.assertPathExists(unpackPaths.get(1), false); + TKit.assertPathExists(unpackPaths.get(3), false); + } + + @Test + public void testRunOnceInitializer() { + final var testSpec = new TestSpecBuilder().doCreateAndUnpack().unpack(TWICE).create(); + + final var initializer = TWICE.createInitializer(); + final var runOnceInitializer = ONCE.createRunOnceInitializer(); + testSpec.run(test -> { + test.forTypes(PackageType.LINUX_RPM, PackageType.WIN_MSI) + .addRunOnceInitializer(runOnceInitializer) + .addInitializer(initializer); + }); + + initializer.verify(); + runOnceInitializer.verify(); + } + + @Test + @Parameter("0") + @Parameter("1") + public void testPurge(int jpackageExitCode) { + + Path[] outputBundle = new Path[1]; + + final var builder = new TestSpecBuilder(); + + builder.actions(Action.CREATE).initializers(new CallbackFactory(1) { + @Override + CountingConsumer createInitializer() { + return new CountingConsumer(1, "custom-init") { + @Override + public void accept(JPackageCommand cmd) { + outputBundle[0] = cmd.outputBundle(); + super.accept(cmd); + } + }; + } + }).create().run(); + TKit.assertFileExists(outputBundle[0]); + + builder.actions(Action.PURGE).initializers(ONCE).jpackageExitCode(jpackageExitCode).create().run(); + TKit.assertPathExists(outputBundle[0], false); + } + + @Test + public void testPackageTestOrder() { + + Set packageTypes = new LinkedHashSet<>(); + + final var initializer = new CountingConsumer(PackageType.NATIVE.size(), "custom-init") { + @Override + public void accept(JPackageCommand cmd) { + packageTypes.add(new JPackageCommand().setArgumentValue( + "--type", cmd.getArgumentValue("--type")).packageType()); + super.accept(cmd); + } + }; + + new TestSpecBuilder().actions(Action.CREATE).create().run(test -> { + test.forTypes().addInitializer(initializer); + }); + + initializer.verify(); + + final var expectedOrder = PackageType.NATIVE.stream() + .sorted().map(PackageType::name).toList(); + final var actualOrder = packageTypes.stream().map(PackageType::name).toList(); + + TKit.assertStringListEquals(expectedOrder, actualOrder, "Check the order or packaging"); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 2d5d035512b..efcd0041579 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -25,6 +25,7 @@ package jdk.jpackage.test; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; @@ -77,6 +78,7 @@ public class JPackageCommand extends CommandArguments { appLayoutAsserts = cmd.appLayoutAsserts; outputValidator = cmd.outputValidator; executeInDirectory = cmd.executeInDirectory; + winMsiLogFile = cmd.winMsiLogFile; } JPackageCommand createImmutableCopy() { @@ -998,6 +1000,22 @@ public class JPackageCommand extends CommandArguments { return this; } + JPackageCommand winMsiLogFile(Path v) { + this.winMsiLogFile = v; + return this; + } + + public Optional winMsiLogFile() { + return Optional.ofNullable(winMsiLogFile); + } + + public Optional> winMsiLogFileContents() { + return winMsiLogFile().map(ThrowingFunction.toFunction(msiLog -> { + // MSI log files are UTF16LE-encoded + return Files.lines(msiLog, StandardCharsets.UTF_16LE); + })); + } + private JPackageCommand adjustArgumentsBeforeExecution() { if (!isWithToolProvider()) { // if jpackage is launched as a process then set the jlink.debug system property @@ -1175,6 +1193,7 @@ public class JPackageCommand extends CommandArguments { private final Actions prerequisiteActions; private final Actions verifyActions; private Path executeInDirectory; + private Path winMsiLogFile; private Set appLayoutAsserts = Set.of(AppLayoutAssert.values()); private Consumer> outputValidator; private static boolean defaultWithToolProvider; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index f97b695d98f..d94b8aee8ac 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -195,58 +195,61 @@ public final class LinuxHelper { } static PackageHandlers createDebPackageHandlers() { - PackageHandlers deb = new PackageHandlers(); - deb.installHandler = cmd -> { - cmd.verifyIsOfType(PackageType.LINUX_DEB); - Executor.of("sudo", "dpkg", "-i") - .addArgument(cmd.outputBundle()) - .execute(); - }; - deb.uninstallHandler = cmd -> { - cmd.verifyIsOfType(PackageType.LINUX_DEB); - var packageName = getPackageName(cmd); - String script = String.format("! dpkg -s %s || sudo dpkg -r %s", - packageName, packageName); - Executor.of("sh", "-c", script).execute(); - }; - deb.unpackHandler = (cmd, destinationDir) -> { - cmd.verifyIsOfType(PackageType.LINUX_DEB); - Executor.of("dpkg", "-x") - .addArgument(cmd.outputBundle()) - .addArgument(destinationDir) - .execute(); - return destinationDir; - }; - return deb; + return new PackageHandlers(LinuxHelper::installDeb, LinuxHelper::uninstallDeb, LinuxHelper::unpackDeb); + } + + private static int installDeb(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX_DEB); + return Executor.of("sudo", "dpkg", "-i") + .addArgument(cmd.outputBundle()) + .execute().getExitCode(); + } + + private static void uninstallDeb(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX_DEB); + var packageName = getPackageName(cmd); + String script = String.format("! dpkg -s %s || sudo dpkg -r %s", + packageName, packageName); + Executor.of("sh", "-c", script).execute(); + } + + private static Path unpackDeb(JPackageCommand cmd, Path destinationDir) { + cmd.verifyIsOfType(PackageType.LINUX_DEB); + Executor.of("dpkg", "-x") + .addArgument(cmd.outputBundle()) + .addArgument(destinationDir) + .execute(0); + return destinationDir; } static PackageHandlers createRpmPackageHandlers() { - PackageHandlers rpm = new PackageHandlers(); - rpm.installHandler = cmd -> { - cmd.verifyIsOfType(PackageType.LINUX_RPM); - Executor.of("sudo", "rpm", "-U") - .addArgument(cmd.outputBundle()) - .execute(); - }; - rpm.uninstallHandler = cmd -> { - cmd.verifyIsOfType(PackageType.LINUX_RPM); - var packageName = getPackageName(cmd); - String script = String.format("! rpm -q %s || sudo rpm -e %s", - packageName, packageName); - Executor.of("sh", "-c", script).execute(); - }; - rpm.unpackHandler = (cmd, destinationDir) -> { - cmd.verifyIsOfType(PackageType.LINUX_RPM); - Executor.of("sh", "-c", String.format( - "rpm2cpio '%s' | cpio -idm --quiet", - JPackageCommand.escapeAndJoin( - cmd.outputBundle().toAbsolutePath().toString()))) - .setDirectory(destinationDir) - .execute(); - return destinationDir; - }; + return new PackageHandlers(LinuxHelper::installRpm, LinuxHelper::uninstallRpm, LinuxHelper::unpackRpm); + } - return rpm; + private static int installRpm(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX_RPM); + return Executor.of("sudo", "rpm", "-U") + .addArgument(cmd.outputBundle()) + .execute().getExitCode(); + } + + private static void uninstallRpm(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX_RPM); + var packageName = getPackageName(cmd); + String script = String.format("! rpm -q %s || sudo rpm -e %s", + packageName, packageName); + Executor.of("sh", "-c", script).execute(); + } + + private static Path unpackRpm(JPackageCommand cmd, Path destinationDir) { + cmd.verifyIsOfType(PackageType.LINUX_RPM); + Executor.of("sh", "-c", String.format( + "rpm2cpio '%s' | cpio -idm --quiet", + JPackageCommand.escapeAndJoin( + cmd.outputBundle().toAbsolutePath().toString()))) + .setDirectory(destinationDir) + .execute(0); + return destinationDir; } static Path getLauncherPath(JPackageCommand cmd) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 72706129213..8d245fb4d96 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -150,113 +150,115 @@ public final class MacHelper { } static PackageHandlers createDmgPackageHandlers() { - PackageHandlers dmg = new PackageHandlers(); + return new PackageHandlers(MacHelper::installDmg, MacHelper::uninstallDmg, MacHelper::unpackDmg); + } - dmg.installHandler = cmd -> { - withExplodedDmg(cmd, dmgImage -> { - Executor.of("sudo", "cp", "-r") - .addArgument(dmgImage) - .addArgument(getInstallationDirectory(cmd).getParent()) - .execute(); - }); - }; - dmg.unpackHandler = (cmd, destinationDir) -> { - Path unpackDir = destinationDir.resolve( - TKit.removeRootFromAbsolutePath( - getInstallationDirectory(cmd)).getParent()); - try { - Files.createDirectories(unpackDir); - } catch (IOException ex) { - throw new RuntimeException(ex); - } + private static int installDmg(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC_DMG); + withExplodedDmg(cmd, dmgImage -> { + Executor.of("sudo", "cp", "-r") + .addArgument(dmgImage) + .addArgument(getInstallationDirectory(cmd).getParent()) + .execute(0); + }); + return 0; + } - withExplodedDmg(cmd, dmgImage -> { - Executor.of("cp", "-r") - .addArgument(dmgImage) - .addArgument(unpackDir) - .execute(); - }); - return destinationDir; - }; - dmg.uninstallHandler = cmd -> { - cmd.verifyIsOfType(PackageType.MAC_DMG); - Executor.of("sudo", "rm", "-rf") - .addArgument(cmd.appInstallationDirectory()) + private static void uninstallDmg(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC_DMG); + Executor.of("sudo", "rm", "-rf") + .addArgument(cmd.appInstallationDirectory()) + .execute(); + } + + private static Path unpackDmg(JPackageCommand cmd, Path destinationDir) { + cmd.verifyIsOfType(PackageType.MAC_DMG); + Path unpackDir = destinationDir.resolve( + TKit.removeRootFromAbsolutePath( + getInstallationDirectory(cmd)).getParent()); + try { + Files.createDirectories(unpackDir); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + withExplodedDmg(cmd, dmgImage -> { + Executor.of("cp", "-r") + .addArgument(dmgImage) + .addArgument(unpackDir) .execute(); - }; - - return dmg; + }); + return destinationDir; } static PackageHandlers createPkgPackageHandlers() { - PackageHandlers pkg = new PackageHandlers(); + return new PackageHandlers(MacHelper::installPkg, MacHelper::uninstallPkg, MacHelper::unpackPkg); + } - pkg.installHandler = cmd -> { - cmd.verifyIsOfType(PackageType.MAC_PKG); - Executor.of("sudo", "/usr/sbin/installer", "-allowUntrusted", "-pkg") - .addArgument(cmd.outputBundle()) - .addArguments("-target", "/") + private static int installPkg(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC_PKG); + return Executor.of("sudo", "/usr/sbin/installer", "-allowUntrusted", "-pkg") + .addArgument(cmd.outputBundle()) + .addArguments("-target", "/") + .execute().getExitCode(); + } + + private static void uninstallPkg(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC_PKG); + if (Files.exists(getUninstallCommand(cmd))) { + Executor.of("sudo", "/bin/sh", + getUninstallCommand(cmd).toString()).execute(); + } else { + Executor.of("sudo", "rm", "-rf") + .addArgument(cmd.appInstallationDirectory()) .execute(); - }; - pkg.unpackHandler = (cmd, destinationDir) -> { - cmd.verifyIsOfType(PackageType.MAC_PKG); + } + } - var dataDir = destinationDir.resolve("data"); + private static Path unpackPkg(JPackageCommand cmd, Path destinationDir) { + cmd.verifyIsOfType(PackageType.MAC_PKG); - Executor.of("pkgutil", "--expand") - .addArgument(cmd.outputBundle()) - .addArgument(dataDir) // We need non-existing folder - .execute(); + var dataDir = destinationDir.resolve("data"); - final Path unpackRoot = destinationDir.resolve("unpacked"); + Executor.of("pkgutil", "--expand") + .addArgument(cmd.outputBundle()) + .addArgument(dataDir) // We need non-existing folder + .execute(); - // Unpack all ".pkg" files from $dataDir folder in $unpackDir folder - try (var dataListing = Files.list(dataDir)) { - dataListing.filter(file -> { - return ".pkg".equals(PathUtils.getSuffix(file.getFileName())); - }).forEach(ThrowingConsumer.toConsumer(pkgDir -> { - // Installation root of the package is stored in - // /pkg-info@install-location attribute in $pkgDir/PackageInfo xml file - var doc = createDocumentBuilder().parse( - new ByteArrayInputStream(Files.readAllBytes( - pkgDir.resolve("PackageInfo")))); - var xPath = XPathFactory.newInstance().newXPath(); + final Path unpackRoot = destinationDir.resolve("unpacked"); - final String installRoot = (String) xPath.evaluate( - "/pkg-info/@install-location", doc, - XPathConstants.STRING); + // Unpack all ".pkg" files from $dataDir folder in $unpackDir folder + try (var dataListing = Files.list(dataDir)) { + dataListing.filter(file -> { + return ".pkg".equals(PathUtils.getSuffix(file.getFileName())); + }).forEach(ThrowingConsumer.toConsumer(pkgDir -> { + // Installation root of the package is stored in + // /pkg-info@install-location attribute in $pkgDir/PackageInfo xml file + var doc = createDocumentBuilder().parse( + new ByteArrayInputStream(Files.readAllBytes( + pkgDir.resolve("PackageInfo")))); + var xPath = XPathFactory.newInstance().newXPath(); - final Path unpackDir = unpackRoot.resolve( - TKit.removeRootFromAbsolutePath(Path.of(installRoot))); + final String installRoot = (String) xPath.evaluate( + "/pkg-info/@install-location", doc, + XPathConstants.STRING); - Files.createDirectories(unpackDir); + final Path unpackDir = unpackRoot.resolve( + TKit.removeRootFromAbsolutePath(Path.of(installRoot))); - Executor.of("tar", "-C") - .addArgument(unpackDir) - .addArgument("-xvf") - .addArgument(pkgDir.resolve("Payload")) - .execute(); - })); - } catch (IOException ex) { - throw new RuntimeException(ex); - } + Files.createDirectories(unpackDir); - return unpackRoot; - }; - pkg.uninstallHandler = cmd -> { - cmd.verifyIsOfType(PackageType.MAC_PKG); - - if (Files.exists(getUninstallCommand(cmd))) { - Executor.of("sudo", "/bin/sh", - getUninstallCommand(cmd).toString()).execute(); - } else { - Executor.of("sudo", "rm", "-rf") - .addArgument(cmd.appInstallationDirectory()) + Executor.of("tar", "-C") + .addArgument(unpackDir) + .addArgument("-xvf") + .addArgument(pkgDir.resolve("Payload")) .execute(); - } - }; + })); + } catch (IOException ex) { + throw new RuntimeException(ex); + } - return pkg; + return unpackRoot; } static void verifyBundleStructure(JPackageCommand cmd) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index f89bd0a60c8..e7b3f3e3a44 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -22,6 +22,15 @@ */ package jdk.jpackage.test; +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; +import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.test.PackageType.LINUX; +import static jdk.jpackage.test.PackageType.MAC_PKG; +import static jdk.jpackage.test.PackageType.NATIVE; +import static jdk.jpackage.test.PackageType.WINDOWS; + import java.awt.GraphicsEnvironment; import java.io.IOException; import java.nio.file.Files; @@ -29,6 +38,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,27 +50,15 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; -import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; import jdk.jpackage.internal.util.function.ThrowingConsumer; -import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; import jdk.jpackage.internal.util.function.ThrowingRunnable; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; -import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; -import static jdk.jpackage.test.PackageType.LINUX; -import static jdk.jpackage.test.PackageType.LINUX_DEB; -import static jdk.jpackage.test.PackageType.LINUX_RPM; -import static jdk.jpackage.test.PackageType.MAC_DMG; -import static jdk.jpackage.test.PackageType.MAC_PKG; -import static jdk.jpackage.test.PackageType.NATIVE; -import static jdk.jpackage.test.PackageType.WINDOWS; -import static jdk.jpackage.test.PackageType.WIN_EXE; -import static jdk.jpackage.test.PackageType.WIN_MSI; /** @@ -73,13 +71,18 @@ import static jdk.jpackage.test.PackageType.WIN_MSI; public final class PackageTest extends RunnablePackageTest { public PackageTest() { + isPackageTypeSupported = PackageType::isSupported; + jpackageFactory = JPackageCommand::new; + packageHandlers = new HashMap<>(); + disabledInstallers = new HashSet<>(); + disabledUninstallers = new HashSet<>(); excludeTypes = new HashSet<>(); forTypes(); setExpectedExitCode(0); + setExpectedInstallExitCode(0); namedInitializers = new HashSet<>(); - handlers = currentTypes.stream() + handlers = NATIVE.stream() .collect(Collectors.toMap(v -> v, v -> new Handler())); - packageHandlers = createDefaultPackageHandlers(); } public PackageTest excludeTypes(PackageType... types) { @@ -93,13 +96,13 @@ public final class PackageTest extends RunnablePackageTest { public PackageTest forTypes(PackageType... types) { Collection newTypes; - if (types == null || types.length == 0) { + if (types.length == 0) { newTypes = NATIVE; } else { newTypes = Stream.of(types).collect(Collectors.toSet()); } currentTypes = newTypes.stream() - .filter(PackageType::isSupported) + .filter(isPackageTypeSupported) .filter(Predicate.not(excludeTypes::contains)) .collect(Collectors.toUnmodifiableSet()); return this; @@ -124,6 +127,11 @@ public final class PackageTest extends RunnablePackageTest { return this; } + public PackageTest setExpectedInstallExitCode(int v) { + expectedInstallExitCode = v; + return this; + } + public PackageTest ignoreBundleOutputDir() { return ignoreBundleOutputDir(true); } @@ -133,8 +141,8 @@ public final class PackageTest extends RunnablePackageTest { return this; } - private PackageTest addInitializer(ThrowingConsumer v, - String id) { + private PackageTest addInitializer(ThrowingConsumer v, String id) { + Objects.requireNonNull(v); if (id != null) { if (namedInitializers.contains(id)) { return this; @@ -142,12 +150,12 @@ public final class PackageTest extends RunnablePackageTest { namedInitializers.add(id); } - currentTypes.forEach(type -> handlers.get(type).addInitializer( - toConsumer(v))); + currentTypes.forEach(type -> handlers.get(type).addInitializer(toConsumer(v))); return this; } private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) { + Objects.requireNonNull(v); return addInitializer(new ThrowingConsumer() { @Override public void accept(JPackageCommand unused) throws Throwable { @@ -169,24 +177,26 @@ public final class PackageTest extends RunnablePackageTest { return addRunOnceInitializer(v, null); } - public PackageTest addBundleVerifier( - ThrowingBiConsumer v) { - currentTypes.forEach(type -> handlers.get(type).addBundleVerifier( - toBiConsumer(v))); + public PackageTest addBundleVerifier(ThrowingBiConsumer v) { + Objects.requireNonNull(v); + currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(toBiConsumer(v))); return this; } public PackageTest addBundleVerifier(ThrowingConsumer v) { + Objects.requireNonNull(v); return addBundleVerifier((cmd, unused) -> toConsumer(v).accept(cmd)); } public PackageTest addBundlePropertyVerifier(String propertyName, Predicate pred, String predLabel) { + Objects.requireNonNull(propertyName); + Objects.requireNonNull(pred); return addBundleVerifier(cmd -> { final String value; - if (TKit.isLinux()) { + if (isOfType(cmd, LINUX)) { value = LinuxHelper.getBundleProperty(cmd, propertyName); - } else if (TKit.isWindows()) { + } else if (isOfType(cmd, WINDOWS)) { value = WindowsHelper.getMsiProperty(cmd, propertyName); } else { throw new IllegalStateException(); @@ -223,19 +233,23 @@ public final class PackageTest extends RunnablePackageTest { } public PackageTest disablePackageInstaller() { - currentTypes.forEach( - type -> packageHandlers.get(type).installHandler = cmd -> {}); + currentTypes.forEach(disabledInstallers::add); return this; } public PackageTest disablePackageUninstaller() { - currentTypes.forEach( - type -> packageHandlers.get(type).uninstallHandler = cmd -> {}); + currentTypes.forEach(disabledUninstallers::add); + return this; + } + + public PackageTest createMsiLog(boolean v) { + createMsiLog = v; return this; } static void withFileAssociationsTestRuns(FileAssociations fa, ThrowingBiConsumer> consumer) { + Objects.requireNonNull(consumer); for (var testRun : fa.getTestRuns()) { TKit.withTempDirectory("fa-test-files", tempDir -> { List testFiles = StreamSupport.stream(testRun.getFileNames().spliterator(), false).map(fname -> { @@ -254,6 +268,7 @@ public final class PackageTest extends RunnablePackageTest { } PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { + Objects.requireNonNull(fa); // Setup test app to have valid jpackage command line before // running check of type of environment. @@ -290,7 +305,7 @@ public final class PackageTest extends RunnablePackageTest { Collections.emptyMap()); }); - if (TKit.isWindows()) { + if (isOfType(cmd, WINDOWS)) { // Verify context menu label in registry. String progId = WindowsHelper.queryRegistryValue( String.format("HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\%s", fa.getSuffix()), ""); @@ -307,8 +322,7 @@ public final class PackageTest extends RunnablePackageTest { } public PackageTest forTypes(Collection types, Runnable action) { - Set oldTypes = Set.of(currentTypes.toArray( - PackageType[]::new)); + final var oldTypes = Set.of(currentTypes.toArray(PackageType[]::new)); try { forTypes(types); action.run(); @@ -374,10 +388,59 @@ public final class PackageTest extends RunnablePackageTest { private final List> handlers; } - static final class PackageHandlers { - Consumer installHandler; - Consumer uninstallHandler; - BiFunction unpackHandler; + PackageTest packageHandlers(PackageHandlers v) { + Objects.requireNonNull(v); + currentTypes.forEach(type -> packageHandlers.put(type, v)); + return this; + } + + PackageTest isPackageTypeSupported(Predicate v) { + Objects.requireNonNull(v); + isPackageTypeSupported = v; + return this; + } + + PackageTest jpackageFactory(Supplier v) { + Objects.requireNonNull(v); + jpackageFactory = v; + return this; + } + + record PackageHandlers(Function installHandler, + Consumer uninstallHandler, + Optional> unpackHandler) { + + PackageHandlers(Function installHandler, + Consumer uninstallHandler, + BiFunction unpackHandler) { + this(installHandler, uninstallHandler, Optional.of(unpackHandler)); + } + + PackageHandlers { + Objects.requireNonNull(installHandler); + Objects.requireNonNull(uninstallHandler); + Objects.requireNonNull(unpackHandler); + } + + PackageHandlers copyWithNopInstaller() { + return new PackageHandlers(cmd -> 0, uninstallHandler, unpackHandler); + } + + PackageHandlers copyWithNopUninstaller() { + return new PackageHandlers(installHandler, cmd -> {}, unpackHandler); + } + + int install(JPackageCommand cmd) { + return installHandler.apply(cmd); + } + + Path unpack(JPackageCommand cmd, Path unpackDir) { + return unpackHandler.orElseThrow().apply(cmd, unpackDir); + } + + void uninstall(JPackageCommand cmd) { + uninstallHandler.accept(cmd); + } } @Override @@ -393,164 +456,197 @@ public final class PackageTest extends RunnablePackageTest { } private List> createPackageTypeHandlers() { - return NATIVE.stream() - .map(type -> { - Handler handler = handlers.entrySet().stream() - .filter(entry -> !entry.getValue().isVoid()) - .filter(entry -> entry.getKey() == type) - .map(entry -> entry.getValue()) - .findAny().orElse(null); - Map.Entry result = null; - if (handler != null) { - result = Map.entry(type, handler); - } - return result; - }) - .filter(Objects::nonNull) - .map(entry -> createPackageTypeHandler( - entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); + return handlers.entrySet().stream() + .filter(entry -> !entry.getValue().isVoid()) + .filter(entry -> NATIVE.contains(entry.getKey())) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .map(entry -> { + return createPackageTypeHandler(entry.getKey(), entry.getValue()); + }).toList(); } - private Consumer createPackageTypeHandler( - PackageType type, Handler handler) { - return toConsumer(new ThrowingConsumer() { - @Override - public void accept(Action action) throws Throwable { - if (terminated) { - throw new IllegalStateException(); + private record PackageTypePipeline(PackageType type, int expectedJPackageExitCode, + int expectedInstallExitCode, PackageHandlers packageHandlers, Handler handler, + JPackageCommand cmd, State state) implements Consumer { + + PackageTypePipeline { + Objects.requireNonNull(type); + Objects.requireNonNull(packageHandlers); + Objects.requireNonNull(handler); + Objects.requireNonNull(cmd); + Objects.requireNonNull(state); + } + + PackageTypePipeline(PackageType type, int expectedJPackageExitCode, + int expectedInstallExitCode, PackageHandlers packageHandlers, + Handler handler, JPackageCommand cmd) { + this(type, expectedJPackageExitCode, expectedInstallExitCode, + packageHandlers, handler, cmd, new State()); + } + + @Override + public void accept(Action action) { + switch(analizeAction(action)) { + case SKIP_NO_PACKAGE_HANDLER -> { + TKit.trace(String.format("No handler of [%s] action for %s command", + action, cmd.getPrintableCommandLine())); + return; } - - if (action == Action.FINALIZE) { - if (unpackDir != null) { - if (Files.isDirectory(unpackDir) - && !unpackDir.startsWith(TKit.workDir())) { - TKit.deleteDirectoryRecursive(unpackDir); - } - unpackDir = null; - } - terminated = true; - } - - boolean skip = false; - - if (unhandledAction != null) { - switch (unhandledAction) { - case CREATE: - skip = true; - break; - case UNPACK: - case INSTALL: - skip = (action == Action.VERIFY_INSTALL); - break; - case UNINSTALL: - skip = (action == Action.VERIFY_UNINSTALL); - break; - default: // NOP - } - } - - if (skip) { + case SKIP -> { TKit.trace(String.format("Skip [%s] action of %s command", action, cmd.getPrintableCommandLine())); return; } - - final Supplier curCmd = () -> { - if (Set.of(Action.INITIALIZE, Action.CREATE).contains(action)) { - return cmd; - } else { - return cmd.createImmutableCopy(); - } - }; - - switch (action) { - case UNPACK: { - cmd.setUnpackedPackageLocation(null); - handleAction(action, - packageHandlers.get(type).unpackHandler, - handler -> { - unpackDir = TKit.createTempDirectory( - String.format("unpacked-%s", - type.getName())); - unpackDir = handler.apply(cmd, unpackDir); - cmd.setUnpackedPackageLocation(unpackDir); - }); - break; - } - - case INSTALL: { - cmd.setUnpackedPackageLocation(null); - handleAction(action, - packageHandlers.get(type).installHandler, - handler -> { - handler.accept(curCmd.get()); - }); - break; - } - - case UNINSTALL: { - handleAction(action, - packageHandlers.get(type).uninstallHandler, - handler -> { - handler.accept(curCmd.get()); - }); - break; - } - - case CREATE: - cmd.setUnpackedPackageLocation(null); - handler.accept(action, curCmd.get()); - handleAction(action, - (expectedJPackageExitCode == 0) ? Boolean.TRUE : null, - handler -> { - }); - return; - - default: - handler.accept(action, curCmd.get()); - break; - } - - Optional.ofNullable(unhandledAction).ifPresent(v -> { - TKit.trace(String.format( - "No handler of [%s] action for %s command", v, - cmd.getPrintableCommandLine())); - }); - } - - private void handleAction(Action action, T handler, - ThrowingConsumer consumer) throws Throwable { - if (handler == null) { - unhandledAction = action; - } else { - unhandledAction = null; - consumer.accept(handler); + case PROCESS -> { } } - private Path unpackDir; - private Action unhandledAction; - private boolean terminated; - private final JPackageCommand cmd = Functional.identity(() -> { - JPackageCommand result = new JPackageCommand(); - result.setDefaultInputOutput().setDefaultAppName(); - if (BUNDLE_OUTPUT_DIR != null && !ignoreBundleOutputDir) { - result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString()); + switch (action) { + case UNPACK -> { + cmd.setUnpackedPackageLocation(null); + final var unpackRootDir = TKit.createTempDirectory( + String.format("unpacked-%s", type.getName())); + final Path unpackDir = packageHandlers.unpack(cmd, unpackRootDir); + if (!unpackDir.startsWith(TKit.workDir())) { + state.deleteUnpackDirs.add(unpackDir); + } + cmd.setUnpackedPackageLocation(unpackDir); } - type.applyTo(result); - return result; - }).get(); - }); + + case INSTALL -> { + cmd.setUnpackedPackageLocation(null); + final int installExitCode = packageHandlers.install(cmd); + TKit.assertEquals(expectedInstallExitCode, installExitCode, + String.format("Check installer exited with %d code", expectedInstallExitCode)); + } + + case UNINSTALL -> { + cmd.setUnpackedPackageLocation(null); + packageHandlers.uninstall(cmd); + } + + case CREATE -> { + cmd.setUnpackedPackageLocation(null); + handler.processAction(action, cmd, expectedJPackageExitCode); + } + + case INITIALIZE -> { + handler.processAction(action, cmd, expectedJPackageExitCode); + } + + case FINALIZE -> { + state.deleteUnpackDirs.forEach(TKit::deleteDirectoryRecursive); + state.deleteUnpackDirs.clear(); + } + + default -> { + handler.processAction(action, cmd.createImmutableCopy(), expectedJPackageExitCode); + } + } + } + + private enum ActionAction { + PROCESS, + SKIP, + SKIP_NO_PACKAGE_HANDLER + } + + private ActionAction analizeAction(Action action) { + Objects.requireNonNull(action); + + if (jpackageFailed()) { + return ActionAction.SKIP; + } + + switch (action) { + case CREATE -> { + state.packageActions.add(action); + } + case INSTALL -> { + state.packageActions.add(action); + state.packageActions.remove(Action.UNPACK); + } + case UNINSTALL -> { + state.packageActions.add(action); + if (installFailed()) { + return ActionAction.SKIP; + } + } + case UNPACK -> { + state.packageActions.add(action); + state.packageActions.remove(Action.INSTALL); + if (unpackNotSupported()) { + return ActionAction.SKIP_NO_PACKAGE_HANDLER; + } + } + case VERIFY_INSTALL -> { + if (unpackNotSupported()) { + return ActionAction.SKIP; + } + + if (installFailed()) { + return ActionAction.SKIP; + } + } + case VERIFY_UNINSTALL -> { + if (installFailed() && processed(Action.UNINSTALL)) { + return ActionAction.SKIP; + } + } + default -> { + // NOP + } + } + + return ActionAction.PROCESS; + } + + private boolean processed(Action action) { + Objects.requireNonNull(action); + return state.packageActions.contains(action); + } + + private boolean installFailed() { + return processed(Action.INSTALL) && expectedInstallExitCode != 0; + } + + private boolean jpackageFailed() { + return processed(Action.CREATE) && expectedJPackageExitCode != 0; + } + + private boolean unpackNotSupported() { + return processed(Action.UNPACK) && packageHandlers.unpackHandler().isEmpty(); + } + + private final static class State { + private final Set packageActions = new HashSet<>(); + private final List deleteUnpackDirs = new ArrayList<>(); + } } - private class Handler implements BiConsumer { + private Consumer createPackageTypeHandler(PackageType type, Handler handler) { + final var cmd = jpackageFactory.get(); + cmd.setDefaultInputOutput().setDefaultAppName(); + if (BUNDLE_OUTPUT_DIR != null && !ignoreBundleOutputDir) { + cmd.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString()); + } + type.applyTo(cmd); + return new PackageTypePipeline(type, expectedJPackageExitCode, + expectedInstallExitCode, getPackageHandlers(type), handler.copy(), cmd); + } + + private record Handler(List> initializers, + List> bundleVerifiers, + List> installVerifiers, + List> uninstallVerifiers) { Handler() { - initializers = new ArrayList<>(); - bundleVerifiers = new ArrayList<>(); - installVerifiers = new ArrayList<>(); - uninstallVerifiers = new ArrayList<>(); + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + } + + Handler copy() { + return new Handler(List.copyOf(initializers), List.copyOf(bundleVerifiers), + List.copyOf(installVerifiers), List.copyOf(uninstallVerifiers)); } boolean isVoid() { @@ -573,18 +669,17 @@ public final class PackageTest extends RunnablePackageTest { uninstallVerifiers.add(v); } - @Override - public void accept(Action action, JPackageCommand cmd) { + public void processAction(Action action, JPackageCommand cmd, int expectedJPackageExitCode) { switch (action) { - case INITIALIZE: + case INITIALIZE -> { initializers.forEach(v -> v.accept(cmd)); if (cmd.isImagePackageType()) { throw new UnsupportedOperationException(); } cmd.executePrerequisiteActions(); - break; + } - case CREATE: + case CREATE -> { Executor.Result result = cmd.execute(expectedJPackageExitCode); if (expectedJPackageExitCode == 0) { TKit.assertFileExists(cmd.outputBundle()); @@ -593,39 +688,38 @@ public final class PackageTest extends RunnablePackageTest { TKit.assertPathExists(outputBundle, false); }); } - verifyPackageBundle(cmd, result); - break; + verifyPackageBundle(cmd, result, expectedJPackageExitCode); + } - case VERIFY_INSTALL: + case VERIFY_INSTALL -> { if (expectedJPackageExitCode == 0) { verifyPackageInstalled(cmd); } - break; + } - case VERIFY_UNINSTALL: + case VERIFY_UNINSTALL -> { if (expectedJPackageExitCode == 0) { verifyPackageUninstalled(cmd); } - break; + } - case PURGE: - if (expectedJPackageExitCode == 0) { - var bundle = cmd.outputBundle(); - if (toSupplier(() -> TKit.deleteIfExists(bundle)).get()) { - TKit.trace(String.format("Deleted [%s] package", - bundle)); - } + case PURGE -> { + var bundle = cmd.outputBundle(); + if (toSupplier(() -> TKit.deleteIfExists(bundle)).get()) { + TKit.trace(String.format("Deleted [%s] package", bundle)); } - break; + } - default: // NOP + default -> { + // NOP + } } } private void verifyPackageBundle(JPackageCommand cmd, - Executor.Result result) { + Executor.Result result, int expectedJPackageExitCode) { if (expectedJPackageExitCode == 0) { - if (LINUX.contains(cmd.packageType())) { + if (isOfType(cmd, LINUX)) { LinuxHelper.verifyPackageBundleEssential(cmd); } } @@ -647,9 +741,7 @@ public final class PackageTest extends RunnablePackageTest { }); if (!cmd.isRuntime()) { - if (WINDOWS.contains(cmd.packageType()) - && !cmd.isPackageUnpacked( - "Not verifying desktop integration")) { + if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) { // Check main launcher WindowsHelper.verifyDesktopIntegration(cmd, null); // Check additional launchers @@ -659,8 +751,7 @@ public final class PackageTest extends RunnablePackageTest { } } - if (LauncherAsServiceVerifier.SUPPORTED_PACKAGES.contains( - cmd.packageType())) { + if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) { LauncherAsServiceVerifier.verify(cmd); } @@ -676,13 +767,13 @@ public final class PackageTest extends RunnablePackageTest { && !LauncherAsServiceVerifier.getLaunchersAsServices(cmd).isEmpty(); final long expectedRootCount; - if (WINDOWS.contains(cmd.packageType())) { + if (isOfType(cmd, WINDOWS)) { // On Windows it is always two entries: // installation home directory and MSI file expectedRootCount = 2; - } else if (withServices && MAC_PKG.equals(cmd.packageType())) { + } else if (withServices && isOfType(cmd, MAC_PKG)) { expectedRootCount = 2; - } else if (LINUX.contains(cmd.packageType())) { + } else if (isOfType(cmd, LINUX)) { Set roots = new HashSet<>(); roots.add(Path.of("/").resolve(Path.of(cmd.getArgumentValue( "--install-dir", () -> "/opt")).getName(0))); @@ -732,7 +823,7 @@ public final class PackageTest extends RunnablePackageTest { if (!cmd.isRuntime()) { TKit.assertPathExists(cmd.appLauncherPath(), false); - if (WINDOWS.contains(cmd.packageType())) { + if (isOfType(cmd, WINDOWS)) { // Check main launcher WindowsHelper.verifyDesktopIntegration(cmd, null); // Check additional launchers @@ -743,54 +834,94 @@ public final class PackageTest extends RunnablePackageTest { } Path appInstallDir = cmd.appInstallationDirectory(); - if (TKit.isLinux() && Path.of("/").equals(appInstallDir)) { + if (isOfType(cmd, LINUX) && Path.of("/").equals(appInstallDir)) { ApplicationLayout appLayout = cmd.appLayout(); TKit.assertPathExists(appLayout.runtimeDirectory(), false); } else { TKit.assertPathExists(appInstallDir, false); } - if (LauncherAsServiceVerifier.SUPPORTED_PACKAGES.contains( - cmd.packageType())) { + if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) { LauncherAsServiceVerifier.verifyUninstalled(cmd); } uninstallVerifiers.forEach(v -> v.accept(cmd)); } - - private final List> initializers; - private final List> bundleVerifiers; - private final List> installVerifiers; - private final List> uninstallVerifiers; } - private static Map createDefaultPackageHandlers() { - HashMap handlers = new HashMap<>(); - if (TKit.isLinux()) { - handlers.put(LINUX_DEB, LinuxHelper.createDebPackageHandlers()); - handlers.put(LINUX_RPM, LinuxHelper.createRpmPackageHandlers()); + private PackageHandlers getDefaultPackageHandlers(PackageType type) { + switch (type) { + case LINUX_DEB -> { + return LinuxHelper.createDebPackageHandlers(); + } + case LINUX_RPM -> { + return LinuxHelper.createRpmPackageHandlers(); + } + case WIN_MSI -> { + return WindowsHelper.createMsiPackageHandlers(createMsiLog); + } + case WIN_EXE -> { + return WindowsHelper.createExePackageHandlers(createMsiLog); + } + case MAC_DMG -> { + return MacHelper.createDmgPackageHandlers(); + } + case MAC_PKG -> { + return MacHelper.createPkgPackageHandlers(); + } + default -> { + throw new IllegalArgumentException(); + } + } + } + + private PackageHandlers getPackageHandlers(PackageType type) { + Objects.requireNonNull(type); + + var reply = Optional.ofNullable(packageHandlers.get(type)).orElseGet(() -> { + if (TKit.isLinux() && !PackageType.LINUX.contains(type)) { + throw new IllegalArgumentException(); + } else if (TKit.isWindows() && !PackageType.WINDOWS.contains(type)) { + throw new IllegalArgumentException(); + } else if (TKit.isOSX() && !PackageType.MAC.contains(type)) { + throw new IllegalArgumentException(); + } else { + return getDefaultPackageHandlers(type); + } + }); + + if (disabledInstallers.contains(type)) { + reply = reply.copyWithNopInstaller(); } - if (TKit.isWindows()) { - handlers.put(WIN_MSI, WindowsHelper.createMsiPackageHandlers()); - handlers.put(WIN_EXE, WindowsHelper.createExePackageHandlers()); + if (disabledUninstallers.contains(type)) { + reply = reply.copyWithNopUninstaller(); } - if (TKit.isOSX()) { - handlers.put(MAC_DMG, MacHelper.createDmgPackageHandlers()); - handlers.put(MAC_PKG, MacHelper.createPkgPackageHandlers()); - } + return reply; + } - return handlers; + private static boolean isOfType(JPackageCommand cmd, PackageType packageTypes) { + return isOfType(cmd, Set.of(packageTypes)); + } + + private static boolean isOfType(JPackageCommand cmd, Set packageTypes) { + return Optional.ofNullable(cmd.packageType()).map(packageTypes::contains).orElse(false); } private Collection currentTypes; private Set excludeTypes; private int expectedJPackageExitCode; - private Map handlers; - private Set namedInitializers; - private Map packageHandlers; + private int expectedInstallExitCode; + private final Map handlers; + private final Set namedInitializers; + private final Map packageHandlers; + private final Set disabledInstallers; + private final Set disabledUninstallers; + private Predicate isPackageTypeSupported; + private Supplier jpackageFactory; private boolean ignoreBundleOutputDir; + private boolean createMsiLog; private static final Path BUNDLE_OUTPUT_DIR; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 48643463e0b..91705afd5fe 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -22,24 +22,25 @@ */ package jdk.jpackage.test; +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.BiConsumer; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; import jdk.jpackage.internal.util.function.ThrowingRunnable; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import jdk.jpackage.test.PackageTest.PackageHandlers; public class WindowsHelper { @@ -67,21 +68,24 @@ public class WindowsHelper { return Path.of(cmd.getArgumentValue("--install-dir", cmd::name)); } - private static void runMsiexecWithRetries(Executor misexec) { + private static int runMsiexecWithRetries(Executor misexec, Optional msiLog) { Executor.Result result = null; + final boolean isUnpack = misexec.getExecutable().orElseThrow().equals(Path.of("cmd")); + final List origArgs = msiLog.isPresent() ? misexec.getAllArguments() : null; for (int attempt = 0; attempt < 8; ++attempt) { + msiLog.ifPresent(v -> misexec.clearArguments().addArguments(origArgs).addArgument("/L*v").addArgument(v)); result = misexec.executeWithoutExitCodeCheck(); if (result.exitCode() == 1605) { // ERROR_UNKNOWN_PRODUCT, attempt to uninstall not installed // package - return; + return result.exitCode(); } // The given Executor may either be of an msiexec command or an // unpack.bat script containing the msiexec command. In the later // case, when misexec returns 1618, the unpack.bat may return 1603 - if ((result.exitCode() == 1618) || (result.exitCode() == 1603)) { + if ((result.exitCode() == 1618) || (result.exitCode() == 1603 && isUnpack)) { // Another installation is already in progress. // Wait a little and try again. Long timeout = 1000L * (attempt + 3); // from 3 to 10 seconds @@ -91,74 +95,123 @@ public class WindowsHelper { break; } - result.assertExitCodeIsZero(); + return result.exitCode(); } - static PackageHandlers createMsiPackageHandlers() { - BiConsumer installMsi = (cmd, install) -> { - cmd.verifyIsOfType(PackageType.WIN_MSI); - var msiPath = TransientMsi.create(cmd).path(); - runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart", - install ? "/i" : "/x").addArgument(msiPath)); - }; + static PackageHandlers createMsiPackageHandlers(boolean createMsiLog) { + return new PackageHandlers(cmd -> installMsi(cmd, createMsiLog), + cmd -> uninstallMsi(cmd, createMsiLog), WindowsHelper::unpackMsi); + } - PackageHandlers msi = new PackageHandlers(); - msi.installHandler = cmd -> installMsi.accept(cmd, true); - msi.uninstallHandler = cmd -> { - if (Files.exists(cmd.outputBundle())) { - installMsi.accept(cmd, false); + private static Optional configureMsiLogFile(JPackageCommand cmd, boolean createMsiLog) { + final Optional msiLogFile; + if (createMsiLog) { + msiLogFile = Optional.of(TKit.createTempFile(String.format("logs\\%s-msi.log", + cmd.packageType().getName()))); + } else { + msiLogFile = Optional.empty(); + } + + cmd.winMsiLogFile(msiLogFile.orElse(null)); + + return msiLogFile; + } + + private static int runMsiInstaller(JPackageCommand cmd, boolean createMsiLog, boolean install) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + final var msiPath = TransientMsi.create(cmd).path(); + return runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart", + install ? "/i" : "/x").addArgument(msiPath), configureMsiLogFile(cmd, createMsiLog)); + } + + private static int installMsi(JPackageCommand cmd, boolean createMsiLog) { + return runMsiInstaller(cmd, createMsiLog, true); + } + + private static void uninstallMsi(JPackageCommand cmd, boolean createMsiLog) { + if (Files.exists(cmd.outputBundle())) { + runMsiInstaller(cmd, createMsiLog, false); + } else { + configureMsiLogFile(cmd, false); + } + } + + private static Path unpackMsi(JPackageCommand cmd, Path destinationDir) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + configureMsiLogFile(cmd, false); + final Path unpackBat = destinationDir.resolve("unpack.bat"); + final Path unpackDir = destinationDir.resolve( + TKit.removeRootFromAbsolutePath( + getInstallationRootDirectory(cmd))); + + final Path msiPath = TransientMsi.create(cmd).path(); + + // Put msiexec in .bat file because can't pass value of TARGETDIR + // property containing spaces through ProcessBuilder properly. + // Set folder permissions to allow msiexec unpack msi bundle. + TKit.createTextFile(unpackBat, List.of( + String.format("icacls \"%s\" /inheritance:e /grant Users:M", + destinationDir), + String.join(" ", List.of( + "msiexec", + "/a", + String.format("\"%s\"", msiPath), + "/qn", + String.format("TARGETDIR=\"%s\"", + unpackDir.toAbsolutePath().normalize()))))); + runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString()), Optional.empty()); + + // + // WiX3 uses "." as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table + // WiX4 uses "PFiles64" as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table + // msiexec creates "Program Files/./" from WiX3 msi which translates to "Program Files/" + // msiexec creates "Program Files/PFiles64/" from WiX4 msi + // So for WiX4 msi we need to transform "Program Files/PFiles64/" into "Program Files/" + // + // WiX4 does the same thing for %LocalAppData%. + // + for (var extraPathComponent : List.of("PFiles64", "LocalApp")) { + if (Files.isDirectory(unpackDir.resolve(extraPathComponent))) { + Path installationSubDirectory = getInstallationSubDirectory(cmd); + Path from = Path.of(extraPathComponent).resolve(installationSubDirectory); + Path to = installationSubDirectory; + TKit.trace(String.format("Convert [%s] into [%s] in [%s] directory", from, to, + unpackDir)); + ThrowingRunnable.toRunnable(() -> { + Files.createDirectories(unpackDir.resolve(to).getParent()); + Files.move(unpackDir.resolve(from), unpackDir.resolve(to)); + TKit.deleteDirectoryRecursive(unpackDir.resolve(extraPathComponent)); + }).run(); } - }; - msi.unpackHandler = (cmd, destinationDir) -> { - cmd.verifyIsOfType(PackageType.WIN_MSI); - final Path unpackBat = destinationDir.resolve("unpack.bat"); - final Path unpackDir = destinationDir.resolve( - TKit.removeRootFromAbsolutePath( - getInstallationRootDirectory(cmd))); + } + return destinationDir; + } - final Path msiPath = TransientMsi.create(cmd).path(); + static PackageHandlers createExePackageHandlers(boolean createMsiLog) { + return new PackageHandlers(cmd -> installExe(cmd, createMsiLog), WindowsHelper::uninstallExe, Optional.empty()); + } - // Put msiexec in .bat file because can't pass value of TARGETDIR - // property containing spaces through ProcessBuilder properly. - // Set folder permissions to allow msiexec unpack msi bundle. - TKit.createTextFile(unpackBat, List.of( - String.format("icacls \"%s\" /inheritance:e /grant Users:M", - destinationDir), - String.join(" ", List.of( - "msiexec", - "/a", - String.format("\"%s\"", msiPath), - "/qn", - String.format("TARGETDIR=\"%s\"", - unpackDir.toAbsolutePath().normalize()))))); - runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString())); + private static int runExeInstaller(JPackageCommand cmd, boolean createMsiLog, boolean install) { + cmd.verifyIsOfType(PackageType.WIN_EXE); + Executor exec = new Executor().setExecutable(cmd.outputBundle()); + if (install) { + exec.addArgument("/qn").addArgument("/norestart"); + } else { + exec.addArgument("uninstall"); + } + return runMsiexecWithRetries(exec, configureMsiLogFile(cmd, createMsiLog)); + } - // - // WiX3 uses "." as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table - // WiX4 uses "PFiles64" as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table - // msiexec creates "Program Files/./" from WiX3 msi which translates to "Program Files/" - // msiexec creates "Program Files/PFiles64/" from WiX4 msi - // So for WiX4 msi we need to transform "Program Files/PFiles64/" into "Program Files/" - // - // WiX4 does the same thing for %LocalAppData%. - // - for (var extraPathComponent : List.of("PFiles64", "LocalApp")) { - if (Files.isDirectory(unpackDir.resolve(extraPathComponent))) { - Path installationSubDirectory = getInstallationSubDirectory(cmd); - Path from = Path.of(extraPathComponent).resolve(installationSubDirectory); - Path to = installationSubDirectory; - TKit.trace(String.format("Convert [%s] into [%s] in [%s] directory", from, to, - unpackDir)); - ThrowingRunnable.toRunnable(() -> { - Files.createDirectories(unpackDir.resolve(to).getParent()); - Files.move(unpackDir.resolve(from), unpackDir.resolve(to)); - TKit.deleteDirectoryRecursive(unpackDir.resolve(extraPathComponent)); - }).run(); - } - } - return destinationDir; - }; - return msi; + private static int installExe(JPackageCommand cmd, boolean createMsiLog) { + return runExeInstaller(cmd, createMsiLog, true); + } + + private static void uninstallExe(JPackageCommand cmd) { + if (Files.exists(cmd.outputBundle())) { + runExeInstaller(cmd, false, false); + } else { + configureMsiLogFile(cmd, false); + } } record TransientMsi(Path path) { @@ -204,28 +257,6 @@ public class WindowsHelper { } } - static PackageHandlers createExePackageHandlers() { - BiConsumer installExe = (cmd, install) -> { - cmd.verifyIsOfType(PackageType.WIN_EXE); - Executor exec = new Executor().setExecutable(cmd.outputBundle()); - if (install) { - exec.addArgument("/qn").addArgument("/norestart"); - } else { - exec.addArgument("uninstall"); - } - runMsiexecWithRetries(exec); - }; - - PackageHandlers exe = new PackageHandlers(); - exe.installHandler = cmd -> installExe.accept(cmd, true); - exe.uninstallHandler = cmd -> { - if (Files.exists(cmd.outputBundle())) { - installExe.accept(cmd, false); - } - }; - return exe; - } - static void verifyDesktopIntegration(JPackageCommand cmd, String launcherName) { new DesktopIntegrationVerifier(cmd, launcherName); @@ -415,14 +446,12 @@ public class WindowsHelper { } private void verifySystemDesktopShortcut(boolean exists) { - Path dir = Path.of(queryRegistryValueCache( - SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop")); + Path dir = SpecialFolder.COMMON_DESKTOP.getPath(); verifyShortcut(dir.resolve(desktopShortcutPath), exists); } private void verifyUserLocalDesktopShortcut(boolean exists) { - Path dir = Path.of( - queryRegistryValueCache(USER_SHELL_FOLDERS_REGKEY, "Desktop")); + Path dir = SpecialFolder.USER_DESKTOP.getPath(); verifyShortcut(dir.resolve(desktopShortcutPath), exists); } @@ -445,19 +474,22 @@ public class WindowsHelper { Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath); verifyShortcut(shortcutPath, exists); if (!exists) { - TKit.assertDirectoryNotEmpty(shortcutPath.getParent()); + final var parentDir = shortcutPath.getParent(); + if (Files.isDirectory(parentDir)) { + TKit.assertDirectoryNotEmpty(parentDir); + } else { + TKit.assertPathExists(parentDir, false); + } } } private void verifySystemStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(Path.of(queryRegistryValueCache( - SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs")), exists); + verifyStartMenuShortcut(SpecialFolder.COMMON_START_MENU_PROGRAMS.getPath(), exists); } private void verifyUserLocalStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(Path.of(queryRegistryValueCache( - USER_SHELL_FOLDERS_REGKEY, "Programs")), exists); + verifyStartMenuShortcut(SpecialFolder.USER_START_MENU_PROGRAMS.getPath(), exists); } private void verifyFileAssociationsRegistry(Path faFile) { @@ -565,16 +597,66 @@ public class WindowsHelper { return value; } - private static String queryRegistryValueCache(String keyPath, - String valueName) { - String key = String.format("[%s][%s]", keyPath, valueName); - String value = REGISTRY_VALUES.get(key); - if (value == null) { - value = queryRegistryValue(keyPath, valueName); - REGISTRY_VALUES.put(key, value); + // See .NET special folders + private enum SpecialFolderDotNet { + Desktop, + CommonDesktop, + + Programs, + CommonPrograms; + + Path getPath() { + final var str = Executor.of("powershell", "-NoLogo", "-NoProfile", + "-NonInteractive", "-Command", + String.format("[Environment]::GetFolderPath('%s')", name()) + ).saveFirstLineOfOutput().execute().getFirstLineOfOutput(); + + TKit.trace(String.format("Value of .NET special folder '%s' is [%s]", name(), str)); + + return Path.of(str); + } + } + + private record RegValuePath(String keyPath, String valueName) { + RegValuePath { + Objects.requireNonNull(keyPath); + Objects.requireNonNull(valueName); } - return value; + Optional findValue() { + return Optional.ofNullable(queryRegistryValue(keyPath, valueName)); + } + } + + private enum SpecialFolder { + COMMON_START_MENU_PROGRAMS(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs", SpecialFolderDotNet.CommonPrograms), + USER_START_MENU_PROGRAMS(USER_SHELL_FOLDERS_REGKEY, "Programs", SpecialFolderDotNet.Programs), + + COMMON_DESKTOP(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop", SpecialFolderDotNet.CommonDesktop), + USER_DESKTOP(USER_SHELL_FOLDERS_REGKEY, "Desktop", SpecialFolderDotNet.Desktop); + + SpecialFolder(String keyPath, String valueName) { + reg = new RegValuePath(keyPath, valueName); + alt = Optional.empty(); + } + + SpecialFolder(String keyPath, String valueName, SpecialFolderDotNet alt) { + reg = new RegValuePath(keyPath, valueName); + this.alt = Optional.of(alt); + } + + Path getPath() { + return CACHE.computeIfAbsent(this, k -> reg.findValue().map(Path::of).orElseGet(() -> { + return alt.map(SpecialFolderDotNet::getPath).orElseThrow(() -> { + return new NoSuchElementException(String.format("Failed to find path to %s folder", name())); + }); + })); + } + + private final RegValuePath reg; + private final Optional alt; + + private final static Map CACHE = new ConcurrentHashMap<>(); } private static final class ShortPathUtils { @@ -617,7 +699,5 @@ public class WindowsHelper { private static final String SYSTEM_SHELL_FOLDERS_REGKEY = "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; private static final String USER_SHELL_FOLDERS_REGKEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; - private static final Map REGISTRY_VALUES = new HashMap<>(); - private static final int WIN_MAX_PATH = 260; } diff --git a/test/jdk/tools/jpackage/resources/fail-os-condition.wxf b/test/jdk/tools/jpackage/resources/fail-os-condition.wxf new file mode 100644 index 00000000000..3b3a79ff2ae --- /dev/null +++ b/test/jdk/tools/jpackage/resources/fail-os-condition.wxf @@ -0,0 +1,32 @@ + + + + + + 0 + + + diff --git a/test/jdk/tools/jpackage/windows/WinOSConditionTest.java b/test/jdk/tools/jpackage/windows/WinOSConditionTest.java new file mode 100644 index 00000000000..3cf4fdd541c --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinOSConditionTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, 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 java.io.IOException; +import java.nio.file.Files; +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.RunnablePackageTest.Action; +import jdk.jpackage.test.TKit; + +/* + * @test + * @summary jpackage test that installer blocks on Windows of older version + * @library /test/jdk/tools/jpackage/helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @compile -Xlint:all -Werror WinOSConditionTest.java + * @requires (os.family == "windows") + * @requires (jpackage.test.SQETest == null) + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinOSConditionTest + */ +public class WinOSConditionTest { + + @Test + public static void test() throws IOException { + // Use custom always failing condition. Installation is expected to fail. + // This way the test covers: + // 1. If jpackage picks custom OS version condition from the resource directory; + // 2. If the installer created by jpackage uses OS version condition. + new PackageTest().ignoreBundleOutputDir() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(JPackageCommand::setFakeRuntime) + .addInitializer(cmd -> { + final var resourceDir = TKit.createTempDirectory("resource-dir"); + Files.copy(TKit.TEST_SRC_ROOT.resolve("resources/fail-os-condition.wxf"), resourceDir.resolve("os-condition.wxf")); + // Create a per-user installer to let user without admin privileges install it. + cmd.addArguments("--win-per-user-install", + "--resource-dir", resourceDir.toString()).setFakeRuntime(); + }) + .addUninstallVerifier(cmd -> { + // MSI error code 1603 is generic. + // Dig into the last msi log file for log messages specific to failed condition. + try (final var lines = cmd.winMsiLogFileContents().orElseThrow()) { + TKit.assertTextStream("Doing action: LaunchConditions").predicate(String::endsWith) + .andThen(TKit.assertTextStream("Not supported on this version of Windows").predicate(String::endsWith)).apply(lines); + } + }) + .createMsiLog(true) + .setExpectedInstallExitCode(1603) + // Create, try install the package (installation should fail) and verify it is not installed. + .run(Action.CREATE, Action.INSTALL, Action.VERIFY_UNINSTALL); + } +}