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 }