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 }