/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package combo; import javax.tools.JavaCompiler; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Stack; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; /** * An helper class for defining combinatorial (aka "combo" tests). A combo test is made up of one * or more 'dimensions' - each of which represent a different axis of the test space. For instance, * if we wanted to test class/interface declaration, one dimension could be the keyword used for * the declaration (i.e. 'class' vs. 'interface') while another dimension could be the class/interface * modifiers (i.e. 'public', 'pachake-private' etc.). A combo test consists in running a test instance * for each point in the test space; that is, for any combination of the combo test dimension: * <p> * 'public' 'class' * 'public' interface' * 'package-private' 'class' * 'package-private' 'interface' * ... * <p> * A new test instance {@link ComboInstance} is created, and executed, after its dimensions have been * initialized accordingly. Each instance can either pass, fail or throw an unexpected error; this helper * class defines several policies for how failures should be handled during a combo test execution * (i.e. should errors be ignored? Do we want the first failure to result in a failure of the whole * combo test?). * <p> * Additionally, this helper class allows to specify filter methods that can be used to throw out * illegal combinations of dimensions - for instance, in the example above, we might want to exclude * all combinations involving 'protected' and 'private' modifiers, which are disallowed for toplevel * declarations. * <p> * While combo tests can be used for a variety of workloads, typically their main task will consist * in performing some kind of javac compilation. For this purpose, this framework defines an optimized * javac context {@link ReusableContext} which can be shared across multiple combo instances, * when the framework detects it's safe to do so. This allows to reduce the overhead associated with * compiler initialization when the test space is big. */ public class ComboTestHelper<X extends ComboInstance<X>> { /** Failure mode. */ FailMode failMode = FailMode.FAIL_FAST; /** Ignore mode. */ IgnoreMode ignoreMode = IgnoreMode.IGNORE_NONE; /** Combo test instance filter. */ Optional<Predicate<X>> optFilter = Optional.empty(); /** Combo test dimensions. */ List<DimensionInfo<?>> dimensionInfos = new ArrayList<>(); /** Combo test stats. */ Info info = new Info(); /** Shared JavaCompiler used across all combo test instances. */ JavaCompiler comp = ToolProvider.getSystemJavaCompiler(); /** Shared file manager used across all combo test instances. */ StandardJavaFileManager fm = comp.getStandardFileManager(null, null, null); /** Shared context used across all combo instances. */ ReusableContext context = new ReusableContext(); /** * Set failure mode for this combo test. */ public ComboTestHelper<X> withFailMode(FailMode failMode) { this.failMode = failMode; return this; } /** * Set ignore mode for this combo test. */ public ComboTestHelper<X> withIgnoreMode(IgnoreMode ignoreMode) { this.ignoreMode = ignoreMode; return this; } /** * Set a filter for combo test instances to be ignored. */ public ComboTestHelper<X> withFilter(Predicate<X> filter) { optFilter = Optional.of(optFilter.map(filter::and).orElse(filter)); return this; } /** * Adds a new dimension to this combo test, with a given name an array of values. */ @SafeVarargs public final <D> ComboTestHelper<X> withDimension(String name, D... dims) { return withDimension(name, null, dims); } /** * Adds a new dimension to this combo test, with a given name, an array of values and a * coresponding setter to be called in order to set the dimension value on the combo test instance * (before test execution). */ @SuppressWarnings("unchecked") @SafeVarargs public final <D> ComboTestHelper<X> withDimension(String name, DimensionSetter<X, D> setter, D... dims) { dimensionInfos.add(new DimensionInfo<>(name, dims, setter)); return this; } /** * Adds a new array dimension to this combo test, with a given base name. This allows to specify * multiple dimensions at once; the names of the underlying dimensions will be generated from the * base name, using standard array bracket notation - i.e. "DIM[0]", "DIM[1]", etc. */ @SafeVarargs public final <D> ComboTestHelper<X> withArrayDimension(String name, int size, D... dims) { return withArrayDimension(name, null, size, dims); } /** * Adds a new array dimension to this combo test, with a given base name, an array of values and a * coresponding array setter to be called in order to set the dimension value on the combo test * instance (before test execution). This allows to specify multiple dimensions at once; the names * of the underlying dimensions will be generated from the base name, using standard array bracket * notation - i.e. "DIM[0]", "DIM[1]", etc. */ @SafeVarargs public final <D> ComboTestHelper<X> withArrayDimension(String name, ArrayDimensionSetter<X, D> setter, int size, D... dims) { for (int i = 0 ; i < size ; i++) { dimensionInfos.add(new ArrayDimensionInfo<>(name, dims, i, setter)); } return this; } /** * Returns the stat object associated with this combo test. */ public Info info() { return info; } /** * Runs this combo test. This will generate the combinatorial explosion of all dimensions, and * execute a new test instance (built using given supplier) for each such combination. */ public void run(Supplier<X> instanceBuilder) { run(instanceBuilder, null); } /** * Runs this combo test. This will generate the combinatorial explosion of all dimensions, and * execute a new test instance (built using given supplier) for each such combination. Before * executing the test instance entry point, the supplied initialization method is called on * the test instance; this is useful for ad-hoc test instance initialization once all the dimension * values have been set. */ public void run(Supplier<X> instanceBuilder, Consumer<X> initAction) { runInternal(0, new Stack<>(), instanceBuilder, Optional.ofNullable(initAction)); end(); } /** * Generate combinatorial explosion of all dimension values and create a new test instance * for each combination. */ @SuppressWarnings({"unchecked", "rawtypes"}) private void runInternal(int index, Stack<DimensionBinding<?>> bindings, Supplier<X> instanceBuilder, Optional<Consumer<X>> initAction) { if (index == dimensionInfos.size()) { runCombo(instanceBuilder, initAction, bindings); } else { DimensionInfo<?> dinfo = dimensionInfos.get(index); for (Object d : dinfo.dims) { bindings.push(new DimensionBinding(d, dinfo)); runInternal(index + 1, bindings, instanceBuilder, initAction); bindings.pop(); } } } /** * Run a new test instance using supplied dimension bindings. All required setters and initialization * method are executed before calling the instance main entry point. Also checks if the instance * is compatible with the specified test filters; if not, the test is simply skipped. */ @SuppressWarnings("unchecked") private void runCombo(Supplier<X> instanceBuilder, Optional<Consumer<X>> initAction, List<DimensionBinding<?>> bindings) { X x = instanceBuilder.get(); for (DimensionBinding<?> binding : bindings) { binding.init(x); } initAction.ifPresent(action -> action.accept(x)); info.comboCount++; if (!optFilter.isPresent() || optFilter.get().test(x)) { x.run(new Env(bindings)); if (failMode.shouldStop(ignoreMode, info)) { end(); } } else { info.skippedCount++; } } /** * This method is executed upon combo test completion (either normal or erroneous). Closes down * all pending resources and dumps useful stats info. */ private void end() { try { fm.close(); if (info.hasFailures()) { throw new AssertionError("Failure when executing combo:" + info.lastFailure.orElse("")); } else if (info.hasErrors()) { throw new AssertionError("Unexpected exception while executing combo", info.lastError.get()); } } catch (IOException ex) { throw new AssertionError("Failure when closing down shared file manager; ", ex); } finally { info.dump(); } } /** * Functional interface for specifying combo test instance setters. */ public interface DimensionSetter<X extends ComboInstance<X>, D> { void set(X x, D d); } /** * Functional interface for specifying combo test instance array setters. The setter method * receives an extra argument for the index of the array element to be set. */ public interface ArrayDimensionSetter<X extends ComboInstance<X>, D> { void set(X x, D d, int index); } /** * Dimension descriptor; each dimension has a name, an array of value and an optional setter * to be called on the associated combo test instance. */ class DimensionInfo<D> { String name; D[] dims; boolean isParameter; Optional<DimensionSetter<X, D>> optSetter; DimensionInfo(String name, D[] dims, DimensionSetter<X, D> setter) { this.name = name; this.dims = dims; this.optSetter = Optional.ofNullable(setter); this.isParameter = dims[0] instanceof ComboParameter; } } /** * Array dimension descriptor. The dimension name is derived from a base name and an index using * standard bracket notation; ; the setter accepts an additional 'index' argument to point * to the array element to be initialized. */ class ArrayDimensionInfo<D> extends DimensionInfo<D> { public ArrayDimensionInfo(String name, D[] dims, int index, ArrayDimensionSetter<X, D> setter) { super(String.format("%s[%d]", name, index), dims, setter != null ? (x, d) -> setter.set(x, d, index) : null); } } /** * Failure policies for a combo test run. */ public enum FailMode { /** Combo test fails when first failure is detected. */ FAIL_FAST, /** Combo test fails after all instances have been executed. */ FAIL_AFTER; boolean shouldStop(IgnoreMode ignoreMode, Info info) { switch (this) { case FAIL_FAST: return !ignoreMode.canIgnore(info); default: return false; } } } /** * Ignore policies for a combo test run. */ public enum IgnoreMode { /** No error or failure is ignored. */ IGNORE_NONE, /** Only errors are ignored. */ IGNORE_ERRORS, /** Only failures are ignored. */ IGNORE_FAILURES, /** Both errors and failures are ignored. */ IGNORE_ALL; boolean canIgnore(Info info) { switch (this) { case IGNORE_ERRORS: return info.failCount == 0; case IGNORE_FAILURES: return info.errCount == 0; case IGNORE_ALL: return true; default: return info.failCount == 0 && info.errCount == 0; } } } /** * A dimension binding. This is essentially a pair of a dimension value and its corresponding * dimension info. */ class DimensionBinding<D> { D d; DimensionInfo<D> info; DimensionBinding(D d, DimensionInfo<D> info) { this.d = d; this.info = info; } void init(X x) { info.optSetter.ifPresent(setter -> setter.set(x, d)); } public String toString() { return String.format("(%s -> %s)", info.name, d); } } /** * This class is used to keep track of combo tests stats; info such as numbero of failures/errors, * number of times a context has been shared/dropped are all recorder here. */ public static class Info { int failCount; int errCount; int passCount; int comboCount; int skippedCount; int ctxReusedCount; int ctxDroppedCount; Optional<String> lastFailure = Optional.empty(); Optional<Throwable> lastError = Optional.empty(); void dump() { System.err.println(String.format("%d total checks executed", comboCount)); System.err.println(String.format("%d successes found", passCount)); System.err.println(String.format("%d failures found", failCount)); System.err.println(String.format("%d errors found", errCount)); System.err.println(String.format("%d skips found", skippedCount)); System.err.println(String.format("%d contexts shared", ctxReusedCount)); System.err.println(String.format("%d contexts dropped", ctxDroppedCount)); } public boolean hasFailures() { return failCount != 0; } public boolean hasErrors() { return errCount != 0; } } /** * THe execution environment for a given combo test instance. An environment contains the * bindings for all the dimensions, along with the combo parameter cache (this is non-empty * only if one or more dimensions are subclasses of the {@code ComboParameter} interface). */ class Env { List<DimensionBinding<?>> bindings; Map<String, ComboParameter> parametersCache = new HashMap<>(); @SuppressWarnings({"Unchecked", "rawtypes"}) Env(List<DimensionBinding<?>> bindings) { this.bindings = bindings; for (DimensionBinding<?> binding : bindings) { if (binding.info.isParameter) { parametersCache.put(binding.info.name, (ComboParameter)binding.d); }; } } Info info() { return ComboTestHelper.this.info(); } StandardJavaFileManager fileManager() { return fm; } JavaCompiler javaCompiler() { return comp; } ReusableContext context() { return context; } ReusableContext setContext(ReusableContext context) { return ComboTestHelper.this.context = context; } } }