001/* 002 * (C) Copyright 2006-2020 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * bstefanescu 018 * Kevin Leturc <kleturc@nuxeo.com> 019 */ 020package org.nuxeo.runtime.test.runner; 021 022import static org.nuxeo.runtime.test.runner.FeaturesRunner.Direction.BACKWARD; 023import static org.nuxeo.runtime.test.runner.FeaturesRunner.Direction.FORWARD; 024 025import java.io.IOException; 026import java.lang.annotation.Annotation; 027import java.net.URL; 028import java.nio.file.Path; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.List; 033import java.util.function.Supplier; 034import java.util.stream.Collectors; 035 036import org.apache.logging.log4j.LogManager; 037import org.apache.logging.log4j.Logger; 038import org.junit.ClassRule; 039import org.junit.Rule; 040import org.junit.internal.AssumptionViolatedException; 041import org.junit.rules.MethodRule; 042import org.junit.rules.TestRule; 043import org.junit.runner.Description; 044import org.junit.runner.notification.RunNotifier; 045import org.junit.runners.BlockJUnit4ClassRunner; 046import org.junit.runners.model.FrameworkMethod; 047import org.junit.runners.model.InitializationError; 048import org.junit.runners.model.Statement; 049import org.junit.runners.model.TestClass; 050import org.nuxeo.common.function.ThrowableConsumer; 051import org.nuxeo.common.function.ThrowableRunnable; 052import org.nuxeo.runtime.RuntimeServiceException; 053import org.nuxeo.runtime.test.TargetResourceLocator; 054 055import com.google.inject.Guice; 056import com.google.inject.Injector; 057import com.google.inject.Module; 058import com.google.inject.Stage; 059import com.google.inject.name.Names; 060 061/** 062 * A Test Case runner that can be extended through features and provide injection though Guice. 063 * 064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 065 */ 066public class FeaturesRunner extends BlockJUnit4ClassRunner { 067 068 private static final Logger log = LogManager.getLogger(FeaturesRunner.class); 069 070 protected static final AnnotationScanner scanner = new AnnotationScanner(); 071 072 /** @since 11.1 */ 073 protected static final String CUSTOM_ENVIRONMENT_SYSTEM_PROPERTY = "custom.environment"; 074 075 /** @since 11.1 */ 076 protected static final String DEFAULT_BUILD_DIRECTORY = "target"; 077 078 /** 079 * Guice injector. 080 */ 081 protected Injector injector; 082 083 protected final FeaturesLoader loader = new FeaturesLoader(this); 084 085 protected final TargetResourceLocator locator; 086 087 public static AnnotationScanner getScanner() { 088 return scanner; 089 } 090 091 /** 092 * Returns the Maven build directory, depending on the {@value #CUSTOM_ENVIRONMENT_SYSTEM_PROPERTY} system property. 093 * 094 * @since 11.1 095 */ 096 public static String getBuildDirectory() { 097 String customEnvironment = System.getProperty(CUSTOM_ENVIRONMENT_SYSTEM_PROPERTY); 098 return customEnvironment == null ? DEFAULT_BUILD_DIRECTORY 099 : String.format("%s-%s", DEFAULT_BUILD_DIRECTORY, customEnvironment); 100 } 101 102 public FeaturesRunner(Class<?> classToRun) throws InitializationError { 103 super(classToRun); 104 locator = new TargetResourceLocator(classToRun); 105 try { 106 loader.loadFeatures(getTargetTestClass()); 107 } catch (Throwable t) { // NOSONAR 108 throw new InitializationError(Collections.singletonList(t)); 109 } 110 } 111 112 public Class<?> getTargetTestClass() { 113 return super.getTestClass().getJavaClass(); 114 } 115 116 /** 117 * May return null if the test class was not yet instantiated 118 */ 119 public Object getTargetTestInstance() { 120 return underTest; 121 } 122 123 /** 124 * @deprecated since 11.1, seems unused 125 */ 126 @Deprecated(since = "11.1") 127 public Path getTargetTestBasepath() { 128 return locator.getBasepath(); 129 } 130 131 public URL getTargetTestResource(String name) throws IOException { 132 return locator.getTargetTestResource(name); 133 } 134 135 public Iterable<RunnerFeature> getFeatures() { 136 return loader.features(); 137 } 138 139 /** 140 * @since 5.6 141 */ 142 public <T extends Annotation> T getConfig(Class<T> type) { 143 List<T> configs = new ArrayList<>(); 144 T annotation = scanner.getAnnotation(getTargetTestClass(), type); 145 if (annotation != null) { 146 configs.add(annotation); 147 } 148 apply("getAnnotation", BACKWARD, holder -> { 149 T hAnnotation = scanner.getAnnotation(holder.type, type); 150 if (hAnnotation != null) { 151 configs.add(hAnnotation); 152 } 153 }); 154 return Defaults.of(type, configs); 155 } 156 157 /** 158 * Get the annotation on the test method, if no annotation has been found, get the annotation from the test class 159 * (See {@link #getConfig(Class)}) 160 * 161 * @since 5.7 162 */ 163 public <T extends Annotation> T getConfig(FrameworkMethod method, Class<T> type) { 164 T config = method.getAnnotation(type); 165 if (config != null) { 166 return config; 167 } 168 // if not define, try to get the config of the class 169 return getConfig(type); 170 } 171 172 @Override 173 protected List<FrameworkMethod> computeTestMethods() { 174 List<FrameworkMethod> methods = super.computeTestMethods(); 175 // sort a copy 176 methods = new ArrayList<>(methods); 177 MethodSorter.sortMethodsUsingSourceOrder(methods); 178 return methods; 179 } 180 181 protected void initialize() throws Exception { 182 log.trace("Invoke initialize on features"); 183 for (RunnerFeature each : getFeatures()) { 184 each.initialize(this); 185 } 186 } 187 188 protected void beforeRun() { 189 apply("beforeRun", FORWARD, holder -> holder.feature.beforeRun(FeaturesRunner.this)); 190 } 191 192 protected void beforeMethodRun(final FrameworkMethod method, final Object test) { 193 apply("beforeMethodRun", FORWARD, holder -> holder.feature.beforeMethodRun(FeaturesRunner.this, method, test)); 194 } 195 196 protected void afterMethodRun(final FrameworkMethod method, final Object test) { 197 apply("afterMethodRun", FORWARD, holder -> holder.feature.afterMethodRun(FeaturesRunner.this, method, test)); 198 } 199 200 protected void afterRun() { 201 apply("afterRun", BACKWARD, holder -> holder.feature.afterRun(FeaturesRunner.this)); 202 } 203 204 protected void start() { 205 apply("start", FORWARD, holder -> holder.feature.start(FeaturesRunner.this)); 206 } 207 208 protected void stop() { 209 apply("stop", BACKWARD, holder -> holder.feature.stop(FeaturesRunner.this)); 210 } 211 212 protected void beforeSetup(final FrameworkMethod method, final Object test) { 213 apply("beforeSetup", FORWARD, holder -> holder.feature.beforeSetup(FeaturesRunner.this, method, test)); 214 injector.injectMembers(underTest); 215 } 216 217 protected void afterTeardown(final FrameworkMethod method, final Object test) { 218 apply("afterTeardown", BACKWARD, holder -> holder.feature.afterTeardown(FeaturesRunner.this, method, test)); 219 } 220 221 /** 222 * @since 11.1 223 */ 224 protected void apply(String id, Direction direction, ThrowableConsumer<FeaturesLoader.Holder, Exception> consumer) { 225 apply(id, direction == FORWARD ? loader.holders() : loader.reversedHolders(), consumer); 226 } 227 228 /** 229 * @since 11.1 230 */ 231 protected void apply(String id, Collection<FeaturesLoader.Holder> holders, 232 ThrowableConsumer<FeaturesLoader.Holder, Exception> consumer) { 233 log.trace("Invoke: {} on features: {}", () -> id, () -> formatFeatures(holders)); 234 Supplier<String> msg = () -> String.format("Error while invoking %s on features: %s", id, 235 formatFeatures(holders)); 236 var errors = new ArrayList<Throwable>(); 237 for (FeaturesLoader.Holder holder : holders) { 238 try { 239 consumer.accept(holder); 240 } catch (AssumptionViolatedException cause) { 241 log.debug("Test ignored by the feature: {}", holder.type); 242 throw cause; 243 } catch (InterruptedException cause) { 244 Thread.currentThread().interrupt(); 245 throw new RuntimeServiceException(msg.get(), cause); 246 } catch (Throwable cause) { // NOSONAR 247 log.debug("Error while invoking: {} on feature: {}", id, holder.type); 248 throwSeriousError(cause); 249 errors.add(cause); 250 } 251 } 252 if (!errors.isEmpty()) { 253 var exception = new AssertionError(msg.get()); 254 errors.forEach(exception::addSuppressed); 255 throw exception; 256 } 257 } 258 259 protected String formatFeatures(Collection<FeaturesLoader.Holder> holders) { 260 return holders.stream().map(h -> h.type.getName()).collect(Collectors.joining(", ", "[", "]")); 261 } 262 263 public void evaluateRunnable(ThrowableRunnable<Throwable> runnable, ThrowableRunnable<Throwable> finisher) 264 throws Throwable { 265 Throwable error = null; 266 try { 267 runnable.run(); 268 } catch (InterruptedException e) { 269 Thread.currentThread().interrupt(); 270 throw new RuntimeServiceException(e); 271 } catch (Throwable t) { // NOSONAR 272 throwSeriousError(t); 273 error = t; 274 } 275 // don't use finally, we want to stop if thread was interrupted 276 try { 277 finisher.run(); 278 } catch (InterruptedException e) { 279 Thread.currentThread().interrupt(); 280 throw new RuntimeServiceException(e); 281 } catch (Throwable t) { // NOSONAR 282 throwSeriousError(t); 283 if (error == null) { 284 error = t; 285 } else { 286 error.addSuppressed(t); 287 } 288 } 289 if (error != null) { 290 throw error; 291 } 292 } 293 294 protected void throwSeriousError(Throwable cause) { 295 if (cause instanceof Error && !(cause instanceof AssertionError)) { 296 throw (Error) cause; // serious error, rethrow immediately 297 } 298 } 299 300 public Injector getInjector() { 301 return injector; 302 } 303 304 protected Injector onInjector(final RunNotifier aNotifier) { 305 return Guice.createInjector(Stage.DEVELOPMENT, (Module) aBinder -> { 306 aBinder.bind(FeaturesRunner.class).toInstance(FeaturesRunner.this); 307 aBinder.bind(RunNotifier.class).toInstance(aNotifier); 308 aBinder.bind(TargetResourceLocator.class).toInstance(locator); 309 }); 310 } 311 312 protected class BeforeClassStatement extends Statement { 313 protected final Statement next; 314 315 protected BeforeClassStatement(Statement aStatement) { 316 next = aStatement; 317 } 318 319 @Override 320 public void evaluate() throws Throwable { 321 initialize(); 322 start(); 323 beforeRun(); 324 injector = injector.createChildInjector(loader.onModule()); 325 try { 326 next.evaluate(); 327 } finally { 328 injector = injector.getParent(); 329 } 330 } 331 332 } 333 334 protected class AfterClassStatement extends Statement { 335 protected final Statement previous; 336 337 protected AfterClassStatement(Statement aStatement) { 338 previous = aStatement; 339 } 340 341 @Override 342 public void evaluate() throws Throwable { 343 previous.evaluate(); 344 evaluateRunnable(FeaturesRunner.this::afterRun, FeaturesRunner.this::stop); 345 } 346 } 347 348 @Override 349 protected Statement withAfterClasses(Statement statement) { 350 Statement actual = statement; 351 actual = super.withAfterClasses(actual); 352 actual = new AfterClassStatement(actual); 353 return actual; 354 } 355 356 @Override 357 protected Statement classBlock(final RunNotifier aNotifier) { 358 injector = onInjector(aNotifier); 359 return super.classBlock(aNotifier); 360 } 361 362 @Override 363 protected List<TestRule> classRules() { 364 final RulesFactory<ClassRule, TestRule> factory = new RulesFactory<>(ClassRule.class, TestRule.class); 365 366 factory.withRule((base, description) -> new BeforeClassStatement(base)).withRules(super.classRules()); 367 apply("classRules", FORWARD, holder -> factory.withRules(holder.testClass, null)); 368 369 return factory.build(); 370 } 371 372 protected abstract static class MethodStatement extends Statement { 373 374 protected final FrameworkMethod method; 375 376 protected final Object target; 377 378 protected final Statement statement; 379 380 public MethodStatement(FrameworkMethod method, Object target, Statement statement) { 381 this.method = method; 382 this.target = target; 383 this.statement = statement; 384 } 385 } 386 387 protected class BeforeMethodRunStatement extends MethodStatement { 388 389 protected BeforeMethodRunStatement(FrameworkMethod method, Object target, Statement statement) { 390 super(method, target, statement); 391 } 392 393 @Override 394 public void evaluate() throws Throwable { 395 beforeMethodRun(method, target); 396 statement.evaluate(); 397 } 398 399 } 400 401 protected class BeforeSetupStatement extends MethodStatement { 402 403 protected BeforeSetupStatement(FrameworkMethod method, Object target, Statement statement) { 404 super(method, target, statement); 405 } 406 407 @Override 408 public void evaluate() throws Throwable { 409 beforeSetup(method, target); 410 statement.evaluate(); 411 } 412 413 } 414 415 @Override 416 protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { 417 Statement actual = statement; 418 actual = new BeforeMethodRunStatement(method, target, actual); 419 actual = super.withBefores(method, target, actual); 420 actual = new BeforeSetupStatement(method, target, actual); 421 return actual; 422 } 423 424 protected class AfterMethodRunStatement extends MethodStatement { 425 426 protected AfterMethodRunStatement(FrameworkMethod method, Object target, Statement statement) { 427 super(method, target, statement); 428 } 429 430 @Override 431 public void evaluate() throws Throwable { 432 evaluateRunnable(statement::evaluate, () -> afterMethodRun(method, target)); 433 } 434 435 } 436 437 protected class AfterTeardownStatement extends MethodStatement { 438 439 protected AfterTeardownStatement(FrameworkMethod method, Object target, Statement statement) { 440 super(method, target, statement); 441 } 442 443 @Override 444 public void evaluate() throws Throwable { 445 evaluateRunnable(statement::evaluate, () -> afterTeardown(method, target)); 446 } 447 448 } 449 450 @Override 451 protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { 452 Statement actual = statement; 453 actual = new AfterMethodRunStatement(method, target, actual); 454 actual = super.withAfters(method, target, actual); 455 actual = new AfterTeardownStatement(method, target, actual); 456 return actual; 457 } 458 459 @Override 460 protected List<TestRule> getTestRules(Object target) { 461 final RulesFactory<Rule, TestRule> factory = new RulesFactory<>(Rule.class, TestRule.class); 462 apply("getTestRules", FORWARD, holder -> factory.withRules(holder.testClass, holder.feature)); 463 factory.withRules(getTestClass(), target); 464 return factory.build(); 465 } 466 467 @Override 468 protected List<MethodRule> rules(Object target) { 469 final RulesFactory<Rule, MethodRule> factory = new RulesFactory<>(Rule.class, MethodRule.class); 470 apply("rules", FORWARD, holder -> factory.withRules(holder.testClass, holder.feature)); 471 factory.withRules(getTestClass(), target); 472 return factory.build(); 473 } 474 475 protected Object underTest; 476 477 @Override 478 public Object createTest() throws Exception { 479 underTest = super.createTest(); 480 apply("createTest", FORWARD, holder -> holder.feature.testCreated(underTest)); 481 // TODO replace underTest member with a binding 482 // Class<?> testType = underTest.getClass(); 483 // injector.getInstance(Binder.class).bind(testType) 484 // .toInstance(testType.cast(underTest)); 485 return underTest; 486 } 487 488 @Override 489 protected void validateZeroArgConstructor(List<Throwable> errors) { 490 // Guice can inject constructors with parameters so we don't want this 491 // method to trigger an error 492 } 493 494 @Override 495 public String toString() { 496 return "FeaturesRunner [fTest=" + getTargetTestClass() + "]"; 497 } 498 499 protected class RulesFactory<A extends Annotation, R> { 500 501 protected Statement build(final Statement base, final String name) { 502 return new Statement() { 503 504 @Override 505 @SuppressWarnings({ "unchecked", "rawtypes" }) 506 public void evaluate() throws Throwable { 507 injector = injector.createChildInjector((Module) binder -> { 508 for (Object each : rules) { 509 binder.bind((Class) each.getClass()).annotatedWith(Names.named(name)).toInstance(each); 510 binder.requestInjection(each); 511 } 512 }); 513 514 try { 515 base.evaluate(); 516 } finally { 517 injector = injector.getParent(); 518 } 519 520 } 521 522 }; 523 524 } 525 526 protected class BindRule implements TestRule, MethodRule { 527 528 @Override 529 public Statement apply(Statement base, FrameworkMethod method, Object target) { 530 Statement statement = build(base, "method"); 531 for (Object each : rules) { 532 statement = ((MethodRule) each).apply(statement, method, target); 533 } 534 return statement; 535 } 536 537 @Override 538 public Statement apply(Statement base, Description description) { 539 if (rules.isEmpty()) { 540 return base; 541 } 542 Statement statement = build(base, "test"); 543 for (Object each : rules) { 544 statement = ((TestRule) each).apply(statement, description); 545 } 546 return statement; 547 } 548 549 } 550 551 protected final Class<A> annotationType; 552 553 protected final Class<R> ruleType; 554 555 protected ArrayList<R> rules = new ArrayList<>(); 556 557 protected RulesFactory(Class<A> anAnnotationType, Class<R> aRuleType) { 558 annotationType = anAnnotationType; 559 ruleType = aRuleType; 560 } 561 562 public RulesFactory<A, R> withRules(List<R> someRules) { 563 this.rules.addAll(someRules); 564 return this; 565 } 566 567 public RulesFactory<A, R> withRule(R aRule) { 568 injector.injectMembers(aRule); 569 rules.add(aRule); 570 return this; 571 } 572 573 public RulesFactory<A, R> withRules(TestClass aType, Object aTest) { 574 for (R each : aType.getAnnotatedFieldValues(aTest, annotationType, ruleType)) { 575 withRule(each); 576 } 577 578 for (FrameworkMethod each : aType.getAnnotatedMethods(annotationType)) { 579 if (ruleType.isAssignableFrom(each.getMethod().getReturnType())) { 580 withRule(onMethod(ruleType, each, aTest)); 581 } 582 } 583 return this; 584 } 585 586 public List<R> build() { 587 return Collections.singletonList(ruleType.cast(new BindRule())); 588 } 589 590 protected R onMethod(Class<R> aRuleType, FrameworkMethod aMethod, Object aTarget, Object... someParms) { 591 try { 592 return aRuleType.cast(aMethod.invokeExplosively(aTarget, someParms)); 593 } catch (Throwable cause) { 594 throw new RuntimeServiceException("Errors in rules factory " + aMethod, cause); 595 } 596 } 597 598 } 599 600 public <T extends RunnerFeature> T getFeature(Class<T> aType) { 601 return loader.getFeature(aType); 602 } 603 604 protected enum Direction { 605 FORWARD, BACKWARD 606 } 607 608}