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 36 37 // This class contains common test utilities for testing CDS 38 public class CDSTestUtils { 39 public interface Checker { 40 public void check(OutputAnalyzer output) throws Exception; 41 } 42 43 /* 44 * INTRODUCTION 45 * 46 * When testing various CDS functionalities, we need to launch JVM processes 47 * using a "launch method" (such as TestCommon.run), and analyze the results of these 48 * processes. 49 * 50 * While typical jtreg tests would use OutputAnalyzer in such cases, due to the 51 * complexity of CDS failure modes, we have added the CDSTestUtils.Result class 52 * to make the analysis more convenient and less error prone. 53 * 54 * A Java process can end in one of the following 4 states: 55 * 56 * 1: Unexpected error - such as JVM crashing. In this case, the "launch method" 57 * will throw a RuntimeException. 58 * 2: Mapping Failure - this happens when the OS (intermittently) fails to map the 59 * CDS archive, normally caused by Address Space Layout Randomization. 60 * We usually treat this as "pass". 61 * 3: Normal Exit - the JVM process has finished without crashing, and the exit code is 0. 62 * 4: Abnormal Exit - the JVM process has finished without crashing, and the exit code is not 0. 63 * 64 * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need 65 * to make sure that our test code is not confused by case 2. 66 * 67 * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old 68 * CDSTestUtils.runWithArchive API, the test may be written as this: 69 * 70 * OutputAnalyzer out = CDSTestUtils.runWithArchive(args); 71 * out.shouldContain("Hi"); 72 * 73 * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output, 74 * and your test case will fail intermittently. 75 * 76 * Instead, the test case should be written as 77 * 78 * CCDSTestUtils.run(args).assertNormalExit("Hi"); 79 * 80 * EXAMPLES/HOWTO 81 * 82 * 1. For simple substring matching: 83 * 84 * CCDSTestUtils.run(args).assertNormalExit("Hi"); 85 * CCDSTestUtils.run(args).assertNormalExit("a", "b", "x"); 86 * CCDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2"); 87 * 88 * 2. For more complex output matching: using Lambda expressions 89 * 90 * CCDSTestUtils.run(args) 91 * .assertNormalExit(output -> output.shouldNotContain("this should not be printed"); 92 * CCDSTestUtils.run(args) 93 * .assertAbnormalExit(output -> { 94 * output.shouldNotContain("this should not be printed"); 95 * output.shouldHaveExitValue(123); 96 * }); 97 * 98 * 3. Chaining several checks: 99 * 100 * CCDSTestUtils.run(args) 101 * .assertNormalExit(output -> output.shouldNotContain("this should not be printed") 102 * .assertNormalExit("should have this", "should have that"); 103 * 104 * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally: 105 * 106 * CCDSTestUtils.run(args) 107 * .ifNormalExit("ths string is printed when exiting with 0") 108 * .ifAbNormalExit("ths string is printed when exiting with 1"); 109 * 110 * NOTE: you usually don't want to write your test case like this -- it should always 111 * exit with the same exit code. (But I kept this API because some existing test cases 112 * behave this way -- need to revisit). 113 */ 114 public static class Result { 115 private final OutputAnalyzer output; 116 private final CDSOptions options; 117 private final boolean hasMappingFailure; 118 private final boolean hasAbnormalExit; 119 private final boolean hasNormalExit; 120 private final String CDS_DISABLED = "warning: CDS is disabled when the"; 121 122 public Result(CDSOptions opts, OutputAnalyzer out) throws Exception { 123 options = opts; 124 output = out; 125 hasMappingFailure = CDSTestUtils.checkCommonExecExceptions(output); 126 hasAbnormalExit = (!hasMappingFailure) && (output.getExitValue() != 0); 127 hasNormalExit = (!hasMappingFailure) && (output.getExitValue() == 0); 128 129 if (hasNormalExit) { 130 if ("on".equals(options.xShareMode) && 131 output.getStderr().contains("java version") && 132 !output.getStderr().contains(CDS_DISABLED)) { 133 // "-showversion" is always passed in the command-line by the execXXX methods. 134 // During normal exit, we require that the VM to show that sharing was enabled. 135 output.shouldContain("sharing"); 136 } 137 } 138 } 139 140 public Result assertNormalExit(Checker checker) throws Exception { 141 if (!hasMappingFailure) { 142 checker.check(output); 143 output.shouldHaveExitValue(0); 144 } 145 return this; 146 } 147 148 public Result assertAbnormalExit(Checker checker) throws Exception { 149 if (!hasMappingFailure) { 150 checker.check(output); 151 output.shouldNotHaveExitValue(0); 152 } 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 if (hasMappingFailure) { 160 throw new RuntimeException("Unexpected mapping failure"); 161 } 162 // this comes from a JVM warning message. 163 output.shouldContain(CDS_DISABLED); 164 165 checker.check(output); 166 return this; 167 } 168 169 public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception { 170 return assertSilentlyDisabledCDS((out) -> { 171 out.shouldHaveExitValue(exitCode); 172 checkMatches(out, matches); 173 }); 174 } 175 176 public Result ifNormalExit(Checker checker) throws Exception { 177 if (hasNormalExit) { 178 checker.check(output); 179 } 180 return this; 181 } 182 183 public Result ifAbnormalExit(Checker checker) throws Exception { 184 if (hasAbnormalExit) { 185 checker.check(output); 186 } 187 return this; 188 } 189 190 public Result ifNoMappingFailure(Checker checker) throws Exception { 191 if (!hasMappingFailure) { 192 checker.check(output); 193 } 194 return this; 195 } 196 197 198 public Result assertNormalExit(String... matches) throws Exception { 199 if (!hasMappingFailure) { 200 checkMatches(output, matches); 201 output.shouldHaveExitValue(0); 202 } 203 return this; 204 } 205 206 public Result assertAbnormalExit(String... matches) throws Exception { 207 if (!hasMappingFailure) { 208 checkMatches(output, matches); 209 output.shouldNotHaveExitValue(0); 210 } 211 212 return this; 213 } 214 } 215 216 // Specify this property to copy sdandard output of the child test process to 217 // the parent/main stdout of the test. 218 // By default such output is logged into a file, and is copied into the main stdout. 219 public static final boolean CopyChildStdoutToMainStdout = 220 Boolean.valueOf(System.getProperty("test.cds.copy.child.stdout", "true")); 221 222 // This property is passed to child test processes 223 public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0"); 224 225 public static final String UnableToMapMsg = 226 "Unable to map shared archive: test did not complete; assumed PASS"; 227 228 // Create bootstrap CDS archive, 229 // use extra JVM command line args as a prefix. 230 // For CDS tests specifying prefix makes more sense than specifying suffix, since 231 // normally there are no classes or arguments to classes, just "-version" 232 // To specify suffix explicitly use CDSOptions.addSuffix() 233 public static OutputAnalyzer createArchive(String... cliPrefix) 234 throws Exception { 235 return createArchive((new CDSOptions()).addPrefix(cliPrefix)); 236 } 237 238 // Create bootstrap CDS archive 239 public static OutputAnalyzer createArchive(CDSOptions opts) 240 throws Exception { 241 242 startNewArchiveName(); 243 244 ArrayList<String> cmd = new ArrayList<String>(); 245 246 for (String p : opts.prefix) cmd.add(p); 247 248 cmd.add("-Xshare:dump"); 249 cmd.add("-Xlog:cds,cds+hashtables"); 250 cmd.add("-XX:+UnlockDiagnosticVMOptions"); 251 if (opts.archiveName == null) 252 opts.archiveName = getDefaultArchiveName(); 253 cmd.add("-XX:SharedArchiveFile=./" + opts.archiveName); 254 255 for (String s : opts.suffix) cmd.add(s); 256 257 String[] cmdLine = cmd.toArray(new String[cmd.size()]); 258 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine); 259 return executeAndLog(pb, "dump"); 260 } 261 262 263 // check result of 'dump-the-archive' operation, that is "-Xshare:dump" 264 public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches) 265 throws Exception { 266 267 output.shouldContain("Loading classes to share"); 268 output.shouldHaveExitValue(0); 269 270 for (String match : extraMatches) { 271 output.shouldContain(match); 272 } 273 274 return output; 275 } 276 277 278 // A commonly used convenience methods to create an archive and check the results 279 // Creates an archive and checks for errors 280 public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts) 281 throws Exception { 282 return checkDump(createArchive(opts)); 283 } 284 285 286 public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix) 287 throws Exception { 288 return checkDump(createArchive(cliPrefix)); 289 } 290 291 292 // This method should be used to check the output of child VM for common exceptions. 293 // Most of CDS tests deal with child VM processes for creating and using the archive. 294 // However exceptions that occur in the child process do not automatically propagate 295 // to the parent process. This mechanism aims to improve the propagation 296 // of exceptions and common errors. 297 // Exception e argument - an exception to be re-thrown if none of the common 298 // exceptions match. Pass null if you wish not to re-throw any exception. 299 public static boolean checkCommonExecExceptions(OutputAnalyzer output, Exception e) 300 throws Exception { 301 if (output.getStdout().contains("http://bugreport.java.com/bugreport/crash.jsp")) { 302 throw new RuntimeException("Hotspot crashed"); 303 } 304 if (output.getStdout().contains("TEST FAILED")) { 305 throw new RuntimeException("Test Failed"); 306 } 307 if (output.getOutput().contains("shared class paths mismatch")) { 308 // throw new RuntimeException("shared class paths mismatch"); 309 } 310 if (output.getOutput().contains("Unable to unmap shared space")) { 311 throw new RuntimeException("Unable to unmap shared space"); 312 } 313 314 // Special case -- sometimes Xshare:on fails because it failed to map 315 // at given address. This behavior is platform-specific, machine config-specific 316 // and can be random (see ASLR). 317 if (isUnableToMap(output)) { 318 System.out.println(UnableToMapMsg); 319 return true; 320 } 321 322 if (e != null) { 323 throw e; 324 } 325 return false; 326 } 327 328 public static boolean checkCommonExecExceptions(OutputAnalyzer output) throws Exception { 329 return checkCommonExecExceptions(output, null); 330 } 331 332 333 // Check the output for indication that mapping of the archive failed. 334 // Performance note: this check seems to be rather costly - searching the entire 335 // output stream of a child process for multiple strings. However, it is necessary 336 // to detect this condition, a failure to map an archive, since this is not a real 337 // failure of the test or VM operation, and results in a test being "skipped". 338 // Suggestions to improve: 339 // 1. VM can designate a special exit code for such condition. 340 // 2. VM can print a single distinct string indicating failure to map an archive, 341 // instead of utilizing multiple messages. 342 // These are suggestions to improve testibility of the VM. However, implementing them 343 // could also improve usability in the field. 344 public static boolean isUnableToMap(OutputAnalyzer output) { 345 String outStr = output.getOutput(); 346 if ((output.getExitValue() == 1) && ( 347 outStr.contains("Unable to reserve shared space at required address") || 348 outStr.contains("Unable to map ReadOnly shared space at required address") || 349 outStr.contains("Unable to map ReadWrite shared space at required address") || 350 outStr.contains("Unable to map MiscData shared space at required address") || 351 outStr.contains("Unable to map MiscCode shared space at required address") || 352 outStr.contains("Unable to map OptionalData shared space at required address") || 353 outStr.contains("Could not allocate metaspace at a compatible address") || 354 outStr.contains("UseSharedSpaces: Unable to allocate region, range is not within java heap") )) 355 { 356 return true; 357 } 358 359 return false; 360 } 361 362 public static Result run(String... cliPrefix) throws Exception { 363 CDSOptions opts = new CDSOptions(); 364 opts.setArchiveName(getDefaultArchiveName()); 365 opts.addPrefix(cliPrefix); 366 return new Result(opts, runWithArchive(opts)); 367 } 368 369 // Execute JVM with CDS archive, specify command line args suffix 370 public static OutputAnalyzer runWithArchive(String... cliPrefix) 371 throws Exception { 372 373 return runWithArchive( (new CDSOptions()) 374 .setArchiveName(getDefaultArchiveName()) 375 .addPrefix(cliPrefix) ); 376 } 377 378 379 // Execute JVM with CDS archive, specify CDSOptions 380 public static OutputAnalyzer runWithArchive(CDSOptions opts) 381 throws Exception { 382 383 ArrayList<String> cmd = new ArrayList<String>(); 384 385 for (String p : opts.prefix) cmd.add(p); 386 387 cmd.add("-Xshare:" + opts.xShareMode); 388 cmd.add("-XX:+UnlockDiagnosticVMOptions"); 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 System.out.println(UnableToMapMsg); 448 return output; 449 } 450 451 output.shouldHaveExitValue(expectedExitValue); 452 checkMatches(output, extraMatches); 453 return output; 454 } 455 456 public static OutputAnalyzer checkMatches(OutputAnalyzer output, 457 String... matches) throws Exception { 458 for (String match : matches) { 459 output.shouldContain(match); 460 } 461 return output; 462 } 463 464 465 // get the file object for the test artifact 466 public static File getTestArtifact(String name, boolean checkExistence) { 467 File dir = new File(System.getProperty("test.classes", ".")); 468 File file = new File(dir, name); 469 470 if (checkExistence && !file.exists()) { 471 throw new RuntimeException("Cannot find " + file.getPath()); 472 } 473 474 return file; 475 } 476 477 478 // create file containing the specified class list 479 public static File makeClassList(String classes[]) 480 throws Exception { 481 return makeClassList(getTestName() + "-", classes); 482 } 483 484 // create file containing the specified class list 485 public static File makeClassList(String testCaseName, String classes[]) 486 throws Exception { 487 488 File classList = getTestArtifact(testCaseName + "test.classlist", false); 489 FileOutputStream fos = new FileOutputStream(classList); 490 PrintStream ps = new PrintStream(fos); 491 492 addToClassList(ps, classes); 493 494 ps.close(); 495 fos.close(); 496 497 return classList; 498 } 499 500 501 public static void addToClassList(PrintStream ps, String classes[]) 502 throws IOException 503 { 504 if (classes != null) { 505 for (String s : classes) { 506 ps.println(s); 507 } 508 } 509 } 510 511 512 // Optimization for getting a test name. 513 // Test name does not change during execution of the test, 514 // but getTestName() uses stack walking hence it is expensive. 515 // Therefore cache it and reuse it. 516 private static String testName; 517 public static String getTestName() { 518 if (testName == null) { 519 testName = Utils.getTestName(); 520 } 521 return testName; 522 } 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 530 public static void startNewArchiveName() { 531 defaultArchiveName = getTestName() + 532 timeStampFormat.format(new Date()) + ".jsa"; 533 } 534 535 public static String getDefaultArchiveName() { 536 return defaultArchiveName; 537 } 538 539 540 // ===================== FILE ACCESS convenience methods 541 public static File getOutputFile(String name) { 542 File dir = new File(System.getProperty("test.classes", ".")); 543 return new File(dir, getTestName() + "-" + name); 544 } 545 546 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 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 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 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 }