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.io.BufferedReader;
  26 import java.io.File;
  27 import java.io.FileOutputStream;
  28 import java.io.FileReader;
  29 import java.io.IOException;
  30 import java.io.PrintStream;
  31 import java.lang.reflect.InvocationTargetException;
  32 import java.nio.file.FileSystems;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.nio.file.StandardWatchEventKinds;
  36 import java.nio.file.StandardCopyOption;
  37 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
  38 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
  39 import java.nio.file.WatchEvent;
  40 import java.nio.file.WatchKey;
  41 import java.nio.file.WatchService;
  42 import java.text.SimpleDateFormat;
  43 import java.util.ArrayList;
  44 import java.util.Collection;
  45 import java.util.Collections;
  46 import java.util.Comparator;
  47 import java.util.Date;
  48 import java.util.Iterator;
  49 import java.util.List;
  50 import java.util.Map;
  51 import java.util.Optional;
  52 import java.util.Set;
  53 import java.util.concurrent.TimeUnit;
  54 import java.util.concurrent.atomic.AtomicInteger;
  55 import java.util.function.BiPredicate;
  56 import java.util.function.Consumer;
  57 import java.util.function.Predicate;
  58 import java.util.function.Supplier;
  59 import java.util.stream.Collectors;
  60 import java.util.stream.Stream;
  61 import jdk.jpackage.test.Functional.ExceptionBox;
  62 import jdk.jpackage.test.Functional.ThrowingConsumer;
  63 import jdk.jpackage.test.Functional.ThrowingRunnable;
  64 import jdk.jpackage.test.Functional.ThrowingSupplier;
  65 
  66 final public class TKit {
  67 
  68     private static final String OS = System.getProperty("os.name").toLowerCase();
  69 
  70     public static final Path TEST_SRC_ROOT = Functional.identity(() -> {
  71         Path root = Path.of(System.getProperty("test.src"));
  72 
  73         for (int i = 0; i != 10; ++i) {
  74             if (root.resolve("apps").toFile().isDirectory()) {
  75                 return root.normalize().toAbsolutePath();
  76             }
  77             root = root.resolve("..");
  78         }
  79 
  80         throw new RuntimeException("Failed to locate apps directory");
  81     }).get();
  82 
  83     public static final Path SRC_ROOT = Functional.identity(() -> {
  84         return TEST_SRC_ROOT.resolve("../../../../src/jdk.incubator.jpackage").normalize().toAbsolutePath();
  85     }).get();
  86 
  87     public final static String ICON_SUFFIX = Functional.identity(() -> {
  88         if (isOSX()) {
  89             return ".icns";
  90         }
  91 
  92         if (isLinux()) {
  93             return ".png";
  94         }
  95 
  96         if (isWindows()) {
  97             return ".ico";
  98         }
  99 
 100         throw throwUnknownPlatformError();
 101     }).get();
 102 
 103     public static void run(String args[], ThrowingRunnable testBody) {
 104         if (currentTest != null) {
 105             throw new IllegalStateException(
 106                     "Unexpeced nested or concurrent Test.run() call");
 107         }
 108 
 109         TestInstance test = new TestInstance(testBody);
 110         ThrowingRunnable.toRunnable(() -> runTests(List.of(test))).run();
 111         test.rethrowIfSkipped();
 112         if (!test.passed()) {
 113             throw new RuntimeException();
 114         }
 115     }
 116 
 117     static void withExtraLogStream(ThrowingRunnable action) {
 118         if (extraLogStream != null) {
 119             ThrowingRunnable.toRunnable(action).run();
 120         } else {
 121             try (PrintStream logStream = openLogStream()) {
 122                 extraLogStream = logStream;
 123                 ThrowingRunnable.toRunnable(action).run();
 124             } finally {
 125                 extraLogStream = null;
 126             }
 127         }
 128     }
 129 
 130     static void runTests(List<TestInstance> tests) {
 131         if (currentTest != null) {
 132             throw new IllegalStateException(
 133                     "Unexpeced nested or concurrent Test.run() call");
 134         }
 135 
 136         withExtraLogStream(() -> {
 137             tests.stream().forEach(test -> {
 138                 currentTest = test;
 139                 try {
 140                     ignoreExceptions(test).run();
 141                 } finally {
 142                     currentTest = null;
 143                     if (extraLogStream != null) {
 144                         extraLogStream.flush();
 145                     }
 146                 }
 147             });
 148         });
 149     }
 150 
 151     static Runnable ignoreExceptions(ThrowingRunnable action) {
 152         return () -> {
 153             try {
 154                 try {
 155                     action.run();
 156                 } catch (Throwable ex) {
 157                     unbox(ex);
 158                 }
 159             } catch (Throwable throwable) {
 160                 printStackTrace(throwable);
 161             }
 162         };
 163     }
 164 
 165     static void unbox(Throwable throwable) throws Throwable {
 166         try {
 167             throw throwable;
 168         } catch (ExceptionBox | InvocationTargetException ex) {
 169             unbox(ex.getCause());
 170         }
 171     }
 172 
 173     public static Path workDir() {
 174         return currentTest.workDir();
 175     }
 176 
 177     static String getCurrentDefaultAppName() {
 178         // Construct app name from swapping and joining test base name
 179         // and test function name.
 180         // Say the test name is `FooTest.testBasic`. Then app name would be `BasicFooTest`.
 181         String appNamePrefix = currentTest.functionName();
 182         if (appNamePrefix != null && appNamePrefix.startsWith("test")) {
 183             appNamePrefix = appNamePrefix.substring("test".length());
 184         }
 185         return Stream.of(appNamePrefix, currentTest.baseName()).filter(
 186                 v -> v != null && !v.isEmpty()).collect(Collectors.joining());
 187     }
 188 
 189     public static boolean isWindows() {
 190         return (OS.contains("win"));
 191     }
 192 
 193     public static boolean isOSX() {
 194         return (OS.contains("mac"));
 195     }
 196 
 197     public static boolean isLinux() {
 198         return ((OS.contains("nix") || OS.contains("nux")));
 199     }
 200 
 201     public static boolean isUbuntu() {
 202         if (!isLinux()) {
 203             return false;
 204         }
 205         File releaseFile = new File("/etc/os-release");
 206         if (releaseFile.exists()) {
 207             try (BufferedReader lineReader = new BufferedReader(new FileReader(releaseFile))) {
 208                 String lineText = null;
 209                 while ((lineText = lineReader.readLine()) != null) {
 210                     if (lineText.indexOf("NAME=\"Ubuntu") != -1) {
 211                         lineReader.close();
 212                         return true;
 213                     }
 214                 }
 215             } catch (IOException e) {
 216                 e.printStackTrace();
 217             }
 218         }
 219         return false;
 220     }
 221 
 222     private static String addTimestamp(String msg) {
 223         SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
 224         Date time = new Date(System.currentTimeMillis());
 225         return String.format("[%s] %s", sdf.format(time), msg);
 226     }
 227 
 228     static void log(String v) {
 229         v = addTimestamp(v);
 230         System.out.println(v);
 231         if (extraLogStream != null) {
 232             extraLogStream.println(v);
 233         }
 234     }
 235 
 236     static Path removeRootFromAbsolutePath(Path v) {
 237         if (!v.isAbsolute()) {
 238             throw new IllegalArgumentException();
 239         }
 240 
 241         if (v.getNameCount() == 0) {
 242             return Path.of("");
 243         }
 244         return v.subpath(0, v.getNameCount());
 245     }
 246 
 247     public static void createTextFile(Path propsFilename, Collection<String> lines) {
 248         createTextFile(propsFilename, lines.stream());
 249     }
 250 
 251     public static void createTextFile(Path propsFilename, Stream<String> lines) {
 252         trace(String.format("Create [%s] text file...",
 253                 propsFilename.toAbsolutePath().normalize()));
 254         ThrowingRunnable.toRunnable(() -> Files.write(propsFilename,
 255                 lines.peek(TKit::trace).collect(Collectors.toList()))).run();
 256         trace("Done");
 257     }
 258 
 259     public static void createPropertiesFile(Path propsFilename,
 260             Collection<Map.Entry<String, String>> props) {
 261         trace(String.format("Create [%s] properties file...",
 262                 propsFilename.toAbsolutePath().normalize()));
 263         ThrowingRunnable.toRunnable(() -> Files.write(propsFilename,
 264                 props.stream().map(e -> String.join("=", e.getKey(),
 265                 e.getValue())).peek(TKit::trace).collect(Collectors.toList()))).run();
 266         trace("Done");
 267     }
 268 
 269     public static void createPropertiesFile(Path propsFilename,
 270             Map.Entry<String, String>... props) {
 271         createPropertiesFile(propsFilename, List.of(props));
 272     }
 273 
 274     public static void createPropertiesFile(Path propsFilename,
 275             Map<String, String> props) {
 276         createPropertiesFile(propsFilename, props.entrySet());
 277     }
 278 
 279     public static void trace(String v) {
 280         if (TRACE) {
 281             log("TRACE: " + v);
 282         }
 283     }
 284 
 285     private static void traceAssert(String v) {
 286         if (TRACE_ASSERTS) {
 287             log("TRACE: " + v);
 288         }
 289     }
 290 
 291     public static void error(String v) {
 292         log("ERROR: " + v);
 293         throw new AssertionError(v);
 294     }
 295 
 296     private final static String TEMP_FILE_PREFIX = null;
 297 
 298     private static Path createUniqueFileName(String defaultName) {
 299         final String[] nameComponents;
 300 
 301         int separatorIdx = defaultName.lastIndexOf('.');
 302         final String baseName;
 303         if (separatorIdx == -1) {
 304             baseName = defaultName;
 305             nameComponents = new String[]{baseName};
 306         } else {
 307             baseName = defaultName.substring(0, separatorIdx);
 308             nameComponents = new String[]{baseName, defaultName.substring(
 309                 separatorIdx + 1)};
 310         }
 311 
 312         final Path basedir = workDir();
 313         int i = 0;
 314         for (; i < 100; ++i) {
 315             Path path = basedir.resolve(String.join(".", nameComponents));
 316             if (!path.toFile().exists()) {
 317                 return path;
 318             }
 319             nameComponents[0] = String.format("%s.%d", baseName, i);
 320         }
 321         throw new IllegalStateException(String.format(
 322                 "Failed to create unique file name from [%s] basename after %d attempts",
 323                 baseName, i));
 324     }
 325 
 326     public static Path createTempDirectory(String role) throws IOException {
 327         if (role == null) {
 328             return Files.createTempDirectory(workDir(), TEMP_FILE_PREFIX);
 329         }
 330         return Files.createDirectory(createUniqueFileName(role));
 331     }
 332 
 333     public static Path createTempFile(Path templateFile) throws
 334             IOException {
 335         return Files.createFile(createUniqueFileName(
 336                 templateFile.getFileName().toString()));
 337     }
 338 
 339     public static Path withTempFile(Path templateFile,
 340             ThrowingConsumer<Path> action) {
 341         final Path tempFile = ThrowingSupplier.toSupplier(() -> createTempFile(
 342                 templateFile)).get();
 343         boolean keepIt = true;
 344         try {
 345             ThrowingConsumer.toConsumer(action).accept(tempFile);
 346             keepIt = false;
 347             return tempFile;
 348         } finally {
 349             if (tempFile != null && !keepIt) {
 350                 ThrowingRunnable.toRunnable(() -> Files.deleteIfExists(tempFile)).run();
 351             }
 352         }
 353     }
 354 
 355     public static Path withTempDirectory(String role,
 356             ThrowingConsumer<Path> action) {
 357         final Path tempDir = ThrowingSupplier.toSupplier(
 358                 () -> createTempDirectory(role)).get();
 359         boolean keepIt = true;
 360         try {
 361             ThrowingConsumer.toConsumer(action).accept(tempDir);
 362             keepIt = false;
 363             return tempDir;
 364         } finally {
 365             if (tempDir != null && tempDir.toFile().isDirectory() && !keepIt) {
 366                 deleteDirectoryRecursive(tempDir, "");
 367             }
 368         }
 369     }
 370 
 371     private static class DirectoryCleaner implements Consumer<Path> {
 372         DirectoryCleaner traceMessage(String v) {
 373             msg = v;
 374             return this;
 375         }
 376 
 377         DirectoryCleaner contentsOnly(boolean v) {
 378             contentsOnly = v;
 379             return this;
 380         }
 381 
 382         @Override
 383         public void accept(Path root) {
 384             if (msg == null) {
 385                 if (contentsOnly) {
 386                     msg = String.format("Cleaning [%s] directory recursively",
 387                             root);
 388                 } else {
 389                     msg = String.format("Deleting [%s] directory recursively",
 390                             root);
 391                 }
 392             }
 393 
 394             if (!msg.isEmpty()) {
 395                 trace(msg);
 396             }
 397 
 398             List<Throwable> errors = new ArrayList<>();
 399             try {
 400                 final List<Path> paths;
 401                 if (contentsOnly) {
 402                     try (var pathStream = Files.walk(root, 0)) {
 403                         paths = pathStream.collect(Collectors.toList());
 404                     }
 405                 } else {
 406                     paths = List.of(root);
 407                 }
 408 
 409                 for (var path : paths) {
 410                     try (var pathStream = Files.walk(path)) {
 411                         pathStream
 412                         .sorted(Comparator.reverseOrder())
 413                         .sequential()
 414                         .forEachOrdered(file -> {
 415                             try {
 416                                 if (isWindows()) {
 417                                     Files.setAttribute(file, "dos:readonly", false);
 418                                 }
 419                                 Files.delete(file);
 420                             } catch (IOException ex) {
 421                                 errors.add(ex);
 422                             }
 423                         });
 424                     }
 425                 }
 426 
 427             } catch (IOException ex) {
 428                 errors.add(ex);
 429             }
 430             errors.forEach(error -> trace(error.toString()));
 431         }
 432 
 433         private String msg;
 434         private boolean contentsOnly;
 435     }
 436 
 437     public static boolean deleteIfExists(Path path) throws IOException {
 438         if (isWindows()) {
 439             if (path.toFile().exists()) {
 440                 Files.setAttribute(path, "dos:readonly", false);
 441             }
 442         }
 443         return Files.deleteIfExists(path);
 444     }
 445 
 446     /**
 447      * Deletes contents of the given directory recursively. Shortcut for
 448      * <code>deleteDirectoryContentsRecursive(path, null)</code>
 449      *
 450      * @param path path to directory to clean
 451      */
 452     public static void deleteDirectoryContentsRecursive(Path path) {
 453         deleteDirectoryContentsRecursive(path, null);
 454     }
 455 
 456     /**
 457      * Deletes contents of the given directory recursively. If <code>path<code> is not a
 458      * directory, request is silently ignored.
 459      *
 460      * @param path path to directory to clean
 461      * @param msg log message. If null, the default log message is used. If
 462      * empty string, no log message will be saved.
 463      */
 464     public static void deleteDirectoryContentsRecursive(Path path, String msg) {
 465         if (path.toFile().isDirectory()) {
 466             new DirectoryCleaner().contentsOnly(true).traceMessage(msg).accept(
 467                     path);
 468         }
 469     }
 470 
 471     /**
 472      * Deletes the given directory recursively. Shortcut for
 473      * <code>deleteDirectoryRecursive(path, null)</code>
 474      *
 475      * @param path path to directory to delete
 476      */
 477     public static void deleteDirectoryRecursive(Path path) {
 478         deleteDirectoryRecursive(path, null);
 479     }
 480 
 481     /**
 482      * Deletes the given directory recursively. If <code>path<code> is not a
 483      * directory, request is silently ignored.
 484      *
 485      * @param path path to directory to delete
 486      * @param msg log message. If null, the default log message is used. If
 487      * empty string, no log message will be saved.
 488      */
 489     public static void deleteDirectoryRecursive(Path path, String msg) {
 490         if (path.toFile().isDirectory()) {
 491             new DirectoryCleaner().traceMessage(msg).accept(path);
 492         }
 493     }
 494 
 495     public static RuntimeException throwUnknownPlatformError() {
 496         if (isWindows() || isLinux() || isOSX()) {
 497             throw new IllegalStateException(
 498                     "Platform is known. throwUnknownPlatformError() called by mistake");
 499         }
 500         throw new IllegalStateException("Unknown platform");
 501     }
 502 
 503     public static RuntimeException throwSkippedException(String reason) {
 504         trace("Skip the test: " + reason);
 505         RuntimeException ex = ThrowingSupplier.toSupplier(
 506                 () -> (RuntimeException) Class.forName("jtreg.SkippedException").getConstructor(
 507                         String.class).newInstance(reason)).get();
 508 
 509         currentTest.notifySkipped(ex);
 510         throw ex;
 511     }
 512 
 513     public static Path createRelativePathCopy(final Path file) {
 514         Path fileCopy = ThrowingSupplier.toSupplier(() -> {
 515             Path localPath = createTempFile(file);
 516             Files.copy(file, localPath, StandardCopyOption.REPLACE_EXISTING);
 517             return localPath;
 518         }).get().toAbsolutePath().normalize();
 519 
 520         final Path basePath = Path.of(".").toAbsolutePath().normalize();
 521         try {
 522             return basePath.relativize(fileCopy);
 523         } catch (IllegalArgumentException ex) {
 524             // May happen on Windows: java.lang.IllegalArgumentException: 'other' has different root
 525             trace(String.format("Failed to relativize [%s] at [%s]", fileCopy,
 526                     basePath));
 527             printStackTrace(ex);
 528         }
 529         return file;
 530     }
 531 
 532     static void waitForFileCreated(Path fileToWaitFor,
 533             long timeoutSeconds) throws IOException {
 534 
 535         trace(String.format("Wait for file [%s] to be available",
 536                                                 fileToWaitFor.toAbsolutePath()));
 537 
 538         WatchService ws = FileSystems.getDefault().newWatchService();
 539 
 540         Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
 541         watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
 542 
 543         long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000;
 544         for (;;) {
 545             long timeout = waitUntil - System.currentTimeMillis();
 546             assertTrue(timeout > 0, String.format(
 547                     "Check timeout value %d is positive", timeout));
 548 
 549             WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout,
 550                     TimeUnit.MILLISECONDS)).get();
 551             if (key == null) {
 552                 if (fileToWaitFor.toFile().exists()) {
 553                     trace(String.format(
 554                             "File [%s] is available after poll timeout expired",
 555                             fileToWaitFor));
 556                     return;
 557                 }
 558                 assertUnexpected(String.format("Timeout expired", timeout));
 559             }
 560 
 561             for (WatchEvent<?> event : key.pollEvents()) {
 562                 if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
 563                     continue;
 564                 }
 565                 Path contextPath = (Path) event.context();
 566                 if (Files.isSameFile(watchDirectory.resolve(contextPath),
 567                         fileToWaitFor)) {
 568                     trace(String.format("File [%s] is available", fileToWaitFor));
 569                     return;
 570                 }
 571             }
 572 
 573             if (!key.reset()) {
 574                 assertUnexpected("Watch key invalidated");
 575             }
 576         }
 577     }
 578 
 579     static void printStackTrace(Throwable throwable) {
 580         if (extraLogStream != null) {
 581             throwable.printStackTrace(extraLogStream);
 582         }
 583         throwable.printStackTrace();
 584     }
 585 
 586     private static String concatMessages(String msg, String msg2) {
 587         if (msg2 != null && !msg2.isBlank()) {
 588             return msg + ": " + msg2;
 589         }
 590         return msg;
 591     }
 592 
 593     public static void assertEquals(long expected, long actual, String msg) {
 594         currentTest.notifyAssert();
 595         if (expected != actual) {
 596             error(concatMessages(String.format(
 597                     "Expected [%d]. Actual [%d]", expected, actual),
 598                     msg));
 599         }
 600 
 601         traceAssert(String.format("assertEquals(%d): %s", expected, msg));
 602     }
 603 
 604     public static void assertNotEquals(long expected, long actual, String msg) {
 605         currentTest.notifyAssert();
 606         if (expected == actual) {
 607             error(concatMessages(String.format("Unexpected [%d] value", actual),
 608                     msg));
 609         }
 610 
 611         traceAssert(String.format("assertNotEquals(%d, %d): %s", expected,
 612                 actual, msg));
 613     }
 614 
 615     public static void assertEquals(String expected, String actual, String msg) {
 616         currentTest.notifyAssert();
 617         if ((actual != null && !actual.equals(expected))
 618                 || (expected != null && !expected.equals(actual))) {
 619             error(concatMessages(String.format(
 620                     "Expected [%s]. Actual [%s]", expected, actual),
 621                     msg));
 622         }
 623 
 624         traceAssert(String.format("assertEquals(%s): %s", expected, msg));
 625     }
 626 
 627     public static void assertNotEquals(String expected, String actual, String msg) {
 628         currentTest.notifyAssert();
 629         if ((actual != null && !actual.equals(expected))
 630                 || (expected != null && !expected.equals(actual))) {
 631 
 632             traceAssert(String.format("assertNotEquals(%s, %s): %s", expected,
 633                 actual, msg));
 634             return;
 635         }
 636 
 637         error(concatMessages(String.format("Unexpected [%s] value", actual), msg));
 638     }
 639 
 640     public static void assertNull(Object value, String msg) {
 641         currentTest.notifyAssert();
 642         if (value != null) {
 643             error(concatMessages(String.format("Unexpected not null value [%s]",
 644                     value), msg));
 645         }
 646 
 647         traceAssert(String.format("assertNull(): %s", msg));
 648     }
 649 
 650     public static void assertNotNull(Object value, String msg) {
 651         currentTest.notifyAssert();
 652         if (value == null) {
 653             error(concatMessages("Unexpected null value", msg));
 654         }
 655 
 656         traceAssert(String.format("assertNotNull(%s): %s", value, msg));
 657     }
 658 
 659     public static void assertTrue(boolean actual, String msg) {
 660         assertTrue(actual, msg, null);
 661     }
 662 
 663     public static void assertFalse(boolean actual, String msg) {
 664         assertFalse(actual, msg, null);
 665     }
 666 
 667     public static void assertTrue(boolean actual, String msg, Runnable onFail) {
 668         currentTest.notifyAssert();
 669         if (!actual) {
 670             if (onFail != null) {
 671                 onFail.run();
 672             }
 673             error(concatMessages("Failed", msg));
 674         }
 675 
 676         traceAssert(String.format("assertTrue(): %s", msg));
 677     }
 678 
 679     public static void assertFalse(boolean actual, String msg, Runnable onFail) {
 680         currentTest.notifyAssert();
 681         if (actual) {
 682             if (onFail != null) {
 683                 onFail.run();
 684             }
 685             error(concatMessages("Failed", msg));
 686         }
 687 
 688         traceAssert(String.format("assertFalse(): %s", msg));
 689     }
 690 
 691     public static void assertPathExists(Path path, boolean exists) {
 692         if (exists) {
 693             assertTrue(path.toFile().exists(), String.format(
 694                     "Check [%s] path exists", path));
 695         } else {
 696             assertFalse(path.toFile().exists(), String.format(
 697                     "Check [%s] path doesn't exist", path));
 698         }
 699     }
 700 
 701      public static void assertDirectoryExists(Path path) {
 702         assertPathExists(path, true);
 703         assertTrue(path.toFile().isDirectory(), String.format(
 704                 "Check [%s] is a directory", path));
 705     }
 706 
 707     public static void assertFileExists(Path path) {
 708         assertPathExists(path, true);
 709         assertTrue(path.toFile().isFile(), String.format("Check [%s] is a file",
 710                 path));
 711     }
 712 
 713     public static void assertExecutableFileExists(Path path) {
 714         assertFileExists(path);
 715         assertTrue(path.toFile().canExecute(), String.format(
 716                 "Check [%s] file is executable", path));
 717     }
 718 
 719     public static void assertReadableFileExists(Path path) {
 720         assertFileExists(path);
 721         assertTrue(path.toFile().canRead(), String.format(
 722                 "Check [%s] file is readable", path));
 723     }
 724 
 725     public static void assertUnexpected(String msg) {
 726         currentTest.notifyAssert();
 727         error(concatMessages("Unexpected", msg));
 728     }
 729 
 730     public static void assertStringListEquals(List<String> expected,
 731             List<String> actual, String msg) {
 732         currentTest.notifyAssert();
 733 
 734         traceAssert(String.format("assertStringListEquals(): %s", msg));
 735 
 736         String idxFieldFormat = Functional.identity(() -> {
 737             int listSize = expected.size();
 738             int width = 0;
 739             while (listSize != 0) {
 740                 listSize = listSize / 10;
 741                 width++;
 742             }
 743             return "%" + width + "d";
 744         }).get();
 745 
 746         AtomicInteger counter = new AtomicInteger(0);
 747         Iterator<String> actualIt = actual.iterator();
 748         expected.stream().sequential().filter(expectedStr -> actualIt.hasNext()).forEach(expectedStr -> {
 749             int idx = counter.incrementAndGet();
 750             String actualStr = actualIt.next();
 751 
 752             if ((actualStr != null && !actualStr.equals(expectedStr))
 753                     || (expectedStr != null && !expectedStr.equals(actualStr))) {
 754                 error(concatMessages(String.format(
 755                         "(" + idxFieldFormat + ") Expected [%s]. Actual [%s]",
 756                         idx, expectedStr, actualStr), msg));
 757             }
 758 
 759             traceAssert(String.format(
 760                     "assertStringListEquals(" + idxFieldFormat + ", %s)", idx,
 761                     expectedStr));
 762         });
 763 
 764         if (expected.size() < actual.size()) {
 765             // Actual string list is longer than expected
 766             error(concatMessages(String.format(
 767                     "Actual list is longer than expected by %d elements",
 768                     actual.size() - expected.size()), msg));
 769         }
 770 
 771         if (actual.size() < expected.size()) {
 772             // Actual string list is shorter than expected
 773             error(concatMessages(String.format(
 774                     "Actual list is longer than expected by %d elements",
 775                     expected.size() - actual.size()), msg));
 776         }
 777     }
 778 
 779     public final static class TextStreamVerifier {
 780         TextStreamVerifier(String value) {
 781             this.value = value;
 782             predicate(String::contains);
 783         }
 784 
 785         public TextStreamVerifier label(String v) {
 786             label = v;
 787             return this;
 788         }
 789 
 790         public TextStreamVerifier predicate(BiPredicate<String, String> v) {
 791             predicate = v;
 792             return this;
 793         }
 794 
 795         public TextStreamVerifier negate() {
 796             negate = true;
 797             return this;
 798         }
 799 
 800         public TextStreamVerifier orElseThrow(RuntimeException v) {
 801             return orElseThrow(() -> v);
 802         }
 803 
 804         public TextStreamVerifier orElseThrow(Supplier<RuntimeException> v) {
 805             createException = v;
 806             return this;
 807         }
 808 
 809         public void apply(Stream<String> lines) {
 810             String matchedStr = lines.filter(line -> predicate.test(line, value)).findFirst().orElse(
 811                     null);
 812             String labelStr = Optional.ofNullable(label).orElse("output");
 813             if (negate) {
 814                 String msg = String.format(
 815                         "Check %s doesn't contain [%s] string", labelStr, value);
 816                 if (createException == null) {
 817                     assertNull(matchedStr, msg);
 818                 } else {
 819                     trace(msg);
 820                     if (matchedStr != null) {
 821                         throw createException.get();
 822                     }
 823                 }
 824             } else {
 825                 String msg = String.format("Check %s contains [%s] string",
 826                         labelStr, value);
 827                 if (createException == null) {
 828                     assertNotNull(matchedStr, msg);
 829                 } else {
 830                     trace(msg);
 831                     if (matchedStr == null) {
 832                         throw createException.get();
 833                     }
 834                 }
 835             }
 836         }
 837 
 838         private BiPredicate<String, String> predicate;
 839         private String label;
 840         private boolean negate;
 841         private Supplier<RuntimeException> createException;
 842         final private String value;
 843     }
 844 
 845     public static TextStreamVerifier assertTextStream(String what) {
 846         return new TextStreamVerifier(what);
 847     }
 848 
 849     private static PrintStream openLogStream() {
 850         if (LOG_FILE == null) {
 851             return null;
 852         }
 853 
 854         return ThrowingSupplier.toSupplier(() -> new PrintStream(
 855                 new FileOutputStream(LOG_FILE.toFile(), true))).get();
 856     }
 857 
 858     private static TestInstance currentTest;
 859     private static PrintStream extraLogStream;
 860 
 861     private static final boolean TRACE;
 862     private static final boolean TRACE_ASSERTS;
 863 
 864     static final boolean VERBOSE_JPACKAGE;
 865     static final boolean VERBOSE_TEST_SETUP;
 866 
 867     static String getConfigProperty(String propertyName) {
 868         return System.getProperty(getConfigPropertyName(propertyName));
 869     }
 870 
 871     static String getConfigPropertyName(String propertyName) {
 872         return "jpackage.test." + propertyName;
 873     }
 874 
 875     static List<String> tokenizeConfigPropertyAsList(String propertyName) {
 876         final String val = TKit.getConfigProperty(propertyName);
 877         if (val == null) {
 878             return null;
 879         }
 880         return Stream.of(val.toLowerCase().split(","))
 881                 .map(String::strip)
 882                 .filter(Predicate.not(String::isEmpty))
 883                 .collect(Collectors.toList());
 884     }
 885 
 886     static Set<String> tokenizeConfigProperty(String propertyName) {
 887         List<String> tokens = tokenizeConfigPropertyAsList(propertyName);
 888         if (tokens == null) {
 889             return null;
 890         }
 891         return tokens.stream().collect(Collectors.toSet());
 892     }
 893 
 894     static final Path LOG_FILE = Functional.identity(() -> {
 895         String val = getConfigProperty("logfile");
 896         if (val == null) {
 897             return null;
 898         }
 899         return Path.of(val);
 900     }).get();
 901 
 902     static {
 903         Set<String> logOptions = tokenizeConfigProperty("suppress-logging");
 904         if (logOptions == null) {
 905             TRACE = true;
 906             TRACE_ASSERTS = true;
 907             VERBOSE_JPACKAGE = true;
 908             VERBOSE_TEST_SETUP = true;
 909         } else if (logOptions.contains("all")) {
 910             TRACE = false;
 911             TRACE_ASSERTS = false;
 912             VERBOSE_JPACKAGE = false;
 913             VERBOSE_TEST_SETUP = false;
 914         } else {
 915             Predicate<Set<String>> isNonOf = options -> {
 916                 return Collections.disjoint(logOptions, options);
 917             };
 918 
 919             TRACE = isNonOf.test(Set.of("trace", "t"));
 920             TRACE_ASSERTS = isNonOf.test(Set.of("assert", "a"));
 921             VERBOSE_JPACKAGE = isNonOf.test(Set.of("jpackage", "jp"));
 922             VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i"));
 923         }
 924     }
 925 }