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