1 /* 2 * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package jdk.test.lib.cds; 24 25 import java.io.IOException; 26 import java.io.File; 27 import java.io.FileOutputStream; 28 import java.io.PrintStream; 29 import java.text.SimpleDateFormat; 30 import java.util.ArrayList; 31 import java.util.Date; 32 import jdk.test.lib.Utils; 33 import jdk.test.lib.process.OutputAnalyzer; 34 import jdk.test.lib.process.ProcessTools; 35 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 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.createJavaProcessBuilder(true, cmdLine); 260 return executeAndLog(pb, "dump"); 261 } 262 263 264 // check result of 'dump-the-archive' operation, that is "-Xshare:dump" 265 public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches) 266 throws Exception { 267 268 output.shouldContain("Loading classes to share"); 269 output.shouldHaveExitValue(0); 270 271 for (String match : extraMatches) { 272 output.shouldContain(match); 273 } 274 275 return output; 276 } 277 278 279 // A commonly used convenience methods to create an archive and check the results 280 // Creates an archive and checks for errors 281 public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts) 282 throws Exception { 283 return checkDump(createArchive(opts)); 284 } 285 286 287 public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix) 288 throws Exception { 289 return checkDump(createArchive(cliPrefix)); 290 } 291 292 293 // This method should be used to check the output of child VM for common exceptions. 294 // Most of CDS tests deal with child VM processes for creating and using the archive. 295 // However exceptions that occur in the child process do not automatically propagate 296 // to the parent process. This mechanism aims to improve the propagation 297 // of exceptions and common errors. 298 // Exception e argument - an exception to be re-thrown if none of the common 299 // exceptions match. Pass null if you wish not to re-throw any exception. 300 public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e) 301 throws Exception { 302 if (output.getStdout().contains("http://bugreport.java.com/bugreport/crash.jsp")) { 303 throw new RuntimeException("Hotspot crashed"); 304 } 305 if (output.getStdout().contains("TEST FAILED")) { 306 throw new RuntimeException("Test Failed"); 307 } 308 if (output.getOutput().contains("Unable to unmap shared space")) { 309 throw new RuntimeException("Unable to unmap shared space"); 310 } 311 312 // Special case -- sometimes Xshare:on fails because it failed to map 313 // at given address. This behavior is platform-specific, machine config-specific 314 // and can be random (see ASLR). 315 if (isUnableToMap(output)) { 316 throw new SkippedException(UnableToMapMsg); 317 } 318 319 if (e != null) { 320 throw e; 321 } 322 } 323 324 public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception { 325 checkCommonExecExceptions(output, null); 326 } 327 328 329 // Check the output for indication that mapping of the archive failed. 330 // Performance note: this check seems to be rather costly - searching the entire 331 // output stream of a child process for multiple strings. However, it is necessary 332 // to detect this condition, a failure to map an archive, since this is not a real 333 // failure of the test or VM operation, and results in a test being "skipped". 334 // Suggestions to improve: 335 // 1. VM can designate a special exit code for such condition. 336 // 2. VM can print a single distinct string indicating failure to map an archive, 337 // instead of utilizing multiple messages. 338 // These are suggestions to improve testibility of the VM. However, implementing them 339 // could also improve usability in the field. 340 public static boolean isUnableToMap(OutputAnalyzer output) { 341 String outStr = output.getOutput(); 342 if ((output.getExitValue() == 1) && ( 343 outStr.contains("Unable to reserve shared space at required address") || 344 outStr.contains("Unable to map ReadOnly shared space at required address") || 345 outStr.contains("Unable to map ReadWrite shared space at required address") || 346 outStr.contains("Unable to map MiscData shared space at required address") || 347 outStr.contains("Unable to map MiscCode shared space at required address") || 348 outStr.contains("Unable to map OptionalData shared space at required address") || 349 outStr.contains("Could not allocate metaspace at a compatible address") || 350 outStr.contains("UseSharedSpaces: Unable to allocate region, range is not within java heap") )) 351 { 352 return true; 353 } 354 355 return false; 356 } 357 358 public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException { 359 if (isUnableToMap(out)) { 360 throw new SkippedException(UnableToMapMsg); 361 } 362 } 363 364 public static Result run(String... cliPrefix) throws Exception { 365 CDSOptions opts = new CDSOptions(); 366 opts.setArchiveName(getDefaultArchiveName()); 367 opts.addPrefix(cliPrefix); 368 return new Result(opts, runWithArchive(opts)); 369 } 370 371 public static Result run(CDSOptions opts) throws Exception { 372 return new Result(opts, runWithArchive(opts)); 373 } 374 375 // Execute JVM with CDS archive, specify command line args suffix 376 public static OutputAnalyzer runWithArchive(String... cliPrefix) 377 throws Exception { 378 379 return runWithArchive( (new CDSOptions()) 380 .setArchiveName(getDefaultArchiveName()) 381 .addPrefix(cliPrefix) ); 382 } 383 384 385 // Execute JVM with CDS archive, specify CDSOptions 386 public static OutputAnalyzer runWithArchive(CDSOptions opts) 387 throws Exception { 388 389 ArrayList<String> cmd = new ArrayList<String>(); 390 391 for (String p : opts.prefix) cmd.add(p); 392 393 cmd.add("-Xshare:" + opts.xShareMode); 394 cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor); 395 396 if (!opts.useSystemArchive) { 397 if (opts.archiveName == null) 398 opts.archiveName = getDefaultArchiveName(); 399 cmd.add("-XX:SharedArchiveFile=" + opts.archiveName); 400 } 401 402 if (opts.useVersion) 403 cmd.add("-version"); 404 405 for (String s : opts.suffix) cmd.add(s); 406 407 String[] cmdLine = cmd.toArray(new String[cmd.size()]); 408 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine); 409 return executeAndLog(pb, "exec"); 410 } 411 412 413 // A commonly used convenience methods to create an archive and check the results 414 // Creates an archive and checks for errors 415 public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception { 416 return checkExec(runWithArchive(opts)); 417 } 418 419 420 public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception { 421 return checkExec(runWithArchive(cliPrefix)); 422 } 423 424 425 public static OutputAnalyzer checkExec(OutputAnalyzer output, 426 String... extraMatches) throws Exception { 427 CDSOptions opts = new CDSOptions(); 428 return checkExec(output, opts, extraMatches); 429 } 430 431 432 // check result of 'exec' operation, that is when JVM is run using the archive 433 public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts, 434 String... extraMatches) throws Exception { 435 try { 436 if ("on".equals(opts.xShareMode)) { 437 output.shouldContain("sharing"); 438 } 439 output.shouldHaveExitValue(0); 440 } catch (RuntimeException e) { 441 checkCommonExecExceptions(output, e); 442 return output; 443 } 444 445 checkMatches(output, extraMatches); 446 return output; 447 } 448 449 450 public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output, 451 int expectedExitValue, 452 String... extraMatches) throws Exception { 453 if (isUnableToMap(output)) { 454 throw new SkippedException(UnableToMapMsg); 455 } 456 457 output.shouldHaveExitValue(expectedExitValue); 458 checkMatches(output, extraMatches); 459 return output; 460 } 461 462 public static OutputAnalyzer checkMatches(OutputAnalyzer output, 463 String... matches) throws Exception { 464 for (String match : matches) { 465 output.shouldContain(match); 466 } 467 return output; 468 } 469 470 471 // get the file object for the test artifact 472 public static File getTestArtifact(String name, boolean checkExistence) { 473 File dir = new File(System.getProperty("test.classes", ".")); 474 File file = new File(dir, name); 475 476 if (checkExistence && !file.exists()) { 477 throw new RuntimeException("Cannot find " + file.getPath()); 478 } 479 480 return file; 481 } 482 483 484 // create file containing the specified class list 485 public static File makeClassList(String classes[]) 486 throws Exception { 487 return makeClassList(getTestName() + "-", classes); 488 } 489 490 // create file containing the specified class list 491 public static File makeClassList(String testCaseName, String classes[]) 492 throws Exception { 493 494 File classList = getTestArtifact(testCaseName + "test.classlist", false); 495 FileOutputStream fos = new FileOutputStream(classList); 496 PrintStream ps = new PrintStream(fos); 497 498 addToClassList(ps, classes); 499 500 ps.close(); 501 fos.close(); 502 503 return classList; 504 } 505 506 507 public static void addToClassList(PrintStream ps, String classes[]) 508 throws IOException 509 { 510 if (classes != null) { 511 for (String s : classes) { 512 ps.println(s); 513 } 514 } 515 } 516 517 518 // Optimization for getting a test name. 519 // Test name does not change during execution of the test, 520 // but getTestName() uses stack walking hence it is expensive. 521 // Therefore cache it and reuse it. 522 private static String testName; 523 public static String getTestName() { 524 if (testName == null) { 525 testName = Utils.getTestName(); 526 } 527 return testName; 528 } 529 530 private static final SimpleDateFormat timeStampFormat = 531 new SimpleDateFormat("HH'h'mm'm'ss's'SSS"); 532 533 private static String defaultArchiveName; 534 535 // Call this method to start new archive with new unique name 536 public static void startNewArchiveName() { 537 defaultArchiveName = getTestName() + 538 timeStampFormat.format(new Date()) + ".jsa"; 539 } 540 541 public static String getDefaultArchiveName() { 542 return defaultArchiveName; 543 } 544 545 546 // ===================== FILE ACCESS convenience methods 547 public static File getOutputFile(String name) { 548 File dir = new File(System.getProperty("test.classes", ".")); 549 return new File(dir, getTestName() + "-" + name); 550 } 551 552 553 public static File getOutputSourceFile(String name) { 554 File dir = new File(System.getProperty("test.classes", ".")); 555 return new File(dir, name); 556 } 557 558 559 public static File getSourceFile(String name) { 560 File dir = new File(System.getProperty("test.src", ".")); 561 return new File(dir, name); 562 } 563 564 565 // ============================= Logging 566 public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception { 567 long started = System.currentTimeMillis(); 568 OutputAnalyzer output = new OutputAnalyzer(pb.start()); 569 String outputFileNamePrefix = 570 getTestName() + "-" + String.format("%04d", getNextLogCounter()) + "-" + logName; 571 572 writeFile(getOutputFile(outputFileNamePrefix + ".stdout"), output.getStdout()); 573 writeFile(getOutputFile(outputFileNamePrefix + ".stderr"), output.getStderr()); 574 System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]"); 575 System.out.println("[logging stdout to " + outputFileNamePrefix + ".stdout]"); 576 System.out.println("[logging stderr to " + outputFileNamePrefix + ".stderr]"); 577 System.out.println("[STDERR]\n" + output.getStderr()); 578 579 if (copyChildStdoutToMainStdout) 580 System.out.println("[STDOUT]\n" + output.getStdout()); 581 582 return output; 583 } 584 585 586 private static void writeFile(File file, String content) throws Exception { 587 FileOutputStream fos = new FileOutputStream(file); 588 PrintStream ps = new PrintStream(fos); 589 ps.print(content); 590 ps.close(); 591 fos.close(); 592 } 593 }