1 /*
   2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package jdk.jpackage.test;
  24 
  25 import java.awt.Desktop;
  26 import java.awt.GraphicsEnvironment;
  27 import java.io.File;
  28 import java.nio.file.Files;
  29 import java.nio.file.Path;
  30 import java.util.*;
  31 import java.util.function.*;
  32 import java.util.stream.Collectors;
  33 import java.util.stream.Stream;
  34 import jdk.jpackage.test.Functional.ThrowingConsumer;
  35 import jdk.incubator.jpackage.internal.AppImageFile;
  36 import jdk.jpackage.test.Functional.ThrowingBiConsumer;
  37 import jdk.jpackage.test.Functional.ThrowingRunnable;
  38 import static jdk.jpackage.test.PackageType.*;
  39 
  40 /**
  41  * Instance of PackageTest is for configuring and running a single jpackage
  42  * command to produce platform specific package bundle.
  43  *
  44  * Provides methods to hook up custom configuration of jpackage command and
  45  * verification of the output bundle.
  46  */
  47 public final class PackageTest extends RunnablePackageTest {
  48 
  49     public PackageTest() {
  50         excludeTypes = new HashSet<>();
  51         forTypes();
  52         setExpectedExitCode(0);
  53         namedInitializers = new HashSet<>();
  54         handlers = currentTypes.stream()
  55                 .collect(Collectors.toMap(v -> v, v -> new Handler()));
  56         packageHandlers = createDefaultPackageHandlers();
  57     }
  58 
  59     public PackageTest excludeTypes(PackageType... types) {
  60         excludeTypes.addAll(Stream.of(types).collect(Collectors.toSet()));
  61         return forTypes(currentTypes);
  62     }
  63 
  64     public PackageTest excludeTypes(Collection<PackageType> types) {
  65         return excludeTypes(types.toArray(PackageType[]::new));
  66     }
  67 
  68     public PackageTest forTypes(PackageType... types) {
  69         Collection<PackageType> newTypes;
  70         if (types == null || types.length == 0) {
  71             newTypes = PackageType.NATIVE;
  72         } else {
  73             newTypes = Stream.of(types).collect(Collectors.toSet());
  74         }
  75         currentTypes = newTypes.stream()
  76                 .filter(PackageType::isSupported)
  77                 .filter(Predicate.not(excludeTypes::contains))
  78                 .collect(Collectors.toUnmodifiableSet());
  79         return this;
  80     }
  81 
  82     public PackageTest forTypes(Collection<PackageType> types) {
  83         return forTypes(types.toArray(PackageType[]::new));
  84     }
  85 
  86     public PackageTest notForTypes(PackageType... types) {
  87         return notForTypes(List.of(types));
  88     }
  89 
  90     public PackageTest notForTypes(Collection<PackageType> types) {
  91         Set<PackageType> workset = new HashSet<>(currentTypes);
  92         workset.removeAll(types);
  93         return forTypes(workset);
  94     }
  95 
  96     public PackageTest setExpectedExitCode(int v) {
  97         expectedJPackageExitCode = v;
  98         return this;
  99     }
 100 
 101     private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
 102             String id) {
 103         if (id != null) {
 104             if (namedInitializers.contains(id)) {
 105                 return this;
 106             }
 107 
 108             namedInitializers.add(id);
 109         }
 110         currentTypes.forEach(type -> handlers.get(type).addInitializer(
 111                 ThrowingConsumer.toConsumer(v)));
 112         return this;
 113     }
 114 
 115     private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) {
 116         return addInitializer(new ThrowingConsumer<JPackageCommand>() {
 117             @Override
 118             public void accept(JPackageCommand unused) throws Throwable {
 119                 if (!executed) {
 120                     executed = true;
 121                     v.run();
 122                 }
 123             }
 124 
 125             private boolean executed;
 126         }, id);
 127     }
 128 
 129     public PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v) {
 130         return addInitializer(v, null);
 131     }
 132 
 133     public PackageTest addRunOnceInitializer(ThrowingRunnable v) {
 134         return addRunOnceInitializer(v, null);
 135     }
 136 
 137     public PackageTest addBundleVerifier(
 138             ThrowingBiConsumer<JPackageCommand, Executor.Result> v) {
 139         currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(
 140                 ThrowingBiConsumer.toBiConsumer(v)));
 141         return this;
 142     }
 143 
 144     public PackageTest addBundleVerifier(ThrowingConsumer<JPackageCommand> v) {
 145         return addBundleVerifier(
 146                 (cmd, unused) -> ThrowingConsumer.toConsumer(v).accept(cmd));
 147     }
 148 
 149     public PackageTest addBundlePropertyVerifier(String propertyName,
 150             Predicate<String> pred, String predLabel) {
 151         return addBundleVerifier(cmd -> {
 152             final String value;
 153             if (TKit.isLinux()) {
 154                 value = LinuxHelper.getBundleProperty(cmd, propertyName);
 155             } else if (TKit.isWindows()) {
 156                 value = WindowsHelper.getMsiProperty(cmd, propertyName);
 157             } else {
 158                 throw new IllegalStateException();
 159             }
 160             TKit.assertTrue(pred.test(value), String.format(
 161                     "Check value of %s property %s [%s]", propertyName,
 162                     predLabel, value));
 163         });
 164     }
 165 
 166     public PackageTest addBundlePropertyVerifier(String propertyName,
 167             String expectedPropertyValue) {
 168         return addBundlePropertyVerifier(propertyName,
 169                 expectedPropertyValue::equals, "is");
 170     }
 171 
 172     public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
 173         forTypes(LINUX, () -> {
 174             LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated);
 175         });
 176         return this;
 177     }
 178 
 179     public PackageTest addInstallVerifier(ThrowingConsumer<JPackageCommand> v) {
 180         currentTypes.forEach(type -> handlers.get(type).addInstallVerifier(
 181                 ThrowingConsumer.toConsumer(v)));
 182         return this;
 183     }
 184 
 185     public PackageTest addUninstallVerifier(ThrowingConsumer<JPackageCommand> v) {
 186         currentTypes.forEach(type -> handlers.get(type).addUninstallVerifier(
 187                 ThrowingConsumer.toConsumer(v)));
 188         return this;
 189     }
 190 
 191     public PackageTest setPackageInstaller(Consumer<JPackageCommand> v) {
 192         currentTypes.forEach(
 193                 type -> packageHandlers.get(type).installHandler = v);
 194         return this;
 195     }
 196 
 197     public PackageTest setPackageUnpacker(
 198             BiFunction<JPackageCommand, Path, Path> v) {
 199         currentTypes.forEach(type -> packageHandlers.get(type).unpackHandler = v);
 200         return this;
 201     }
 202 
 203     public PackageTest setPackageUninstaller(Consumer<JPackageCommand> v) {
 204         currentTypes.forEach(
 205                 type -> packageHandlers.get(type).uninstallHandler = v);
 206         return this;
 207     }
 208 
 209     static void withTestFileAssociationsFile(FileAssociations fa,
 210             ThrowingConsumer<Path> consumer) {
 211         final Path testFileDefaultName = Path.of("test" + fa.getSuffix());
 212         TKit.withTempFile(testFileDefaultName, testFile -> {
 213             if (TKit.isLinux()) {
 214                 LinuxHelper.initFileAssociationsTestFile(testFile);
 215             }
 216             consumer.accept(testFile);
 217         });
 218     }
 219 
 220     PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
 221             String... faLauncherDefaultArgs) {
 222 
 223         // Setup test app to have valid jpackage command line before
 224         // running check of type of environment.
 225         addHelloAppInitializer(null);
 226 
 227         String noActionMsg = "Not running file associations test";
 228         if (GraphicsEnvironment.isHeadless()) {
 229             TKit.trace(String.format(
 230                     "%s because running in headless environment", noActionMsg));
 231             return this;
 232         }
 233 
 234         addInstallVerifier(cmd -> {
 235             if (cmd.isFakeRuntime(noActionMsg) || cmd.isPackageUnpacked(noActionMsg)) {
 236                 return;
 237             }
 238 
 239             withTestFileAssociationsFile(fa, testFile -> {
 240                 testFile = testFile.toAbsolutePath().normalize();
 241 
 242                 final Path appOutput = testFile.getParent()
 243                         .resolve(HelloApp.OUTPUT_FILENAME);
 244                 Files.deleteIfExists(appOutput);
 245 
 246                 TKit.trace(String.format("Use desktop to open [%s] file",
 247                         testFile));
 248                 Desktop.getDesktop().open(testFile.toFile());
 249                 TKit.waitForFileCreated(appOutput, 7);
 250 
 251                 List<String> expectedArgs = new ArrayList<>(List.of(
 252                         faLauncherDefaultArgs));
 253                 expectedArgs.add(testFile.toString());
 254 
 255                 // Wait a little bit after file has been created to
 256                 // make sure there are no pending writes into it.
 257                 Thread.sleep(3000);
 258                 HelloApp.verifyOutputFile(appOutput, expectedArgs,
 259                         Collections.emptyMap());
 260             });
 261         });
 262 
 263         forTypes(PackageType.LINUX, () -> {
 264             LinuxHelper.addFileAssociationsVerifier(this, fa);
 265         });
 266 
 267         return this;
 268     }
 269 
 270     public PackageTest forTypes(Collection<PackageType> types, Runnable action) {
 271         Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
 272                 PackageType[]::new));
 273         try {
 274             forTypes(types);
 275             action.run();
 276         } finally {
 277             forTypes(oldTypes);
 278         }
 279         return this;
 280     }
 281 
 282     public PackageTest forTypes(PackageType type, Runnable action) {
 283         return forTypes(List.of(type), action);
 284     }
 285 
 286     public PackageTest notForTypes(Collection<PackageType> types, Runnable action) {
 287         Set<PackageType> workset = new HashSet<>(currentTypes);
 288         workset.removeAll(types);
 289         return forTypes(workset, action);
 290     }
 291 
 292     public PackageTest notForTypes(PackageType type, Runnable action) {
 293         return notForTypes(List.of(type), action);
 294     }
 295 
 296     public PackageTest configureHelloApp() {
 297         return configureHelloApp(null);
 298     }
 299 
 300     public PackageTest configureHelloApp(String javaAppDesc) {
 301         addHelloAppInitializer(javaAppDesc);
 302         addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
 303         return this;
 304     }
 305 
 306     public final static class Group extends RunnablePackageTest {
 307         public Group(PackageTest... tests) {
 308             handlers = Stream.of(tests)
 309                     .map(PackageTest::createPackageTypeHandlers)
 310                     .flatMap(List<Consumer<Action>>::stream)
 311                     .collect(Collectors.toUnmodifiableList());
 312         }
 313 
 314         @Override
 315         protected void runAction(Action... action) {
 316             if (Set.of(action).contains(Action.UNINSTALL)) {
 317                 ListIterator<Consumer<Action>> listIterator = handlers.listIterator(
 318                         handlers.size());
 319                 while (listIterator.hasPrevious()) {
 320                     var handler = listIterator.previous();
 321                     List.of(action).forEach(handler::accept);
 322                 }
 323             } else {
 324                 handlers.forEach(handler -> List.of(action).forEach(handler::accept));
 325             }
 326         }
 327 
 328         private final List<Consumer<Action>> handlers;
 329     }
 330 
 331     final static class PackageHandlers {
 332         Consumer<JPackageCommand> installHandler;
 333         Consumer<JPackageCommand> uninstallHandler;
 334         BiFunction<JPackageCommand, Path, Path> unpackHandler;
 335     }
 336 
 337     @Override
 338     protected void runActions(List<Action[]> actions) {
 339         createPackageTypeHandlers().forEach(
 340                 handler -> actions.forEach(
 341                         action -> List.of(action).forEach(handler::accept)));
 342     }
 343 
 344     @Override
 345     protected void runAction(Action... action) {
 346         throw new UnsupportedOperationException();
 347     }
 348 
 349     private void addHelloAppInitializer(String javaAppDesc) {
 350         addInitializer(
 351                 cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd),
 352                 "HelloApp");
 353     }
 354 
 355     private List<Consumer<Action>> createPackageTypeHandlers() {
 356         return PackageType.NATIVE.stream()
 357                 .map(type -> {
 358                     Handler handler = handlers.entrySet().stream()
 359                         .filter(entry -> !entry.getValue().isVoid())
 360                         .filter(entry -> entry.getKey() == type)
 361                         .map(entry -> entry.getValue())
 362                         .findAny().orElse(null);
 363                     Map.Entry<PackageType, Handler> result = null;
 364                     if (handler != null) {
 365                         result = Map.entry(type, handler);
 366                     }
 367                     return result;
 368                 })
 369                 .filter(Objects::nonNull)
 370                 .map(entry -> createPackageTypeHandler(
 371                         entry.getKey(), entry.getValue()))
 372                 .collect(Collectors.toList());
 373     }
 374 
 375     private Consumer<Action> createPackageTypeHandler(
 376             PackageType type, Handler handler) {
 377         return ThrowingConsumer.toConsumer(new ThrowingConsumer<Action>() {
 378             @Override
 379             public void accept(Action action) throws Throwable {
 380                 if (action == Action.FINALIZE) {
 381                     if (unpackDir != null && Files.isDirectory(unpackDir)
 382                             && !unpackDir.startsWith(TKit.workDir())) {
 383                         TKit.deleteDirectoryRecursive(unpackDir);
 384                     }
 385                 }
 386 
 387                 if (aborted) {
 388                     return;
 389                 }
 390 
 391                 final JPackageCommand curCmd;
 392                 if (Set.of(Action.INITIALIZE, Action.CREATE).contains(action)) {
 393                     curCmd = cmd;
 394                 } else {
 395                     curCmd = cmd.createImmutableCopy();
 396                 }
 397 
 398                 switch (action) {
 399                     case UNPACK: {
 400                         var handler = packageHandlers.get(type).unpackHandler;
 401                         if (!(aborted = (handler == null))) {
 402                             unpackDir = TKit.createTempDirectory(
 403                                             String.format("unpacked-%s",
 404                                                     type.getName()));
 405                             unpackDir = handler.apply(cmd, unpackDir);
 406                             cmd.setUnpackedPackageLocation(unpackDir);
 407                         }
 408                         break;
 409                     }
 410 
 411                     case INSTALL: {
 412                         var handler = packageHandlers.get(type).installHandler;
 413                         if (!(aborted = (handler == null))) {
 414                             handler.accept(curCmd);
 415                         }
 416                         break;
 417                     }
 418 
 419                     case UNINSTALL: {
 420                         var handler = packageHandlers.get(type).uninstallHandler;
 421                         if (!(aborted = (handler == null))) {
 422                             handler.accept(curCmd);
 423                         }
 424                         break;
 425                     }
 426 
 427                     case CREATE:
 428                         handler.accept(action, curCmd);
 429                         aborted = (expectedJPackageExitCode != 0);
 430                         return;
 431 
 432                     default:
 433                         handler.accept(action, curCmd);
 434                         break;
 435                 }
 436 
 437                 if (aborted) {
 438                     TKit.trace(
 439                             String.format("Aborted [%s] action of %s command",
 440                                     action, cmd.getPrintableCommandLine()));
 441                 }
 442             }
 443 
 444             private Path unpackDir;
 445             private boolean aborted;
 446             private final JPackageCommand cmd = Functional.identity(() -> {
 447                 JPackageCommand result = new JPackageCommand();
 448                 result.setDefaultInputOutput().setDefaultAppName();
 449                 if (BUNDLE_OUTPUT_DIR != null) {
 450                     result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString());
 451                 }
 452                 type.applyTo(result);
 453                 return result;
 454             }).get();
 455         });
 456     }
 457 
 458     private class Handler implements BiConsumer<Action, JPackageCommand> {
 459 
 460         Handler() {
 461             initializers = new ArrayList<>();
 462             bundleVerifiers = new ArrayList<>();
 463             installVerifiers = new ArrayList<>();
 464             uninstallVerifiers = new ArrayList<>();
 465         }
 466 
 467         boolean isVoid() {
 468             return initializers.isEmpty();
 469         }
 470 
 471         void addInitializer(Consumer<JPackageCommand> v) {
 472             initializers.add(v);
 473         }
 474 
 475         void addBundleVerifier(BiConsumer<JPackageCommand, Executor.Result> v) {
 476             bundleVerifiers.add(v);
 477         }
 478 
 479         void addInstallVerifier(Consumer<JPackageCommand> v) {
 480             installVerifiers.add(v);
 481         }
 482 
 483         void addUninstallVerifier(Consumer<JPackageCommand> v) {
 484             uninstallVerifiers.add(v);
 485         }
 486 
 487         @Override
 488         public void accept(Action action, JPackageCommand cmd) {
 489             switch (action) {
 490                 case INITIALIZE:
 491                     initializers.forEach(v -> v.accept(cmd));
 492                     if (cmd.isImagePackageType()) {
 493                         throw new UnsupportedOperationException();
 494                     }
 495                     cmd.executePrerequisiteActions();
 496                     break;
 497 
 498                 case CREATE:
 499                     Executor.Result result = cmd.execute(expectedJPackageExitCode);
 500                     if (expectedJPackageExitCode == 0) {
 501                         TKit.assertFileExists(cmd.outputBundle());
 502                     } else {
 503                         TKit.assertPathExists(cmd.outputBundle(), false);
 504                     }
 505                     verifyPackageBundle(cmd, result);
 506                     break;
 507 
 508                 case VERIFY_INSTALL:
 509                     if (expectedJPackageExitCode == 0) {
 510                         verifyPackageInstalled(cmd);
 511                     }
 512                     break;
 513 
 514                 case VERIFY_UNINSTALL:
 515                     if (expectedJPackageExitCode == 0) {
 516                         verifyPackageUninstalled(cmd);
 517                     }
 518                     break;
 519             }
 520         }
 521 
 522         private void verifyPackageBundle(JPackageCommand cmd,
 523                 Executor.Result result) {
 524             if (expectedJPackageExitCode == 0) {
 525                 if (PackageType.LINUX.contains(cmd.packageType())) {
 526                     LinuxHelper.verifyPackageBundleEssential(cmd);
 527                 }
 528             }
 529             bundleVerifiers.forEach(v -> v.accept(cmd, result));
 530         }
 531 
 532         private void verifyPackageInstalled(JPackageCommand cmd) {
 533             final String formatString;
 534             if (cmd.isPackageUnpacked()) {
 535                 formatString = "Verify unpacked: %s";
 536             } else {
 537                 formatString = "Verify installed: %s";
 538             }
 539             TKit.trace(String.format(formatString, cmd.getPrintableCommandLine()));
 540 
 541             TKit.assertDirectoryExists(cmd.appRuntimeDirectory());
 542             if (!cmd.isRuntime()) {
 543                 TKit.assertExecutableFileExists(cmd.appLauncherPath());
 544 
 545                 if (PackageType.WINDOWS.contains(cmd.packageType())
 546                         && !cmd.isPackageUnpacked(
 547                                 "Not verifying desktop integration")) {
 548                     new WindowsHelper.DesktopIntegrationVerifier(cmd);
 549                 }
 550             }
 551 
 552             TKit.assertPathExists(AppImageFile.getPathInAppImage(
 553                     cmd.appInstallationDirectory()), false);
 554 
 555             installVerifiers.forEach(v -> v.accept(cmd));
 556         }
 557 
 558         private void verifyPackageUninstalled(JPackageCommand cmd) {
 559             TKit.trace(String.format("Verify uninstalled: %s",
 560                     cmd.getPrintableCommandLine()));
 561             if (!cmd.isRuntime()) {
 562                 TKit.assertPathExists(cmd.appLauncherPath(), false);
 563 
 564                 if (PackageType.WINDOWS.contains(cmd.packageType())) {
 565                     new WindowsHelper.DesktopIntegrationVerifier(cmd);
 566                 }
 567             }
 568 
 569             TKit.assertPathExists(cmd.appInstallationDirectory(), false);
 570 
 571             uninstallVerifiers.forEach(v -> v.accept(cmd));
 572         }
 573 
 574         private final List<Consumer<JPackageCommand>> initializers;
 575         private final List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers;
 576         private final List<Consumer<JPackageCommand>> installVerifiers;
 577         private final List<Consumer<JPackageCommand>> uninstallVerifiers;
 578     }
 579 
 580     private static Map<PackageType, PackageHandlers> createDefaultPackageHandlers() {
 581         HashMap<PackageType, PackageHandlers> handlers = new HashMap<>();
 582         if (TKit.isLinux()) {
 583             handlers.put(PackageType.LINUX_DEB, LinuxHelper.createDebPackageHandlers());
 584             handlers.put(PackageType.LINUX_RPM, LinuxHelper.createRpmPackageHandlers());
 585         }
 586 
 587         if (TKit.isWindows()) {
 588             handlers.put(PackageType.WIN_MSI, WindowsHelper.createMsiPackageHandlers());
 589             handlers.put(PackageType.WIN_EXE, WindowsHelper.createExePackageHandlers());
 590         }
 591 
 592         if (TKit.isOSX()) {
 593             handlers.put(PackageType.MAC_DMG,  MacHelper.createDmgPackageHandlers());
 594             handlers.put(PackageType.MAC_PKG,  MacHelper.createPkgPackageHandlers());
 595         }
 596 
 597         return handlers;
 598     }
 599 
 600     private Collection<PackageType> currentTypes;
 601     private Set<PackageType> excludeTypes;
 602     private int expectedJPackageExitCode;
 603     private Map<PackageType, Handler> handlers;
 604     private Set<String> namedInitializers;
 605     private Map<PackageType, PackageHandlers> packageHandlers;
 606 
 607     private final static File BUNDLE_OUTPUT_DIR;
 608 
 609     static {
 610         final String propertyName = "output";
 611         String val = TKit.getConfigProperty(propertyName);
 612         if (val == null) {
 613             BUNDLE_OUTPUT_DIR = null;
 614         } else {
 615             BUNDLE_OUTPUT_DIR = new File(val).getAbsoluteFile();
 616 
 617             if (!BUNDLE_OUTPUT_DIR.isDirectory()) {
 618                 throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory",
 619                         TKit.getConfigPropertyName(propertyName),
 620                         BUNDLE_OUTPUT_DIR));
 621             }
 622         }
 623     }
 624 }