test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java
author herrick
Wed, 16 Oct 2019 10:32:08 -0400
branchJDK-8200758-branch
changeset 58648 3bf53ffa9ae7
parent 58416 f09bf58c1f17
child 58671 3b578a5976df
permissions -rw-r--r--
8232279 : Improve test helpers #2 Submitted-by: asemenyuk Reviewed-by: aherrick, almatvee

/*
 * 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.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.*;
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
 * anything. The simplest is to compile test application and pack in a jar for
 * use on jpackage command line.
 */
public final class JPackageCommand extends CommandArguments<JPackageCommand> {

    public JPackageCommand() {
        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(this);
        reply.immutable = true;
        return reply;
    }

    public JPackageCommand setArgumentValue(String argName, String newValue) {
        verifyMutable();

        String prevArg = null;
        ListIterator<String> it = args.listIterator();
        while (it.hasNext()) {
            String value = it.next();
            if (prevArg != null && prevArg.equals(argName)) {
                if (newValue != null) {
                    it.set(newValue);
                } else {
                    it.remove();
                    it.previous();
                    it.remove();
                }
                return this;
            }
            prevArg = value;
        }

        if (newValue != null) {
            addArguments(argName, newValue);
        }

        return this;
    }

    public JPackageCommand setArgumentValue(String argName, Path newValue) {
        return setArgumentValue(argName, newValue.toString());
    }

    public JPackageCommand removeArgumentWithValue(String argName) {
        return setArgumentValue(argName, (String)null);
    }

    public JPackageCommand removeArgument(String argName) {
        args = args.stream().filter(arg -> !arg.equals(argName)).collect(
                Collectors.toList());
        return this;
    }

    public boolean hasArgument(String argName) {
        return args.contains(argName);
    }

    public <T> T getArgumentValue(String argName,
            Function<JPackageCommand, T> defaultValueSupplier,
            Function<String, T> stringConverter) {
        String prevArg = null;
        for (String arg : args) {
            if (prevArg != null && prevArg.equals(argName)) {
                return stringConverter.apply(arg);
            }
            prevArg = arg;
        }
        if (defaultValueSupplier != null) {
            return defaultValueSupplier.apply(this);
        }
        return null;
    }

    public String getArgumentValue(String argName,
            Function<JPackageCommand, String> defaultValueSupplier) {
        return getArgumentValue(argName, defaultValueSupplier, v -> v);
    }

    public <T> T getArgumentValue(String argName,
            Supplier<T> defaultValueSupplier,
            Function<String, T> stringConverter) {
        return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(),
                stringConverter);
    }

    public String getArgumentValue(String argName,
            Supplier<String> defaultValueSupplier) {
        return getArgumentValue(argName, defaultValueSupplier, v -> v);
    }

    public String getArgumentValue(String argName) {
        return getArgumentValue(argName, (Supplier<String>)null);
    }

    public String[] getAllArgumentValues(String argName) {
        List<String> values = new ArrayList<>();
        String prevArg = null;
        for (String arg : args) {
            if (prevArg != null && prevArg.equals(argName)) {
                values.add(arg);
            }
            prevArg = arg;
        }
        return values.toArray(String[]::new);
    }

    public JPackageCommand addArguments(String name, Path value) {
        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
        // this is operator's fault.
        return getArgumentValue("--package-type",
                () -> {
                    throw new IllegalStateException("Package type not set");
                }, PACKAGE_TYPES::get);
    }

    public Path outputDir() {
        return getArgumentValue("--dest", () -> Path.of("."), Path::of);
    }

    public Path inputDir() {
        return getArgumentValue("--input", () -> null, Path::of);
    }

    public String version() {
        return getArgumentValue("--app-version", () -> "1.0");
    }

    public String name() {
        return getArgumentValue("--name", () -> getArgumentValue("--main-class"));
    }

    public boolean isRuntime() {
        return  hasArgument("--runtime-image")
                && !hasArgument("--main-jar")
                && !hasArgument("--module")
                && !hasArgument("--app-image");
    }

    public JPackageCommand setDefaultInputOutput() {
        addArguments("--input", TKit.defaultInputDir());
        addArguments("--dest", TKit.defaultOutputDir());
        return this;
    }

    public JPackageCommand setFakeRuntime() {
        verifyMutable();

        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()) {
                // Needed to make WindowsAppBundler happy as it copies MSVC dlls
                // from `bin` directory.
                // Need to make the code in rpm spec happy as it assumes there is
                // always something in application image.
                fakeRuntimeDir.resolve("bin").toFile().mkdir();
            }

            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.
            createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk")));

            cmd.addArguments("--runtime-image", fakeRuntimeDir);
        });

        return this;
    }

    JPackageCommand addAction(ThrowingConsumer<JPackageCommand> action) {
        verifyMutable();
        actions.add(ThrowingConsumer.toConsumer(action));
        return this;
    }

    /**
     * Shorthand for {@code helloAppImage(null)}.
     */
    public static JPackageCommand helloAppImage() {
        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
     * #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);
        new HelloApp(javaAppDesc).addTo(cmd);
        return cmd;
    }

    public JPackageCommand setPackageType(PackageType type) {
        verifyMutable();
        type.applyTo(this);
        return this;
    }

    JPackageCommand setDefaultAppName() {
        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 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 (TKit.isWindows()) {
            bundleName = WindowsHelper.getBundleName(this);
        } else if (TKit.isOSX()) {
            bundleName = MacHelper.getBundleName(this);
        } else {
            throw TKit.throwUnknownPlatformError();
        }

        return outputDir().resolve(bundleName);
    }

    /**
     * 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() {
        if (isImagePackageType()) {
            return null;
        }

        if (TKit.isLinux()) {
            if (isRuntime()) {
                // Not fancy, but OK.
                return Path.of(getArgumentValue("--install-dir", () -> "/opt"),
                        LinuxHelper.getPackageName(this));
            }

            // Launcher is in "bin" subfolder of the installation directory.
            return appLauncherPath().getParent().getParent();
        }

        if (TKit.isWindows()) {
            return WindowsHelper.getInstallationDirectory(this);
        }

        if (TKit.isOSX()) {
            return MacHelper.getInstallationDirectory(this);
        }

        throw TKit.throwUnknownPlatformError();
    }

    /**
     * Returns path to application's Java runtime.
     * If the command will package Java runtime only, returns correct path to
     * runtime directory.
     *
     * 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 appRuntimeDirectory() {
        return appLayout().runtimeDirectory();
    }

    /**
     * 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();
        }

        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);
    }

    /**
     * Shorthand for {@code appLauncherPath(null)}.
     */
    public Path appLauncherPath() {
        return appLauncherPath(null);
    }

    private void verifyNotRuntime() {
        if (isRuntime()) {
            throw new IllegalArgumentException("Java runtime packaging");
        }
    }

    /**
     * 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");
    }

    public boolean isFakeRuntime(String msg) {
        Path runtimeDir = appRuntimeDirectory();

        final Collection<Path> criticalRuntimeFiles;
        if (TKit.isWindows()) {
            criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES;
        } else if (TKit.isLinux()) {
            criticalRuntimeFiles = LinuxHelper.CRITICAL_RUNTIME_FILES;
        } else if (TKit.isOSX()) {
            criticalRuntimeFiles = MacHelper.CRITICAL_RUNTIME_FILES;
        } else {
            throw TKit.throwUnknownPlatformError();
        }

        if (criticalRuntimeFiles.stream().filter(
                v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse(
                        null) == null) {
            // Fake runtime
            TKit.trace(String.format(
                    "%s because application runtime directory [%s] is incomplete",
                    msg, runtimeDir));
            return true;
        }
        return false;
    }

    public static void useToolProviderByDefault() {
        defaultWithToolProvider = true;
    }

    public static void useExecutableByDefault() {
        defaultWithToolProvider = false;
    }

    public JPackageCommand useToolProvider(boolean v) {
        verifyMutable();
        withToolProvider = v;
        return this;
    }

    public JPackageCommand saveConsoleOutput(boolean v) {
        verifyMutable();
        saveConsoleOutput = v;
        return this;
    }

    public JPackageCommand dumpOutput(boolean v) {
        verifyMutable();
        suppressOutput = !v;
        return this;
    }

    public JPackageCommand ignoreDefaultRuntime(boolean v) {
        verifyMutable();
        ignoreDefaultRuntime = v;
        return this;
    }

    public boolean isWithToolProvider() {
        return Optional.ofNullable(withToolProvider).orElse(
                defaultWithToolProvider);
    }

    public JPackageCommand executePrerequisiteActions() {
        verifyMutable();
        if (!actionsExecuted) {
            actionsExecuted = true;
            if (actions != null) {
                actions.stream().forEach(r -> r.accept(this));
            }
        }
        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 (isImagePackageType()) {
            TKit.deleteDirectoryContentsRecursive(outputDir());
        }

        return new JPackageCommand(this)
                .adjustArgumentsBeforeExecution()
                .createExecutor()
                .execute();
    }

    public JPackageCommand executeAndAssertHelloAppImageCreated() {
        executeAndAssertImageCreated();
        HelloApp.executeLauncherAndVerifyOutput(this);
        return this;
    }

    public JPackageCommand executeAndAssertImageCreated() {
        execute().assertExitCodeIsZero();
        return assertImageCreated();
    }

    public JPackageCommand assertImageCreated() {
        verifyIsOfType(PackageType.IMAGE);
        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 && !ignoreDefaultRuntime) {
            addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
        }

        if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE) {
            addArgument("--verbose");
        }

        return this;
    }

    String getPrintableCommandLine() {
        return new Executor()
                .setExecutable(JavaTool.JPACKAGE)
                .addArguments(args)
                .getPrintableCommandLine();
    }

    public void verifyIsOfType(Collection<PackageType> types) {
        verifyIsOfType(types.toArray(PackageType[]::new));
    }

    public void verifyIsOfType(PackageType ... types) {
        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) {
        return escapeAndJoin(List.of(args));
    }

    public static String escapeAndJoin(List<String> args) {
        Pattern whitespaceRegexp = Pattern.compile("\\s");

        return args.stream().map(v -> {
            String str = v;
            // Escape quotes.
            str = str.replace("\"", "\\\"");
            // Escape backslashes.
            str = str.replace("\\", "\\\\");
            // If value contains whitespace characters, put the value in quotes
            if (whitespaceRegexp.matcher(str).find()) {
                str = "\"" + str + "\"";
            }
            return str;
        }).collect(Collectors.joining(" "));
    }

    public static Path relativePathInRuntime(JavaTool tool) {
        Path path = tool.relativePathInJavaHome();
        if (TKit.isOSX()) {
            path = Path.of("Contents/Home").resolve(path);
        }
        return path;
    }

    public static Stream<String> filterOutput(Stream<String> jpackageOutput) {
        // Skip "WARNING: Using experimental tool jpackage" first line of output
        return jpackageOutput.skip(1);
    }

    public static List<String> filterOutput(List<String> jpackageOutput) {
        return filterOutput(jpackageOutput.stream()).collect(Collectors.toList());
    }

    @Override
    protected boolean isMutable() {
        return !immutable;
    }

    private Boolean withToolProvider;
    private boolean saveConsoleOutput;
    private boolean suppressOutput;
    private boolean ignoreDefaultRuntime;
    private boolean immutable;
    private boolean actionsExecuted;
    private final List<Consumer<JPackageCommand>> actions;
    private static boolean defaultWithToolProvider;

    private final static Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
            () -> {
                Map<String, PackageType> reply = new HashMap<>();
                for (PackageType type : PackageType.values()) {
                    reply.put(type.getName(), type);
                }
                return reply;
            }).get();

    public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
        // Set the property to the path of run-time image to speed up
        // building app images and platform bundles by avoiding running jlink
        // The value of the property will be automativcally appended to
        // jpackage command line if the command line doesn't have
        // `--runtime-image` parameter set.
        String val = TKit.getConfigProperty("runtime-image");
        if (val != null) {
            return Path.of(val);
        }
        return null;
    }).get();
}