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