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 { check(OutputAnalyzer output)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 Result(CDSOptions opts, OutputAnalyzer out)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 assertNormalExit(Checker checker)146 public Result assertNormalExit(Checker checker) throws Exception { 147 checker.check(output); 148 output.shouldHaveExitValue(0); 149 return this; 150 } 151 assertAbnormalExit(Checker checker)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. assertSilentlyDisabledCDS(Checker checker)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 assertSilentlyDisabledCDS(int exitCode, String... matches)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 ifNormalExit(Checker checker)174 public Result ifNormalExit(Checker checker) throws Exception { 175 if (hasNormalExit) { 176 checker.check(output); 177 } 178 return this; 179 } 180 ifAbnormalExit(Checker checker)181 public Result ifAbnormalExit(Checker checker) throws Exception { 182 if (!hasNormalExit) { 183 checker.check(output); 184 } 185 return this; 186 } 187 ifNoMappingFailure(Checker checker)188 public Result ifNoMappingFailure(Checker checker) throws Exception { 189 checker.check(output); 190 return this; 191 } 192 193 assertNormalExit(String... matches)194 public Result assertNormalExit(String... matches) throws Exception { 195 checkMatches(output, matches); 196 output.shouldHaveExitValue(0); 197 return this; 198 } 199 assertAbnormalExit(String... matches)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 getNextLogCounter()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() createArchive(String... cliPrefix)232 public static OutputAnalyzer createArchive(String... cliPrefix) 233 throws Exception { 234 return createArchive((new CDSOptions()).addPrefix(cliPrefix)); 235 } 236 237 // Create bootstrap CDS archive createArchive(CDSOptions opts)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 isDynamicArchive()265 public static boolean isDynamicArchive() { 266 return DYNAMIC_DUMP; 267 } 268 269 // check result of 'dump-the-archive' operation, that is "-Xshare:dump" checkDump(OutputAnalyzer output, String... extraMatches)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 createArchiveAndCheck(CDSOptions opts)291 public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts) 292 throws Exception { 293 return checkDump(createArchive(opts)); 294 } 295 296 createArchiveAndCheck(String... cliPrefix)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. checkCommonExecExceptions(OutputAnalyzer output, Exception e)310 public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e) 311 throws Exception { 312 if (output.getStdout().contains("https://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 checkCommonExecExceptions(OutputAnalyzer output)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. isUnableToMap(OutputAnalyzer output)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 checkMappingFailure(OutputAnalyzer out)369 public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException { 370 if (isUnableToMap(out)) { 371 throw new SkippedException(UnableToMapMsg); 372 } 373 } 374 run(String... cliPrefix)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 run(CDSOptions opts)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 runWithArchive(String... cliPrefix)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 runWithArchive(CDSOptions opts)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 runWithArchiveAndCheck(CDSOptions opts)426 public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception { 427 return checkExec(runWithArchive(opts)); 428 } 429 430 runWithArchiveAndCheck(String... cliPrefix)431 public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception { 432 return checkExec(runWithArchive(cliPrefix)); 433 } 434 435 checkExec(OutputAnalyzer output, String... extraMatches)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 checkExec(OutputAnalyzer output, CDSOptions opts, String... extraMatches)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 checkExecExpectError(OutputAnalyzer output, int expectedExitValue, String... extraMatches)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 checkMatches(OutputAnalyzer output, String... matches)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 getTestArtifact(String name, boolean checkExistence)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 makeClassList(String classes[])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 makeClassList(String testCaseName, String classes[])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 addToClassList(PrintStream ps, String classes[])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; getTestName()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 startNewArchiveName()547 public static void startNewArchiveName() { 548 defaultArchiveName = getTestName() + 549 timeStampFormat.format(new Date()) + ".jsa"; 550 } 551 getDefaultArchiveName()552 public static String getDefaultArchiveName() { 553 return defaultArchiveName; 554 } 555 556 557 // ===================== FILE ACCESS convenience methods getOutputFile(String name)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 getOutputSourceFile(String name)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 getSourceFile(String name)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 executeAndLog(ProcessBuilder pb, String logName)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 writeFile(File file, String content)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 } 605