1 /* 2 * Copyright (c) 2017, 2020, 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 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 { check(OutputAnalyzer output)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 hasMappingFailure; 125 private final boolean hasAbnormalExit; 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 options = opts; 131 output = out; 132 hasMappingFailure = CDSTestUtils.checkCommonExecExceptions(output); 133 hasAbnormalExit = (!hasMappingFailure) && (output.getExitValue() != 0); 134 hasNormalExit = (!hasMappingFailure) && (output.getExitValue() == 0); 135 136 if (hasNormalExit) { 137 if ("on".equals(options.xShareMode) && 138 output.getStderr().contains("java version") && 139 !output.getStderr().contains(CDS_DISABLED)) { 140 // "-showversion" is always passed in the command-line by the execXXX methods. 141 // During normal exit, we require that the VM to show that sharing was enabled. 142 output.shouldContain("sharing"); 143 } 144 } 145 } 146 assertNormalExit(Checker checker)147 public Result assertNormalExit(Checker checker) throws Exception { 148 if (!hasMappingFailure) { 149 checker.check(output); 150 output.shouldHaveExitValue(0); 151 } 152 return this; 153 } 154 assertAbnormalExit(Checker checker)155 public Result assertAbnormalExit(Checker checker) throws Exception { 156 if (!hasMappingFailure) { 157 checker.check(output); 158 output.shouldNotHaveExitValue(0); 159 } 160 return this; 161 } 162 163 // When {--limit-modules, --patch-module, and/or --upgrade-module-path} 164 // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on. assertSilentlyDisabledCDS(Checker checker)165 public Result assertSilentlyDisabledCDS(Checker checker) throws Exception { 166 if (hasMappingFailure) { 167 throw new RuntimeException("Unexpected mapping failure"); 168 } 169 // this comes from a JVM warning message. 170 output.shouldContain(CDS_DISABLED); 171 172 checker.check(output); 173 return this; 174 } 175 assertSilentlyDisabledCDS(int exitCode, String... matches)176 public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception { 177 return assertSilentlyDisabledCDS((out) -> { 178 out.shouldHaveExitValue(exitCode); 179 checkMatches(out, matches); 180 }); 181 } 182 ifNormalExit(Checker checker)183 public Result ifNormalExit(Checker checker) throws Exception { 184 if (hasNormalExit) { 185 checker.check(output); 186 } 187 return this; 188 } 189 ifAbnormalExit(Checker checker)190 public Result ifAbnormalExit(Checker checker) throws Exception { 191 if (hasAbnormalExit) { 192 checker.check(output); 193 } 194 return this; 195 } 196 ifNoMappingFailure(Checker checker)197 public Result ifNoMappingFailure(Checker checker) throws Exception { 198 if (!hasMappingFailure) { 199 checker.check(output); 200 } 201 return this; 202 } 203 204 assertNormalExit(String... matches)205 public Result assertNormalExit(String... matches) throws Exception { 206 if (!hasMappingFailure) { 207 checkMatches(output, matches); 208 output.shouldHaveExitValue(0); 209 } 210 return this; 211 } 212 assertAbnormalExit(String... matches)213 public Result assertAbnormalExit(String... matches) throws Exception { 214 if (!hasMappingFailure) { 215 checkMatches(output, matches); 216 output.shouldNotHaveExitValue(0); 217 } 218 219 return this; 220 } 221 } 222 223 // Specify this property to copy sdandard output of the child test process to 224 // the parent/main stdout of the test. 225 // By default such output is logged into a file, and is copied into the main stdout. 226 public static final boolean CopyChildStdoutToMainStdout = 227 Boolean.valueOf(System.getProperty("test.cds.copy.child.stdout", "true")); 228 229 // This property is passed to child test processes 230 public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0"); 231 232 public static final String UnableToMapMsg = 233 "Unable to map shared archive: test did not complete; assumed PASS"; 234 235 // Create bootstrap CDS archive, 236 // use extra JVM command line args as a prefix. 237 // For CDS tests specifying prefix makes more sense than specifying suffix, since 238 // normally there are no classes or arguments to classes, just "-version" 239 // To specify suffix explicitly use CDSOptions.addSuffix() createArchive(String... cliPrefix)240 public static OutputAnalyzer createArchive(String... cliPrefix) 241 throws Exception { 242 return createArchive((new CDSOptions()).addPrefix(cliPrefix)); 243 } 244 245 // Create bootstrap CDS archive createArchive(CDSOptions opts)246 public static OutputAnalyzer createArchive(CDSOptions opts) 247 throws Exception { 248 249 startNewArchiveName(); 250 251 ArrayList<String> cmd = new ArrayList<String>(); 252 253 for (String p : opts.prefix) cmd.add(p); 254 255 cmd.add("-Xshare:dump"); 256 cmd.add("-Xlog:cds,cds+hashtables"); 257 if (opts.archiveName == null) 258 opts.archiveName = getDefaultArchiveName(); 259 cmd.add("-XX:SharedArchiveFile=./" + opts.archiveName); 260 261 for (String s : opts.suffix) cmd.add(s); 262 263 String[] cmdLine = cmd.toArray(new String[cmd.size()]); 264 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine); 265 return executeAndLog(pb, "dump"); 266 } 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 output.shouldContain("Loading classes to share"); 274 output.shouldHaveExitValue(0); 275 276 for (String match : extraMatches) { 277 output.shouldContain(match); 278 } 279 280 return output; 281 } 282 283 284 // A commonly used convenience methods to create an archive and check the results 285 // Creates an archive and checks for errors createArchiveAndCheck(CDSOptions opts)286 public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts) 287 throws Exception { 288 return checkDump(createArchive(opts)); 289 } 290 291 createArchiveAndCheck(String... cliPrefix)292 public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix) 293 throws Exception { 294 return checkDump(createArchive(cliPrefix)); 295 } 296 297 298 // This method should be used to check the output of child VM for common exceptions. 299 // Most of CDS tests deal with child VM processes for creating and using the archive. 300 // However exceptions that occur in the child process do not automatically propagate 301 // to the parent process. This mechanism aims to improve the propagation 302 // of exceptions and common errors. 303 // Exception e argument - an exception to be re-thrown if none of the common 304 // exceptions match. Pass null if you wish not to re-throw any exception. checkCommonExecExceptions(OutputAnalyzer output, Exception e)305 public static boolean checkCommonExecExceptions(OutputAnalyzer output, Exception e) 306 throws Exception { 307 if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) { 308 throw new RuntimeException("Hotspot crashed"); 309 } 310 if (output.getStdout().contains("TEST FAILED")) { 311 throw new RuntimeException("Test Failed"); 312 } 313 if (output.getOutput().contains("shared class paths mismatch")) { 314 // throw new RuntimeException("shared class paths mismatch"); 315 } 316 if (output.getOutput().contains("Unable to unmap shared space")) { 317 throw new RuntimeException("Unable to unmap shared space"); 318 } 319 320 // Special case -- sometimes Xshare:on fails because it failed to map 321 // at given address. This behavior is platform-specific, machine config-specific 322 // and can be random (see ASLR). 323 if (isUnableToMap(output)) { 324 System.out.println(UnableToMapMsg); 325 return true; 326 } 327 328 if (e != null) { 329 throw e; 330 } 331 return false; 332 } 333 checkCommonExecExceptions(OutputAnalyzer output)334 public static boolean checkCommonExecExceptions(OutputAnalyzer output) throws Exception { 335 return 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 { 362 return true; 363 } 364 365 return false; 366 } 367 run(String... cliPrefix)368 public static Result run(String... cliPrefix) throws Exception { 369 CDSOptions opts = new CDSOptions(); 370 opts.setArchiveName(getDefaultArchiveName()); 371 opts.addPrefix(cliPrefix); 372 return new Result(opts, runWithArchive(opts)); 373 } 374 run(CDSOptions opts)375 public static Result run(CDSOptions opts) throws Exception { 376 return new Result(opts, runWithArchive(opts)); 377 } 378 379 // Execute JVM with CDS archive, specify command line args suffix runWithArchive(String... cliPrefix)380 public static OutputAnalyzer runWithArchive(String... cliPrefix) 381 throws Exception { 382 383 return runWithArchive( (new CDSOptions()) 384 .setArchiveName(getDefaultArchiveName()) 385 .addPrefix(cliPrefix) ); 386 } 387 388 389 // Execute JVM with CDS archive, specify CDSOptions runWithArchive(CDSOptions opts)390 public static OutputAnalyzer runWithArchive(CDSOptions opts) 391 throws Exception { 392 393 ArrayList<String> cmd = new ArrayList<String>(); 394 395 for (String p : opts.prefix) cmd.add(p); 396 397 cmd.add("-Xshare:" + opts.xShareMode); 398 cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor); 399 400 if (!opts.useSystemArchive) { 401 if (opts.archiveName == null) 402 opts.archiveName = getDefaultArchiveName(); 403 cmd.add("-XX:SharedArchiveFile=" + opts.archiveName); 404 } 405 406 if (opts.useVersion) 407 cmd.add("-version"); 408 409 for (String s : opts.suffix) cmd.add(s); 410 411 String[] cmdLine = cmd.toArray(new String[cmd.size()]); 412 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine); 413 return executeAndLog(pb, "exec"); 414 } 415 416 417 // A commonly used convenience methods to create an archive and check the results 418 // Creates an archive and checks for errors runWithArchiveAndCheck(CDSOptions opts)419 public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception { 420 return checkExec(runWithArchive(opts)); 421 } 422 423 runWithArchiveAndCheck(String... cliPrefix)424 public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception { 425 return checkExec(runWithArchive(cliPrefix)); 426 } 427 428 checkExec(OutputAnalyzer output, String... extraMatches)429 public static OutputAnalyzer checkExec(OutputAnalyzer output, 430 String... extraMatches) throws Exception { 431 CDSOptions opts = new CDSOptions(); 432 return checkExec(output, opts, extraMatches); 433 } 434 435 436 // check result of 'exec' operation, that is when JVM is run using the archive checkExec(OutputAnalyzer output, CDSOptions opts, String... extraMatches)437 public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts, 438 String... extraMatches) throws Exception { 439 try { 440 if ("on".equals(opts.xShareMode)) { 441 output.shouldContain("sharing"); 442 } 443 output.shouldHaveExitValue(0); 444 } catch (RuntimeException e) { 445 checkCommonExecExceptions(output, e); 446 return output; 447 } 448 449 checkMatches(output, extraMatches); 450 return output; 451 } 452 453 checkExecExpectError(OutputAnalyzer output, int expectedExitValue, String... extraMatches)454 public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output, 455 int expectedExitValue, 456 String... extraMatches) throws Exception { 457 if (isUnableToMap(output)) { 458 System.out.println(UnableToMapMsg); 459 return output; 460 } 461 462 output.shouldHaveExitValue(expectedExitValue); 463 checkMatches(output, extraMatches); 464 return output; 465 } 466 checkMatches(OutputAnalyzer output, String... matches)467 public static OutputAnalyzer checkMatches(OutputAnalyzer output, 468 String... matches) throws Exception { 469 for (String match : matches) { 470 output.shouldContain(match); 471 } 472 return output; 473 } 474 475 476 // get the file object for the test artifact getTestArtifact(String name, boolean checkExistence)477 public static File getTestArtifact(String name, boolean checkExistence) { 478 File dir = new File(System.getProperty("test.classes", ".")); 479 File file = new File(dir, name); 480 481 if (checkExistence && !file.exists()) { 482 throw new RuntimeException("Cannot find " + file.getPath()); 483 } 484 485 return file; 486 } 487 488 489 // create file containing the specified class list makeClassList(String classes[])490 public static File makeClassList(String classes[]) 491 throws Exception { 492 return makeClassList(testName + "-", classes); 493 } 494 495 // create file containing the specified class list makeClassList(String testCaseName, String classes[])496 public static File makeClassList(String testCaseName, String classes[]) 497 throws Exception { 498 499 File classList = getTestArtifact(testCaseName + "test.classlist", false); 500 FileOutputStream fos = new FileOutputStream(classList); 501 PrintStream ps = new PrintStream(fos); 502 503 addToClassList(ps, classes); 504 505 ps.close(); 506 fos.close(); 507 508 return classList; 509 } 510 511 addToClassList(PrintStream ps, String classes[])512 public static void addToClassList(PrintStream ps, String classes[]) 513 throws IOException 514 { 515 if (classes != null) { 516 for (String s : classes) { 517 ps.println(s); 518 } 519 } 520 } 521 522 private static String testName = Utils.TEST_NAME.replace('/', '.'); 523 524 private static final SimpleDateFormat timeStampFormat = 525 new SimpleDateFormat("HH'h'mm'm'ss's'SSS"); 526 527 private static String defaultArchiveName; 528 529 // Call this method to start new archive with new unique name startNewArchiveName()530 public static void startNewArchiveName() { 531 defaultArchiveName = testName + 532 timeStampFormat.format(new Date()) + ".jsa"; 533 } 534 getDefaultArchiveName()535 public static String getDefaultArchiveName() { 536 return defaultArchiveName; 537 } 538 539 540 // ===================== FILE ACCESS convenience methods getOutputFile(String name)541 public static File getOutputFile(String name) { 542 File dir = new File(System.getProperty("test.classes", ".")); 543 return new File(dir, testName + "-" + name); 544 } 545 546 getOutputSourceFile(String name)547 public static File getOutputSourceFile(String name) { 548 File dir = new File(System.getProperty("test.classes", ".")); 549 return new File(dir, name); 550 } 551 552 getSourceFile(String name)553 public static File getSourceFile(String name) { 554 File dir = new File(System.getProperty("test.src", ".")); 555 return new File(dir, name); 556 } 557 558 559 // ============================= Logging executeAndLog(ProcessBuilder pb, String logName)560 public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception { 561 long started = System.currentTimeMillis(); 562 OutputAnalyzer output = new OutputAnalyzer(pb.start()); 563 564 writeFile(getOutputFile(logName + ".stdout"), output.getStdout()); 565 writeFile(getOutputFile(logName + ".stderr"), output.getStderr()); 566 System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]"); 567 System.out.println("[STDERR]\n" + output.getStderr()); 568 569 if (CopyChildStdoutToMainStdout) 570 System.out.println("[STDOUT]\n" + output.getStdout()); 571 572 return output; 573 } 574 575 writeFile(File file, String content)576 private static void writeFile(File file, String content) throws Exception { 577 FileOutputStream fos = new FileOutputStream(file); 578 PrintStream ps = new PrintStream(fos); 579 ps.print(content); 580 ps.close(); 581 fos.close(); 582 } 583 } 584