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.BiConsumer;
  32 import java.util.function.Consumer;
  33 import java.util.function.Predicate;
  34 import java.util.function.Supplier;
  35 import java.util.stream.Collectors;
  36 import java.util.stream.Stream;
  37 import jdk.jpackage.test.Functional.ThrowingConsumer;
  38 import jdk.incubator.jpackage.internal.AppImageFile;
  39 import static jdk.jpackage.test.PackageType.*;
  40 
  41 /**
  42  * Instance of PackageTest is for configuring and running a single jpackage
  43  * command to produce platform specific package bundle.
  44  *
  45  * Provides methods to hook up custom configuration of jpackage command and
  46  * verification of the output bundle.
  47  */
  48 public final class PackageTest {
  49 
  50     /**
  51      * Default test configuration for jpackage command. Default jpackage command
  52      * initialization includes:
  53      * <li>Set --input and --dest parameters.
  54      * <li>Set --name parameter. Value of the parameter is the name of the first
  55      * class with main function found in the callers stack. Defaults can be
  56      * overridden with custom initializers set with subsequent addInitializer()
  57      * function calls.
  58      */
  59     public PackageTest() {
  60         action = DEFAULT_ACTION;
  61         excludeTypes = new HashSet<>();
  62         forTypes();
  63         setExpectedExitCode(0);
  64         handlers = new HashMap<>();
  65         namedInitializers = new HashSet<>();
  66         currentTypes.forEach(v -> handlers.put(v, new Handler(v)));
  67     }
  68 
  69     public PackageTest excludeTypes(PackageType... types) {
  70         excludeTypes.addAll(Stream.of(types).collect(Collectors.toSet()));
  71         return forTypes(currentTypes);
  72     }
  73 
  74     public PackageTest excludeTypes(Collection<PackageType> types) {
  75         return excludeTypes(types.toArray(PackageType[]::new));
  76     }
  77 
  78     public PackageTest forTypes(PackageType... types) {
  79         Collection<PackageType> newTypes;
  80         if (types == null || types.length == 0) {
  81             newTypes = PackageType.NATIVE;
  82         } else {
  83             newTypes = Stream.of(types).collect(Collectors.toSet());
  84         }
  85         currentTypes = newTypes.stream()
  86                 .filter(PackageType::isSupported)
  87                 .filter(Predicate.not(excludeTypes::contains))
  88                 .collect(Collectors.toUnmodifiableSet());
  89         return this;
  90     }
  91 
  92     public PackageTest forTypes(Collection<PackageType> types) {
  93         return forTypes(types.toArray(PackageType[]::new));
  94     }
  95 
  96     public PackageTest notForTypes(PackageType... types) {
  97         return notForTypes(List.of(types));
  98     }
  99 
 100     public PackageTest notForTypes(Collection<PackageType> types) {
 101         Set<PackageType> workset = new HashSet<>(currentTypes);
 102         workset.removeAll(types);
 103         return forTypes(workset);
 104     }
 105 
 106     public PackageTest setExpectedExitCode(int v) {
 107         expectedJPackageExitCode = v;
 108         return this;
 109     }
 110 
 111     private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
 112             String id) {
 113         if (id != null) {
 114             if (namedInitializers.contains(id)) {
 115                 return this;
 116             }
 117 
 118             namedInitializers.add(id);
 119         }
 120         currentTypes.stream().forEach(type -> handlers.get(type).addInitializer(
 121                 ThrowingConsumer.toConsumer(v)));
 122         return this;
 123     }
 124 
 125     public PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v) {
 126         return addInitializer(v, null);
 127     }
 128 
 129     public PackageTest addBundleVerifier(
 130             BiConsumer<JPackageCommand, Executor.Result> v) {
 131         currentTypes.stream().forEach(
 132                 type -> handlers.get(type).addBundleVerifier(v));
 133         return this;
 134     }
 135 
 136     public PackageTest addBundleVerifier(ThrowingConsumer<JPackageCommand> v) {
 137         return addBundleVerifier(
 138                 (cmd, unused) -> ThrowingConsumer.toConsumer(v).accept(cmd));
 139     }
 140 
 141     public PackageTest addBundlePropertyVerifier(String propertyName,
 142             BiConsumer<String, String> pred) {
 143         return addBundleVerifier(cmd -> {
 144             pred.accept(propertyName,
 145                     LinuxHelper.getBundleProperty(cmd, propertyName));
 146         });
 147     }
 148 
 149     public PackageTest addBundlePropertyVerifier(String propertyName,
 150             String expectedPropertyValue) {
 151         return addBundlePropertyVerifier(propertyName, (unused, v) -> {
 152             TKit.assertEquals(expectedPropertyValue, v, String.format(
 153                     "Check value of %s property is [%s]", propertyName, v));
 154         });
 155     }
 156 
 157     public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
 158         forTypes(LINUX, () -> {
 159             LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated);
 160         });
 161         return this;
 162     }
 163 
 164     public PackageTest addInstallVerifier(ThrowingConsumer<JPackageCommand> v) {
 165         currentTypes.stream().forEach(
 166                 type -> handlers.get(type).addInstallVerifier(
 167                         ThrowingConsumer.toConsumer(v)));
 168         return this;
 169     }
 170 
 171     public PackageTest addUninstallVerifier(ThrowingConsumer<JPackageCommand> v) {
 172         currentTypes.stream().forEach(
 173                 type -> handlers.get(type).addUninstallVerifier(
 174                         ThrowingConsumer.toConsumer(v)));
 175         return this;
 176     }
 177 
 178     static void withTestFileAssociationsFile(FileAssociations fa,
 179             ThrowingConsumer<Path> consumer) {
 180         final String testFileDefaultName = String.join(".", "test",
 181                 fa.getSuffix());
 182         TKit.withTempFile(testFileDefaultName, fa.getSuffix(), testFile -> {
 183             if (TKit.isLinux()) {
 184                 LinuxHelper.initFileAssociationsTestFile(testFile);
 185             }
 186             consumer.accept(testFile);
 187         });
 188     }
 189 
 190     PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
 191             String... faLauncherDefaultArgs) {
 192 
 193         // Setup test app to have valid jpackage command line before
 194         // running check of type of environment.
 195         addInitializer(cmd -> new HelloApp(null).addTo(cmd), "HelloApp");
 196 
 197         String noActionMsg = "Not running file associations test";
 198         if (GraphicsEnvironment.isHeadless()) {
 199             TKit.trace(String.format(
 200                     "%s because running in headless environment", noActionMsg));
 201             return this;
 202         }
 203 
 204         addInstallVerifier(cmd -> {
 205             if (cmd.isFakeRuntime(noActionMsg)) {
 206                 return;
 207             }
 208 
 209             withTestFileAssociationsFile(fa, testFile -> {
 210                 testFile = testFile.toAbsolutePath().normalize();
 211 
 212                 final Path appOutput = testFile.getParent()
 213                         .resolve(HelloApp.OUTPUT_FILENAME);
 214                 Files.deleteIfExists(appOutput);
 215 
 216                 TKit.trace(String.format("Use desktop to open [%s] file",
 217                         testFile));
 218                 Desktop.getDesktop().open(testFile.toFile());
 219                 TKit.waitForFileCreated(appOutput, 7);
 220 
 221                 List<String> expectedArgs = new ArrayList<>(List.of(
 222                         faLauncherDefaultArgs));
 223                 expectedArgs.add(testFile.toString());
 224 
 225                 // Wait a little bit after file has been created to
 226                 // make sure there are no pending writes into it.
 227                 Thread.sleep(3000);
 228                 HelloApp.verifyOutputFile(appOutput, expectedArgs);
 229             });
 230         });
 231 
 232         forTypes(PackageType.LINUX, () -> {
 233             LinuxHelper.addFileAssociationsVerifier(this, fa);
 234         });
 235 
 236         return this;
 237     }
 238 
 239     PackageTest forTypes(Collection<PackageType> types, Runnable action) {
 240         Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
 241                 PackageType[]::new));
 242         try {
 243             forTypes(types);
 244             action.run();
 245         } finally {
 246             forTypes(oldTypes);
 247         }
 248         return this;
 249     }
 250 
 251     PackageTest forTypes(PackageType type, Runnable action) {
 252         return forTypes(List.of(type), action);
 253     }
 254 
 255     PackageTest notForTypes(Collection<PackageType> types, Runnable action) {
 256         Set<PackageType> workset = new HashSet<>(currentTypes);
 257         workset.removeAll(types);
 258         return forTypes(workset, action);
 259     }
 260 
 261     PackageTest notForTypes(PackageType type, Runnable action) {
 262         return notForTypes(List.of(type), action);
 263     }
 264 
 265     public PackageTest configureHelloApp() {
 266         return configureHelloApp(null);
 267     }
 268 
 269     public PackageTest configureHelloApp(String encodedName) {
 270         addInitializer(
 271                 cmd -> new HelloApp(JavaAppDesc.parse(encodedName)).addTo(cmd));
 272         addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
 273         return this;
 274     }
 275 
 276     public void run() {
 277         List<Handler> supportedHandlers = handlers.values().stream()
 278                 .filter(entry -> !entry.isVoid())
 279                 .collect(Collectors.toList());
 280 
 281         if (supportedHandlers.isEmpty()) {
 282             // No handlers with initializers found. Nothing to do.
 283             return;
 284         }
 285 
 286         Supplier<JPackageCommand> initializer = new Supplier<>() {
 287             @Override
 288             public JPackageCommand get() {
 289                 JPackageCommand cmd = new JPackageCommand().setDefaultInputOutput();
 290                 if (bundleOutputDir != null) {
 291                     cmd.setArgumentValue("--dest", bundleOutputDir.toString());
 292                 }
 293                 cmd.setDefaultAppName();
 294                 return cmd;
 295             }
 296         };
 297 
 298         supportedHandlers.forEach(handler -> handler.accept(initializer.get()));
 299     }
 300 
 301     public PackageTest setAction(Action value) {
 302         action = value;
 303         return this;
 304     }
 305 
 306     public Action getAction() {
 307         return action;
 308     }
 309 
 310     private class Handler implements Consumer<JPackageCommand> {
 311 
 312         Handler(PackageType type) {
 313             if (!PackageType.NATIVE.contains(type)) {
 314                 throw new IllegalArgumentException(
 315                         "Attempt to configure a test for image packaging");
 316             }
 317             this.type = type;
 318             initializers = new ArrayList<>();
 319             bundleVerifiers = new ArrayList<>();
 320             installVerifiers = new ArrayList<>();
 321             uninstallVerifiers = new ArrayList<>();
 322         }
 323 
 324         boolean isVoid() {
 325             return initializers.isEmpty();
 326         }
 327 
 328         void addInitializer(Consumer<JPackageCommand> v) {
 329             initializers.add(v);
 330         }
 331 
 332         void addBundleVerifier(BiConsumer<JPackageCommand, Executor.Result> v) {
 333             bundleVerifiers.add(v);
 334         }
 335 
 336         void addInstallVerifier(Consumer<JPackageCommand> v) {
 337             installVerifiers.add(v);
 338         }
 339 
 340         void addUninstallVerifier(Consumer<JPackageCommand> v) {
 341             uninstallVerifiers.add(v);
 342         }
 343 
 344         @Override
 345         public void accept(JPackageCommand cmd) {
 346             type.applyTo(cmd);
 347 
 348             initializers.stream().forEach(v -> v.accept(cmd));
 349             cmd.executePrerequisiteActions();
 350 
 351             switch (action) {
 352                 case CREATE:
 353                     Executor.Result result = cmd.execute();
 354                     result.assertExitCodeIs(expectedJPackageExitCode);
 355                     if (expectedJPackageExitCode == 0) {
 356                         TKit.assertFileExists(cmd.outputBundle());
 357                     } else {
 358                         TKit.assertPathExists(cmd.outputBundle(), false);
 359                     }
 360                     verifyPackageBundle(cmd.createImmutableCopy(), result);
 361                     break;
 362 
 363                 case VERIFY_INSTALL:
 364                     if (expectedJPackageExitCode == 0) {
 365                         verifyPackageInstalled(cmd.createImmutableCopy());
 366                     }
 367                     break;
 368 
 369                 case VERIFY_UNINSTALL:
 370                     if (expectedJPackageExitCode == 0) {
 371                         verifyPackageUninstalled(cmd.createImmutableCopy());
 372                     }
 373                     break;
 374             }
 375         }
 376 
 377         private void verifyPackageBundle(JPackageCommand cmd,
 378                 Executor.Result result) {
 379             if (expectedJPackageExitCode == 0) {
 380                 if (PackageType.LINUX.contains(cmd.packageType())) {
 381                     LinuxHelper.verifyPackageBundleEssential(cmd);
 382                 }
 383             }
 384             bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
 385         }
 386 
 387         private void verifyPackageInstalled(JPackageCommand cmd) {
 388             TKit.trace(String.format("Verify installed: %s",
 389                     cmd.getPrintableCommandLine()));
 390             TKit.assertDirectoryExists(cmd.appRuntimeDirectory());
 391             if (!cmd.isRuntime()) {
 392                 TKit.assertExecutableFileExists(cmd.appLauncherPath());
 393 
 394                 if (PackageType.WINDOWS.contains(cmd.packageType())) {
 395                     new WindowsHelper.AppVerifier(cmd);
 396                 }
 397             }
 398 
 399             TKit.assertPathExists(AppImageFile.getPathInAppImage(
 400                     cmd.appInstallationDirectory()), false);
 401 
 402             installVerifiers.stream().forEach(v -> v.accept(cmd));
 403         }
 404 
 405         private void verifyPackageUninstalled(JPackageCommand cmd) {
 406             TKit.trace(String.format("Verify uninstalled: %s",
 407                     cmd.getPrintableCommandLine()));
 408             if (!cmd.isRuntime()) {
 409                 TKit.assertPathExists(cmd.appLauncherPath(), false);
 410 
 411                 if (PackageType.WINDOWS.contains(cmd.packageType())) {
 412                     new WindowsHelper.AppVerifier(cmd);
 413                 }
 414             }
 415 
 416             TKit.assertPathExists(cmd.appInstallationDirectory(), false);
 417 
 418             uninstallVerifiers.stream().forEach(v -> v.accept(cmd));
 419         }
 420 
 421         private final PackageType type;
 422         private final List<Consumer<JPackageCommand>> initializers;
 423         private final List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers;
 424         private final List<Consumer<JPackageCommand>> installVerifiers;
 425         private final List<Consumer<JPackageCommand>> uninstallVerifiers;
 426     }
 427 
 428     private Collection<PackageType> currentTypes;
 429     private Set<PackageType> excludeTypes;
 430     private int expectedJPackageExitCode;
 431     private Map<PackageType, Handler> handlers;
 432     private Set<String> namedInitializers;
 433     private Action action;
 434 
 435     /**
 436      * Test action.
 437      */
 438     static public enum Action {
 439         /**
 440          * Create bundle.
 441          */
 442         CREATE,
 443         /**
 444          * Verify bundle installed.
 445          */
 446         VERIFY_INSTALL,
 447         /**
 448          * Verify bundle uninstalled.
 449          */
 450         VERIFY_UNINSTALL;
 451 
 452         @Override
 453         public String toString() {
 454             return name().toLowerCase().replace('_', '-');
 455         }
 456     };
 457     private final static Action DEFAULT_ACTION;
 458     private final static File bundleOutputDir;
 459 
 460     static {
 461         final String propertyName = "output";
 462         String val = TKit.getConfigProperty(propertyName);
 463         if (val == null) {
 464             bundleOutputDir = null;
 465         } else {
 466             bundleOutputDir = new File(val).getAbsoluteFile();
 467 
 468             if (!bundleOutputDir.isDirectory()) {
 469                 throw new IllegalArgumentException(String.format(
 470                         "Invalid value of %s sytem property: [%s]. Should be existing directory",
 471                         TKit.getConfigPropertyName(propertyName),
 472                         bundleOutputDir));
 473             }
 474         }
 475     }
 476 
 477     static {
 478         final String propertyName = "action";
 479         String action = Optional.ofNullable(TKit.getConfigProperty(propertyName)).orElse(
 480                 Action.CREATE.toString()).toLowerCase();
 481         DEFAULT_ACTION = Stream.of(Action.values()).filter(
 482                 a -> a.toString().equals(action)).findFirst().orElseThrow(
 483                         () -> new IllegalArgumentException(String.format(
 484                                 "Unrecognized value of %s property: [%s]",
 485                                 TKit.getConfigPropertyName(propertyName), action)));
 486     }
 487 }