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.File;
  26 import java.io.FileNotFoundException;
  27 import java.io.FileOutputStream;
  28 import java.io.IOException;
  29 import java.io.PrintStream;
  30 import java.nio.file.FileSystems;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.nio.file.StandardWatchEventKinds;
  34 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
  35 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
  36 import java.nio.file.WatchEvent;
  37 import java.nio.file.WatchKey;
  38 import java.nio.file.WatchService;
  39 import java.util.Collection;
  40 import java.util.Comparator;
  41 import java.util.Iterator;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.concurrent.TimeUnit;
  46 import java.util.concurrent.atomic.AtomicInteger;
  47 import java.util.function.Supplier;
  48 import java.util.stream.Collectors;
  49 import jdk.jpackage.test.Functional.ThrowingConsumer;
  50 import jdk.jpackage.test.Functional.ThrowingRunnable;
  51 import jdk.jpackage.test.Functional.ThrowingSupplier;
  52 
  53 final public class Test {
  54 
  55     public static final Path TEST_SRC_ROOT = new Supplier<Path>() {
  56         @Override
  57         public Path get() {
  58             Path root = Path.of(System.getProperty("test.src"));
  59 
  60             for (int i = 0; i != 10; ++i) {
  61                 if (root.resolve("apps").toFile().isDirectory()) {
  62                     return root.toAbsolutePath();
  63                 }
  64                 root = root.resolve("..");
  65             }
  66 
  67             throw new RuntimeException("Failed to locate apps directory");
  68         }
  69     }.get();
  70 
  71     private static class Instance implements AutoCloseable {
  72         Instance(String args[]) {
  73             assertCount = 0;
  74 
  75             name = enclosingMainMethodClass().getSimpleName();
  76             extraLogStream = openLogStream();
  77 
  78             currentTest = this;
  79 
  80             log(String.format("[ RUN      ] %s", name));
  81         }
  82 
  83         @Override
  84         public void close() {
  85             log(String.format("%s %s; checks=%d",
  86                     success ? "[       OK ]" : "[  FAILED  ]", name, assertCount));
  87 
  88             if (extraLogStream != null) {
  89                 extraLogStream.close();
  90             }
  91         }
  92 
  93         void notifyAssert() {
  94             assertCount++;
  95         }
  96 
  97         void notifySuccess() {
  98             success = true;
  99         }
 100 
 101         private int assertCount;
 102         private boolean success;
 103         private final String name;
 104         private final PrintStream extraLogStream;
 105     }
 106 
 107     public static void run(String args[], TestBody action) {
 108         if (currentTest != null) {
 109             throw new IllegalStateException(
 110                     "Unexpeced nested or concurrent Test.run() call");
 111         }
 112 
 113         try (Instance instance = new Instance(args)) {
 114             action.run();
 115             instance.notifySuccess();
 116         } catch (Exception ex) {
 117             throw new RuntimeException(ex);
 118         } finally {
 119             currentTest = null;
 120         }
 121     }
 122 
 123     public static interface TestBody {
 124         public void run() throws Exception;
 125     }
 126 
 127     public static Path workDir() {
 128         return Path.of(".");
 129     }
 130 
 131     static Path defaultInputDir() {
 132         return workDir().resolve("input");
 133     }
 134 
 135     static Path defaultOutputDir() {
 136         return workDir().resolve("output");
 137     }
 138 
 139     static Class enclosingMainMethodClass() {
 140         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 141         for (StackTraceElement ste : st) {
 142             if ("main".equals(ste.getMethodName())) {
 143                 try {
 144                     return Class.forName(ste.getClassName());
 145                 } catch (ClassNotFoundException ex) {
 146                     throw new RuntimeException(ex);
 147                 }
 148             }
 149         }
 150         return null;
 151     }
 152 
 153     static boolean isWindows() {
 154         return (OS.contains("win"));
 155     }
 156 
 157     static boolean isOSX() {
 158         return (OS.contains("mac"));
 159     }
 160 
 161     static boolean isLinux() {
 162         return ((OS.contains("nix") || OS.contains("nux")));
 163     }
 164 
 165     static private void log(String v) {
 166         System.out.println(v);
 167         if (currentTest != null && currentTest.extraLogStream != null) {
 168             currentTest.extraLogStream.println(v);
 169         }
 170     }
 171 
 172     public static Class getTestClass () {
 173         return enclosingMainMethodClass();
 174     }
 175 
 176     public static void createPropertiesFile(Path propsFilename,
 177             Collection<Map.Entry<String, String>> props) {
 178         trace(String.format("Create [%s] properties file...",
 179                 propsFilename.toAbsolutePath().normalize()));
 180         try {
 181             Files.write(propsFilename, props.stream().peek(e -> trace(
 182                     String.format("%s=%s", e.getKey(), e.getValue()))).map(
 183                     e -> String.format("%s=%s", e.getKey(), e.getValue())).collect(
 184                             Collectors.toList()));
 185         } catch (IOException ex) {
 186             throw new RuntimeException(ex);
 187         }
 188         trace("Done");
 189     }
 190 
 191     public static void createPropertiesFile(Path propsFilename,
 192             Map.Entry<String, String>... props) {
 193         createPropertiesFile(propsFilename, List.of(props));
 194     }
 195 
 196     public static void createPropertiesFile(Path propsFilename,
 197             Map<String, String> props) {
 198         createPropertiesFile(propsFilename, props.entrySet());
 199     }
 200 
 201     public static void trace(String v) {
 202         if (TRACE) {
 203             log("TRACE: " + v);
 204         }
 205     }
 206 
 207     private static void traceAssert(String v) {
 208         if (TRACE_ASSERTS) {
 209             log("TRACE: " + v);
 210         }
 211     }
 212 
 213     public static void error(String v) {
 214         log("ERROR: " + v);
 215         throw new AssertionError(v);
 216     }
 217 
 218     private static final String TEMP_FILE_PREFIX = null;
 219 
 220     public static Path createTempDirectory() throws IOException {
 221         return Files.createTempDirectory(workDir(), TEMP_FILE_PREFIX);
 222     }
 223 
 224     public static Path createTempFile(String suffix) throws IOException {
 225         return Files.createTempFile(workDir(), TEMP_FILE_PREFIX, suffix);
 226     }
 227 
 228     public static void withTempFile(String suffix, ThrowingConsumer<Path> action) {
 229         final Path tempFile = ThrowingSupplier.toSupplier(() -> createTempFile(
 230                 suffix)).get();
 231         boolean keepIt = true;
 232         try {
 233             ThrowingConsumer.toConsumer(action).accept(tempFile);
 234             keepIt = false;
 235         } finally {
 236             if (tempFile != null && !keepIt) {
 237                 ThrowingRunnable.toRunnable(() -> Files.deleteIfExists(tempFile)).run();
 238             }
 239         }
 240     }
 241 
 242     public static void withTempDirectory(ThrowingConsumer<Path> action) {
 243         final Path tempDir = ThrowingSupplier.toSupplier(
 244                 () -> createTempDirectory()).get();
 245         boolean keepIt = true;
 246         try {
 247             ThrowingConsumer.toConsumer(action).accept(tempDir);
 248             keepIt = false;
 249         } finally {
 250             if (tempDir != null && tempDir.toFile().isDirectory() && !keepIt) {
 251                 deleteDirectoryRecursive(tempDir);
 252             }
 253         }
 254     }
 255 
 256     static void deleteDirectoryRecursive(Path path) {
 257         ThrowingRunnable.toRunnable(() -> Files.walk(path).sorted(
 258                 Comparator.reverseOrder()).map(Path::toFile).forEach(
 259                 File::delete)).run();
 260     }
 261 
 262     static void waitForFileCreated(Path fileToWaitFor,
 263             long timeoutSeconds) throws IOException {
 264 
 265         trace(String.format("Wait for file [%s] to be available", fileToWaitFor));
 266 
 267         WatchService ws = FileSystems.getDefault().newWatchService();
 268 
 269         Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
 270         watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
 271 
 272         long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000;
 273         for (;;) {
 274             long timeout = waitUntil - System.currentTimeMillis();
 275             assertTrue(timeout > 0, String.format(
 276                     "Check timeout value %d is positive", timeout));
 277 
 278             WatchKey key = null;
 279             try {
 280                 key = ws.poll(timeout, TimeUnit.MILLISECONDS);
 281             } catch (InterruptedException ex) {
 282                 throw new RuntimeException(ex);
 283             }
 284 
 285             if (key == null) {
 286                 if (fileToWaitFor.toFile().exists()) {
 287                     trace(String.format(
 288                             "File [%s] is available after poll timeout expired",
 289                             fileToWaitFor));
 290                     return;
 291                 }
 292                 assertUnexpected(String.format("Timeout expired", timeout));
 293             }
 294 
 295             for (WatchEvent<?> event : key.pollEvents()) {
 296                 if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
 297                     continue;
 298                 }
 299                 Path contextPath = (Path) event.context();
 300                 if (Files.isSameFile(watchDirectory.resolve(contextPath),
 301                         fileToWaitFor)) {
 302                     trace(String.format("File [%s] is available", fileToWaitFor));
 303                     return;
 304                 }
 305             }
 306 
 307             if (!key.reset()) {
 308                 assertUnexpected("Watch key invalidated");
 309             }
 310         }
 311     }
 312 
 313     private static String concatMessages(String msg, String msg2) {
 314         if (msg2 != null && !msg2.isBlank()) {
 315             return msg + ": " + msg2;
 316         }
 317         return msg;
 318     }
 319 
 320     public static void assertEquals(long expected, long actual, String msg) {
 321         currentTest.notifyAssert();
 322         if (expected != actual) {
 323             error(concatMessages(String.format(
 324                     "Expected [%d]. Actual [%d]", expected, actual),
 325                     msg));
 326         }
 327 
 328         traceAssert(String.format("assertEquals(%d): %s", expected, msg));
 329     }
 330 
 331     public static void assertNotEquals(long expected, long actual, String msg) {
 332         currentTest.notifyAssert();
 333         if (expected == actual) {
 334             error(concatMessages(String.format("Unexpected [%d] value", actual),
 335                     msg));
 336         }
 337 
 338         traceAssert(String.format("assertNotEquals(%d, %d): %s", expected,
 339                 actual, msg));
 340     }
 341 
 342     public static void assertEquals(String expected, String actual, String msg) {
 343         currentTest.notifyAssert();
 344         if ((actual != null && !actual.equals(expected))
 345                 || (expected != null && !expected.equals(actual))) {
 346             error(concatMessages(String.format(
 347                     "Expected [%s]. Actual [%s]", expected, actual),
 348                     msg));
 349         }
 350 
 351         traceAssert(String.format("assertEquals(%s): %s", expected, msg));
 352     }
 353 
 354     public static void assertNotEquals(String expected, String actual, String msg) {
 355         currentTest.notifyAssert();
 356         if ((actual != null && !actual.equals(expected))
 357                 || (expected != null && !expected.equals(actual))) {
 358 
 359             traceAssert(String.format("assertNotEquals(%s, %s): %s", expected,
 360                 actual, msg));
 361             return;
 362         }
 363 
 364         error(concatMessages(String.format("Unexpected [%s] value", actual), msg));
 365     }
 366 
 367     public static void assertNull(Object value, String msg) {
 368         currentTest.notifyAssert();
 369         if (value != null) {
 370             error(concatMessages(String.format("Unexpected not null value [%s]",
 371                     value), msg));
 372         }
 373 
 374         traceAssert(String.format("assertNull(): %s", msg));
 375     }
 376 
 377     public static void assertNotNull(Object value, String msg) {
 378         currentTest.notifyAssert();
 379         if (value == null) {
 380             error(concatMessages("Unexpected null value", msg));
 381         }
 382 
 383         traceAssert(String.format("assertNotNull(%s): %s", value, msg));
 384     }
 385 
 386     public static void assertTrue(boolean actual, String msg) {
 387         currentTest.notifyAssert();
 388         if (!actual) {
 389             error(concatMessages("Unexpected FALSE", msg));
 390         }
 391 
 392         traceAssert(String.format("assertTrue(): %s", msg));
 393     }
 394 
 395     public static void assertFalse(boolean actual, String msg) {
 396         currentTest.notifyAssert();
 397         if (actual) {
 398             error(concatMessages("Unexpected TRUE", msg));
 399         }
 400 
 401         traceAssert(String.format("assertFalse(): %s", msg));
 402     }
 403 
 404     public static void assertPathExists(Path path, boolean exists) {
 405         if (exists) {
 406             assertTrue(path.toFile().exists(), String.format(
 407                     "Check [%s] path exists", path));
 408         } else {
 409             assertFalse(path.toFile().exists(), String.format(
 410                     "Check [%s] path doesn't exist", path));
 411         }
 412     }
 413 
 414     public static void assertDirectoryExists(Path path, boolean exists) {
 415         assertPathExists(path, exists);
 416         if (exists) {
 417             assertTrue(path.toFile().isDirectory(), String.format(
 418                     "Check [%s] is a directory", path));
 419         }
 420     }
 421 
 422     public static void assertFileExists(Path path, boolean exists) {
 423         assertPathExists(path, exists);
 424         if (exists) {
 425             assertTrue(path.toFile().isFile(), String.format(
 426                     "Check [%s] is a file", path));
 427         }
 428     }
 429 
 430     public static void assertExecutableFileExists(Path path, boolean exists) {
 431         assertFileExists(path, exists);
 432         if (exists) {
 433             assertTrue(path.toFile().canExecute(), String.format(
 434                     "Check [%s] file is executable", path));
 435         }
 436     }
 437 
 438     public static void assertReadableFileExists(Path path) {
 439         assertFileExists(path, true);
 440         assertTrue(path.toFile().canRead(), String.format(
 441                 "Check [%s] file is readable", path));
 442      }
 443 
 444     public static void assertUnexpected(String msg) {
 445         currentTest.notifyAssert();
 446         error(concatMessages("Unexpected", msg));
 447     }
 448 
 449     public static void assertStringListEquals(List<String> expected,
 450             List<String> actual, String msg) {
 451         currentTest.notifyAssert();
 452 
 453         if (expected.size() < actual.size()) {
 454             // Actual string list is longer than expected
 455             error(concatMessages(String.format(
 456                     "Actual list is longer than expected by %d elements",
 457                     actual.size() - expected.size()), msg));
 458         }
 459 
 460         if (actual.size() < expected.size()) {
 461             // Actual string list is shorter than expected
 462             error(concatMessages(String.format(
 463                     "Actual list is longer than expected by %d elements",
 464                     expected.size() - actual.size()), msg));
 465         }
 466 
 467         traceAssert(String.format("assertStringListEquals(): %s", msg));
 468 
 469         String idxFieldFormat = Functional.identity(() -> {
 470             int listSize = expected.size();
 471             int width = 0;
 472             while (listSize != 0) {
 473                 listSize = listSize / 10;
 474                 width++;
 475             }
 476             return "%" + width + "d";
 477         }).get();
 478 
 479         AtomicInteger counter = new AtomicInteger(0);
 480         Iterator<String> actualIt = actual.iterator();
 481         expected.stream().sequential().filter(expectedStr -> actualIt.hasNext()).forEach(expectedStr -> {
 482             int idx = counter.incrementAndGet();
 483             String actualStr = actualIt.next();
 484 
 485             if ((actualStr != null && !actualStr.equals(expectedStr))
 486                     || (expectedStr != null && !expectedStr.equals(actualStr))) {
 487                 error(concatMessages(String.format(
 488                         "(" + idxFieldFormat + ") Expected [%s]. Actual [%s]",
 489                         idx, expectedStr, actualStr), msg));
 490             }
 491 
 492             traceAssert(String.format(
 493                     "assertStringListEquals(" + idxFieldFormat + ", %s)", idx,
 494                     expectedStr));
 495         });
 496     }
 497 
 498     private static PrintStream openLogStream() {
 499         if (LOG_FILE == null) {
 500             return null;
 501         }
 502 
 503         try {
 504             return new PrintStream(new FileOutputStream(LOG_FILE.toFile(), true));
 505         } catch (FileNotFoundException ex) {
 506             throw new RuntimeException(ex);
 507         }
 508     }
 509 
 510     private static Instance currentTest;
 511 
 512     private static final boolean TRACE;
 513     private static final boolean TRACE_ASSERTS;
 514 
 515     static final boolean VERBOSE_JPACKAGE;
 516 
 517     static String getConfigProperty(String propertyName) {
 518         return System.getProperty(getConfigPropertyName(propertyName));
 519     }
 520 
 521     static String getConfigPropertyName(String propertyName) {
 522         return "jpackage.test." + propertyName;
 523     }
 524 
 525     static final Path LOG_FILE = Functional.identity(() -> {
 526         String val = getConfigProperty("logfile");
 527         if (val == null) {
 528             return null;
 529         }
 530         return Path.of(val);
 531     }).get();
 532 
 533     static {
 534         String val = getConfigProperty("suppress-logging");
 535         if (val == null) {
 536             TRACE = true;
 537             TRACE_ASSERTS = true;
 538             VERBOSE_JPACKAGE = true;
 539         } else if ("all".equals(val.toLowerCase())) {
 540             TRACE = false;
 541             TRACE_ASSERTS = false;
 542             VERBOSE_JPACKAGE = false;
 543         } else {
 544             Set<String> logOptions = Set.of(val.toLowerCase().split(","));
 545             TRACE = !(logOptions.contains("trace") || logOptions.contains("t"));
 546             TRACE_ASSERTS = !(logOptions.contains("assert") || logOptions.contains(
 547                     "a"));
 548             VERBOSE_JPACKAGE = !(logOptions.contains("jpackage") || logOptions.contains(
 549                     "jp"));
 550         }
 551     }
 552 
 553     private static final String OS = System.getProperty("os.name").toLowerCase();
 554 }