1 /*
2  * Copyright (c) 2017, 2019, 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 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 static final boolean DYNAMIC_DUMP = Boolean.getBoolean("test.dynamic.cds.archive");
47 
48     public interface Checker {
check(OutputAnalyzer output)49         public void check(OutputAnalyzer output) throws Exception;
50     }
51 
52     /*
53      * INTRODUCTION
54      *
55      * When testing various CDS functionalities, we need to launch JVM processes
56      * using a "launch method" (such as TestCommon.run), and analyze the results of these
57      * processes.
58      *
59      * While typical jtreg tests would use OutputAnalyzer in such cases, due to the
60      * complexity of CDS failure modes, we have added the CDSTestUtils.Result class
61      * to make the analysis more convenient and less error prone.
62      *
63      * A Java process can end in one of the following 4 states:
64      *
65      *    1: Unexpected error - such as JVM crashing. In this case, the "launch method"
66      *                          will throw a RuntimeException.
67      *    2: Mapping Failure  - this happens when the OS (intermittently) fails to map the
68      *                          CDS archive, normally caused by Address Space Layout Randomization.
69      *                          We usually treat this as "pass".
70      *    3: Normal Exit      - the JVM process has finished without crashing, and the exit code is 0.
71      *    4: Abnormal Exit    - the JVM process has finished without crashing, and the exit code is not 0.
72      *
73      * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need
74      * to make sure that our test code is not confused by case 2.
75      *
76      * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old
77      * CDSTestUtils.runWithArchive API, the test may be written as this:
78      *
79      *     OutputAnalyzer out = CDSTestUtils.runWithArchive(args);
80      *     out.shouldContain("Hi");
81      *
82      * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output,
83      * and your test case will fail intermittently.
84      *
85      * Instead, the test case should be written as
86      *
87      *      CDSTestUtils.run(args).assertNormalExit("Hi");
88      *
89      * EXAMPLES/HOWTO
90      *
91      * 1. For simple substring matching:
92      *
93      *      CDSTestUtils.run(args).assertNormalExit("Hi");
94      *      CDSTestUtils.run(args).assertNormalExit("a", "b", "x");
95      *      CDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2");
96      *
97      * 2. For more complex output matching: using Lambda expressions
98      *
99      *      CDSTestUtils.run(args)
100      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed");
101      *      CDSTestUtils.run(args)
102      *         .assertAbnormalExit(output -> {
103      *             output.shouldNotContain("this should not be printed");
104      *             output.shouldHaveExitValue(123);
105      *           });
106      *
107      * 3. Chaining several checks:
108      *
109      *      CDSTestUtils.run(args)
110      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed")
111      *         .assertNormalExit("should have this", "should have that");
112      *
113      * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally:
114      *
115      *      CDSTestUtils.run(args)
116      *         .ifNormalExit("ths string is printed when exiting with 0")
117      *         .ifAbNormalExit("ths string is printed when exiting with 1");
118      *
119      *    NOTE: you usually don't want to write your test case like this -- it should always
120      *    exit with the same exit code. (But I kept this API because some existing test cases
121      *    behave this way -- need to revisit).
122      */
123     public static class Result {
124         private final OutputAnalyzer output;
125         private final CDSOptions options;
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             checkMappingFailure(out);
131             this.options = opts;
132             this.output = out;
133             hasNormalExit = (output.getExitValue() == 0);
134 
135             if (hasNormalExit) {
136                 if ("on".equals(options.xShareMode) &&
137                     output.getStderr().contains("java version") &&
138                     !output.getStderr().contains(CDS_DISABLED)) {
139                     // "-showversion" is always passed in the command-line by the execXXX methods.
140                     // During normal exit, we require that the VM to show that sharing was enabled.
141                     output.shouldContain("sharing");
142                 }
143             }
144         }
145 
assertNormalExit(Checker checker)146         public Result assertNormalExit(Checker checker) throws Exception {
147             checker.check(output);
148             output.shouldHaveExitValue(0);
149             return this;
150         }
151 
assertAbnormalExit(Checker checker)152         public Result assertAbnormalExit(Checker checker) throws Exception {
153             checker.check(output);
154             output.shouldNotHaveExitValue(0);
155             return this;
156         }
157 
158         // When {--limit-modules, --patch-module, and/or --upgrade-module-path}
159         // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on.
assertSilentlyDisabledCDS(Checker checker)160         public Result assertSilentlyDisabledCDS(Checker checker) throws Exception {
161             // this comes from a JVM warning message.
162             output.shouldContain(CDS_DISABLED);
163             checker.check(output);
164             return this;
165         }
166 
assertSilentlyDisabledCDS(int exitCode, String... matches)167         public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception {
168             return assertSilentlyDisabledCDS((out) -> {
169                 out.shouldHaveExitValue(exitCode);
170                 checkMatches(out, matches);
171                    });
172         }
173 
ifNormalExit(Checker checker)174         public Result ifNormalExit(Checker checker) throws Exception {
175             if (hasNormalExit) {
176                 checker.check(output);
177             }
178             return this;
179         }
180 
ifAbnormalExit(Checker checker)181         public Result ifAbnormalExit(Checker checker) throws Exception {
182             if (!hasNormalExit) {
183                 checker.check(output);
184             }
185             return this;
186         }
187 
ifNoMappingFailure(Checker checker)188         public Result ifNoMappingFailure(Checker checker) throws Exception {
189             checker.check(output);
190             return this;
191         }
192 
193 
assertNormalExit(String... matches)194         public Result assertNormalExit(String... matches) throws Exception {
195             checkMatches(output, matches);
196             output.shouldHaveExitValue(0);
197             return this;
198         }
199 
assertAbnormalExit(String... matches)200         public Result assertAbnormalExit(String... matches) throws Exception {
201             checkMatches(output, matches);
202             output.shouldNotHaveExitValue(0);
203             return this;
204         }
205     }
206 
207     // A number to be included in the filename of the stdout and the stderr output file.
208     static int logCounter = 0;
209 
getNextLogCounter()210     private static int getNextLogCounter() {
211         return logCounter++;
212     }
213 
214     // By default, stdout of child processes are logged in files such as
215     // <testname>-0000-exec.stdout. If you want to also include the stdout
216     // inside jtr files, you can override this in the jtreg command line like
217     // "jtreg -Dtest.cds.copy.child.stdout=true ...."
218     public static final boolean copyChildStdoutToMainStdout =
219         Boolean.getBoolean("test.cds.copy.child.stdout");
220 
221     // This property is passed to child test processes
222     public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0");
223 
224     public static final String UnableToMapMsg =
225         "Unable to map shared archive: test did not complete";
226 
227     // Create bootstrap CDS archive,
228     // use extra JVM command line args as a prefix.
229     // For CDS tests specifying prefix makes more sense than specifying suffix, since
230     // normally there are no classes or arguments to classes, just "-version"
231     // To specify suffix explicitly use CDSOptions.addSuffix()
createArchive(String... cliPrefix)232     public static OutputAnalyzer createArchive(String... cliPrefix)
233         throws Exception {
234         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
235     }
236 
237     // Create bootstrap CDS archive
createArchive(CDSOptions opts)238     public static OutputAnalyzer createArchive(CDSOptions opts)
239         throws Exception {
240 
241         startNewArchiveName();
242 
243         ArrayList<String> cmd = new ArrayList<String>();
244 
245         for (String p : opts.prefix) cmd.add(p);
246 
247         cmd.add("-Xshare:dump");
248         cmd.add("-Xlog:cds,cds+hashtables");
249         if (opts.archiveName == null)
250             opts.archiveName = getDefaultArchiveName();
251         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
252 
253         if (opts.classList != null) {
254             File classListFile = makeClassList(opts.classList);
255             cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
256         }
257 
258         for (String s : opts.suffix) cmd.add(s);
259 
260         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
261         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
262         return executeAndLog(pb, "dump");
263     }
264 
isDynamicArchive()265     public static boolean isDynamicArchive() {
266         return DYNAMIC_DUMP;
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         if (!DYNAMIC_DUMP) {
274             output.shouldContain("Loading classes to share");
275         } else {
276             output.shouldContain("Buffer-space to target-space delta")
277                   .shouldContain("Written dynamic archive 0x");
278         }
279         output.shouldHaveExitValue(0);
280 
281         for (String match : extraMatches) {
282             output.shouldContain(match);
283         }
284 
285         return output;
286     }
287 
288 
289     // A commonly used convenience methods to create an archive and check the results
290     // Creates an archive and checks for errors
createArchiveAndCheck(CDSOptions opts)291     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
292         throws Exception {
293         return checkDump(createArchive(opts));
294     }
295 
296 
createArchiveAndCheck(String... cliPrefix)297     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
298         throws Exception {
299         return checkDump(createArchive(cliPrefix));
300     }
301 
302 
303     // This method should be used to check the output of child VM for common exceptions.
304     // Most of CDS tests deal with child VM processes for creating and using the archive.
305     // However exceptions that occur in the child process do not automatically propagate
306     // to the parent process. This mechanism aims to improve the propagation
307     // of exceptions and common errors.
308     // Exception e argument - an exception to be re-thrown if none of the common
309     // exceptions match. Pass null if you wish not to re-throw any exception.
checkCommonExecExceptions(OutputAnalyzer output, Exception e)310     public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e)
311         throws Exception {
312         if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) {
313             throw new RuntimeException("Hotspot crashed");
314         }
315         if (output.getStdout().contains("TEST FAILED")) {
316             throw new RuntimeException("Test Failed");
317         }
318         if (output.getOutput().contains("Unable to unmap shared space")) {
319             throw new RuntimeException("Unable to unmap shared space");
320         }
321 
322         // Special case -- sometimes Xshare:on fails because it failed to map
323         // at given address. This behavior is platform-specific, machine config-specific
324         // and can be random (see ASLR).
325         if (isUnableToMap(output)) {
326             throw new SkippedException(UnableToMapMsg);
327         }
328 
329         if (e != null) {
330             throw e;
331         }
332     }
333 
checkCommonExecExceptions(OutputAnalyzer output)334     public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
335         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             outStr.contains("DynamicDumpSharedSpaces is unsupported when base CDS archive is not loaded") ))
362         {
363             return true;
364         }
365 
366         return false;
367     }
368 
checkMappingFailure(OutputAnalyzer out)369     public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException {
370         if (isUnableToMap(out)) {
371             throw new SkippedException(UnableToMapMsg);
372         }
373     }
374 
run(String... cliPrefix)375     public static Result run(String... cliPrefix) throws Exception {
376         CDSOptions opts = new CDSOptions();
377         opts.setArchiveName(getDefaultArchiveName());
378         opts.addPrefix(cliPrefix);
379         return new Result(opts, runWithArchive(opts));
380     }
381 
run(CDSOptions opts)382     public static Result run(CDSOptions opts) throws Exception {
383         return new Result(opts, runWithArchive(opts));
384     }
385 
386     // Execute JVM with CDS archive, specify command line args suffix
runWithArchive(String... cliPrefix)387     public static OutputAnalyzer runWithArchive(String... cliPrefix)
388         throws Exception {
389 
390         return runWithArchive( (new CDSOptions())
391                                .setArchiveName(getDefaultArchiveName())
392                                .addPrefix(cliPrefix) );
393     }
394 
395 
396     // Execute JVM with CDS archive, specify CDSOptions
runWithArchive(CDSOptions opts)397     public static OutputAnalyzer runWithArchive(CDSOptions opts)
398         throws Exception {
399 
400         ArrayList<String> cmd = new ArrayList<String>();
401 
402         for (String p : opts.prefix) cmd.add(p);
403 
404         cmd.add("-Xshare:" + opts.xShareMode);
405         cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor);
406 
407         if (!opts.useSystemArchive) {
408             if (opts.archiveName == null)
409                 opts.archiveName = getDefaultArchiveName();
410             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
411         }
412 
413         if (opts.useVersion)
414             cmd.add("-version");
415 
416         for (String s : opts.suffix) cmd.add(s);
417 
418         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
419         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
420         return executeAndLog(pb, "exec");
421     }
422 
423 
424     // A commonly used convenience methods to create an archive and check the results
425     // Creates an archive and checks for errors
runWithArchiveAndCheck(CDSOptions opts)426     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
427         return checkExec(runWithArchive(opts));
428     }
429 
430 
runWithArchiveAndCheck(String... cliPrefix)431     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
432         return checkExec(runWithArchive(cliPrefix));
433     }
434 
435 
checkExec(OutputAnalyzer output, String... extraMatches)436     public static OutputAnalyzer checkExec(OutputAnalyzer output,
437                                      String... extraMatches) throws Exception {
438         CDSOptions opts = new CDSOptions();
439         return checkExec(output, opts, extraMatches);
440     }
441 
442 
443     // check result of 'exec' operation, that is when JVM is run using the archive
checkExec(OutputAnalyzer output, CDSOptions opts, String... extraMatches)444     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
445                                      String... extraMatches) throws Exception {
446         try {
447             if ("on".equals(opts.xShareMode)) {
448                 output.shouldContain("sharing");
449             }
450             output.shouldHaveExitValue(0);
451         } catch (RuntimeException e) {
452             checkCommonExecExceptions(output, e);
453             return output;
454         }
455 
456         checkMatches(output, extraMatches);
457         return output;
458     }
459 
460 
checkExecExpectError(OutputAnalyzer output, int expectedExitValue, String... extraMatches)461     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
462                                              int expectedExitValue,
463                                              String... extraMatches) throws Exception {
464         if (isUnableToMap(output)) {
465             throw new SkippedException(UnableToMapMsg);
466         }
467 
468         output.shouldHaveExitValue(expectedExitValue);
469         checkMatches(output, extraMatches);
470         return output;
471     }
472 
checkMatches(OutputAnalyzer output, String... matches)473     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
474                                               String... matches) throws Exception {
475         for (String match : matches) {
476             output.shouldContain(match);
477         }
478         return output;
479     }
480 
481 
482     // get the file object for the test artifact
getTestArtifact(String name, boolean checkExistence)483     public static File getTestArtifact(String name, boolean checkExistence) {
484         File dir = new File(System.getProperty("test.classes", "."));
485         File file = new File(dir, name);
486 
487         if (checkExistence && !file.exists()) {
488             throw new RuntimeException("Cannot find " + file.getPath());
489         }
490 
491         return file;
492     }
493 
494 
495     // create file containing the specified class list
makeClassList(String classes[])496     public static File makeClassList(String classes[])
497         throws Exception {
498         return makeClassList(getTestName() + "-", classes);
499     }
500 
501     // create file containing the specified class list
makeClassList(String testCaseName, String classes[])502     public static File makeClassList(String testCaseName, String classes[])
503         throws Exception {
504 
505         File classList = getTestArtifact(testCaseName + "test.classlist", false);
506         FileOutputStream fos = new FileOutputStream(classList);
507         PrintStream ps = new PrintStream(fos);
508 
509         addToClassList(ps, classes);
510 
511         ps.close();
512         fos.close();
513 
514         return classList;
515     }
516 
517 
addToClassList(PrintStream ps, String classes[])518     public static void addToClassList(PrintStream ps, String classes[])
519         throws IOException
520     {
521         if (classes != null) {
522             for (String s : classes) {
523                 ps.println(s);
524             }
525         }
526     }
527 
528 
529     // Optimization for getting a test name.
530     // Test name does not change during execution of the test,
531     // but getTestName() uses stack walking hence it is expensive.
532     // Therefore cache it and reuse it.
533     private static String testName;
getTestName()534     public static String getTestName() {
535         if (testName == null) {
536             testName = Utils.getTestName();
537         }
538         return testName;
539     }
540 
541     private static final SimpleDateFormat timeStampFormat =
542         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
543 
544     private static String defaultArchiveName;
545 
546     // Call this method to start new archive with new unique name
startNewArchiveName()547     public static void startNewArchiveName() {
548         defaultArchiveName = getTestName() +
549             timeStampFormat.format(new Date()) + ".jsa";
550     }
551 
getDefaultArchiveName()552     public static String getDefaultArchiveName() {
553         return defaultArchiveName;
554     }
555 
556 
557     // ===================== FILE ACCESS convenience methods
getOutputFile(String name)558     public static File getOutputFile(String name) {
559         File dir = new File(System.getProperty("test.classes", "."));
560         return new File(dir, getTestName() + "-" + name);
561     }
562 
563 
getOutputSourceFile(String name)564     public static File getOutputSourceFile(String name) {
565         File dir = new File(System.getProperty("test.classes", "."));
566         return new File(dir, name);
567     }
568 
569 
getSourceFile(String name)570     public static File getSourceFile(String name) {
571         File dir = new File(System.getProperty("test.src", "."));
572         return new File(dir, name);
573     }
574 
575 
576     // ============================= Logging
executeAndLog(ProcessBuilder pb, String logName)577     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
578         long started = System.currentTimeMillis();
579         OutputAnalyzer output = new OutputAnalyzer(pb.start());
580         String outputFileNamePrefix =
581             getTestName() + "-" + String.format("%04d", getNextLogCounter()) + "-" + logName;
582 
583         writeFile(getOutputFile(outputFileNamePrefix + ".stdout"), output.getStdout());
584         writeFile(getOutputFile(outputFileNamePrefix + ".stderr"), output.getStderr());
585         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
586         System.out.println("[logging stdout to " + outputFileNamePrefix + ".stdout]");
587         System.out.println("[logging stderr to " + outputFileNamePrefix + ".stderr]");
588         System.out.println("[STDERR]\n" + output.getStderr());
589 
590         if (copyChildStdoutToMainStdout)
591             System.out.println("[STDOUT]\n" + output.getStdout());
592 
593         return output;
594     }
595 
596 
writeFile(File file, String content)597     private static void writeFile(File file, String content) throws Exception {
598         FileOutputStream fos = new FileOutputStream(file);
599         PrintStream ps = new PrintStream(fos);
600         ps.print(content);
601         ps.close();
602         fos.close();
603     }
604 }
605