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