1 /*
  2  * Copyright (c) 2017, 2025, 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.nio.file.Files;
 30 import java.nio.file.CopyOption;
 31 import java.nio.file.Path;
 32 import java.nio.file.Paths;
 33 import java.nio.file.StandardCopyOption;
 34 import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
 35 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 36 import java.text.SimpleDateFormat;
 37 import java.util.List;
 38 import java.util.ArrayList;
 39 import java.util.Date;
 40 import jdk.test.lib.JDKToolFinder;
 41 import jdk.test.lib.Utils;
 42 import jdk.test.lib.process.OutputAnalyzer;
 43 import jdk.test.lib.process.ProcessTools;
 44 import jtreg.SkippedException;
 45 
 46 // This class contains common test utilities for testing CDS
 47 public class CDSTestUtils {
 48     public static final String MSG_RANGE_NOT_WITHIN_HEAP =
 49         "Unable to allocate region, range is not within java heap.";
 50     public static final String MSG_RANGE_ALREADT_IN_USE =
 51         "Unable to allocate region, java heap range is already in use.";
 52     public static final String MSG_DYNAMIC_NOT_SUPPORTED =
 53         "-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded";
 54     public static final String MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE =
 55         "an object points to a static field that may hold a different value at runtime";
 56     public static final boolean DYNAMIC_DUMP = Boolean.getBoolean("test.dynamic.cds.archive");
 57 
 58     public interface Checker {
 59         public void check(OutputAnalyzer output) throws Exception;
 60     }
 61 
 62     /*
 63      * INTRODUCTION
 64      *
 65      * When testing various CDS functionalities, we need to launch JVM processes
 66      * using a "launch method" (such as TestCommon.run), and analyze the results of these
 67      * processes.
 68      *
 69      * While typical jtreg tests would use OutputAnalyzer in such cases, due to the
 70      * complexity of CDS failure modes, we have added the CDSTestUtils.Result class
 71      * to make the analysis more convenient and less error prone.
 72      *
 73      * A Java process can end in one of the following 4 states:
 74      *
 75      *    1: Unexpected error - such as JVM crashing. In this case, the "launch method"
 76      *                          will throw a RuntimeException.
 77      *    2: Mapping Failure  - this happens when the OS (intermittently) fails to map the
 78      *                          CDS archive, normally caused by Address Space Layout Randomization.
 79      *                          We usually treat this as "pass".
 80      *    3: Normal Exit      - the JVM process has finished without crashing, and the exit code is 0.
 81      *    4: Abnormal Exit    - the JVM process has finished without crashing, and the exit code is not 0.
 82      *
 83      * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need
 84      * to make sure that our test code is not confused by case 2.
 85      *
 86      * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old
 87      * CDSTestUtils.runWithArchive API, the test may be written as this:
 88      *
 89      *     OutputAnalyzer out = CDSTestUtils.runWithArchive(args);
 90      *     out.shouldContain("Hi");
 91      *
 92      * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output,
 93      * and your test case will fail intermittently.
 94      *
 95      * Instead, the test case should be written as
 96      *
 97      *      CDSTestUtils.run(args).assertNormalExit("Hi");
 98      *
 99      * EXAMPLES/HOWTO
100      *
101      * 1. For simple substring matching:
102      *
103      *      CDSTestUtils.run(args).assertNormalExit("Hi");
104      *      CDSTestUtils.run(args).assertNormalExit("a", "b", "x");
105      *      CDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2");
106      *
107      * 2. For more complex output matching: using Lambda expressions
108      *
109      *      CDSTestUtils.run(args)
110      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed");
111      *      CDSTestUtils.run(args)
112      *         .assertAbnormalExit(output -> {
113      *             output.shouldNotContain("this should not be printed");
114      *             output.shouldHaveExitValue(123);
115      *           });
116      *
117      * 3. Chaining several checks:
118      *
119      *      CDSTestUtils.run(args)
120      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed")
121      *         .assertNormalExit("should have this", "should have that");
122      *
123      * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally:
124      *
125      *      CDSTestUtils.run(args)
126      *         .ifNormalExit("ths string is printed when exiting with 0")
127      *         .ifAbNormalExit("ths string is printed when exiting with 1");
128      *
129      *    NOTE: you usually don't want to write your test case like this -- it should always
130      *    exit with the same exit code. (But I kept this API because some existing test cases
131      *    behave this way -- need to revisit).
132      */
133     public static class Result {
134         private final OutputAnalyzer output;
135         private final CDSOptions options;
136         private final boolean hasNormalExit;
137         private final String CDS_DISABLED = "warning: CDS is disabled when the";
138 
139         public Result(CDSOptions opts, OutputAnalyzer out) throws Exception {
140             checkMappingFailure(out);
141             this.options = opts;
142             this.output = out;
143             hasNormalExit = (output.getExitValue() == 0);
144 
145             if (hasNormalExit) {
146                 if ("on".equals(options.xShareMode) &&
147                     output.getStderr().contains("java version") &&
148                     !output.getStderr().contains(CDS_DISABLED)) {
149                     // "-showversion" is always passed in the command-line by the execXXX methods.
150                     // During normal exit, we require that the VM to show that sharing was enabled.
151                     output.shouldContain("sharing");
152                 }
153             }
154         }
155 
156         public Result assertNormalExit(Checker checker) throws Exception {
157             checker.check(output);
158             output.shouldHaveExitValue(0);
159             return this;
160         }
161 
162         public Result assertAbnormalExit(Checker checker) throws Exception {
163             checker.check(output);
164             output.shouldNotHaveExitValue(0);
165             return this;
166         }
167 
168         // When {--limit-modules, --patch-module, and/or --upgrade-module-path}
169         // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on.
170         public Result assertSilentlyDisabledCDS(Checker checker) throws Exception {
171             // this comes from a JVM warning message.
172             output.shouldContain(CDS_DISABLED);
173             checker.check(output);
174             return this;
175         }
176 
177         public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception {
178             return assertSilentlyDisabledCDS((out) -> {
179                 out.shouldHaveExitValue(exitCode);
180                 checkMatches(out, matches);
181                    });
182         }
183 
184         public Result ifNormalExit(Checker checker) throws Exception {
185             if (hasNormalExit) {
186                 checker.check(output);
187             }
188             return this;
189         }
190 
191         public Result ifAbnormalExit(Checker checker) throws Exception {
192             if (!hasNormalExit) {
193                 checker.check(output);
194             }
195             return this;
196         }
197 
198         public Result ifNoMappingFailure(Checker checker) throws Exception {
199             checker.check(output);
200             return this;
201         }
202 
203 
204         public Result assertNormalExit(String... matches) throws Exception {
205             checkMatches(output, matches);
206             output.shouldHaveExitValue(0);
207             return this;
208         }
209 
210         public Result assertAbnormalExit(String... matches) throws Exception {
211             checkMatches(output, matches);
212             output.shouldNotHaveExitValue(0);
213             return this;
214         }
215     }
216 
217     // A number to be included in the filename of the stdout and the stderr output file.
218     static int logCounter = 0;
219 
220     private static int getNextLogCounter() {
221         return logCounter++;
222     }
223 
224     // By default, stdout of child processes are logged in files such as
225     // <testname>-0000-exec.stdout. If you want to also include the stdout
226     // inside jtr files, you can override this in the jtreg command line like
227     // "jtreg -Dtest.cds.copy.child.stdout=true ...."
228     public static final boolean copyChildStdoutToMainStdout =
229         Boolean.getBoolean("test.cds.copy.child.stdout");
230 
231     // This property is passed to child test processes
232     public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0");
233 
234     public static final String UnableToMapMsg =
235         "Unable to map shared archive: test did not complete";
236 
237     // Create bootstrap CDS archive,
238     // use extra JVM command line args as a prefix.
239     // For CDS tests specifying prefix makes more sense than specifying suffix, since
240     // normally there are no classes or arguments to classes, just "-version"
241     // To specify suffix explicitly use CDSOptions.addSuffix()
242     public static OutputAnalyzer createArchive(String... cliPrefix)
243         throws Exception {
244         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
245     }
246 
247     // Create bootstrap CDS archive
248     public static OutputAnalyzer createArchive(CDSOptions opts)
249         throws Exception {
250 
251         startNewArchiveName();
252 
253         ArrayList<String> cmd = new ArrayList<String>();
254 
255         for (String p : opts.prefix) cmd.add(p);
256 
257         cmd.add("-Xshare:dump");
258         cmd.add("-Xlog:cds,aot+hashtables");
259         if (opts.archiveName == null)
260             opts.archiveName = getDefaultArchiveName();
261         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
262 
263         if (opts.classList != null) {
264             File classListFile = makeClassList(opts.classList);
265             cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
266         }
267 
268         for (String s : opts.suffix) cmd.add(s);
269 
270         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
271         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
272         return executeAndLog(pb, "dump");
273     }
274 
275     public static boolean isDynamicArchive() {
276         return DYNAMIC_DUMP;
277     }
278 
279     // check result of 'dump-the-archive' operation, that is "-Xshare:dump"
280     public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches)
281         throws Exception {
282 
283         if (!DYNAMIC_DUMP) {
284             output.shouldContain("Loading classes to share");
285         } else {
286             output.shouldContain("Written dynamic archive 0x");
287         }
288         output.shouldHaveExitValue(0);
289         //output.shouldNotContain(MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE); // FIXME -- leyden+JEP483 merge
290 
291         for (String match : extraMatches) {
292             output.shouldContain(match);
293         }
294 
295         return output;
296     }
297 
298     // check result of dumping base archive
299     public static OutputAnalyzer checkBaseDump(OutputAnalyzer output) throws Exception {
300         output.shouldContain("Loading classes to share");
301         output.shouldHaveExitValue(0);
302         //output.shouldNotContain(MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE); // FIXME -- leyden+JEP483 merge
303         return output;
304     }
305 
306     // A commonly used convenience methods to create an archive and check the results
307     // Creates an archive and checks for errors
308     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
309         throws Exception {
310         return checkDump(createArchive(opts));
311     }
312 
313 
314     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
315         throws Exception {
316         return checkDump(createArchive(cliPrefix));
317     }
318 
319 
320     // This method should be used to check the output of child VM for common exceptions.
321     // Most of CDS tests deal with child VM processes for creating and using the archive.
322     // However exceptions that occur in the child process do not automatically propagate
323     // to the parent process. This mechanism aims to improve the propagation
324     // of exceptions and common errors.
325     // Exception e argument - an exception to be re-thrown if none of the common
326     // exceptions match. Pass null if you wish not to re-throw any exception.
327     public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e)
328         throws Exception {
329         if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) {
330             throw new RuntimeException("Hotspot crashed");
331         }
332         if (output.getStdout().contains("TEST FAILED")) {
333             throw new RuntimeException("Test Failed");
334         }
335         if (output.getOutput().contains("Unable to unmap shared space")) {
336             throw new RuntimeException("Unable to unmap shared space");
337         }
338 
339         // Special case -- sometimes Xshare:on fails because it failed to map
340         // at given address. This behavior is platform-specific, machine config-specific
341         // and can be random (see ASLR).
342         checkMappingFailure(output);
343 
344         if (e != null) {
345             throw e;
346         }
347     }
348 
349     public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
350         checkCommonExecExceptions(output, null);
351     }
352 
353 
354     // Check the output for indication that mapping of the archive failed.
355     // Performance note: this check seems to be rather costly - searching the entire
356     // output stream of a child process for multiple strings. However, it is necessary
357     // to detect this condition, a failure to map an archive, since this is not a real
358     // failure of the test or VM operation, and results in a test being "skipped".
359     // Suggestions to improve:
360     // 1. VM can designate a special exit code for such condition.
361     // 2. VM can print a single distinct string indicating failure to map an archive,
362     //    instead of utilizing multiple messages.
363     // These are suggestions to improve testibility of the VM. However, implementing them
364     // could also improve usability in the field.
365     private static String hasUnableToMapMessage(OutputAnalyzer output) {
366         String outStr = output.getOutput();
367         if ((output.getExitValue() == 1)) {
368             if (outStr.contains(MSG_RANGE_NOT_WITHIN_HEAP)) {
369                 return MSG_RANGE_NOT_WITHIN_HEAP;
370             }
371             if (outStr.contains(MSG_DYNAMIC_NOT_SUPPORTED)) {
372                 return MSG_DYNAMIC_NOT_SUPPORTED;
373             }
374         }
375 
376         return null;
377     }
378 
379     public static boolean isUnableToMap(OutputAnalyzer output) {
380         return hasUnableToMapMessage(output) != null;
381     }
382 
383     public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException {
384         String match = hasUnableToMapMessage(out);
385         if (match != null) {
386             throw new SkippedException(UnableToMapMsg + ": " + match);
387         }
388     }
389 
390     public static Result run(String... cliPrefix) throws Exception {
391         CDSOptions opts = new CDSOptions();
392         opts.setArchiveName(getDefaultArchiveName());
393         opts.addPrefix(cliPrefix);
394         return new Result(opts, runWithArchive(opts));
395     }
396 
397     public static Result run(CDSOptions opts) throws Exception {
398         return new Result(opts, runWithArchive(opts));
399     }
400 
401     // Dump a classlist using the -XX:DumpLoadedClassList option.
402     public static Result dumpClassList(String classListName, String... cli)
403         throws Exception {
404         CDSOptions opts = (new CDSOptions())
405             .setUseVersion(false)
406             .setXShareMode("auto")
407             .addPrefix("-XX:DumpLoadedClassList=" + classListName)
408             .addSuffix(cli);
409         Result res = run(opts).assertNormalExit();
410         return res;
411     }
412 
413     // Execute JVM with CDS archive, specify command line args suffix
414     public static OutputAnalyzer runWithArchive(String... cliPrefix)
415         throws Exception {
416 
417         return runWithArchive( (new CDSOptions())
418                                .setArchiveName(getDefaultArchiveName())
419                                .addPrefix(cliPrefix) );
420     }
421 
422     // Enable basic verification (VerifyArchivedFields=1, no side effects) for all CDS
423     // tests to make sure the archived heap objects are mapped/loaded properly.
424     public static void addVerifyArchivedFields(ArrayList<String> cmd) {
425         cmd.add("-XX:+UnlockDiagnosticVMOptions");
426         cmd.add("-XX:VerifyArchivedFields=1");
427     }
428 
429     // Execute JVM with CDS archive, specify CDSOptions
430     public static OutputAnalyzer runWithArchive(CDSOptions opts)
431         throws Exception {
432 
433         ArrayList<String> cmd = new ArrayList<String>();
434         cmd.addAll(opts.prefix);
435         cmd.add("-Xshare:" + opts.xShareMode);
436         cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor);
437 
438         if (!opts.useSystemArchive) {
439             if (opts.archiveName == null)
440                 opts.archiveName = getDefaultArchiveName();
441             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
442         }
443         if (!opts.benchmarkMode) {
444           addVerifyArchivedFields(cmd);
445         }
446 
447         if (opts.useVersion)
448             cmd.add("-version");
449 
450         for (String s : opts.suffix) cmd.add(s);
451 
452         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
453         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
454         return executeAndLog(pb, "exec");
455     }
456 
457 
458     // A commonly used convenience methods to create an archive and check the results
459     // Creates an archive and checks for errors
460     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
461         return checkExec(runWithArchive(opts));
462     }
463 
464 
465     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
466         return checkExec(runWithArchive(cliPrefix));
467     }
468 
469 
470     public static OutputAnalyzer checkExec(OutputAnalyzer output,
471                                      String... extraMatches) throws Exception {
472         CDSOptions opts = new CDSOptions();
473         return checkExec(output, opts, extraMatches);
474     }
475 
476 
477     // check result of 'exec' operation, that is when JVM is run using the archive
478     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
479                                      String... extraMatches) throws Exception {
480         try {
481             if ("on".equals(opts.xShareMode)) {
482                 output.shouldContain("sharing");
483             }
484             output.shouldHaveExitValue(0);
485         } catch (RuntimeException e) {
486             checkCommonExecExceptions(output, e);
487             return output;
488         }
489 
490         checkMatches(output, extraMatches);
491         return output;
492     }
493 
494 
495     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
496                                              int expectedExitValue,
497                                              String... extraMatches) throws Exception {
498         checkMappingFailure(output);
499         output.shouldHaveExitValue(expectedExitValue);
500         checkMatches(output, extraMatches);
501         return output;
502     }
503 
504     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
505                                               String... matches) throws Exception {
506         for (String match : matches) {
507             output.shouldContain(match);
508         }
509         return output;
510     }
511 
512     private static final String outputDir;
513     private static final File outputDirAsFile;
514 
515     static {
516         outputDir = System.getProperty("user.dir", ".");
517         outputDirAsFile = new File(outputDir);
518     }
519 
520     public static String getOutputDir() {
521         return outputDir;
522     }
523 
524     public static File getOutputDirAsFile() {
525         return outputDirAsFile;
526     }
527 
528     // get the file object for the test artifact
529     public static File getTestArtifact(String name, boolean checkExistence) {
530         File file = new File(outputDirAsFile, name);
531 
532         if (checkExistence && !file.exists()) {
533             throw new RuntimeException("Cannot find " + file.getPath());
534         }
535 
536         return file;
537     }
538 
539 
540     // create file containing the specified class list
541     public static File makeClassList(String classes[])
542         throws Exception {
543         return makeClassList(testName + "-", classes);
544     }
545 
546     // create file containing the specified class list
547     public static File makeClassList(String testCaseName, String classes[])
548         throws Exception {
549 
550         File classList = getTestArtifact(testCaseName + "test.classlist", false);
551         FileOutputStream fos = new FileOutputStream(classList);
552         PrintStream ps = new PrintStream(fos);
553 
554         addToClassList(ps, classes);
555 
556         ps.close();
557         fos.close();
558 
559         return classList;
560     }
561 
562 
563     public static void addToClassList(PrintStream ps, String classes[])
564         throws IOException
565     {
566         if (classes != null) {
567             for (String s : classes) {
568                 ps.println(s);
569             }
570         }
571     }
572 
573     private static String testName = Utils.TEST_NAME.replace('/', '.');
574 
575     private static final SimpleDateFormat timeStampFormat =
576         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
577 
578     private static String defaultArchiveName;
579 
580     // Call this method to start new archive with new unique name
581     public static void startNewArchiveName() {
582         defaultArchiveName = testName +
583             timeStampFormat.format(new Date()) + ".jsa";
584     }
585 
586     public static String getDefaultArchiveName() {
587         return defaultArchiveName;
588     }
589 
590 
591     // ===================== FILE ACCESS convenience methods
592     public static File getOutputFile(String name) {
593         return new File(outputDirAsFile, testName + "-" + name);
594     }
595 
596     public static String getOutputFileName(String name) {
597         return getOutputFile(name).getName();
598     }
599 
600 
601     public static File getOutputSourceFile(String name) {
602         return new File(outputDirAsFile, name);
603     }
604 
605 
606     public static File getSourceFile(String name) {
607         File dir = new File(System.getProperty("test.src", "."));
608         return new File(dir, name);
609     }
610 
611     // Check commandline for the last instance of Xshare to see if the process can load
612     // a CDS archive
613     public static boolean isRunningWithArchive(List<String> cmd) {
614         // -Xshare only works for the java executable
615         if (!cmd.get(0).equals(JDKToolFinder.getJDKTool("java")) || cmd.size() < 2) {
616             return false;
617         }
618 
619         // -Xshare options are likely at the end of the args list
620         for (int i = cmd.size() - 1; i >= 1; i--) {
621             String s = cmd.get(i);
622             if (s.equals("-Xshare:dump") || s.equals("-Xshare:off")) {
623                 return false;
624             }
625         }
626         return true;
627     }
628 
629     public static boolean isGCOption(String s) {
630         return s.startsWith("-XX:+Use") && s.endsWith("GC");
631     }
632 
633     public static boolean hasGCOption(List<String> cmd) {
634         for (String s : cmd) {
635             if (isGCOption(s)) {
636                 return true;
637             }
638         }
639         return false;
640     }
641 
642     // Handle and insert test.cds.runtime.options to commandline
643     // The test.cds.runtime.options property is used to inject extra VM options to
644     // subprocesses launched by the CDS test cases using executeAndLog().
645     // The injection applies only to subprocesses that:
646     //   - are launched by the standard java launcher (bin/java)
647     //   - are not dumping the CDS archive with -Xshare:dump
648     //   - do not explicitly disable CDS via -Xshare:off
649     //
650     // The main purpose of this property is to test the runtime loading of
651     // the CDS "archive heap region" with non-default garbage collectors. E.g.,
652     //
653     // jtreg -vmoptions:-Dtest.cds.runtime.options=-XX:+UnlockExperimentalVMOptions,-XX:+UseEpsilonGC \
654     //       test/hotspot/jtreg/runtime/cds
655     //
656     // Note that the injection is not applied to -Xshare:dump, so that the CDS archives
657     // will be dumped with G1, which is the only collector that supports dumping
658     // the archive heap region. Similarly, if a UseXxxGC option already exists in the command line,
659     // the UseXxxGC option added in test.cds.runtime.options will be ignored.
660     public static void handleCDSRuntimeOptions(ProcessBuilder pb) {
661         List<String> cmd = pb.command();
662         String jtropts = System.getProperty("test.cds.runtime.options");
663         if (jtropts != null && isRunningWithArchive(cmd)) {
664             // There cannot be multiple GC options in the command line so some
665             // options may be ignored
666             ArrayList<String> cdsRuntimeOpts = new ArrayList<String>();
667             boolean hasGCOption = hasGCOption(cmd);
668             for (String s : jtropts.split(",")) {
669                 if (!CDSOptions.disabledRuntimePrefixes.contains(s) &&
670                     !(hasGCOption && isGCOption(s))) {
671                     cdsRuntimeOpts.add(s);
672                 }
673             }
674             pb.command().addAll(1, cdsRuntimeOpts);
675         }
676     }
677 
678     // ============================= Logging
679     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
680         handleCDSRuntimeOptions(pb);
681         return executeAndLog(pb.start(), logName);
682     }
683 
684     public static OutputAnalyzer executeAndLog(Process process, String logName) throws Exception {
685         long started = System.currentTimeMillis();
686 
687         OutputAnalyzer output = new OutputAnalyzer(process);
688         String logFileNameStem =
689             String.format("%04d", getNextLogCounter()) + "-" + logName;
690 
691         File stdout = getOutputFile(logFileNameStem + ".stdout");
692         File stderr = getOutputFile(logFileNameStem + ".stderr");
693 
694         writeFile(stdout, output.getStdout());
695         writeFile(stderr, output.getStderr());
696         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
697         System.out.println("[logging stdout to " + stdout + "]");
698         System.out.println("[logging stderr to " + stderr + "]");
699         System.out.println("[STDERR]\n" + output.getStderr());
700 
701         if (copyChildStdoutToMainStdout)
702             System.out.println("[STDOUT]\n" + output.getStdout());
703 
704         if (output.getExitValue() != 0 && output.getStdout().contains("A fatal error has been detected")) {
705           throw new RuntimeException("Hotspot crashed");
706         }
707         return output;
708     }
709 
710 
711     private static void writeFile(File file, String content) throws Exception {
712         FileOutputStream fos = new FileOutputStream(file);
713         PrintStream ps = new PrintStream(fos);
714         ps.print(content);
715         ps.close();
716         fos.close();
717     }
718 
719     // Format a line that defines an extra symbol in the file specify by -XX:SharedArchiveConfigFile=<file>
720     public static String formatArchiveConfigSymbol(String symbol) {
721         int refCount = -1; // This is always -1 in the current HotSpot implementation.
722         if (isAsciiPrintable(symbol)) {
723             return symbol.length() + " " + refCount + ": " + symbol;
724         } else {
725             StringBuilder sb = new StringBuilder();
726             int utf8_length = escapeArchiveConfigString(sb, symbol);
727             return utf8_length + " " + refCount + ": " + sb.toString();
728         }
729     }
730 
731     // This method generates the same format as HashtableTextDump::put_utf8() in HotSpot,
732     // to be used by -XX:SharedArchiveConfigFile=<file>.
733     private static int escapeArchiveConfigString(StringBuilder sb, String s) {
734         byte arr[];
735         try {
736             arr = s.getBytes("UTF8");
737         } catch (java.io.UnsupportedEncodingException e) {
738             throw new RuntimeException("Unexpected", e);
739         }
740         for (int i = 0; i < arr.length; i++) {
741             char ch = (char)(arr[i] & 0xff);
742             if (isAsciiPrintable(ch)) {
743                 sb.append(ch);
744             } else if (ch == '\t') {
745                 sb.append("\\t");
746             } else if (ch == '\r') {
747                 sb.append("\\r");
748             } else if (ch == '\n') {
749                 sb.append("\\n");
750             } else if (ch == '\\') {
751                 sb.append("\\\\");
752             } else {
753                 String hex = Integer.toHexString(ch);
754                 if (ch < 16) {
755                     sb.append("\\x0");
756                 } else {
757                     sb.append("\\x");
758                 }
759                 sb.append(hex);
760             }
761         }
762 
763         return arr.length;
764     }
765 
766     private static boolean isAsciiPrintable(String s) {
767         for (int i = 0; i < s.length(); i++) {
768             if (!isAsciiPrintable(s.charAt(i))) {
769                 return false;
770             }
771         }
772         return true;
773     }
774 
775     private static boolean isAsciiPrintable(char ch) {
776         return ch >= 32 && ch < 127;
777     }
778 
779     // JDK utility
780 
781     // Do a cheap clone of the JDK. Most files can be sym-linked. However, $JAVA_HOME/bin/java and $JAVA_HOME/lib/.../libjvm.so"
782     // must be copied, because the java.home property is derived from the canonicalized paths of these 2 files.
783     // The jvm.cfg file must be copied because the cds/NonJVMVariantLocation.java
784     // test is testing a CDS archive can be loaded from a non-JVM variant directory.
785     // Set a list of {jvm, "java", "jvm.cfg"} which will be physically copied. If a file needs copied physically, add it to the list.
786     private static String[] phCopied = {System.mapLibraryName("jvm"), "java", "jvm.cfg"};
787     public static void clone(File src, File dst) throws Exception {
788         if (dst.exists()) {
789             if (!dst.isDirectory()) {
790                 throw new RuntimeException("Not a directory :" + dst);
791             }
792         } else {
793             if (!dst.mkdir()) {
794                 throw new RuntimeException("Cannot create directory: " + dst);
795             }
796         }
797         // final String jvmLib = System.mapLibraryName("jvm");
798         for (String child : src.list()) {
799             if (child.equals(".") || child.equals("..")) {
800                 continue;
801             }
802 
803             File child_src = new File(src, child);
804             File child_dst = new File(dst, child);
805             if (child_dst.exists()) {
806                 throw new RuntimeException("Already exists: " + child_dst);
807             }
808             if (child_src.isFile()) {
809                 boolean needPhCopy = false;
810                 for (String target : phCopied) {
811                     if (child.equals(target)) {
812                         needPhCopy = true;
813                         break;
814                     }
815                 }
816                 if (needPhCopy) {
817                     Files.copy(child_src.toPath(), /* copy data to -> */ child_dst.toPath(),
818                                new CopyOption[] { StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES});
819                 } else {
820                     Files.createSymbolicLink(child_dst.toPath(),  /* link to -> */ child_src.toPath());
821                 }
822             } else {
823                 clone(child_src, child_dst);
824             }
825         }
826     }
827 
828     // modulesDir, like $JDK/lib
829     // oldName, module name under modulesDir
830     // newName, new name for oldName
831     public static void rename(File fromFile, File toFile) throws Exception {
832         if (!fromFile.exists()) {
833             throw new RuntimeException(fromFile.getName() + " does not exist");
834         }
835 
836         if (toFile.exists()) {
837             throw new RuntimeException(toFile.getName() + " already exists");
838         }
839 
840         boolean success = fromFile.renameTo(toFile);
841         if (!success) {
842             throw new RuntimeException("rename file " + fromFile.getName()+ " to " + toFile.getName() + " failed");
843         }
844     }
845 
846     public static ProcessBuilder makeBuilder(String... args) throws Exception {
847         System.out.print("[");
848         for (String s : args) {
849             System.out.print(" " + s);
850         }
851         System.out.println(" ]");
852         return new ProcessBuilder(args);
853     }
854 
855     public static Path copyFile(String srcFile, String destDir) throws Exception {
856         int idx = srcFile.lastIndexOf(File.separator);
857         String jarName = srcFile.substring(idx + 1);
858         Path srcPath = Paths.get(jarName);
859         Path newPath = Paths.get(destDir);
860         Path newDir;
861         if (!Files.exists(newPath)) {
862             newDir = Files.createDirectories(newPath);
863         } else {
864             newDir = newPath;
865         }
866         Path destPath = newDir.resolve(jarName);
867         Files.copy(srcPath, destPath, REPLACE_EXISTING, COPY_ATTRIBUTES);
868         return destPath;
869     }
870 
871     // Some tests were initially written without the knowledge of -XX:+AOTClassLinking. These tests need to
872     // be adjusted if -XX:+AOTClassLinking is specified in jtreg -vmoptions or -javaoptions:
873     public static boolean isAOTClassLinkingEnabled() {
874         return isBooleanVMOptionEnabledInCommandLine("AOTClassLinking");
875     }
876 
877     public static boolean isBooleanVMOptionEnabledInCommandLine(String optionName) {
878         String lastMatch = null;
879         String pattern = "^-XX:." + optionName + "$";
880         for (String s : Utils.getTestJavaOpts()) {
881             if (s.matches(pattern)) {
882                 lastMatch = s;
883             }
884         }
885         if (lastMatch != null && lastMatch.equals("-XX:+" + optionName)) {
886             return true;
887         }
888         return false;
889     }
890 }