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