langtools/test/tools/javac/lib/combo/ComboTestHelper.java
changeset 32454 b0ac04e0fefe
equal deleted inserted replaced
32453:8eebd1f0b8ea 32454:b0ac04e0fefe
       
     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 
       
    24 package combo;
       
    25 
       
    26 import javax.tools.JavaCompiler;
       
    27 import javax.tools.StandardJavaFileManager;
       
    28 import javax.tools.ToolProvider;
       
    29 
       
    30 import java.io.IOException;
       
    31 import java.util.ArrayList;
       
    32 import java.util.HashMap;
       
    33 import java.util.List;
       
    34 import java.util.Map;
       
    35 import java.util.Optional;
       
    36 import java.util.Stack;
       
    37 import java.util.function.Consumer;
       
    38 import java.util.function.Predicate;
       
    39 import java.util.function.Supplier;
       
    40 
       
    41 
       
    42 /**
       
    43  * An helper class for defining combinatorial (aka "combo" tests). A combo test is made up of one
       
    44  * or more 'dimensions' - each of which represent a different axis of the test space. For instance,
       
    45  * if we wanted to test class/interface declaration, one dimension could be the keyword used for
       
    46  * the declaration (i.e. 'class' vs. 'interface') while another dimension could be the class/interface
       
    47  * modifiers (i.e. 'public', 'pachake-private' etc.). A combo test consists in running a test instance
       
    48  * for each point in the test space; that is, for any combination of the combo test dimension:
       
    49  * <p>
       
    50  * 'public' 'class'
       
    51  * 'public' interface'
       
    52  * 'package-private' 'class'
       
    53  * 'package-private' 'interface'
       
    54  * ...
       
    55  * <p>
       
    56  * A new test instance {@link ComboInstance} is created, and executed, after its dimensions have been
       
    57  * initialized accordingly. Each instance can either pass, fail or throw an unexpected error; this helper
       
    58  * class defines several policies for how failures should be handled during a combo test execution
       
    59  * (i.e. should errors be ignored? Do we want the first failure to result in a failure of the whole
       
    60  * combo test?).
       
    61  * <p>
       
    62  * Additionally, this helper class allows to specify filter methods that can be used to throw out
       
    63  * illegal combinations of dimensions - for instance, in the example above, we might want to exclude
       
    64  * all combinations involving 'protected' and 'private' modifiers, which are disallowed for toplevel
       
    65  * declarations.
       
    66  * <p>
       
    67  * While combo tests can be used for a variety of workloads, typically their main task will consist
       
    68  * in performing some kind of javac compilation. For this purpose, this framework defines an optimized
       
    69  * javac context {@link ReusableContext} which can be shared across multiple combo instances,
       
    70  * when the framework detects it's safe to do so. This allows to reduce the overhead associated with
       
    71  * compiler initialization when the test space is big.
       
    72  */
       
    73 public class ComboTestHelper<X extends ComboInstance<X>> {
       
    74 
       
    75     /** Failure mode. */
       
    76     FailMode failMode = FailMode.FAIL_FAST;
       
    77 
       
    78     /** Ignore mode. */
       
    79     IgnoreMode ignoreMode = IgnoreMode.IGNORE_NONE;
       
    80 
       
    81     /** Combo test instance filter. */
       
    82     Optional<Predicate<X>> optFilter = Optional.empty();
       
    83 
       
    84     /** Combo test dimensions. */
       
    85     List<DimensionInfo<?>> dimensionInfos = new ArrayList<>();
       
    86 
       
    87     /** Combo test stats. */
       
    88     Info info = new Info();
       
    89 
       
    90     /** Shared JavaCompiler used across all combo test instances. */
       
    91     JavaCompiler comp = ToolProvider.getSystemJavaCompiler();
       
    92 
       
    93     /** Shared file manager used across all combo test instances. */
       
    94     StandardJavaFileManager fm = comp.getStandardFileManager(null, null, null);
       
    95 
       
    96     /** Shared context used across all combo instances. */
       
    97     ReusableContext context = new ReusableContext();
       
    98 
       
    99     /**
       
   100      * Set failure mode for this combo test.
       
   101      */
       
   102     public ComboTestHelper<X> withFailMode(FailMode failMode) {
       
   103         this.failMode = failMode;
       
   104         return this;
       
   105     }
       
   106 
       
   107     /**
       
   108      * Set ignore mode for this combo test.
       
   109      */
       
   110     public ComboTestHelper<X> withIgnoreMode(IgnoreMode ignoreMode) {
       
   111         this.ignoreMode = ignoreMode;
       
   112         return this;
       
   113     }
       
   114 
       
   115     /**
       
   116      * Set a filter for combo test instances to be ignored.
       
   117      */
       
   118     public ComboTestHelper<X> withFilter(Predicate<X> filter) {
       
   119         optFilter = Optional.of(optFilter.map(filter::and).orElse(filter));
       
   120         return this;
       
   121     }
       
   122 
       
   123     /**
       
   124      * Adds a new dimension to this combo test, with a given name an array of values.
       
   125      */
       
   126     @SafeVarargs
       
   127     public final <D> ComboTestHelper<X> withDimension(String name, D... dims) {
       
   128         return withDimension(name, null, dims);
       
   129     }
       
   130 
       
   131     /**
       
   132      * Adds a new dimension to this combo test, with a given name, an array of values and a
       
   133      * coresponding setter to be called in order to set the dimension value on the combo test instance
       
   134      * (before test execution).
       
   135      */
       
   136     @SuppressWarnings("unchecked")
       
   137     @SafeVarargs
       
   138     public final <D> ComboTestHelper<X> withDimension(String name, DimensionSetter<X, D> setter, D... dims) {
       
   139         dimensionInfos.add(new DimensionInfo<>(name, dims, setter));
       
   140         return this;
       
   141     }
       
   142 
       
   143     /**
       
   144      * Adds a new array dimension to this combo test, with a given base name. This allows to specify
       
   145      * multiple dimensions at once; the names of the underlying dimensions will be generated from the
       
   146      * base name, using standard array bracket notation - i.e. "DIM[0]", "DIM[1]", etc.
       
   147      */
       
   148     @SafeVarargs
       
   149     public final <D> ComboTestHelper<X> withArrayDimension(String name, int size, D... dims) {
       
   150         return withArrayDimension(name, null, size, dims);
       
   151     }
       
   152 
       
   153     /**
       
   154      * Adds a new array dimension to this combo test, with a given base name, an array of values and a
       
   155      * coresponding array setter to be called in order to set the dimension value on the combo test
       
   156      * instance (before test execution). This allows to specify multiple dimensions at once; the names
       
   157      * of the underlying dimensions will be generated from the base name, using standard array bracket
       
   158      * notation - i.e. "DIM[0]", "DIM[1]", etc.
       
   159      */
       
   160     @SafeVarargs
       
   161     public final <D> ComboTestHelper<X> withArrayDimension(String name, ArrayDimensionSetter<X, D> setter, int size, D... dims) {
       
   162         for (int i = 0 ; i < size ; i++) {
       
   163             dimensionInfos.add(new ArrayDimensionInfo<>(name, dims, i, setter));
       
   164         }
       
   165         return this;
       
   166     }
       
   167 
       
   168     /**
       
   169      * Returns the stat object associated with this combo test.
       
   170      */
       
   171     public Info info() {
       
   172         return info;
       
   173     }
       
   174 
       
   175     /**
       
   176      * Runs this combo test. This will generate the combinatorial explosion of all dimensions, and
       
   177      * execute a new test instance (built using given supplier) for each such combination.
       
   178      */
       
   179     public void run(Supplier<X> instanceBuilder) {
       
   180         run(instanceBuilder, null);
       
   181     }
       
   182 
       
   183     /**
       
   184      * Runs this combo test. This will generate the combinatorial explosion of all dimensions, and
       
   185      * execute a new test instance (built using given supplier) for each such combination. Before
       
   186      * executing the test instance entry point, the supplied initialization method is called on
       
   187      * the test instance; this is useful for ad-hoc test instance initialization once all the dimension
       
   188      * values have been set.
       
   189      */
       
   190     public void run(Supplier<X> instanceBuilder, Consumer<X> initAction) {
       
   191         runInternal(0, new Stack<>(), instanceBuilder, Optional.ofNullable(initAction));
       
   192         end();
       
   193     }
       
   194 
       
   195     /**
       
   196      * Generate combinatorial explosion of all dimension values and create a new test instance
       
   197      * for each combination.
       
   198      */
       
   199     @SuppressWarnings({"unchecked", "rawtypes"})
       
   200     private void runInternal(int index, Stack<DimensionBinding<?>> bindings, Supplier<X> instanceBuilder, Optional<Consumer<X>> initAction) {
       
   201         if (index == dimensionInfos.size()) {
       
   202             runCombo(instanceBuilder, initAction, bindings);
       
   203         } else {
       
   204             DimensionInfo<?> dinfo = dimensionInfos.get(index);
       
   205             for (Object d : dinfo.dims) {
       
   206                 bindings.push(new DimensionBinding(d, dinfo));
       
   207                 runInternal(index + 1, bindings, instanceBuilder, initAction);
       
   208                 bindings.pop();
       
   209             }
       
   210         }
       
   211     }
       
   212 
       
   213     /**
       
   214      * Run a new test instance using supplied dimension bindings. All required setters and initialization
       
   215      * method are executed before calling the instance main entry point. Also checks if the instance
       
   216      * is compatible with the specified test filters; if not, the test is simply skipped.
       
   217      */
       
   218     @SuppressWarnings("unchecked")
       
   219     private void runCombo(Supplier<X> instanceBuilder, Optional<Consumer<X>> initAction, List<DimensionBinding<?>> bindings) {
       
   220         X x = instanceBuilder.get();
       
   221         for (DimensionBinding<?> binding : bindings) {
       
   222             binding.init(x);
       
   223         }
       
   224         initAction.ifPresent(action -> action.accept(x));
       
   225         info.comboCount++;
       
   226         if (!optFilter.isPresent() || optFilter.get().test(x)) {
       
   227             x.run(new Env(bindings));
       
   228             if (failMode.shouldStop(ignoreMode, info)) {
       
   229                 end();
       
   230             }
       
   231         } else {
       
   232             info.skippedCount++;
       
   233         }
       
   234     }
       
   235 
       
   236     /**
       
   237      * This method is executed upon combo test completion (either normal or erroneous). Closes down
       
   238      * all pending resources and dumps useful stats info.
       
   239      */
       
   240     private void end() {
       
   241         try {
       
   242             fm.close();
       
   243             if (info.hasFailures()) {
       
   244                 throw new AssertionError("Failure when executing combo:" + info.lastFailure.orElse(""));
       
   245             } else if (info.hasErrors()) {
       
   246                 throw new AssertionError("Unexpected exception while executing combo", info.lastError.get());
       
   247             }
       
   248         } catch (IOException ex) {
       
   249             throw new AssertionError("Failure when closing down shared file manager; ", ex);
       
   250         } finally {
       
   251             info.dump();
       
   252         }
       
   253     }
       
   254 
       
   255     /**
       
   256      * Functional interface for specifying combo test instance setters.
       
   257      */
       
   258     public interface DimensionSetter<X extends ComboInstance<X>, D> {
       
   259         void set(X x, D d);
       
   260     }
       
   261 
       
   262     /**
       
   263      * Functional interface for specifying combo test instance array setters. The setter method
       
   264      * receives an extra argument for the index of the array element to be set.
       
   265      */
       
   266     public interface ArrayDimensionSetter<X extends ComboInstance<X>, D> {
       
   267         void set(X x, D d, int index);
       
   268     }
       
   269 
       
   270     /**
       
   271      * Dimension descriptor; each dimension has a name, an array of value and an optional setter
       
   272      * to be called on the associated combo test instance.
       
   273      */
       
   274     class DimensionInfo<D> {
       
   275         String name;
       
   276         D[] dims;
       
   277         boolean isParameter;
       
   278         Optional<DimensionSetter<X, D>> optSetter;
       
   279 
       
   280         DimensionInfo(String name, D[] dims, DimensionSetter<X, D> setter) {
       
   281             this.name = name;
       
   282             this.dims = dims;
       
   283             this.optSetter = Optional.ofNullable(setter);
       
   284             this.isParameter = dims[0] instanceof ComboParameter;
       
   285         }
       
   286     }
       
   287 
       
   288     /**
       
   289      * Array dimension descriptor. The dimension name is derived from a base name and an index using
       
   290      * standard bracket notation; ; the setter accepts an additional 'index' argument to point
       
   291      * to the array element to be initialized.
       
   292      */
       
   293     class ArrayDimensionInfo<D> extends DimensionInfo<D> {
       
   294         public ArrayDimensionInfo(String name, D[] dims, int index, ArrayDimensionSetter<X, D> setter) {
       
   295             super(String.format("%s[%d]", name, index), dims,
       
   296                     setter != null ? (x, d) -> setter.set(x, d, index) : null);
       
   297         }
       
   298     }
       
   299 
       
   300     /**
       
   301      * Failure policies for a combo test run.
       
   302      */
       
   303     public enum FailMode {
       
   304         /** Combo test fails when first failure is detected. */
       
   305         FAIL_FAST,
       
   306         /** Combo test fails after all instances have been executed. */
       
   307         FAIL_AFTER;
       
   308 
       
   309         boolean shouldStop(IgnoreMode ignoreMode, Info info) {
       
   310             switch (this) {
       
   311                 case FAIL_FAST:
       
   312                     return !ignoreMode.canIgnore(info);
       
   313                 default:
       
   314                     return false;
       
   315             }
       
   316         }
       
   317     }
       
   318 
       
   319     /**
       
   320      * Ignore policies for a combo test run.
       
   321      */
       
   322     public enum IgnoreMode {
       
   323         /** No error or failure is ignored. */
       
   324         IGNORE_NONE,
       
   325         /** Only errors are ignored. */
       
   326         IGNORE_ERRORS,
       
   327         /** Only failures are ignored. */
       
   328         IGNORE_FAILURES,
       
   329         /** Both errors and failures are ignored. */
       
   330         IGNORE_ALL;
       
   331 
       
   332         boolean canIgnore(Info info) {
       
   333             switch (this) {
       
   334                 case IGNORE_ERRORS:
       
   335                     return info.failCount == 0;
       
   336                 case IGNORE_FAILURES:
       
   337                     return info.errCount == 0;
       
   338                 case IGNORE_ALL:
       
   339                     return true;
       
   340                 default:
       
   341                     return info.failCount == 0 && info.errCount == 0;
       
   342             }
       
   343         }
       
   344     }
       
   345 
       
   346     /**
       
   347      * A dimension binding. This is essentially a pair of a dimension value and its corresponding
       
   348      * dimension info.
       
   349      */
       
   350     class DimensionBinding<D> {
       
   351         D d;
       
   352         DimensionInfo<D> info;
       
   353 
       
   354         DimensionBinding(D d, DimensionInfo<D> info) {
       
   355             this.d = d;
       
   356             this.info = info;
       
   357         }
       
   358 
       
   359         void init(X x) {
       
   360             info.optSetter.ifPresent(setter -> setter.set(x, d));
       
   361         }
       
   362 
       
   363         public String toString() {
       
   364             return String.format("(%s -> %s)", info.name, d);
       
   365         }
       
   366     }
       
   367 
       
   368     /**
       
   369      * This class is used to keep track of combo tests stats; info such as numbero of failures/errors,
       
   370      * number of times a context has been shared/dropped are all recorder here.
       
   371      */
       
   372     public static class Info {
       
   373         int failCount;
       
   374         int errCount;
       
   375         int passCount;
       
   376         int comboCount;
       
   377         int skippedCount;
       
   378         int ctxReusedCount;
       
   379         int ctxDroppedCount;
       
   380         Optional<String> lastFailure = Optional.empty();
       
   381         Optional<Throwable> lastError = Optional.empty();
       
   382 
       
   383         void dump() {
       
   384             System.err.println(String.format("%d total checks executed", comboCount));
       
   385             System.err.println(String.format("%d successes found", passCount));
       
   386             System.err.println(String.format("%d failures found", failCount));
       
   387             System.err.println(String.format("%d errors found", errCount));
       
   388             System.err.println(String.format("%d skips found", skippedCount));
       
   389             System.err.println(String.format("%d contexts shared", ctxReusedCount));
       
   390             System.err.println(String.format("%d contexts dropped", ctxDroppedCount));
       
   391         }
       
   392 
       
   393         public boolean hasFailures() {
       
   394             return failCount != 0;
       
   395         }
       
   396 
       
   397         public boolean hasErrors() {
       
   398             return errCount != 0;
       
   399         }
       
   400     }
       
   401 
       
   402     /**
       
   403      * THe execution environment for a given combo test instance. An environment contains the
       
   404      * bindings for all the dimensions, along with the combo parameter cache (this is non-empty
       
   405      * only if one or more dimensions are subclasses of the {@code ComboParameter} interface).
       
   406      */
       
   407     class Env {
       
   408         List<DimensionBinding<?>> bindings;
       
   409         Map<String, ComboParameter> parametersCache = new HashMap<>();
       
   410 
       
   411         @SuppressWarnings({"Unchecked", "rawtypes"})
       
   412         Env(List<DimensionBinding<?>> bindings) {
       
   413             this.bindings = bindings;
       
   414             for (DimensionBinding<?> binding : bindings) {
       
   415                 if (binding.info.isParameter) {
       
   416                     parametersCache.put(binding.info.name, (ComboParameter)binding.d);
       
   417                 };
       
   418             }
       
   419         }
       
   420 
       
   421         Info info() {
       
   422             return ComboTestHelper.this.info();
       
   423         }
       
   424 
       
   425         StandardJavaFileManager fileManager() {
       
   426             return fm;
       
   427         }
       
   428 
       
   429         JavaCompiler javaCompiler() {
       
   430             return comp;
       
   431         }
       
   432 
       
   433         ReusableContext context() {
       
   434             return context;
       
   435         }
       
   436 
       
   437         ReusableContext setContext(ReusableContext context) {
       
   438             return ComboTestHelper.this.context = context;
       
   439         }
       
   440     }
       
   441 }
       
   442 
       
   443 
       
   444