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 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"; 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 throw new SkippedException(UnableToMapMsg); 318 } 319 320 if (e != null) { 321 throw e; 322 } 323 return false; 324 } 325 326 public static boolean checkCommonExecExceptions(OutputAnalyzer output) throws Exception { 327 return checkCommonExecExceptions(output, null); 328 } 329 330 331 // Check the output for indication that mapping of the archive failed. 332 // Performance note: this check seems to be rather costly - searching the entire 333 // output stream of a child process for multiple strings. However, it is necessary 334 // to detect this condition, a failure to map an archive, since this is not a real 335 // failure of the test or VM operation, and results in a test being "skipped". 336 // Suggestions to improve: 337 // 1. VM can designate a special exit code for such condition. 338 // 2. VM can print a single distinct string indicating failure to map an archive, 339 // instead of utilizing multiple messages. 340 // These are suggestions to improve testibility of the VM. However, implementing them 341 // could also improve usability in the field. 342 public static boolean isUnableToMap(OutputAnalyzer output) { 343 String outStr = output.getOutput(); 344 if ((output.getExitValue() == 1) && ( 345 outStr.contains("Unable to reserve shared space at required address") || 346 outStr.contains("Unable to map ReadOnly shared space at required address") || 347 outStr.contains("Unable to map ReadWrite shared space at required address") || 348 outStr.contains("Unable to map MiscData shared space at required address") || 349 outStr.contains("Unable to map MiscCode shared space at required address") || 350 outStr.contains("Unable to map OptionalData shared space at required address") || 351 outStr.contains("Could not allocate metaspace at a compatible address") || 352 outStr.contains("UseSharedSpaces: Unable to allocate region, range is not within java heap") )) 353 { 354 return true; 355 } 356 357 return false; 358 } 359 360 public static Result run(String... cliPrefix) throws Exception { 361 CDSOptions opts = new CDSOptions(); 362 opts.setArchiveName(getDefaultArchiveName()); 363 opts.addPrefix(cliPrefix); 364 return new Result(opts, runWithArchive(opts)); 365 } 366 367 public static Result run(CDSOptions opts) throws Exception { 368 return new Result(opts, runWithArchive(opts)); 369 } 370 371 // Execute JVM with CDS archive, specify command line args suffix 372 public static OutputAnalyzer runWithArchive(String... cliPrefix) 373 throws Exception { 374 375 return runWithArchive( (new CDSOptions()) 376 .setArchiveName(getDefaultArchiveName()) 377 .addPrefix(cliPrefix) ); 378 } 379 380 381 // Execute JVM with CDS archive, specify CDSOptions 382 public static OutputAnalyzer runWithArchive(CDSOptions opts) 383 throws Exception { 384 385 ArrayList<String> cmd = new ArrayList<String>(); 386 387 for (String p : opts.prefix) cmd.add(p); 388 389 cmd.add("-Xshare:" + opts.xShareMode); 390 cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor); 391 392 if (opts.archiveName == null) 393 opts.archiveName = getDefaultArchiveName(); 394 cmd.add("-XX:SharedArchiveFile=" + opts.archiveName); 395 396 if (opts.useVersion) 397 cmd.add("-version"); 398 399 for (String s : opts.suffix) cmd.add(s); 400 401 String[] cmdLine = cmd.toArray(new String[cmd.size()]); 402 ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine); 403 return executeAndLog(pb, "exec"); 404 } 405 406 407 // A commonly used convenience methods to create an archive and check the results 408 // Creates an archive and checks for errors 409 public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception { 410 return checkExec(runWithArchive(opts)); 411 } 412 413 414 public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception { 415 return checkExec(runWithArchive(cliPrefix)); 416 } 417 418 419 public static OutputAnalyzer checkExec(OutputAnalyzer output, 420 String... extraMatches) throws Exception { 421 CDSOptions opts = new CDSOptions(); 422 return checkExec(output, opts, extraMatches); 423 } 424 425 426 // check result of 'exec' operation, that is when JVM is run using the archive 427 public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts, 428 String... extraMatches) throws Exception { 429 try { 430 if ("on".equals(opts.xShareMode)) { 431 output.shouldContain("sharing"); 432 } 433 output.shouldHaveExitValue(0); 434 } catch (RuntimeException e) { 435 checkCommonExecExceptions(output, e); 436 return output; 437 } 438 439 checkMatches(output, extraMatches); 440 return output; 441 } 442 443 444 public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output, 445 int expectedExitValue, 446 String... extraMatches) throws Exception { 447 if (isUnableToMap(output)) { 448 throw new SkippedException(UnableToMapMsg); 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 }