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