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