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