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