8232279 : Improve test helpers #2 JDK-8200758-branch
authorherrick
Wed, 16 Oct 2019 10:32:08 -0400
branchJDK-8200758-branch
changeset 58648 3bf53ffa9ae7
parent 58647 2c43b89b1679
child 58670 6fb9e12d5595
8232279 : Improve test helpers #2 Submitted-by: asemenyuk Reviewed-by: aherrick, almatvee
src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.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/JavaAppDesc.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/Main.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.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/junit/jdk/jpackage/internal/AppImageFileTest.java
test/jdk/tools/jpackage/macosx/BundleIdentifierTest.java
test/jdk/tools/jpackage/macosx/BundleNameTest.java
test/jdk/tools/jpackage/macosx/MacPropertiesTest.java
test/jdk/tools/jpackage/macosx/SigningAppImageTest.java
test/jdk/tools/jpackage/macosx/SigningPackageTest.java
test/jdk/tools/jpackage/macosx/base/SigningBase.java
test/jdk/tools/jpackage/macosx/base/SigningCheck.java
test/jdk/tools/jpackage/share/AddModulesTest.java
test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java
test/jdk/tools/jpackage/share/AppMainClassModuleTest.java
test/jdk/tools/jpackage/share/AppVersionModuleTest.java
test/jdk/tools/jpackage/share/AppVersionTest.java
test/jdk/tools/jpackage/share/ArgumentsTest.java
test/jdk/tools/jpackage/share/FileAssociationsTest.java
test/jdk/tools/jpackage/share/HelpTest.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/MainClassAttributeTest.java
test/jdk/tools/jpackage/share/MainClassErrorTest.java
test/jdk/tools/jpackage/share/ModuleMainClassErrorTest.java
test/jdk/tools/jpackage/share/ModulePathTest.java
test/jdk/tools/jpackage/share/ResourceTest.java
test/jdk/tools/jpackage/share/SimplePackageTest.java
test/jdk/tools/jpackage/share/VerboseTest.java
test/jdk/tools/jpackage/share/WithSpaceTest.java
test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.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/windows/WinConsoleTest.java
test/jdk/tools/jpackage/windows/WinMenuGroupTest.java
test/jdk/tools/jpackage/windows/WinMenuTest.java
test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java
test/jdk/tools/jpackage/windows/WinShortcutTest.java
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java	Wed Oct 16 10:32:08 2019 -0400
@@ -129,8 +129,6 @@
         ));
     }
 
-
-
     public static ApplicationLayout platformAppImage() {
         if (Platform.isWindows()) {
             return windowsAppImage();
@@ -147,7 +145,7 @@
         throw Platform.throwUnknownPlatformError();
     }
 
-    static ApplicationLayout javaRuntime() {
+    public static ApplicationLayout javaRuntime() {
         return new ApplicationLayout(Map.of(PathRole.RUNTIME, Path.of("")));
     }
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java	Wed Oct 16 10:32:08 2019 -0400
@@ -32,21 +32,36 @@
 
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
+    public @interface BeforeEach {
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface AfterEach {
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
     public @interface Test {
     }
 
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
-    @Repeatable(Parameters.class)
+    @Repeatable(ParameterGroup.class)
     public @interface Parameter {
 
-        String value();
+        String[] value();
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface ParameterGroup {
+
+        Parameter[] value();
     }
 
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
     public @interface Parameters {
-
-        Parameter[] value();
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public final class CfgFile {
+    public String getValue(String section, String key) {
+        Objects.requireNonNull(section);
+        Objects.requireNonNull(key);
+
+        Map<String, String> entries = data.get(section);
+        TKit.assertTrue(entries != null, String.format(
+                "Check section [%s] is found in [%s] cfg file", section, id));
+
+        String value = entries.get(key);
+        TKit.assertNotNull(value, String.format(
+                "Check key [%s] is found in [%s] section of [%s] cfg file", key,
+                section, id));
+
+        return value;
+    }
+
+    private CfgFile(Map<String, Map<String, String>> data, String id) {
+        this.data = data;
+        this.id = id;
+    }
+
+    public static CfgFile readFromFile(Path path) throws IOException {
+        TKit.trace(String.format("Read [%s] jpackage cfg file", path));
+
+        final Pattern sectionBeginRegex = Pattern.compile( "\\s*\\[([^]]*)\\]\\s*");
+        final Pattern keyRegex = Pattern.compile( "\\s*([^=]*)=(.*)" );
+
+        Map<String, Map<String, String>> result = new HashMap<>();
+
+        String currentSectionName = null;
+        Map<String, String> currentSection = new HashMap<>();
+        for (String line : Files.readAllLines(path)) {
+            Matcher matcher = sectionBeginRegex.matcher(line);
+            if (matcher.find()) {
+                if (currentSectionName != null) {
+                    result.put(currentSectionName, Collections.unmodifiableMap(
+                            new HashMap<>(currentSection)));
+                }
+                currentSectionName = matcher.group(1);
+                currentSection.clear();
+                continue;
+            }
+
+            matcher = keyRegex.matcher(line);
+            if (matcher.find()) {
+                currentSection.put(matcher.group(1), matcher.group(2));
+                continue;
+            }
+        }
+
+        if (!currentSection.isEmpty()) {
+            result.put("", Collections.unmodifiableMap(currentSection));
+        }
+
+        return new CfgFile(Collections.unmodifiableMap(result), path.toString());
+    }
+
+    private final Map<String, Map<String, String>> data;
+    private final String id;
+}
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Wed Oct 16 10:32:08 2019 -0400
@@ -59,6 +59,10 @@
         return this;
     }
 
+    public Executor setToolProvider(JavaTool v) {
+        return setToolProvider(v.asToolProvider());
+    }
+
     public Executor setDirectory(Path v) {
         directory = v;
         return this;
@@ -167,6 +171,11 @@
     }
 
     public Result execute() {
+        if (toolProvider != null && directory != null) {
+            throw new IllegalArgumentException(
+                    "Can't change directory when using tool provider");
+        }
+
         return ThrowingSupplier.toSupplier(() -> {
             if (toolProvider != null) {
                 return runToolProvider();
@@ -193,9 +202,22 @@
                 SaveOutputType.FIRST_LINE);
     }
 
+    private Path executablePath() {
+        if (directory == null || executable.isAbsolute()) {
+            return executable;
+        }
+
+        // If relative path to executable is used it seems to be broken when
+        // ProcessBuilder changes the directory. On Windows it changes the
+        // directory first and on Linux it looks up for executable before
+        // changing the directory. So to stay of safe side, use absolute path
+        // to executable.
+        return executable.toAbsolutePath();
+    }
+
     private Result runExecutable() throws IOException, InterruptedException {
         List<String> command = new ArrayList<>();
-        command.add(executable.toString());
+        command.add(executablePath().toString());
         command.addAll(args);
         ProcessBuilder builder = new ProcessBuilder(command);
         StringBuilder sb = new StringBuilder(getPrintableCommandLine());
@@ -315,7 +337,7 @@
             format = "tool provider " + format;
             exec = toolProvider.name();
         } else {
-            exec = executable.toString();
+            exec = executablePath().toString();
         }
 
         return String.format(format, printCommandLine(exec, args),
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Wed Oct 16 10:32:08 2019 -0400
@@ -37,43 +37,27 @@
 
 public class HelloApp {
 
-    private HelloApp() {
-        setClassName(CLASS_NAME).setJarFileName("hello.jar");
-    }
-
-    /**
-     * Set fully qualified class name. E.g: foo.bar.Buzz.
-     */
-    private HelloApp setClassName(String v) {
-        qualifiedClassName = v;
-        return this;
-    }
-
-    private HelloApp setModuleName(String v) {
-        moduleName = v;
-        return this;
-    }
-
-    private HelloApp setJarFileName(String v) {
-        jarFileName = v;
-        return this;
-    }
-
-    private HelloApp setModuleVersion(String v) {
-        moduleVersion = v;
-        return this;
+    HelloApp(JavaAppDesc appDesc) {
+        if (appDesc == null) {
+            this.appDesc = createDefaltAppDesc();
+        } else {
+            this.appDesc = appDesc;
+        }
     }
 
     private JarBuilder prepareSources(Path srcDir) throws IOException {
+        final String qualifiedClassName = appDesc.className();
+
         final String className = qualifiedClassName.substring(
                 qualifiedClassName.lastIndexOf('.') + 1);
-        final String packageName = packageName();
+        final String packageName = appDesc.packageName();
 
         final Path srcFile = srcDir.resolve(Path.of(String.join(
                 File.separator, qualifiedClassName.split("\\.")) + ".java"));
         Files.createDirectories(srcFile.getParent());
 
         JarBuilder jarBuilder = createJarBuilder().addSourceFile(srcFile);
+        final String moduleName = appDesc.moduleName();
         if (moduleName != null) {
             Path moduleInfoFile = srcDir.resolve("module-info.java");
             TKit.createTextFile(moduleInfoFile, List.of(
@@ -82,9 +66,7 @@
                     "}"
             ));
             jarBuilder.addSourceFile(moduleInfoFile);
-            if (moduleVersion != null) {
-                jarBuilder.setModuleVersion(moduleVersion);
-            }
+            jarBuilder.setModuleVersion(appDesc.moduleVersion());
         }
 
         // Add package directive and replace class name in java source file.
@@ -124,11 +106,19 @@
     }
 
     private JarBuilder createJarBuilder() {
-        return new JarBuilder().setMainClass(qualifiedClassName);
+        JarBuilder builder = new JarBuilder();
+        if (appDesc.jarWithMainClass()) {
+            builder.setMainClass(appDesc.className());
+        }
+        return builder;
     }
 
-    private void addTo(JPackageCommand cmd) {
-        if (moduleName != null && packageName() == null) {
+    void addTo(JPackageCommand cmd) {
+        final String moduleName = appDesc.moduleName();
+        final String jarFileName = appDesc.jarFileName();
+        final String qualifiedClassName = appDesc.className();
+
+        if (moduleName != null && appDesc.packageName() == null) {
             throw new IllegalArgumentException(String.format(
                     "Module [%s] with default package", moduleName));
         }
@@ -177,55 +167,9 @@
         }
     }
 
-    private String packageName() {
-        int lastDotIdx = qualifiedClassName.lastIndexOf('.');
-        if (lastDotIdx == -1) {
-            return null;
-        }
-        return qualifiedClassName.substring(0, lastDotIdx);
-    }
-
-    /**
-     * Configures Java application to be used with the given jpackage command.
-     * Syntax of encoded Java application description is
-     * [jar_file:][module_name/]qualified_class_name[@module_version].
-     *
-     * E.g.: duke.jar:com.other/com.other.foo.bar.Buz@3.7 encodes modular
-     * application. Module name is `com.other`. Main class is
-     * `com.other.foo.bar.Buz`. Module version is `3.7`. Application will be
-     * compiled and packed in `duke.jar` jar file.
-     *
-     * @param cmd jpackage command to configure
-     * @param javaAppDesc encoded Java application description
-     */
-    static void addTo(JPackageCommand cmd, String javaAppDesc) {
-        HelloApp helloApp = new HelloApp();
-        if (javaAppDesc != null) {
-            String moduleNameAndOther = Functional.identity(() -> {
-                String[] components = javaAppDesc.split(":", 2);
-                if (components.length == 2) {
-                    helloApp.setJarFileName(components[0]);
-                }
-                return components[components.length - 1];
-            }).get();
-
-            String classNameAndOther = Functional.identity(() -> {
-                String[] components = moduleNameAndOther.split("/", 2);
-                if (components.length == 2) {
-                    helloApp.setModuleName(components[0]);
-                }
-                return components[components.length - 1];
-            }).get();
-
-            Functional.identity(() -> {
-                String[] components = classNameAndOther.split("@", 2);
-                helloApp.setClassName(components[0]);
-                if (components.length == 2) {
-                    helloApp.setModuleVersion(components[1]);
-                }
-            }).run();
-        }
-        helloApp.addTo(cmd);
+    static JavaAppDesc createDefaltAppDesc() {
+        return new JavaAppDesc().setClassName(CLASS_NAME).setJarFileName(
+                "hello.jar");
     }
 
     static void verifyOutputFile(Path outputFile, List<String> args) {
@@ -250,24 +194,12 @@
     }
 
     public static void executeLauncherAndVerifyOutput(JPackageCommand cmd) {
-        final Path launcherPath;
-        if (cmd.packageType() == PackageType.IMAGE) {
-            launcherPath = cmd.appImage().resolve(cmd.launcherPathInAppImage());
-            if (cmd.isFakeRuntimeInAppImage(String.format(
-                    "Not running [%s] launcher from application image",
-                    launcherPath))) {
-                return;
-            }
-        } else {
-            launcherPath = cmd.launcherInstallationPath();
-            if (cmd.isFakeRuntimeInstalled(String.format(
-                    "Not running [%s] launcher", launcherPath))) {
-                return;
-            }
+        final Path launcherPath = cmd.appLauncherPath();
+        if (!cmd.isFakeRuntime(String.format("Not running [%s] launcher",
+                launcherPath))) {
+            executeAndVerifyOutput(launcherPath, cmd.getAllArgumentValues(
+                    "--arguments"));
         }
-
-        executeAndVerifyOutput(launcherPath, cmd.getAllArgumentValues(
-                "--arguments"));
     }
 
     public static void executeAndVerifyOutput(Path helloAppLauncher,
@@ -278,7 +210,7 @@
     public static void executeAndVerifyOutput(Path helloAppLauncher,
             List<String> defaultLauncherArgs) {
         // Output file will be created in the current directory.
-        Path outputFile = Path.of(OUTPUT_FILENAME);
+        Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
         ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
         new Executor()
                 .setDirectory(outputFile.getParent())
@@ -291,10 +223,7 @@
 
     final static String OUTPUT_FILENAME = "appOutput.txt";
 
-    private String qualifiedClassName;
-    private String moduleName;
-    private String jarFileName;
-    private String moduleVersion;
+    private final JavaAppDesc appDesc;
 
     private static final Path HELLO_JAVA = TKit.TEST_SRC_ROOT.resolve(
             "apps/image/Hello.java");
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Wed Oct 16 10:32:08 2019 -0400
@@ -31,10 +31,13 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 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.jpackage.internal.ApplicationLayout;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
+import jdk.jpackage.test.Functional.ThrowingFunction;
 
 /**
  * jpackage command line with prerequisite actions. Prerequisite actions can be
@@ -47,10 +50,20 @@
         actions = new ArrayList<>();
     }
 
+    public JPackageCommand(JPackageCommand cmd) {
+        this();
+        args.addAll(cmd.args);
+        withToolProvider = cmd.withToolProvider;
+        saveConsoleOutput = cmd.saveConsoleOutput;
+        suppressOutput = cmd.suppressOutput;
+        ignoreDefaultRuntime = cmd.ignoreDefaultRuntime;
+        immutable = cmd.immutable;
+        actionsExecuted = cmd.actionsExecuted;
+    }
+
     JPackageCommand createImmutableCopy() {
-        JPackageCommand reply = new JPackageCommand();
+        JPackageCommand reply = new JPackageCommand(this);
         reply.immutable = true;
-        reply.args.addAll(args);
         return reply;
     }
 
@@ -152,6 +165,11 @@
         return addArguments(name, value.toString());
     }
 
+    public boolean isImagePackageType() {
+        return PackageType.IMAGE == getArgumentValue("--package-type",
+                () -> null, PACKAGE_TYPES::get);
+    }
+
     public PackageType packageType() {
         // Don't try to be in sync with jpackage defaults. Keep it simple:
         // if no `--package-type` explicitely set on the command line, consider
@@ -194,8 +212,21 @@
     public JPackageCommand setFakeRuntime() {
         verifyMutable();
 
-        try {
+        ThrowingConsumer<Path> createBulkFile = path -> {
+            Files.createDirectories(path.getParent());
+            try (FileOutputStream out = new FileOutputStream(path.toFile())) {
+                byte[] bytes = new byte[4 * 1024];
+                new SecureRandom().nextBytes(bytes);
+                out.write(bytes);
+            }
+        };
+
+        addAction(cmd -> {
             Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime");
+
+            TKit.trace(String.format("Init fake runtime in [%s] directory",
+                    fakeRuntimeDir));
+
             Files.createDirectories(fakeRuntimeDir);
 
             if (TKit.isWindows() || TKit.isLinux()) {
@@ -206,22 +237,19 @@
                 fakeRuntimeDir.resolve("bin").toFile().mkdir();
             }
 
-            Path bulk = fakeRuntimeDir.resolve(Path.of("bin", "bulk"));
+            if (TKit.isOSX()) {
+                // Make MacAppImageBuilder happy
+                createBulkFile.accept(fakeRuntimeDir.resolve(Path.of(
+                        "Contents/Home/lib/jli/libjli.dylib")));
+            }
 
             // Mak sure fake runtime takes some disk space.
             // Package bundles with 0KB size are unexpected and considered
             // an error by PackageTest.
-            Files.createDirectories(bulk.getParent());
-            try (FileOutputStream out = new FileOutputStream(bulk.toFile())) {
-                byte[] bytes = new byte[4 * 1024];
-                new SecureRandom().nextBytes(bytes);
-                out.write(bytes);
-            }
+            createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk")));
 
-            addArguments("--runtime-image", fakeRuntimeDir);
-        } catch (IOException ex) {
-            throw new RuntimeException(ex);
-        }
+            cmd.addArguments("--runtime-image", fakeRuntimeDir);
+        });
 
         return this;
     }
@@ -232,23 +260,37 @@
         return this;
     }
 
+    /**
+     * Shorthand for {@code helloAppImage(null)}.
+     */
     public static JPackageCommand helloAppImage() {
-        return helloAppImage(null);
+        JavaAppDesc javaAppDesc = null;
+        return helloAppImage(javaAppDesc);
     }
 
     /**
      * Creates new JPackageCommand instance configured with the test Java app.
      * For the explanation of `javaAppDesc` parameter, see documentation for
-     * HelloApp.addTo() method.
+     * #JavaAppDesc.parse() method.
      *
      * @param javaAppDesc Java application description
      * @return this
      */
     public static JPackageCommand helloAppImage(String javaAppDesc) {
+        final JavaAppDesc appDesc;
+        if (javaAppDesc == null) {
+            appDesc = null;
+        } else {
+            appDesc = JavaAppDesc.parse(javaAppDesc);
+        }
+        return helloAppImage(appDesc);
+    }
+
+    public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) {
         JPackageCommand cmd = new JPackageCommand();
         cmd.setDefaultInputOutput().setDefaultAppName();
         PackageType.IMAGE.applyTo(cmd);
-        HelloApp.addTo(cmd, javaAppDesc);
+        new HelloApp(javaAppDesc).addTo(cmd);
         return cmd;
     }
 
@@ -262,37 +304,70 @@
         return addArguments("--name", TKit.getCurrentDefaultAppName());
     }
 
+    /**
+     * Returns path to output bundle of configured jpackage command.
+     *
+     * If this is build image command, returns path to application image directory.
+     */
     public Path outputBundle() {
-        final PackageType type = packageType();
-        if (PackageType.IMAGE == type) {
-            return null;
-        }
-
-        String bundleName = null;
-        if (PackageType.LINUX.contains(type)) {
+        final String bundleName;
+        if (isImagePackageType()) {
+            String dirName = name();
+            if (TKit.isOSX()) {
+                dirName = dirName + ".app";
+            }
+            bundleName = dirName;
+        } else if (TKit.isLinux()) {
             bundleName = LinuxHelper.getBundleName(this);
-        } else if (PackageType.WINDOWS.contains(type)) {
+        } else if (TKit.isWindows()) {
             bundleName = WindowsHelper.getBundleName(this);
-        } else if (PackageType.MAC.contains(type)) {
+        } else if (TKit.isOSX()) {
             bundleName = MacHelper.getBundleName(this);
+        } else {
+            throw TKit.throwUnknownPlatformError();
         }
 
         return outputDir().resolve(bundleName);
     }
 
     /**
-     * Returns path to directory where application will be installed.
+     * Returns application layout.
+     *
+     * If this is build image command, returns application image layout of the
+     * output bundle relative to output directory. Otherwise returns layout of
+     * installed application relative to the root directory.
+     *
+     * If this command builds Java runtime, not an application, returns
+     * corresponding layout.
+     */
+    public ApplicationLayout appLayout() {
+        final ApplicationLayout layout;
+        if (isRuntime()) {
+            layout = ApplicationLayout.javaRuntime();
+        } else {
+            layout = ApplicationLayout.platformAppImage();
+        }
+
+        if (isImagePackageType()) {
+            return layout.resolveAt(outputBundle());
+        }
+
+        return layout.resolveAt(appInstallationDirectory());
+    }
+
+    /**
+     * Returns path to directory where application will be installed or null if
+     * this is build image command.
      *
      * E.g. on Linux for app named Foo default the function will return
      * `/opt/foo`
      */
     public Path appInstallationDirectory() {
-        final PackageType type = packageType();
-        if (PackageType.IMAGE == type) {
+        if (isImagePackageType()) {
             return null;
         }
 
-        if (PackageType.LINUX.contains(type)) {
+        if (TKit.isLinux()) {
             if (isRuntime()) {
                 // Not fancy, but OK.
                 return Path.of(getArgumentValue("--install-dir", () -> "/opt"),
@@ -300,139 +375,104 @@
             }
 
             // Launcher is in "bin" subfolder of the installation directory.
-            return launcherInstallationPath().getParent().getParent();
+            return appLauncherPath().getParent().getParent();
         }
 
-        if (PackageType.WINDOWS.contains(type)) {
+        if (TKit.isWindows()) {
             return WindowsHelper.getInstallationDirectory(this);
         }
 
-        if (PackageType.MAC.contains(type)) {
+        if (TKit.isOSX()) {
             return MacHelper.getInstallationDirectory(this);
         }
 
-        throw throwUnexpectedPackageTypeError();
-    }
-
-    /**
-     * Returns path where application's Java runtime will be installed.
-     * If the command will package Java run-time only, still returns path to
-     * runtime subdirectory.
-     *
-     * E.g. on Linux for app named `Foo` the function will return
-     * `/opt/foo/runtime`
-     */
-    public Path appRuntimeInstallationDirectory() {
-        if (PackageType.IMAGE == packageType()) {
-            return null;
-        }
-        return appInstallationDirectory().resolve(appRuntimePath(packageType()));
-    }
-
-    /**
-     * Returns path where application launcher will be installed.
-     * If the command will package Java run-time only, still returns path to
-     * application launcher.
-     *
-     * E.g. on Linux for app named Foo default the function will return
-     * `/opt/foo/bin/Foo`
-     */
-    public Path launcherInstallationPath() {
-        final PackageType type = packageType();
-        if (PackageType.IMAGE == type) {
-            return null;
-        }
-
-        if (PackageType.LINUX.contains(type)) {
-            return outputDir().resolve(LinuxHelper.getLauncherPath(this));
-        }
-
-        if (PackageType.WINDOWS.contains(type)) {
-            return appInstallationDirectory().resolve(name() + ".exe");
-        }
-
-        if (PackageType.MAC.contains(type)) {
-            return appInstallationDirectory().resolve(Path.of("Contents", "MacOS", name()));
-        }
-
-        throw throwUnexpectedPackageTypeError();
-    }
-
-    /**
-     * Returns path to application image directory.
-     *
-     * E.g. if --dest is set to `foo` and --name is set to `bar` the function
-     * will return `foo/bar` path on Linux and Windows and `foo/bar.app` on macOS.
-     *
-     * @throws IllegalArgumentException is command is doing platform packaging
-     */
-    public Path appImage() {
-        verifyIsOfType(PackageType.IMAGE);
-        String dirName = name();
-        if (TKit.isOSX()) {
-            dirName = dirName + ".app";
-        }
-        return outputDir().resolve(dirName);
-    }
-
-    /**
-     * Returns path to application launcher relative to image directory.
-     *
-     * E.g. if --name is set to `Foo` the function will return `bin/Foo` path on
-     * Linux, and `Foo.exe` on Windows.
-     *
-     * @throws IllegalArgumentException is command is doing platform packaging
-     */
-    public Path launcherPathInAppImage() {
-        verifyIsOfType(PackageType.IMAGE);
-
-        if (TKit.isLinux()) {
-            return Path.of("bin", name());
-        }
-
-        if (TKit.isOSX()) {
-            return Path.of("Contents", "MacOS", name());
-        }
-
-        if (TKit.isWindows()) {
-            return Path.of(name() + ".exe");
-        }
-
         throw TKit.throwUnknownPlatformError();
     }
 
     /**
-     * Returns path to runtime directory relative to image directory.
+     * Returns path to application's Java runtime.
+     * If the command will package Java runtime only, returns correct path to
+     * runtime directory.
      *
-     * @throws IllegalArgumentException if command is configured for platform
-     * packaging
+     * E.g.:
+     * [jpackage --name Foo --package-type rpm] -> `/opt/foo/lib/runtime`
+     * [jpackage --name Foo --package-type app-image --dest bar] -> `bar/Foo/lib/runtime`
+     * [jpackage --name Foo --package-type rpm --runtime-image java] -> `/opt/foo`
      */
-    public Path appRuntimeDirectoryInAppImage() {
-        verifyIsOfType(PackageType.IMAGE);
-        return appRuntimePath(packageType());
+    public Path appRuntimeDirectory() {
+        return appLayout().runtimeDirectory();
     }
 
-    private static Path appRuntimePath(PackageType type) {
-        if (TKit.isLinux()) {
-            return Path.of("lib/runtime");
-        }
-        if (TKit.isOSX()) {
-            return Path.of("Contents/runtime");
+    /**
+     * Returns path for application launcher with the given name.
+     *
+     * E.g.: [jpackage --name Foo --package-type rpm] -> `/opt/foo/bin/Foo`
+     * [jpackage --name Foo --package-type app-image --dest bar] ->
+     * `bar/Foo/bin/Foo`
+     *
+     * @param launcherName name of launcher or {@code null} for the main
+     * launcher
+     *
+     * @throws IllegalArgumentException if the command is configured for
+     * packaging Java runtime
+     */
+    public Path appLauncherPath(String launcherName) {
+        verifyNotRuntime();
+        if (launcherName == null) {
+            launcherName = name();
         }
 
-        return Path.of("runtime");
+        if (TKit.isWindows()) {
+            launcherName = launcherName + ".exe";
+        }
+
+        if (isImagePackageType()) {
+            return appLayout().launchersDirectory().resolve(launcherName);
+        }
+
+        if (TKit.isLinux()) {
+            LinuxHelper.getLauncherPath(this).getParent().resolve(launcherName);
+        }
+
+        return appLayout().launchersDirectory().resolve(launcherName);
     }
 
-    public boolean isFakeRuntimeInAppImage(String msg) {
-        return isFakeRuntime(appImage().resolve(
-                appRuntimeDirectoryInAppImage()), msg);
+    /**
+     * Shorthand for {@code appLauncherPath(null)}.
+     */
+    public Path appLauncherPath() {
+        return appLauncherPath(null);
+    }
+
+    private void verifyNotRuntime() {
+        if (isRuntime()) {
+            throw new IllegalArgumentException("Java runtime packaging");
+        }
     }
 
-    public boolean isFakeRuntimeInstalled(String msg) {
-        return isFakeRuntime(appRuntimeInstallationDirectory(), msg);
+    /**
+     * Returns path to .cfg file of the given application launcher.
+     *
+     * E.g.:
+     * [jpackage --name Foo --package-type rpm] -> `/opt/foo/lib/app/Foo.cfg`
+     * [jpackage --name Foo --package-type app-image --dest bar] -> `bar/Foo/lib/app/Foo.cfg`
+     *
+     * @param launcher name of launcher or {@code null} for the main launcher
+     *
+     * @throws IllegalArgumentException if the command is configured for
+     * packaging Java runtime
+     */
+    public Path appLauncherCfgPath(String launcherName) {
+        verifyNotRuntime();
+        if (launcherName == null) {
+            launcherName = name();
+        }
+        return appLayout().appDirectory().resolve(launcherName + ".cfg");
     }
 
-    private static boolean isFakeRuntime(Path runtimeDir, String msg) {
+    public boolean isFakeRuntime(String msg) {
+        Path runtimeDir = appRuntimeDirectory();
+
         final Collection<Path> criticalRuntimeFiles;
         if (TKit.isWindows()) {
             criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES;
@@ -482,12 +522,18 @@
         return this;
     }
 
+    public JPackageCommand ignoreDefaultRuntime(boolean v) {
+        verifyMutable();
+        ignoreDefaultRuntime = v;
+        return this;
+    }
+
     public boolean isWithToolProvider() {
         return Optional.ofNullable(withToolProvider).orElse(
                 defaultWithToolProvider);
     }
 
-    public void executePrerequisiteActions() {
+    public JPackageCommand executePrerequisiteActions() {
         verifyMutable();
         if (!actionsExecuted) {
             actionsExecuted = true;
@@ -495,26 +541,35 @@
                 actions.stream().forEach(r -> r.accept(this));
             }
         }
+        return this;
+    }
+
+    public Executor createExecutor() {
+        verifyMutable();
+        Executor exec = new Executor()
+                .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
+                .addArguments(args);
+
+        if (isWithToolProvider()) {
+            exec.setToolProvider(JavaTool.JPACKAGE);
+        } else {
+            exec.setExecutable(JavaTool.JPACKAGE);
+        }
+
+        return exec;
     }
 
     public Executor.Result execute() {
         executePrerequisiteActions();
 
-        if (packageType() == PackageType.IMAGE) {
+        if (isImagePackageType()) {
             TKit.deleteDirectoryContentsRecursive(outputDir());
         }
 
-        Executor exec = new Executor()
-                .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
-                .addArguments(new JPackageCommand().addArguments(
-                                args).adjustArgumentsBeforeExecution().args);
-
-        if (isWithToolProvider()) {
-            exec.setToolProvider(JavaTool.JPACKAGE.asToolProvider());
-        } else {
-            exec.setExecutable(JavaTool.JPACKAGE);
-        }
-        return exec.execute();
+        return new JPackageCommand(this)
+                .adjustArgumentsBeforeExecution()
+                .createExecutor()
+                .execute();
     }
 
     public JPackageCommand executeAndAssertHelloAppImageCreated() {
@@ -530,15 +585,18 @@
 
     public JPackageCommand assertImageCreated() {
         verifyIsOfType(PackageType.IMAGE);
-        TKit.assertExecutableFileExists(appImage().resolve(
-                launcherPathInAppImage()));
-        TKit.assertDirectoryExists(appImage().resolve(
-                appRuntimeDirectoryInAppImage()));
+        TKit.assertDirectoryExists(appRuntimeDirectory());
+
+        if (!isRuntime()) {
+            TKit.assertExecutableFileExists(appLauncherPath());
+            TKit.assertFileExists(appLauncherCfgPath(null));
+        }
+
         return this;
     }
 
     private JPackageCommand adjustArgumentsBeforeExecution() {
-        if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null) {
+        if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) {
             addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
         }
 
@@ -549,10 +607,6 @@
         return this;
     }
 
-    private static RuntimeException throwUnexpectedPackageTypeError() {
-        throw new IllegalArgumentException("Unexpected package type");
-    }
-
     String getPrintableCommandLine() {
         return new Executor()
                 .setExecutable(JavaTool.JPACKAGE)
@@ -565,9 +619,41 @@
     }
 
     public void verifyIsOfType(PackageType ... types) {
-        if (!Arrays.asList(types).contains(packageType())) {
-            throwUnexpectedPackageTypeError();
+        final Set<PackageType> typesSet = Set.of(types);
+        if (!hasArgument("--package-type")) {
+            if (!isImagePackageType()) {
+                if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) {
+                    return;
+                }
+
+                if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) {
+                    return;
+                }
+
+                if (TKit.isOSX() && typesSet.equals(PackageType.MAC)) {
+                    return;
+                }
+            } else if (typesSet.equals(Set.of(PackageType.IMAGE))) {
+                return;
+            }
         }
+
+        if (!typesSet.contains(packageType())) {
+            throw new IllegalArgumentException("Unexpected package type");
+        }
+    }
+
+    public CfgFile readLaunherCfgFile() {
+        return readLaunherCfgFile(null);
+    }
+
+    public CfgFile readLaunherCfgFile(String launcherName) {
+        verifyIsOfType(PackageType.IMAGE);
+        if (isRuntime()) {
+            return null;
+        }
+        return ThrowingFunction.toFunction(CfgFile::readFromFile).apply(
+                appLauncherCfgPath(launcherName));
     }
 
     public static String escapeAndJoin(String... args) {
@@ -616,6 +702,7 @@
     private Boolean withToolProvider;
     private boolean saveConsoleOutput;
     private boolean suppressOutput;
+    private boolean ignoreDefaultRuntime;
     private boolean immutable;
     private boolean actionsExecuted;
     private final List<Consumer<JPackageCommand>> actions;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Wed Oct 16 10:32:08 2019 -0400
@@ -62,15 +62,16 @@
 
     public void create() {
         TKit.withTempDirectory("jar-workdir", workDir -> {
-            Executor.Result javacReply = new Executor()
-                    .setExecutable(JavaTool.JAVAC)
-                    .addArguments("-d", workDir.toString())
-                    .addPathArguments(sourceFiles).execute();
-            javacReply.assertExitCodeIsZero();
+            if (!sourceFiles.isEmpty()) {
+                new Executor()
+                        .setToolProvider(JavaTool.JAVAC)
+                        .addArguments("-d", workDir.toString())
+                        .addPathArguments(sourceFiles)
+                        .execute().assertExitCodeIsZero();
+            }
             Path tmpJar = workDir.resolve("foo.jar");
             Executor jarExe = new Executor();
-            jarExe.setExecutable(JavaTool.JAR).addArguments("-c", "-v", "-f",
-                    tmpJar.toString());
+            jarExe.setToolProvider(JavaTool.JAR).addArguments("-c", "-f", tmpJar.toString());
             if (moduleVersion != null) {
                 jarExe.addArguments(String.format("--module-version=%s",
                         moduleVersion));
@@ -79,8 +80,7 @@
                 jarExe.addArguments("-e", mainClass);
             }
             jarExe.addArguments("-C", workDir.toString(), ".");
-            javacReply = jarExe.execute();
-            javacReply.assertExitCodeIsZero();
+            jarExe.execute().assertExitCodeIsZero();
             outputJar.getParentFile().mkdirs();
             Files.copy(tmpJar, outputJar.toPath(), REPLACE_EXISTING);
         });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.test;
+
+import java.util.Objects;
+
+
+public final class JavaAppDesc {
+    public JavaAppDesc() {
+    }
+
+    public JavaAppDesc setClassName(String v) {
+        qualifiedClassName = v;
+        return this;
+    }
+
+    public JavaAppDesc setModuleName(String v) {
+        moduleName = v;
+        return this;
+    }
+
+    public JavaAppDesc setJarFileName(String v) {
+        jarFileName = v;
+        return this;
+    }
+
+    public JavaAppDesc setModuleVersion(String v) {
+        moduleVersion = v;
+        return this;
+    }
+
+    public JavaAppDesc setJarWithMainClass(boolean v) {
+        jarWithMainClass = v;
+        return this;
+    }
+
+    public String className() {
+        return qualifiedClassName;
+    }
+
+    public String moduleName() {
+        return moduleName;
+    }
+
+    public String packageName() {
+        int lastDotIdx = qualifiedClassName.lastIndexOf('.');
+        if (lastDotIdx == -1) {
+            return null;
+        }
+        return qualifiedClassName.substring(0, lastDotIdx);
+    }
+
+    public String jarFileName() {
+        return jarFileName;
+    }
+
+    public String moduleVersion() {
+        return moduleVersion;
+    }
+
+    public boolean jarWithMainClass() {
+        return jarWithMainClass;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (jarFileName != null) {
+            sb.append(jarFileName).append(':');
+        }
+        if (moduleName != null) {
+            sb.append(moduleName).append('/');
+        }
+        if (qualifiedClassName != null) {
+            sb.append(qualifiedClassName);
+        }
+        if (jarWithMainClass) {
+            sb.append('!');
+        }
+        if (moduleVersion != null) {
+            sb.append('@').append(moduleVersion);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Create Java application description form encoded string value.
+     *
+     * Syntax of encoded Java application description is
+     * [jar_file:][module_name/]qualified_class_name[!][@module_version].
+     *
+     * E.g.: `duke.jar:com.other/com.other.foo.bar.Buz!@3.7` encodes modular
+     * application. Module name is `com.other`. Main class is
+     * `com.other.foo.bar.Buz`. Module version is `3.7`. Application will be
+     * compiled and packed in `duke.jar` jar file. jar command will set module
+     * version (3.7) and main class (Buz) attributes in the jar file.
+     *
+     * E.g.: `Ciao` encodes non-modular `Ciao` class in the default package.
+     * jar command will not put main class attribute in the jar file.
+     * Default name will be picked for jar file - `hello.jar`.
+     *
+     * @param cmd jpackage command to configure
+     * @param javaAppDesc encoded Java application description
+     */
+    public static JavaAppDesc parse(String javaAppDesc) {
+        JavaAppDesc desc = HelloApp.createDefaltAppDesc();
+
+        if (javaAppDesc == null) {
+            return desc;
+        }
+
+        String moduleNameAndOther = Functional.identity(() -> {
+            String[] components = javaAppDesc.split(":", 2);
+            if (components.length == 2) {
+                desc.setJarFileName(components[0]);
+            }
+            return components[components.length - 1];
+        }).get();
+
+        String classNameAndOther = Functional.identity(() -> {
+            String[] components = moduleNameAndOther.split("/", 2);
+            if (components.length == 2) {
+                desc.setModuleName(components[0]);
+            }
+            return components[components.length - 1];
+        }).get();
+
+        Functional.identity(() -> {
+            String[] components = classNameAndOther.split("@", 2);
+            if (components[0].endsWith("!")) {
+                components[0] = components[0].substring(0,
+                        components[0].length() - 1);
+                desc.setJarWithMainClass(true);
+            }
+            desc.setClassName(components[0]);
+            if (components.length == 2) {
+                desc.setModuleVersion(components[1]);
+            }
+        }).run();
+
+        return desc;
+    }
+
+    private String qualifiedClassName;
+    private String moduleName;
+    private String jarFileName;
+    private String moduleVersion;
+    private boolean jarWithMainClass;
+}
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Wed Oct 16 10:32:08 2019 -0400
@@ -200,7 +200,7 @@
 
         final boolean checkPrerequisites;
         if (cmd.isRuntime()) {
-            Path runtimeDir = cmd.appRuntimeInstallationDirectory();
+            Path runtimeDir = cmd.appRuntimeDirectory();
             Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map(
                     runtimeDir::resolve).collect(Collectors.toSet());
             Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter(
@@ -339,7 +339,7 @@
 
                 TKit.trace(String.format("Done"));
 
-                TKit.assertEquals(cmd.launcherInstallationPath().toString(),
+                TKit.assertEquals(cmd.appLauncherPath().toString(),
                         mimeHandler, String.format(
                                 "Check mime type handler is the main application launcher"));
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java	Wed Oct 16 10:32:08 2019 -0400
@@ -22,11 +22,70 @@
  */
 package jdk.jpackage.test;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+import jdk.jpackage.test.Functional.ThrowingConsumer;
+import jdk.jpackage.test.Functional.ThrowingSupplier;
+import org.xml.sax.SAXException;
 
 public class MacHelper {
 
+    public static void withExplodedDmg(JPackageCommand cmd,
+            ThrowingConsumer<Path> consumer) {
+        cmd.verifyIsOfType(PackageType.MAC_DMG);
+
+        var plist = readPList(new Executor()
+                .setExecutable("/usr/bin/hdiutil")
+                .dumpOutput()
+                .addArguments("attach", cmd.outputBundle().toString(), "-plist")
+                .executeAndGetOutput());
+
+        final Path mountPoint = Path.of(plist.queryValue("mount-point"));
+        try {
+            Path dmgImage = mountPoint.resolve(cmd.name() + ".app");
+            TKit.trace(String.format("Exploded [%s] in [%s] directory",
+                    cmd.outputBundle(), dmgImage));
+            ThrowingConsumer.toConsumer(consumer).accept(dmgImage);
+        } finally {
+            new Executor()
+                    .setExecutable("/usr/bin/hdiutil")
+                    .addArgument("detach").addArgument(mountPoint)
+                    .execute().assertExitCodeIsZero();
+        }
+    }
+
+    public static PListWrapper readPListFromAppImage(Path appImage) {
+        return readPList(appImage.resolve("Contents/Info.plist"));
+    }
+
+    public static PListWrapper readPList(Path path) {
+        TKit.assertReadableFileExists(path);
+        return ThrowingSupplier.toSupplier(() -> readPList(Files.readAllLines(
+                path))).get();
+    }
+
+    public static PListWrapper readPList(List<String> lines) {
+        return readPList(lines.stream());
+    }
+
+    public static PListWrapper readPList(Stream<String> lines) {
+        return ThrowingSupplier.toSupplier(() -> new PListWrapper(lines.collect(
+                Collectors.joining()))).get();
+    }
+
     static String getBundleName(JPackageCommand cmd) {
         cmd.verifyIsOfType(PackageType.MAC);
         return String.format("%s-%s%s", getPackageName(cmd), cmd.version(),
@@ -45,6 +104,35 @@
                 () -> cmd.name());
     }
 
+    public static final class PListWrapper {
+        public String queryValue(String keyName) {
+            XPath xPath = XPathFactory.newInstance().newXPath();
+            // Query for the value of <string> element preceding <key> element
+            // with value equal to `keyName`
+            String query = String.format(
+                    "//string[preceding-sibling::key = \"%s\"][1]", keyName);
+            return ThrowingSupplier.toSupplier(() -> (String) xPath.evaluate(
+                    query, doc, XPathConstants.STRING)).get();
+        }
+
+        PListWrapper(String xml) throws ParserConfigurationException,
+                SAXException, IOException {
+            doc = createDocumentBuilder().parse(new ByteArrayInputStream(
+                    xml.getBytes(StandardCharsets.UTF_8)));
+        }
+
+        private static DocumentBuilder createDocumentBuilder() throws
+                ParserConfigurationException {
+            DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance();
+            dbf.setFeature(
+                    "http://apache.org/xml/features/nonvalidating/load-external-dtd",
+                    false);
+            return dbf.newDocumentBuilder();
+        }
+
+        private final org.w3c.dom.Document doc;
+    }
+
     static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
             "Contents/Home/lib/server/libjvm.dylib"));
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java	Wed Oct 16 10:32:08 2019 -0400
@@ -25,6 +25,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import static jdk.jpackage.test.TestBuilder.CMDLINE_ARG_PREFIX;
@@ -36,9 +37,7 @@
         List<TestInstance> tests = new ArrayList<>();
         try (TestBuilder testBuilder = new TestBuilder(tests::add)) {
             for (var arg : args) {
-                if (TKit.VERBOSE_TEST_SETUP) {
-                    TKit.log(String.format("Parsing [%s]...", arg));
-                }
+                TestBuilder.trace(String.format("Parsing [%s]...", arg));
 
                 if ((CMDLINE_ARG_PREFIX + "list").equals(arg)) {
                     listTests = true;
@@ -62,15 +61,21 @@
         }
 
         // Order tests by their full names to have stable test sequence.
-        tests = tests.stream().sorted((a, b) -> a.fullName().compareTo(
-                b.fullName())).collect(Collectors.toList());
+        List<TestInstance> orderedTests = tests.stream()
+                .sorted((a, b) -> a.fullName().compareTo(b.fullName()))
+                .collect(Collectors.toList());
 
         if (listTests) {
             // Just list the tests
-            tests.stream().forEach(test -> System.out.println(test.fullName()));
+            orderedTests.stream().forEach(test -> System.out.println(String.format(
+                    "%s; workDir=[%s]", test.fullName(), test.workDir())));
             return;
         }
 
+        TKit.withExtraLogStream(() -> runTests(orderedTests));
+    }
+
+    private static void runTests(List<TestInstance> tests) {
         TKit.runTests(tests);
 
         final long passedCount = tests.stream().filter(TestInstance::passed).count();
@@ -78,8 +83,8 @@
         TKit.log(String.format("[  PASSED  ] %d %s", passedCount,
                 passedCount == 1 ? "test" : "tests"));
 
-        reportDetails(tests, "[  SKIPPED ]", TestInstance::skipped);
-        reportDetails(tests, "[  FAILED  ]", TestInstance::failed);
+        reportDetails(tests, "[  SKIPPED ]", TestInstance::skipped, false);
+        reportDetails(tests, "[  FAILED  ]", TestInstance::failed, true);
 
         var withSkipped = reportSummary(tests, "SKIPPED", TestInstance::skipped);
         var withFailures = reportSummary(tests, "FAILED", TestInstance::failed);
@@ -94,13 +99,22 @@
     }
 
     private static long reportDetails(List<TestInstance> tests,
-            String label, Predicate<TestInstance> selector) {
+            String label, Predicate<TestInstance> selector, boolean printWorkDir) {
+
+        final Function<TestInstance, String> makeMessage = test -> {
+            if (printWorkDir) {
+                return String.format("%s %s; workDir=[%s]", label,
+                        test.fullName(), test.workDir());
+            }
+            return String.format("%s %s", label, test.fullName());
+        };
+
         final long count = tests.stream().filter(selector).count();
         if (count != 0) {
             TKit.log(String.format("%s %d %s, listed below", label, count, count
                     == 1 ? "test" : "tests"));
-            tests.stream().filter(selector).forEach(test -> TKit.log(
-                    String.format("%s %s", label, test.fullName())));
+            tests.stream().filter(selector).map(makeMessage).forEachOrdered(
+                    TKit::log);
         }
 
         return count;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java	Wed Oct 16 10:32:08 2019 -0400
@@ -23,40 +23,131 @@
 package jdk.jpackage.test;
 
 import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
-import jdk.jpackage.test.Functional.ThrowingSupplier;
 import jdk.jpackage.test.TestInstance.TestDesc;
 
 class MethodCall implements ThrowingConsumer {
 
-    MethodCall(Method method, Object... args) {
+    MethodCall(Object[] instanceCtorArgs, Method method) {
+        this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse(
+                DEFAULT_CTOR_ARGS);
         this.method = method;
-        this.args = args;
+        this.methodArgs = new Object[0];
+    }
+
+    MethodCall(Object[] instanceCtorArgs, Method method, Object arg) {
+        this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse(
+                DEFAULT_CTOR_ARGS);
+        this.method = method;
+        this.methodArgs = new Object[]{arg};
     }
 
     TestDesc createDescription() {
-        return TestDesc.create(method, args);
+        var descBuilder = TestDesc.createBuilder().method(method);
+        if (methodArgs.length != 0) {
+            descBuilder.methodArgs(methodArgs);
+        }
+
+        if (ctorArgs.length != 0) {
+            descBuilder.ctorArgs(ctorArgs);
+        }
+
+        return descBuilder.get();
+    }
+
+    Method getMethod() {
+        return method;
     }
 
-    Constructor getRequiredConstructor() throws NoSuchMethodException {
-        return MethodCall.getRequiredConstructor(method);
+    Object newInstance() throws NoSuchMethodException, InstantiationException,
+            IllegalAccessException, IllegalArgumentException,
+            InvocationTargetException {
+        if ((method.getModifiers() & Modifier.STATIC) != 0) {
+            return null;
+        }
+
+        Constructor ctor = findRequiredConstructor(method.getDeclaringClass(),
+                ctorArgs);
+        if (ctor.isVarArgs()) {
+            // Assume constructor doesn't have fixed, only variable parameters.
+            return ctor.newInstance(new Object[]{ctorArgs});
+        }
+
+        return ctor.newInstance(ctorArgs);
+    }
+
+    void checkRequiredConstructor() throws NoSuchMethodException {
+        if ((method.getModifiers() & Modifier.STATIC) == 0) {
+            findRequiredConstructor(method.getDeclaringClass(), ctorArgs);
+        }
     }
 
-    static Constructor getRequiredConstructor(Method method) throws
-            NoSuchMethodException {
-        if ((method.getModifiers() & Modifier.STATIC) == 0) {
-            return method.getDeclaringClass().getConstructor();
+    private static Constructor findVarArgConstructor(Class type) {
+        return Stream.of(type.getConstructors()).filter(
+                Constructor::isVarArgs).findFirst().orElse(null);
+    }
+
+    private Constructor findRequiredConstructor(Class type, Object... ctorArgs)
+            throws NoSuchMethodException {
+
+        Supplier<NoSuchMethodException> notFoundException = () -> {
+            return new NoSuchMethodException(String.format(
+                    "No public contructor in %s for %s arguments", type,
+                    Arrays.deepToString(ctorArgs)));
+        };
+
+        if (Stream.of(ctorArgs).allMatch(Objects::nonNull)) {
+            // No `null` in constructor args, take easy path
+            try {
+                return type.getConstructor(Stream.of(ctorArgs).map(
+                        Object::getClass).collect(Collectors.toList()).toArray(
+                        Class[]::new));
+            } catch (NoSuchMethodException ex) {
+                // Failed to find ctor that can take the given arguments.
+                Constructor varArgCtor = findVarArgConstructor(type);
+                if (varArgCtor != null) {
+                    // There is one with variable number of arguments. Use it.
+                    return varArgCtor;
+                }
+                throw notFoundException.get();
+            }
         }
-        return null;
+
+        List<Constructor> ctors = Stream.of(type.getConstructors())
+                .filter(ctor -> ctor.getParameterCount() == ctorArgs.length)
+                .collect(Collectors.toList());
+
+        if (ctors.isEmpty()) {
+            // No public constructors that can handle the given arguments.
+            throw notFoundException.get();
+        }
+
+        if (ctors.size() == 1) {
+            return ctors.iterator().next();
+        }
+
+        // Revisit this tricky case when it will start bothering.
+        throw notFoundException.get();
     }
 
     @Override
     public void accept(Object thiz) throws Throwable {
-        method.invoke(thiz, args);
+        method.invoke(thiz, methodArgs);
     }
 
-    private final Object[] args;
+    private final Object[] methodArgs;
     private final Method method;
+    private final Object[] ctorArgs;
+
+    final static Object[] DEFAULT_CTOR_ARGS = new Object[0];
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -162,10 +162,9 @@
     public PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
             String... faLauncherDefaultArgs) {
 
-        addInitializer(cmd -> HelloApp.addTo(cmd, null), "HelloApp");
+        addInitializer(cmd -> new HelloApp(null).addTo(cmd), "HelloApp");
         addInstallVerifier(cmd -> {
-            if (cmd.isFakeRuntimeInstalled(
-                    "Not running file associations test")) {
+            if (cmd.isFakeRuntime("Not running file associations test")) {
                 return;
             }
 
@@ -219,7 +218,8 @@
     }
 
     public PackageTest configureHelloApp(String encodedName) {
-        addInitializer(cmd -> HelloApp.addTo(cmd, encodedName));
+        addInitializer(
+                cmd -> new HelloApp(JavaAppDesc.parse(encodedName)).addTo(cmd));
         addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
         return this;
     }
@@ -297,6 +297,8 @@
             type.applyTo(cmd);
 
             initializers.stream().forEach(v -> v.accept(cmd));
+            cmd.executePrerequisiteActions();
+
             switch (action) {
                 case CREATE:
                     Executor.Result result = cmd.execute();
@@ -336,15 +338,13 @@
         private void verifyPackageInstalled(JPackageCommand cmd) {
             TKit.trace(String.format("Verify installed: %s",
                     cmd.getPrintableCommandLine()));
-            if (cmd.isRuntime()) {
-                TKit.assertPathExists(
-                        cmd.appRuntimeInstallationDirectory(), false);
-            } else {
-                TKit.assertExecutableFileExists(cmd.launcherInstallationPath());
-            }
+            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())) {
+                    new WindowsHelper.AppVerifier(cmd);
+                }
             }
 
             installVerifiers.stream().forEach(v -> v.accept(cmd));
@@ -354,13 +354,14 @@
             TKit.trace(String.format("Verify uninstalled: %s",
                     cmd.getPrintableCommandLine()));
             if (!cmd.isRuntime()) {
-                TKit.assertPathExists(cmd.launcherInstallationPath(), false);
-                TKit.assertPathExists(cmd.appInstallationDirectory(), false);
+                TKit.assertPathExists(cmd.appLauncherPath(), false);
+
+                if (PackageType.WINDOWS.contains(cmd.packageType())) {
+                    new WindowsHelper.AppVerifier(cmd);
+                }
             }
 
-            if (PackageType.WINDOWS.contains(cmd.packageType())) {
-                new WindowsHelper.AppVerifier(cmd);
-            }
+            TKit.assertPathExists(cmd.appInstallationDirectory(), false);
 
             uninstallVerifiers.stream().forEach(v -> v.accept(cmd));
         }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.Collections;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -117,9 +118,8 @@
 
     private final static class Inner {
 
-        private final static Set<String> DISABLED_PACKAGERS = Stream.of(
-                Optional.ofNullable(
-                        TKit.getConfigProperty("disabledPackagers")).orElse(
-                        "").split(",")).collect(Collectors.toUnmodifiableSet());
+        private final static Set<String> DISABLED_PACKAGERS = Optional.ofNullable(
+                TKit.tokenizeConfigProperty("disabledPackagers")).orElse(
+                Collections.emptySet());
     }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Wed Oct 16 10:32:08 2019 -0400
@@ -22,28 +22,24 @@
  */
 package jdk.jpackage.test;
 
-import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.lang.reflect.InvocationTargetException;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.*;
 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import jdk.jpackage.test.Functional.ExceptionBox;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
-import jdk.jpackage.test.Functional.ThrowingFunction;
 import jdk.jpackage.test.Functional.ThrowingRunnable;
 import jdk.jpackage.test.Functional.ThrowingSupplier;
 
@@ -76,14 +72,26 @@
         }
     }
 
+    static void withExtraLogStream(ThrowingRunnable action) {
+        if (extraLogStream != null) {
+            ThrowingRunnable.toRunnable(action).run();
+        } else {
+            try (PrintStream logStream = openLogStream()) {
+                extraLogStream = logStream;
+                ThrowingRunnable.toRunnable(action).run();
+            } finally {
+                extraLogStream = null;
+            }
+        }
+    }
+
     static void runTests(List<TestInstance> tests) {
         if (currentTest != null) {
             throw new IllegalStateException(
                     "Unexpeced nested or concurrent Test.run() call");
         }
 
-        try (PrintStream logStream = openLogStream()) {
-            extraLogStream = logStream;
+        withExtraLogStream(() -> {
             tests.stream().forEach(test -> {
                 currentTest = test;
                 try {
@@ -95,9 +103,7 @@
                     }
                 }
             });
-        } finally {
-            extraLogStream = null;
-        }
+        });
     }
 
     static Runnable ignoreExceptions(ThrowingRunnable action) {
@@ -109,10 +115,7 @@
                     unbox(ex);
                 }
             } catch (Throwable throwable) {
-                if (extraLogStream != null) {
-                    throwable.printStackTrace(extraLogStream);
-                }
-                throwable.printStackTrace();
+                printStackTrace(throwable);
             }
         };
     }
@@ -126,12 +129,7 @@
     }
 
     public static Path workDir() {
-        Path result = Path.of(".");
-        String testFunctionName = currentTest.functionName();
-        if (testFunctionName != null) {
-            result = result.resolve(testFunctionName);
-        }
-        return result;
+        return currentTest.workDir();
     }
 
     static Path defaultInputDir() {
@@ -174,10 +172,14 @@
     }
 
     public static void createTextFile(Path propsFilename, Collection<String> lines) {
+        createTextFile(propsFilename, lines.stream());
+    }
+
+    public static void createTextFile(Path propsFilename, Stream<String> lines) {
         trace(String.format("Create [%s] text file...",
                 propsFilename.toAbsolutePath().normalize()));
         ThrowingRunnable.toRunnable(() -> Files.write(propsFilename,
-                lines.stream().peek(TKit::trace).collect(Collectors.toList()))).run();
+                lines.peek(TKit::trace).collect(Collectors.toList()))).run();
         trace("Done");
     }
 
@@ -263,7 +265,7 @@
         return Files.createFile(createUniqueFileName(role));
     }
 
-    public static void withTempFile(String role, String suffix,
+    public static Path withTempFile(String role, String suffix,
             ThrowingConsumer<Path> action) {
         final Path tempFile = ThrowingSupplier.toSupplier(() -> createTempFile(
                 role, suffix)).get();
@@ -271,6 +273,7 @@
         try {
             ThrowingConsumer.toConsumer(action).accept(tempFile);
             keepIt = false;
+            return tempFile;
         } finally {
             if (tempFile != null && !keepIt) {
                 ThrowingRunnable.toRunnable(() -> Files.deleteIfExists(tempFile)).run();
@@ -278,7 +281,7 @@
         }
     }
 
-    public static void withTempDirectory(String role,
+    public static Path withTempDirectory(String role,
             ThrowingConsumer<Path> action) {
         final Path tempDir = ThrowingSupplier.toSupplier(
                 () -> createTempDirectory(role)).get();
@@ -286,6 +289,7 @@
         try {
             ThrowingConsumer.toConsumer(action).accept(tempDir);
             keepIt = false;
+            return tempDir;
         } finally {
             if (tempDir != null && tempDir.toFile().isDirectory() && !keepIt) {
                 deleteDirectoryRecursive(tempDir, "");
@@ -293,6 +297,72 @@
         }
     }
 
+    private static class DirectoryCleaner implements Consumer<Path> {
+        DirectoryCleaner traceMessage(String v) {
+            msg = v;
+            return this;
+        }
+
+        DirectoryCleaner contentsOnly(boolean v) {
+            contentsOnly = v;
+            return this;
+        }
+
+        @Override
+        public void accept(Path root) {
+            if (msg == null) {
+                if (contentsOnly) {
+                    msg = String.format("Cleaning [%s] directory recursively",
+                            root);
+                } else {
+                    msg = String.format("Deleting [%s] directory recursively",
+                            root);
+                }
+            }
+
+            if (!msg.isEmpty()) {
+                trace(msg);
+            }
+
+            List<Throwable> errors = new ArrayList<>();
+            try {
+                final List<Path> paths;
+                if (contentsOnly) {
+                    try (var pathStream = Files.walk(root, 0)) {
+                        paths = pathStream.collect(Collectors.toList());
+                    }
+                } else {
+                    paths = List.of(root);
+                }
+
+                for (var path : paths) {
+                    try (var pathStream = Files.walk(path)) {
+                        pathStream
+                        .sorted(Comparator.reverseOrder())
+                        .sequential()
+                        .forEachOrdered(file -> {
+                            try {
+                                if (isWindows()) {
+                                    Files.setAttribute(file, "dos:readonly", false);
+                                }
+                                Files.delete(file);
+                            } catch (IOException ex) {
+                                errors.add(ex);
+                            }
+                        });
+                    }
+                }
+
+            } catch (IOException ex) {
+                errors.add(ex);
+            }
+            errors.forEach(error -> trace(error.toString()));
+        }
+
+        private String msg;
+        private boolean contentsOnly;
+    }
+
     /**
      * Deletes contents of the given directory recursively. Shortcut for
      * <code>deleteDirectoryContentsRecursive(path, null)</code>
@@ -313,21 +383,8 @@
      */
     public static void deleteDirectoryContentsRecursive(Path path, String msg) {
         if (path.toFile().isDirectory()) {
-            if (msg == null) {
-                msg = String.format("Cleaning [%s] directory recursively", path);
-            }
-
-            if (!msg.isEmpty()) {
-                TKit.trace(msg);
-            }
-
-            // Walk all children of `path` in sorted order to hit files first
-            // and directories last and delete each item.
-            ThrowingRunnable.toRunnable(() -> Stream.of(
-                    path.toFile().listFiles()).map(File::toPath).map(
-                    ThrowingFunction.toFunction(Files::walk)).flatMap(x -> x).sorted(
-                    Comparator.reverseOrder()).map(Path::toFile).forEach(
-                    File::delete)).run();
+            new DirectoryCleaner().contentsOnly(true).traceMessage(msg).accept(
+                    path);
         }
     }
 
@@ -351,11 +408,7 @@
      */
     public static void deleteDirectoryRecursive(Path path, String msg) {
         if (path.toFile().isDirectory()) {
-            if (msg == null) {
-                msg = String.format("Deleting [%s] directory recursively", path);
-            }
-            deleteDirectoryContentsRecursive(path, msg);
-            ThrowingConsumer.toConsumer(Files::delete).accept(path);
+            new DirectoryCleaner().traceMessage(msg).accept(path);
         }
     }
 
@@ -377,6 +430,24 @@
         throw ex;
     }
 
+    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();
+
+        final Path basePath = Path.of(".").toAbsolutePath().normalize();
+        try {
+            return basePath.relativize(fileCopy);
+        } catch (IllegalArgumentException ex) {
+            // May happen on Windows: java.lang.IllegalArgumentException: 'other' has different root
+            trace(String.format("Failed to relativize [%s] at [%s]", fileCopy,
+                    basePath));
+            printStackTrace(ex);
+        }
+        return file;
+    }
+
     static void waitForFileCreated(Path fileToWaitFor,
             long timeoutSeconds) throws IOException {
 
@@ -423,6 +494,13 @@
         }
     }
 
+    static void printStackTrace(Throwable throwable) {
+        if (extraLogStream != null) {
+            throwable.printStackTrace(extraLogStream);
+        }
+        throwable.printStackTrace();
+    }
+
     private static String concatMessages(String msg, String msg2) {
         if (msg2 != null && !msg2.isBlank()) {
             return msg + ": " + msg2;
@@ -602,6 +680,76 @@
         }
     }
 
+    public final static class TextStreamAsserter {
+        TextStreamAsserter(String value) {
+            this.value = value;
+            predicate(String::contains);
+        }
+
+        public TextStreamAsserter label(String v) {
+            label = v;
+            return this;
+        }
+
+        public TextStreamAsserter predicate(BiPredicate<String, String> v) {
+            predicate = v;
+            return this;
+        }
+
+        public TextStreamAsserter negate() {
+            negate = true;
+            return this;
+        }
+
+        public TextStreamAsserter orElseThrow(RuntimeException v) {
+            return orElseThrow(() -> v);
+        }
+
+        public TextStreamAsserter orElseThrow(Supplier<RuntimeException> v) {
+            createException = v;
+            return this;
+        }
+
+        public void apply(Stream<String> lines) {
+            String matchedStr = lines.filter(line -> predicate.test(line, value)).findFirst().orElse(
+                    null);
+            String labelStr = Optional.ofNullable(label).orElse("output");
+            if (negate) {
+                String msg = String.format(
+                        "Check %s doesn't contain [%s] string", labelStr, value);
+                if (createException == null) {
+                    assertNull(matchedStr, msg);
+                } else {
+                    trace(msg);
+                    if (matchedStr != null) {
+                        throw createException.get();
+                    }
+                }
+            } else {
+                String msg = String.format("Check %s contains [%s] string",
+                        labelStr, value);
+                if (createException == null) {
+                    assertNotNull(matchedStr, msg);
+                } else {
+                    trace(msg);
+                    if (matchedStr == null) {
+                        throw createException.get();
+                    }
+                }
+            }
+        }
+
+        private BiPredicate<String, String> predicate;
+        private String label;
+        private boolean negate;
+        private Supplier<RuntimeException> createException;
+        final private String value;
+    }
+
+    public static TextStreamAsserter assertTextStream(String what) {
+        return new TextStreamAsserter(what);
+    }
+
     private static PrintStream openLogStream() {
         if (LOG_FILE == null) {
             return null;
@@ -628,6 +776,15 @@
         return "jpackage.test." + propertyName;
     }
 
+    static Set<String> tokenizeConfigProperty(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());
+    }
+
     static final Path LOG_FILE = Functional.identity(() -> {
         String val = getConfigProperty("logfile");
         if (val == null) {
@@ -637,25 +794,26 @@
     }).get();
 
     static {
-        String val = getConfigProperty("suppress-logging");
-        if (val == null) {
+        Set<String> logOptions = tokenizeConfigProperty("suppress-logging");
+        if (logOptions == null) {
             TRACE = true;
             TRACE_ASSERTS = true;
             VERBOSE_JPACKAGE = true;
             VERBOSE_TEST_SETUP = true;
-        } else if ("all".equals(val.toLowerCase())) {
+        } else if (logOptions.contains("all")) {
             TRACE = false;
             TRACE_ASSERTS = false;
             VERBOSE_JPACKAGE = false;
             VERBOSE_TEST_SETUP = false;
         } else {
-            Set<String> logOptions = Set.of(val.toLowerCase().split(","));
-            TRACE = !(logOptions.contains("trace") || logOptions.contains("t"));
-            TRACE_ASSERTS = !(logOptions.contains("assert") || logOptions.contains(
-                    "a"));
-            VERBOSE_JPACKAGE = !(logOptions.contains("jpackage") || logOptions.contains(
-                    "jp"));
-            VERBOSE_TEST_SETUP = !logOptions.contains("init");
+            Predicate<Set<String>> isNonOf = options -> {
+                return Collections.disjoint(logOptions, options);
+            };
+
+            TRACE = isNonOf.test(Set.of("trace", "t"));
+            TRACE_ASSERTS = isNonOf.test(Set.of("assert", "a"));
+            VERBOSE_JPACKAGE = isNonOf.test(Set.of("jpackage", "jp"));
+            VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i"));
         }
     }
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Wed Oct 16 10:32:08 2019 -0400
@@ -23,43 +23,66 @@
 
 package jdk.jpackage.test;
 
-import java.lang.reflect.Constructor;
+import java.lang.reflect.Array;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jdk.jpackage.test.Annotations.AfterEach;
+import jdk.jpackage.test.Annotations.BeforeEach;
 import jdk.jpackage.test.Annotations.Parameter;
+import jdk.jpackage.test.Annotations.ParameterGroup;
 import jdk.jpackage.test.Annotations.Parameters;
 import jdk.jpackage.test.Annotations.Test;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
 import jdk.jpackage.test.Functional.ThrowingFunction;
-import jdk.jpackage.test.Functional.ThrowingSupplier;
 
 final class TestBuilder implements AutoCloseable {
 
     @Override
     public void close() throws Exception {
-        flushTestGroup(null);
+        flushTestGroup();
     }
 
     TestBuilder(Consumer<TestInstance> testConsumer) {
-        argProcessors = Map.of(CMDLINE_ARG_PREFIX + "after-run",
-                arg -> getJavaMethodsFromArg(arg).forEach(
-                        (method) -> afterActions.add(wrap(method, dryRun))),
+        argProcessors = Map.of(
+                CMDLINE_ARG_PREFIX + "after-run",
+                arg -> getJavaMethodsFromArg(arg).map(
+                        this::wrap).forEachOrdered(afterActions::add),
+
                 CMDLINE_ARG_PREFIX + "before-run",
-                arg -> getJavaMethodsFromArg(arg).forEach(
-                        (method) -> beforeActions.add(wrap(method, dryRun))),
+                arg -> getJavaMethodsFromArg(arg).map(
+                        this::wrap).forEachOrdered(beforeActions::add),
+
                 CMDLINE_ARG_PREFIX + "run",
-                arg -> flushTestGroup(getJavaMethodsFromArg(arg).map(
-                        TestBuilder::toMethodCalls).flatMap(List::stream).collect(
+                arg -> addTestGroup(getJavaMethodsFromArg(arg).map(
+                        ThrowingFunction.toFunction(
+                                TestBuilder::toMethodCalls)).flatMap(s -> s).collect(
                         Collectors.toList())),
+
+                CMDLINE_ARG_PREFIX + "exclude",
+                arg -> (excludedTests = Optional.ofNullable(
+                        excludedTests).orElse(new HashSet<String>())).add(arg),
+
+                CMDLINE_ARG_PREFIX + "include",
+                arg -> (includedTests = Optional.ofNullable(
+                        includedTests).orElse(new HashSet<String>())).add(arg),
+
+                CMDLINE_ARG_PREFIX + "space-subst",
+                arg -> spaceSubstitute = arg,
+
+                CMDLINE_ARG_PREFIX + "group",
+                arg -> flushTestGroup(),
+
                 CMDLINE_ARG_PREFIX + "dry-run",
-                arg -> dryRun = true);
+                arg -> dryRun = true
+        );
         this.testConsumer = testConsumer;
         clear();
     }
@@ -87,50 +110,93 @@
         }
     }
 
-    private void flushTestGroup(List<MethodCall> newTestGroup) {
+    private void addTestGroup(List<MethodCall> newTestGroup) {
         if (testGroup != null) {
-            testGroup.forEach(testBody -> createTestInstance(testBody));
+            testGroup.addAll(newTestGroup);
+        } else {
+            testGroup = newTestGroup;
+        }
+    }
+
+    private static Stream<MethodCall> filterTests(Stream<MethodCall> tests,
+            Set<String> filters, UnaryOperator<Boolean> pred, String logMsg) {
+        if (filters == null) {
+            return tests;
+        }
+
+        // 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));
+            if (match) {
+                trace(String.format(logMsg + ": %s", testDescription));
+            }
+            return pred.apply(match);
+        }).collect(Collectors.toList()).stream();
+    }
+
+    private Stream<MethodCall> filterTestGroup() {
+        Objects.requireNonNull(testGroup);
+
+        UnaryOperator<Set<String>> restoreSpaces = filters -> {
+            if (spaceSubstitute == null || filters == null) {
+                return filters;
+            }
+            return filters.stream().map(
+                    filter -> filter.replace(spaceSubstitute, " ")).collect(
+                            Collectors.toSet());
+        };
+
+        if (includedTests != null) {
+            return filterTests(testGroup.stream(), restoreSpaces.apply(
+                    includedTests), x -> x, "Include");
+        }
+
+        return filterTests(testGroup.stream(),
+                restoreSpaces.apply(excludedTests), x -> !x, "Exclude");
+    }
+
+    private void flushTestGroup() {
+        if (testGroup != null) {
+            filterTestGroup().forEach(testBody -> createTestInstance(testBody));
             clear();
         }
-        testGroup = newTestGroup;
     }
 
     private void createTestInstance(MethodCall testBody) {
-        ThrowingFunction<MethodCall, Object> testContructor;
-        if (dryRun) {
-            testContructor = (unused) -> null;
-            testBody = DRY_RUN_TEST_BODY;
+        final List<ThrowingConsumer> curBeforeActions;
+        final List<ThrowingConsumer> curAfterActions;
+
+        Method testMethod = testBody.getMethod();
+        if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch(
+                type -> testMethod.isAnnotationPresent(type))) {
+            curBeforeActions = beforeActions;
+            curAfterActions = afterActions;
         } else {
-            testContructor = TestBuilder::constructTest;
+            curBeforeActions = new ArrayList<>(beforeActions);
+            curAfterActions = new ArrayList<>(afterActions);
+
+            selectFrameMethods(testMethod.getDeclaringClass(), BeforeEach.class).map(
+                    this::wrap).forEachOrdered(curBeforeActions::add);
+            selectFrameMethods(testMethod.getDeclaringClass(), AfterEach.class).map(
+                    this::wrap).forEachOrdered(curAfterActions::add);
         }
 
-        TestInstance test = new TestInstance(testContructor, testBody,
-                beforeActions, afterActions);
-        trace(String.format("[%s] test constructed", test.fullName()));
+        TestInstance test = new TestInstance(testBody, curBeforeActions,
+                curAfterActions, dryRun);
+        if (includedTests == null) {
+            trace(String.format("Create: %s", test.fullName()));
+        }
         testConsumer.accept(test);
     }
 
-    public static void nop () {
-    }
-
-    private final static MethodCall DRY_RUN_TEST_BODY = ThrowingSupplier.toSupplier(() -> {
-        return new MethodCall(TestBuilder.class.getMethod("nop"));
-    }).get();
-
-    private static Object constructTest(MethodCall testBody) throws
-            NoSuchMethodException, InstantiationException,
-            IllegalAccessException, IllegalArgumentException,
-            InvocationTargetException {
-        Constructor ctor = testBody.getRequiredConstructor();
-        if (ctor == null) {
-            return null;
-        }
-        return ctor.newInstance();
-    }
-
     private void clear() {
         beforeActions = new ArrayList<>();
         afterActions = new ArrayList<>();
+        excludedTests = null;
+        includedTests = null;
+        spaceSubstitute = null;
         testGroup = null;
     }
 
@@ -142,6 +208,14 @@
         }
     }
 
+    private static Stream<Method> selectFrameMethods(Class type, Class annotationType) {
+        return Stream.of(type.getMethods())
+                .filter(m -> m.getParameterCount() == 0)
+                .filter(m -> !m.isAnnotationPresent(Test.class))
+                .filter(m -> m.isAnnotationPresent(annotationType))
+                .sorted((a, b) -> a.getName().compareTo(b.getName()));
+    }
+
     private static Stream<String> cmdLineArgValueToMethodNames(String v) {
         List<String> result = new ArrayList<>();
         String defaultClassName = null;
@@ -156,6 +230,7 @@
                         m -> m.isAnnotationPresent(Test.class)).map(
                                 Method::getName).distinct().forEach(
                                 name -> result.add(String.join(".", token, name)));
+
                 continue;
             }
 
@@ -188,7 +263,7 @@
     }
 
     private static boolean isParametrized(Method method) {
-        return method.isAnnotationPresent(Parameters.class) || method.isAnnotationPresent(
+        return method.isAnnotationPresent(ParameterGroup.class) || method.isAnnotationPresent(
                 Parameter.class);
     }
 
@@ -215,19 +290,6 @@
                     "Method [%s] not found in [%s] class;",
                     methodName, className));
         }
-        // Make sure default constructor is accessible if the one is needed.
-        // Need to probe all methods as some of them might be static and
-        // some class members.
-        // Onlu class members require default ctor.
-        for (Method method : methods) {
-            try {
-                MethodCall.getRequiredConstructor(method);
-            } catch (NoSuchMethodException ex) {
-                throw new ParseException(String.format(
-                        "Default constructor not found in [%s] class;",
-                        className));
-            }
-        }
 
         trace(String.format("%s -> %s", qualifiedMethodName, methods));
         return methods;
@@ -240,9 +302,9 @@
                         List::stream).sequential();
     }
 
-    private static Parameter[] getParameters(Method method) {
-        if (method.isAnnotationPresent(Parameters.class)) {
-            return ((Parameters) method.getAnnotation(Parameters.class)).value();
+    private static Parameter[] getMethodParameters(Method method) {
+        if (method.isAnnotationPresent(ParameterGroup.class)) {
+            return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value();
         }
 
         if (method.isAnnotationPresent(Parameter.class)) {
@@ -254,20 +316,73 @@
         return null;
     }
 
-    private static List<MethodCall> toMethodCalls(Method method) {
-        if (!isParametrized(method)) {
-            return List.of(new MethodCall(method));
+    private static Stream<Object[]> toCtorArgs(Method method) throws
+            IllegalAccessException, InvocationTargetException {
+        Class type = method.getDeclaringClass();
+        List<Method> paremetersProviders = Stream.of(type.getMethods())
+                .filter(m -> m.getParameterCount() == 0)
+                .filter(m -> (m.getModifiers() & Modifier.STATIC) != 0)
+                .filter(m -> m.isAnnotationPresent(Parameters.class))
+                .sorted()
+                .collect(Collectors.toList());
+        if (paremetersProviders.isEmpty()) {
+            // Single instance using the default constructor.
+            return Stream.ofNullable(MethodCall.DEFAULT_CTOR_ARGS);
+        }
+
+        // Pick the first method from the list.
+        Method paremetersProvider = paremetersProviders.iterator().next();
+        if (paremetersProviders.size() > 1) {
+            trace(String.format(
+                    "Found %d public static methods without arguments with %s annotation. Will use %s",
+                    paremetersProviders.size(), Parameters.class,
+                    paremetersProvider));
+            paremetersProviders.stream().map(Method::toString).forEach(
+                    TestBuilder::trace);
         }
-        Parameter[] annotations = getParameters(method);
+
+        // Construct collection of arguments for test class instances.
+        return ((Collection) paremetersProvider.invoke(null)).stream();
+    }
+
+    private static Stream<MethodCall> toMethodCalls(Method method) throws
+            IllegalAccessException, InvocationTargetException {
+        return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap(
+                s -> s).peek(methodCall -> {
+                    // Make sure required constructor is accessible if the one is needed.
+                    // Need to probe all methods as some of them might be static
+                    // and some class members.
+                    // Only class members require ctors.
+                    try {
+                        methodCall.checkRequiredConstructor();
+                    } catch (NoSuchMethodException ex) {
+                        throw new ParseException(ex.getMessage() + ".");
+                    }
+                });
+    }
+
+    private static Stream<MethodCall> toMethodCalls(Object[] ctorArgs, Method method) {
+        if (!isParametrized(method)) {
+            return Stream.of(new MethodCall(ctorArgs, method));
+        }
+        Parameter[] annotations = getMethodParameters(method);
         if (annotations.length == 0) {
-            return List.of(new MethodCall(method));
+            return Stream.of(new MethodCall(ctorArgs, method));
         }
         return Stream.of(annotations).map((a) -> {
-            String annotationValue = a.value();
-            Class paramClass = method.getParameterTypes()[0];
-            return new MethodCall(method,
-                    fromString(annotationValue, paramClass));
-        }).collect(Collectors.toList());
+            Class paramType = method.getParameterTypes()[0];
+            final Object annotationValue;
+            if (!paramType.isArray()) {
+                annotationValue = fromString(a.value()[0], paramType);
+            } else {
+                Class paramComponentType = paramType.getComponentType();
+                annotationValue = Array.newInstance(paramComponentType, a.value().length);
+                var idx = new AtomicInteger(-1);
+                Stream.of(a.value()).map(v -> fromString(v, paramComponentType)).sequential().forEach(
+                        v -> Array.set(annotationValue, idx.incrementAndGet(), v));
+            }
+            return new MethodCall(ctorArgs, method, annotationValue);
+        });
     }
 
     private static Object fromString(String value, Class toType) {
@@ -281,7 +396,7 @@
     }
 
     // Wraps Method.invike() into ThrowingRunnable.run()
-    private static ThrowingConsumer wrap(Method method, boolean dryRun) {
+    private ThrowingConsumer wrap(Method method) {
         return (test) -> {
             Class methodClass = method.getDeclaringClass();
             String methodName = String.join(".", methodClass.getName(),
@@ -318,7 +433,7 @@
         private String badCmdLineArg;
     }
 
-    private static void trace(String msg) {
+    static void trace(String msg) {
         if (TKit.VERBOSE_TEST_SETUP) {
             TKit.log(msg);
         }
@@ -329,6 +444,9 @@
     private List<MethodCall> testGroup;
     private List<ThrowingConsumer> beforeActions;
     private List<ThrowingConsumer> afterActions;
+    private Set<String> excludedTests;
+    private Set<String> includedTests;
+    private String spaceSubstitute;
     private boolean dryRun;
 
     private final static Map<Class, Function<String, Object>> conv = Map.of(
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Wed Oct 16 10:32:08 2019 -0400
@@ -23,18 +23,20 @@
 
 package jdk.jpackage.test;
 
+import java.lang.reflect.Array;
 import java.lang.reflect.Method;
 import java.nio.file.Files;
-import java.util.Collections;
-import java.util.List;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
 import jdk.jpackage.test.Functional.ThrowingFunction;
 import jdk.jpackage.test.Functional.ThrowingRunnable;
-import jdk.jpackage.test.Functional.ThrowingSupplier;
 
-class TestInstance implements ThrowingRunnable {
+final class TestInstance implements ThrowingRunnable {
 
     static class TestDesc {
         private TestDesc() {
@@ -43,24 +45,81 @@
         String testFullName() {
             StringBuilder sb = new StringBuilder();
             sb.append(clazz.getSimpleName());
+            if (instanceArgs != null) {
+                sb.append('(').append(instanceArgs).append(')');
+            }
             if (functionName != null) {
                 sb.append('.');
                 sb.append(functionName);
-                if (args != null) {
-                    sb.append('(').append(args).append(')');
+                if (functionArgs != null) {
+                    sb.append('(').append(functionArgs).append(')');
                 }
             }
             return sb.toString();
         }
 
-        private Class clazz;
-        private String functionName;
-        private String args;
+        static Builder createBuilder() {
+            return new Builder();
+        }
+
+        static final class Builder implements Supplier<TestDesc> {
+            private Builder() {
+            }
+
+            Builder method(Method v) {
+                method = v;
+                return this;
+            }
+
+            Builder ctorArgs(Object... v) {
+                ctorArgs = ofNullable(v);
+                return this;
+            }
+
+            Builder methodArgs(Object... v) {
+                methodArgs = ofNullable(v);
+                return this;
+            }
 
-        private static TestDesc create() {
-            TestDesc desc = new TestDesc();
-            desc.clazz = enclosingMainMethodClass();
-            return desc;
+            @Override
+            public TestDesc get() {
+                TestDesc desc = new TestDesc();
+                if (method == null) {
+                    desc.clazz = enclosingMainMethodClass();
+                } else {
+                    desc.clazz = method.getDeclaringClass();
+                    desc.functionName = method.getName();
+                    desc.functionArgs = formatArgs(methodArgs);
+                    desc.instanceArgs = formatArgs(ctorArgs);
+                }
+                return desc;
+            }
+
+            private static String formatArgs(List<Object> values) {
+                if (values == null) {
+                    return null;
+                }
+                return values.stream().map(v -> {
+                    if (v != null && v.getClass().isArray()) {
+                        return String.format("%s(length=%d)",
+                                Arrays.deepToString((Object[]) v),
+                                Array.getLength(v));
+                    }
+                    return String.format("%s", v);
+                }).collect(Collectors.joining(", "));
+            }
+
+            private static List<Object> ofNullable(Object... values) {
+                List<Object> result = new ArrayList();
+                for (var v: values) {
+                    result.add(v);
+                }
+                return result;
+            }
+
+            private List<Object> ctorArgs;
+            private List<Object> methodArgs;
+            private Method method;
         }
 
         static TestDesc create(Method m, Object... args) {
@@ -68,11 +127,22 @@
             desc.clazz = m.getDeclaringClass();
             desc.functionName = m.getName();
             if (args.length != 0) {
-                desc.args = Stream.of(args).map(Object::toString).collect(
-                        Collectors.joining(","));
+                desc.functionArgs = Stream.of(args).map(v -> {
+                    if (v.getClass().isArray()) {
+                        return String.format("%s(length=%d)",
+                                Arrays.deepToString((Object[]) v),
+                                Array.getLength(v));
+                    }
+                    return String.format("%s", v);
+                }).collect(Collectors.joining(", "));
             }
             return desc;
         }
+
+        private Class clazz;
+        private String functionName;
+        private String functionArgs;
+        private String instanceArgs;
     }
 
     TestInstance(ThrowingRunnable testBody) {
@@ -81,18 +151,19 @@
         this.testBody = (unused) -> testBody.run();
         this.beforeActions = Collections.emptyList();
         this.afterActions = Collections.emptyList();
-        this.testDesc = TestDesc.create();
+        this.testDesc = TestDesc.createBuilder().get();
+        this.dryRun = false;
     }
 
-    TestInstance(ThrowingFunction testConstructor, MethodCall testBody,
-            List<ThrowingConsumer> beforeActions,
-            List<ThrowingConsumer> afterActions) {
+    TestInstance(MethodCall testBody, List<ThrowingConsumer> beforeActions,
+            List<ThrowingConsumer> afterActions, boolean dryRun) {
         assertCount = 0;
-        this.testConstructor = testConstructor;
+        this.testConstructor = v -> ((MethodCall)v).newInstance();
         this.testBody = testBody;
         this.beforeActions = beforeActions;
         this.afterActions = afterActions;
         this.testDesc = testBody.createDescription();
+        this.dryRun = dryRun;
     }
 
     void notifyAssert() {
@@ -133,17 +204,44 @@
         }
     }
 
+    Path workDir() {
+        Path result = Path.of(".");
+        List<String> components = new ArrayList<>();
+
+        String testFunctionName = functionName();
+        if (testFunctionName != null) {
+            components.add(testFunctionName);
+        }
+
+        if (isPrametrized()) {
+            components.add(String.format("%08x", fullName().hashCode()));
+        }
+
+        if (!components.isEmpty()) {
+            result = result.resolve(String.join(".", components));
+        }
+
+        return result;
+    }
+
+    boolean isPrametrized() {
+        return Stream.of(testDesc.functionArgs, testDesc.instanceArgs).anyMatch(
+                Objects::nonNull);
+    }
+
     @Override
     public void run() throws Throwable {
-        final String fullName = testDesc.testFullName();
+        final String fullName = fullName();
         TKit.log(String.format("[ RUN      ] %s", fullName));
         try {
             Object testInstance = testConstructor.apply(testBody);
-            beforeActions.forEach((a) -> ThrowingConsumer.toConsumer(a).accept(
+            beforeActions.forEach(a -> ThrowingConsumer.toConsumer(a).accept(
                     testInstance));
-            Files.createDirectories(TKit.workDir());
             try {
-                testBody.accept(testInstance);
+                if (!dryRun) {
+                    Files.createDirectories(workDir());
+                    testBody.accept(testInstance);
+                }
             } finally {
                 afterActions.forEach(a -> TKit.ignoreExceptions(() -> a.accept(
                         testInstance)));
@@ -155,6 +253,11 @@
             } else if (status == null) {
                 status = Status.Failed;
             }
+
+            if (!KEEP_WORK_DIR.contains(status)) {
+                TKit.deleteDirectoryRecursive(workDir());
+            }
+
             TKit.log(String.format("%s %s; checks=%d", status, fullName,
                     assertCount));
         }
@@ -196,4 +299,30 @@
     private final ThrowingConsumer testBody;
     private final List<ThrowingConsumer> beforeActions;
     private final List<ThrowingConsumer> afterActions;
+    private final boolean dryRun;
+
+    private final static Set<Status> KEEP_WORK_DIR = Functional.identity(
+            () -> {
+                final String propertyName = "keep-work-dir";
+                Set<String> keepWorkDir = TKit.tokenizeConfigProperty(
+                        propertyName);
+                if (keepWorkDir == null) {
+                    return Set.of(Status.Failed);
+                }
+
+                Predicate<Set<String>> isOneOf = options -> {
+                    return !Collections.disjoint(keepWorkDir, options);
+                };
+
+                Set<Status> result = new HashSet<>();
+                if (isOneOf.test(Set.of("pass", "p"))) {
+                    result.add(Status.Passed);
+                }
+                if (isOneOf.test(Set.of("fail", "f"))) {
+                    result.add(Status.Failed);
+                }
+
+                return Collections.unmodifiableSet(result);
+            }).get();
+
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Wed Oct 16 10:32:08 2019 -0400
@@ -62,7 +62,7 @@
         }
 
         private void verifyDesktopShortcut() {
-            boolean appInstalled = cmd.launcherInstallationPath().toFile().exists();
+            boolean appInstalled = cmd.appLauncherPath().toFile().exists();
             if (cmd.hasArgument("--win-shortcut")) {
                 if (isUserLocalInstall(cmd)) {
                     verifyUserLocalDesktopShortcut(appInstalled);
@@ -102,7 +102,7 @@
         }
 
         private void verifyStartMenuShortcut() {
-            boolean appInstalled = cmd.launcherInstallationPath().toFile().exists();
+            boolean appInstalled = cmd.appLauncherPath().toFile().exists();
             if (cmd.hasArgument("--win-menu")) {
                 if (isUserLocalInstall(cmd)) {
                     verifyUserLocalStartMenuShortcut(appInstalled);
@@ -122,16 +122,23 @@
                     () -> "Unknown"), cmd.name() + ".lnk");
         }
 
+        private void verifyStartMenuShortcut(Path shortcutsRoot, boolean exists) {
+            Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath());
+            verifyShortcut(shortcutPath, exists);
+            if (!exists) {
+                TKit.assertPathExists(shortcutPath.getParent(), false);
+            }
+        }
+
         private void verifySystemStartMenuShortcut(boolean exists) {
-            Path dir = Path.of(queryRegistryValueCache(
-                    SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs"));
-            verifyShortcut(dir.resolve(startMenuShortcutPath()), exists);
+            verifyStartMenuShortcut(Path.of(queryRegistryValueCache(
+                    SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs")), exists);
+
         }
 
         private void verifyUserLocalStartMenuShortcut(boolean exists) {
-            Path dir = Path.of(queryRegistryValueCache(
-                    USER_SHELL_FOLDERS_REGKEY, "Programs"));
-            verifyShortcut(dir.resolve(startMenuShortcutPath()), exists);
+            verifyStartMenuShortcut(Path.of(queryRegistryValueCache(
+                    USER_SHELL_FOLDERS_REGKEY, "Programs")), exists);
         }
 
         private void verifyFileAssociationsRegistry() {
@@ -140,7 +147,7 @@
         }
 
         private void verifyFileAssociationsRegistry(Path faFile) {
-            boolean appInstalled = cmd.launcherInstallationPath().toFile().exists();
+            boolean appInstalled = cmd.appLauncherPath().toFile().exists();
             try {
                 TKit.trace(String.format(
                         "Get file association properties from [%s] file",
--- a/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -77,6 +77,11 @@
                 "<jpackage-state>",
                     "<main-launcher></main-launcher>",
                 "</jpackage-state>"));
+        assertInvalid(createFromXml(
+                "<jpackage-state>",
+                    "<launcher>A</launcher>",
+                    "<launcher>B</launcher>",
+                "</jpackage-state>"));
     }
 
     @Test
@@ -86,9 +91,9 @@
                     "<main-launcher>Foo</main-launcher>",
                 "</jpackage-state>")).getLauncherName());
 
-        Assert.assertEquals("Foo", (createFromXml(
+        Assert.assertEquals("Boo", (createFromXml(
                 "<jpackage-state>",
-                    "<main-launcher>Foo</main-launcher>",
+                    "<main-launcher>Boo</main-launcher>",
                     "<main-launcher>Bar</main-launcher>",
                 "</jpackage-state>")).getLauncherName());
 
@@ -130,7 +135,7 @@
         params.put("add-launcher", launchersAsMap);
         AppImageFile aif = create(params);
 
-	List<String> addLauncherNames = aif.getAddLauncherNames();
+        List<String> addLauncherNames = aif.getAddLauncherNames();
         Assert.assertEquals(2, addLauncherNames.size());
         Assert.assertTrue(addLauncherNames.contains("Launcher2Name"));
         Assert.assertTrue(addLauncherNames.contains("Launcher3Name"));
--- a/test/jdk/tools/jpackage/macosx/BundleIdentifierTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.nio.file.Files;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathConstants;
-import javax.xml.xpath.XPathFactory;
-
-/*
- * @test
- * @summary jpackage create image bundle identifier test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @requires (os.family == "mac")
- * @run main/othervm -Xmx512m BundleIdentifierTest
- */
-public class BundleIdentifierTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-    private static final String MAC_PACKAGE_IDENTIFIER = "TestBundleIdentifier";
-    private static final String APP_NAME = "test";
-    private static final String MAIN_CLASS = "Hello";
-
-    private static final String [] CMD_1 = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", APP_NAME,
-        "--main-jar", "hello.jar",
-        "--main-class", MAIN_CLASS
-    };
-
-    private static final String [] CMD_2 = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", APP_NAME,
-        "--main-jar", "hello.jar",
-        "--main-class", MAIN_CLASS,
-        "--mac-package-identifier", MAC_PACKAGE_IDENTIFIER
-    };
-
-    private static void validateResult(String[] result) throws Exception {
-        if (result.length != 2) {
-            throw new AssertionError(
-                   "Unexpected number of lines: " + result.length);
-        }
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    private static void validate() throws Exception {
-        int retVal = JPackageHelper.execute(null, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                   "Test application exited with error: " + retVal);
-        }
-
-        File outfile = new File(appOutput);
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-        validateResult(result);
-    }
-
-    private static void validateBundleIdentifier(String bundleIdentifier)
-                                                              throws Exception {
-        System.out.println("Validating bundleIdentifier: " + bundleIdentifier);
-
-        File infoPList = new File(OUTPUT + File.separator + APP_NAME + ".app" +
-                File.separator + "Contents" + File.separator + "Info.plist");
-
-        DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance();
-        dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
-        DocumentBuilder b = dbf.newDocumentBuilder();
-        org.w3c.dom.Document doc = b.parse(new FileInputStream(
-                                                  infoPList.getAbsolutePath()));
-
-        XPath xPath = XPathFactory.newInstance().newXPath();
-        // Query for the value of <string> element preceding <key> element
-        // with value equal to CFBundleIdentifier
-        String v = (String)xPath.evaluate(
-                       "//string[preceding-sibling::key = \"CFBundleIdentifier\"][1]",
-                       doc, XPathConstants.STRING);
-
-        if (!v.equals(bundleIdentifier)) {
-            throw new AssertionError("Unexpected value of CFBundleIdentifier key: ["
-                                  + v + "]. Expected value: [" + bundleIdentifier + "]");
-        }
-    }
-
-    private static void testCreateAppImage(String [] cmd,
-                                         String bundleIdentifier,
-                                         boolean validateApp) throws Exception {
-        JPackageHelper.executeCLI(true, cmd);
-        if (validateApp) {
-            validate();
-        }
-        validateBundleIdentifier(bundleIdentifier);
-    }
-
-    private static void testCreateAppImageToolProvider(String [] cmd,
-                                         String bundleIdentifier,
-                                         boolean validateApp) throws Exception {
-        JPackageHelper.executeToolProvider(true, cmd);
-        if (validateApp) {
-            validate();
-        }
-        validateBundleIdentifier(bundleIdentifier);
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-        testCreateAppImage(CMD_1, MAIN_CLASS, false);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImageToolProvider(CMD_1, MAIN_CLASS, false);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImage(CMD_2, MAC_PACKAGE_IDENTIFIER, true);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImageToolProvider(CMD_2, MAC_PACKAGE_IDENTIFIER, true);
-    }
-}
--- a/test/jdk/tools/jpackage/macosx/BundleNameTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.nio.file.Files;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathConstants;
-import javax.xml.xpath.XPathFactory;
-
-/*
- * @test
- * @summary jpackage create image bundle name test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @requires (os.family == "mac")
- * @run main/othervm -Xmx512m BundleNameTest
- */
-public class BundleNameTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-    private static final String MAC_BUNDLE_NAME = "TestBundleName";
-    private static final String APP_NAME = "test";
-
-    private static final String [] CMD_1 = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", APP_NAME,
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello"
-    };
-
-    private static final String [] CMD_2 = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", APP_NAME,
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-        "--mac-package-name", MAC_BUNDLE_NAME
-    };
-
-    private static void validateResult(String[] result) throws Exception {
-        if (result.length != 2) {
-            throw new AssertionError(
-                   "Unexpected number of lines: " + result.length);
-        }
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    private static void validate() throws Exception {
-        int retVal = JPackageHelper.execute(null, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                   "Test application exited with error: " + retVal);
-        }
-
-        File outfile = new File(appOutput);
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-        validateResult(result);
-    }
-
-    private static void validateBundleName(String bundleName) throws Exception {
-        System.out.println("Validating bundleName: " + bundleName);
-
-        File infoPList = new File(OUTPUT + File.separator + APP_NAME + ".app" +
-                File.separator + "Contents" + File.separator + "Info.plist");
-
-        DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance();
-        dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
-        DocumentBuilder b = dbf.newDocumentBuilder();
-        org.w3c.dom.Document doc = b.parse(new FileInputStream(
-                                                  infoPList.getAbsolutePath()));
-
-        XPath xPath = XPathFactory.newInstance().newXPath();
-        // Query for the value of <string> element preceding <key> element
-        // with value equal to CFBundleName
-        String v = (String)xPath.evaluate(
-                       "//string[preceding-sibling::key = \"CFBundleName\"][1]",
-                       doc, XPathConstants.STRING);
-
-        if (!v.equals(bundleName)) {
-            throw new AssertionError("Unexpected value of CFBundleName key: ["
-                                  + v + "]. Expected value: [" + bundleName + "]");
-        }
-    }
-
-    private static void testCreateAppImage(String [] cmd,
-                                         String bundleName,
-                                         boolean validateApp) throws Exception {
-        JPackageHelper.executeCLI(true, cmd);
-        if (validateApp) {
-            validate();
-        }
-        validateBundleName(bundleName);
-    }
-
-    private static void testCreateAppImageToolProvider(String [] cmd,
-                                         String bundleName,
-                                         boolean validateApp) throws Exception {
-        JPackageHelper.executeToolProvider(true, cmd);
-        if (validateApp) {
-            validate();
-        }
-        validateBundleName(bundleName);
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-        testCreateAppImage(CMD_1, APP_NAME, false);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImageToolProvider(CMD_1, APP_NAME, false);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImage(CMD_2, MAC_BUNDLE_NAME, true);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        testCreateAppImageToolProvider(CMD_2, MAC_BUNDLE_NAME, true);
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import jdk.jpackage.test.TKit;
+import jdk.jpackage.test.MacHelper;
+import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.Annotations.Test;
+import jdk.jpackage.test.Annotations.Parameter;
+
+
+/**
+ * Test --mac-package-name, --mac-package-identifier parameters.
+ */
+
+/*
+ * @test
+ * @summary jpackage with --mac-package-name, --mac-package-identifier
+ * @library ../helpers
+ * @build jdk.jpackage.test.*
+ * @requires (os.family == "mac")
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile MacPropertiesTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=MacPropertiesTest
+ */
+public class MacPropertiesTest {
+    @Test
+    @Parameter("MacPackageNameTest")
+    public void testPackageName(String packageName) {
+        testParameterInAppImage("--mac-package-name", "CFBundleName",
+                packageName);
+    }
+
+    @Test
+    @Parameter("Foo")
+    public void testPackageIdetifier(String packageId) {
+        testParameterInAppImage("--mac-package-identifier", "CFBundleIdentifier",
+                packageId);
+    }
+
+    private static void testParameterInAppImage(String jpackageParameterName,
+            String plistKeyName, String value) {
+        JPackageCommand cmd = JPackageCommand.helloAppImage()
+                .addArguments(jpackageParameterName, value);
+
+        cmd.executeAndAssertHelloAppImageCreated();
+
+        var plist = MacHelper.readPListFromAppImage(cmd.outputBundle());
+
+        TKit.assertEquals(value, plist.queryValue(plistKeyName), String.format(
+                "Check value of %s plist key", plistKeyName));
+    }
+}
--- a/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -59,10 +59,10 @@
                     "jpackagerTest.keychain");
             cmd.executeAndAssertHelloAppImageCreated();
 
-            Path launcherPath = cmd.appImage().resolve(cmd.launcherPathInAppImage());
+            Path launcherPath = cmd.appLauncherPath();
             SigningBase.verifyCodesign(launcherPath, true);
 
-            Path appImage = cmd.appImage();
+            Path appImage = cmd.outputBundle();
             SigningBase.verifyCodesign(appImage, true);
             SigningBase.verifySpctl(appImage, "exec");
         });
--- a/test/jdk/tools/jpackage/macosx/SigningPackageTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -60,25 +60,14 @@
         SigningBase.verifyCodesign(outputBundle, false);
     }
 
-    private static void verifyAppImageInDMG(JPackageCommand cmd) throws Exception {
-        Path disk = Paths.get("/Volumes", cmd.name());
-        try {
-            new Executor()
-                    .setExecutable("/usr/bin/hdiutil")
-                    .addArgument("attach").addArgument(cmd.outputBundle())
-                    .execute().assertExitCodeIsZero();
-
+    private static void verifyAppImageInDMG(JPackageCommand cmd) {
+        MacHelper.withExplodedDmg(cmd, disk -> {
             Path appImageInDMG = disk.resolve(cmd.name() + ".app");
             Path launcherPath = appImageInDMG.resolve(Path.of("Contents", "MacOS", cmd.name()));
             SigningBase.verifyCodesign(launcherPath, true);
             SigningBase.verifyCodesign(appImageInDMG, true);
             SigningBase.verifySpctl(appImageInDMG, "exec");
-        } finally {
-            new Executor()
-                    .setExecutable("/usr/bin/hdiutil")
-                    .addArgument("detach").addArgument(disk)
-                    .execute().assertExitCodeIsZero();
-        }
+        });
     }
 
     public static void main(String[] args) throws Exception {
@@ -94,14 +83,10 @@
                                 "--mac-signing-keychain", "jpackagerTest.keychain");
                     })
                     .forTypes(PackageType.MAC_PKG)
-                    .addBundleVerifier(cmd -> {
-                        verifyPKG(cmd);
-                    })
+                    .addBundleVerifier(SigningPackageTest::verifyPKG)
                     .forTypes(PackageType.MAC_DMG)
-                    .addBundleVerifier(cmd -> {
-                        verifyDMG(cmd);
-                        verifyAppImageInDMG(cmd);
-                    })
+                    .addBundleVerifier(SigningPackageTest::verifyDMG)
+                    .addBundleVerifier(SigningPackageTest::verifyAppImageInDMG)
                     .run();
         });
     }
--- a/test/jdk/tools/jpackage/macosx/base/SigningBase.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/macosx/base/SigningBase.java	Wed Oct 16 10:32:08 2019 -0400
@@ -37,14 +37,8 @@
     public static String KEYCHAIN = "jpackagerTest.keychain";
 
     private static void checkString(List<String> result, String lookupString) {
-        result.stream()
-                .filter(line -> line.trim().equals(lookupString)).findFirst().or(
-                () -> {
-                    TKit.assertUnexpected(String.format(
-                            "Failed to find [%s] string in the output",
-                            lookupString));
-                    return null;
-                });
+        TKit.assertTextStream(lookupString).predicate(
+                (line, what) -> line.trim().equals(what)).apply(result.stream());
     }
 
     private static List<String> codesignResult(Path target, boolean signed) {
@@ -62,7 +56,7 @@
 
     private static void verifyCodesignResult(List<String> result, Path target,
             boolean signed) {
-        result.stream().peek(TKit::trace);
+        result.stream().forEachOrdered(TKit::trace);
         if (signed) {
             String lookupString = target.toString() + ": valid on disk";
             checkString(result, lookupString);
@@ -86,7 +80,7 @@
     }
 
     private static void verifySpctlResult(List<String> result, Path target, String type) {
-        result.stream().peek(TKit::trace);
+        result.stream().forEachOrdered(TKit::trace);
         String lookupString = target.toString() + ": accepted";
         checkString(result, lookupString);
         lookupString = "source=" + DEV_NAME;
@@ -110,7 +104,7 @@
     }
 
     private static void verifyPkgutilResult(List<String> result) {
-        result.stream().peek(line -> TKit.trace(line));
+        result.stream().forEachOrdered(TKit::trace);
         String lookupString = "Status: signed by a certificate trusted for current user";
         checkString(result, lookupString);
         lookupString = "1. " + INSTALLER_CERT;
--- a/test/jdk/tools/jpackage/macosx/base/SigningCheck.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/macosx/base/SigningCheck.java	Wed Oct 16 10:32:08 2019 -0400
@@ -88,13 +88,12 @@
                 .setExecutable("security")
                 .addArguments("dump-trust-settings")
                 .executeAndGetOutput();
-        result.stream().peek(TKit::trace);
-        result.stream()
-                .filter(line -> line.trim().endsWith(name)).findFirst().orElseThrow(
-                () -> {
-                    throw TKit.throwSkippedException("Certifcate not trusted by current user: "
-                            + name);
-                });
+        result.stream().forEachOrdered(TKit::trace);
+        TKit.assertTextStream(name)
+                .predicate((line, what) -> line.trim().endsWith(what))
+                .orElseThrow(() -> TKit.throwSkippedException(
+                        "Certifcate not trusted by current user: " + name))
+                .apply(result.stream());
     }
 
 }
--- a/test/jdk/tools/jpackage/share/AddModulesTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
- /*
- * @test
- * @summary jpackage create image module test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @build Base
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m AddModulesTest
- */
-public class AddModulesTest {
-    private static final String OUTPUT = "output";
-
-    private static final String [] CMD1 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-        "--add-modules", "java.desktop",
-    };
-
-    private static final String [] CMD2 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-        "--add-modules", "java.desktop,java.xml",
-    };
-
-    private static final String [] CMD3 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-        "--add-modules", "java.desktop",
-        "--add-modules", "java.xml",
-    };
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloModule();
-        Base.testCreateAppImage(CMD1);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        Base.testCreateAppImageToolProvider(CMD1);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        Base.testCreateAppImageToolProvider(CMD2);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        Base.testCreateAppImageToolProvider(CMD3);
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -31,6 +31,7 @@
 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;
 
 /**
@@ -47,41 +48,43 @@
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m AdditionalLaunchersTest
+ * @compile AdditionalLaunchersTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=AdditionalLaunchersTest
  */
+
 public class AdditionalLaunchersTest {
 
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            FileAssociations fa = new FileAssociations(
-                    MethodHandles.lookup().lookupClass().getSimpleName());
+    @Test
+    public void test() {
+        FileAssociations fa = new FileAssociations(
+                MethodHandles.lookup().lookupClass().getSimpleName());
 
-            // Configure a bunch of additional launchers and also setup
-            // file association to make sure it will be linked only to the main
-            // launcher.
+        // Configure a bunch of additional launchers and also setup
+        // file association to make sure it will be linked only to the main
+        // launcher.
 
-            PackageTest packageTest = new PackageTest().configureHelloApp()
-            .addInitializer(cmd -> {
-                fa.createFile();
-                cmd.addArguments("--file-associations", fa.getPropertiesFile());
-                cmd.addArguments("--arguments", "Duke", "--arguments", "is",
-                        "--arguments", "the", "--arguments", "King");
-            });
+        PackageTest packageTest = new PackageTest().configureHelloApp()
+        .addInitializer(cmd -> {
+            fa.createFile();
+            cmd.addArguments("--file-associations", fa.getPropertiesFile());
+            cmd.addArguments("--arguments", "Duke", "--arguments", "is",
+                    "--arguments", "the", "--arguments", "King");
+        });
 
-            packageTest.addHelloAppFileAssociationsVerifier(fa);
+        packageTest.addHelloAppFileAssociationsVerifier(fa);
 
-            new AdditionalLauncher("Baz2").setArguments().applyTo(packageTest);
-            new AdditionalLauncher("foo").setArguments("yep!").applyTo(packageTest);
+        new AdditionalLauncher("Baz2").setArguments().applyTo(packageTest);
+        new AdditionalLauncher("foo").setArguments("yep!").applyTo(packageTest);
 
-            AdditionalLauncher barLauncher = new AdditionalLauncher("Bar").setArguments(
-                    "one", "two", "three");
-            packageTest.forTypes(PackageType.LINUX).addInitializer(cmd -> {
-                barLauncher.setIcon(TKit.TEST_SRC_ROOT.resolve("apps/dukeplug.png"));
-            });
-            barLauncher.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();
-        });
+        packageTest.run();
     }
 
     private static Path replaceFileName(Path path, String newFileName) {
@@ -129,12 +132,11 @@
                 TKit.createPropertiesFile(propsFile, properties);
             });
             test.addInstallVerifier(cmd -> {
-                Path launcherPath = replaceFileName(
-                        cmd.launcherInstallationPath(), name);
+                Path launcherPath = replaceFileName(cmd.appLauncherPath(), name);
 
                 TKit.assertExecutableFileExists(launcherPath);
 
-                if (cmd.isFakeRuntimeInstalled(String.format(
+                if (cmd.isFakeRuntime(String.format(
                         "Not running %s launcher", launcherPath))) {
                     return;
                 }
@@ -143,8 +145,7 @@
                                 String[]::new));
             });
             test.addUninstallVerifier(cmd -> {
-                Path launcherPath = replaceFileName(
-                        cmd.launcherInstallationPath(), name);
+                Path launcherPath = replaceFileName(cmd.appLauncherPath(), name);
 
                 TKit.assertPathExists(launcherPath, false);
             });
--- a/test/jdk/tools/jpackage/share/AppMainClassModuleTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image using main class from main module
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m AppMainClassModuleTest
- */
-public class AppMainClassModuleTest {
-
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello",
-        "--module-path", "input"
-    };
-
-    private static final String[] CMD_MAIN_CLASS = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input"
-    };
-
-    private static void validate(String buildOutput) throws Exception {
-
-        File outfile = new File(appOutput);
-        int retVal = JPackageHelper.execute(outfile, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                    "Test application exited with error: ");
-        }
-
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    private static void testMainClassFromModule() throws Exception {
-        JPackageHelper.createHelloModule(
-                new JPackageHelper.ModuleArgs(null, "com.hello.Hello"));
-
-        validate(JPackageHelper.executeCLI(true, CMD));
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeToolProvider(true, CMD));
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-    }
-
-    private static void testMainClassFromCLI() throws Exception {
-        JPackageHelper.createHelloModule(
-                new JPackageHelper.ModuleArgs(null, "com.hello.Hello2"));
-
-        validate(JPackageHelper.executeCLI(true, CMD_MAIN_CLASS));
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeToolProvider(true, CMD_MAIN_CLASS));
-    }
-
-    public static void main(String[] args) throws Exception {
-        testMainClassFromModule();
-        testMainClassFromCLI();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/AppVersionModuleTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image using version from main module
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m AppVersionModuleTest
- */
-public class AppVersionModuleTest {
-    private static final String OUTPUT = "output";
-    private static final String appCfg = JPackagePath.getAppCfg();
-    private static final String MODULE_VERSION = "2.7";
-    private static final String CLI_VERSION = "3.5";
-
-    private static final String[] CMD_MODULE_VERSION = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input"
-    };
-
-    private static final String[] CMD_CLI_VERSION = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-        "--app-version", CLI_VERSION
-    };
-
-    private static void validate(String version)
-            throws Exception {
-        File outfile = new File(appCfg);
-        if (!outfile.exists()) {
-            throw new AssertionError(appCfg + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        if (version == null) {
-            version = MODULE_VERSION;
-        }
-
-        String expected = "app.version=" + version;
-        if (!output.contains(expected)) {
-            System.err.println("Expected: " + expected);
-            throw new AssertionError("Cannot find expected entry in config file");
-        }
-    }
-
-    private static void testVersion() throws Exception {
-        JPackageHelper.executeCLI(true, CMD_MODULE_VERSION);
-        validate(null);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeCLI(true, CMD_CLI_VERSION);
-        validate(CLI_VERSION);
-    }
-
-    private static void testVersionToolProvider() throws Exception {
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD_MODULE_VERSION);
-        validate(null);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD_CLI_VERSION);
-        validate(CLI_VERSION);
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloModule(
-                new JPackageHelper.ModuleArgs(MODULE_VERSION, null));
-        testVersion();
-        testVersionToolProvider();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/AppVersionTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image --app-version test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m AppVersionTest
- */
-public class AppVersionTest {
-    private static final String OUTPUT = "output";
-    private static final String appCfg = JPackagePath.getAppCfg();
-    private static final String VERSION = "2.3";
-    private static final String VERSION_DEFAULT = "1.0";
-
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-   };
-
-    private static final String[] CMD_VERSION = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-        "--app-version", VERSION};
-
-    private static void validate(String version)
-            throws Exception {
-        File outfile = new File(appCfg);
-        if (!outfile.exists()) {
-            throw new AssertionError(appCfg + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        if (version == null) {
-            version = VERSION_DEFAULT;
-        }
-
-        String expected = "app.version=" + version;
-        if (!output.contains(expected)) {
-            System.err.println("Expected: " + expected);
-            throw new AssertionError("Cannot find expected entry in config file");
-        }
-    }
-
-    private static void testVersion() throws Exception {
-        JPackageHelper.executeCLI(true, CMD);
-        validate(null);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeCLI(true, CMD_VERSION);
-        validate(VERSION);
-    }
-
-    private static void testVersionToolProvider() throws Exception {
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD);
-        validate(null);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD_VERSION);
-        validate(VERSION);
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-        testVersion();
-        testVersionToolProvider();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/ArgumentsTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/ArgumentsTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -48,13 +48,18 @@
  * @summary jpackage create image with --arguments test
  * @library ../helpers
  * @build jdk.jpackage.test.*
- * @modules jdk.jpackage
+ * @modules jdk.jpackage/jdk.jpackage.internal
  * @compile ArgumentsTest.java
  * @run main/othervm -Xmx512m jdk.jpackage.test.Main
  *  --jpt-run=ArgumentsTest
- *  --jpt-before-run=jdk.jpackage.test.JPackageCommand.useToolProviderByDefault
  */
 public class ArgumentsTest {
+
+    @BeforeEach
+    public static void useJPackageToolProvider() {
+        JPackageCommand.useToolProviderByDefault();
+    }
+
     @Test
     @Parameter("Goodbye")
     @Parameter("com.hello/com.hello.Hello")
@@ -73,8 +78,8 @@
 
         cmd.executeAndAssertImageCreated();
 
-        Path launcherPath = cmd.appImage().resolve(cmd.launcherPathInAppImage());
-        if (!cmd.isFakeRuntimeInAppImage(String.format(
+        Path launcherPath = cmd.appLauncherPath();
+        if (!cmd.isFakeRuntime(String.format(
                 "Not running [%s] launcher", launcherPath))) {
             HelloApp.executeAndVerifyOutput(launcherPath, TRICKY_ARGUMENTS);
         }
--- a/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.FileAssociations;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --file-associations parameter. Output of the test should be
@@ -49,18 +50,19 @@
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m FileAssociationsTest
+ * @compile FileAssociationsTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=FileAssociationsTest
  */
 public class FileAssociationsTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            PackageTest packageTest = new PackageTest();
+    @Test
+    public static void test() {
+        PackageTest packageTest = new PackageTest();
 
-            applyFileAssociations(packageTest, new FileAssociations("jptest1"));
-            applyFileAssociations(packageTest,
-                    new FileAssociations("jptest2").setFilename("fa2"));
-            packageTest.run();
-        });
+        applyFileAssociations(packageTest, new FileAssociations("jptest1"));
+        applyFileAssociations(packageTest,
+                new FileAssociations("jptest2").setFilename("fa2"));
+        packageTest.run();
     }
 
     private static void applyFileAssociations(PackageTest test,
--- a/test/jdk/tools/jpackage/share/HelpTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
- /*
- * @test
- * @summary jpackage help test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m HelpTest
- */
-public class HelpTest {
-
-    // Platform specific help messages.
-    private static final String WINDOWS_HELP =
-            "--win-dir-chooser";
-    private static final String OSX_HELP =
-            "--mac-package-identifier";
-    private static final String LINUX_HELP =
-            "--linux-package-name";
-
-    private static void validate(String output1, String output2)
-            throws Exception {
-        if (output1.split("\n").length < 25) {
-            throw new AssertionError("jpacakger --help failed");
-        }
-
-        if (output2.split("\n").length < 25) {
-            throw new AssertionError("jpacakger -h failed");
-        }
-
-        // Make sure output matches for --help and -h
-        if (!output1.equals(output2)) {
-            System.err.println("================= --help =================");
-            System.err.println(output1);
-
-            System.err.println("=================== -h ===================");
-            System.err.println(output2);
-
-            throw new AssertionError(
-                    "jpacakger help text does not match between --help and -h");
-        }
-
-        if (JPackageHelper.isWindows()) {
-            if (!output1.contains(WINDOWS_HELP)) {
-                throw new AssertionError(
-                  "jpacakger help text missing Windows specific help");
-            }
-
-            if (output1.contains(OSX_HELP) || output1.contains(LINUX_HELP)) {
-                throw new AssertionError(
-                  "jpacakger help text contains other platforms specific help");
-
-            }
-        } else if (JPackageHelper.isOSX()) {
-            if (!output1.contains(OSX_HELP)) {
-                throw new AssertionError(
-                  "jpacakger help text missing OS X specific help");
-            }
-
-            if (output1.contains(WINDOWS_HELP) ||
-                    output1.contains(LINUX_HELP)) {
-                throw new AssertionError(
-                 "jpacakger help text contains other platforms specific help");
-            }
-        } else if (JPackageHelper.isLinux()) {
-            if (!output1.contains(LINUX_HELP)) {
-                throw new AssertionError(
-                  "jpacakger help text missing Linux specific help");
-            }
-
-            if (output1.contains(OSX_HELP) || output1.contains(WINDOWS_HELP)) {
-                throw new AssertionError(
-                  "jpacakger help text contains other platforms specific help");
-            }
-        }
-    }
-
-    private static void testHelp() throws Exception {
-        String output1 = JPackageHelper.executeCLI(true, "--help");
-        String output2 = JPackageHelper.executeCLI(true, "-h");
-        validate(output1, output2);
-    }
-
-    private static void testHelpToolProvider() throws Exception {
-        String output1 = JPackageHelper.executeToolProvider(true, "--help");
-        String output2 = JPackageHelper.executeToolProvider(true, "-h");
-        validate(output1, output2);
-    }
-
-    public static void main(String[] args) throws Exception {
-        testHelp();
-        testHelpToolProvider();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/IconTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/IconTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -21,38 +21,70 @@
  * questions.
  */
 
+import java.io.IOException;
 import java.nio.file.Files;
-import jdk.jpackage.internal.ApplicationLayout;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.Functional;
+import jdk.jpackage.test.Annotations.*;
 import jdk.jpackage.test.JPackageCommand;
 
 /*
  * @test
- * @summary jpackage create image to verify --icon
+ * @summary jpackage create image with custom icon
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m IconTest
+ * @compile IconTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=IconTest
  */
+
 public class IconTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            JPackageCommand cmd = JPackageCommand.helloAppImage().addArguments("--icon", GOLDEN_ICON);
-            cmd.useToolProvider(true).executeAndAssertHelloAppImageCreated();
+    @Test
+    public static void testResourceDir() throws IOException {
+        TKit.withTempDirectory("resources", tempDir -> {
+            JPackageCommand cmd = JPackageCommand.helloAppImage()
+                    .addArguments("--resource-dir", tempDir);
+
+            Files.copy(GOLDEN_ICON, tempDir.resolve(appIconFileName(cmd)),
+                    StandardCopyOption.REPLACE_EXISTING);
+
+            testIt(cmd);
+        });
+    }
 
-            Path iconPath = ApplicationLayout.platformAppImage().resolveAt(
-                    cmd.appImage()).destktopIntegrationDirectory().resolve(
-                    cmd.launcherPathInAppImage().getFileName().toString().replaceAll(
-                            "\\.[^.]*$", "") + ICON_SUFFIX);
+    @Test
+    @Parameter("true")
+    @Parameter("false")
+    public static void testParameter(boolean relativePath) throws IOException {
+        final Path iconPath;
+        if (relativePath) {
+            iconPath = TKit.createRelativePathCopy(GOLDEN_ICON);
+        } else {
+            iconPath = GOLDEN_ICON;
+        }
+
+        testIt(JPackageCommand.helloAppImage().addArguments("--icon", iconPath));
+    }
 
-            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 String appIconFileName(JPackageCommand cmd) {
+        return cmd.appLauncherPath().getFileName().toString().replaceAll(
+                "\\.[^.]*$", "") + ICON_SUFFIX;
+    }
+
+    private static void testIt(JPackageCommand cmd) throws IOException {
+        cmd.executeAndAssertHelloAppImageCreated();
+
+        Path iconPath = cmd.appLayout().destktopIntegrationDirectory().resolve(
+                appIconFileName(cmd));
+
+        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 final static String ICON_SUFFIX = Functional.identity(() -> {
--- a/test/jdk/tools/jpackage/share/InstallDirTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/InstallDirTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -33,17 +33,18 @@
 import jdk.jpackage.test.Annotations.Parameter;
 
 /**
- * Test --install-dir parameter. Output of the test should be installdirtest*.*
- * package bundle. The output package should provide the same functionality as
- * the default package but install test application in specified directory.
+ * Test --install-dir parameter. Output of the test should be
+ * commoninstalldirtest*.* package bundle. The output package should provide the
+ * same functionality as the default package but install test application in
+ * specified directory.
  *
  * Linux:
  *
- * Application should be installed in /opt/jpackage/installdirtest folder.
+ * Application should be installed in /opt/jpackage/commoninstalldirtest folder.
  *
  * Mac:
  *
- * Application should be installed in /Applications/jpackage/installdirtest.app
+ * Application should be installed in /Applications/jpackage/commoninstalldirtest.app
  * folder.
  *
  * Windows:
@@ -116,20 +117,20 @@
     private static void testLinuxBad(String installDir,
             String errorMessageSubstring) {
         new PackageTest().configureHelloApp()
-                .setExpectedExitCode(1)
-                .forTypes(PackageType.LINUX)
-                .addInitializer(cmd -> {
-                    cmd.addArguments("--install-dir", installDir);
-                    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));
-                })
-                .run();
+        .setExpectedExitCode(1)
+        .forTypes(PackageType.LINUX)
+        .addInitializer(cmd -> {
+            cmd.addArguments("--install-dir", installDir);
+            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));
+        })
+        .run();
     }
 }
--- a/test/jdk/tools/jpackage/share/LicenseTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/LicenseTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -38,14 +38,14 @@
 import jdk.jpackage.test.TKit;
 
 /**
- * Test --license-file parameter. Output of the test should be licensetest*.*
+ * Test --license-file parameter. Output of the test should be commonlicensetest*.*
  * package bundle. The output package should provide the same functionality as
  * the default package and also incorporate license information from
  * test/jdk/tools/jpackage/resources/license.txt file from OpenJDK repo.
  *
  * deb:
  *
- * Package should install license file /usr/share/doc/licensetest/copyright
+ * Package should install license file /opt/commonlicensetest/share/doc/copyright
  * file.
  *
  * rpm:
@@ -88,20 +88,8 @@
     public static void testCommon() {
         new PackageTest().configureHelloApp()
         .addInitializer(cmd -> {
-            Path licenseCopy = TKit.workDir().resolve(LICENSE_FILE.getFileName()).toAbsolutePath().normalize();
-            Files.copy(LICENSE_FILE, licenseCopy,
-                    StandardCopyOption.REPLACE_EXISTING);
-            final Path basePath = Path.of(".").toAbsolutePath().normalize();
-            try {
-                licenseCopy = basePath.relativize(licenseCopy);
-            } catch (IllegalArgumentException ex) {
-                // May happen on Windows: java.lang.IllegalArgumentException: 'other' has different root
-                TKit.trace(String.format(
-                        "Not using relative path to license file for --license-file parameter. Failed to relativize [%s] at [%s]",
-                        licenseCopy, basePath));
-                ex.printStackTrace();
-            }
-            cmd.addArguments("--license-file", licenseCopy);
+            cmd.addArguments("--license-file", TKit.createRelativePathCopy(
+                    LICENSE_FILE));
         })
         .forTypes(PackageType.LINUX)
         .addBundleVerifier(cmd -> {
--- a/test/jdk/tools/jpackage/share/MainClassAttributeTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image with no main class arguments and with main-class attribute
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m MainClassAttributeTest
- */
-public class MainClassAttributeTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar"};
-
-    private static void validateResult(String[] result) throws Exception {
-        if (result.length != 2) {
-            throw new AssertionError(
-                   "Unexpected number of lines: " + result.length);
-        }
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    private static void validate() throws Exception {
-        int retVal = JPackageHelper.execute(null, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                   "Test application exited with error: " + retVal);
-        }
-
-        File outfile = new File(appOutput);
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-        validateResult(result);
-    }
-
-    private static void testMainClassAttribute() throws Exception {
-        JPackageHelper.executeCLI(true, CMD);
-        validate();
-    }
-
-    private static void testMainClassAttributeToolProvider() throws Exception {
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD);
-        validate();
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJarWithMainClass();
-        testMainClassAttribute();
-        testMainClassAttributeToolProvider();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/MainClassErrorTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image with no main class arguments and with main-class attribute
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m MainClassErrorTest
- */
-public class MainClassErrorTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar"};
-
-    private static void validate(String output) throws Exception {
-        String[] result = JPackageHelper.splitAndFilter(output);
-        if (result.length != 2) {
-            throw new AssertionError(
-                   "Unexpected number of lines: " + result.length);
-        }
-
-        if (!result[0].trim().contains("main class was not specified")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().startsWith("Advice to fix: ")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeCLI(false, CMD));
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeToolProvider(false, CMD));
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/ModuleMainClassErrorTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image with no main class arguments and with main-class attribute
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m ModuleMainClassErrorTest
- */
-public class ModuleMainClassErrorTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp();
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-
-    private static final String [] CMD1 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello",
-        "--module-path", "input"};
-
-    private static final String [] CMD2 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input"};
-
-    private static void validate(String buildOutput) throws Exception {
-
-        File outfile = new File(appOutput);
-        int retVal = JPackageHelper.execute(outfile, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                   "Test application exited with error: ");
-        }
-
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloModule();
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeCLI(false, CMD1);
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(false, CMD1);
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeCLI(true, CMD2));
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        validate(JPackageHelper.executeToolProvider(true, CMD2));
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/ModulePathTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
- /*
- * @test
- * @summary jpackage create image module test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @build Base
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m ModulePathTest
- */
-
-import java.io.File;
-
-public class ModulePathTest {
-    private static final String OUTPUT = "output";
-
-    private static final String [] CMD1 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-    };
-
-    private static final String [] CMD2 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input" + File.pathSeparator + "input-other",
-    };
-
-    private static final String [] CMD3 = {
-        "--package-type", "app-image",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--module", "com.hello/com.hello.Hello",
-        "--module-path", "input",
-        "--module-path", "input-other",
-    };
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloModule();
-        Base.testCreateAppImageToolProvider(CMD1);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        Base.testCreateAppImageToolProvider(CMD2);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        Base.testCreateAppImageToolProvider(CMD3);
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/ResourceTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-import java.nio.file.Files;
-
-/*
- * @test
- * @summary jpackage create image to verify --icon
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @requires ((os.family == "windows") | (os.family == "mac"))
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m ResourceTest
- */
-public class ResourceTest {
-    private static final String OUTPUT = "output";
-    private static final String app = JPackagePath.getApp("icon");
-    private static final String appOutput = JPackagePath.getAppOutputFile();
-    private static final String resourceDir =
-            JPackagePath.getTestSrcRoot() + File.separator + "resources";
-
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--name", "icon",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-        "--resource-dir", resourceDir,
-        "--dest", OUTPUT};
-
-    private static void validateResult(String[] result) throws Exception {
-        if (result.length != 2) {
-            throw new AssertionError(
-                   "Unexpected number of lines: " + result.length);
-        }
-
-        if (!result[0].trim().equals("jpackage test application")) {
-            throw new AssertionError("Unexpected result[0]: " + result[0]);
-        }
-
-        if (!result[1].trim().equals("args.length: 0")) {
-            throw new AssertionError("Unexpected result[1]: " + result[1]);
-        }
-    }
-
-    private static void validate() throws Exception {
-        int retVal = JPackageHelper.execute(null, app);
-        if (retVal != 0) {
-            throw new AssertionError(
-                   "Test application exited with error: " + retVal);
-        }
-
-        File outfile = new File(appOutput);
-        if (!outfile.exists()) {
-            throw new AssertionError(appOutput + " was not created");
-        }
-
-        String output = Files.readString(outfile.toPath());
-        String[] result = output.split("\n");
-        validateResult(result);
-    }
-
-    private static void validateIcon() throws Exception {
-        File origIcon = new File(getIconPath());
-        File icon = new File(JPackagePath.getAppIcon("icon"));
-        if (origIcon.length() != icon.length()) {
-            System.err.println("file: " + origIcon + " - origIcon.length(): "
-                    + origIcon.length());
-            System.err.println("file: " + icon + " - icon.length(): "
-                    + icon.length());
-            throw new AssertionError("Icons size does not match");
-        }
-    }
-
-    private static void testIcon() throws Exception {
-        JPackageHelper.executeCLI(true, CMD);
-        validate();
-        validateIcon();
-    }
-
-    private static void testIconToolProvider() throws Exception {
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.executeToolProvider(true, CMD);
-        validate();
-        validateIcon();
-    }
-
-    private static String getResourcenPath() {
-        return JPackagePath.getTestSrcRoot() + File.separator + "resources";
-    }
-
-    private static String getIconPath() {
-        String ext = ".ico";
-        if (JPackageHelper.isOSX()) {
-            ext = ".icns";
-        } else if (JPackageHelper.isLinux()) {
-            ext = ".png";
-        }
-        return resourceDir + File.separator + "icon" + ext;
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-        testIcon();
-        testIconToolProvider();
-    }
-}
--- a/test/jdk/tools/jpackage/share/SimplePackageTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/SimplePackageTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -23,6 +23,7 @@
 
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Simple platform specific packaging test. Output of the test should be
@@ -43,16 +44,17 @@
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m SimplePackageTest
+ * @compile SimplePackageTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=SimplePackageTest
  */
 public class SimplePackageTest {
 
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            new PackageTest()
-            .configureHelloApp()
-            .addBundleDesktopIntegrationVerifier(false)
-            .run();
-        });
+    @Test
+    public static void test() {
+        new PackageTest()
+        .configureHelloApp()
+        .addBundleDesktopIntegrationVerifier(false)
+        .run();
     }
 }
--- a/test/jdk/tools/jpackage/share/VerboseTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @summary jpackage create image verbose test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m VerboseTest
- */
-public class VerboseTest {
-    private static final String OUTPUT = "output";
-    private static final String[] CMD = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-    };
-
-    private static final String[] CMD_VERBOSE = {
-        "--package-type", "app-image",
-        "--input", "input",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-        "--verbose"};
-
-    private static void validate(String result, String resultVerbose)
-            throws Exception {
-        String[] r = result.split("\n");
-        String[] rv = resultVerbose.split("\n");
-
-        if (r.length >= rv.length) {
-            System.err.println("r.length: " + r.length);
-            System.err.println(result);
-            System.err.println("rv.length: " + rv.length);
-            System.err.println(resultVerbose);
-            throw new AssertionError(
-                    "non-verbose output is less or equal to verbose output");
-        }
-    }
-
-    private static void testCreateAppImage() throws Exception {
-        String result = JPackageHelper.executeCLI(true, CMD);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        String resultVerbose = JPackageHelper.executeCLI(true, CMD_VERBOSE);
-        validate(result, resultVerbose);
-    }
-
-    private static void testCreateAppImageToolProvider() throws Exception {
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        String result = JPackageHelper.executeToolProvider(true, CMD);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        String resultVerbose =
-                JPackageHelper.executeToolProvider(true, CMD_VERBOSE);
-        validate(result, resultVerbose);
-    }
-
-    public static void main(String[] args) throws Exception {
-        JPackageHelper.createHelloImageJar();
-        testCreateAppImage();
-        testCreateAppImageToolProvider();
-    }
-
-}
--- a/test/jdk/tools/jpackage/share/WithSpaceTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.File;
-
- /*
- * @test
- * @summary jpackage create image test
- * @library ../helpers
- * @build JPackageHelper
- * @build JPackagePath
- * @build Base
- * @modules jdk.jpackage
- * @run main/othervm -Xmx512m WithSpaceTest
- */
-public class WithSpaceTest {
-    private static final String OUTPUT = "output";
-
-    private static final String [] CMD1 = {
-        "--package-type", "app-image",
-        "--input", "input dir",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "hello.jar",
-        "--main-class", "Hello",
-    };
-
-    private static final String [] CMD2 = {
-        "--package-type", "app-image",
-        "--input", "input dir2",
-        "--dest", OUTPUT,
-        "--name", "test",
-        "--main-jar", "sub dir/hello.jar",
-        "--main-class", "Hello",
-    };
-
-    public static void main(String[] args) throws Exception {
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.createHelloImageJar("input dir");
-        Base.testCreateAppImage(CMD1);
-
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-        JPackageHelper.createHelloImageJar(
-                "input dir2" + File.separator + "sub dir");
-
-        Base.testCreateAppImageToolProvider(CMD2);
-        JPackageHelper.deleteOutputFolder(OUTPUT);
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jpackage.tests;
+
+import java.util.Collection;
+import java.util.List;
+import jdk.jpackage.test.Annotations.Parameters;
+import jdk.jpackage.test.Annotations.Test;
+import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.TKit;
+
+/*
+ * @test
+ * @summary jpackage application version testing
+ * @library ../../../../helpers
+ * @build jdk.jpackage.test.*
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile AppVersionTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=jdk.jpackage.tests.AppVersionTest
+ */
+
+public final class AppVersionTest {
+
+    @Parameters
+    public static Collection input() {
+        return List.of(new Object[][]{
+            // Default jpackage version
+            {"1.0", "Hello", null},
+            {"1.0", "com.other/com.other.Hello", null},
+            // Version should be picked from --app-version
+            {"3.1", "Hello", new String[]{"--app-version", "3.1"}},
+            {"3.2", "com.other/com.other.Hello", new String[]{"--app-version",
+                "3.2"}},
+            // Version should be picked from the last --app-version
+            {"3.3", "Hello", new String[]{"--app-version", "4", "--app-version",
+                "3.3"}},
+            {"7.8", "com.other/com.other.Hello", new String[]{"--app-version",
+                "4", "--app-version", "7.8"}},
+            // Pick version from jar
+            {"3.10.17", "com.other/com.other.Hello@3.10.17", null},
+            // Ignore version in jar if --app-version given
+            {"7.5.81", "com.other/com.other.Hello@3.10.17", new String[]{
+                "--app-version", "7.5.81"}}
+        });
+    }
+
+    public AppVersionTest(String expectedVersion, String javaAppDesc,
+            String[] jpackageArgs) {
+        this.expectedVersion = expectedVersion;
+
+        cmd = JPackageCommand.helloAppImage(javaAppDesc);
+        if (jpackageArgs != null) {
+            cmd.addArguments(jpackageArgs);
+        }
+    }
+
+    @Test
+    public void test() {
+        cmd.executeAndAssertHelloAppImageCreated();
+        String actualVersion = cmd.readLaunherCfgFile().getValue("Application",
+                "app.version");
+        TKit.assertEquals(expectedVersion, actualVersion,
+                "Check application version");
+    }
+
+    private final String expectedVersion;
+    private final JPackageCommand cmd;
+}
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -28,13 +28,12 @@
 import java.nio.file.Path;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import jdk.jpackage.test.Executor;
-import jdk.jpackage.test.JPackageCommand;
-import jdk.jpackage.test.TKit;
-import jdk.jpackage.test.JavaTool;
-import jdk.jpackage.test.HelloApp;
+import java.util.stream.Stream;
+import jdk.jpackage.test.*;
 import jdk.jpackage.test.Annotations.*;
 
 /*
@@ -42,13 +41,13 @@
  * @summary jpackage basic testing
  * @library ../../../../helpers
  * @build jdk.jpackage.test.*
- * @modules jdk.jpackage
+ * @modules jdk.jpackage/jdk.jpackage.internal
  * @compile BasicTest.java
  * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
  *  --jpt-run=jdk.jpackage.tests.BasicTest
  */
 
-public class BasicTest {
+public final class BasicTest {
     @Test
     public void testNoArgs() {
         List<String> output = JPackageCommand.filterOutput(
@@ -69,11 +68,101 @@
     }
 
     @Test
+    public void testHelp() {
+        List<String> hOutput = getJPackageToolProvider()
+                .addArgument("-h").executeAndGetOutput();
+        List<String> helpOutput = getJPackageToolProvider()
+                .addArgument("--help").executeAndGetOutput();
+
+        TKit.assertStringListEquals(hOutput, helpOutput,
+                "Check -h and --help parameters produce the same output");
+
+        final String windowsPrefix = "--win-";
+        final String linuxPrefix = "--linux-";
+        final String osxPrefix = "--mac-";
+
+        final String expectedPrefix;
+        final List<String> unexpectedPrefixes;
+
+        if (TKit.isWindows()) {
+            expectedPrefix = windowsPrefix;
+            unexpectedPrefixes = List.of(osxPrefix, linuxPrefix);
+        } else if (TKit.isLinux()) {
+            expectedPrefix = linuxPrefix;
+            unexpectedPrefixes = List.of(windowsPrefix, osxPrefix);
+        } else if (TKit.isOSX()) {
+            expectedPrefix = osxPrefix;
+            unexpectedPrefixes = List.of(linuxPrefix,  windowsPrefix);
+        } else {
+            throw TKit.throwUnknownPlatformError();
+        }
+
+        Function<String, Predicate<String>> createPattern = (prefix) -> {
+            return Pattern.compile("^  " + prefix).asPredicate();
+        };
+
+        Function<List<String>, Long> countStrings = (prefixes) -> {
+            return hOutput.stream().filter(
+                    prefixes.stream().map(createPattern).reduce(x -> false,
+                            Predicate::or)).peek(TKit::trace).count();
+        };
+
+        TKit.trace("Check parameters in help text");
+        TKit.assertNotEquals(0, countStrings.apply(List.of(expectedPrefix)),
+                "Check help text contains plaform specific parameters");
+        TKit.assertEquals(0, countStrings.apply(unexpectedPrefixes),
+                "Check help text doesn't contain unexpected parameters");
+    }
+
+    @Test
+    public void testVerbose() {
+        JPackageCommand cmd = JPackageCommand.helloAppImage()
+                .setFakeRuntime().executePrerequisiteActions();
+
+        List<String> expectedVerboseOutputStrings = new ArrayList<>();
+        expectedVerboseOutputStrings.add("Creating app package:");
+        if (TKit.isWindows()) {
+            expectedVerboseOutputStrings.add("Result application bundle:");
+            expectedVerboseOutputStrings.add(
+                    "Succeeded in building Windows Application Image package");
+        } else if (TKit.isLinux()) {
+            expectedVerboseOutputStrings.add(
+                    "Succeeded in building Linux Application Image package");
+        } else if (TKit.isOSX()) {
+            expectedVerboseOutputStrings.add("Preparing Info.plist:");
+            expectedVerboseOutputStrings.add(
+                    "Succeeded in building Mac Application Image package");
+        } else {
+            TKit.throwUnknownPlatformError();
+        }
+
+        TKit.deleteDirectoryContentsRecursive(cmd.outputDir());
+        List<String> nonVerboseOutput = cmd.createExecutor().executeAndGetOutput();
+
+        TKit.deleteDirectoryContentsRecursive(cmd.outputDir());
+        List<String> verboseOutput = cmd.createExecutor().addArgument(
+                "--verbose").executeAndGetOutput();
+
+        TKit.assertTrue(nonVerboseOutput.size() < verboseOutput.size(),
+                "Check verbose output is longer than regular");
+
+        expectedVerboseOutputStrings.forEach(str -> {
+            TKit.assertTextStream(str).label("regular output")
+                    .predicate(String::contains).negate()
+                    .apply(nonVerboseOutput.stream());
+        });
+
+        expectedVerboseOutputStrings.forEach(str -> {
+            TKit.assertTextStream(str).label("verbose output")
+                    .apply(verboseOutput.stream());
+        });
+    }
+
+    @Test
     public void testNoName() {
         final String mainClassName = "Greetings";
 
-        JPackageCommand cmd = new JPackageCommand()
-                .helloAppImage(mainClassName)
+        JPackageCommand cmd = JPackageCommand.helloAppImage(mainClassName)
                 .removeArgumentWithValue("--name");
 
         Path expectedImageDir = cmd.outputDir().resolve(mainClassName);
@@ -84,51 +173,60 @@
 
         cmd.executeAndAssertHelloAppImageCreated();
         TKit.assertEquals(expectedImageDir.toAbsolutePath().normalize().toString(),
-                cmd.appImage().toAbsolutePath().normalize().toString(),
+                cmd.outputBundle().toAbsolutePath().normalize().toString(),
                 String.format(
                         "Check [%s] directory is filled with application image data",
                         expectedImageDir));
     }
 
     @Test
-    public void testApp() {
-        new JPackageCommand()
-        .helloAppImage()
+     // Regular app
+    @Parameter("Hello")
+    // Modular app
+    @Parameter("com.other/com.other.Hello")
+    public void testApp(String javaAppDesc) {
+        JPackageCommand.helloAppImage(javaAppDesc)
         .executeAndAssertHelloAppImageCreated();
     }
 
     @Test
-    public void testModularApp() {
-        new JPackageCommand()
-        .helloAppImage("com.other/com.other.Hello")
+    public void testWhitespaceInPaths() {
+        JPackageCommand.helloAppImage("a/b c.jar:Hello")
+        .setArgumentValue("--input", TKit.workDir().resolve("The quick brown fox"))
+        .setArgumentValue("--dest", TKit.workDir().resolve("jumps over the lazy dog"))
         .executeAndAssertHelloAppImageCreated();
     }
 
     @Test
     @Parameter("ALL-MODULE-PATH")
     @Parameter("ALL-DEFAULT")
-    @Parameter("jdk.desktop,jdk.jartool")
-    public void testAddModules(String addModulesArg) {
-        JPackageCommand cmd = new JPackageCommand()
-            .helloAppImage("com.other/com.other.Hello");
-        if (!addModulesArg.isEmpty()) {
-            cmd.addArguments("--add-modules", addModulesArg);
-        }
+    @Parameter("java.desktop")
+    @Parameter("java.desktop,jdk.jartool")
+    @Parameter({ "java.desktop", "jdk.jartool" })
+    public void testAddModules(String... addModulesArg) {
+        JPackageCommand cmd = JPackageCommand
+                .helloAppImage("goodbye.jar:com.other/com.other.Hello");
+        Stream.of(addModulesArg).map(v -> Stream.of("--add-modules", v)).flatMap(
+                s -> s).forEachOrdered(cmd::addArgument);
         cmd.executeAndAssertHelloAppImageCreated();
     }
 
     /**
      * Test --temp option. Doesn't make much sense for app image as temporary
-     * directory is used only on Windows.
+     * directory is used only on Windows. Test it in packaging mode.
      * @throws IOException
      */
-//    @Test
+    @Test
     public void testTemp() throws IOException {
-        JPackageCommand cmd = new JPackageCommand().helloAppImage();
+        JPackageCommand cmd = JPackageCommand.helloAppImage()
+                .removeArgumentWithValue("--package-type")
+                .addArguments("--verbose")
+                .setFakeRuntime();
+
         TKit.withTempDirectory("temp-root", tempDir -> {
             cmd.addArguments("--temp", tempDir);
 
-            cmd.executeAndAssertHelloAppImageCreated();
+            cmd.execute().assertExitCodeIsZero();
 
             // Check jpackage actually used the supplied directory.
             TKit.assertNotEquals(0, tempDir.toFile().list().length,
@@ -144,7 +242,7 @@
 
     @Test
     public void testAtFile() throws IOException {
-        JPackageCommand cmd = new JPackageCommand().helloAppImage();
+        JPackageCommand cmd = JPackageCommand.helloAppImage();
 
         // Init options file with the list of options configured
         // for JPackageCommand instance.
@@ -211,8 +309,7 @@
             cmd.addArguments("--runtime-image", runtimeDir);
             cmd.executeAndAssertHelloAppImageCreated();
 
-            final Path appImageRuntimePath = cmd.appImage().resolve(
-                    cmd.appRuntimeDirectoryInAppImage());
+            final Path appImageRuntimePath = cmd.appRuntimeDirectory();
 
             //
             // This is an overkill to list modules in jlink output as we have
@@ -251,8 +348,6 @@
     }
 
     private static Executor getToolProvider(JavaTool tool) {
-        return new Executor()
-                .dumpOutput().saveOutput()
-                .setToolProvider(tool.asToolProvider());
+        return new Executor().dumpOutput().saveOutput().setToolProvider(tool);
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jpackage.tests;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.nio.file.Path;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import jdk.jpackage.test.Annotations.Parameters;
+import jdk.jpackage.test.Annotations.Test;
+import jdk.jpackage.test.*;
+import jdk.jpackage.test.Functional.ThrowingConsumer;
+import static jdk.jpackage.tests.MainClassTest.Script.MainClassType.*;
+
+
+/*
+ * @test
+ * @summary test different settings of main class name for jpackage
+ * @library ../../../../helpers
+ * @build jdk.jpackage.test.*
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile MainClassTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=jdk.jpackage.tests.MainClassTest
+ *  --jpt-space-subst=_
+ *  --jpt-exclude=modular=y;_main-class=n;_jar-main-class=b;_jlink=y
+ *  --jpt-exclude=modular=y;_main-class=n;_jar-main-class=n;_jlink=n
+ */
+
+public final class MainClassTest {
+
+    static final class Script {
+        Script() {
+            appDesc = JavaAppDesc.parse("test.Hello");
+        }
+
+        Script modular(boolean v) {
+            appDesc.setModuleName(v ? null : "com.other");
+            return this;
+        }
+
+        Script withJLink(boolean v) {
+            withJLink = v;
+            return this;
+        }
+
+        Script withMainClass(MainClassType v) {
+            mainClass = v;
+            return this;
+        }
+
+        Script withJarMainClass(MainClassType v) {
+            appDesc.setJarWithMainClass(v != NotSet);
+            jarMainClass = v;
+            return this;
+        }
+
+        Script expectedErrorMessage(String v) {
+            expectedErrorMessage = v;
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return Stream.of(
+                    format("modular", appDesc.moduleName() != null ? 'y' : 'n'),
+                    format("main-class", mainClass),
+                    format("jar-main-class", jarMainClass),
+                    format("jlink", withJLink ? 'y' : 'n'),
+                    format("error", expectedErrorMessage)
+            ).filter(Objects::nonNull).collect(Collectors.joining("; "));
+        }
+
+        private static String format(String key, Object value) {
+            if (value == null) {
+                return null;
+            }
+            return String.join("=", key, value.toString());
+        }
+
+        enum MainClassType {
+            NotSet("n"),
+            SetWrong("b"),
+            SetRight("y");
+
+            MainClassType(String label) {
+                this.label = label;
+            }
+
+            @Override
+            public String toString() {
+                return label;
+            }
+
+            private final String label;
+        };
+
+        private JavaAppDesc appDesc;
+        private boolean withJLink;
+        private MainClassType mainClass;
+        private MainClassType jarMainClass;
+        private String expectedErrorMessage;
+    }
+
+    public MainClassTest(Script script) {
+        this.script = script;
+
+        nonExistingMainClass = Stream.of(
+                script.appDesc.packageName(), "ThereIsNoSuchClass").filter(
+                Objects::nonNull).collect(Collectors.joining("."));
+
+        cmd = JPackageCommand.helloAppImage(script.appDesc);
+        if (!script.withJLink) {
+            cmd.setFakeRuntime();
+        }
+
+        final String moduleName = script.appDesc.moduleName();
+        switch (script.mainClass) {
+            case NotSet:
+                if (moduleName != null) {
+                    // Don't specify class name, only module name.
+                    cmd.setArgumentValue("--module", moduleName);
+                } else {
+                    cmd.removeArgumentWithValue("--main-class");
+                }
+                break;
+
+            case SetWrong:
+                if (moduleName != null) {
+                    cmd.setArgumentValue("--module",
+                            String.join("/", moduleName, nonExistingMainClass));
+                } else {
+                    cmd.setArgumentValue("--main-class", nonExistingMainClass);
+                }
+        }
+    }
+
+    @Parameters
+    public static Collection scripts() {
+        final var withMainClass = Set.of(SetWrong, SetRight);
+
+        List<Script[]> scripts = new ArrayList<>();
+        for (var withJLink : List.of(true, false)) {
+            for (var modular : List.of(true, false)) {
+                for (var mainClass : Script.MainClassType.values()) {
+                    for (var jarMainClass : Script.MainClassType.values()) {
+                        if (!withJLink && (jarMainClass == SetWrong || mainClass
+                                == SetWrong)) {
+                            // Without runtime can't run app to verify it will fail, so
+                            // there is no point in such testing.
+                            continue;
+                        }
+
+                        Script script = new Script()
+                            .modular(modular)
+                            .withJLink(withJLink)
+                            .withMainClass(mainClass)
+                            .withJarMainClass(jarMainClass);
+
+                        if (withMainClass.contains(jarMainClass)
+                                || withMainClass.contains(mainClass)) {
+                        } else if (modular) {
+                            script.expectedErrorMessage(
+                                    "A main class was not specified nor was one found in the jar");
+                        } else {
+                            script.expectedErrorMessage(
+                                    "Error: Main application class is missing");
+                        }
+
+                        scripts.add(new Script[]{script});
+                    }
+                }
+            }
+        }
+        return scripts;
+    }
+
+    @Test
+    public void test() throws IOException {
+        if (script.jarMainClass == SetWrong) {
+            initJarWithWrongMainClass();
+        }
+
+        if (script.expectedErrorMessage != null) {
+            // This is the case when main class is not found nor in jar
+            // file nor on command line.
+            List<String> output = cmd
+                    .saveConsoleOutput(true)
+                    .execute()
+                    .assertExitCodeIs(1)
+                    .getOutput();
+            TKit.assertTextStream(script.expectedErrorMessage).apply(output.stream());
+            return;
+        }
+
+        // Get here only if main class is specified.
+        boolean appShouldSucceed = false;
+
+        // Should succeed if valid main class is set on the command line.
+        appShouldSucceed |= (script.mainClass == SetRight);
+
+        // Should succeed if main class is not set on the command line but set
+        // to valid value in the jar.
+        appShouldSucceed |= (script.mainClass == NotSet && script.jarMainClass == SetRight);
+
+        if (appShouldSucceed) {
+            cmd.executeAndAssertHelloAppImageCreated();
+        } else {
+            cmd.executeAndAssertImageCreated();
+            if (!cmd.isFakeRuntime(String.format("Not running [%s]",
+                    cmd.appLauncherPath()))) {
+                List<String> output = new Executor()
+                    .setDirectory(cmd.outputDir())
+                    .setExecutable(cmd.appLauncherPath())
+                    .dumpOutput().saveOutput()
+                    .execute().assertExitCodeIs(1).getOutput();
+                TKit.assertTextStream(String.format(
+                        "Error: Could not find or load main class %s",
+                        nonExistingMainClass)).apply(output.stream());
+            }
+        }
+    }
+
+    private void initJarWithWrongMainClass() {
+        // Call JPackageCommand.executePrerequisiteActions() to build app's jar.
+        // executePrerequisiteActions() is called by JPackageCommand instance
+        // only once.
+        cmd.executePrerequisiteActions();
+
+        Path jarFile;
+        if (script.appDesc.moduleName() != null) {
+            jarFile = Path.of(cmd.getArgumentValue("--module-path"),
+                    script.appDesc.jarFileName());
+        } else {
+            jarFile = cmd.inputDir().resolve(cmd.getArgumentValue("--main-jar"));
+        }
+
+        // Create jar file with the main class attribute in manifest set to
+        // non-existing class.
+        TKit.withTempDirectory("repack-jar", workDir -> {
+            Path manifestFile = workDir.resolve("META-INF/MANIFEST.MF");
+            try (var jar = new JarFile(jarFile.toFile())) {
+                jar.stream()
+                .filter(Predicate.not(JarEntry::isDirectory))
+                .sequential().forEachOrdered(ThrowingConsumer.toConsumer(
+                    jarEntry -> {
+                        try (var in = jar.getInputStream(jarEntry)) {
+                            Path fileName = workDir.resolve(jarEntry.getName());
+                            Files.createDirectories(fileName.getParent());
+                            Files.copy(in, fileName);
+                        }
+                    }));
+            }
+
+            Files.delete(jarFile);
+
+            // Adjust manifest.
+            TKit.createTextFile(manifestFile, Files.readAllLines(
+                    manifestFile).stream().map(line -> line.replace(
+                    script.appDesc.className(), nonExistingMainClass)));
+
+            new Executor().setToolProvider(JavaTool.JAR)
+            .addArguments("-c", "-M", "-f", jarFile.toString())
+            .addArguments("-C", workDir.toString(), ".")
+            .execute().assertExitCodeIsZero();
+        });
+    }
+
+    private final JPackageCommand cmd;
+    private final Script script;
+    private final String nonExistingMainClass;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jpackage.tests;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import jdk.jpackage.test.Annotations.Parameters;
+import jdk.jpackage.test.Annotations.Test;
+import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.TKit;
+
+
+/*
+ * @test
+ * @summary jpackage with --module-path testing
+ * @library ../../../../helpers
+ * @build jdk.jpackage.test.*
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @compile ModulePathTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=jdk.jpackage.tests.ModulePathTest
+ */
+
+public final class ModulePathTest {
+
+    @Parameters
+    public static Collection data() {
+        return List.of(new String[][]{
+            {GOOD_PATH, EMPTY_DIR, NON_EXISTING_DIR},
+            {EMPTY_DIR, NON_EXISTING_DIR, GOOD_PATH},
+            {GOOD_PATH + "/a/b/c/d", GOOD_PATH},
+            {String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR,
+                GOOD_PATH)},
+            {String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR),
+                String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR,
+                GOOD_PATH)},
+            {},
+            {EMPTY_DIR}
+        });
+    }
+
+    public ModulePathTest(String... modulePathArgs) {
+        this.modulePathArgs = List.of(modulePathArgs);
+    }
+
+    @Test
+    public void test() {
+        final String moduleName = "com.foo";
+        JPackageCommand cmd = JPackageCommand.helloAppImage(
+                "benvenuto.jar:" + moduleName + "/com.foo.Hello");
+        // Build app jar file.
+        cmd.executePrerequisiteActions();
+
+        // Ignore runtime that can be set for all tests. Usually if default
+        // runtime is set, it is fake one to save time on running jlink and
+        // copying megabytes of data from Java home to application image.
+        // We need proper runtime for this test.
+        cmd.ignoreDefaultRuntime(true);
+
+        // --module-path should be set in JPackageCommand.helloAppImage call
+        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 -> {
+                    });
+
+            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()));
+
+            if (withGoodPath) {
+                cmd.executeAndAssertHelloAppImageCreated();
+            } 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);
+                }
+
+                List<String> output = cmd
+                        .saveConsoleOutput(true)
+                        .execute()
+                        .assertExitCodeIs(1)
+                        .getOutput();
+                TKit.assertTextStream(expectedErrorMessage).apply(output.stream());
+            }
+        });
+    }
+
+    private final List<String> modulePathArgs;
+
+    private final static String GOOD_PATH = "@GoodPath@";
+    private final static String EMPTY_DIR = "@EmptyDir@";
+    private final static String NON_EXISTING_DIR = "@NonExistingDir@";
+}
--- a/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -36,7 +36,7 @@
  * @library ../helpers
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
- * @modules jdk.jpackage
+ * @modules jdk.jpackage/jdk.jpackage.internal
  * @compile WinConsoleTest.java
  *
  * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
@@ -57,8 +57,7 @@
             cmd.removeArgument("--win-console");
         }
         cmd.executeAndAssertHelloAppImageCreated();
-        checkSubsystem(cmd.appImage().resolve(cmd.launcherPathInAppImage()),
-                withWinConsole);
+        checkSubsystem(cmd.appLauncherPath(), withWinConsole);
     }
 
     private static void checkSubsystem(Path path, boolean isConsole) throws
--- a/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --win-menu and --win-menu-group parameters.
@@ -42,18 +43,19 @@
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m WinMenuGroupTest
+ * @compile WinMenuGroupTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinMenuGroupTest
  */
 
 public class WinMenuGroupTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.WINDOWS)
-            .configureHelloApp()
-            .addInitializer(cmd -> cmd.addArguments(
-                    "--win-menu", "--win-menu-group", "WinMenuGroupTest_MenuGroup"))
-            .run();
-        });
+    @Test
+    public static void test() {
+        new PackageTest()
+        .forTypes(PackageType.WINDOWS)
+        .configureHelloApp()
+        .addInitializer(cmd -> cmd.addArguments(
+                "--win-menu", "--win-menu-group", "WinMenuGroupTest_MenuGroup"))
+        .run();
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinMenuTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinMenuTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --win-menu parameter. Output of the test should be WinMenuTest-1.0.exe
@@ -39,16 +40,17 @@
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m WinMenuTest
+ * @compile WinMenuTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinMenuTest
  */
 
 public class WinMenuTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.WINDOWS)
-            .configureHelloApp()
-            .addInitializer(cmd -> cmd.addArgument("--win-menu")).run();
-        });
+    @Test
+    public static void test() {
+        new PackageTest()
+        .forTypes(PackageType.WINDOWS)
+        .configureHelloApp()
+        .addInitializer(cmd -> cmd.addArgument("--win-menu")).run();
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --win-per-user-install, --win-menu, --win-menu-group parameters.
@@ -41,20 +42,21 @@
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m WinPerUserInstallTest
+ * @compile WinPerUserInstallTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinPerUserInstallTest
  */
 
 public class WinPerUserInstallTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.WINDOWS)
-            .configureHelloApp()
-            .addInitializer(cmd -> cmd.addArguments(
-                    "--win-menu",
-                    "--win-menu-group", "WinPerUserInstallTest_MenuGroup",
-                    "--win-per-user-install"))
-            .run();
-        });
+    @Test
+    public static void test() {
+        new PackageTest()
+        .forTypes(PackageType.WINDOWS)
+        .configureHelloApp()
+        .addInitializer(cmd -> cmd.addArguments(
+                "--win-menu",
+                "--win-menu-group", "WinPerUserInstallTest_MenuGroup",
+                "--win-per-user-install"))
+        .run();
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinShortcutTest.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinShortcutTest.java	Wed Oct 16 10:32:08 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.Annotations.Test;
 
 /**
  * Test --win-shortcut parameter. Output of the test should be
@@ -40,17 +41,18 @@
  * @build jdk.jpackage.test.*
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m WinShortcutTest
+ * @compile WinShortcutTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=WinShortcutTest
  */
 
 public class WinShortcutTest {
-    public static void main(String[] args) {
-        TKit.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.WINDOWS)
-            .configureHelloApp()
-            .addInitializer(cmd -> cmd.addArgument("--win-shortcut"))
-            .run();
-        });
+    @Test
+    public static void test() {
+        new PackageTest()
+        .forTypes(PackageType.WINDOWS)
+        .configureHelloApp()
+        .addInitializer(cmd -> cmd.addArgument("--win-shortcut"))
+        .run();
     }
 }