1 /*
   2  * Copyright (c) 2014, 2017, 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 
  24 package jdk.test.lib.cli;
  25 
  26 import java.util.List;
  27 import java.util.ArrayList;
  28 import java.util.Collections;
  29 import java.util.function.BooleanSupplier;
  30 
  31 import jdk.test.lib.management.InputArguments;
  32 import jdk.test.lib.process.ExitCode;
  33 import jdk.test.lib.process.ProcessTools;
  34 import jdk.test.lib.process.OutputAnalyzer;
  35 import jdk.test.lib.Platform;
  36 import jdk.test.lib.Utils;
  37 
  38 /**
  39  * Base class for command line option tests.
  40  */
  41 public abstract class CommandLineOptionTest {
  42     public static final String UNLOCK_DIAGNOSTIC_VM_OPTIONS
  43             = "-XX:+UnlockDiagnosticVMOptions";
  44     public static final String UNLOCK_EXPERIMENTAL_VM_OPTIONS
  45             = "-XX:+UnlockExperimentalVMOptions";
  46     protected static final String UNRECOGNIZED_OPTION_ERROR_FORMAT
  47             = "Unrecognized VM option '[+-]?%s(=.*)?'";
  48     protected static final String EXPERIMENTAL_OPTION_ERROR_FORMAT
  49             = "VM option '%s' is experimental and must be enabled via "
  50             + "-XX:\\+UnlockExperimentalVMOptions.";
  51     protected static final String DIAGNOSTIC_OPTION_ERROR_FORMAT
  52             = " VM option '%s' is diagnostic and must be enabled via "
  53             + "-XX:\\+UnlockDiagnosticVMOptions.";
  54     private static final String PRINT_FLAGS_FINAL_FORMAT = "%s\\s*:?=\\s*%s";
  55 
  56     /**
  57      * Verifies that JVM startup behavior matches our expectations.
  58      *
  59      * @param option an option that should be passed to JVM
  60      * @param expectedMessages an array of patterns that should occur
  61      *                          in JVM output. If {@code null} then
  62      *                          JVM output could be empty.
  63      * @param unexpectedMessages an array of patterns that should not
  64      *                           occur in JVM output. If {@code null} then
  65      *                           JVM output could be empty.
  66      * @param exitErrorMessage message that will be shown if exit code is not
  67      *                           as expected.
  68      * @param wrongWarningMessage message that will be shown if warning
  69      *                           messages are not as expected.
  70      * @param exitCode expected exit code.
  71      * @throws Throwable if verification fails or some other issues occur.
  72      */
  73     public static void verifyJVMStartup(String option,
  74             String expectedMessages[], String unexpectedMessages[],
  75             String exitErrorMessage, String wrongWarningMessage,
  76             ExitCode exitCode) throws Throwable {
  77         CommandLineOptionTest.verifyJVMStartup(expectedMessages,
  78                 unexpectedMessages, exitErrorMessage,
  79                 wrongWarningMessage, exitCode, false, option);
  80     }
  81 
  82     /**
  83      * Verifies that JVM startup behavior matches our expectations.
  84      *
  85      * @param expectedMessages an array of patterns that should occur
  86      *                         in JVM output. If {@code null} then
  87      *                         JVM output could be empty.
  88      * @param unexpectedMessages an array of patterns that should not
  89      *                           occur in JVM output. If {@code null} then
  90      *                           JVM output could be empty.
  91      * @param exitErrorMessage message that will be shown if exit code is not
  92      *                           as expected.
  93      * @param wrongWarningMessage message that will be shown if warning
  94      *                           messages are not as expected.
  95      * @param exitCode expected exit code.
  96      * @param addTestVMOptions if {@code true} then test VM options will be
  97      *                         passed to VM.
  98      * @param options options that should be passed to VM in addition to mode
  99      *                flag.
 100      * @throws Throwable if verification fails or some other issues occur.
 101      */
 102     public static void verifyJVMStartup(String expectedMessages[],
 103             String unexpectedMessages[], String exitErrorMessage,
 104             String wrongWarningMessage, ExitCode exitCode,
 105             boolean addTestVMOptions, String... options)
 106                     throws Throwable {
 107         List<String> finalOptions = new ArrayList<>();
 108         if (addTestVMOptions) {
 109             Collections.addAll(finalOptions, InputArguments.getVmInputArgs());
 110             Collections.addAll(finalOptions, Utils.getTestJavaOpts());
 111         }
 112         Collections.addAll(finalOptions, options);
 113         finalOptions.add("-version");
 114 
 115         ProcessBuilder processBuilder
 116                 = ProcessTools.createJavaProcessBuilder(finalOptions.toArray(
 117                 new String[finalOptions.size()]));
 118         OutputAnalyzer outputAnalyzer
 119                 = new OutputAnalyzer(processBuilder.start());
 120 
 121         try {
 122                 outputAnalyzer.shouldHaveExitValue(exitCode.value);
 123         } catch (RuntimeException e) {
 124             String errorMessage = String.format(
 125                     "JVM process should have exit value '%d'.%n%s",
 126                     exitCode.value, exitErrorMessage);
 127             throw new AssertionError(errorMessage, e);
 128         }
 129 
 130         verifyOutput(expectedMessages, unexpectedMessages,
 131                 wrongWarningMessage, outputAnalyzer);
 132     }
 133 
 134     /**
 135      * Verifies that JVM startup behavior matches our expectations.
 136      *
 137      * @param expectedMessages an array of patterns that should occur in JVM
 138      *                         output. If {@code null} then
 139      *                         JVM output could be empty.
 140      * @param unexpectedMessages an array of patterns that should not occur
 141      *                           in JVM output. If {@code null} then
 142      *                           JVM output could be empty.
 143      * @param wrongWarningMessage message that will be shown if messages are
 144      *                            not as expected.
 145      * @param outputAnalyzer OutputAnalyzer instance
 146      * @throws AssertionError if verification fails.
 147      */
 148     public static void verifyOutput(String[] expectedMessages,
 149             String[] unexpectedMessages, String wrongWarningMessage,
 150             OutputAnalyzer outputAnalyzer) {
 151         if (expectedMessages != null) {
 152             for (String expectedMessage : expectedMessages) {
 153                 try {
 154                     outputAnalyzer.shouldMatch(expectedMessage);
 155                 } catch (RuntimeException e) {
 156                     String errorMessage = String.format(
 157                             "Expected message not found: '%s'.%n%s",
 158                             expectedMessage, wrongWarningMessage);
 159                     throw new AssertionError(errorMessage, e);
 160                 }
 161             }
 162         }
 163 
 164         if (unexpectedMessages != null) {
 165             for (String unexpectedMessage : unexpectedMessages) {
 166                 try {
 167                     outputAnalyzer.shouldNotMatch(unexpectedMessage);
 168                 } catch (RuntimeException e) {
 169                     String errorMessage = String.format(
 170                             "Unexpected message found: '%s'.%n%s",
 171                             unexpectedMessage, wrongWarningMessage);
 172                     throw new AssertionError(errorMessage, e);
 173                 }
 174             }
 175         }
 176     }
 177 
 178     /**
 179      * Verifies that JVM startup behavior matches our expectations when type
 180      * of newly started VM is the same as the type of current.
 181      *
 182      * @param expectedMessages an array of patterns that should occur
 183      *                         in JVM output. If {@code null} then
 184      *                         JVM output could be empty.
 185      * @param unexpectedMessages an array of patterns that should not
 186      *                           occur in JVM output. If {@code null} then
 187      *                           JVM output could be empty.
 188      * @param exitErrorMessage Message that will be shown if exit value is not
 189      *                           as expected.
 190      * @param wrongWarningMessage message that will be shown if warning
 191      *                           messages are not as expected.
 192      * @param exitCode expected exit code.
 193      * @param options options that should be passed to VM in addition to mode
 194      *                flag.
 195      * @throws Throwable if verification fails or some other issues occur.
 196      */
 197     public static void verifySameJVMStartup(String expectedMessages[],
 198             String unexpectedMessages[], String exitErrorMessage,
 199             String wrongWarningMessage, ExitCode exitCode, String... options)
 200             throws Throwable {
 201         List<String> finalOptions = new ArrayList<>();
 202         finalOptions.add(CommandLineOptionTest.getVMTypeOption());
 203         String extraFlagForEmulated = CommandLineOptionTest.getVMTypeOptionForEmulated();
 204         if (extraFlagForEmulated != null) {
 205             finalOptions.add(extraFlagForEmulated);
 206         }
 207         Collections.addAll(finalOptions, options);
 208 
 209         CommandLineOptionTest.verifyJVMStartup(expectedMessages,
 210                 unexpectedMessages, exitErrorMessage,
 211                 wrongWarningMessage, exitCode, false,
 212                 finalOptions.toArray(new String[finalOptions.size()]));
 213     }
 214 
 215     /**
 216      * Verifies that value of specified JVM option is the same as
 217      * expected value.
 218      * This method filter out option with {@code optionName}
 219      * name from test java options.
 220      *
 221      * @param optionName a name of tested option.
 222      * @param expectedValue expected value of tested option.
 223      * @param optionErrorString message will be shown if option value is not as
 224      *                         expected.
 225      * @param additionalVMOpts additional options that should be
 226      *                         passed to JVM.
 227      * @throws Throwable if verification fails or some other issues occur.
 228      */
 229     public static void verifyOptionValue(String optionName,
 230             String expectedValue, String optionErrorString,
 231             String... additionalVMOpts) throws Throwable {
 232         verifyOptionValue(optionName, expectedValue, optionErrorString,
 233                 true, additionalVMOpts);
 234     }
 235 
 236     /**
 237      * Verifies that value of specified JVM option is the same as
 238      * expected value.
 239      * This method filter out option with {@code optionName}
 240      * name from test java options.
 241      *
 242      * @param optionName a name of tested option.
 243      * @param expectedValue expected value of tested option.
 244      * @param addTestVmOptions if {@code true}, then test VM options
 245      *                         will be used.
 246      * @param optionErrorString message will be shown if option value is not as
 247      *                         expected.
 248      * @param additionalVMOpts additional options that should be
 249      *                         passed to JVM.
 250      * @throws Throwable if verification fails or some other issues
 251      *                          occur.
 252      */
 253     public static void verifyOptionValue(String optionName,
 254             String expectedValue, String optionErrorString,
 255             boolean addTestVmOptions, String... additionalVMOpts)
 256                     throws Throwable {
 257         List<String> vmOpts = new ArrayList<>();
 258 
 259         if (addTestVmOptions) {
 260             Collections.addAll(vmOpts,
 261                                Utils.getFilteredTestJavaOpts(optionName));
 262         }
 263         Collections.addAll(vmOpts, additionalVMOpts);
 264         Collections.addAll(vmOpts, "-XX:+PrintFlagsFinal", "-version");
 265 
 266         ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
 267                 vmOpts.toArray(new String[vmOpts.size()]));
 268 
 269         OutputAnalyzer outputAnalyzer
 270                 = new OutputAnalyzer(processBuilder.start());
 271 
 272         try {
 273             outputAnalyzer.shouldHaveExitValue(0);
 274         } catch (RuntimeException e) {
 275             String errorMessage = String.format(
 276                     "JVM should start with option '%s' without errors.",
 277                     optionName);
 278             throw new AssertionError(errorMessage, e);
 279         }
 280         verifyOptionValue(optionName, expectedValue, optionErrorString,
 281                 outputAnalyzer);
 282     }
 283 
 284     /**
 285      * Verifies that value of specified JVM option is the same as
 286      * expected value.
 287      *
 288      * @param optionName a name of tested option.
 289      * @param expectedValue expected value of tested option.
 290      * @param optionErrorString message will be shown if option value is not
 291      *                          as expected.
 292      * @param outputAnalyzer OutputAnalyzer instance
 293      * @throws AssertionError if verification fails
 294      */
 295     public static void verifyOptionValue(String optionName,
 296             String expectedValue, String optionErrorString,
 297             OutputAnalyzer outputAnalyzer) {
 298         try {
 299             outputAnalyzer.shouldMatch(String.format(
 300                     CommandLineOptionTest.PRINT_FLAGS_FINAL_FORMAT,
 301                     optionName, expectedValue));
 302         } catch (RuntimeException e) {
 303             String errorMessage = String.format(
 304                     "Option '%s' is expected to have '%s' value%n%s",
 305                     optionName, expectedValue,
 306                     optionErrorString);
 307             throw new AssertionError(errorMessage, e);
 308         }
 309     }
 310 
 311     /**
 312      * Start VM with given options and values.
 313      * Generates command line option flags from
 314      * {@code optionNames} and {@code optionValues}.
 315      *
 316      * @param optionNames names of options to pass in
 317      * @param optionValues  values of option
 318      * @param additionalVMOpts additional options that should be
 319      *                         passed to JVM.
 320      * @return output from vm process
 321      */
 322     public static OutputAnalyzer startVMWithOptions(String[] optionNames,
 323             String[] optionValues,
 324             String... additionalVMOpts) throws Throwable {
 325         List<String> vmOpts = new ArrayList<>();
 326         if (optionNames == null || optionValues == null || optionNames.length != optionValues.length) {
 327             throw new IllegalArgumentException("optionNames and/or optionValues");
 328         }
 329 
 330         for (int i = 0; i < optionNames.length; i++) {
 331           vmOpts.add(prepareFlag(optionNames[i], optionValues[i]));
 332         }
 333         Collections.addAll(vmOpts, additionalVMOpts);
 334         Collections.addAll(vmOpts, "-version");
 335 
 336         ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
 337                 vmOpts.toArray(new String[vmOpts.size()]));
 338 
 339         return new OutputAnalyzer(processBuilder.start());
 340     }
 341 
 342     /**
 343      * Verifies from the output that values of specified JVM options were the same as
 344      * expected values.
 345      *
 346      * @param outputAnalyzer search output for expect options and values.
 347      * @param optionNames names of tested options.
 348      * @param expectedValues expected values of tested options.
 349      * @throws Throwable if verification fails or some other issues occur.
 350      */
 351     public static void verifyOptionValuesFromOutput(OutputAnalyzer outputAnalyzer,
 352             String[] optionNames,
 353             String[] expectedValues) throws Throwable {
 354         outputAnalyzer.shouldHaveExitValue(0);
 355         for (int i = 0; i < optionNames.length; i++) {
 356           outputAnalyzer.shouldMatch(String.format(
 357                 CommandLineOptionTest.PRINT_FLAGS_FINAL_FORMAT,
 358                 optionNames[i], expectedValues[i]));
 359         }
 360     }
 361 
 362    /**
 363      * Verifies that value of specified JVM options are the same as
 364      * expected values.
 365      * Generates command line option flags from
 366      * {@code optionNames} and {@code expectedValues}.
 367      *
 368      * @param optionNames names of tested options.
 369      * @param expectedValues expected values of tested options.
 370      * @throws Throwable if verification fails or some other issues occur.
 371      */
 372     public static void verifyOptionValues(String[] optionNames,
 373             String[] expectedValues) throws Throwable {
 374        OutputAnalyzer outputAnalyzer = startVMWithOptions(optionNames, expectedValues, "-XX:+PrintFlagsFinal");
 375        verifyOptionValuesFromOutput(outputAnalyzer, optionNames, expectedValues);
 376     }
 377 
 378     /**
 379      * Verifies that value of specified JVM when type of newly started VM
 380      * is the same as the type of current.
 381      * This method filter out option with {@code optionName}
 382      * name from test java options.
 383      * Only mode flag will be passed to VM in addition to
 384      * {@code additionalVMOpts}
 385      *
 386      * @param optionName name of tested option.
 387      * @param expectedValue expected value of tested option.
 388      * @param optionErrorString message to show if option has another value
 389      * @param additionalVMOpts additional options that should be
 390      *                         passed to JVM.
 391      * @throws Throwable if verification fails or some other issues occur.
 392      */
 393     public static void verifyOptionValueForSameVM(String optionName,
 394             String expectedValue, String optionErrorString,
 395             String... additionalVMOpts) throws Throwable {
 396         List<String> finalOptions = new ArrayList<>();
 397         finalOptions.add(CommandLineOptionTest.getVMTypeOption());
 398         String extraFlagForEmulated = CommandLineOptionTest.getVMTypeOptionForEmulated();
 399         if (extraFlagForEmulated != null) {
 400             finalOptions.add(extraFlagForEmulated);
 401         }
 402         Collections.addAll(finalOptions, additionalVMOpts);
 403 
 404         CommandLineOptionTest.verifyOptionValue(optionName, expectedValue,
 405                 optionErrorString, false,
 406                 finalOptions.toArray(new String[finalOptions.size()]));
 407     }
 408 
 409     /**
 410      * Prepares boolean command line flag with name {@code name} according
 411      * to it's {@code value}.
 412      *
 413      * @param name the name of option to be prepared
 414      * @param value the value of option
 415      * @return prepared command line flag
 416      */
 417     public static String prepareBooleanFlag(String name, boolean value) {
 418         return String.format("-XX:%c%s", (value ? '+' : '-'), name);
 419     }
 420 
 421     /**
 422      * Prepares numeric command line flag with name {@code name} by setting
 423      * it's value to {@code value}.
 424      *
 425      * @param name the name of option to be prepared
 426      * @param value the value of option
 427      * @return prepared command line flag
 428      */
 429     public static String prepareNumericFlag(String name, Number value) {
 430         return String.format("-XX:%s=%s", name, value.toString());
 431     }
 432 
 433     /**
 434      * Prepares generic command line flag with name {@code name} by setting
 435      * it's value to {@code value}.
 436      *
 437      * @param name the name of option to be prepared
 438      * @param value the value of option ("+" or "-" can be used instead of "true" or "false")
 439      * @return prepared command line flag
 440      */
 441     public static String prepareFlag(String name, String value) {
 442         if (value.equals("+") || value.equalsIgnoreCase("true")) {
 443           return "-XX:+" + name;
 444       } else if (value.equals("-") || value.equalsIgnoreCase("false")) {
 445         return "-XX:-" + name;
 446       } else {
 447         return "-XX:" + name + "=" + value;
 448       }
 449     }
 450 
 451     /**
 452      * Returns message that should occur in VM output if option
 453      * {@code optionName} if unrecognized.
 454      *
 455      * @param optionName the name of option for which message should be returned
 456      * @return message saying that option {@code optionName} is unrecognized
 457      */
 458     public static String getUnrecognizedOptionErrorMessage(String optionName) {
 459         return String.format(
 460                 CommandLineOptionTest.UNRECOGNIZED_OPTION_ERROR_FORMAT,
 461                 optionName);
 462     }
 463 
 464     /**
 465      * Returns message that should occur in VM output if option
 466      * {@code optionName} is experimental and
 467      * -XX:+UnlockExperimentalVMOptions was not passed to VM.
 468      *
 469      * @param optionName the name of option for which message should be returned
 470      * @return message saying that option {@code optionName} is experimental
 471      */
 472     public static String getExperimentalOptionErrorMessage(String optionName) {
 473         return String.format(
 474                 CommandLineOptionTest.EXPERIMENTAL_OPTION_ERROR_FORMAT,
 475                 optionName);
 476     }
 477 
 478     /**
 479      * Returns message that should occur in VM output if option
 480      * {@code optionName} is diagnostic and -XX:+UnlockDiagnosticVMOptions
 481      * was not passed to VM.
 482      *
 483      * @param optionName the name of option for which message should be returned
 484      * @return message saying that option {@code optionName} is diganostic
 485      */
 486     public static String getDiagnosticOptionErrorMessage(String optionName) {
 487         return String.format(
 488                 CommandLineOptionTest.DIAGNOSTIC_OPTION_ERROR_FORMAT,
 489                 optionName);
 490     }
 491 
 492     /**
 493      * @return option required to start a new VM with the same type as current.
 494      * @throws RuntimeException when VM type is unknown.
 495      */
 496     private static String getVMTypeOption() {
 497         if (Platform.isServer()) {
 498             return "-server";
 499         } else if (Platform.isClient()) {
 500             return "-client";
 501         } else if (Platform.isMinimal()) {
 502             return "-minimal";
 503         } else if (Platform.isGraal()) {
 504             return "-graal";
 505         }
 506         throw new RuntimeException("Unknown VM mode.");
 507     }
 508 
 509     /**
 510      * @return addtional VMoptions(Emulated related) required to start a new VM with the same type as current.
 511      */
 512     private static String getVMTypeOptionForEmulated() {
 513         if (Platform.isServer() && !Platform.isEmulatedClient()) {
 514             return "-XX:-NeverActAsServerClassMachine";
 515         } else if (Platform.isEmulatedClient()) {
 516             return "-XX:+NeverActAsServerClassMachine";
 517         }
 518         return null;
 519     }
 520 
 521     private final BooleanSupplier predicate;
 522 
 523     /**
 524      * Constructs new CommandLineOptionTest that will be executed only if
 525      * predicate {@code predicate} return {@code true}.
 526      * @param predicate a predicate responsible for test's preconditions check.
 527      */
 528     public CommandLineOptionTest(BooleanSupplier predicate) {
 529         this.predicate = predicate;
 530     }
 531 
 532     /**
 533      * Runs command line option test.
 534      */
 535     public final void test() throws Throwable {
 536         if (predicate.getAsBoolean()) {
 537             runTestCases();
 538         }
 539     }
 540 
 541     /**
 542      * @throws Throwable if some issue happened during test cases execution.
 543      */
 544     protected abstract void runTestCases() throws Throwable;
 545 }