blob: a88b6401c6832fdfa09f168ab6549ba09f9877ce [file] [log] [blame]
/*
* Copyright (c) 2019, 2020, 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.awt.GraphicsEnvironment;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.incubator.jpackage.internal.AppImageFile;
import jdk.incubator.jpackage.internal.ApplicationLayout;
import jdk.jpackage.test.Functional.ThrowingBiConsumer;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.Functional.ThrowingRunnable;
import jdk.jpackage.test.Functional.ThrowingSupplier;
/**
* Instance of PackageTest is for configuring and running a single jpackage
* command to produce platform specific package bundle.
*
* Provides methods to hook up custom configuration of jpackage command and
* verification of the output bundle.
*/
public final class PackageTest extends RunnablePackageTest {
public PackageTest() {
excludeTypes = new HashSet<>();
forTypes();
setExpectedExitCode(0);
namedInitializers = new HashSet<>();
handlers = currentTypes.stream()
.collect(Collectors.toMap(v -> v, v -> new Handler()));
packageHandlers = createDefaultPackageHandlers();
}
public PackageTest excludeTypes(PackageType... types) {
excludeTypes.addAll(Stream.of(types).collect(Collectors.toSet()));
return forTypes(currentTypes);
}
public PackageTest excludeTypes(Collection<PackageType> types) {
return excludeTypes(types.toArray(PackageType[]::new));
}
public PackageTest forTypes(PackageType... types) {
Collection<PackageType> newTypes;
if (types == null || types.length == 0) {
newTypes = PackageType.NATIVE;
} else {
newTypes = Stream.of(types).collect(Collectors.toSet());
}
currentTypes = newTypes.stream()
.filter(PackageType::isSupported)
.filter(Predicate.not(excludeTypes::contains))
.collect(Collectors.toUnmodifiableSet());
return this;
}
public PackageTest forTypes(Collection<PackageType> types) {
return forTypes(types.toArray(PackageType[]::new));
}
public PackageTest notForTypes(PackageType... types) {
return notForTypes(List.of(types));
}
public PackageTest notForTypes(Collection<PackageType> types) {
Set<PackageType> workset = new HashSet<>(currentTypes);
workset.removeAll(types);
return forTypes(workset);
}
public PackageTest setExpectedExitCode(int v) {
expectedJPackageExitCode = v;
return this;
}
private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
String id) {
if (id != null) {
if (namedInitializers.contains(id)) {
return this;
}
namedInitializers.add(id);
}
currentTypes.forEach(type -> handlers.get(type).addInitializer(
ThrowingConsumer.toConsumer(v)));
return this;
}
private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) {
return addInitializer(new ThrowingConsumer<JPackageCommand>() {
@Override
public void accept(JPackageCommand unused) throws Throwable {
if (!executed) {
executed = true;
v.run();
}
}
private boolean executed;
}, id);
}
public PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v) {
return addInitializer(v, null);
}
public PackageTest addRunOnceInitializer(ThrowingRunnable v) {
return addRunOnceInitializer(v, null);
}
public PackageTest addBundleVerifier(
ThrowingBiConsumer<JPackageCommand, Executor.Result> v) {
currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(
ThrowingBiConsumer.toBiConsumer(v)));
return this;
}
public PackageTest addBundleVerifier(ThrowingConsumer<JPackageCommand> v) {
return addBundleVerifier(
(cmd, unused) -> ThrowingConsumer.toConsumer(v).accept(cmd));
}
public PackageTest addBundlePropertyVerifier(String propertyName,
Predicate<String> pred, String predLabel) {
return addBundleVerifier(cmd -> {
final String value;
if (TKit.isLinux()) {
value = LinuxHelper.getBundleProperty(cmd, propertyName);
} else if (TKit.isWindows()) {
value = WindowsHelper.getMsiProperty(cmd, propertyName);
} else {
throw new IllegalStateException();
}
TKit.assertTrue(pred.test(value), String.format(
"Check value of %s property %s [%s]", propertyName,
predLabel, value));
});
}
public PackageTest addBundlePropertyVerifier(String propertyName,
String expectedPropertyValue) {
return addBundlePropertyVerifier(propertyName,
expectedPropertyValue::equals, "is");
}
public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
forTypes(PackageType.LINUX, () -> {
LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated);
});
return this;
}
public PackageTest addInstallVerifier(ThrowingConsumer<JPackageCommand> v) {
currentTypes.forEach(type -> handlers.get(type).addInstallVerifier(
ThrowingConsumer.toConsumer(v)));
return this;
}
public PackageTest addUninstallVerifier(ThrowingConsumer<JPackageCommand> v) {
currentTypes.forEach(type -> handlers.get(type).addUninstallVerifier(
ThrowingConsumer.toConsumer(v)));
return this;
}
public PackageTest setPackageInstaller(Consumer<JPackageCommand> v) {
currentTypes.forEach(
type -> packageHandlers.get(type).installHandler = v);
return this;
}
public PackageTest setPackageUnpacker(
BiFunction<JPackageCommand, Path, Path> v) {
currentTypes.forEach(type -> packageHandlers.get(type).unpackHandler = v);
return this;
}
public PackageTest setPackageUninstaller(Consumer<JPackageCommand> v) {
currentTypes.forEach(
type -> packageHandlers.get(type).uninstallHandler = v);
return this;
}
static void withTestFileAssociationsFile(FileAssociations fa,
ThrowingConsumer<Path> consumer) {
final Path testFileDefaultName = Path.of("test" + fa.getSuffix());
TKit.withTempFile(testFileDefaultName, testFile -> {
if (TKit.isLinux()) {
LinuxHelper.initFileAssociationsTestFile(testFile);
}
consumer.accept(testFile);
});
}
PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
String... faLauncherDefaultArgs) {
// Setup test app to have valid jpackage command line before
// running check of type of environment.
addHelloAppInitializer(null);
forTypes(PackageType.LINUX, () -> {
LinuxHelper.addFileAssociationsVerifier(this, fa);
});
String noActionMsg = "Not running file associations test";
if (GraphicsEnvironment.isHeadless()) {
TKit.trace(String.format(
"%s because running in headless environment", noActionMsg));
return this;
}
addInstallVerifier(cmd -> {
if (cmd.isFakeRuntime(noActionMsg) || cmd.isPackageUnpacked(noActionMsg)) {
return;
}
withTestFileAssociationsFile(fa, testFile -> {
testFile = testFile.toAbsolutePath().normalize();
final Path appOutput = testFile.getParent()
.resolve(HelloApp.OUTPUT_FILENAME);
Files.deleteIfExists(appOutput);
TKit.trace(String.format("Use desktop to open [%s] file",
testFile));
Desktop.getDesktop().open(testFile.toFile());
TKit.waitForFileCreated(appOutput, 7);
List<String> 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,
Collections.emptyMap());
});
});
return this;
}
public PackageTest forTypes(Collection<PackageType> types, Runnable action) {
Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
PackageType[]::new));
try {
forTypes(types);
action.run();
} finally {
forTypes(oldTypes);
}
return this;
}
public PackageTest forTypes(PackageType type, Runnable action) {
return forTypes(List.of(type), action);
}
public PackageTest notForTypes(Collection<PackageType> types, Runnable action) {
Set<PackageType> workset = new HashSet<>(currentTypes);
workset.removeAll(types);
return forTypes(workset, action);
}
public PackageTest notForTypes(PackageType type, Runnable action) {
return notForTypes(List.of(type), action);
}
public PackageTest configureHelloApp() {
return configureHelloApp(null);
}
public PackageTest configureHelloApp(String javaAppDesc) {
addHelloAppInitializer(javaAppDesc);
addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
return this;
}
public final static class Group extends RunnablePackageTest {
public Group(PackageTest... tests) {
handlers = Stream.of(tests)
.map(PackageTest::createPackageTypeHandlers)
.flatMap(List<Consumer<Action>>::stream)
.collect(Collectors.toUnmodifiableList());
}
@Override
protected void runAction(Action... action) {
if (Set.of(action).contains(Action.UNINSTALL)) {
ListIterator<Consumer<Action>> listIterator = handlers.listIterator(
handlers.size());
while (listIterator.hasPrevious()) {
var handler = listIterator.previous();
List.of(action).forEach(handler::accept);
}
} else {
handlers.forEach(handler -> List.of(action).forEach(handler::accept));
}
}
private final List<Consumer<Action>> handlers;
}
final static class PackageHandlers {
Consumer<JPackageCommand> installHandler;
Consumer<JPackageCommand> uninstallHandler;
BiFunction<JPackageCommand, Path, Path> unpackHandler;
}
@Override
protected void runActions(List<Action[]> actions) {
createPackageTypeHandlers().forEach(
handler -> actions.forEach(
action -> List.of(action).forEach(handler::accept)));
}
@Override
protected void runAction(Action... action) {
throw new UnsupportedOperationException();
}
private void addHelloAppInitializer(String javaAppDesc) {
addInitializer(
cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd),
"HelloApp");
}
private List<Consumer<Action>> createPackageTypeHandlers() {
return PackageType.NATIVE.stream()
.map(type -> {
Handler handler = handlers.entrySet().stream()
.filter(entry -> !entry.getValue().isVoid())
.filter(entry -> entry.getKey() == type)
.map(entry -> entry.getValue())
.findAny().orElse(null);
Map.Entry<PackageType, Handler> result = null;
if (handler != null) {
result = Map.entry(type, handler);
}
return result;
})
.filter(Objects::nonNull)
.map(entry -> createPackageTypeHandler(
entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private Consumer<Action> createPackageTypeHandler(
PackageType type, Handler handler) {
return ThrowingConsumer.toConsumer(new ThrowingConsumer<Action>() {
@Override
public void accept(Action action) throws Throwable {
if (action == Action.FINALIZE) {
if (unpackDir != null && Files.isDirectory(unpackDir)
&& !unpackDir.startsWith(TKit.workDir())) {
TKit.deleteDirectoryRecursive(unpackDir);
}
}
if (aborted) {
return;
}
final JPackageCommand curCmd;
if (Set.of(Action.INITIALIZE, Action.CREATE).contains(action)) {
curCmd = cmd;
} else {
curCmd = cmd.createImmutableCopy();
}
switch (action) {
case UNPACK: {
var handler = packageHandlers.get(type).unpackHandler;
if (!(aborted = (handler == null))) {
unpackDir = TKit.createTempDirectory(
String.format("unpacked-%s",
type.getName()));
unpackDir = handler.apply(cmd, unpackDir);
cmd.setUnpackedPackageLocation(unpackDir);
}
break;
}
case INSTALL: {
var handler = packageHandlers.get(type).installHandler;
if (!(aborted = (handler == null))) {
handler.accept(curCmd);
}
break;
}
case UNINSTALL: {
var handler = packageHandlers.get(type).uninstallHandler;
if (!(aborted = (handler == null))) {
handler.accept(curCmd);
}
break;
}
case CREATE:
handler.accept(action, curCmd);
aborted = (expectedJPackageExitCode != 0);
return;
default:
handler.accept(action, curCmd);
break;
}
if (aborted) {
TKit.trace(
String.format("Aborted [%s] action of %s command",
action, cmd.getPrintableCommandLine()));
}
}
private Path unpackDir;
private boolean aborted;
private final JPackageCommand cmd = Functional.identity(() -> {
JPackageCommand result = new JPackageCommand();
result.setDefaultInputOutput().setDefaultAppName();
if (BUNDLE_OUTPUT_DIR != null) {
result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString());
}
type.applyTo(result);
return result;
}).get();
});
}
private class Handler implements BiConsumer<Action, JPackageCommand> {
Handler() {
initializers = new ArrayList<>();
bundleVerifiers = new ArrayList<>();
installVerifiers = new ArrayList<>();
uninstallVerifiers = new ArrayList<>();
}
boolean isVoid() {
return initializers.isEmpty();
}
void addInitializer(Consumer<JPackageCommand> v) {
initializers.add(v);
}
void addBundleVerifier(BiConsumer<JPackageCommand, Executor.Result> v) {
bundleVerifiers.add(v);
}
void addInstallVerifier(Consumer<JPackageCommand> v) {
installVerifiers.add(v);
}
void addUninstallVerifier(Consumer<JPackageCommand> v) {
uninstallVerifiers.add(v);
}
@Override
public void accept(Action action, JPackageCommand cmd) {
switch (action) {
case INITIALIZE:
initializers.forEach(v -> v.accept(cmd));
if (cmd.isImagePackageType()) {
throw new UnsupportedOperationException();
}
cmd.executePrerequisiteActions();
break;
case CREATE:
Executor.Result result = cmd.execute(expectedJPackageExitCode);
if (expectedJPackageExitCode == 0) {
TKit.assertFileExists(cmd.outputBundle());
} else {
TKit.assertPathExists(cmd.outputBundle(), false);
}
verifyPackageBundle(cmd, result);
break;
case VERIFY_INSTALL:
if (expectedJPackageExitCode == 0) {
verifyPackageInstalled(cmd);
}
break;
case VERIFY_UNINSTALL:
if (expectedJPackageExitCode == 0) {
verifyPackageUninstalled(cmd);
}
break;
}
}
private void verifyPackageBundle(JPackageCommand cmd,
Executor.Result result) {
if (expectedJPackageExitCode == 0) {
if (PackageType.LINUX.contains(cmd.packageType())) {
LinuxHelper.verifyPackageBundleEssential(cmd);
}
}
bundleVerifiers.forEach(v -> v.accept(cmd, result));
}
private void verifyPackageInstalled(JPackageCommand cmd) {
final String formatString;
if (cmd.isPackageUnpacked()) {
formatString = "Verify unpacked: %s";
} else {
formatString = "Verify installed: %s";
}
TKit.trace(String.format(formatString, cmd.getPrintableCommandLine()));
if (!cmd.isRuntime()) {
if (PackageType.WINDOWS.contains(cmd.packageType())
&& !cmd.isPackageUnpacked(
"Not verifying desktop integration")) {
new WindowsHelper.DesktopIntegrationVerifier(cmd);
}
}
cmd.assertAppLayout();
installVerifiers.forEach(v -> v.accept(cmd));
}
private void verifyPackageUninstalled(JPackageCommand cmd) {
TKit.trace(String.format("Verify uninstalled: %s",
cmd.getPrintableCommandLine()));
if (!cmd.isRuntime()) {
TKit.assertPathExists(cmd.appLauncherPath(), false);
if (PackageType.WINDOWS.contains(cmd.packageType())) {
new WindowsHelper.DesktopIntegrationVerifier(cmd);
}
}
Path appInstallDir = cmd.appInstallationDirectory();
if (TKit.isLinux() && Path.of("/").equals(appInstallDir)) {
ApplicationLayout appLayout = cmd.appLayout();
TKit.assertPathExists(appLayout.runtimeDirectory(), false);
} else {
TKit.assertPathExists(appInstallDir, false);
}
uninstallVerifiers.forEach(v -> v.accept(cmd));
}
private final List<Consumer<JPackageCommand>> initializers;
private final List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers;
private final List<Consumer<JPackageCommand>> installVerifiers;
private final List<Consumer<JPackageCommand>> uninstallVerifiers;
}
private static Map<PackageType, PackageHandlers> createDefaultPackageHandlers() {
HashMap<PackageType, PackageHandlers> handlers = new HashMap<>();
if (TKit.isLinux()) {
handlers.put(PackageType.LINUX_DEB, LinuxHelper.createDebPackageHandlers());
handlers.put(PackageType.LINUX_RPM, LinuxHelper.createRpmPackageHandlers());
}
if (TKit.isWindows()) {
handlers.put(PackageType.WIN_MSI, WindowsHelper.createMsiPackageHandlers());
handlers.put(PackageType.WIN_EXE, WindowsHelper.createExePackageHandlers());
}
if (TKit.isOSX()) {
handlers.put(PackageType.MAC_DMG, MacHelper.createDmgPackageHandlers());
handlers.put(PackageType.MAC_PKG, MacHelper.createPkgPackageHandlers());
}
return handlers;
}
private Collection<PackageType> currentTypes;
private Set<PackageType> excludeTypes;
private int expectedJPackageExitCode;
private Map<PackageType, Handler> handlers;
private Set<String> namedInitializers;
private Map<PackageType, PackageHandlers> packageHandlers;
private final static File BUNDLE_OUTPUT_DIR;
static {
final String propertyName = "output";
String val = TKit.getConfigProperty(propertyName);
if (val == null) {
BUNDLE_OUTPUT_DIR = null;
} else {
BUNDLE_OUTPUT_DIR = new File(val).getAbsoluteFile();
if (!BUNDLE_OUTPUT_DIR.isDirectory()) {
throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory",
TKit.getConfigPropertyName(propertyName),
BUNDLE_OUTPUT_DIR));
}
}
}
}