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