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