ComboTestHelper.java revision 3019:176472b94f2e
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 24package combo; 25 26import javax.tools.JavaCompiler; 27import javax.tools.StandardJavaFileManager; 28import javax.tools.ToolProvider; 29 30import java.io.IOException; 31import java.util.ArrayList; 32import java.util.HashMap; 33import java.util.List; 34import java.util.Map; 35import java.util.Optional; 36import java.util.Stack; 37import java.util.function.Consumer; 38import java.util.function.Predicate; 39import 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 */ 73public 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 445