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