1 /*
   2  * Copyright (c) 2015, 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 optionsvalidation;
  24 
  25 import com.sun.tools.attach.VirtualMachine;
  26 import com.sun.tools.attach.AttachOperationFailedException;
  27 import java.util.ArrayList;
  28 import java.util.Arrays;
  29 import java.util.HashSet;
  30 import java.util.List;
  31 import java.util.Set;
  32 import jdk.test.lib.DynamicVMOption;
  33 import jdk.test.lib.OutputAnalyzer;
  34 import jdk.test.lib.ProcessTools;
  35 import jdk.test.lib.dcmd.CommandExecutor;
  36 import jdk.test.lib.dcmd.JMXExecutor;
  37 import sun.tools.attach.HotSpotVirtualMachine;
  38 
  39 import static optionsvalidation.JVMOptionsUtils.failedMessage;
  40 import static optionsvalidation.JVMOptionsUtils.printOutputContent;
  41 import static optionsvalidation.JVMOptionsUtils.VMType;
  42 
  43 public abstract class JVMOption {
  44 
  45     /**
  46      * Executor for JCMD
  47      */
  48     private final static CommandExecutor executor = new JMXExecutor();
  49 
  50     /**
  51      * Name of the tested parameter
  52      */
  53     protected String name;
  54 
  55     /**
  56      * Range is defined for option inside VM
  57      */
  58     protected boolean withRange;
  59 
  60     /**
  61      * Test valid min range value and additional small values
  62      */
  63     protected boolean testMinRange;
  64 
  65     /**
  66      * Test valid max range value and additional big values
  67      */
  68     protected boolean testMaxRange;
  69 
  70     private Set<Integer> allowedExitCodes;
  71 
  72     /**
  73      * Prepend string which added before testing option to the command line
  74      */
  75     private final List<String> prepend;
  76     private final StringBuilder prependString;
  77 
  78     protected JVMOption() {
  79         this.prepend = new ArrayList<>();
  80         prependString = new StringBuilder();
  81         allowedExitCodes = new HashSet<>();
  82         allowedExitCodes.add(0);
  83         allowedExitCodes.add(1);
  84         withRange = false;
  85         testMinRange = true;
  86         testMaxRange = true;
  87     }
  88 
  89     /**
  90      * Create JVM Option with given type and name.
  91      *
  92      * @param type type: "intx", "size_t", "uintx", "uint64_t" or "double"
  93      * @param name name of the option
  94      * @return created JVMOption
  95      */
  96     static JVMOption createVMOption(String type, String name) {
  97         JVMOption parameter;
  98 
  99         switch (type) {
 100             case "int":
 101             case "intx":
 102             case "size_t":
 103             case "uint":
 104             case "uintx":
 105             case "uint64_t":
 106                 parameter = new IntJVMOption(name, type);
 107                 break;
 108             case "double":
 109                 parameter = new DoubleJVMOption(name);
 110                 break;
 111             default:
 112                 throw new Error("Expected only \"int\", \"intx\", \"size_t\", "
 113                         + "\"uint\", \"uintx\", \"uint64_t\", or \"double\" "
 114                         + "option types! Got " + type + " type!");
 115         }
 116 
 117         return parameter;
 118     }
 119 
 120     /**
 121      * Add passed options to the prepend options of the option. Prepend options
 122      * will be added before testing option to the command line.
 123      *
 124      * @param options array of prepend options
 125      */
 126     public final void addPrepend(String... options) {
 127         String toAdd;
 128 
 129         for (String option : options) {
 130             if (option.startsWith("-")) {
 131                 toAdd = option;
 132             } else {
 133                 /* Add "-" before parameter name */
 134                 toAdd = "-" + option;
 135 
 136             }
 137             prepend.add(toAdd);
 138             prependString.append(toAdd).append(" ");
 139         }
 140     }
 141 
 142     /**
 143      * Get name of the option
 144      *
 145      * @return name of the option
 146      */
 147     final String getName() {
 148         return name;
 149     }
 150 
 151     /**
 152      * Mark this option as option which range is defined inside VM
 153      */
 154     final void optionWithRange() {
 155         withRange = true;
 156     }
 157 
 158     /**
 159      * Exclude testing of min range value for this option
 160      */
 161     public final void excludeTestMinRange() {
 162         testMinRange = false;
 163     }
 164 
 165     /**
 166      * Exclude testing of max range value for this option
 167      */
 168     public final void excludeTestMaxRange() {
 169         testMaxRange = false;
 170     }
 171 
 172     public final void setAllowedExitCodes(Integer... allowedExitCodes) {
 173         this.allowedExitCodes.addAll(Arrays.asList(allowedExitCodes));
 174     }
 175 
 176     /**
 177      * Set new minimum option value
 178      *
 179      * @param min new minimum value
 180      */
 181     abstract void setMin(String min);
 182 
 183     /**
 184      * Get string with minimum value of the option
 185      *
 186      * @return string with minimum value of the option
 187      */
 188     abstract String getMin();
 189 
 190     /**
 191      * Set new maximum option value
 192      *
 193      * @param max new maximum value
 194      */
 195     abstract void setMax(String min);
 196 
 197     /**
 198      * Get string with maximum value of the option
 199      *
 200      * @return string with maximum value of the option
 201      */
 202     abstract String getMax();
 203 
 204     /**
 205      * Return list of strings with valid option values which used for testing
 206      * using jcmd, attach and etc.
 207      *
 208      * @return list of strings which contain valid values for option
 209      */
 210     protected abstract List<String> getValidValues();
 211 
 212     /**
 213      * Return list of strings with invalid option values which used for testing
 214      * using jcmd, attach and etc.
 215      *
 216      * @return list of strings which contain invalid values for option
 217      */
 218     protected abstract List<String> getInvalidValues();
 219 
 220     /**
 221      * Return expected error message for option with value "value" when it used
 222      * on command line with passed value
 223      *
 224      * @param value option value
 225      * @return expected error message
 226      */
 227     protected abstract String getErrorMessageCommandLine(String value);
 228 
 229     /**
 230      * Testing writeable option using DynamicVMOption isValidValue and
 231      * isInvalidValue methods
 232      *
 233      * @return number of failed tests
 234      */
 235     public int testDynamic() {
 236         DynamicVMOption option = new DynamicVMOption(name);
 237         int failedTests = 0;
 238         String origValue;
 239 
 240         if (option.isWriteable()) {
 241 
 242             System.out.println("Testing " + name + " option dynamically by DynamicVMOption");
 243 
 244             origValue = option.getValue();
 245 
 246             for (String value : getValidValues()) {
 247                 if (!option.isValidValue(value)) {
 248                     failedMessage(String.format("Option %s: Valid value \"%s\" is invalid", name, value));
 249                     failedTests++;
 250                 }
 251             }
 252 
 253             for (String value : getInvalidValues()) {
 254                 if (option.isValidValue(value)) {
 255                     failedMessage(String.format("Option %s: Invalid value \"%s\" is valid", name, value));
 256                     failedTests++;
 257                 }
 258             }
 259 
 260             option.setValue(origValue);
 261         }
 262 
 263         return failedTests;
 264     }
 265 
 266     /**
 267      * Testing writeable option using Jcmd
 268      *
 269      * @return number of failed tests
 270      */
 271     public int testJcmd() {
 272         DynamicVMOption option = new DynamicVMOption(name);
 273         int failedTests = 0;
 274         OutputAnalyzer out;
 275         String origValue;
 276 
 277         if (option.isWriteable()) {
 278 
 279             System.out.println("Testing " + name + " option dynamically by jcmd");
 280 
 281             origValue = option.getValue();
 282 
 283             for (String value : getValidValues()) {
 284                 out = executor.execute(String.format("VM.set_flag %s %s", name, value), true);
 285 
 286                 if (out.getOutput().contains(name + " error")) {
 287                     failedMessage(String.format("Option %s: Can not change "
 288                             + "option to valid value \"%s\" via jcmd", name, value));
 289                     printOutputContent(out);
 290                     failedTests++;
 291                 }
 292             }
 293 
 294             for (String value : getInvalidValues()) {
 295                 out = executor.execute(String.format("VM.set_flag %s %s", name, value), true);
 296 
 297                 if (!out.getOutput().contains(name + " error")) {
 298                     failedMessage(String.format("Option %s: Error not reported for "
 299                             + "option when it chagned to invalid value \"%s\" via jcmd", name, value));
 300                     printOutputContent(out);
 301                     failedTests++;
 302                 }
 303             }
 304 
 305             option.setValue(origValue);
 306         }
 307 
 308         return failedTests;
 309     }
 310 
 311     private boolean setFlagAttach(HotSpotVirtualMachine vm, String flagName, String flagValue) throws Exception {
 312         boolean result;
 313 
 314         try {
 315             vm.setFlag(flagName, flagValue);
 316             result = true;
 317         } catch (AttachOperationFailedException e) {
 318             result = false;
 319         }
 320 
 321         return result;
 322     }
 323 
 324     /**
 325      * Testing writeable option using attach method
 326      *
 327      * @return number of failed tests
 328      * @throws Exception if an error occurred while attaching to the target JVM
 329      */
 330     public int testAttach() throws Exception {
 331         DynamicVMOption option = new DynamicVMOption(name);
 332         int failedTests = 0;
 333         String origValue;
 334 
 335         if (option.isWriteable()) {
 336 
 337             System.out.println("Testing " + name + " option dynamically via attach");
 338 
 339             origValue = option.getValue();
 340 
 341             HotSpotVirtualMachine vm = (HotSpotVirtualMachine) VirtualMachine.attach(String.valueOf(ProcessTools.getProcessId()));
 342 
 343             for (String value : getValidValues()) {
 344                 if (!setFlagAttach(vm, name, value)) {
 345                     failedMessage(String.format("Option %s: Can not change option to valid value \"%s\" via attach", name, value));
 346                     failedTests++;
 347                 }
 348             }
 349 
 350             for (String value : getInvalidValues()) {
 351                 if (setFlagAttach(vm, name, value)) {
 352                     failedMessage(String.format("Option %s: Option changed to invalid value \"%s\" via attach", name, value));
 353                     failedTests++;
 354                 }
 355             }
 356 
 357             vm.detach();
 358 
 359             option.setValue(origValue);
 360         }
 361 
 362         return failedTests;
 363     }
 364 
 365     /**
 366      * Run java with passed parameter and check the result depending on the
 367      * 'valid' parameter
 368      *
 369      * @param param tested parameter passed to the JVM
 370      * @param valid indicates whether the JVM should fail or not
 371      * @return true - if test passed
 372      * @throws Exception if java process can not be started
 373      */
 374     private boolean runJavaWithParam(String optionValue, boolean valid) throws Exception {
 375         int exitCode;
 376         boolean result = true;
 377         String value = optionValue.substring(optionValue.lastIndexOf("=") + 1);
 378         String fullOptionString = prependString.toString() + optionValue;
 379         List<String> runJava = new ArrayList<>();
 380         OutputAnalyzer out;
 381 
 382         if (VMType != null) {
 383             runJava.add(VMType);
 384         }
 385         runJava.addAll(prepend);
 386         runJava.add(optionValue);
 387         runJava.add(JVMOptionsUtils.class.getName());
 388 
 389         out = new OutputAnalyzer(ProcessTools.createJavaProcessBuilder(runJava.toArray(new String[0])).start());
 390 
 391         exitCode = out.getExitValue();
 392 
 393         if (out.getOutput().contains("A fatal error has been detected by the Java Runtime Environment")) {
 394             /* Always consider "fatal error" in output as fail */
 395             failedMessage(name, fullOptionString, valid, "JVM output reports a fatal error. JVM exited with code " + exitCode + "!");
 396             printOutputContent(out);
 397             result = false;
 398         } else if (valid == true) {
 399             if (!allowedExitCodes.contains(exitCode)) {
 400                 failedMessage(name, fullOptionString, valid, "JVM exited with unexpected error code = " + exitCode);
 401                 printOutputContent(out);
 402                 result = false;
 403             } else if ((exitCode != 0) && (out.getOutput().isEmpty() == true)) {
 404                 failedMessage(name, fullOptionString, valid, "JVM exited with error(exitcode == " + exitCode +
 405                         "), but with empty stdout and stderr. Description of error is needed!");
 406                 result = false;
 407             } else if (out.getOutput().contains("is outside the allowed range")) {
 408                 failedMessage(name, fullOptionString, valid, "JVM output contains \"is outside the allowed range\"");
 409                 printOutputContent(out);
 410                 result = false;
 411             }
 412         } else {
 413             // valid == false
 414             if (exitCode == 0) {
 415                 failedMessage(name, fullOptionString, valid, "JVM successfully exit");
 416                 result = false;
 417             } else if (exitCode != 1) {
 418                 failedMessage(name, fullOptionString, valid, "JVM exited with code "
 419                         + exitCode + " which not equal to 1");
 420                 result = false;
 421             } else if (!out.getOutput().contains(getErrorMessageCommandLine(value))) {
 422                 failedMessage(name, fullOptionString, valid, "JVM output does not contain "
 423                         + "expected output \"" + getErrorMessageCommandLine(value) + "\"");
 424                 printOutputContent(out);
 425                 result = false;
 426             }
 427         }
 428 
 429         System.out.println("");
 430 
 431         return result;
 432     }
 433 
 434     /**
 435      * Construct option string with passed value
 436      *
 437      * @param value parameter value
 438      * @return string containing option with passed value
 439      */
 440     private String constructOption(String value) {
 441         return "-XX:" + name + "=" + value;
 442     }
 443 
 444     /**
 445      * Return list of strings which contain options with valid values which can
 446      * be used for testing on command line
 447      *
 448      * @return list of strings which contain options with valid values
 449      */
 450     private List<String> getValidCommandLineOptions() {
 451         List<String> validParameters = new ArrayList<>();
 452 
 453         for (String value : getValidValues()) {
 454             validParameters.add(constructOption(value));
 455         }
 456 
 457         return validParameters;
 458     }
 459 
 460     /**
 461      * Return list of strings which contain options with invalid values which
 462      * can be used for testing on command line
 463      *
 464      * @return list of strings which contain options with invalid values
 465      */
 466     private List<String> getInvalidCommandLineOptions() {
 467         List<String> invalidParameters = new ArrayList<>();
 468 
 469         for (String value : getInvalidValues()) {
 470             invalidParameters.add(constructOption(value));
 471         }
 472 
 473         return invalidParameters;
 474     }
 475 
 476     /**
 477      * Perform test of the parameter. Call java with valid option values and
 478      * with invalid option values.
 479      *
 480      * @return number of failed tests
 481      * @throws Exception if java process can not be started
 482      */
 483     public int testCommandLine() throws Exception {
 484         ProcessBuilder pb;
 485         int failed = 0;
 486         List<String> optionValuesList;
 487 
 488         optionValuesList = getValidCommandLineOptions();
 489 
 490         if (optionValuesList.isEmpty() != true) {
 491             System.out.println("Testing valid " + name + " values.");
 492             for (String optionValid : optionValuesList) {
 493                 if (runJavaWithParam(optionValid, true) == false) {
 494                     failed++;
 495                 }
 496             }
 497         }
 498 
 499         optionValuesList = getInvalidCommandLineOptions();
 500 
 501         if (optionValuesList.isEmpty() != true) {
 502             System.out.println("Testing invalid " + name + " values.");
 503 
 504             for (String optionInvalid : optionValuesList) {
 505                 if (runJavaWithParam(optionInvalid, false) == false) {
 506                     failed++;
 507                 }
 508             }
 509         }
 510 
 511         /* return number of failed tests for this option */
 512         return failed;
 513     }
 514 
 515 }