1 /*
   2  * Copyright (c) 2017, 2018, 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.test.lib.cds;
  24 
  25 import java.io.IOException;
  26 import java.io.File;
  27 import java.io.FileOutputStream;
  28 import java.io.PrintStream;
  29 import java.text.SimpleDateFormat;
  30 import java.util.ArrayList;
  31 import java.util.Date;
  32 import jdk.test.lib.Utils;
  33 import jdk.test.lib.process.OutputAnalyzer;
  34 import jdk.test.lib.process.ProcessTools;
  35 
  36 
  37 // This class contains common test utilities for testing CDS
  38 public class CDSTestUtils {
  39     public interface Checker {
  40         public void check(OutputAnalyzer output) throws Exception;
  41     }
  42 
  43     /*
  44      * INTRODUCTION
  45      *
  46      * When testing various CDS functionalities, we need to launch JVM processes
  47      * using a "launch method" (such as TestCommon.run), and analyze the results of these
  48      * processes.
  49      *
  50      * While typical jtreg tests would use OutputAnalyzer in such cases, due to the
  51      * complexity of CDS failure modes, we have added the CDSTestUtils.Result class
  52      * to make the analysis more convenient and less error prone.
  53      *
  54      * A Java process can end in one of the following 4 states:
  55      *
  56      *    1: Unexpected error - such as JVM crashing. In this case, the "launch method"
  57      *                          will throw a RuntimeException.
  58      *    2: Mapping Failure  - this happens when the OS (intermittently) fails to map the
  59      *                          CDS archive, normally caused by Address Space Layout Randomization.
  60      *                          We usually treat this as "pass".
  61      *    3: Normal Exit      - the JVM process has finished without crashing, and the exit code is 0.
  62      *    4: Abnormal Exit    - the JVM process has finished without crashing, and the exit code is not 0.
  63      *
  64      * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need
  65      * to make sure that our test code is not confused by case 2.
  66      *
  67      * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old
  68      * CDSTestUtils.runWithArchive API, the test may be written as this:
  69      *
  70      *     OutputAnalyzer out = CDSTestUtils.runWithArchive(args);
  71      *     out.shouldContain("Hi");
  72      *
  73      * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output,
  74      * and your test case will fail intermittently.
  75      *
  76      * Instead, the test case should be written as
  77      *
  78      *      CCDSTestUtils.run(args).assertNormalExit("Hi");
  79      *
  80      * EXAMPLES/HOWTO
  81      *
  82      * 1. For simple substring matching:
  83      *
  84      *      CCDSTestUtils.run(args).assertNormalExit("Hi");
  85      *      CCDSTestUtils.run(args).assertNormalExit("a", "b", "x");
  86      *      CCDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2");
  87      *
  88      * 2. For more complex output matching: using Lambda expressions
  89      *
  90      *      CCDSTestUtils.run(args)
  91      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed");
  92      *      CCDSTestUtils.run(args)
  93      *         .assertAbnormalExit(output -> {
  94      *             output.shouldNotContain("this should not be printed");
  95      *             output.shouldHaveExitValue(123);
  96      *           });
  97      *
  98      * 3. Chaining several checks:
  99      *
 100      *      CCDSTestUtils.run(args)
 101      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed")
 102      *         .assertNormalExit("should have this", "should have that");
 103      *
 104      * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally:
 105      *
 106      *      CCDSTestUtils.run(args)
 107      *         .ifNormalExit("ths string is printed when exiting with 0")
 108      *         .ifAbNormalExit("ths string is printed when exiting with 1");
 109      *
 110      *    NOTE: you usually don't want to write your test case like this -- it should always
 111      *    exit with the same exit code. (But I kept this API because some existing test cases
 112      *    behave this way -- need to revisit).
 113      */
 114     public static class Result {
 115         private final OutputAnalyzer output;
 116         private final CDSOptions options;
 117         private final boolean hasMappingFailure;
 118         private final boolean hasAbnormalExit;
 119         private final boolean hasNormalExit;
 120         private final String CDS_DISABLED = "warning: CDS is disabled when the";
 121 
 122         public Result(CDSOptions opts, OutputAnalyzer out) throws Exception {
 123             options = opts;
 124             output = out;
 125             hasMappingFailure = CDSTestUtils.checkCommonExecExceptions(output);
 126             hasAbnormalExit   = (!hasMappingFailure) && (output.getExitValue() != 0);
 127             hasNormalExit     = (!hasMappingFailure) && (output.getExitValue() == 0);
 128 
 129             if (hasNormalExit) {
 130                 if ("on".equals(options.xShareMode) &&
 131                     output.getStderr().contains("java version") &&
 132                     !output.getStderr().contains(CDS_DISABLED)) {
 133                     // "-showversion" is always passed in the command-line by the execXXX methods.
 134                     // During normal exit, we require that the VM to show that sharing was enabled.
 135                     output.shouldContain("sharing");
 136                 }
 137             }
 138         }
 139 
 140         public Result assertNormalExit(Checker checker) throws Exception {
 141             if (!hasMappingFailure) {
 142                 checker.check(output);
 143                 output.shouldHaveExitValue(0);
 144             }
 145             return this;
 146         }
 147 
 148         public Result assertAbnormalExit(Checker checker) throws Exception {
 149             if (!hasMappingFailure) {
 150                 checker.check(output);
 151                 output.shouldNotHaveExitValue(0);
 152             }
 153             return this;
 154         }
 155 
 156         // When {--limit-modules, --patch-module, and/or --upgrade-module-path}
 157         // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on.
 158         public Result assertSilentlyDisabledCDS(Checker checker) throws Exception {
 159             if (hasMappingFailure) {
 160                 throw new RuntimeException("Unexpected mapping failure");
 161             }
 162             // this comes from a JVM warning message.
 163             output.shouldContain(CDS_DISABLED);
 164 
 165             checker.check(output);
 166             return this;
 167         }
 168 
 169         public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception {
 170             return assertSilentlyDisabledCDS((out) -> {
 171                 out.shouldHaveExitValue(exitCode);
 172                 checkMatches(out, matches);
 173                    });
 174         }
 175 
 176         public Result ifNormalExit(Checker checker) throws Exception {
 177             if (hasNormalExit) {
 178                 checker.check(output);
 179             }
 180             return this;
 181         }
 182 
 183         public Result ifAbnormalExit(Checker checker) throws Exception {
 184             if (hasAbnormalExit) {
 185                 checker.check(output);
 186             }
 187             return this;
 188         }
 189 
 190         public Result ifNoMappingFailure(Checker checker) throws Exception {
 191             if (!hasMappingFailure) {
 192                 checker.check(output);
 193             }
 194             return this;
 195         }
 196 
 197 
 198         public Result assertNormalExit(String... matches) throws Exception {
 199             if (!hasMappingFailure) {
 200                 checkMatches(output, matches);
 201                 output.shouldHaveExitValue(0);
 202             }
 203             return this;
 204         }
 205 
 206         public Result assertAbnormalExit(String... matches) throws Exception {
 207             if (!hasMappingFailure) {
 208                 checkMatches(output, matches);
 209                 output.shouldNotHaveExitValue(0);
 210             }
 211 
 212             return this;
 213         }
 214     }
 215 
 216     // Specify this property to copy sdandard output of the child test process to
 217     // the parent/main stdout of the test.
 218     // By default such output is logged into a file, and is copied into the main stdout.
 219     public static final boolean CopyChildStdoutToMainStdout =
 220         Boolean.valueOf(System.getProperty("test.cds.copy.child.stdout", "true"));
 221 
 222     // This property is passed to child test processes
 223     public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0");
 224 
 225     public static final String UnableToMapMsg =
 226         "Unable to map shared archive: test did not complete; assumed PASS";
 227 
 228     // Create bootstrap CDS archive,
 229     // use extra JVM command line args as a prefix.
 230     // For CDS tests specifying prefix makes more sense than specifying suffix, since
 231     // normally there are no classes or arguments to classes, just "-version"
 232     // To specify suffix explicitly use CDSOptions.addSuffix()
 233     public static OutputAnalyzer createArchive(String... cliPrefix)
 234         throws Exception {
 235         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
 236     }
 237 
 238     // Create bootstrap CDS archive
 239     public static OutputAnalyzer createArchive(CDSOptions opts)
 240         throws Exception {
 241 
 242         startNewArchiveName();
 243 
 244         ArrayList<String> cmd = new ArrayList<String>();
 245 
 246         for (String p : opts.prefix) cmd.add(p);
 247 
 248         cmd.add("-Xshare:dump");
 249         cmd.add("-Xlog:cds,cds+hashtables");
 250         cmd.add("-XX:+UnlockDiagnosticVMOptions");
 251         if (opts.archiveName == null)
 252             opts.archiveName = getDefaultArchiveName();
 253         cmd.add("-XX:SharedArchiveFile=./" + opts.archiveName);
 254 
 255         for (String s : opts.suffix) cmd.add(s);
 256 
 257         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 258         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
 259         return executeAndLog(pb, "dump");
 260     }
 261 
 262 
 263     // check result of 'dump-the-archive' operation, that is "-Xshare:dump"
 264     public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches)
 265         throws Exception {
 266 
 267         output.shouldContain("Loading classes to share");
 268         output.shouldHaveExitValue(0);
 269 
 270         for (String match : extraMatches) {
 271             output.shouldContain(match);
 272         }
 273 
 274         return output;
 275     }
 276 
 277 
 278     // A commonly used convenience methods to create an archive and check the results
 279     // Creates an archive and checks for errors
 280     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
 281         throws Exception {
 282         return checkDump(createArchive(opts));
 283     }
 284 
 285 
 286     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
 287         throws Exception {
 288         return checkDump(createArchive(cliPrefix));
 289     }
 290 
 291 
 292     // This method should be used to check the output of child VM for common exceptions.
 293     // Most of CDS tests deal with child VM processes for creating and using the archive.
 294     // However exceptions that occur in the child process do not automatically propagate
 295     // to the parent process. This mechanism aims to improve the propagation
 296     // of exceptions and common errors.
 297     // Exception e argument - an exception to be re-thrown if none of the common
 298     // exceptions match. Pass null if you wish not to re-throw any exception.
 299     public static boolean checkCommonExecExceptions(OutputAnalyzer output, Exception e)
 300         throws Exception {
 301         if (output.getStdout().contains("http://bugreport.java.com/bugreport/crash.jsp")) {
 302             throw new RuntimeException("Hotspot crashed");
 303         }
 304         if (output.getStdout().contains("TEST FAILED")) {
 305             throw new RuntimeException("Test Failed");
 306         }
 307         if (output.getOutput().contains("shared class paths mismatch")) {
 308 //            throw new RuntimeException("shared class paths mismatch");
 309         }
 310         if (output.getOutput().contains("Unable to unmap shared space")) {
 311             throw new RuntimeException("Unable to unmap shared space");
 312         }
 313 
 314         // Special case -- sometimes Xshare:on fails because it failed to map
 315         // at given address. This behavior is platform-specific, machine config-specific
 316         // and can be random (see ASLR).
 317         if (isUnableToMap(output)) {
 318             System.out.println(UnableToMapMsg);
 319             return true;
 320         }
 321 
 322         if (e != null) {
 323             throw e;
 324         }
 325         return false;
 326     }
 327 
 328     public static boolean checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
 329         return checkCommonExecExceptions(output, null);
 330     }
 331 
 332 
 333     // Check the output for indication that mapping of the archive failed.
 334     // Performance note: this check seems to be rather costly - searching the entire
 335     // output stream of a child process for multiple strings. However, it is necessary
 336     // to detect this condition, a failure to map an archive, since this is not a real
 337     // failure of the test or VM operation, and results in a test being "skipped".
 338     // Suggestions to improve:
 339     // 1. VM can designate a special exit code for such condition.
 340     // 2. VM can print a single distinct string indicating failure to map an archive,
 341     //    instead of utilizing multiple messages.
 342     // These are suggestions to improve testibility of the VM. However, implementing them
 343     // could also improve usability in the field.
 344     public static boolean isUnableToMap(OutputAnalyzer output) {
 345         String outStr = output.getOutput();
 346         if ((output.getExitValue() == 1) && (
 347             outStr.contains("Unable to reserve shared space at required address") ||
 348             outStr.contains("Unable to map ReadOnly shared space at required address") ||
 349             outStr.contains("Unable to map ReadWrite shared space at required address") ||
 350             outStr.contains("Unable to map MiscData shared space at required address") ||
 351             outStr.contains("Unable to map MiscCode shared space at required address") ||
 352             outStr.contains("Unable to map OptionalData shared space at required address") ||
 353             outStr.contains("Could not allocate metaspace at a compatible address") ||
 354             outStr.contains("UseSharedSpaces: Unable to allocate region, range is not within java heap") ))
 355         {
 356             return true;
 357         }
 358 
 359         return false;
 360     }
 361 
 362     public static Result run(String... cliPrefix) throws Exception {
 363         CDSOptions opts = new CDSOptions();
 364         opts.setArchiveName(getDefaultArchiveName());
 365         opts.addPrefix(cliPrefix);
 366         return new Result(opts, runWithArchive(opts));
 367     }
 368 
 369     // Execute JVM with CDS archive, specify command line args suffix
 370     public static OutputAnalyzer runWithArchive(String... cliPrefix)
 371         throws Exception {
 372 
 373         return runWithArchive( (new CDSOptions())
 374                                .setArchiveName(getDefaultArchiveName())
 375                                .addPrefix(cliPrefix) );
 376     }
 377 
 378 
 379     // Execute JVM with CDS archive, specify CDSOptions
 380     public static OutputAnalyzer runWithArchive(CDSOptions opts)
 381         throws Exception {
 382 
 383         ArrayList<String> cmd = new ArrayList<String>();
 384 
 385         for (String p : opts.prefix) cmd.add(p);
 386 
 387         cmd.add("-Xshare:" + opts.xShareMode);
 388         cmd.add("-XX:+UnlockDiagnosticVMOptions");
 389         cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor);
 390 
 391         if (opts.archiveName == null)
 392             opts.archiveName = getDefaultArchiveName();
 393         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
 394 
 395         if (opts.useVersion)
 396             cmd.add("-version");
 397 
 398         for (String s : opts.suffix) cmd.add(s);
 399 
 400         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 401         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
 402         return executeAndLog(pb, "exec");
 403     }
 404 
 405 
 406     // A commonly used convenience methods to create an archive and check the results
 407     // Creates an archive and checks for errors
 408     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
 409         return checkExec(runWithArchive(opts));
 410     }
 411 
 412 
 413     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
 414         return checkExec(runWithArchive(cliPrefix));
 415     }
 416 
 417 
 418     public static OutputAnalyzer checkExec(OutputAnalyzer output,
 419                                      String... extraMatches) throws Exception {
 420         CDSOptions opts = new CDSOptions();
 421         return checkExec(output, opts, extraMatches);
 422     }
 423 
 424 
 425     // check result of 'exec' operation, that is when JVM is run using the archive
 426     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
 427                                      String... extraMatches) throws Exception {
 428         try {
 429             if ("on".equals(opts.xShareMode)) {
 430                 output.shouldContain("sharing");
 431             }
 432             output.shouldHaveExitValue(0);
 433         } catch (RuntimeException e) {
 434             checkCommonExecExceptions(output, e);
 435             return output;
 436         }
 437 
 438         checkMatches(output, extraMatches);
 439         return output;
 440     }
 441 
 442 
 443     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
 444                                              int expectedExitValue,
 445                                              String... extraMatches) throws Exception {
 446         if (isUnableToMap(output)) {
 447             System.out.println(UnableToMapMsg);
 448             return output;
 449         }
 450 
 451         output.shouldHaveExitValue(expectedExitValue);
 452         checkMatches(output, extraMatches);
 453         return output;
 454     }
 455 
 456     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
 457                                               String... matches) throws Exception {
 458         for (String match : matches) {
 459             output.shouldContain(match);
 460         }
 461         return output;
 462     }
 463 
 464 
 465     // get the file object for the test artifact
 466     public static File getTestArtifact(String name, boolean checkExistence) {
 467         File dir = new File(System.getProperty("test.classes", "."));
 468         File file = new File(dir, name);
 469 
 470         if (checkExistence && !file.exists()) {
 471             throw new RuntimeException("Cannot find " + file.getPath());
 472         }
 473 
 474         return file;
 475     }
 476 
 477 
 478     // create file containing the specified class list
 479     public static File makeClassList(String classes[])
 480         throws Exception {
 481         return makeClassList(getTestName() + "-", classes);
 482     }
 483 
 484     // create file containing the specified class list
 485     public static File makeClassList(String testCaseName, String classes[])
 486         throws Exception {
 487 
 488         File classList = getTestArtifact(testCaseName + "test.classlist", false);
 489         FileOutputStream fos = new FileOutputStream(classList);
 490         PrintStream ps = new PrintStream(fos);
 491 
 492         addToClassList(ps, classes);
 493 
 494         ps.close();
 495         fos.close();
 496 
 497         return classList;
 498     }
 499 
 500 
 501     public static void addToClassList(PrintStream ps, String classes[])
 502         throws IOException
 503     {
 504         if (classes != null) {
 505             for (String s : classes) {
 506                 ps.println(s);
 507             }
 508         }
 509     }
 510 
 511 
 512     // Optimization for getting a test name.
 513     // Test name does not change during execution of the test,
 514     // but getTestName() uses stack walking hence it is expensive.
 515     // Therefore cache it and reuse it.
 516     private static String testName;
 517     public static String getTestName() {
 518         if (testName == null) {
 519             testName = Utils.getTestName();
 520         }
 521         return testName;
 522     }
 523 
 524     private static final SimpleDateFormat timeStampFormat =
 525         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
 526 
 527     private static String defaultArchiveName;
 528 
 529     // Call this method to start new archive with new unique name
 530     public static void startNewArchiveName() {
 531         defaultArchiveName = getTestName() +
 532             timeStampFormat.format(new Date()) + ".jsa";
 533     }
 534 
 535     public static String getDefaultArchiveName() {
 536         return defaultArchiveName;
 537     }
 538 
 539 
 540     // ===================== FILE ACCESS convenience methods
 541     public static File getOutputFile(String name) {
 542         File dir = new File(System.getProperty("test.classes", "."));
 543         return new File(dir, getTestName() + "-" + name);
 544     }
 545 
 546 
 547     public static File getOutputSourceFile(String name) {
 548         File dir = new File(System.getProperty("test.classes", "."));
 549         return new File(dir, name);
 550     }
 551 
 552 
 553     public static File getSourceFile(String name) {
 554         File dir = new File(System.getProperty("test.src", "."));
 555         return new File(dir, name);
 556     }
 557 
 558 
 559     // ============================= Logging
 560     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
 561         long started = System.currentTimeMillis();
 562         OutputAnalyzer output = new OutputAnalyzer(pb.start());
 563 
 564         writeFile(getOutputFile(logName + ".stdout"), output.getStdout());
 565         writeFile(getOutputFile(logName + ".stderr"), output.getStderr());
 566         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
 567         System.out.println("[STDERR]\n" + output.getStderr());
 568 
 569         if (CopyChildStdoutToMainStdout)
 570             System.out.println("[STDOUT]\n" + output.getStdout());
 571 
 572         return output;
 573     }
 574 
 575 
 576     private static void writeFile(File file, String content) throws Exception {
 577         FileOutputStream fos = new FileOutputStream(file);
 578         PrintStream ps = new PrintStream(fos);
 579         ps.print(content);
 580         ps.close();
 581         fos.close();
 582     }
 583 }