/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.test;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.jpackage.test.PackageType.LINUX_DEB;
import static jdk.jpackage.test.PackageType.LINUX_RPM;
/**
* Instance of PackageTest is for configuring and running a single jpackage
* command to produce platform specific package bundle.
*
* Provides methods hook up custom configuration of jpackage command and
* verification of the output bundle.
*/
public final class PackageTest {
/**
* Default test configuration for jpackage command. Default jpackage command
* initialization includes:
*
Set --input and --dest parameters.
* Set --name parameter. Value of the parameter is the name of the first
* class with main function found in the callers stack. Defaults can be
* overridden with custom initializers set with subsequent addInitializer()
* function calls.
*/
public PackageTest() {
action = DEFAULT_ACTION;
forTypes();
setJPackageExitCode(0);
handlers = new HashMap<>();
namedInitializers = new HashSet<>();
currentTypes.forEach(v -> handlers.put(v, new Handler(v)));
}
public PackageTest forTypes(PackageType... types) {
Collection newTypes;
if (types == null || types.length == 0) {
newTypes = PackageType.NATIVE;
} else {
newTypes = Set.of(types);
}
currentTypes = newTypes.stream().filter(type -> type.isSupported()).collect(
Collectors.toUnmodifiableSet());
return this;
}
public PackageTest forTypes(Collection types) {
return forTypes(types.toArray(PackageType[]::new));
}
public PackageTest setJPackageExitCode(int v) {
expectedJPackageExitCode = v;
return this;
}
private PackageTest addInitializer(Consumer v, String id) {
if (id != null) {
if (namedInitializers.contains(id)) {
return this;
}
namedInitializers.add(id);
}
currentTypes.stream().forEach(type -> handlers.get(type).addInitializer(
v));
return this;
}
public PackageTest addInitializer(Consumer v) {
return addInitializer(v, null);
}
public PackageTest addBundleVerifier(
BiConsumer v) {
currentTypes.stream().forEach(
type -> handlers.get(type).addBundleVerifier(v));
return this;
}
public PackageTest addBundleVerifier(Consumer v) {
return addBundleVerifier((cmd, unused) -> v.accept(cmd));
}
public PackageTest addBundlePropertyVerifier(String propertyName,
BiConsumer pred) {
return addBundleVerifier(cmd -> {
String propertyValue = null;
switch (cmd.packageType()) {
case LINUX_DEB:
propertyValue = LinuxHelper.getDebBundleProperty(
cmd.outputBundle(), propertyName);
break;
case LINUX_RPM:
propertyValue = LinuxHelper.getRpmBundleProperty(
cmd.outputBundle(), propertyName);
break;
default:
throw new UnsupportedOperationException();
}
pred.accept(propertyName, propertyValue);
});
}
public PackageTest addBundlePropertyVerifier(String propertyName,
String expectedPropertyValue) {
return addBundlePropertyVerifier(propertyName, (unused, v) -> {
Test.assertEquals(expectedPropertyValue, v, String.format(
"Check value of %s property is [%s]", propertyName, v));
});
}
public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
forTypes(LINUX_DEB, () -> {
LinuxHelper.addDebBundleDesktopIntegrationVerifier(this, integrated);
});
return this;
}
public PackageTest addInstallVerifier(Consumer v) {
currentTypes.stream().forEach(
type -> handlers.get(type).addInstallVerifier(v));
return this;
}
public PackageTest addUninstallVerifier(Consumer v) {
currentTypes.stream().forEach(
type -> handlers.get(type).addUninstallVerifier(v));
return this;
}
public PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
String... faLauncherDefaultArgs) {
addInitializer(cmd -> HelloApp.addTo(cmd), "HelloApp");
addInstallVerifier(cmd -> {
if (cmd.isFakeRuntimeInstalled(
"Not running file associations test")) {
return;
}
Test.withTempFile(fa.getSuffix(), testFile -> {
if (PackageType.LINUX.contains(cmd.packageType())) {
LinuxHelper.initFileAssociationsTestFile(testFile);
}
try {
final Path appOutput = Path.of(HelloApp.OUTPUT_FILENAME);
Files.deleteIfExists(appOutput);
Test.trace(String.format("Use desktop to open [%s] file",
testFile));
Desktop.getDesktop().open(testFile.toFile());
Test.waitForFileCreated(appOutput, 7);
List expectedArgs = new ArrayList<>(List.of(
faLauncherDefaultArgs));
expectedArgs.add(testFile.toString());
// Wait a little bit after file has been created to
// make sure there are no pending writes into it.
Thread.sleep(3000);
HelloApp.verifyOutputFile(appOutput, expectedArgs.toArray(
String[]::new));
} catch (IOException | InterruptedException ex) {
throw new RuntimeException(ex);
}
});
});
forTypes(PackageType.LINUX, () -> {
LinuxHelper.addFileAssociationsVerifier(this, fa);
});
return this;
}
private void forTypes(Collection types, Runnable action) {
Set oldTypes = Set.of(currentTypes.toArray(
PackageType[]::new));
try {
forTypes(types);
action.run();
} finally {
forTypes(oldTypes);
}
}
private void forTypes(PackageType type, Runnable action) {
forTypes(List.of(type), action);
}
public PackageTest configureHelloApp() {
addInitializer(cmd -> HelloApp.addTo(cmd), "HelloApp");
addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
return this;
}
public void run() {
List supportedHandlers = handlers.values().stream()
.filter(entry -> !entry.isVoid())
.collect(Collectors.toList());
if (supportedHandlers.isEmpty()) {
// No handlers with initializers found. Nothing to do.
return;
}
Supplier initializer = new Supplier<>() {
@Override
public JPackageCommand get() {
JPackageCommand cmd = new JPackageCommand().setDefaultInputOutput();
if (bundleOutputDir != null) {
cmd.setArgumentValue("--dest", bundleOutputDir.toString());
}
cmd.setDefaultAppName();
return cmd;
}
};
supportedHandlers.forEach(handler -> handler.accept(initializer.get()));
}
public PackageTest setAction(Action value) {
action = value;
return this;
}
public Action getAction() {
return action;
}
private class Handler implements Consumer {
Handler(PackageType type) {
if (!PackageType.NATIVE.contains(type)) {
throw new IllegalArgumentException(
"Attempt to configure a test for image packaging");
}
this.type = type;
initializers = new ArrayList<>();
bundleVerifiers = new ArrayList<>();
installVerifiers = new ArrayList<>();
uninstallVerifiers = new ArrayList<>();
}
boolean isVoid() {
return initializers.isEmpty();
}
void addInitializer(Consumer v) {
initializers.add(v);
}
void addBundleVerifier(BiConsumer v) {
bundleVerifiers.add(v);
}
void addInstallVerifier(Consumer v) {
installVerifiers.add(v);
}
void addUninstallVerifier(Consumer v) {
uninstallVerifiers.add(v);
}
@Override
public void accept(JPackageCommand cmd) {
type.applyTo(cmd);
initializers.stream().forEach(v -> v.accept(cmd));
switch (action) {
case CREATE:
Executor.Result result = cmd.execute();
result.assertExitCodeIs(expectedJPackageExitCode);
Test.assertFileExists(cmd.outputBundle(),
expectedJPackageExitCode == 0);
verifyPackageBundle(cmd.createImmutableCopy(), result);
break;
case VERIFY_INSTALL:
verifyPackageInstalled(cmd.createImmutableCopy());
break;
case VERIFY_UNINSTALL:
verifyPackageUninstalled(cmd.createImmutableCopy());
break;
}
}
private void verifyPackageBundle(JPackageCommand cmd,
Executor.Result result) {
if (PackageType.LINUX.contains(cmd.packageType())) {
Test.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
cmd), String.format(
"Check installed size of [%s] package in KB is not zero",
LinuxHelper.getPackageName(cmd)));
}
bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
}
private void verifyPackageInstalled(JPackageCommand cmd) {
Test.trace(String.format("Verify installed: %s",
cmd.getPrintableCommandLine()));
if (cmd.isRuntime()) {
Test.assertDirectoryExists(
cmd.appRuntimeInstallationDirectory(), false);
Test.assertDirectoryExists(
cmd.appInstallationDirectory().resolve("app"), false);
} else {
Test.assertExecutableFileExists(cmd.launcherInstallationPath(),
true);
}
if (PackageType.WINDOWS.contains(cmd.packageType())) {
new WindowsHelper.AppVerifier(cmd);
}
installVerifiers.stream().forEach(v -> v.accept(cmd));
}
private void verifyPackageUninstalled(JPackageCommand cmd) {
Test.trace(String.format("Verify uninstalled: %s",
cmd.getPrintableCommandLine()));
if (!cmd.isRuntime()) {
Test.assertFileExists(cmd.launcherInstallationPath(), false);
Test.assertDirectoryExists(cmd.appInstallationDirectory(), false);
}
if (PackageType.WINDOWS.contains(cmd.packageType())) {
new WindowsHelper.AppVerifier(cmd);
}
uninstallVerifiers.stream().forEach(v -> v.accept(cmd));
}
private final PackageType type;
private final List> initializers;
private final List> bundleVerifiers;
private final List> installVerifiers;
private final List> uninstallVerifiers;
}
private Collection currentTypes;
private int expectedJPackageExitCode;
private Map handlers;
private Set namedInitializers;
private Action action;
/**
* Test action.
*/
static public enum Action {
/**
* Create bundle.
*/
CREATE,
/**
* Verify bundle installed.
*/
VERIFY_INSTALL,
/**
* Verify bundle uninstalled.
*/
VERIFY_UNINSTALL;
@Override
public String toString() {
return name().toLowerCase().replace('_', '-');
}
};
private final static Action DEFAULT_ACTION;
private final static File bundleOutputDir;
static {
final String propertyName = "output";
String val = Test.getConfigProperty(propertyName);
if (val == null) {
bundleOutputDir = null;
} else {
bundleOutputDir = new File(val).getAbsoluteFile();
if (!bundleOutputDir.isDirectory()) {
throw new IllegalArgumentException(String.format(
"Invalid value of %s sytem property: [%s]. Should be existing directory",
Test.getConfigPropertyName(propertyName),
bundleOutputDir));
}
}
}
static {
final String propertyName = "action";
String action = Optional.ofNullable(Test.getConfigProperty(propertyName)).orElse(
Action.CREATE.toString()).toLowerCase();
DEFAULT_ACTION = Stream.of(Action.values()).filter(
a -> a.toString().equals(action)).findFirst().orElseThrow(
() -> new IllegalArgumentException(String.format(
"Unrecognized value of %s property: [%s]",
Test.getConfigPropertyName(propertyName), action)));
}
}