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     public static final String UnableToMapMsg =
232         "Unable to map shared archive: test did not complete";
233 
234     // Create bootstrap CDS archive,
235     // use extra JVM command line args as a prefix.
236     // For CDS tests specifying prefix makes more sense than specifying suffix, since
237     // normally there are no classes or arguments to classes, just "-version"
238     // To specify suffix explicitly use CDSOptions.addSuffix()
239     public static OutputAnalyzer createArchive(String... cliPrefix)
240         throws Exception {
241         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
242     }
243 
244     // Create bootstrap CDS archive
245     public static OutputAnalyzer createArchive(CDSOptions opts)
246         throws Exception {
247 
248         startNewArchiveName();
249 
250         ArrayList<String> cmd = new ArrayList<String>();
251 
252         for (String p : opts.prefix) cmd.add(p);
253 
254         cmd.add("-Xshare:dump");
255         cmd.add("-Xlog:cds,aot+hashtables");
256         if (opts.archiveName == null)
257             opts.archiveName = getDefaultArchiveName();
258         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
259 
260         if (opts.classList != null) {
261             File classListFile = makeClassList(opts.classList);
262             cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
263         }
264 
265         for (String s : opts.suffix) cmd.add(s);
266 
267         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
268         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
269         return executeAndLog(pb, "dump");
270     }
271 
272     public static boolean isDynamicArchive() {
273         return DYNAMIC_DUMP;
274     }
275 
276     // check result of 'dump-the-archive' operation, that is "-Xshare:dump"
277     public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches)
278         throws Exception {
279 
280         if (!DYNAMIC_DUMP) {
281             output.shouldContain("Loading classes to share");
282         } else {
283             output.shouldContain("Written dynamic archive 0x");
284         }
285         output.shouldHaveExitValue(0);
286         //output.shouldNotContain(MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE); // FIXME -- leyden+JEP483 merge
287 
288         for (String match : extraMatches) {
289             output.shouldContain(match);
290         }
291 
292         return output;
293     }
294 
295     // check result of dumping base archive
296     public static OutputAnalyzer checkBaseDump(OutputAnalyzer output) throws Exception {
297         output.shouldContain("Loading classes to share");
298         output.shouldHaveExitValue(0);
299         //output.shouldNotContain(MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE); // FIXME -- leyden+JEP483 merge
300         return output;
301     }
302 
303     // A commonly used convenience methods to create an archive and check the results
304     // Creates an archive and checks for errors
305     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
306         throws Exception {
307         return checkDump(createArchive(opts));
308     }
309 
310 
311     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
312         throws Exception {
313         return checkDump(createArchive(cliPrefix));
314     }
315 
316 
317     // This method should be used to check the output of child VM for common exceptions.
318     // Most of CDS tests deal with child VM processes for creating and using the archive.
319     // However exceptions that occur in the child process do not automatically propagate
320     // to the parent process. This mechanism aims to improve the propagation
321     // of exceptions and common errors.
322     // Exception e argument - an exception to be re-thrown if none of the common
323     // exceptions match. Pass null if you wish not to re-throw any exception.
324     public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e)
325         throws Exception {
326         if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) {
327             throw new RuntimeException("Hotspot crashed");
328         }
329         if (output.getStdout().contains("TEST FAILED")) {
330             throw new RuntimeException("Test Failed");
331         }
332         if (output.getOutput().contains("Unable to unmap shared space")) {
333             throw new RuntimeException("Unable to unmap shared space");
334         }
335 
336         // Special case -- sometimes Xshare:on fails because it failed to map
337         // at given address. This behavior is platform-specific, machine config-specific
338         // and can be random (see ASLR).
339         checkMappingFailure(output);
340 
341         if (e != null) {
342             throw e;
343         }
344     }
345 
346     public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
347         checkCommonExecExceptions(output, null);
348     }
349 
350 
351     // Check the output for indication that mapping of the archive failed.
352     // Performance note: this check seems to be rather costly - searching the entire
353     // output stream of a child process for multiple strings. However, it is necessary
354     // to detect this condition, a failure to map an archive, since this is not a real
355     // failure of the test or VM operation, and results in a test being "skipped".
356     // Suggestions to improve:
357     // 1. VM can designate a special exit code for such condition.
358     // 2. VM can print a single distinct string indicating failure to map an archive,
359     //    instead of utilizing multiple messages.
360     // These are suggestions to improve testibility of the VM. However, implementing them
361     // could also improve usability in the field.
362     private static String hasUnableToMapMessage(OutputAnalyzer output) {
363         String outStr = output.getOutput();
364         if ((output.getExitValue() == 1)) {
365             if (outStr.contains(MSG_RANGE_NOT_WITHIN_HEAP)) {
366                 return MSG_RANGE_NOT_WITHIN_HEAP;
367             }
368             if (outStr.contains(MSG_DYNAMIC_NOT_SUPPORTED)) {
369                 return MSG_DYNAMIC_NOT_SUPPORTED;
370             }
371         }
372 
373         return null;
374     }
375 
376     public static boolean isUnableToMap(OutputAnalyzer output) {
377         return hasUnableToMapMessage(output) != null;
378     }
379 
380     public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException {
381         String match = hasUnableToMapMessage(out);
382         if (match != null) {
383             throw new SkippedException(UnableToMapMsg + ": " + match);
384         }
385     }
386 
387     public static Result run(String... cliPrefix) throws Exception {
388         CDSOptions opts = new CDSOptions();
389         opts.setArchiveName(getDefaultArchiveName());
390         opts.addPrefix(cliPrefix);
391         return new Result(opts, runWithArchive(opts));
392     }
393 
394     public static Result run(CDSOptions opts) throws Exception {
395         return new Result(opts, runWithArchive(opts));
396     }
397 
398     // Dump a classlist using the -XX:DumpLoadedClassList option.
399     public static Result dumpClassList(String classListName, String... cli)
400         throws Exception {
401         CDSOptions opts = (new CDSOptions())
402             .setUseVersion(false)
403             .setXShareMode("auto")
404             .addPrefix("-XX:DumpLoadedClassList=" + classListName)
405             .addSuffix(cli);
406         Result res = run(opts).assertNormalExit();
407         return res;
408     }
409 
410     // Execute JVM with CDS archive, specify command line args suffix
411     public static OutputAnalyzer runWithArchive(String... cliPrefix)
412         throws Exception {
413 
414         return runWithArchive( (new CDSOptions())
415                                .setArchiveName(getDefaultArchiveName())
416                                .addPrefix(cliPrefix) );
417     }
418 
419     // Enable basic verification (VerifyArchivedFields=1, no side effects) for all CDS
420     // tests to make sure the archived heap objects are mapped/loaded properly.
421     public static void addVerifyArchivedFields(ArrayList<String> cmd) {
422         cmd.add("-XX:+UnlockDiagnosticVMOptions");
423         cmd.add("-XX:VerifyArchivedFields=1");
424     }
425 
426     // Execute JVM with CDS archive, specify CDSOptions
427     public static OutputAnalyzer runWithArchive(CDSOptions opts)
428         throws Exception {
429 
430         ArrayList<String> cmd = new ArrayList<String>();
431         cmd.addAll(opts.prefix);
432         cmd.add("-Xshare:" + opts.xShareMode);
433         cmd.add("-Dtest.timeout.factor=" + Utils.TIMEOUT_FACTOR);
434 
435         if (!opts.useSystemArchive) {
436             if (opts.archiveName == null)
437                 opts.archiveName = getDefaultArchiveName();
438             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
439         }
440 
441         if (opts.useVersion)
442             cmd.add("-version");
443 
444         for (String s : opts.suffix) cmd.add(s);
445 
446         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
447         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
448         return executeAndLog(pb, "exec");
449     }
450 
451 
452     // A commonly used convenience methods to create an archive and check the results
453     // Creates an archive and checks for errors
454     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
455         return checkExec(runWithArchive(opts));
456     }
457 
458 
459     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
460         return checkExec(runWithArchive(cliPrefix));
461     }
462 
463 
464     public static OutputAnalyzer checkExec(OutputAnalyzer output,
465                                      String... extraMatches) throws Exception {
466         CDSOptions opts = new CDSOptions();
467         return checkExec(output, opts, extraMatches);
468     }
469 
470 
471     // check result of 'exec' operation, that is when JVM is run using the archive
472     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
473                                      String... extraMatches) throws Exception {
474         try {
475             if ("on".equals(opts.xShareMode)) {
476                 output.shouldContain("sharing");
477             }
478             output.shouldHaveExitValue(0);
479         } catch (RuntimeException e) {
480             checkCommonExecExceptions(output, e);
481             return output;
482         }
483 
484         checkMatches(output, extraMatches);
485         return output;
486     }
487 
488 
489     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
490                                              int expectedExitValue,
491                                              String... extraMatches) throws Exception {
492         checkMappingFailure(output);
493         output.shouldHaveExitValue(expectedExitValue);
494         checkMatches(output, extraMatches);
495         return output;
496     }
497 
498     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
499                                               String... matches) throws Exception {
500         for (String match : matches) {
501             output.shouldContain(match);
502         }
503         return output;
504     }
505 
506     private static final String outputDir;
507     private static final File outputDirAsFile;
508 
509     static {
510         outputDir = System.getProperty("user.dir", ".");
511         outputDirAsFile = new File(outputDir);
512     }
513 
514     public static String getOutputDir() {
515         return outputDir;
516     }
517 
518     public static File getOutputDirAsFile() {
519         return outputDirAsFile;
520     }
521 
522     // get the file object for the test artifact
523     public static File getTestArtifact(String name, boolean checkExistence) {
524         File file = new File(outputDirAsFile, name);
525 
526         if (checkExistence && !file.exists()) {
527             throw new RuntimeException("Cannot find " + file.getPath());
528         }
529 
530         return file;
531     }
532 
533 
534     // create file containing the specified class list
535     public static File makeClassList(String classes[])
536         throws Exception {
537         return makeClassList(testName + "-", classes);
538     }
539 
540     // create file containing the specified class list
541     public static File makeClassList(String testCaseName, String classes[])
542         throws Exception {
543 
544         File classList = getTestArtifact(testCaseName + "test.classlist", false);
545         FileOutputStream fos = new FileOutputStream(classList);
546         PrintStream ps = new PrintStream(fos);
547 
548         addToClassList(ps, classes);
549 
550         ps.close();
551         fos.close();
552 
553         return classList;
554     }
555 
556 
557     public static void addToClassList(PrintStream ps, String classes[])
558         throws IOException
559     {
560         if (classes != null) {
561             for (String s : classes) {
562                 ps.println(s);
563             }
564         }
565     }
566 
567     private static String testName = Utils.TEST_NAME.replace('/', '.');
568 
569     private static final SimpleDateFormat timeStampFormat =
570         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
571 
572     private static String defaultArchiveName;
573 
574     // Call this method to start new archive with new unique name
575     public static void startNewArchiveName() {
576         defaultArchiveName = testName +
577             timeStampFormat.format(new Date()) + ".jsa";
578     }
579 
580     public static String getDefaultArchiveName() {
581         return defaultArchiveName;
582     }
583 
584 
585     // ===================== FILE ACCESS convenience methods
586     public static File getOutputFile(String name) {
587         return new File(outputDirAsFile, testName + "-" + name);
588     }
589 
590     public static String getOutputFileName(String name) {
591         return getOutputFile(name).getName();
592     }
593 
594 
595     public static File getOutputSourceFile(String name) {
596         return new File(outputDirAsFile, name);
597     }
598 
599 
600     public static File getSourceFile(String name) {
601         File dir = new File(System.getProperty("test.src", "."));
602         return new File(dir, name);
603     }
604 
605     // Check commandline for the last instance of Xshare to see if the process can load
606     // a CDS archive
607     public static boolean isRunningWithArchive(List<String> cmd) {
608         // -Xshare only works for the java executable
609         if (!cmd.get(0).equals(JDKToolFinder.getJDKTool("java")) || cmd.size() < 2) {
610             return false;
611         }
612 
613         // -Xshare options are likely at the end of the args list
614         for (int i = cmd.size() - 1; i >= 1; i--) {
615             String s = cmd.get(i);
616             if (s.equals("-Xshare:dump") || s.equals("-Xshare:off")) {
617                 return false;
618             }
619         }
620         return true;
621     }
622 
623     public static boolean isGCOption(String s) {
624         return s.startsWith("-XX:+Use") && s.endsWith("GC");
625     }
626 
627     public static boolean hasGCOption(List<String> cmd) {
628         for (String s : cmd) {
629             if (isGCOption(s)) {
630                 return true;
631             }
632         }
633         return false;
634     }
635 
636     // Handle and insert test.cds.runtime.options to commandline
637     // The test.cds.runtime.options property is used to inject extra VM options to
638     // subprocesses launched by the CDS test cases using executeAndLog().
639     // The injection applies only to subprocesses that:
640     //   - are launched by the standard java launcher (bin/java)
641     //   - are not dumping the CDS archive with -Xshare:dump
642     //   - do not explicitly disable CDS via -Xshare:off
643     //
644     // The main purpose of this property is to test the runtime loading of
645     // the CDS "archive heap region" with non-default garbage collectors. E.g.,
646     //
647     // jtreg -vmoptions:-Dtest.cds.runtime.options=-XX:+UnlockExperimentalVMOptions,-XX:+UseEpsilonGC \
648     //       test/hotspot/jtreg/runtime/cds
649     //
650     // Note that the injection is not applied to -Xshare:dump, so that the CDS archives
651     // will be dumped with G1, which is the only collector that supports dumping
652     // the archive heap region. Similarly, if a UseXxxGC option already exists in the command line,
653     // the UseXxxGC option added in test.cds.runtime.options will be ignored.
654     public static void handleCDSRuntimeOptions(ProcessBuilder pb) {
655         List<String> cmd = pb.command();
656         String jtropts = System.getProperty("test.cds.runtime.options");
657         if (jtropts != null && isRunningWithArchive(cmd)) {
658             // There cannot be multiple GC options in the command line so some
659             // options may be ignored
660             ArrayList<String> cdsRuntimeOpts = new ArrayList<String>();
661             boolean hasGCOption = hasGCOption(cmd);
662             for (String s : jtropts.split(",")) {
663                 if (!CDSOptions.disabledRuntimePrefixes.contains(s) &&
664                     !(hasGCOption && isGCOption(s))) {
665                     cdsRuntimeOpts.add(s);
666                 }
667             }
668             pb.command().addAll(1, cdsRuntimeOpts);
669         }
670     }
671 
672     // ============================= Logging
673     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
674         handleCDSRuntimeOptions(pb);
675         return executeAndLog(pb.start(), logName);
676     }
677 
678     public static OutputAnalyzer executeAndLog(Process process, String logName) throws Exception {
679         long started = System.currentTimeMillis();
680 
681         OutputAnalyzer output = new OutputAnalyzer(process);
682         String logFileNameStem =
683             String.format("%04d", getNextLogCounter()) + "-" + logName;
684 
685         File stdout = getOutputFile(logFileNameStem + ".stdout");
686         File stderr = getOutputFile(logFileNameStem + ".stderr");
687 
688         writeFile(stdout, output.getStdout());
689         writeFile(stderr, output.getStderr());
690         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
691         System.out.println("[logging stdout to " + stdout + "]");
692         System.out.println("[logging stderr to " + stderr + "]");
693         System.out.println("[STDERR]\n" + output.getStderr());
694 
695         if (copyChildStdoutToMainStdout)
696             System.out.println("[STDOUT]\n" + output.getStdout());
697 
698         if (output.getExitValue() != 0 && output.getStdout().contains("A fatal error has been detected")) {
699           throw new RuntimeException("Hotspot crashed");
700         }
701         return output;
702     }
703 
704 
705     private static void writeFile(File file, String content) throws Exception {
706         FileOutputStream fos = new FileOutputStream(file);
707         PrintStream ps = new PrintStream(fos);
708         ps.print(content);
709         ps.close();
710         fos.close();
711     }
712 
713     // Format a line that defines an extra symbol in the file specify by -XX:SharedArchiveConfigFile=<file>
714     public static String formatArchiveConfigSymbol(String symbol) {
715         int refCount = -1; // This is always -1 in the current HotSpot implementation.
716         if (isAsciiPrintable(symbol)) {
717             return symbol.length() + " " + refCount + ": " + symbol;
718         } else {
719             StringBuilder sb = new StringBuilder();
720             int utf8_length = escapeArchiveConfigString(sb, symbol);
721             return utf8_length + " " + refCount + ": " + sb.toString();
722         }
723     }
724 
725     // This method generates the same format as HashtableTextDump::put_utf8() in HotSpot,
726     // to be used by -XX:SharedArchiveConfigFile=<file>.
727     private static int escapeArchiveConfigString(StringBuilder sb, String s) {
728         byte arr[];
729         try {
730             arr = s.getBytes("UTF8");
731         } catch (java.io.UnsupportedEncodingException e) {
732             throw new RuntimeException("Unexpected", e);
733         }
734         for (int i = 0; i < arr.length; i++) {
735             char ch = (char)(arr[i] & 0xff);
736             if (isAsciiPrintable(ch)) {
737                 sb.append(ch);
738             } else if (ch == '\t') {
739                 sb.append("\\t");
740             } else if (ch == '\r') {
741                 sb.append("\\r");
742             } else if (ch == '\n') {
743                 sb.append("\\n");
744             } else if (ch == '\\') {
745                 sb.append("\\\\");
746             } else {
747                 String hex = Integer.toHexString(ch);
748                 if (ch < 16) {
749                     sb.append("\\x0");
750                 } else {
751                     sb.append("\\x");
752                 }
753                 sb.append(hex);
754             }
755         }
756 
757         return arr.length;
758     }
759 
760     private static boolean isAsciiPrintable(String s) {
761         for (int i = 0; i < s.length(); i++) {
762             if (!isAsciiPrintable(s.charAt(i))) {
763                 return false;
764             }
765         }
766         return true;
767     }
768 
769     private static boolean isAsciiPrintable(char ch) {
770         return ch >= 32 && ch < 127;
771     }
772 
773     // JDK utility
774 
775     // Do a cheap clone of the JDK. Most files can be sym-linked. However, $JAVA_HOME/bin/java and $JAVA_HOME/lib/.../libjvm.so"
776     // must be copied, because the java.home property is derived from the canonicalized paths of these 2 files.
777     // The jvm.cfg file must be copied because the cds/NonJVMVariantLocation.java
778     // test is testing a CDS archive can be loaded from a non-JVM variant directory.
779     // Set a list of {jvm, "java", "jvm.cfg"} which will be physically copied. If a file needs copied physically, add it to the list.
780     private static String[] phCopied = {System.mapLibraryName("jvm"), "java", "jvm.cfg"};
781     public static void clone(File src, File dst) throws Exception {
782         if (dst.exists()) {
783             if (!dst.isDirectory()) {
784                 throw new RuntimeException("Not a directory :" + dst);
785             }
786         } else {
787             if (!dst.mkdir()) {
788                 throw new RuntimeException("Cannot create directory: " + dst);
789             }
790         }
791         // final String jvmLib = System.mapLibraryName("jvm");
792         for (String child : src.list()) {
793             if (child.equals(".") || child.equals("..")) {
794                 continue;
795             }
796 
797             File child_src = new File(src, child);
798             File child_dst = new File(dst, child);
799             if (child_dst.exists()) {
800                 throw new RuntimeException("Already exists: " + child_dst);
801             }
802             if (child_src.isFile()) {
803                 boolean needPhCopy = false;
804                 for (String target : phCopied) {
805                     if (child.equals(target)) {
806                         needPhCopy = true;
807                         break;
808                     }
809                 }
810                 if (needPhCopy) {
811                     Files.copy(child_src.toPath(), /* copy data to -> */ child_dst.toPath(),
812                                new CopyOption[] { StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES});
813                 } else {
814                     Files.createSymbolicLink(child_dst.toPath(),  /* link to -> */ child_src.toPath());
815                 }
816             } else {
817                 clone(child_src, child_dst);
818             }
819         }
820     }
821 
822     // modulesDir, like $JDK/lib
823     // oldName, module name under modulesDir
824     // newName, new name for oldName
825     public static void rename(File fromFile, File toFile) throws Exception {
826         if (!fromFile.exists()) {
827             throw new RuntimeException(fromFile.getName() + " does not exist");
828         }
829 
830         if (toFile.exists()) {
831             throw new RuntimeException(toFile.getName() + " already exists");
832         }
833 
834         boolean success = fromFile.renameTo(toFile);
835         if (!success) {
836             throw new RuntimeException("rename file " + fromFile.getName()+ " to " + toFile.getName() + " failed");
837         }
838     }
839 
840     public static ProcessBuilder makeBuilder(String... args) throws Exception {
841         System.out.print("[");
842         for (String s : args) {
843             System.out.print(" " + s);
844         }
845         System.out.println(" ]");
846         return new ProcessBuilder(args);
847     }
848 
849     public static Path copyFile(String srcFile, String destDir) throws Exception {
850         int idx = srcFile.lastIndexOf(File.separator);
851         String jarName = srcFile.substring(idx + 1);
852         Path srcPath = Paths.get(jarName);
853         Path newPath = Paths.get(destDir);
854         Path newDir;
855         if (!Files.exists(newPath)) {
856             newDir = Files.createDirectories(newPath);
857         } else {
858             newDir = newPath;
859         }
860         Path destPath = newDir.resolve(jarName);
861         Files.copy(srcPath, destPath, REPLACE_EXISTING, COPY_ATTRIBUTES);
862         return destPath;
863     }
864 
865     // Some tests were initially written without the knowledge of -XX:+AOTClassLinking. These tests need to
866     // be adjusted if -XX:+AOTClassLinking is specified in jtreg -vmoptions or -javaoptions:
867     public static boolean isAOTClassLinkingEnabled() {
868         return isBooleanVMOptionEnabledInCommandLine("AOTClassLinking");
869     }
870 
871     public static boolean isBooleanVMOptionEnabledInCommandLine(String optionName) {
872         String lastMatch = null;
873         String pattern = "^-XX:." + optionName + "$";
874         for (String s : Utils.getTestJavaOpts()) {
875             if (s.matches(pattern)) {
876                 lastMatch = s;
877             }
878         }
879         if (lastMatch != null && lastMatch.equals("-XX:+" + optionName)) {
880             return true;
881         }
882         return false;
883     }
884 }