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