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 }