/* * Copyright (c) 2019, 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.awt.Desktop; import java.awt.GraphicsEnvironment; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.*; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.test.Functional.ThrowingConsumer; import jdk.incubator.jpackage.internal.AppImageFile; import jdk.jpackage.test.Functional.ThrowingBiConsumer; import jdk.jpackage.test.Functional.ThrowingRunnable; import static jdk.jpackage.test.PackageType.*; /** * Instance of PackageTest is for configuring and running a single jpackage * command to produce platform specific package bundle. * * Provides methods to hook up custom configuration of jpackage command and * verification of the output bundle. */ public final class PackageTest extends RunnablePackageTest { public PackageTest() { excludeTypes = new HashSet<>(); forTypes(); setExpectedExitCode(0); namedInitializers = new HashSet<>(); handlers = currentTypes.stream() .collect(Collectors.toMap(v -> v, v -> new Handler())); packageHandlers = createDefaultPackageHandlers(); } public PackageTest excludeTypes(PackageType... types) { excludeTypes.addAll(Stream.of(types).collect(Collectors.toSet())); return forTypes(currentTypes); } public PackageTest excludeTypes(Collection types) { return excludeTypes(types.toArray(PackageType[]::new)); } public PackageTest forTypes(PackageType... types) { Collection newTypes; if (types == null || types.length == 0) { newTypes = PackageType.NATIVE; } else { newTypes = Stream.of(types).collect(Collectors.toSet()); } currentTypes = newTypes.stream() .filter(PackageType::isSupported) .filter(Predicate.not(excludeTypes::contains)) .collect(Collectors.toUnmodifiableSet()); return this; } public PackageTest forTypes(Collection types) { return forTypes(types.toArray(PackageType[]::new)); } public PackageTest notForTypes(PackageType... types) { return notForTypes(List.of(types)); } public PackageTest notForTypes(Collection types) { Set workset = new HashSet<>(currentTypes); workset.removeAll(types); return forTypes(workset); } public PackageTest setExpectedExitCode(int v) { expectedJPackageExitCode = v; return this; } private PackageTest addInitializer(ThrowingConsumer v, String id) { if (id != null) { if (namedInitializers.contains(id)) { return this; } namedInitializers.add(id); } currentTypes.forEach(type -> handlers.get(type).addInitializer( ThrowingConsumer.toConsumer(v))); return this; } private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) { return addInitializer(new ThrowingConsumer() { @Override public void accept(JPackageCommand unused) throws Throwable { if (!executed) { executed = true; v.run(); } } private boolean executed; }, id); } public PackageTest addInitializer(ThrowingConsumer v) { return addInitializer(v, null); } public PackageTest addRunOnceInitializer(ThrowingRunnable v) { return addRunOnceInitializer(v, null); } public PackageTest addBundleVerifier( ThrowingBiConsumer v) { currentTypes.forEach(type -> handlers.get(type).addBundleVerifier( ThrowingBiConsumer.toBiConsumer(v))); return this; } public PackageTest addBundleVerifier(ThrowingConsumer v) { return addBundleVerifier( (cmd, unused) -> ThrowingConsumer.toConsumer(v).accept(cmd)); } public PackageTest addBundlePropertyVerifier(String propertyName, Predicate pred, String predLabel) { return addBundleVerifier(cmd -> { final String value; if (TKit.isLinux()) { value = LinuxHelper.getBundleProperty(cmd, propertyName); } else if (TKit.isWindows()) { value = WindowsHelper.getMsiProperty(cmd, propertyName); } else { throw new IllegalStateException(); } TKit.assertTrue(pred.test(value), String.format( "Check value of %s property %s [%s]", propertyName, predLabel, value)); }); } public PackageTest addBundlePropertyVerifier(String propertyName, String expectedPropertyValue) { return addBundlePropertyVerifier(propertyName, expectedPropertyValue::equals, "is"); } public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) { forTypes(LINUX, () -> { LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated); }); return this; } public PackageTest addInstallVerifier(ThrowingConsumer v) { currentTypes.forEach(type -> handlers.get(type).addInstallVerifier( ThrowingConsumer.toConsumer(v))); return this; } public PackageTest addUninstallVerifier(ThrowingConsumer v) { currentTypes.forEach(type -> handlers.get(type).addUninstallVerifier( ThrowingConsumer.toConsumer(v))); return this; } public PackageTest setPackageInstaller(Consumer v) { currentTypes.forEach( type -> packageHandlers.get(type).installHandler = v); return this; } public PackageTest setPackageUnpacker( BiFunction v) { currentTypes.forEach(type -> packageHandlers.get(type).unpackHandler = v); return this; } public PackageTest setPackageUninstaller(Consumer v) { currentTypes.forEach( type -> packageHandlers.get(type).uninstallHandler = v); return this; } static void withTestFileAssociationsFile(FileAssociations fa, ThrowingConsumer consumer) { final Path testFileDefaultName = Path.of("test" + fa.getSuffix()); TKit.withTempFile(testFileDefaultName, testFile -> { if (TKit.isLinux()) { LinuxHelper.initFileAssociationsTestFile(testFile); } consumer.accept(testFile); }); } PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa, String... faLauncherDefaultArgs) { // Setup test app to have valid jpackage command line before // running check of type of environment. addHelloAppInitializer(null); String noActionMsg = "Not running file associations test"; if (GraphicsEnvironment.isHeadless()) { TKit.trace(String.format( "%s because running in headless environment", noActionMsg)); return this; } addInstallVerifier(cmd -> { if (cmd.isFakeRuntime(noActionMsg) || cmd.isPackageUnpacked(noActionMsg)) { return; } withTestFileAssociationsFile(fa, testFile -> { testFile = testFile.toAbsolutePath().normalize(); final Path appOutput = testFile.getParent() .resolve(HelloApp.OUTPUT_FILENAME); Files.deleteIfExists(appOutput); TKit.trace(String.format("Use desktop to open [%s] file", testFile)); Desktop.getDesktop().open(testFile.toFile()); TKit.waitForFileCreated(appOutput, 7); List expectedArgs = new ArrayList<>(List.of( faLauncherDefaultArgs)); expectedArgs.add(testFile.toString()); // Wait a little bit after file has been created to // make sure there are no pending writes into it. Thread.sleep(3000); HelloApp.verifyOutputFile(appOutput, expectedArgs, Collections.emptyMap()); }); }); forTypes(PackageType.LINUX, () -> { LinuxHelper.addFileAssociationsVerifier(this, fa); }); return this; } public PackageTest forTypes(Collection types, Runnable action) { Set oldTypes = Set.of(currentTypes.toArray( PackageType[]::new)); try { forTypes(types); action.run(); } finally { forTypes(oldTypes); } return this; } public PackageTest forTypes(PackageType type, Runnable action) { return forTypes(List.of(type), action); } public PackageTest notForTypes(Collection types, Runnable action) { Set workset = new HashSet<>(currentTypes); workset.removeAll(types); return forTypes(workset, action); } public PackageTest notForTypes(PackageType type, Runnable action) { return notForTypes(List.of(type), action); } public PackageTest configureHelloApp() { return configureHelloApp(null); } public PackageTest configureHelloApp(String javaAppDesc) { addHelloAppInitializer(javaAppDesc); addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput); return this; } public final static class Group extends RunnablePackageTest { public Group(PackageTest... tests) { handlers = Stream.of(tests) .map(PackageTest::createPackageTypeHandlers) .flatMap(List>::stream) .collect(Collectors.toUnmodifiableList()); } @Override protected void runAction(Action... action) { if (Set.of(action).contains(Action.UNINSTALL)) { ListIterator> listIterator = handlers.listIterator( handlers.size()); while (listIterator.hasPrevious()) { var handler = listIterator.previous(); List.of(action).forEach(handler::accept); } } else { handlers.forEach(handler -> List.of(action).forEach(handler::accept)); } } private final List> handlers; } final static class PackageHandlers { Consumer installHandler; Consumer uninstallHandler; BiFunction unpackHandler; } @Override protected void runActions(List actions) { createPackageTypeHandlers().forEach( handler -> actions.forEach( action -> List.of(action).forEach(handler::accept))); } @Override protected void runAction(Action... action) { throw new UnsupportedOperationException(); } private void addHelloAppInitializer(String javaAppDesc) { addInitializer( cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd), "HelloApp"); } private List> createPackageTypeHandlers() { return PackageType.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()); } private Consumer createPackageTypeHandler( PackageType type, Handler handler) { return ThrowingConsumer.toConsumer(new ThrowingConsumer() { @Override public void accept(Action action) throws Throwable { if (action == Action.FINALIZE) { if (unpackDir != null && Files.isDirectory(unpackDir) && !unpackDir.startsWith(TKit.workDir())) { TKit.deleteDirectoryRecursive(unpackDir); } } if (aborted) { return; } final JPackageCommand curCmd; if (Set.of(Action.INITIALIZE, Action.CREATE).contains(action)) { curCmd = cmd; } else { curCmd = cmd.createImmutableCopy(); } switch (action) { case UNPACK: { var handler = packageHandlers.get(type).unpackHandler; if (!(aborted = (handler == null))) { unpackDir = TKit.createTempDirectory( String.format("unpacked-%s", type.getName())); unpackDir = handler.apply(cmd, unpackDir); cmd.setUnpackedPackageLocation(unpackDir); } break; } case INSTALL: { var handler = packageHandlers.get(type).installHandler; if (!(aborted = (handler == null))) { handler.accept(curCmd); } break; } case UNINSTALL: { var handler = packageHandlers.get(type).uninstallHandler; if (!(aborted = (handler == null))) { handler.accept(curCmd); } break; } case CREATE: handler.accept(action, curCmd); aborted = (expectedJPackageExitCode != 0); return; default: handler.accept(action, curCmd); break; } if (aborted) { TKit.trace( String.format("Aborted [%s] action of %s command", action, cmd.getPrintableCommandLine())); } } private Path unpackDir; private boolean aborted; private final JPackageCommand cmd = Functional.identity(() -> { JPackageCommand result = new JPackageCommand(); result.setDefaultInputOutput().setDefaultAppName(); if (BUNDLE_OUTPUT_DIR != null) { result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString()); } type.applyTo(result); return result; }).get(); }); } private class Handler implements BiConsumer { Handler() { initializers = new ArrayList<>(); bundleVerifiers = new ArrayList<>(); installVerifiers = new ArrayList<>(); uninstallVerifiers = new ArrayList<>(); } boolean isVoid() { return initializers.isEmpty(); } void addInitializer(Consumer v) { initializers.add(v); } void addBundleVerifier(BiConsumer v) { bundleVerifiers.add(v); } void addInstallVerifier(Consumer v) { installVerifiers.add(v); } void addUninstallVerifier(Consumer v) { uninstallVerifiers.add(v); } @Override public void accept(Action action, JPackageCommand cmd) { switch (action) { case INITIALIZE: initializers.forEach(v -> v.accept(cmd)); if (cmd.isImagePackageType()) { throw new UnsupportedOperationException(); } cmd.executePrerequisiteActions(); break; case CREATE: Executor.Result result = cmd.execute(expectedJPackageExitCode); if (expectedJPackageExitCode == 0) { TKit.assertFileExists(cmd.outputBundle()); } else { TKit.assertPathExists(cmd.outputBundle(), false); } verifyPackageBundle(cmd, result); break; case VERIFY_INSTALL: if (expectedJPackageExitCode == 0) { verifyPackageInstalled(cmd); } break; case VERIFY_UNINSTALL: if (expectedJPackageExitCode == 0) { verifyPackageUninstalled(cmd); } break; } } private void verifyPackageBundle(JPackageCommand cmd, Executor.Result result) { if (expectedJPackageExitCode == 0) { if (PackageType.LINUX.contains(cmd.packageType())) { LinuxHelper.verifyPackageBundleEssential(cmd); } } bundleVerifiers.forEach(v -> v.accept(cmd, result)); } private void verifyPackageInstalled(JPackageCommand cmd) { final String formatString; if (cmd.isPackageUnpacked()) { formatString = "Verify unpacked: %s"; } else { formatString = "Verify installed: %s"; } TKit.trace(String.format(formatString, cmd.getPrintableCommandLine())); TKit.assertDirectoryExists(cmd.appRuntimeDirectory()); if (!cmd.isRuntime()) { TKit.assertExecutableFileExists(cmd.appLauncherPath()); if (PackageType.WINDOWS.contains(cmd.packageType()) && !cmd.isPackageUnpacked( "Not verifying desktop integration")) { new WindowsHelper.DesktopIntegrationVerifier(cmd); } } TKit.assertPathExists(AppImageFile.getPathInAppImage( cmd.appInstallationDirectory()), false); installVerifiers.forEach(v -> v.accept(cmd)); } private void verifyPackageUninstalled(JPackageCommand cmd) { TKit.trace(String.format("Verify uninstalled: %s", cmd.getPrintableCommandLine())); if (!cmd.isRuntime()) { TKit.assertPathExists(cmd.appLauncherPath(), false); if (PackageType.WINDOWS.contains(cmd.packageType())) { new WindowsHelper.DesktopIntegrationVerifier(cmd); } } TKit.assertPathExists(cmd.appInstallationDirectory(), false); 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(PackageType.LINUX_DEB, LinuxHelper.createDebPackageHandlers()); handlers.put(PackageType.LINUX_RPM, LinuxHelper.createRpmPackageHandlers()); } if (TKit.isWindows()) { handlers.put(PackageType.WIN_MSI, WindowsHelper.createMsiPackageHandlers()); handlers.put(PackageType.WIN_EXE, WindowsHelper.createExePackageHandlers()); } if (TKit.isOSX()) { handlers.put(PackageType.MAC_DMG, MacHelper.createDmgPackageHandlers()); handlers.put(PackageType.MAC_PKG, MacHelper.createPkgPackageHandlers()); } return handlers; } private Collection currentTypes; private Set excludeTypes; private int expectedJPackageExitCode; private Map handlers; private Set namedInitializers; private Map packageHandlers; private final static File BUNDLE_OUTPUT_DIR; static { final String propertyName = "output"; String val = TKit.getConfigProperty(propertyName); if (val == null) { BUNDLE_OUTPUT_DIR = null; } else { BUNDLE_OUTPUT_DIR = new File(val).getAbsoluteFile(); if (!BUNDLE_OUTPUT_DIR.isDirectory()) { throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory", TKit.getConfigPropertyName(propertyName), BUNDLE_OUTPUT_DIR)); } } } }