changeset 57476:1b1a7893c78a

8233270: Add support to jtreg helpers to unpack packages 8230933: Default icon is not set for additional launchers Reviewed-by: herrick, prr, almatvee
author asemenyuk
date Tue, 17 Dec 2019 13:56:47 -0500
parents cfaa2457a60a
children aaec84f8c400
files src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java test/jdk/tools/jpackage/helpers/JPackageHelper.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java test/jdk/tools/jpackage/linux/MaintainerTest.java test/jdk/tools/jpackage/linux/PackageDepsTest.java test/jdk/tools/jpackage/linux/ReleaseTest.java test/jdk/tools/jpackage/linux/ShortcutHintTest.java test/jdk/tools/jpackage/macosx/base/SigningBase.java test/jdk/tools/jpackage/resources/icon.png test/jdk/tools/jpackage/run_tests.sh test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java test/jdk/tools/jpackage/share/AppImagePackageTest.java test/jdk/tools/jpackage/share/ArgumentsTest.java test/jdk/tools/jpackage/share/IconTest.java test/jdk/tools/jpackage/share/InstallDirTest.java test/jdk/tools/jpackage/share/LicenseTest.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java test/jdk/tools/jpackage/test_jpackage.sh test/jdk/tools/jpackage/windows/WinConsoleTest.java test/jdk/tools/jpackage/windows/WinScriptTest.java test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java
diffstat 44 files changed, 2035 insertions(+), 886 deletions(-) [+]
line wrap: on
line diff
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java	Tue Dec 17 13:56:47 2019 -0500
@@ -48,8 +48,9 @@
     static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
     static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
 
-    DesktopIntegration(PlatformPackage thePackage,
-            Map<String, ? super Object> params) {
+    private DesktopIntegration(PlatformPackage thePackage,
+            Map<String, ? super Object> params,
+            Map<String, ? super Object> mainParams) throws IOException {
 
         associations = FileAssociation.fetchFrom(params).stream()
                 .filter(fa -> !fa.mimeTypes.isEmpty())
@@ -60,11 +61,25 @@
 
         this.thePackage = thePackage;
 
-        final File customIconFile = ICON_PNG.fetchFrom(params);
+        // Need desktop and icon files if one of conditions is met:
+        //  - there are file associations configured
+        //  - user explicitely requested to create a shortcut
+        boolean withDesktopFile = !associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params);
 
-        iconResource = createResource(DEFAULT_ICON, params)
-                .setCategory(I18N.getString("resource.menu-icon"))
-                .setExternal(customIconFile);
+        var curIconResource = LinuxAppImageBuilder.createIconResource(DEFAULT_ICON,
+                ICON_PNG, params, mainParams);
+        if (curIconResource == null) {
+            // This is additional launcher with explicit `no icon` configuration.
+            withDesktopFile = false;
+        } else {
+            final Path nullPath = null;
+            if (curIconResource.saveToFile(nullPath)
+                    != OverridableResource.Source.DefaultResource) {
+                // This launcher has custom icon configured.
+                withDesktopFile = true;
+            }
+        }
+
         desktopFileResource = createResource("template.desktop", params)
                 .setCategory(I18N.getString("resource.menu-shortcut-descriptor"))
                 .setPublicName(APP_NAME.fetchFrom(params) + ".desktop");
@@ -79,27 +94,42 @@
 
         mimeInfoFile = new DesktopFile(mimeInfoFileName);
 
-        if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) {
-            //
-            // Create primary .desktop file if one of conditions is met:
-            // - there are file associations configured
-            // - user explicitely requested to create a shortcut
-            // - custom icon specified
-            //
+        if (withDesktopFile) {
             desktopFile = new DesktopFile(desktopFileName);
             iconFile = new DesktopFile(APP_NAME.fetchFrom(params)
                     + IOUtils.getSuffix(Path.of(DEFAULT_ICON)));
+
+            if (curIconResource == null) {
+                // Create default icon.
+                curIconResource = LinuxAppImageBuilder.createIconResource(
+                        DEFAULT_ICON, ICON_PNG, mainParams, null);
+            }
         } else {
             desktopFile = null;
             iconFile = null;
         }
 
+        iconResource = curIconResource;
+
         desktopFileData = Collections.unmodifiableMap(
                 createDataForDesktopFile(params));
 
-        nestedIntegrations = launchers.stream().map(
-                launcherParams -> new DesktopIntegration(thePackage,
-                        launcherParams)).collect(Collectors.toList());
+        nestedIntegrations = new ArrayList<>();
+        for (var launcherParams : launchers) {
+            launcherParams = AddLauncherArguments.merge(params, launcherParams,
+                    ICON.getID(), ICON_PNG.getID(), ADD_LAUNCHERS.getID(),
+                    FILE_ASSOCIATIONS.getID());
+            nestedIntegrations.add(new DesktopIntegration(thePackage,
+                    launcherParams, params));
+        }
+    }
+
+    static DesktopIntegration create(PlatformPackage thePackage,
+            Map<String, ? super Object> params) throws IOException {
+        if (StandardBundlerParam.isRuntimeInstaller(params)) {
+            return null;
+        }
+        return new DesktopIntegration(thePackage, params, null);
     }
 
     List<String> requiredPackages() {
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java	Tue Dec 17 13:56:47 2019 -0500
@@ -30,10 +30,10 @@
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.text.MessageFormat;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import static jdk.incubator.jpackage.internal.LinuxAppBundler.ICON_PNG;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
 
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
@@ -45,21 +45,6 @@
 
     private final ApplicationLayout appLayout;
 
-    public static final BundlerParamInfo<File> ICON_PNG =
-            new StandardBundlerParam<>(
-            "icon.png",
-            File.class,
-            params -> {
-                File f = ICON.fetchFrom(params);
-                if (f != null && !f.getName().toLowerCase().endsWith(".png")) {
-                    Log.error(MessageFormat.format(I18N.getString(
-                            "message.icon-not-png"), f));
-                    return null;
-                }
-                return f;
-            },
-            (s, p) -> new File(s));
-
     private static ApplicationLayout createAppLayout(Map<String, Object> params,
             Path imageOutDir) {
         return ApplicationLayout.linuxAppImage().resolveAt(
@@ -113,8 +98,6 @@
     @Override
     public void prepareApplicationFiles(Map<String, ? super Object> params)
             throws IOException {
-        Map<String, ? super Object> originalParams = new HashMap<>(params);
-
         appLayout.roots().stream().forEach(dir -> {
             try {
                 IOUtils.writableOutputDir(dir);
@@ -124,7 +107,7 @@
         });
 
         // create the primary launcher
-        createLauncherForEntryPoint(params);
+        createLauncherForEntryPoint(params, null);
 
         // Copy library to the launcher folder
         try (InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
@@ -135,23 +118,20 @@
         List<Map<String, ? super Object>> entryPoints
                 = StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
         for (Map<String, ? super Object> entryPoint : entryPoints) {
-            createLauncherForEntryPoint(
-                    AddLauncherArguments.merge(originalParams, entryPoint));
+            createLauncherForEntryPoint(AddLauncherArguments.merge(params,
+                    entryPoint, ICON.getID(), ICON_PNG.getID()), params);
         }
 
         // Copy class path entries to Java folder
         copyApplication(params);
-
-        // Copy icon to Resources folder
-        copyIcon(params);
     }
 
     @Override
     public void prepareJreFiles(Map<String, ? super Object> params)
             throws IOException {}
 
-    private void createLauncherForEntryPoint(
-            Map<String, ? super Object> params) throws IOException {
+    private void createLauncherForEntryPoint(Map<String, ? super Object> params,
+            Map<String, ? super Object> mainParams) throws IOException {
         // Copy executable to launchers folder
         Path executableFile = appLayout.launchersDirectory().resolve(getLauncherName(params));
         try (InputStream is_launcher =
@@ -163,19 +143,15 @@
         executableFile.toFile().setWritable(true, true);
 
         writeCfgFile(params, getLauncherCfgPath(params).toFile());
-    }
 
-    private void copyIcon(Map<String, ? super Object> params)
-            throws IOException {
-
-        Path iconTarget = appLayout.destktopIntegrationDirectory().resolve(
-                APP_NAME.fetchFrom(params) + IOUtils.getSuffix(Path.of(
-                DEFAULT_ICON)));
-
-        createResource(DEFAULT_ICON, params)
-                .setCategory("icon")
-                .setExternal(ICON_PNG.fetchFrom(params))
-                .saveToFile(iconTarget);
+        var iconResource = createIconResource(DEFAULT_ICON, ICON_PNG, params,
+                mainParams);
+        if (iconResource != null) {
+            Path iconTarget = appLayout.destktopIntegrationDirectory().resolve(
+                    APP_NAME.fetchFrom(params) + IOUtils.getSuffix(Path.of(
+                    DEFAULT_ICON)));
+            iconResource.saveToFile(iconTarget);
+        }
     }
 
     private void copyApplication(Map<String, ? super Object> params)
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java	Tue Dec 17 13:56:47 2019 -0500
@@ -135,11 +135,7 @@
                 }
             }
 
-            if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-                desktopIntegration = new DesktopIntegration(thePackage, params);
-            } else {
-                desktopIntegration = null;
-            }
+            desktopIntegration = DesktopIntegration.create(thePackage, params);
 
             Map<String, String> data = createDefaultReplacementData(params);
             if (desktopIntegration != null) {
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java	Tue Dec 17 13:56:47 2019 -0500
@@ -102,21 +102,6 @@
             params -> IDENTIFIER.fetchFrom(params) + ".",
             (s, p) -> s);
 
-    public static final BundlerParamInfo<File> ICON_ICNS =
-            new StandardBundlerParam<>(
-            "icon.icns",
-            File.class,
-            params -> {
-                File f = ICON.fetchFrom(params);
-                if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
-                    Log.error(MessageFormat.format(
-                            I18N.getString("message.icon-not-icns"), f));
-                    return null;
-                }
-                return f;
-            },
-            (s, p) -> new File(s));
-
     public static boolean validCFBundleVersion(String v) {
         // CFBundleVersion (String - iOS, OS X) specifies the build version
         // number of the bundle, which identifies an iteration (released or
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java	Tue Dec 17 13:56:47 2019 -0500
@@ -29,6 +29,7 @@
 import java.nio.file.Files;
 import java.text.MessageFormat;
 import java.util.*;
+import static jdk.incubator.jpackage.internal.MacAppImageBuilder.ICON_ICNS;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
 
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
@@ -160,7 +161,7 @@
 
         createResource(TEMPLATE_BUNDLE_ICON, params)
                 .setCategory(I18N.getString("resource.volume-icon"))
-                .setExternal(MacAppBundler.ICON_ICNS.fetchFrom(params))
+                .setExternal(ICON_ICNS.fetchFrom(params))
                 .saveToFile(getConfig_VolumeIcon(params));
 
         createResource(null, params)
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java	Tue Dec 17 13:56:47 2019 -0500
@@ -25,23 +25,21 @@
 
 package jdk.incubator.jpackage.internal;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.text.MessageFormat;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
-import java.util.ArrayList;
+import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
 
 import jdk.incubator.jpackage.internal.resources.ResourceLocator;
 
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
 
 /*
  * AbstractAppImageBuilder
@@ -188,4 +186,57 @@
         }
         return sb.toString();
     }
+
+    public static OverridableResource createIconResource(String defaultIconName,
+            BundlerParamInfo<File> iconParam, Map<String, ? super Object> params,
+            Map<String, ? super Object> mainParams) throws IOException {
+
+        if (mainParams != null) {
+            params = AddLauncherArguments.merge(mainParams, params, ICON.getID(),
+                    iconParam.getID());
+        }
+
+        final String resourcePublicName = APP_NAME.fetchFrom(params)
+                + IOUtils.getSuffix(Path.of(defaultIconName));
+
+        IconType iconType = getLauncherIconType(params);
+        if (iconType == IconType.NoIcon) {
+            return null;
+        }
+
+        OverridableResource resource = createResource(defaultIconName, params)
+                .setCategory("icon")
+                .setExternal(iconParam.fetchFrom(params))
+                .setPublicName(resourcePublicName);
+
+        if (iconType == IconType.DefaultOrResourceDirIcon && mainParams != null) {
+            // No icon explicitly configured for this launcher.
+            // Dry-run resource creation to figure out its source.
+            final Path nullPath = null;
+            if (resource.saveToFile(nullPath)
+                    != OverridableResource.Source.ResourceDir) {
+                // No icon in resource dir for this launcher, inherit icon
+                // configured for the main launcher.
+                resource = createIconResource(defaultIconName, iconParam,
+                        mainParams, null).setLogPublicName(resourcePublicName);
+            }
+        }
+
+        return resource;
+    }
+
+    private enum IconType { DefaultOrResourceDirIcon, CustomIcon, NoIcon };
+
+    private static IconType getLauncherIconType(Map<String, ? super Object> params) {
+        File launcherIcon = ICON.fetchFrom(params);
+        if (launcherIcon == null) {
+            return IconType.DefaultOrResourceDirIcon;
+        }
+
+        if (launcherIcon.getName().isEmpty()) {
+            return IconType.NoIcon;
+        }
+
+        return IconType.CustomIcon;
+    }
 }
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java	Tue Dec 17 13:56:47 2019 -0500
@@ -29,6 +29,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.io.File;
+import java.util.List;
 import jdk.incubator.jpackage.internal.Arguments.CLIOptions;
 
 /*
@@ -160,8 +161,10 @@
 
     static Map<String, ? super Object> merge(
             Map<String, ? super Object> original,
-            Map<String, ? super Object> additional) {
+            Map<String, ? super Object> additional, String... exclude) {
         Map<String, ? super Object> tmp = new HashMap<>(original);
+        List.of(exclude).forEach(tmp::remove);
+
         if (additional.containsKey(CLIOptions.MODULE.getId())) {
             tmp.remove(CLIOptions.MAIN_JAR.getId());
             tmp.remove(CLIOptions.APPCLASS.getId());
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java	Tue Dec 17 13:56:47 2019 -0500
@@ -30,10 +30,7 @@
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
@@ -64,6 +61,7 @@
 
     OverridableResource(String defaultName) {
         this.defaultName = defaultName;
+        setSourceOrder(Source.values());
     }
 
     OverridableResource setSubstitutionData(Map<String, String> v) {
@@ -90,6 +88,15 @@
         return setResourceDir(toPath(v));
     }
 
+    enum Source { External, ResourceDir, DefaultResource };
+
+    OverridableResource setSourceOrder(Source... v) {
+        sources = Stream.of(v)
+                .map(source -> Map.entry(source, getHandler(source)))
+                .collect(Collectors.toList());
+        return this;
+    }
+
     /**
      * Set name of file to look for in resource dir.
      *
@@ -104,6 +111,20 @@
         return setPublicName(Path.of(v));
     }
 
+    /**
+     * Set name of file to look for in resource dir to put in verbose log.
+     *
+     * @return this
+     */
+    OverridableResource setLogPublicName(Path v) {
+        logPublicName = v;
+        return this;
+    }
+
+    OverridableResource setLogPublicName(String v) {
+        return setLogPublicName(Path.of(v));
+    }
+
     OverridableResource setExternal(Path v) {
         externalPath = v;
         return this;
@@ -113,57 +134,17 @@
         return setExternal(toPath(v));
     }
 
-    void saveToFile(Path dest) throws IOException {
-        final String printableCategory;
-        if (category != null) {
-            printableCategory = String.format("[%s]", category);
-        } else {
-            printableCategory = "";
-        }
-
-        if (externalPath != null && externalPath.toFile().exists()) {
-            Log.verbose(MessageFormat.format(I18N.getString(
-                    "message.using-custom-resource-from-file"),
-                    printableCategory,
-                    externalPath.toAbsolutePath().normalize()));
-
-            try (InputStream in = Files.newInputStream(externalPath)) {
-                processResourceStream(in, dest);
-            }
-            return;
-        }
-
-        final Path resourceName = Optional.ofNullable(publicName).orElse(
-                dest.getFileName());
-
-        if (resourceDir != null) {
-            final Path customResource = resourceDir.resolve(resourceName);
-            if (customResource.toFile().exists()) {
-                Log.verbose(MessageFormat.format(I18N.getString(
-                        "message.using-custom-resource"), printableCategory,
-                        resourceDir.normalize().toAbsolutePath().relativize(
-                                customResource.normalize().toAbsolutePath())));
-
-                try (InputStream in = Files.newInputStream(customResource)) {
-                    processResourceStream(in, dest);
-                }
-                return;
+    Source saveToFile(Path dest) throws IOException {
+        for (var source: sources) {
+            if (source.getValue().apply(dest)) {
+                return source.getKey();
             }
         }
-
-        if (defaultName != null) {
-            Log.verbose(MessageFormat.format(
-                    I18N.getString("message.using-default-resource"),
-                    defaultName, printableCategory, resourceName));
-
-            try (InputStream in = readDefault(defaultName)) {
-                processResourceStream(in, dest);
-            }
-        }
+        return null;
     }
 
-    void saveToFile(File dest) throws IOException {
-        saveToFile(dest.toPath());
+    Source saveToFile(File dest) throws IOException {
+        return saveToFile(toPath(dest));
     }
 
     static InputStream readDefault(String resourceName) {
@@ -176,6 +157,81 @@
                 RESOURCE_DIR.fetchFrom(params));
     }
 
+    private String getPrintableCategory() {
+        if (category != null) {
+            return String.format("[%s]", category);
+        }
+        return "";
+    }
+
+    private boolean useExternal(Path dest) throws IOException {
+        boolean used = externalPath != null && Files.exists(externalPath);
+        if (used && dest != null) {
+            Log.verbose(MessageFormat.format(I18N.getString(
+                    "message.using-custom-resource-from-file"),
+                    getPrintableCategory(),
+                    externalPath.toAbsolutePath().normalize()));
+
+            try (InputStream in = Files.newInputStream(externalPath)) {
+                processResourceStream(in, dest);
+            }
+        }
+        return used;
+    }
+
+    private boolean useResourceDir(Path dest) throws IOException {
+        boolean used = false;
+
+        if (dest == null && publicName == null) {
+            throw new IllegalStateException();
+        }
+
+        final Path resourceName = Optional.ofNullable(publicName).orElseGet(
+                () -> dest.getFileName());
+
+        if (resourceDir != null) {
+            final Path customResource = resourceDir.resolve(resourceName);
+            used = Files.exists(customResource);
+            if (used && dest != null) {
+                final Path logResourceName;
+                if (logPublicName != null) {
+                    logResourceName = logPublicName.normalize();
+                } else {
+                    logResourceName = resourceName.normalize();
+                }
+
+                Log.verbose(MessageFormat.format(I18N.getString(
+                        "message.using-custom-resource"), getPrintableCategory(),
+                        logResourceName));
+
+                try (InputStream in = Files.newInputStream(customResource)) {
+                    processResourceStream(in, dest);
+                }
+            }
+        }
+
+        return used;
+    }
+
+    private boolean useDefault(Path dest) throws IOException {
+        boolean used = defaultName != null;
+        if (used && dest != null) {
+            final Path resourceName = Optional
+                    .ofNullable(logPublicName)
+                    .orElse(Optional
+                            .ofNullable(publicName)
+                            .orElseGet(() -> dest.getFileName()));
+            Log.verbose(MessageFormat.format(
+                    I18N.getString("message.using-default-resource"),
+                    defaultName, getPrintableCategory(), resourceName));
+
+            try (InputStream in = readDefault(defaultName)) {
+                processResourceStream(in, dest);
+            }
+        }
+        return used;
+    }
+
     private static List<String> substitute(Stream<String> lines,
             Map<String, String> substitutionData) {
         return lines.map(line -> {
@@ -210,10 +266,33 @@
         }
     }
 
+    private SourceHandler getHandler(Source sourceType) {
+        switch (sourceType) {
+            case DefaultResource:
+                return this::useDefault;
+
+            case External:
+                return this::useExternal;
+
+            case ResourceDir:
+                return this::useResourceDir;
+
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
     private Map<String, String> substitutionData;
     private String category;
     private Path resourceDir;
     private Path publicName;
+    private Path logPublicName;
     private Path externalPath;
     private final String defaultName;
+    private List<Map.Entry<Source, SourceHandler>> sources;
+
+    @FunctionalInterface
+    static interface SourceHandler {
+        public boolean apply(Path dest) throws IOException;
+    }
 }
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java	Tue Dec 17 13:56:47 2019 -0500
@@ -37,21 +37,6 @@
     private static final ResourceBundle I18N = ResourceBundle.getBundle(
             "jdk.incubator.jpackage.internal.resources.WinResources");
 
-    static final BundlerParamInfo<File> ICON_ICO =
-            new StandardBundlerParam<>(
-            "icon.ico",
-            File.class,
-            params -> {
-                File f = ICON.fetchFrom(params);
-                if (f != null && !f.getName().toLowerCase().endsWith(".ico")) {
-                    Log.error(MessageFormat.format(
-                            I18N.getString("message.icon-not-ico"), f));
-                    return null;
-                }
-                return f;
-            },
-            (s, p) -> new File(s));
-
     @Override
     public boolean validate(Map<String, ? super Object> params)
             throws ConfigException {
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java	Tue Dec 17 13:56:47 2019 -0500
@@ -152,11 +152,6 @@
         return "app/" + APP_NAME.fetchFrom(params) +".cfg";
     }
 
-    private File getConfig_AppIcon(Map<String, ? super Object> params) {
-        return new File(getConfigRoot(params),
-                APP_NAME.fetchFrom(params) + ".ico");
-    }
-
     private File getConfig_ExecutableProperties(
            Map<String, ? super Object> params) {
         return new File(getConfigRoot(params),
@@ -180,8 +175,6 @@
     @Override
     public void prepareApplicationFiles(Map<String, ? super Object> params)
             throws IOException {
-        Map<String, ? super Object> originalParams = new HashMap<>(params);
-
         try {
             IOUtils.writableOutputDir(root);
             IOUtils.writableOutputDir(binDir);
@@ -191,7 +184,7 @@
         AppImageFile.save(root, params);
 
         // create the .exe launchers
-        createLauncherForEntryPoint(params);
+        createLauncherForEntryPoint(params, null);
 
         // copy the jars
         copyApplication(params);
@@ -207,8 +200,8 @@
         List<Map<String, ? super Object>> entryPoints =
                 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
         for (Map<String, ? super Object> entryPoint : entryPoints) {
-            createLauncherForEntryPoint(
-                    AddLauncherArguments.merge(originalParams, entryPoint));
+            createLauncherForEntryPoint(AddLauncherArguments.merge(params,
+                    entryPoint, ICON.getID(), ICON_ICO.getID()), params);
         }
     }
 
@@ -272,15 +265,18 @@
                 .saveToFile(getConfig_ExecutableProperties(params));
     }
 
-    private void createLauncherForEntryPoint(
-            Map<String, ? super Object> params) throws IOException {
-
-        File iconTarget = getConfig_AppIcon(params);
+    private void createLauncherForEntryPoint(Map<String, ? super Object> params,
+            Map<String, ? super Object> mainParams) throws IOException {
 
-        createResource(TEMPLATE_APP_ICON, params)
-                .setCategory("icon")
-                .setExternal(ICON_ICO.fetchFrom(params))
-                .saveToFile(iconTarget);
+        var iconResource = createIconResource(TEMPLATE_APP_ICON, ICON_ICO, params,
+                mainParams);
+        Path iconTarget = null;
+        if (iconResource != null) {
+            iconTarget = binDir.resolve(APP_NAME.fetchFrom(params) + ".ico");
+            if (null == iconResource.saveToFile(iconTarget)) {
+                iconTarget = null;
+            }
+        }
 
         writeCfgFile(params, root.resolve(
                 getLauncherCfgName(params)).toFile());
@@ -315,8 +311,8 @@
 
                 launcher.setWritable(true);
 
-                if (iconTarget.exists()) {
-                    iconSwap(iconTarget.getAbsolutePath(),
+                if (iconTarget != null) {
+                    iconSwap(iconTarget.toAbsolutePath().toString(),
                             launcher.getAbsolutePath());
                 }
 
@@ -336,9 +332,6 @@
                 executableFile.toFile().setReadOnly();
             }
         }
-
-        Files.copy(iconTarget.toPath(),
-                binDir.resolve(APP_NAME.fetchFrom(params) + ".ico"));
     }
 
     private void copyApplication(Map<String, ? super Object> params)
--- a/test/jdk/tools/jpackage/helpers/JPackageHelper.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/JPackageHelper.java	Tue Dec 17 13:56:47 2019 -0500
@@ -398,10 +398,6 @@
         createModule("Hello.java", "input", "hello", moduleArgs, true);
     }
 
-    public static void createOtherModule() throws Exception {
-        createModule("Other.java", "input-other", "other", null, false);
-    }
-
     private static void createModule(String javaFile, String inputDir, String aName,
             ModuleArgs moduleArgs, boolean createModularJar) throws Exception {
         int retVal;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Tue Dec 17 13:56:47 2019 -0500
@@ -39,6 +39,11 @@
 
 public final class Executor extends CommandArguments<Executor> {
 
+    public static Executor of(String... cmdline) {
+        return new Executor().setExecutable(cmdline[0]).addArguments(
+                Arrays.copyOfRange(cmdline, 1, cmdline.length));
+    }
+
     public Executor() {
         saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE));
     }
@@ -170,7 +175,7 @@
         private List<String> output;
     }
 
-    public Result execute() {
+    public Result executeWithoutExitCodeCheck() {
         if (toolProvider != null && directory != null) {
             throw new IllegalArgumentException(
                     "Can't change directory when using tool provider");
@@ -189,12 +194,20 @@
         }).get();
     }
 
+    public Result execute(int expectedCode) {
+        return executeWithoutExitCodeCheck().assertExitCodeIs(expectedCode);
+    }
+
+    public Result execute() {
+        return execute(0);
+    }
+
     public String executeAndGetFirstLineOfOutput() {
-        return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput();
+        return saveFirstLineOfOutput().execute().getFirstLineOfOutput();
     }
 
     public List<String> executeAndGetOutput() {
-        return saveOutput().execute().assertExitCodeIsZero().getOutput();
+        return saveOutput().execute().getOutput();
     }
 
     private boolean withSavedOutput() {
@@ -203,7 +216,9 @@
     }
 
     private Path executablePath() {
-        if (directory == null || executable.isAbsolute()) {
+        if (directory == null
+                || executable.isAbsolute()
+                || !Set.of(".", "..").contains(executable.getName(0).toString())) {
             return executable;
         }
 
@@ -237,7 +252,7 @@
             sb.append(String.format("; in directory [%s]", directory));
         }
 
-        TKit.trace("Execute " + sb.toString() + "...");
+        trace("Execute " + sb.toString() + "...");
         Process process = builder.start();
 
         List<String> outputLines = null;
@@ -266,7 +281,7 @@
         }
 
         Result reply = new Result(process.waitFor());
-        TKit.trace("Done. Exit code: " + reply.exitCode);
+        trace("Done. Exit code: " + reply.exitCode);
 
         if (outputLines != null) {
             reply.output = Collections.unmodifiableList(outputLines);
@@ -275,10 +290,10 @@
     }
 
     private Result runToolProvider(PrintStream out, PrintStream err) {
-        TKit.trace("Execute " + getPrintableCommandLine() + "...");
+        trace("Execute " + getPrintableCommandLine() + "...");
         Result reply = new Result(toolProvider.run(out, err, args.toArray(
                 String[]::new)));
-        TKit.trace("Done. Exit code: " + reply.exitCode);
+        trace("Done. Exit code: " + reply.exitCode);
         return reply;
     }
 
@@ -353,6 +368,10 @@
                         Collectors.joining(" "));
     }
 
+    private static void trace(String msg) {
+        TKit.trace(String.format("exec: %s", msg));
+    }
+
     private ToolProvider toolProvider;
     private Path executable;
     private Set<SaveOutputType> saveOutputType;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java	Tue Dec 17 13:56:47 2019 -0500
@@ -23,10 +23,7 @@
 package jdk.jpackage.test;
 
 import java.lang.reflect.InvocationTargetException;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
+import java.util.function.*;
 
 
 public class Functional {
@@ -46,6 +43,21 @@
     }
 
     @FunctionalInterface
+    public interface ThrowingBiConsumer<T, U> {
+        void accept(T t, U u) throws Throwable;
+
+        public static <T, U> BiConsumer<T, U> toBiConsumer(ThrowingBiConsumer<T, U> v) {
+            return (t, u) -> {
+                try {
+                    v.accept(t, u);
+                } catch (Throwable ex) {
+                    rethrowUnchecked(ex);
+                }
+            };
+        }
+    }
+
+    @FunctionalInterface
     public interface ThrowingSupplier<T> {
         T get() throws Throwable;
 
@@ -102,6 +114,10 @@
         return v;
     }
 
+    public static <T, U> BiConsumer<T, U> identity(BiConsumer<T, U> v) {
+        return v;
+    }
+
     public static Runnable identity(Runnable v) {
         return v;
     }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Tue Dec 17 13:56:47 2019 -0500
@@ -26,16 +26,16 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import jdk.jpackage.test.Functional.ThrowingFunction;
 import jdk.jpackage.test.Functional.ThrowingSupplier;
 
-public class HelloApp {
+public final class HelloApp {
 
     HelloApp(JavaAppDesc appDesc) {
         if (appDesc == null) {
@@ -131,13 +131,13 @@
 
         if (moduleName == null && CLASS_NAME.equals(qualifiedClassName)) {
             // Use Hello.java as is.
-            cmd.addAction((self) -> {
+            cmd.addPrerequisiteAction((self) -> {
                 Path jarFile = self.inputDir().resolve(jarFileName);
                 createJarBuilder().setOutputJar(jarFile).addSourceFile(
                         HELLO_JAVA).create();
             });
         } else {
-            cmd.addAction((self) -> {
+            cmd.addPrerequisiteAction((self) -> {
                 final Path jarFile;
                 if (moduleName == null) {
                     jarFile = self.inputDir().resolve(jarFileName);
@@ -177,9 +177,11 @@
                 "hello.jar");
     }
 
-    static void verifyOutputFile(Path outputFile, List<String> args) {
+    static void verifyOutputFile(Path outputFile, List<String> args,
+            Map<String, String> params) {
         if (!outputFile.isAbsolute()) {
-            verifyOutputFile(outputFile.toAbsolutePath().normalize(), args);
+            verifyOutputFile(outputFile.toAbsolutePath().normalize(), args,
+                    params);
             return;
         }
 
@@ -193,38 +195,121 @@
                 String.format("args.length: %d", args.size())
         ));
         expected.addAll(args);
+        expected.addAll(params.entrySet().stream()
+                .sorted(Comparator.comparing(Map.Entry::getKey))
+                .map(entry -> String.format("-D%s=%s", entry.getKey(),
+                        entry.getValue()))
+                .collect(Collectors.toList()));
 
         TKit.assertStringListEquals(expected, contents, String.format(
                 "Check contents of [%s] file", outputFile));
     }
 
-    public static void executeLauncherAndVerifyOutput(JPackageCommand cmd) {
+    public static void executeLauncherAndVerifyOutput(JPackageCommand cmd,
+            String... args) {
         final Path launcherPath = cmd.appLauncherPath();
-        if (!cmd.isFakeRuntime(String.format("Not running [%s] launcher",
+        if (cmd.isFakeRuntime(String.format("Not running [%s] launcher",
                 launcherPath))) {
-            executeAndVerifyOutput(launcherPath, cmd.getAllArgumentValues(
-                    "--arguments"));
+            return;
         }
-    }
 
-    public static void executeAndVerifyOutput(Path helloAppLauncher,
-            String... defaultLauncherArgs) {
-        executeAndVerifyOutput(helloAppLauncher, List.of(defaultLauncherArgs));
+        assertApp(launcherPath)
+        .addDefaultArguments(Optional
+                .ofNullable(cmd.getAllArgumentValues("--arguments"))
+                .orElseGet(() -> new String[0]))
+        .addJavaOptions(Optional
+                .ofNullable(cmd.getAllArgumentValues("--java-options"))
+                .orElseGet(() -> new String[0]))
+        .executeAndVerifyOutput(args);
     }
 
-    public static void executeAndVerifyOutput(Path helloAppLauncher,
-            List<String> defaultLauncherArgs) {
-        // Output file will be created in the current directory.
-        Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
-        ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
-        new Executor()
-                .setDirectory(outputFile.getParent())
-                .setExecutable(helloAppLauncher)
-                .dumpOutput()
-                .execute()
-                .assertExitCodeIsZero();
+    public final static class AppOutputVerifier {
+        AppOutputVerifier(Path helloAppLauncher) {
+            this.launcherPath = helloAppLauncher;
+            this.params = new HashMap<>();
+            this.defaultLauncherArgs = new ArrayList<>();
+        }
+
+        public AppOutputVerifier addDefaultArguments(String... v) {
+            return addDefaultArguments(List.of(v));
+        }
+
+        public AppOutputVerifier addDefaultArguments(Collection<String> v) {
+            defaultLauncherArgs.addAll(v);
+            return this;
+        }
+
+        public AppOutputVerifier addParam(String name, String value) {
+            if (name.startsWith("param")) {
+                params.put(name, value);
+            }
+            return this;
+        }
+
+        public AppOutputVerifier addParams(Collection<Map.Entry<String, String>> v) {
+            v.forEach(entry -> addParam(entry.getKey(), entry.getValue()));
+            return this;
+        }
+        public AppOutputVerifier addParams(Map<String, String> v) {
+            return addParams(v.entrySet());
+        }
+
+        public AppOutputVerifier addParams(Map.Entry<String, String>... v) {
+            return addParams(List.of(v));
+        }
+
+        public AppOutputVerifier addJavaOptions(String... v) {
+            return addJavaOptions(List.of(v));
+        }
 
-        verifyOutputFile(outputFile, defaultLauncherArgs);
+        public AppOutputVerifier addJavaOptions(Collection<String> v) {
+            return addParams(v.stream()
+            .filter(javaOpt -> javaOpt.startsWith("-D"))
+            .map(javaOpt -> {
+                var components = javaOpt.split("=", 2);
+                return Map.entry(components[0].substring(2), components[1]);
+            })
+            .collect(Collectors.toList()));
+        }
+
+        public void executeAndVerifyOutput(String... args) {
+            // Output file will be created in the current directory.
+            Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
+            ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
+
+            final Path executablePath;
+            if (launcherPath.isAbsolute()) {
+                executablePath = launcherPath;
+            } else {
+                // Make sure path to executable is relative to the current directory.
+                executablePath = Path.of(".").resolve(launcherPath.normalize());
+            }
+
+            final List<String> launcherArgs = List.of(args);
+            new Executor()
+                    .setDirectory(outputFile.getParent())
+                    .setExecutable(executablePath)
+                    .addArguments(launcherArgs)
+                    .dumpOutput()
+                    .execute();
+
+            final List<String> appArgs;
+            if (launcherArgs.isEmpty()) {
+                appArgs = defaultLauncherArgs;
+            } else {
+                appArgs = launcherArgs;
+            }
+
+            verifyOutputFile(outputFile, appArgs, params);
+        }
+
+        private final Path launcherPath;
+        private final List<String> defaultLauncherArgs;
+        private final Map<String, String> params;
+    }
+
+    public static AppOutputVerifier assertApp(Path helloAppLauncher) {
+        return new AppOutputVerifier(helloAppLauncher);
     }
 
     final static String OUTPUT_FILENAME = "appOutput.txt";
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Tue Dec 17 13:56:47 2019 -0500
@@ -30,14 +30,15 @@
 import java.util.*;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import jdk.incubator.jpackage.internal.ApplicationLayout;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
 import jdk.jpackage.test.Functional.ThrowingFunction;
+import jdk.jpackage.test.Functional.ThrowingSupplier;
 
 /**
  * jpackage command line with prerequisite actions. Prerequisite actions can be
@@ -47,18 +48,20 @@
 public final class JPackageCommand extends CommandArguments<JPackageCommand> {
 
     public JPackageCommand() {
-        actions = new ArrayList<>();
+        prerequisiteActions = new Actions();
+        verifyActions = new Actions();
     }
 
     public JPackageCommand(JPackageCommand cmd) {
-        this();
         args.addAll(cmd.args);
         withToolProvider = cmd.withToolProvider;
         saveConsoleOutput = cmd.saveConsoleOutput;
         suppressOutput = cmd.suppressOutput;
         ignoreDefaultRuntime = cmd.ignoreDefaultRuntime;
+        ignoreDefaultVerbose = cmd.ignoreDefaultVerbose;
         immutable = cmd.immutable;
-        actionsExecuted = cmd.actionsExecuted;
+        prerequisiteActions = new Actions(cmd.prerequisiteActions);
+        verifyActions = new Actions(cmd.verifyActions);
     }
 
     JPackageCommand createImmutableCopy() {
@@ -204,8 +207,8 @@
     }
 
     public JPackageCommand setDefaultInputOutput() {
-        addArguments("--input", TKit.defaultInputDir());
-        addArguments("--dest", TKit.defaultOutputDir());
+        setArgumentValue("--input", TKit.workDir().resolve("input"));
+        setArgumentValue("--dest", TKit.workDir().resolve("output"));
         return this;
     }
 
@@ -221,7 +224,7 @@
             }
         };
 
-        addAction(cmd -> {
+        addPrerequisiteAction(cmd -> {
             Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime");
 
             TKit.trace(String.format("Init fake runtime in [%s] directory",
@@ -254,9 +257,15 @@
         return this;
     }
 
-    JPackageCommand addAction(ThrowingConsumer<JPackageCommand> action) {
+    JPackageCommand addPrerequisiteAction(ThrowingConsumer<JPackageCommand> action) {
         verifyMutable();
-        actions.add(ThrowingConsumer.toConsumer(action));
+        prerequisiteActions.add(action);
+        return this;
+    }
+
+    JPackageCommand addVerifyAction(ThrowingConsumer<JPackageCommand> action) {
+        verifyMutable();
+        verifyActions.add(action);
         return this;
     }
 
@@ -363,6 +372,12 @@
      * `/opt/foo`
      */
     public Path appInstallationDirectory() {
+        Path unpackedDir = getArgumentValue(UNPACKED_PATH_ARGNAME, () -> null,
+                Path::of);
+        if (unpackedDir != null) {
+            return unpackedDir;
+        }
+
         if (isImagePackageType()) {
             return null;
         }
@@ -426,7 +441,7 @@
             launcherName = launcherName + ".exe";
         }
 
-        if (isImagePackageType()) {
+        if (isImagePackageType() || isPackageUnpacked()) {
             return appLayout().launchersDirectory().resolve(launcherName);
         }
 
@@ -496,6 +511,19 @@
         return false;
     }
 
+    public boolean isPackageUnpacked(String msg) {
+        if (isPackageUnpacked()) {
+            TKit.trace(String.format(
+                    "%s because package was unpacked, not installed", msg));
+            return true;
+        }
+        return false;
+    }
+
+    boolean isPackageUnpacked() {
+        return hasArgument(UNPACKED_PATH_ARGNAME);
+    }
+
     public static void useToolProviderByDefault() {
         defaultWithToolProvider = true;
     }
@@ -528,24 +556,28 @@
         return this;
     }
 
+    public JPackageCommand ignoreDefaultVerbose(boolean v) {
+        verifyMutable();
+        ignoreDefaultVerbose = v;
+        return this;
+    }
+
     public boolean isWithToolProvider() {
         return Optional.ofNullable(withToolProvider).orElse(
                 defaultWithToolProvider);
     }
 
     public JPackageCommand executePrerequisiteActions() {
-        verifyMutable();
-        if (!actionsExecuted) {
-            actionsExecuted = true;
-            if (actions != null) {
-                actions.stream().forEach(r -> r.accept(this));
-            }
-        }
+        prerequisiteActions.run();
         return this;
     }
 
-    public Executor createExecutor() {
-        verifyMutable();
+    public JPackageCommand executeVerifyActions() {
+        verifyActions.run();
+        return this;
+    }
+
+    private Executor createExecutor() {
         Executor exec = new Executor()
                 .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
                 .addArguments(args);
@@ -560,27 +592,60 @@
     }
 
     public Executor.Result execute() {
+        return execute(0);
+    }
+
+    public Executor.Result execute(int expectedExitCode) {
         executePrerequisiteActions();
 
         if (isImagePackageType()) {
             TKit.deleteDirectoryContentsRecursive(outputDir());
+        } else if (ThrowingSupplier.toSupplier(() -> Files.deleteIfExists(
+                outputBundle())).get()) {
+            TKit.trace(
+                    String.format("Deleted [%s] file before running jpackage",
+                            outputBundle()));
         }
 
-        return new JPackageCommand(this)
+        Path resourceDir = getArgumentValue("--resource-dir", () -> null, Path::of);
+        if (resourceDir != null && Files.isDirectory(resourceDir)) {
+            TKit.trace(String.format("Files in [%s] resource dir:",
+                    resourceDir));
+            try (var files = Files.walk(resourceDir, 1)) {
+                files.sequential()
+                        .filter(Predicate.not(resourceDir::equals))
+                        .map(path -> String.format("[%s]", path.getFileName()))
+                        .forEachOrdered(TKit::trace);
+                TKit.trace("Done");
+            } catch (IOException ex) {
+                TKit.trace(String.format(
+                        "Failed to list files in [%s] resource directory: %s",
+                        resourceDir, ex));
+            }
+        }
+
+        Executor.Result result = new JPackageCommand(this)
                 .adjustArgumentsBeforeExecution()
                 .createExecutor()
-                .execute();
+                .execute(expectedExitCode);
+
+        if (result.exitCode == 0) {
+            executeVerifyActions();
+        }
+
+        return result;
     }
 
-    public JPackageCommand executeAndAssertHelloAppImageCreated() {
-        executeAndAssertImageCreated();
+    public Executor.Result executeAndAssertHelloAppImageCreated() {
+        Executor.Result result = executeAndAssertImageCreated();
         HelloApp.executeLauncherAndVerifyOutput(this);
-        return this;
+        return result;
     }
 
-    public JPackageCommand executeAndAssertImageCreated() {
-        execute().assertExitCodeIsZero();
-        return assertImageCreated();
+    public Executor.Result executeAndAssertImageCreated() {
+        Executor.Result result = execute();
+        assertImageCreated();
+        return result;
     }
 
     public JPackageCommand assertImageCreated() {
@@ -595,23 +660,26 @@
         return this;
     }
 
+    JPackageCommand setUnpackedPackageLocation(Path path) {
+        verifyIsOfType(PackageType.NATIVE);
+        setArgumentValue(UNPACKED_PATH_ARGNAME, path);
+        return this;
+    }
+
     private JPackageCommand adjustArgumentsBeforeExecution() {
         if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) {
             addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
         }
 
-        if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE) {
+        if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE && !ignoreDefaultVerbose) {
             addArgument("--verbose");
         }
 
         return this;
     }
 
-    String getPrintableCommandLine() {
-        return new Executor()
-                .setExecutable(JavaTool.JPACKAGE)
-                .addArguments(args)
-                .getPrintableCommandLine();
+    public String getPrintableCommandLine() {
+        return createExecutor().getPrintableCommandLine();
     }
 
     public void verifyIsOfType(Collection<PackageType> types) {
@@ -699,13 +767,48 @@
         return !immutable;
     }
 
+    private final class Actions implements Runnable {
+        Actions() {
+            actions = new ArrayList<>();
+        }
+
+        Actions(Actions other) {
+            this();
+            actions.addAll(other.actions);
+        }
+
+        void add(ThrowingConsumer<JPackageCommand> action) {
+            Objects.requireNonNull(action);
+            verifyMutable();
+            actions.add(new Consumer<JPackageCommand>() {
+                @Override
+                public void accept(JPackageCommand t) {
+                    if (!executed) {
+                        executed = true;
+                        ThrowingConsumer.toConsumer(action).accept(t);
+                    }
+                }
+                private boolean executed;
+            });
+        }
+
+        @Override
+        public void run() {
+            verifyMutable();
+            actions.forEach(action -> action.accept(JPackageCommand.this));
+        }
+
+        private final List<Consumer<JPackageCommand>> actions;
+    }
+
     private Boolean withToolProvider;
     private boolean saveConsoleOutput;
     private boolean suppressOutput;
     private boolean ignoreDefaultRuntime;
+    private boolean ignoreDefaultVerbose;
     private boolean immutable;
-    private boolean actionsExecuted;
-    private final List<Consumer<JPackageCommand>> actions;
+    private final Actions prerequisiteActions;
+    private final Actions verifyActions;
     private static boolean defaultWithToolProvider;
 
     private final static Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
@@ -729,4 +832,6 @@
         }
         return null;
     }).get();
+
+    private final static String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder";
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Tue Dec 17 13:56:47 2019 -0500
@@ -67,7 +67,7 @@
                         .setToolProvider(JavaTool.JAVAC)
                         .addArguments("-d", workDir.toString())
                         .addPathArguments(sourceFiles)
-                        .execute().assertExitCodeIsZero();
+                        .execute();
             }
 
             Files.createDirectories(outputJar.getParent());
@@ -87,7 +87,7 @@
                 jarExe.addArguments("-e", mainClass);
             }
             jarExe.addArguments("-C", workDir.toString(), ".");
-            jarExe.execute().assertExitCodeIsZero();
+            jarExe.execute();
         });
     }
     private List<Path> sourceFiles;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Tue Dec 17 13:56:47 2019 -0500
@@ -25,14 +25,11 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jdk.jpackage.test.PackageTest.PackageHandlers;
 
 public class LinuxHelper {
     private static String getRelease(JPackageCommand cmd) {
@@ -45,6 +42,19 @@
                 () -> cmd.name().toLowerCase());
     }
 
+    public static Path getDesktopFile(JPackageCommand cmd) {
+        return getDesktopFile(cmd, null);
+    }
+
+    public static Path getDesktopFile(JPackageCommand cmd, String launcherName) {
+        cmd.verifyIsOfType(PackageType.LINUX);
+        String desktopFileName = String.format("%s-%s.desktop", getPackageName(
+                cmd), Optional.ofNullable(launcherName).orElseGet(
+                        () -> cmd.name()));
+        return cmd.appLayout().destktopIntegrationDirectory().resolve(
+                desktopFileName);
+    }
+
     static String getBundleName(JPackageCommand cmd) {
         cmd.verifyIsOfType(PackageType.LINUX);
 
@@ -73,18 +83,14 @@
         final PackageType packageType = cmd.packageType();
         final Path packageFile = cmd.outputBundle();
 
-        Executor exec = new Executor();
+        Executor exec = null;
         switch (packageType) {
             case LINUX_DEB:
-                exec.setExecutable("dpkg")
-                        .addArgument("--contents")
-                        .addArgument(packageFile);
+                exec = Executor.of("dpkg", "--contents").addArgument(packageFile);
                 break;
 
             case LINUX_RPM:
-                exec.setExecutable("rpm")
-                        .addArgument("-qpl")
-                        .addArgument(packageFile);
+                exec = Executor.of("rpm", "-qpl").addArgument(packageFile);
                 break;
         }
 
@@ -109,8 +115,8 @@
                         Collectors.toList());
 
             case LINUX_RPM:
-                return new Executor().setExecutable("rpm")
-                .addArguments("-qp", "-R", cmd.outputBundle().toString())
+                return Executor.of("rpm", "-qp", "-R")
+                .addArgument(cmd.outputBundle())
                 .executeAndGetOutput();
         }
         // Unreachable
@@ -141,6 +147,57 @@
         return null;
     }
 
+    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);
+            Executor.of("sudo", "dpkg", "-r", getPackageName(cmd)).execute();
+        };
+        deb.unpackHandler = (cmd, destinationDir) -> {
+            cmd.verifyIsOfType(PackageType.LINUX_DEB);
+            Executor.of("dpkg", "-x")
+            .addArgument(cmd.outputBundle())
+            .addArgument(destinationDir)
+            .execute();
+            return destinationDir.resolve(String.format(".%s",
+                    cmd.appInstallationDirectory())).normalize();
+        };
+        return deb;
+    }
+
+    static PackageHandlers createRpmPackageHandlers() {
+        PackageHandlers rpm = new PackageHandlers();
+        rpm.installHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.LINUX_RPM);
+            Executor.of("sudo", "rpm", "-i")
+            .addArgument(cmd.outputBundle())
+            .execute();
+        };
+        rpm.uninstallHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.LINUX_RPM);
+            Executor.of("sudo", "rpm", "-e", getPackageName(cmd)).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.resolve(String.format(".%s",
+                    cmd.appInstallationDirectory())).normalize();
+        };
+
+        return rpm;
+    }
+
     static Path getLauncherPath(JPackageCommand cmd) {
         cmd.verifyIsOfType(PackageType.LINUX);
 
@@ -173,20 +230,15 @@
     }
 
     static String getDebBundleProperty(Path bundle, String fieldName) {
-        return new Executor()
-                .setExecutable("dpkg-deb")
-                .addArguments("-f", bundle.toString(), fieldName)
+        return Executor.of("dpkg-deb", "-f")
+                .addArgument(bundle)
+                .addArgument(fieldName)
                 .executeAndGetFirstLineOfOutput();
     }
 
     static String getRpmBundleProperty(Path bundle, String fieldName) {
-        return new Executor()
-                .setExecutable("rpm")
-                .addArguments(
-                        "-qp",
-                        "--queryformat",
-                        String.format("%%{%s}", fieldName),
-                        bundle.toString())
+        return Executor.of("rpm", "-qp", "--queryformat", String.format("%%{%s}", fieldName))
+                .addArgument(bundle)
                 .executeAndGetFirstLineOfOutput();
     }
 
@@ -264,13 +316,10 @@
         test.addBundleVerifier(cmd -> {
             TKit.withTempDirectory("dpkg-control-files", tempDir -> {
                 // Extract control Debian package files into temporary directory
-                new Executor()
-                .setExecutable("dpkg")
-                .addArguments(
-                        "-e",
-                        cmd.outputBundle().toString(),
-                        tempDir.toString()
-                ).execute().assertExitCodeIsZero();
+                Executor.of("dpkg", "-e")
+                .addArgument(cmd.outputBundle())
+                .addArgument(tempDir)
+                .execute();
 
                 Path controlFile = Path.of("postinst");
 
@@ -318,6 +367,10 @@
 
     static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) {
         test.addInstallVerifier(cmd -> {
+            if (cmd.isPackageUnpacked("Not running file associations checks")) {
+                return;
+            }
+
             PackageTest.withTestFileAssociationsFile(fa, testFile -> {
                 String mimeType = queryFileMimeType(testFile);
 
@@ -363,16 +416,12 @@
     }
 
     private static String queryFileMimeType(Path file) {
-        return new Executor()
-                .setExecutable("xdg-mime")
-                .addArguments("query", "filetype", file.toString())
+        return Executor.of("xdg-mime", "query", "filetype").addArgument(file)
                 .executeAndGetFirstLineOfOutput();
     }
 
     private static String queryMimeTypeDefaultHandler(String mimeType) {
-        return new Executor()
-                .setExecutable("xdg-mime")
-                .addArguments("query", "default", mimeType)
+        return Executor.of("xdg-mime", "query", "default", mimeType)
                 .executeAndGetFirstLineOfOutput();
     }
 
@@ -383,16 +432,14 @@
 
         String arch = archs.get(type);
         if (arch == null) {
-            Executor exec = new Executor();
+            Executor exec = null;
             switch (type) {
                 case LINUX_DEB:
-                    exec.setExecutable("dpkg").addArgument(
-                            "--print-architecture");
+                    exec = Executor.of("dpkg", "--print-architecture");
                     break;
 
                 case LINUX_RPM:
-                    exec.setExecutable("rpmbuild").addArgument(
-                            "--eval=%{_target_cpu}");
+                    exec = Executor.of("rpmbuild", "--eval=%{_target_cpu}");
                     break;
             }
             arch = exec.executeAndGetFirstLineOfOutput();
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java	Tue Dec 17 13:56:47 2019 -0500
@@ -29,6 +29,7 @@
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.xml.parsers.DocumentBuilder;
@@ -39,6 +40,7 @@
 import javax.xml.xpath.XPathFactory;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
 import jdk.jpackage.test.Functional.ThrowingSupplier;
+import jdk.jpackage.test.PackageTest.PackageHandlers;
 import org.xml.sax.SAXException;
 
 public class MacHelper {
@@ -47,10 +49,12 @@
             ThrowingConsumer<Path> consumer) {
         cmd.verifyIsOfType(PackageType.MAC_DMG);
 
-        var plist = readPList(new Executor()
-                .setExecutable("/usr/bin/hdiutil")
+        // Explode DMG assuming this can require interaction, thus use `yes`.
+        var plist = readPList(Executor.of("sh", "-c",
+                String.join(" ", "yes", "|", "/usr/bin/hdiutil", "attach",
+                        JPackageCommand.escapeAndJoin(
+                                cmd.outputBundle().toString()), "-plist"))
                 .dumpOutput()
-                .addArguments("attach", cmd.outputBundle().toString(), "-plist")
                 .executeAndGetOutput());
 
         final Path mountPoint = Path.of(plist.queryValue("mount-point"));
@@ -60,10 +64,7 @@
                     cmd.outputBundle(), dmgImage));
             ThrowingConsumer.toConsumer(consumer).accept(dmgImage);
         } finally {
-            new Executor()
-                    .setExecutable("/usr/bin/hdiutil")
-                    .addArgument("detach").addArgument(mountPoint)
-                    .execute().assertExitCodeIsZero();
+            Executor.of("/usr/bin/hdiutil", "detach").addArgument(mountPoint).execute();
         }
     }
 
@@ -82,8 +83,62 @@
     }
 
     public static PListWrapper readPList(Stream<String> lines) {
-        return ThrowingSupplier.toSupplier(() -> new PListWrapper(lines.collect(
-                Collectors.joining()))).get();
+        return ThrowingSupplier.toSupplier(() -> new PListWrapper(lines
+                // Skip leading lines before xml declaration
+                .dropWhile(Pattern.compile("\\s?<\\?xml\\b.+\\?>").asPredicate().negate())
+                .collect(Collectors.joining()))).get();
+    }
+
+    static PackageHandlers createDmgPackageHandlers() {
+        PackageHandlers dmg = new PackageHandlers();
+
+        dmg.installHandler = cmd -> {
+            withExplodedDmg(cmd, dmgImage -> {
+                Executor.of("sudo", "cp", "-r")
+                .addArgument(dmgImage)
+                .addArgument("/Applications")
+                .execute();
+            });
+        };
+        dmg.unpackHandler = (cmd, destinationDir) -> {
+            Path[] unpackedFolder = new Path[1];
+            withExplodedDmg(cmd, dmgImage -> {
+                Executor.of("cp", "-r")
+                .addArgument(dmgImage)
+                .addArgument(destinationDir)
+                .execute();
+                unpackedFolder[0] = destinationDir.resolve(dmgImage.getFileName());
+            });
+            return unpackedFolder[0];
+        };
+        dmg.uninstallHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.MAC_DMG);
+            Executor.of("sudo", "rm", "-rf")
+            .addArgument(cmd.appInstallationDirectory())
+            .execute();
+        };
+
+        return dmg;
+    }
+
+    static PackageHandlers createPkgPackageHandlers() {
+        PackageHandlers pkg = new PackageHandlers();
+
+        pkg.installHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.MAC_PKG);
+            Executor.of("sudo", "/usr/sbin/installer", "-allowUntrusted", "-pkg")
+            .addArgument(cmd.outputBundle())
+            .addArguments("-target", "/")
+            .execute();
+        };
+        pkg.uninstallHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.MAC_PKG);
+            Executor.of("sudo", "rm", "-rf")
+            .addArgument(cmd.appInstallationDirectory())
+            .execute();
+        };
+
+        return pkg;
     }
 
     static String getBundleName(JPackageCommand cmd) {
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -28,14 +28,13 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.*;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
+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.*;
 
 /**
@@ -45,25 +44,16 @@
  * Provides methods to hook up custom configuration of jpackage command and
  * verification of the output bundle.
  */
-public final class PackageTest {
+public final class PackageTest extends RunnablePackageTest {
 
-    /**
-     * Default test configuration for jpackage command. Default jpackage command
-     * initialization includes:
-     * <li>Set --input and --dest parameters.
-     * <li>Set --name parameter. Value of the parameter is the name of the first
-     * class with main function found in the callers stack. Defaults can be
-     * overridden with custom initializers set with subsequent addInitializer()
-     * function calls.
-     */
     public PackageTest() {
-        action = DEFAULT_ACTION;
         excludeTypes = new HashSet<>();
         forTypes();
         setExpectedExitCode(0);
-        handlers = new HashMap<>();
         namedInitializers = new HashSet<>();
-        currentTypes.forEach(v -> handlers.put(v, new Handler(v)));
+        handlers = currentTypes.stream()
+                .collect(Collectors.toMap(v -> v, v -> new Handler()));
+        packageHandlers = createDefaultPackageHandlers();
     }
 
     public PackageTest excludeTypes(PackageType... types) {
@@ -117,19 +107,37 @@
 
             namedInitializers.add(id);
         }
-        currentTypes.stream().forEach(type -> handlers.get(type).addInitializer(
+        currentTypes.forEach(type -> handlers.get(type).addInitializer(
                 ThrowingConsumer.toConsumer(v)));
         return this;
     }
 
+    private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) {
+        return addInitializer(new ThrowingConsumer<JPackageCommand>() {
+            @Override
+            public void accept(JPackageCommand unused) throws Throwable {
+                if (!executed) {
+                    executed = true;
+                    v.run();
+                }
+            }
+
+            private boolean executed;
+        }, id);
+    }
+
     public PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v) {
         return addInitializer(v, null);
     }
 
+    public PackageTest addRunOnceInitializer(ThrowingRunnable v) {
+        return addRunOnceInitializer(v, null);
+    }
+
     public PackageTest addBundleVerifier(
-            BiConsumer<JPackageCommand, Executor.Result> v) {
-        currentTypes.stream().forEach(
-                type -> handlers.get(type).addBundleVerifier(v));
+            ThrowingBiConsumer<JPackageCommand, Executor.Result> v) {
+        currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(
+                ThrowingBiConsumer.toBiConsumer(v)));
         return this;
     }
 
@@ -139,19 +147,26 @@
     }
 
     public PackageTest addBundlePropertyVerifier(String propertyName,
-            BiConsumer<String, String> pred) {
+            Predicate<String> pred, String predLabel) {
         return addBundleVerifier(cmd -> {
-            pred.accept(propertyName,
-                    LinuxHelper.getBundleProperty(cmd, propertyName));
+            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, (unused, v) -> {
-            TKit.assertEquals(expectedPropertyValue, v, String.format(
-                    "Check value of %s property is [%s]", propertyName, v));
-        });
+        return addBundlePropertyVerifier(propertyName,
+                expectedPropertyValue::equals, "is");
     }
 
     public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
@@ -162,24 +177,39 @@
     }
 
     public PackageTest addInstallVerifier(ThrowingConsumer<JPackageCommand> v) {
-        currentTypes.stream().forEach(
-                type -> handlers.get(type).addInstallVerifier(
-                        ThrowingConsumer.toConsumer(v)));
+        currentTypes.forEach(type -> handlers.get(type).addInstallVerifier(
+                ThrowingConsumer.toConsumer(v)));
         return this;
     }
 
     public PackageTest addUninstallVerifier(ThrowingConsumer<JPackageCommand> v) {
-        currentTypes.stream().forEach(
-                type -> handlers.get(type).addUninstallVerifier(
-                        ThrowingConsumer.toConsumer(v)));
+        currentTypes.forEach(type -> handlers.get(type).addUninstallVerifier(
+                ThrowingConsumer.toConsumer(v)));
+        return this;
+    }
+
+    public PackageTest setPackageInstaller(Consumer<JPackageCommand> v) {
+        currentTypes.forEach(
+                type -> packageHandlers.get(type).installHandler = v);
+        return this;
+    }
+
+    public PackageTest setPackageUnpacker(
+            BiFunction<JPackageCommand, Path, Path> v) {
+        currentTypes.forEach(type -> packageHandlers.get(type).unpackHandler = v);
+        return this;
+    }
+
+    public PackageTest setPackageUninstaller(Consumer<JPackageCommand> v) {
+        currentTypes.forEach(
+                type -> packageHandlers.get(type).uninstallHandler = v);
         return this;
     }
 
     static void withTestFileAssociationsFile(FileAssociations fa,
             ThrowingConsumer<Path> consumer) {
-        final String testFileDefaultName = String.join(".", "test",
-                fa.getSuffix());
-        TKit.withTempFile(testFileDefaultName, fa.getSuffix(), testFile -> {
+        final Path testFileDefaultName = Path.of("test" + fa.getSuffix());
+        TKit.withTempFile(testFileDefaultName, testFile -> {
             if (TKit.isLinux()) {
                 LinuxHelper.initFileAssociationsTestFile(testFile);
             }
@@ -192,7 +222,7 @@
 
         // Setup test app to have valid jpackage command line before
         // running check of type of environment.
-        addInitializer(cmd -> new HelloApp(null).addTo(cmd), "HelloApp");
+        addHelloAppInitializer(null);
 
         String noActionMsg = "Not running file associations test";
         if (GraphicsEnvironment.isHeadless()) {
@@ -202,7 +232,7 @@
         }
 
         addInstallVerifier(cmd -> {
-            if (cmd.isFakeRuntime(noActionMsg)) {
+            if (cmd.isFakeRuntime(noActionMsg) || cmd.isPackageUnpacked(noActionMsg)) {
                 return;
             }
 
@@ -225,7 +255,8 @@
                 // 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);
+                HelloApp.verifyOutputFile(appOutput, expectedArgs,
+                        Collections.emptyMap());
             });
         });
 
@@ -236,7 +267,7 @@
         return this;
     }
 
-    PackageTest forTypes(Collection<PackageType> types, Runnable action) {
+    public PackageTest forTypes(Collection<PackageType> types, Runnable action) {
         Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
                 PackageType[]::new));
         try {
@@ -248,17 +279,17 @@
         return this;
     }
 
-    PackageTest forTypes(PackageType type, Runnable action) {
+    public PackageTest forTypes(PackageType type, Runnable action) {
         return forTypes(List.of(type), action);
     }
 
-    PackageTest notForTypes(Collection<PackageType> types, Runnable action) {
+    public PackageTest notForTypes(Collection<PackageType> types, Runnable action) {
         Set<PackageType> workset = new HashSet<>(currentTypes);
         workset.removeAll(types);
         return forTypes(workset, action);
     }
 
-    PackageTest notForTypes(PackageType type, Runnable action) {
+    public PackageTest notForTypes(PackageType type, Runnable action) {
         return notForTypes(List.of(type), action);
     }
 
@@ -266,55 +297,167 @@
         return configureHelloApp(null);
     }
 
-    public PackageTest configureHelloApp(String encodedName) {
-        addInitializer(
-                cmd -> new HelloApp(JavaAppDesc.parse(encodedName)).addTo(cmd));
+    public PackageTest configureHelloApp(String javaAppDesc) {
+        addHelloAppInitializer(javaAppDesc);
         addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
         return this;
     }
 
-    public void run() {
-        List<Handler> supportedHandlers = handlers.values().stream()
-                .filter(entry -> !entry.isVoid())
-                .collect(Collectors.toList());
+    public final static class Group extends RunnablePackageTest {
+        public Group(PackageTest... tests) {
+            handlers = Stream.of(tests)
+                    .map(PackageTest::createPackageTypeHandlers)
+                    .flatMap(List<Consumer<Action>>::stream)
+                    .collect(Collectors.toUnmodifiableList());
+        }
 
-        if (supportedHandlers.isEmpty()) {
-            // No handlers with initializers found. Nothing to do.
-            return;
+        @Override
+        protected void runAction(Action... action) {
+            if (Set.of(action).contains(Action.UNINSTALL)) {
+                ListIterator<Consumer<Action>> 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));
+            }
         }
 
-        Supplier<JPackageCommand> initializer = new Supplier<>() {
-            @Override
-            public JPackageCommand get() {
-                JPackageCommand cmd = new JPackageCommand().setDefaultInputOutput();
-                if (bundleOutputDir != null) {
-                    cmd.setArgumentValue("--dest", bundleOutputDir.toString());
-                }
-                cmd.setDefaultAppName();
-                return cmd;
-            }
-        };
+        private final List<Consumer<Action>> handlers;
+    }
+
+    final static class PackageHandlers {
+        Consumer<JPackageCommand> installHandler;
+        Consumer<JPackageCommand> uninstallHandler;
+        BiFunction<JPackageCommand, Path, Path> unpackHandler;
+    }
+
+    @Override
+    protected void runActions(List<Action[]> actions) {
+        createPackageTypeHandlers().forEach(
+                handler -> actions.forEach(
+                        action -> List.of(action).forEach(handler::accept)));
+    }
+
+    @Override
+    protected void runAction(Action... action) {
+        throw new UnsupportedOperationException();
+    }
 
-        supportedHandlers.forEach(handler -> handler.accept(initializer.get()));
+    private void addHelloAppInitializer(String javaAppDesc) {
+        addInitializer(
+                cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd),
+                "HelloApp");
+    }
+
+    private List<Consumer<Action>> 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<PackageType, Handler> 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());
     }
 
-    public PackageTest setAction(Action value) {
-        action = value;
-        return this;
-    }
+    private Consumer<Action> createPackageTypeHandler(
+            PackageType type, Handler handler) {
+        return ThrowingConsumer.toConsumer(new ThrowingConsumer<Action>() {
+            @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;
+                    }
 
-    public Action getAction() {
-        return action;
+                    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 Consumer<JPackageCommand> {
+    private class Handler implements BiConsumer<Action, JPackageCommand> {
 
-        Handler(PackageType type) {
-            if (!PackageType.NATIVE.contains(type)) {
-                throw new IllegalArgumentException(
-                        "Attempt to configure a test for image packaging");
-            }
-            this.type = type;
+        Handler() {
             initializers = new ArrayList<>();
             bundleVerifiers = new ArrayList<>();
             installVerifiers = new ArrayList<>();
@@ -342,33 +485,35 @@
         }
 
         @Override
-        public void accept(JPackageCommand cmd) {
-            type.applyTo(cmd);
+        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;
 
-            initializers.stream().forEach(v -> v.accept(cmd));
-            cmd.executePrerequisiteActions();
-
-            switch (action) {
                 case CREATE:
-                    Executor.Result result = cmd.execute();
-                    result.assertExitCodeIs(expectedJPackageExitCode);
+                    Executor.Result result = cmd.execute(expectedJPackageExitCode);
                     if (expectedJPackageExitCode == 0) {
                         TKit.assertFileExists(cmd.outputBundle());
                     } else {
                         TKit.assertPathExists(cmd.outputBundle(), false);
                     }
-                    verifyPackageBundle(cmd.createImmutableCopy(), result);
+                    verifyPackageBundle(cmd, result);
                     break;
 
                 case VERIFY_INSTALL:
                     if (expectedJPackageExitCode == 0) {
-                        verifyPackageInstalled(cmd.createImmutableCopy());
+                        verifyPackageInstalled(cmd);
                     }
                     break;
 
                 case VERIFY_UNINSTALL:
                     if (expectedJPackageExitCode == 0) {
-                        verifyPackageUninstalled(cmd.createImmutableCopy());
+                        verifyPackageUninstalled(cmd);
                     }
                     break;
             }
@@ -381,25 +526,33 @@
                     LinuxHelper.verifyPackageBundleEssential(cmd);
                 }
             }
-            bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
+            bundleVerifiers.forEach(v -> v.accept(cmd, result));
         }
 
         private void verifyPackageInstalled(JPackageCommand cmd) {
-            TKit.trace(String.format("Verify installed: %s",
-                    cmd.getPrintableCommandLine()));
+            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())) {
-                    new WindowsHelper.AppVerifier(cmd);
+                if (PackageType.WINDOWS.contains(cmd.packageType())
+                        && !cmd.isPackageUnpacked(
+                                "Not verifying desktop integration")) {
+                    new WindowsHelper.DesktopIntegrationVerifier(cmd);
                 }
             }
 
             TKit.assertPathExists(AppImageFile.getPathInAppImage(
                     cmd.appInstallationDirectory()), false);
 
-            installVerifiers.stream().forEach(v -> v.accept(cmd));
+            installVerifiers.forEach(v -> v.accept(cmd));
         }
 
         private void verifyPackageUninstalled(JPackageCommand cmd) {
@@ -409,79 +562,63 @@
                 TKit.assertPathExists(cmd.appLauncherPath(), false);
 
                 if (PackageType.WINDOWS.contains(cmd.packageType())) {
-                    new WindowsHelper.AppVerifier(cmd);
+                    new WindowsHelper.DesktopIntegrationVerifier(cmd);
                 }
             }
 
             TKit.assertPathExists(cmd.appInstallationDirectory(), false);
 
-            uninstallVerifiers.stream().forEach(v -> v.accept(cmd));
+            uninstallVerifiers.forEach(v -> v.accept(cmd));
         }
 
-        private final PackageType type;
         private final List<Consumer<JPackageCommand>> initializers;
         private final List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers;
         private final List<Consumer<JPackageCommand>> installVerifiers;
         private final List<Consumer<JPackageCommand>> uninstallVerifiers;
     }
 
+    private static Map<PackageType, PackageHandlers> createDefaultPackageHandlers() {
+        HashMap<PackageType, PackageHandlers> 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<PackageType> currentTypes;
     private Set<PackageType> excludeTypes;
     private int expectedJPackageExitCode;
     private Map<PackageType, Handler> handlers;
     private Set<String> namedInitializers;
-    private Action action;
+    private Map<PackageType, PackageHandlers> packageHandlers;
 
-    /**
-     * Test action.
-     */
-    static public enum Action {
-        /**
-         * Create bundle.
-         */
-        CREATE,
-        /**
-         * Verify bundle installed.
-         */
-        VERIFY_INSTALL,
-        /**
-         * Verify bundle uninstalled.
-         */
-        VERIFY_UNINSTALL;
-
-        @Override
-        public String toString() {
-            return name().toLowerCase().replace('_', '-');
-        }
-    };
-    private final static Action DEFAULT_ACTION;
-    private final static File bundleOutputDir;
+    private final static File BUNDLE_OUTPUT_DIR;
 
     static {
         final String propertyName = "output";
         String val = TKit.getConfigProperty(propertyName);
         if (val == null) {
-            bundleOutputDir = null;
+            BUNDLE_OUTPUT_DIR = null;
         } else {
-            bundleOutputDir = new File(val).getAbsoluteFile();
+            BUNDLE_OUTPUT_DIR = new File(val).getAbsoluteFile();
 
-            if (!bundleOutputDir.isDirectory()) {
-                throw new IllegalArgumentException(String.format(
-                        "Invalid value of %s sytem property: [%s]. Should be existing directory",
+            if (!BUNDLE_OUTPUT_DIR.isDirectory()) {
+                throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory",
                         TKit.getConfigPropertyName(propertyName),
-                        bundleOutputDir));
+                        BUNDLE_OUTPUT_DIR));
             }
         }
     }
-
-    static {
-        final String propertyName = "action";
-        String action = Optional.ofNullable(TKit.getConfigProperty(propertyName)).orElse(
-                Action.CREATE.toString()).toLowerCase();
-        DEFAULT_ACTION = Stream.of(Action.values()).filter(
-                a -> a.toString().equals(action)).findFirst().orElseThrow(
-                        () -> new IllegalArgumentException(String.format(
-                                "Unrecognized value of %s property: [%s]",
-                                TKit.getConfigPropertyName(propertyName), action)));
-    }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Tue Dec 17 13:56:47 2019 -0500
@@ -65,7 +65,7 @@
     }
 
     void applyTo(JPackageCommand cmd) {
-        cmd.addArguments("--type", getName());
+        cmd.setArgumentValue("--type", getName());
     }
 
     String getSuffix() {
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Tue Dec 17 13:56:47 2019 -0500
@@ -52,7 +52,7 @@
 
         for (int i = 0; i != 10; ++i) {
             if (root.resolve("apps").toFile().isDirectory()) {
-                return root.toAbsolutePath();
+                return root.normalize().toAbsolutePath();
             }
             root = root.resolve("..");
         }
@@ -60,6 +60,10 @@
         throw new RuntimeException("Failed to locate apps directory");
     }).get();
 
+    public static final Path SRC_ROOT = Functional.identity(() -> {
+        return TEST_SRC_ROOT.resolve("../../../../src/jdk.incubator.jpackage").normalize().toAbsolutePath();
+    }).get();
+
     public final static String ICON_SUFFIX = Functional.identity(() -> {
         if (isOSX()) {
             return ".icns";
@@ -150,14 +154,6 @@
         return currentTest.workDir();
     }
 
-    static Path defaultInputDir() {
-        return workDir().resolve("input");
-    }
-
-    static Path defaultOutputDir() {
-        return workDir().resolve("output");
-    }
-
     static String getCurrentDefaultAppName() {
         // Construct app name from swapping and joining test base name
         // and test function name.
@@ -275,18 +271,16 @@
         return Files.createDirectory(createUniqueFileName(role));
     }
 
-    public static Path createTempFile(String role, String suffix) throws
+    public static Path createTempFile(Path templateFile) throws
             IOException {
-        if (role == null) {
-            return Files.createTempFile(workDir(), TEMP_FILE_PREFIX, suffix);
-        }
-        return Files.createFile(createUniqueFileName(role));
+        return Files.createFile(createUniqueFileName(
+                templateFile.getFileName().toString()));
     }
 
-    public static Path withTempFile(String role, String suffix,
+    public static Path withTempFile(Path templateFile,
             ThrowingConsumer<Path> action) {
         final Path tempFile = ThrowingSupplier.toSupplier(() -> createTempFile(
-                role, suffix)).get();
+                templateFile)).get();
         boolean keepIt = true;
         try {
             ThrowingConsumer.toConsumer(action).accept(tempFile);
@@ -449,10 +443,11 @@
     }
 
     public static Path createRelativePathCopy(final Path file) {
-        Path fileCopy = workDir().resolve(file.getFileName()).toAbsolutePath().normalize();
-
-        ThrowingRunnable.toRunnable(() -> Files.copy(file, fileCopy,
-                StandardCopyOption.REPLACE_EXISTING)).run();
+        Path fileCopy = ThrowingSupplier.toSupplier(() -> {
+            Path localPath = createTempFile(file);
+            Files.copy(file, localPath, StandardCopyOption.REPLACE_EXISTING);
+            return localPath;
+        }).get().toAbsolutePath().normalize();
 
         final Path basePath = Path.of(".").toAbsolutePath().normalize();
         try {
@@ -713,32 +708,32 @@
         }
     }
 
-    public final static class TextStreamAsserter {
-        TextStreamAsserter(String value) {
+    public final static class TextStreamVerifier {
+        TextStreamVerifier(String value) {
             this.value = value;
             predicate(String::contains);
         }
 
-        public TextStreamAsserter label(String v) {
+        public TextStreamVerifier label(String v) {
             label = v;
             return this;
         }
 
-        public TextStreamAsserter predicate(BiPredicate<String, String> v) {
+        public TextStreamVerifier predicate(BiPredicate<String, String> v) {
             predicate = v;
             return this;
         }
 
-        public TextStreamAsserter negate() {
+        public TextStreamVerifier negate() {
             negate = true;
             return this;
         }
 
-        public TextStreamAsserter orElseThrow(RuntimeException v) {
+        public TextStreamVerifier orElseThrow(RuntimeException v) {
             return orElseThrow(() -> v);
         }
 
-        public TextStreamAsserter orElseThrow(Supplier<RuntimeException> v) {
+        public TextStreamVerifier orElseThrow(Supplier<RuntimeException> v) {
             createException = v;
             return this;
         }
@@ -779,8 +774,8 @@
         final private String value;
     }
 
-    public static TextStreamAsserter assertTextStream(String what) {
-        return new TextStreamAsserter(what);
+    public static TextStreamVerifier assertTextStream(String what) {
+        return new TextStreamVerifier(what);
     }
 
     private static PrintStream openLogStream() {
@@ -809,13 +804,23 @@
         return "jpackage.test." + propertyName;
     }
 
-    static Set<String> tokenizeConfigProperty(String propertyName) {
+    static List<String> tokenizeConfigPropertyAsList(String propertyName) {
         final String val = TKit.getConfigProperty(propertyName);
         if (val == null) {
             return null;
         }
-        return Stream.of(val.toLowerCase().split(",")).map(String::strip).filter(
-                Predicate.not(String::isEmpty)).collect(Collectors.toSet());
+        return Stream.of(val.toLowerCase().split(","))
+                .map(String::strip)
+                .filter(Predicate.not(String::isEmpty))
+                .collect(Collectors.toList());
+    }
+
+    static Set<String> tokenizeConfigProperty(String propertyName) {
+        List<String> tokens = tokenizeConfigPropertyAsList(propertyName);
+        if (tokens == null) {
+            return null;
+        }
+        return tokens.stream().collect(Collectors.toSet());
     }
 
     static final Path LOG_FILE = Functional.identity(() -> {
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Tue Dec 17 13:56:47 2019 -0500
@@ -68,11 +68,11 @@
 
                 CMDLINE_ARG_PREFIX + "exclude",
                 arg -> (excludedTests = Optional.ofNullable(
-                        excludedTests).orElse(new HashSet<String>())).add(arg),
+                        excludedTests).orElseGet(() -> new HashSet<String>())).add(arg),
 
                 CMDLINE_ARG_PREFIX + "include",
                 arg -> (includedTests = Optional.ofNullable(
-                        includedTests).orElse(new HashSet<String>())).add(arg),
+                        includedTests).orElseGet(() -> new HashSet<String>())).add(arg),
 
                 CMDLINE_ARG_PREFIX + "space-subst",
                 arg -> spaceSubstitute = arg,
@@ -127,8 +127,7 @@
         // Log all matches before returning from the function
         return tests.filter(test -> {
             String testDescription = test.createDescription().testFullName();
-            boolean match = filters.stream().anyMatch(
-                    v -> testDescription.contains(v));
+            boolean match = filters.stream().anyMatch(testDescription::contains);
             if (match) {
                 trace(String.format(logMsg + ": %s", testDescription));
             }
@@ -159,7 +158,7 @@
 
     private void flushTestGroup() {
         if (testGroup != null) {
-            filterTestGroup().forEach(testBody -> createTestInstance(testBody));
+            filterTestGroup().forEach(this::createTestInstance);
             clear();
         }
     }
@@ -170,7 +169,7 @@
 
         Method testMethod = testBody.getMethod();
         if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch(
-                type -> testMethod.isAnnotationPresent(type))) {
+                testMethod::isAnnotationPresent)) {
             curBeforeActions = beforeActions;
             curAfterActions = afterActions;
         } else {
@@ -286,7 +285,7 @@
         List<Method> methods = Stream.of(methodClass.getMethods()).filter(
                 (m) -> filterMethod(methodName, m)).collect(Collectors.toList());
         if (methods.isEmpty()) {
-            new ParseException(String.format(
+            throw new ParseException(String.format(
                     "Method [%s] not found in [%s] class;",
                     methodName, className));
         }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Tue Dec 17 13:56:47 2019 -0500
@@ -236,7 +236,13 @@
             }
 
             if (!KEEP_WORK_DIR.contains(status)) {
-                TKit.deleteDirectoryRecursive(workDir);
+                if (Files.isSameFile(workDir, Path.of("."))) {
+                    // 1. If the work directory is the current directory, don't
+                    // delete it, just clean as deleting it would be confusing.
+                    TKit.deleteDirectoryContentsRecursive(workDir);
+                } else {
+                    TKit.deleteDirectoryRecursive(workDir);
+                }
             }
 
             TKit.log(String.format("%s %s; checks=%d", status, fullName,
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Tue Dec 17 13:56:47 2019 -0500
@@ -26,8 +26,11 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.*;
+import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jdk.jpackage.test.Functional.ThrowingRunnable;
+import jdk.jpackage.test.PackageTest.PackageHandlers;
 
 public class WindowsHelper {
 
@@ -38,22 +41,82 @@
     }
 
     static Path getInstallationDirectory(JPackageCommand cmd) {
-        cmd.verifyIsOfType(PackageType.WINDOWS);
-        Path installDir = Path.of(
-                cmd.getArgumentValue("--install-dir", () -> cmd.name()));
+        Path installSubDir = getInstallationSubDirectory(cmd);
         if (isUserLocalInstall(cmd)) {
-            return USER_LOCAL.resolve(installDir);
+            return USER_LOCAL.resolve(installSubDir);
+        }
+        return PROGRAM_FILES.resolve(installSubDir);
+    }
+
+    static Path getInstallationSubDirectory(JPackageCommand cmd) {
+        cmd.verifyIsOfType(PackageType.WINDOWS);
+        return Path.of(cmd.getArgumentValue("--install-dir", () -> cmd.name()));
+    }
+
+    private static void runMsiexecWithRetries(Executor misexec) {
+        Executor.Result result = null;
+        for (int attempt = 0; attempt != 3; ++attempt) {
+            result = misexec.executeWithoutExitCodeCheck();
+            if (result.exitCode == 1618) {
+                // Another installation is already in progress.
+                // Wait a little and try again.
+                ThrowingRunnable.toRunnable(() -> Thread.sleep(3000)).run();
+                continue;
+            }
+            break;
         }
-        return PROGRAM_FILES.resolve(installDir);
+
+        result.assertExitCodeIsZero();
+    }
+
+    static PackageHandlers createMsiPackageHandlers() {
+        BiConsumer<JPackageCommand, Boolean> installMsi = (cmd, install) -> {
+            cmd.verifyIsOfType(PackageType.WIN_MSI);
+            runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart",
+                    install ? "/i" : "/x").addArgument(cmd.outputBundle()));
+        };
+
+        PackageHandlers msi = new PackageHandlers();
+        msi.installHandler = cmd -> installMsi.accept(cmd, true);
+        msi.uninstallHandler = cmd -> installMsi.accept(cmd, false);
+        msi.unpackHandler = (cmd, destinationDir) -> {
+            cmd.verifyIsOfType(PackageType.WIN_MSI);
+            runMsiexecWithRetries(Executor.of("msiexec", "/a")
+                    .addArgument(cmd.outputBundle().normalize())
+                    .addArguments("/qn", String.format("TARGETDIR=%s",
+                            destinationDir.toAbsolutePath().normalize())));
+            return destinationDir.resolve(getInstallationSubDirectory(cmd));
+        };
+        return msi;
+    }
+
+    static PackageHandlers createExePackageHandlers() {
+        PackageHandlers exe = new PackageHandlers();
+        exe.installHandler = cmd -> {
+            cmd.verifyIsOfType(PackageType.WIN_EXE);
+            new Executor().setExecutable(cmd.outputBundle()).execute();
+        };
+
+        return exe;
+    }
+
+    public static String getMsiProperty(JPackageCommand cmd, String propertyName) {
+        cmd.verifyIsOfType(PackageType.WIN_MSI);
+        return Executor.of("cscript.exe", "//Nologo")
+        .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/query-msi-property.js"))
+        .addArgument(cmd.outputBundle())
+        .addArgument(propertyName)
+        .dumpOutput()
+        .executeAndGetOutput().stream().collect(Collectors.joining("\n"));
     }
 
     private static boolean isUserLocalInstall(JPackageCommand cmd) {
         return cmd.hasArgument("--win-per-user-install");
     }
 
-    static class AppVerifier {
+    static class DesktopIntegrationVerifier {
 
-        AppVerifier(JPackageCommand cmd) {
+        DesktopIntegrationVerifier(JPackageCommand cmd) {
             cmd.verifyIsOfType(PackageType.WINDOWS);
             this.cmd = cmd;
             verifyStartMenuShortcut();
@@ -201,16 +264,15 @@
     }
 
     private static String queryRegistryValue(String keyPath, String valueName) {
-        Executor.Result status = new Executor()
-                .setExecutable("reg")
+        var status = Executor.of("reg", "query", keyPath, "/v", valueName)
                 .saveOutput()
-                .addArguments("query", keyPath, "/v", valueName)
-                .execute();
+                .executeWithoutExitCodeCheck();
         if (status.exitCode == 1) {
             // Should be the case of no such registry value or key
             String lookupString = "ERROR: The system was unable to find the specified registry key or value.";
-            status.getOutput().stream().filter(line -> line.equals(lookupString)).findFirst().orElseThrow(
-                    () -> new RuntimeException(String.format(
+            TKit.assertTextStream(lookupString)
+                    .predicate(String::equals)
+                    .orElseThrow(() -> new RuntimeException(String.format(
                             "Failed to find [%s] string in the output",
                             lookupString)));
             TKit.trace(String.format(
--- a/test/jdk/tools/jpackage/linux/MaintainerTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/linux/MaintainerTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -56,12 +56,10 @@
             .addInitializer(cmd -> {
                 cmd.addArguments("--linux-deb-maintainer", MAINTAINER);
             })
-            .addBundlePropertyVerifier("Maintainer", (propName, propValue) -> {
+            .addBundlePropertyVerifier("Maintainer", value -> {
                 String lookupValue = "<" + MAINTAINER + ">";
-                TKit.assertTrue(propValue.endsWith(lookupValue),
-                        String.format("Check value of %s property [%s] ends with %s",
-                                propName, propValue, lookupValue));
-            })
+                return value.endsWith(lookupValue);
+            }, "ends with")
             .run();
         });
     }
--- a/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -25,6 +25,7 @@
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.LinuxHelper;
+import jdk.jpackage.test.Annotations.Test;
 
 
 /**
@@ -50,41 +51,38 @@
  * @build jdk.jpackage.test.*
  * @requires (os.family == "linux")
  * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m PackageDepsTest
+ * @compile PackageDepsTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=PackageDepsTest
  */
 public class PackageDepsTest {
 
-    public static void main(String[] args) {
-        // Pick the name of prerequisite package to be alphabetically
-        // preceeding the main package name.
-        // This is needed to make Bash script batch installing/uninstalling packages
-        // produced by jtreg tests install/uninstall packages in the right order.
+    @Test
+    public static void test() {
         final String PREREQ_PACKAGE_NAME = "apackagedepstestprereq";
 
-        TKit.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.LINUX)
-            .configureHelloApp()
-            .addInitializer(cmd -> {
-                cmd.setArgumentValue("--name", PREREQ_PACKAGE_NAME);
-            })
-            .run();
+        PackageTest test1 = new PackageTest()
+        .forTypes(PackageType.LINUX)
+        .configureHelloApp()
+        .addInitializer(cmd -> {
+            cmd.setArgumentValue("--name", PREREQ_PACKAGE_NAME);
+        });
 
-            new PackageTest()
-            .forTypes(PackageType.LINUX)
-            .configureHelloApp()
-            .addInitializer(cmd -> {
-                cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
-            })
-            .forTypes(PackageType.LINUX)
-            .addBundleVerifier(cmd -> {
-                TKit.assertTrue(
-                        LinuxHelper.getPrerequisitePackages(cmd).contains(
-                                PREREQ_PACKAGE_NAME), String.format(
-                                "Check package depends on [%s] package",
-                                PREREQ_PACKAGE_NAME));
-            })
-            .run();
+        PackageTest test2 = new PackageTest()
+        .forTypes(PackageType.LINUX)
+        .configureHelloApp()
+        .addInitializer(cmd -> {
+            cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
+        })
+        .forTypes(PackageType.LINUX)
+        .addBundleVerifier(cmd -> {
+            TKit.assertTrue(
+                    LinuxHelper.getPrerequisitePackages(cmd).contains(
+                            PREREQ_PACKAGE_NAME), String.format(
+                            "Check package depends on [%s] package",
+                            PREREQ_PACKAGE_NAME));
         });
+
+        new PackageTest.Group(test1, test2).run();
     }
 }
--- a/test/jdk/tools/jpackage/linux/ReleaseTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/linux/ReleaseTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -64,11 +64,9 @@
             .forTypes(PackageType.LINUX_RPM)
             .addBundlePropertyVerifier("Release", RELEASE)
             .forTypes(PackageType.LINUX_DEB)
-            .addBundlePropertyVerifier("Version", (propName, propValue) -> {
-                TKit.assertTrue(propValue.endsWith("-" + RELEASE),
-                        String.format("Check value of %s property [%s] ends with %s",
-                                propName, propValue, RELEASE));
-            })
+            .addBundlePropertyVerifier("Version", propValue -> {
+                return propValue.endsWith("-" + RELEASE);
+            }, "ends with")
             .run();
         });
     }
--- a/test/jdk/tools/jpackage/linux/ShortcutHintTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -21,12 +21,12 @@
  * questions.
  */
 
-import java.nio.charset.StandardCharsets;
+import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Map;
 import java.nio.file.Path;
 import java.util.List;
-import java.util.stream.Collectors;
+import jdk.jpackage.test.AdditionalLauncher;
 import jdk.jpackage.test.FileAssociations;
 import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.PackageTest;
@@ -127,56 +127,50 @@
      */
     @Test
     public static void testAdditionaltLaunchers() {
-        createTest().addInitializer(cmd -> {
-            cmd.setFakeRuntime();
-
-            final String launcherName = "Foo";
-            final Path propsFile = TKit.workDir().resolve(
-                    launcherName + ".properties");
+        PackageTest test = createTest();
 
-            cmd.addArguments("--add-launcher", String.format("%s=%s",
-                    launcherName, propsFile));
+        new AdditionalLauncher("Foo").setIcon(TKit.TEST_SRC_ROOT.resolve(
+                "apps/dukeplug.png")).applyTo(test);
 
-            TKit.createPropertiesFile(propsFile, Map.entry("icon",
-                    TKit.TEST_SRC_ROOT.resolve("apps/dukeplug.png").toString()));
-        }).run();
+        test.addInitializer(JPackageCommand::setFakeRuntime).run();
     }
 
     /**
      * .desktop file from resource dir.
      */
     @Test
-    public static void testDesktopFileFromResourceDir() {
+    public static void testDesktopFileFromResourceDir() throws IOException {
         final String expectedVersionString = "Version=12345678";
-        TKit.withTempDirectory("resources", tempDir -> {
-            createTest().addInitializer(cmd -> {
-                cmd.setFakeRuntime();
+
+        final Path tempDir = TKit.createTempDirectory("resources");
 
-                cmd.addArgument("--linux-shortcut");
-                cmd.addArguments("--resource-dir", tempDir);
+        createTest().addInitializer(cmd -> {
+            cmd.setFakeRuntime();
+
+            cmd.addArgument("--linux-shortcut");
+            cmd.addArguments("--resource-dir", tempDir);
 
-                // Create custom .desktop file in resource directory
-                TKit.createTextFile(tempDir.resolve(cmd.name() + ".desktop"),
-                        List.of(
-                                "[Desktop Entry]",
-                                "Name=APPLICATION_NAME",
-                                "Exec=APPLICATION_LAUNCHER",
-                                "Terminal=false",
-                                "Type=Application",
-                                "Categories=DEPLOY_BUNDLE_CATEGORY",
-                                expectedVersionString
-                        ));
-            })
-            .addInstallVerifier(cmd -> {
-                Path desktopFile = cmd.appLayout().destktopIntegrationDirectory().resolve(
-                        String.format("%s-%s.desktop",
-                                LinuxHelper.getPackageName(cmd), cmd.name()));
-                TKit.assertFileExists(desktopFile);
-                TKit.assertTextStream(expectedVersionString)
-                        .label(String.format("[%s] file", desktopFile))
-                        .predicate(String::equals)
-                        .apply(Files.readAllLines(desktopFile).stream());
-            }).run();
-        });
+            // Create custom .desktop file in resource directory
+            TKit.createTextFile(tempDir.resolve(cmd.name() + ".desktop"),
+                    List.of(
+                            "[Desktop Entry]",
+                            "Name=APPLICATION_NAME",
+                            "Exec=APPLICATION_LAUNCHER",
+                            "Terminal=false",
+                            "Type=Application",
+                            "Categories=DEPLOY_BUNDLE_CATEGORY",
+                            expectedVersionString
+                    ));
+        })
+        .addInstallVerifier(cmd -> {
+            Path desktopFile = cmd.appLayout().destktopIntegrationDirectory().resolve(
+                    String.format("%s-%s.desktop",
+                            LinuxHelper.getPackageName(cmd), cmd.name()));
+            TKit.assertFileExists(desktopFile);
+            TKit.assertTextStream(expectedVersionString)
+                    .label(String.format("[%s] file", desktopFile))
+                    .predicate(String::equals)
+                    .apply(Files.readAllLines(desktopFile).stream());
+        }).run();
     }
 }
--- a/test/jdk/tools/jpackage/macosx/base/SigningBase.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/macosx/base/SigningBase.java	Tue Dec 17 13:56:47 2019 -0500
@@ -48,8 +48,7 @@
                 .addArguments("--verify", "--deep", "--strict", "--verbose=2",
                         target.toString())
                 .saveOutput()
-                .execute()
-                .assertExitCodeIs(exitCode).getOutput();
+                .execute(exitCode).getOutput();
 
         return result;
     }
Binary file test/jdk/tools/jpackage/resources/icon.png has changed
--- a/test/jdk/tools/jpackage/run_tests.sh	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/run_tests.sh	Tue Dec 17 13:56:47 2019 -0500
@@ -41,7 +41,7 @@
 
 help_usage ()
 {
-  echo "Usage: `basename $0` [options] [test_names]"
+  echo "Usage: `basename $0` [options] [--] [jtreg_options|test_names]"
   echo "Options:"
   echo "  -h              - print this message"
   echo "  -v              - verbose output"
@@ -58,16 +58,12 @@
   echo '  -l <logfile>    - value for `jpackage.test.logfile` property.'
   echo "                    Optional, for jtreg tests debug purposes only."
   echo "  -m <mode>       - mode to run jtreg tests."
-  echo '                    Should be one of `create`, `update`, `verify-install` or `verify-uninstall`.'
+  echo '                    Should be one of `create`, `update` or `print-default-tests`.'
   echo '                    Optional, default mode is `update`.'
   echo '                    - `create`'
   echo '                      Remove all package bundles from the output directory before running jtreg tests.'
   echo '                    - `update`'
   echo '                      Run jtreg tests and overrite existing package bundles in the output directory.'
-  echo '                    - `verify-install`'
-  echo '                      Verify installed packages created with the previous run of the script.'
-  echo '                    - `verify-uninstall`'
-  echo '                      Verify packages created with the previous run of the script were uninstalled cleanly.'
   echo '                    - `print-default-tests`'
   echo '                      Print default list of packaging tests and exit.'
 }
@@ -135,7 +131,10 @@
 # jtreg extra arguments
 declare -a jtreg_args
 
-# Run all tests
+# Create packages only
+jtreg_args+=("-Djpackage.test.action=create")
+
+# run all tests
 run_all_tests=
 
 mapfile -t tests < <(find_all_packaging_tests)
@@ -206,10 +205,6 @@
   true
 elif [ "$mode" = update ]; then
   true
-elif [ "$mode" = verify-install ]; then
-  jtreg_args+=("-Djpackage.test.action=$mode")
-elif [ "$mode" = verify-uninstall ]; then
-  jtreg_args+=("-Djpackage.test.action=$mode")
 else
   fatal_with_help_usage 'Invalid value of -m option:' [$mode]
 fi
@@ -218,7 +213,11 @@
   jtreg_args+=(-Djpackage.test.SQETest=yes)
 fi
 
-# All remaining command line arguments are tests to run that should override the defaults
+# Drop arguments separator
+[ "$1" != "--" ] || shift
+
+# All remaining command line arguments are tests to run
+# that should override the defaults and explicit jtreg arguments
 [ $# -eq 0 ] || tests=($@)
 
 
--- a/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -27,12 +27,8 @@
 import java.util.List;
 import java.util.Optional;
 import java.lang.invoke.MethodHandles;
-import jdk.jpackage.test.HelloApp;
-import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.PackageType;
-import jdk.jpackage.test.FileAssociations;
-import jdk.jpackage.test.Annotations.Test;
-import jdk.jpackage.test.TKit;
+import jdk.jpackage.test.*;
+import jdk.jpackage.test.Annotations.*;
 
 /**
  * Test --add-launcher parameter. Output of the test should be
@@ -46,11 +42,25 @@
  * @test
  * @summary jpackage with --add-launcher
  * @key jpackagePlatformPackage
+ * @requires (jpackage.test.SQETest != null)
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
  * @compile AdditionalLaunchersTest.java
  * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=AdditionalLaunchersTest.test
+ */
+
+/*
+ * @test
+ * @summary jpackage with --add-launcher
+ * @key jpackagePlatformPackage
+ * @requires (jpackage.test.SQETest == null)
+ * @library ../helpers
+ * @build jdk.jpackage.test.*
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile AdditionalLaunchersTest.java
+ * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
  *  --jpt-run=AdditionalLaunchersTest
  */
 
@@ -72,87 +82,122 @@
                 MethodHandles.lookup().lookupClass().getSimpleName()).applyTo(
                 packageTest);
 
-        new AdditionalLauncher("Baz2").setArguments().applyTo(packageTest);
-        new AdditionalLauncher("foo").setArguments("yep!").applyTo(packageTest);
+        new AdditionalLauncher("Baz2")
+                .setDefaultArguments()
+                .applyTo(packageTest);
+
+        new AdditionalLauncher("foo")
+                .setDefaultArguments("yep!")
+                .applyTo(packageTest);
+
+        new AdditionalLauncher("Bar")
+                .setDefaultArguments("one", "two", "three")
+                .setIcon(GOLDEN_ICON)
+                .applyTo(packageTest);
 
-        AdditionalLauncher barLauncher = new AdditionalLauncher("Bar").setArguments(
-                "one", "two", "three");
-        if (TKit.isLinux()) {
-            barLauncher.setIcon(TKit.TEST_SRC_ROOT.resolve("apps/dukeplug.png"));
-        }
-        barLauncher.applyTo(packageTest);
+        packageTest.run();
+    }
+
+    @Test
+    public void bug8230933() {
+        PackageTest packageTest = new PackageTest().configureHelloApp();
+
+        new AdditionalLauncher("default_icon")
+                .applyTo(packageTest);
+
+        new AdditionalLauncher("no_icon")
+                .setNoIcon().applyTo(packageTest);
+
+        new AdditionalLauncher("custom_icon")
+                .setIcon(GOLDEN_ICON)
+                .applyTo(packageTest);
 
         packageTest.run();
     }
 
-    private static Path replaceFileName(Path path, String newFileName) {
-        String fname = path.getFileName().toString();
-        int lastDotIndex = fname.lastIndexOf(".");
-        if (lastDotIndex != -1) {
-            fname = newFileName + fname.substring(lastDotIndex);
-        } else {
-            fname = newFileName;
-        }
-        return path.getParent().resolve(fname);
+    @Test
+    // Regular app
+    @Parameter("Hello")
+    // Modular app
+    @Parameter("com.other/com.other.CiaoBella")
+    public void testJavaOptions(String javaAppDesc) {
+        JPackageCommand cmd = JPackageCommand.helloAppImage(javaAppDesc)
+        .addArguments("--arguments", "courageous")
+        .addArguments("--java-options", "-Dparam1=xxx")
+        .addArguments("--java-options", "-Dparam2=yyy")
+        .addArguments("--java-options", "-Dparam3=zzz");
+
+        new AdditionalLauncher("Jack")
+                .addDefaultArguments("Jack of All Trades", "Master of None")
+                .setJavaOptions("-Dparam1=Contractor")
+                .applyTo(cmd);
+
+        new AdditionalLauncher("Monday")
+                .addDefaultArguments("Knock Your", "Socks Off")
+                .setJavaOptions("-Dparam2=Surprise workers!")
+                .applyTo(cmd);
+
+        // Should inherit default arguments and java options from the main launcher
+        new AdditionalLauncher("void").applyTo(cmd);
+
+        cmd.executeAndAssertHelloAppImageCreated();
     }
 
-    static class AdditionalLauncher {
+    /**
+     * Test usage of modular and non modular apps in additional launchers.
+     */
+    @Test
+    @Parameter("true")
+    @Parameter("fase")
+    public void testMainLauncherIsModular(boolean mainLauncherIsModular) {
+        final var nonModularAppDesc = JavaAppDesc.parse("a.b.c.Hello");
+        final var modularAppDesc = JavaAppDesc.parse(
+                "module.jar:com.that/com.that.main.Florence");
 
-        AdditionalLauncher(String name) {
-            this.name = name;
-        }
+        final var nonModularJarCmd = JPackageCommand.helloAppImage(nonModularAppDesc);
+        final var modularJarCmd = JPackageCommand.helloAppImage(modularAppDesc);
 
-        AdditionalLauncher setArguments(String... args) {
-            arguments = List.of(args);
-            return this;
-        }
+        final JPackageCommand cmd;
+        if (mainLauncherIsModular) {
+            // Create non modular jar.
+            nonModularJarCmd.executePrerequisiteActions();
 
-        AdditionalLauncher setIcon(Path iconPath) {
-            icon = iconPath;
-            return this;
+            cmd = modularJarCmd;
+            cmd.addArguments("--description",
+                    "Test modular app with multiple add-launchers where one is modular app and other is non modular app");
+            cmd.addArguments("--input", nonModularJarCmd.getArgumentValue(
+                    "--input"));
+        } else {
+            // Create modular jar.
+            modularJarCmd.executePrerequisiteActions();
+
+            cmd = nonModularJarCmd;
+            cmd.addArguments("--description",
+                    "Test non modular app with multiple add-launchers where one is modular app and other is non modular app");
+            cmd.addArguments("--module-path", modularJarCmd.getArgumentValue(
+                    "--module-path"));
+            cmd.addArguments("--add-modules", modularAppDesc.moduleName());
         }
 
-        void applyTo(PackageTest test) {
-            final Path propsFile = TKit.workDir().resolve(name + ".properties");
-
-            test.addInitializer(cmd -> {
-                cmd.addArguments("--add-launcher", String.format("%s=%s", name,
-                        propsFile));
-
-                Map<String, String> properties = new HashMap<>();
-                if (arguments != null) {
-                    properties.put("arguments", String.join(" ",
-                            arguments.toArray(String[]::new)));
-                }
-
-                if (icon != null) {
-                    properties.put("icon", icon.toAbsolutePath().toString());
-                }
-
-                TKit.createPropertiesFile(propsFile, properties);
-            });
-            test.addInstallVerifier(cmd -> {
-                Path launcherPath = replaceFileName(cmd.appLauncherPath(), name);
+        new AdditionalLauncher("ModularAppLauncher")
+        .addRawProperties(Map.entry("module", JavaAppDesc.parse(
+                modularAppDesc.toString()).setJarFileName(null).toString()))
+        .addRawProperties(Map.entry("main-jar", ""))
+        .applyTo(cmd);
 
-                TKit.assertExecutableFileExists(launcherPath);
+        new AdditionalLauncher("NonModularAppLauncher")
+        // Use space ( ) character instead of equality sign (=) as
+        // a key/value separator
+        .setPersistenceHandler((path, properties) -> TKit.createTextFile(path,
+                properties.stream().map(entry -> String.join(" ", entry.getKey(),
+                        entry.getValue()))))
+        .addRawProperties(Map.entry("main-class", nonModularAppDesc.className()))
+        .addRawProperties(Map.entry("main-jar", nonModularAppDesc.jarFileName()))
+        .applyTo(cmd);
 
-                if (cmd.isFakeRuntime(String.format(
-                        "Not running %s launcher", launcherPath))) {
-                    return;
-                }
-                HelloApp.executeAndVerifyOutput(launcherPath,
-                        Optional.ofNullable(arguments).orElse(List.of()).toArray(
-                                String[]::new));
-            });
-            test.addUninstallVerifier(cmd -> {
-                Path launcherPath = replaceFileName(cmd.appLauncherPath(), name);
+        cmd.executeAndAssertHelloAppImageCreated();
+    }
 
-                TKit.assertPathExists(launcherPath, false);
-            });
-        }
-
-        private List<String> arguments;
-        private Path icon;
-        private final String name;
-    }
+    private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of(
+            "resources", "icon" + TKit.ICON_SUFFIX));
 }
--- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -23,9 +23,9 @@
 
 import java.nio.file.Path;
 import jdk.jpackage.test.TKit;
+import jdk.jpackage.test.JPackageCommand;
 import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.PackageType;
-import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --app-image parameter. The output installer should provide the same
@@ -41,34 +41,24 @@
  * @requires (jpackage.test.SQETest == null)
  * @build jdk.jpackage.test.*
  * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
- * @run main/othervm/timeout=540 -Xmx512m AppImagePackageTest
+ * @compile AppImagePackageTest.java
+ * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=AppImagePackageTest
  */
 public class AppImagePackageTest {
 
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            Path appimageOutput = Path.of("appimage");
+    @Test
+    public static void test() {
+        Path appimageOutput = TKit.workDir().resolve("appimage");
 
-            JPackageCommand appImageCmd = JPackageCommand.helloAppImage()
-                    .setArgumentValue("--dest", appimageOutput)
-                    .addArguments("--type", "app-image");
-
-            PackageTest packageTest = new PackageTest();
-            if (packageTest.getAction() == PackageTest.Action.CREATE) {
-                appImageCmd.execute();
-            }
+        JPackageCommand appImageCmd = JPackageCommand.helloAppImage()
+                .setArgumentValue("--dest", appimageOutput);
 
-            packageTest.addInitializer(cmd -> {
-                Path appimageInput = appimageOutput.resolve(appImageCmd.name());
-
-                if (PackageType.MAC.contains(cmd.packageType())) {
-                    // Why so complicated on macOS?
-                    appimageInput = Path.of(appimageInput.toString() + ".app");
-                }
-
-                cmd.addArguments("--app-image", appimageInput);
-                cmd.removeArgumentWithValue("--input");
-            }).addBundleDesktopIntegrationVerifier(false).run();
-        });
+        new PackageTest()
+        .addRunOnceInitializer(() -> appImageCmd.execute())
+        .addInitializer(cmd -> {
+            cmd.addArguments("--app-image", appImageCmd.outputBundle());
+            cmd.removeArgumentWithValue("--input");
+        }).addBundleDesktopIntegrationVerifier(false).run();
     }
 }
--- a/test/jdk/tools/jpackage/share/ArgumentsTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/ArgumentsTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -81,7 +81,9 @@
         Path launcherPath = cmd.appLauncherPath();
         if (!cmd.isFakeRuntime(String.format(
                 "Not running [%s] launcher", launcherPath))) {
-            HelloApp.executeAndVerifyOutput(launcherPath, TRICKY_ARGUMENTS);
+            HelloApp.assertApp(launcherPath)
+                    .addDefaultArguments(TRICKY_ARGUMENTS)
+                    .executeAndVerifyOutput();
         }
     }
 
--- a/test/jdk/tools/jpackage/share/IconTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/IconTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -22,72 +22,414 @@
  */
 
 import java.io.IOException;
+import java.util.*;
+import java.util.stream.Stream;
+import java.util.stream.Collectors;
+import java.util.function.Consumer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
-import jdk.incubator.jpackage.internal.IOUtils;
-import jdk.jpackage.test.TKit;
-import jdk.jpackage.test.Functional;
+import jdk.jpackage.test.*;
+import jdk.jpackage.test.Functional.ThrowingConsumer;
+import jdk.jpackage.test.Functional.ThrowingBiConsumer;
 import jdk.jpackage.test.Annotations.*;
-import jdk.jpackage.test.JPackageCommand;
 
 /*
  * @test
- * @summary jpackage create image with custom icon
+ * @summary jpackage create image and package with custom icons for the main and additional launcher
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
  * @compile IconTest.java
- * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
  *  --jpt-run=IconTest
  */
 
 public class IconTest {
-    @Test
-    public static void testResourceDir() throws IOException {
-        TKit.withTempDirectory("resources", tempDir -> {
-            JPackageCommand cmd = JPackageCommand.helloAppImage()
-                    .addArguments("--resource-dir", tempDir);
+
+    enum IconType {
+        /**
+         * Icon not specified.
+         */
+        DefaultIcon,
+
+        /**
+         * Explicit no icon.
+         */
+        NoIcon,
+
+        /**
+         * Custom icon on command line.
+         */
+        CustomIcon,
+
+        /**
+         * Custom icon in resource dir.
+         */
+        ResourceDirIcon,
+
+        /**
+         * Custom icon on command line and in resource dir.
+         */
+        CustomWithResourceDirIcon
+    }
+
+    enum BundleType { AppImage, Package }
+
+    public IconTest(BundleType bundleType, IconType mainLauncherIconType,
+            IconType additionalLauncherIconType, String[] extraJPackageArgs) {
+        this.appImage = (bundleType == BundleType.AppImage);
+        this.extraJPackageArgs = extraJPackageArgs;
+        config = Map.of(
+                Launcher.Main, mainLauncherIconType,
+                Launcher.Additional, additionalLauncherIconType);
+    }
+
+    public IconTest(BundleType bundleType, IconType mainLauncherIconType,
+            IconType additionalLauncherIconType) {
+        this.appImage = (bundleType == BundleType.AppImage);
+        this.extraJPackageArgs = new String[0];
+        config = Map.of(
+                Launcher.Main, mainLauncherIconType,
+                Launcher.Additional, additionalLauncherIconType);
+    }
 
-            Files.copy(GOLDEN_ICON, tempDir.resolve(appIconFileName(cmd)),
-                    StandardCopyOption.REPLACE_EXISTING);
+    public IconTest(BundleType bundleType, IconType mainLauncherIconType) {
+        this.appImage = (bundleType == BundleType.AppImage);
+        this.extraJPackageArgs = new String[0];
+        config = Map.of(Launcher.Main, mainLauncherIconType);
+    }
+
+    @Parameters
+    public static Collection data() {
+        List<Object[]> data = new ArrayList<>();
+
+        var withLinuxShortcut = Set.of(IconType.DefaultIcon, IconType.NoIcon);
 
-            testIt(cmd);
-        });
+        for (var bundleType : BundleType.values()) {
+            if (TKit.isWindows() && bundleType == BundleType.Package) {
+                // On Windows icons are embedded in launcher executables in
+                // application image. Nothing is changed when app image is
+                // packed in msi/exe package bundle, so skip testing of package
+                // bundle.
+                continue;
+            }
+            for (var mainLauncherIconType : IconType.values()) {
+                if (mainLauncherIconType == IconType.NoIcon) {
+                    // `No icon` setting is not applicable for the main launcher.
+                    continue;
+                }
+
+                if (TKit.isOSX()) {
+                    // Custom icons not supported for additional launchers on Mac.
+                    data.add(new Object[]{bundleType, mainLauncherIconType});
+                    continue;
+                }
+
+                for (var additionalLauncherIconType : IconType.values()) {
+                    data.add(new Object[]{bundleType, mainLauncherIconType,
+                        additionalLauncherIconType});
+
+                    if (TKit.isLinux() && bundleType == BundleType.Package
+                            && withLinuxShortcut.contains(mainLauncherIconType)
+                            && withLinuxShortcut.contains(
+                                    additionalLauncherIconType)) {
+                        data.add(new Object[]{bundleType, mainLauncherIconType,
+                            additionalLauncherIconType, new String[]{
+                            "--linux-shortcut"}});
+                    }
+                }
+            }
+        }
+        return data;
     }
 
     @Test
-    @Parameter("true")
-    @Parameter("false")
-    public static void testParameter(boolean relativePath) throws IOException {
-        final Path iconPath;
-        if (relativePath) {
-            iconPath = TKit.createRelativePathCopy(GOLDEN_ICON);
+    public void test() throws IOException {
+        if (appImage) {
+            JPackageCommand cmd = initAppImageTest();
+            var result = cmd.executeAndAssertImageCreated();
+            ThrowingConsumer.toConsumer(createInstallVerifier()).accept(cmd);
+            ThrowingBiConsumer.toBiConsumer(createBundleVerifier()).accept(cmd, result);
         } else {
-            iconPath = GOLDEN_ICON;
+            PackageTest test = initPackageTest();
+            test.addInstallVerifier(createInstallVerifier());
+            test.addBundleVerifier(createBundleVerifier());
+
+            test.addBundleDesktopIntegrationVerifier(config.values().stream()
+                    .anyMatch(this::isWithDesktopIntegration));
+
+            test.run(PackageTest.Action.CREATE_AND_UNPACK);
+        }
+    }
+
+    boolean isWithDesktopIntegration(IconType iconType) {
+        if (appImage) {
+            return false;
+        }
+        boolean withDesktopFile = !Set.of(
+                IconType.NoIcon,
+                IconType.DefaultIcon).contains(iconType);
+        withDesktopFile |= List.of(extraJPackageArgs).contains("--linux-shortcut");
+        return withDesktopFile;
+    }
+
+    private ThrowingBiConsumer<JPackageCommand, Executor.Result> createBundleVerifier() {
+        return (cmd, result) -> {
+            var verifier = createConsoleOutputVerifier(cmd.name(), config.get(
+                    Launcher.Main), null);
+            if (verifier != null) {
+                verifier.apply(result.getOutput().stream());
+            }
+
+            if (config.containsKey(Launcher.Additional)) {
+                verifier = createConsoleOutputVerifier(
+                        Launcher.Additional.launcherName, config.get(
+                                Launcher.Additional), config.get(Launcher.Main));
+                if (verifier != null) {
+                    verifier.apply(result.getOutput().stream());
+                }
+            }
+        };
+    }
+
+    private TKit.TextStreamVerifier createConsoleOutputVerifier(
+            String launcherName, IconType iconType, IconType mainIconType) {
+        if (iconType == IconType.DefaultIcon && mainIconType != null) {
+            iconType = mainIconType;
+        }
+        return createConsoleOutputVerifier(launcherName, iconType);
+    }
+
+    private static TKit.TextStreamVerifier createConsoleOutputVerifier(
+            String launcherName, IconType iconType) {
+        String lookupString = null;
+        switch (iconType) {
+            case DefaultIcon:
+                lookupString = String.format(
+                        "Using default package resource %s [icon] (add %s%s to the resource-dir to customize)",
+                        LauncherIconVerifier.getDefaultIcon().getFileName(),
+                        launcherName, TKit.ICON_SUFFIX);
+                break;
+
+            case ResourceDirIcon:
+                lookupString = String.format(
+                        "Using custom package resource [icon] (loaded from %s%s)",
+                        launcherName, TKit.ICON_SUFFIX);
+                break;
+
+            case CustomIcon:
+            case CustomWithResourceDirIcon:
+                lookupString = "Using custom package resource [icon] (loaded from file";
+                break;
+
+            default:
+                return null;
         }
 
-        testIt(JPackageCommand.helloAppImage().addArguments("--icon", iconPath));
+        return TKit.assertTextStream(lookupString);
+    }
+
+    private ThrowingConsumer<JPackageCommand> createInstallVerifier() {
+        LauncherIconVerifier verifier = new LauncherIconVerifier();
+        switch (config.get(Launcher.Main)) {
+            case NoIcon:
+                verifier.setExpectedIcon(null);
+                break;
+
+            case DefaultIcon:
+                verifier.setExpectedDefaultIcon();
+                break;
+
+            case CustomIcon:
+                verifier.setExpectedIcon(Launcher.Main.cmdlineIcon);
+                break;
+
+            case ResourceDirIcon:
+                verifier.setExpectedIcon(Launcher.Main.resourceDirIcon);
+                break;
+
+            case CustomWithResourceDirIcon:
+                verifier.setExpectedIcon(Launcher.Main2.cmdlineIcon);
+                break;
+        }
+
+        return cmd -> {
+            verifier.applyTo(cmd);
+            if (TKit.isLinux() && !cmd.isImagePackageType()) {
+                Path desktopFile = LinuxHelper.getDesktopFile(cmd);
+                if (isWithDesktopIntegration(config.get(Launcher.Main))) {
+                    TKit.assertFileExists(desktopFile);
+                } else {
+                    TKit.assertPathExists(desktopFile, false);
+                }
+            }
+        };
+    }
+
+    private void initTest(JPackageCommand cmd, PackageTest test) {
+        config.entrySet().forEach(ThrowingConsumer.toConsumer(entry -> {
+            initTest(entry.getKey(), entry.getValue(), cmd, test);
+        }));
+
+        ThrowingConsumer<JPackageCommand> initializer = testCmd -> {
+            testCmd.saveConsoleOutput(true);
+            testCmd.setFakeRuntime();
+            testCmd.addArguments(extraJPackageArgs);
+        };
+
+        if (test != null) {
+            test.addInitializer(initializer);
+        } else {
+            ThrowingConsumer.toConsumer(initializer).accept(cmd);
+        }
     }
 
-    private static String appIconFileName(JPackageCommand cmd) {
-        return IOUtils.replaceSuffix(cmd.appLauncherPath().getFileName(),
-                TKit.ICON_SUFFIX).toString();
+    private static void initTest(Launcher cfg, IconType iconType,
+            JPackageCommand cmd, PackageTest test) throws IOException {
+        Consumer<AdditionalLauncher> addLauncher = v -> {
+            if (test != null) {
+                v.applyTo(test);
+            } else {
+                v.applyTo(cmd);
+            }
+        };
+
+        switch (iconType) {
+            case DefaultIcon:
+                if (cfg.launcherName != null) {
+                    addLauncher.accept(new AdditionalLauncher(cfg.launcherName));
+                }
+                break;
+
+            case NoIcon:
+                if (cfg.launcherName != null) {
+                    addLauncher.accept(
+                            new AdditionalLauncher(cfg.launcherName).setNoIcon());
+                }
+                break;
+
+            case CustomIcon:
+                if (test != null) {
+                    addCustomIcon(null, test, cfg.launcherName, cfg.cmdlineIcon);
+                } else {
+                    addCustomIcon(cmd, null, cfg.launcherName, cfg.cmdlineIcon);
+                }
+                break;
+
+            case ResourceDirIcon:
+                if (Launcher.PRIMARY.contains(cfg) && cfg.launcherName != null) {
+                    addLauncher.accept(new AdditionalLauncher(cfg.launcherName));
+                }
+                if (test != null) {
+                    test.addInitializer(testCmd -> {
+                        addResourceDirIcon(testCmd, cfg.launcherName,
+                                cfg.resourceDirIcon);
+                    });
+                } else {
+                    addResourceDirIcon(cmd, cfg.launcherName, cfg.resourceDirIcon);
+                }
+                break;
+
+            case CustomWithResourceDirIcon:
+                switch (cfg) {
+                    case Main:
+                        initTest(Launcher.Main2, IconType.CustomIcon, cmd, test);
+                        initTest(Launcher.Main2, IconType.ResourceDirIcon, cmd, test);
+                        break;
+
+                    case Additional:
+                        initTest(Launcher.Additional2, IconType.CustomIcon, cmd, test);
+                        initTest(Launcher.Additional2, IconType.ResourceDirIcon, cmd, test);
+                        break;
+
+                    default:
+                        throw new IllegalArgumentException();
+                }
+                break;
+        }
+    }
+
+    private JPackageCommand initAppImageTest() {
+        JPackageCommand cmd = JPackageCommand.helloAppImage();
+        initTest(cmd, null);
+        return cmd;
     }
 
-    private static void testIt(JPackageCommand cmd) throws IOException {
-        cmd.executeAndAssertHelloAppImageCreated();
-
-        Path iconPath = cmd.appLayout().destktopIntegrationDirectory().resolve(
-                appIconFileName(cmd));
+    private PackageTest initPackageTest() {
+        PackageTest test = new PackageTest().configureHelloApp();
+        initTest(null, test);
+        return test;
+    }
 
-        TKit.assertFileExists(iconPath);
-        TKit.assertTrue(-1 == Files.mismatch(GOLDEN_ICON, iconPath),
-                String.format(
-                        "Check application icon file [%s] is a copy of source icon file [%s]",
-                        iconPath, GOLDEN_ICON));
+    private static void addResourceDirIcon(JPackageCommand cmd,
+            String launcherName, Path iconPath) throws IOException {
+        Path resourceDir = cmd.getArgumentValue("--resource-dir", () -> null,
+                Path::of);
+        if (resourceDir == null) {
+            resourceDir = TKit.createTempDirectory("resources");
+            cmd.addArguments("--resource-dir", resourceDir);
+        }
+
+        String dstIconFileName = Optional.ofNullable(launcherName).orElseGet(
+                () -> cmd.name()) + TKit.ICON_SUFFIX;
+
+        TKit.trace(String.format("Resource file: [%s] <- [%s]",
+                resourceDir.resolve(dstIconFileName), iconPath));
+        Files.copy(iconPath, resourceDir.resolve(dstIconFileName),
+                StandardCopyOption.REPLACE_EXISTING);
     }
 
-    private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of(
-            "resources", "icon" + TKit.ICON_SUFFIX));
+    private static void addCustomIcon(JPackageCommand cmd, PackageTest test,
+            String launcherName, Path iconPath) throws IOException {
+
+        if (launcherName != null) {
+            AdditionalLauncher al = new AdditionalLauncher(launcherName).setIcon(
+                    iconPath);
+            if (test != null) {
+                al.applyTo(test);
+            } else {
+                al.applyTo(cmd);
+            }
+        } else if (test != null) {
+            test.addInitializer(testCmd -> {
+                testCmd.addArguments("--icon", iconPath);
+            });
+        } else {
+            cmd.addArguments("--icon", iconPath);
+        }
+    }
+
+    private enum Launcher {
+        Main(null, ICONS[0], ICONS[1]),
+        Main2(null, ICONS[1], ICONS[0]),
+        Additional("x", ICONS[2], ICONS[3]),
+        Additional2("x", ICONS[3], ICONS[2]);
+
+        Launcher(String name, Path cmdlineIcon, Path resourceDirIcon) {
+            this.launcherName = name;
+            this.cmdlineIcon = cmdlineIcon;
+            this.resourceDirIcon = resourceDirIcon;
+        }
+
+        private final String launcherName;
+        private final Path cmdlineIcon;
+        private final Path resourceDirIcon;
+
+        private final static Set<Launcher> PRIMARY = Set.of(Main, Additional);
+    }
+
+    private final boolean appImage;
+    private final Map<Launcher, IconType> config;
+    private final String[] extraJPackageArgs;
+
+    private static Path iconPath(String name) {
+        return TKit.TEST_SRC_ROOT.resolve(Path.of("resources", name
+                + TKit.ICON_SUFFIX));
+    }
+
+    private final static Path[] ICONS = Stream.of("icon", "icon2", "icon3",
+            "icon4")
+            .map(IconTest::iconPath)
+            .collect(Collectors.toList()).toArray(Path[]::new);
 }
--- a/test/jdk/tools/jpackage/share/InstallDirTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/InstallDirTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -126,12 +126,8 @@
             cmd.saveConsoleOutput(true);
         })
         .addBundleVerifier((cmd, result) -> {
-            String errorMessage = JPackageCommand.filterOutput(
-                    result.getOutput().stream()).filter(line -> line.contains(
-                    errorMessageSubstring)).findFirst().orElse(null);
-            TKit.assertNotNull(errorMessage, String.format(
-                    "Check output contains [%s] substring",
-                    errorMessageSubstring));
+            TKit.assertTextStream(errorMessageSubstring).apply(
+                    result.getOutput().stream());
         })
         .run();
     }
--- a/test/jdk/tools/jpackage/share/LicenseTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/LicenseTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -99,7 +99,10 @@
             verifyLicenseFileInLinuxPackage(cmd, linuxLicenseFile(cmd));
         })
         .addInstallVerifier(cmd -> {
-            TKit.assertReadableFileExists(linuxLicenseFile(cmd));
+            Path path = linuxLicenseFile(cmd);
+            if (path != null) {
+                TKit.assertReadableFileExists(path);
+            }
         })
         .addUninstallVerifier(cmd -> {
             verifyLicenseFileNotInstalledLinux(linuxLicenseFile(cmd));
@@ -110,7 +113,10 @@
         })
         .forTypes(PackageType.LINUX_RPM)
         .addInstallVerifier(cmd -> {
-            verifyLicenseFileInstalledRpm(rpmLicenseFile(cmd));
+            Path path = rpmLicenseFile(cmd);
+            if (path != null) {
+                verifyLicenseFileInstalledRpm(path);
+            }
         })
         .run();
     }
@@ -124,6 +130,10 @@
     }
 
     private static Path rpmLicenseFile(JPackageCommand cmd) {
+        if (cmd.isPackageUnpacked("Not checking for rpm license file")) {
+            return null;
+        }
+
         final Path licenseRoot = Path.of(
                 new Executor()
                 .setExecutable("rpm")
@@ -236,7 +246,7 @@
 
         void run() {
             final Path srcLicenseFile = TKit.workDir().resolve("license");
-            new PackageTest().configureHelloApp().forTypes(PackageType.LINUX_DEB)
+            new PackageTest().forTypes(PackageType.LINUX_DEB).configureHelloApp()
             .addInitializer(cmd -> {
                 // Create source license file.
                 Files.write(srcLicenseFile, List.of(
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -30,6 +30,7 @@
 import java.util.ArrayList;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -119,6 +120,10 @@
     @SuppressWarnings("unchecked")
     public void testVerbose() {
         JPackageCommand cmd = JPackageCommand.helloAppImage()
+                // Disable default logic adding `--verbose` option
+                // to jpackage command line.
+                .ignoreDefaultVerbose(true)
+                .saveConsoleOutput(true)
                 .setFakeRuntime().executePrerequisiteActions();
 
         List<String> expectedVerboseOutputStrings = new ArrayList<>();
@@ -139,17 +144,17 @@
         }
 
         TKit.deleteDirectoryContentsRecursive(cmd.outputDir());
-        List<String> nonVerboseOutput = cmd.createExecutor().executeAndGetOutput();
+        List<String> nonVerboseOutput = cmd.execute().getOutput();
         List<String>[] verboseOutput = (List<String>[])new List<?>[1];
 
         // Directory clean up is not 100% reliable on Windows because of
         // antivirus software that can lock .exe files. Setup
-        // diffreent output directory instead of cleaning the default one for
+        // different output directory instead of cleaning the default one for
         // verbose jpackage run.
         TKit.withTempDirectory("verbose-output", tempDir -> {
             cmd.setArgumentValue("--dest", tempDir);
-            verboseOutput[0] = cmd.createExecutor().addArgument(
-                    "--verbose").executeAndGetOutput();
+            cmd.addArgument("--verbose");
+            verboseOutput[0] = cmd.execute().getOutput();
         });
 
         TKit.assertTrue(nonVerboseOutput.size() < verboseOutput[0].size(),
@@ -189,7 +194,7 @@
     }
 
     @Test
-     // Regular app
+    // Regular app
     @Parameter("Hello")
     // Modular app
     @Parameter("com.other/com.other.Hello")
@@ -227,62 +232,66 @@
      */
     @Test
     public void testTemp() throws IOException {
-        TKit.withTempDirectory("temp-root", tempRoot -> {
-            Function<JPackageCommand, Path> getTempDir = cmd -> {
-                return tempRoot.resolve(cmd.outputBundle().getFileName());
-            };
+        final Path tempRoot = TKit.createTempDirectory("temp-root");
+
+        Function<JPackageCommand, Path> getTempDir = cmd -> {
+            return tempRoot.resolve(cmd.outputBundle().getFileName());
+        };
 
-            ThrowingConsumer<JPackageCommand> addTempDir = cmd -> {
+        Supplier<PackageTest> createTest = () -> {
+            return new PackageTest()
+            .configureHelloApp()
+            // Force save of package bundle in test work directory.
+            .addInitializer(JPackageCommand::setDefaultInputOutput)
+            .addInitializer(cmd -> {
                 Path tempDir = getTempDir.apply(cmd);
                 Files.createDirectories(tempDir);
                 cmd.addArguments("--temp", tempDir);
-            };
+            });
+        };
 
-            new PackageTest().configureHelloApp().addInitializer(addTempDir)
-            .addBundleVerifier(cmd -> {
-                // Check jpackage actually used the supplied directory.
-                Path tempDir = getTempDir.apply(cmd);
-                TKit.assertNotEquals(0, tempDir.toFile().list().length,
-                        String.format(
-                                "Check jpackage wrote some data in the supplied temporary directory [%s]",
-                                tempDir));
-            })
-            .run();
+        createTest.get()
+        .addBundleVerifier(cmd -> {
+            // Check jpackage actually used the supplied directory.
+            Path tempDir = getTempDir.apply(cmd);
+            TKit.assertNotEquals(0, tempDir.toFile().list().length,
+                    String.format(
+                            "Check jpackage wrote some data in the supplied temporary directory [%s]",
+                            tempDir));
+        })
+        .run(PackageTest.Action.CREATE);
 
-            new PackageTest().configureHelloApp().addInitializer(addTempDir)
-            .addInitializer(cmd -> {
-                // Clean output from the previus jpackage run.
-                Files.delete(cmd.outputBundle());
-            })
-            // Temporary directory should not be empty,
-            // jpackage should exit with error.
-            .setExpectedExitCode(1)
-            .run();
-        });
+        createTest.get()
+        .addInitializer(cmd -> {
+            // Clean output from the previus jpackage run.
+            Files.delete(cmd.outputBundle());
+        })
+        // Temporary directory should not be empty,
+        // jpackage should exit with error.
+        .setExpectedExitCode(1)
+        .run(PackageTest.Action.CREATE);
     }
 
     @Test
     public void testAtFile() throws IOException {
-        JPackageCommand cmd = JPackageCommand.helloAppImage();
+        JPackageCommand cmd = JPackageCommand
+                .helloAppImage()
+                .setArgumentValue("--dest", TKit.createTempDirectory("output"));
 
         // Init options file with the list of options configured
         // for JPackageCommand instance.
-        final Path optionsFile = TKit.workDir().resolve("options");
+        final Path optionsFile = TKit.createTempFile(Path.of("options"));
         Files.write(optionsFile,
                 List.of(String.join(" ", cmd.getAllArguments())));
 
         // Build app jar file.
         cmd.executePrerequisiteActions();
 
-        // Make sure output directory is empty. Normally JPackageCommand would
-        // do this automatically.
-        TKit.deleteDirectoryContentsRecursive(cmd.outputDir());
-
         // Instead of running jpackage command through configured
         // JPackageCommand instance, run vanilla jpackage command with @ file.
         getJPackageToolProvider()
                 .addArgument(String.format("@%s", optionsFile))
-                .execute().assertExitCodeIsZero();
+                .execute();
 
         // Verify output of jpackage command.
         cmd.assertImageCreated();
@@ -292,50 +301,45 @@
     @Parameter("Hello")
     @Parameter("com.foo/com.foo.main.Aloha")
     @Test
-    public void testJLinkRuntime(String javaAppDesc) {
-        JPackageCommand cmd = JPackageCommand.helloAppImage(javaAppDesc);
+    public void testJLinkRuntime(String javaAppDesc) throws IOException {
+        JavaAppDesc appDesc = JavaAppDesc.parse(javaAppDesc);
 
-        // If `--module` parameter was set on jpackage command line, get its
-        // value and extract module name.
-        // E.g.: foo.bar2/foo.bar.Buz -> foo.bar2
-        // Note: HelloApp class manages `--module` parameter on jpackage command line
-        final String moduleName = cmd.getArgumentValue("--module", () -> null,
-                (v) -> v.split("/", 2)[0]);
+        JPackageCommand cmd = JPackageCommand.helloAppImage(appDesc);
+
+        final String moduleName = appDesc.moduleName();
 
         if (moduleName != null) {
             // Build module jar.
             cmd.executePrerequisiteActions();
         }
 
-        TKit.withTempDirectory("runtime", tempDir -> {
-            final Path runtimeDir = tempDir.resolve("data");
+        final Path runtimeDir = TKit.createTempDirectory("runtime").resolve("data");
 
-            // List of modules required for test app.
-            final var modules = new String[] {
-                "java.base",
-                "java.desktop"
-            };
+        // List of modules required for test app.
+        final var modules = new String[] {
+            "java.base",
+            "java.desktop"
+        };
 
-            Executor jlink = getToolProvider(JavaTool.JLINK)
-            .saveOutput(false)
-            .addArguments(
-                    "--add-modules", String.join(",", modules),
-                    "--output", runtimeDir.toString(),
-                    "--strip-debug",
-                    "--no-header-files",
-                    "--no-man-pages");
+        Executor jlink = getToolProvider(JavaTool.JLINK)
+        .saveOutput(false)
+        .addArguments(
+                "--add-modules", String.join(",", modules),
+                "--output", runtimeDir.toString(),
+                "--strip-debug",
+                "--no-header-files",
+                "--no-man-pages");
 
-            if (moduleName != null) {
-                jlink.addArguments("--add-modules", moduleName, "--module-path",
-                        Path.of(cmd.getArgumentValue("--module-path")).resolve(
-                                "hello.jar").toString());
-            }
+        if (moduleName != null) {
+            jlink.addArguments("--add-modules", moduleName, "--module-path",
+                    Path.of(cmd.getArgumentValue("--module-path")).resolve(
+                            "hello.jar").toString());
+        }
 
-            jlink.execute().assertExitCodeIsZero();
+        jlink.execute();
 
-            cmd.addArguments("--runtime-image", runtimeDir);
-            cmd.executeAndAssertHelloAppImageCreated();
-        });
+        cmd.addArguments("--runtime-image", runtimeDir);
+        cmd.executeAndAssertHelloAppImageCreated();
     }
 
     private static Executor getJPackageToolProvider() {
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -209,8 +209,7 @@
             // file nor on command line.
             List<String> output = cmd
                     .saveConsoleOutput(true)
-                    .execute()
-                    .assertExitCodeIs(1)
+                    .execute(1)
                     .getOutput();
             TKit.assertTextStream(script.expectedErrorMessage).apply(output.stream());
             return;
@@ -236,7 +235,7 @@
                     .setDirectory(cmd.outputDir())
                     .setExecutable(cmd.appLauncherPath())
                     .dumpOutput().saveOutput()
-                    .execute().assertExitCodeIs(1).getOutput();
+                    .execute(1).getOutput();
                 TKit.assertTextStream(String.format(
                         "Error: Could not find or load main class %s",
                         nonExistingMainClass)).apply(output.stream());
@@ -289,7 +288,7 @@
             .addArguments("-v", "-c", "-M", "-f", jarFile.toString())
             .addArguments("-C", workDir.toString(), ".")
             .dumpOutput()
-            .execute().assertExitCodeIsZero();
+            .execute();
         });
     }
 
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -24,6 +24,7 @@
 package jdk.jpackage.tests;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
@@ -71,7 +72,7 @@
     }
 
     @Test
-    public void test() {
+    public void test() throws IOException {
         final String moduleName = "com.foo";
         JPackageCommand cmd = JPackageCommand.helloAppImage(
                 "benvenuto.jar:" + moduleName + "/com.foo.Hello");
@@ -88,45 +89,42 @@
         String goodModulePath = Objects.requireNonNull(cmd.getArgumentValue(
                 "--module-path"));
         cmd.removeArgumentWithValue("--module-path");
-        TKit.withTempDirectory("empty-dir", emptyDir -> {
-            Path nonExistingDir = TKit.withTempDirectory("non-existing-dir",
-                    unused -> {
-                    });
+
+        Path emptyDir = TKit.createTempDirectory("empty-dir");
+        Path nonExistingDir = TKit.withTempDirectory("non-existing-dir", x -> {});
 
-            Function<String, String> substitute = str -> {
-                String v = str;
-                v = v.replace(GOOD_PATH, goodModulePath);
-                v = v.replace(EMPTY_DIR, emptyDir.toString());
-                v = v.replace(NON_EXISTING_DIR, nonExistingDir.toString());
-                return v;
-            };
+        Function<String, String> substitute = str -> {
+            String v = str;
+            v = v.replace(GOOD_PATH, goodModulePath);
+            v = v.replace(EMPTY_DIR, emptyDir.toString());
+            v = v.replace(NON_EXISTING_DIR, nonExistingDir.toString());
+            return v;
+        };
 
-            boolean withGoodPath = modulePathArgs.stream().anyMatch(
-                    s -> s.contains(GOOD_PATH));
-
-            cmd.addArguments(modulePathArgs.stream().map(arg -> Stream.of(
-                    "--module-path", substitute.apply(arg))).flatMap(s -> s).collect(
-                    Collectors.toList()));
+        boolean withGoodPath = modulePathArgs.stream().anyMatch(
+                s -> s.contains(GOOD_PATH));
 
-            if (withGoodPath) {
-                cmd.executeAndAssertHelloAppImageCreated();
+        cmd.addArguments(modulePathArgs.stream().map(arg -> Stream.of(
+                "--module-path", substitute.apply(arg))).flatMap(s -> s).collect(
+                Collectors.toList()));
+
+        if (withGoodPath) {
+            cmd.executeAndAssertHelloAppImageCreated();
+        } else {
+            final String expectedErrorMessage;
+            if (modulePathArgs.isEmpty()) {
+                expectedErrorMessage = "Error: Missing argument: --runtime-image or --module-path";
             } else {
-                final String expectedErrorMessage;
-                if (modulePathArgs.isEmpty()) {
-                    expectedErrorMessage = "Error: Missing argument: --runtime-image or --module-path";
-                } else {
-                    expectedErrorMessage = String.format(
-                            "Error: Module %s not found", moduleName);
-                }
+                expectedErrorMessage = String.format(
+                        "Error: Module %s not found", moduleName);
+            }
 
-                List<String> output = cmd
-                        .saveConsoleOutput(true)
-                        .execute()
-                        .assertExitCodeIs(1)
-                        .getOutput();
-                TKit.assertTextStream(expectedErrorMessage).apply(output.stream());
-            }
-        });
+            List<String> output = cmd
+                    .saveConsoleOutput(true)
+                    .execute(1)
+                    .getOutput();
+            TKit.assertTextStream(expectedErrorMessage).apply(output.stream());
+        }
     }
 
     private final List<String> modulePathArgs;
--- a/test/jdk/tools/jpackage/test_jpackage.sh	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/test_jpackage.sh	Tue Dec 17 13:56:47 2019 -0500
@@ -29,13 +29,20 @@
   local arg_is_output_dir=
   local arg_is_mode=
   local output_dir_set=
+  local with_append_actions=yes
   for arg in "$@"; do
     if [ "$arg" == "-o" ]; then
       arg_is_output_dir=yes
       output_dir_set=yes
     elif [ "$arg" == "-m" ]; then
       arg_is_mode=yes
-    continue
+      continue
+    elif [ "$arg" == '--' ]; then
+      append_actions
+      with_append_actions=
+      continue
+    elif ! case "$arg" in -Djpackage.test.action=*) false;; esac; then
+      continue
     elif [ -n "$arg_is_output_dir" ]; then
       arg_is_output_dir=
       output_dir="$arg"
@@ -47,6 +54,13 @@
     args+=( "$arg" )
   done
   [ -n "$output_dir_set" ] || args=( -o "$output_dir" "${args[@]}" )
+  [ -z "$with_append_actions" ] || append_actions
+}
+
+
+append_actions ()
+{
+  args+=( '--' '-Djpackage.test.action=create,install,verify-install,uninstall,verify-uninstall' )
 }
 
 
@@ -62,7 +76,3 @@
 set_args "$@"
 basedir="$(dirname $0)"
 exec_command "$basedir/run_tests.sh" -m create "${args[@]}"
-exec_command "$basedir/manage_packages.sh" -d "$output_dir"
-exec_command "$basedir/run_tests.sh" -m verify-install "${args[@]}"
-exec_command "$basedir/manage_packages.sh" -d "$output_dir" -u
-exec_command "$basedir/run_tests.sh" -m verify-uninstall "${args[@]}"
--- a/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -26,6 +26,7 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import jdk.jpackage.test.TKit;
+import jdk.jpackage.test.HelloApp;
 import jdk.jpackage.test.JPackageCommand;
 import jdk.jpackage.test.Annotations.Test;
 import jdk.jpackage.test.Annotations.Parameter;
@@ -58,6 +59,10 @@
         }
         cmd.executeAndAssertHelloAppImageCreated();
         checkSubsystem(cmd.appLauncherPath(), withWinConsole);
+
+        // Run launcher with a number of arguments to make sure they go through
+        // regardless the launcher has or doesn't have console.
+        HelloApp.executeLauncherAndVerifyOutput(cmd, "a", "b", "c");
     }
 
     private static void checkSubsystem(Path path, boolean isConsole) throws
--- a/test/jdk/tools/jpackage/windows/WinScriptTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/windows/WinScriptTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -70,7 +70,7 @@
     @Test
     @Parameter("0")
     @Parameter("10")
-    public void test(int wsfExitCode) {
+    public void test(int wsfExitCode) throws IOException {
         final ScriptData appImageScriptData;
         if (wsfExitCode != 0 && packageType == PackageType.WIN_EXE) {
             appImageScriptData = new ScriptData(PackageType.WIN_MSI, 0);
@@ -81,29 +81,32 @@
         final ScriptData msiScriptData = new ScriptData(PackageType.WIN_EXE, wsfExitCode);
 
         test.setExpectedExitCode(wsfExitCode == 0 ? 0 : 1);
-        TKit.withTempDirectory("resources", tempDir -> {
-            test.addInitializer(cmd -> {
-                cmd.addArguments("--resource-dir", tempDir);
+
+        final Path tempDir = TKit.createTempDirectory("resources");
+
+        test.addInitializer(cmd -> {
+            cmd.addArguments("--resource-dir", tempDir);
 
-                appImageScriptData.createScript(cmd);
-                msiScriptData.createScript(cmd);
-            });
+            appImageScriptData.createScript(cmd);
+            msiScriptData.createScript(cmd);
+        });
 
-            if (packageType == PackageType.WIN_MSI) {
+        switch (packageType) {
+            case WIN_MSI:
                 test.addBundleVerifier((cmd, result) -> {
                     appImageScriptData.assertJPackageOutput(result.getOutput());
                 });
-            }
+                break;
 
-            if (packageType == PackageType.WIN_EXE) {
+            case WIN_EXE:
                 test.addBundleVerifier((cmd, result) -> {
                     appImageScriptData.assertJPackageOutput(result.getOutput());
                     msiScriptData.assertJPackageOutput(result.getOutput());
                 });
-            }
+                break;
+        }
 
-            test.run();
-        });
+        test.run();
     }
 
     private static class ScriptData {
--- a/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java	Tue Dec 17 18:23:33 2019 +0100
+++ b/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java	Tue Dec 17 13:56:47 2019 -0500
@@ -21,9 +21,16 @@
  * questions.
  */
 
-import jdk.jpackage.test.TKit;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Supplier;
+import jdk.jpackage.test.Annotations.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.WindowsHelper;
+import jdk.jpackage.test.TKit;
 
 /**
  * Test both --win-upgrade-uuid and --app-version parameters. Output of the test
@@ -41,34 +48,155 @@
  * @summary jpackage with --win-upgrade-uuid and --app-version
  * @library ../helpers
  * @key jpackagePlatformPackage
+ * @requires (jpackage.test.SQETest != null)
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
  * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m WinUpgradeUUIDTest
+ * @compile WinUpgradeUUIDTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinUpgradeUUIDTest.test
+ */
+
+/*
+ * @test
+ * @summary jpackage with --win-upgrade-uuid and --app-version
+ * @library ../helpers
+ * @key jpackagePlatformPackage
+ * @requires (jpackage.test.SQETest == null)
+ * @build jdk.jpackage.test.*
+ * @requires (os.family == "windows")
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile WinUpgradeUUIDTest.java
+ * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinUpgradeUUIDTest
  */
 
 public class WinUpgradeUUIDTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            PackageTest test = init();
-            if (test.getAction() != PackageTest.Action.VERIFY_INSTALL) {
-                test.run();
+
+    @Test
+    public static void test() {
+        Supplier<PackageTest> init = () -> {
+            final UUID upgradeCode = UUID.fromString(
+                    "F0B18E75-52AD-41A2-BC86-6BE4FCD50BEB");
+            return new PackageTest()
+                .forTypes(PackageType.WINDOWS)
+                .configureHelloApp()
+                .addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid",
+                        upgradeCode.toString()))
+                .forTypes(PackageType.WIN_MSI)
+                .addBundlePropertyVerifier("UpgradeCode", value -> {
+                    if (value.startsWith("{")) {
+                        value = value.substring(1);
+                    }
+                    if (value.endsWith("}")) {
+                        value = value.substring(0, value.length() - 1);
+                    }
+                    return UUID.fromString(value).equals(upgradeCode);
+                }, "is a match with");
+        };
+
+        // Replace real uninstall command for the first package with nop action.
+        // It will be uninstalled automatically when the second
+        // package will be installed.
+        // However uninstall verification for the first package will be executed.
+        PackageTest test1 = init.get().setPackageUninstaller(cmd -> {});
+
+        PackageTest test2 = init.get().addInitializer(cmd -> {
+            cmd.setArgumentValue("--app-version", "2.0");
+            cmd.setArgumentValue("--arguments", "bar");
+        });
+
+        new PackageTest.Group(test1, test2).run();
+    }
+
+    /**
+     * Running jpackage multiple times with the same parameters should produce
+     * MSI packages with the same UpgradeCode and ProductCode values.
+     */
+    @Test
+    public static void testUUIDs() {
+        Supplier<PackageTest> init = () -> {
+            return new PackageTest()
+            .forTypes(PackageType.WIN_MSI)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.setFakeRuntime();
+                cmd.setArgumentValue("--dest", TKit.createTempDirectory("output"));
+            });
+        };
+
+        PackageTest test1 = init.get();
+        PackageTest test2 = init.get();
+        PackageTest test3 = init.get().addInitializer(cmd -> {
+            cmd.addArguments("--app-version", "2.0");
+        });
+        PackageTest test4 = init.get().addInitializer(cmd -> {
+            cmd.addArguments("--app-version", "2.0");
+            cmd.addArguments("--vendor", "Foo Inc.");
+        });
+
+        PackageTest[] tests = new PackageTest[] { test1, test2, test3, test4 };
+
+        var productCodeVerifier = createPropertyVerifier("ProductCode", tests);
+        var upgradeCodeVerifier = createPropertyVerifier("UpgradeCode", tests);
+
+        List.of(tests).forEach(test -> {
+            test.run(PackageTest.Action.CREATE);
+        });
+
+        productCodeVerifier.assertEquals(test1, test2);
+        productCodeVerifier.assertNotEquals(test1, test3);
+        productCodeVerifier.assertNotEquals(test1, test4);
+        productCodeVerifier.assertNotEquals(test3, test4);
+
+        upgradeCodeVerifier.assertEquals(test1, test2);
+        upgradeCodeVerifier.assertEquals(test1, test3);
+        upgradeCodeVerifier.assertNotEquals(test1, test4);
+    }
+
+    private static PropertyVerifier createPropertyVerifier(String propertyName,
+            PackageTest... tests) {
+        Map<PackageTest, Map.Entry<String, String>> properties = new HashMap<>();
+        List.of(tests).forEach(test -> {
+            test.addBundleVerifier(cmd -> {
+                properties.put(test, Map.entry(cmd.getPrintableCommandLine(),
+                        WindowsHelper.getMsiProperty(cmd, propertyName)));
+            });
+        });
+
+        return new PropertyVerifier() {
+            @Override
+            protected String propertyName() {
+                return propertyName;
             }
 
-            test = init();
-            test.addInitializer(cmd -> {
-                cmd.setArgumentValue("--app-version", "2.0");
-                cmd.setArgumentValue("--arguments", "bar");
-            });
-            test.run();
-        });
+            @Override
+            protected Map<PackageTest, Map.Entry<String, String>> propertyValues() {
+                return properties;
+            }
+        };
     }
 
-    private static PackageTest init() {
-        return new PackageTest()
-            .forTypes(PackageType.WINDOWS)
-            .configureHelloApp()
-            .addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid",
-                    "F0B18E75-52AD-41A2-BC86-6BE4FCD50BEB"));
+    static abstract class PropertyVerifier {
+        void assertEquals(PackageTest x, PackageTest y) {
+            var entryX = propertyValues().get(x);
+            var entryY = propertyValues().get(y);
+            TKit.assertEquals(entryX.getValue(), entryY.getValue(),
+                    String.format(
+                            "Check %s is the same for %s and %s command lines",
+                            propertyName(), entryX.getKey(), entryY.getKey()));
+        }
+
+        void assertNotEquals(PackageTest x, PackageTest y) {
+            var entryX = propertyValues().get(x);
+            var entryY = propertyValues().get(y);
+            TKit.assertNotEquals(entryX.getValue(), entryY.getValue(),
+                    String.format(
+                            "Check %s is different for %s and %s command lines",
+                            propertyName(), entryX.getKey(), entryY.getKey()));
+        }
+
+        protected abstract String propertyName();
+        protected abstract Map<PackageTest, Map.Entry<String, String>> propertyValues();
     }
 }