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