/* * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * bstefanescu */ package org.nuxeo.runtime.test.runner; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.ClassRule; import org.junit.Rule; import org.junit.rules.MethodRule; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import org.nuxeo.runtime.test.TargetResourceLocator; import org.nuxeo.runtime.test.runner.FeaturesLoader.Callable; import org.nuxeo.runtime.test.runner.FeaturesLoader.Direction; import org.nuxeo.runtime.test.runner.FeaturesLoader.Holder; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; import com.google.inject.name.Names; /** * A Test Case runner that can be extended through features and provide injection though Guice. * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class FeaturesRunner extends BlockJUnit4ClassRunner { protected static final AnnotationScanner scanner = new AnnotationScanner(); /** * Guice injector. */ protected Injector injector; protected final FeaturesLoader loader = new FeaturesLoader(this); protected final TargetResourceLocator locator; public static AnnotationScanner getScanner() { return scanner; } public FeaturesRunner(Class<?> classToRun) throws InitializationError { super(classToRun); locator = new TargetResourceLocator(classToRun); try { loader.loadFeatures(getTargetTestClass()); } catch (Throwable t) { throw new InitializationError(Collections.singletonList(t)); } } public Class<?> getTargetTestClass() { return super.getTestClass().getJavaClass(); } public Path getTargetTestBasepath() { return locator.getBasepath(); } public URL getTargetTestResource(String name) throws IOException { return locator.getTargetTestResource(name); } public Iterable<RunnerFeature> getFeatures() { return loader.features(); } /** * @since 5.6 */ public <T extends Annotation> T getConfig(Class<T> type) { List<T> configs = new ArrayList<>(); T annotation = scanner.getAnnotation(getTargetTestClass(), type); if (annotation != null) { configs.add(annotation); } loader.apply(Direction.BACKWARD, new Callable() { @Override public void call(Holder holder) throws Exception { T hAnnotation = scanner.getAnnotation(holder.type, type); if (hAnnotation != null) { configs.add(hAnnotation); } } }); return Defaults.of(type, configs); } /** * Get the annotation on the test method, if no annotation has been found, get the annotation from the test class * (See {@link #getConfig(Class)}) * * @since 5.7 */ public <T extends Annotation> T getConfig(FrameworkMethod method, Class<T> type) { T config = method.getAnnotation(type); if (config != null) { return config; } // if not define, try to get the config of the class return getConfig(type); } @Override protected List<FrameworkMethod> computeTestMethods() { List<FrameworkMethod> methods = super.computeTestMethods(); // sort a copy methods = new ArrayList<>(methods); MethodSorter.sortMethodsUsingSourceOrder(methods); return methods; } protected void initialize() throws Exception { for (RunnerFeature each : getFeatures()) { each.initialize(this); } } protected void beforeRun() throws Exception { loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.beforeRun(FeaturesRunner.this); } }); } protected void beforeMethodRun(final FrameworkMethod method, final Object test) throws Exception { loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.beforeMethodRun(FeaturesRunner.this, method, test); } }); } protected void afterMethodRun(final FrameworkMethod method, final Object test) throws Exception { loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.afterMethodRun(FeaturesRunner.this, method, test); } }); } protected void afterRun() throws Exception { loader.apply(Direction.BACKWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.afterRun(FeaturesRunner.this); } }); } protected void start() throws Exception { loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.start(FeaturesRunner.this); } }); } protected void stop() throws Exception { loader.apply(Direction.BACKWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.stop(FeaturesRunner.this); } }); } protected void beforeSetup() throws Exception { loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.beforeSetup(FeaturesRunner.this); } }); injector.injectMembers(underTest); } protected void afterTeardown() { loader.apply(Direction.BACKWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.afterTeardown(FeaturesRunner.this); } }); } public Injector getInjector() { return injector; } protected Injector onInjector(final RunNotifier aNotifier) { return Guice.createInjector(Stage.DEVELOPMENT, new Module() { @Override public void configure(Binder aBinder) { aBinder.bind(FeaturesRunner.class).toInstance(FeaturesRunner.this); aBinder.bind(RunNotifier.class).toInstance(aNotifier); aBinder.bind(TargetResourceLocator.class).toInstance(locator); } }); } protected class BeforeClassStatement extends Statement { protected final Statement next; protected BeforeClassStatement(Statement aStatement) { next = aStatement; } @Override public void evaluate() throws Throwable { initialize(); start(); beforeRun(); injector = injector.createChildInjector(loader.onModule()); try { next.evaluate(); } finally { injector = injector.getParent(); } } } protected class AfterClassStatement extends Statement { protected final Statement previous; protected AfterClassStatement(Statement aStatement) { previous = aStatement; } @Override public void evaluate() throws Throwable { previous.evaluate(); try { afterRun(); } finally { stop(); } } } @Override protected Statement withAfterClasses(Statement statement) { Statement actual = statement; actual = super.withAfterClasses(actual); actual = new AfterClassStatement(actual); return actual; } @Override protected Statement classBlock(final RunNotifier aNotifier) { injector = onInjector(aNotifier); return super.classBlock(aNotifier); } @Override protected List<TestRule> classRules() { final RulesFactory<ClassRule, TestRule> factory = new RulesFactory<>(ClassRule.class, TestRule.class); factory.withRule(new TestRule() { @Override public Statement apply(Statement base, Description description) { return new BeforeClassStatement(base); } }).withRules(super.classRules()); loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { factory.withRules(holder.testClass, null); } }); return factory.build(); } protected class BeforeMethodRunStatement extends Statement { protected final Statement next; protected final FrameworkMethod method; protected final Object target; protected BeforeMethodRunStatement(FrameworkMethod aMethod, Object aTarget, Statement aStatement) { method = aMethod; target = aTarget; next = aStatement; } @Override public void evaluate() throws Throwable { beforeMethodRun(method, target); next.evaluate(); } } protected class BeforeSetupStatement extends Statement { protected final Statement next; protected BeforeSetupStatement(Statement aStatement) { next = aStatement; } @Override public void evaluate() throws Throwable { beforeSetup(); next.evaluate(); } } @Override protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { Statement actual = statement; actual = new BeforeMethodRunStatement(method, target, actual); actual = super.withBefores(method, target, actual); actual = new BeforeSetupStatement(actual); return actual; } protected class AfterMethodRunStatement extends Statement { protected final Statement previous; protected final FrameworkMethod method; protected final Object target; protected AfterMethodRunStatement(FrameworkMethod aMethod, Object aTarget, Statement aStatement) { method = aMethod; target = aTarget; previous = aStatement; } @Override public void evaluate() throws Throwable { try { previous.evaluate(); } finally { afterMethodRun(method, target); } } } protected class AfterTeardownStatement extends Statement { protected final Statement previous; protected AfterTeardownStatement(Statement aStatement) { previous = aStatement; } @Override public void evaluate() throws Throwable { try { previous.evaluate(); } finally { afterTeardown(); } } } @Override protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { Statement actual = statement; actual = new AfterMethodRunStatement(method, target, actual); actual = super.withAfters(method, target, actual); actual = new AfterTeardownStatement(actual); return actual; } @Override protected List<TestRule> getTestRules(Object target) { final RulesFactory<Rule, TestRule> factory = new RulesFactory<>(Rule.class, TestRule.class); loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { factory.withRules(holder.testClass, holder.feature); } }); factory.withRules(getTestClass(), target); return factory.build(); } @Override protected List<MethodRule> rules(Object target) { final RulesFactory<Rule, MethodRule> factory = new RulesFactory<>(Rule.class, MethodRule.class); loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { factory.withRules(holder.testClass, holder.feature); } }); factory.withRules(getTestClass(), target); return factory.build(); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { final Statement actual = super.methodInvoker(method, test); return new Statement() { @Override public void evaluate() throws Throwable { injector.injectMembers(underTest); actual.evaluate(); } }; } protected Object underTest; @Override public Object createTest() throws Exception { underTest = super.createTest(); loader.apply(Direction.FORWARD, new Callable() { @Override public void call(Holder holder) throws Exception { holder.feature.testCreated(underTest); } }); // TODO replace underTest member with a binding // Class<?> testType = underTest.getClass(); // injector.getInstance(Binder.class).bind(testType) // .toInstance(testType.cast(underTest)); return underTest; } @Override protected void validateZeroArgConstructor(List<Throwable> errors) { // Guice can inject constructors with parameters so we don't want this // method to trigger an error } @Override public String toString() { return "FeaturesRunner [fTest=" + getTargetTestClass() + "]"; } protected class RulesFactory<A extends Annotation, R> { protected Statement build(final Statement base, final String name) { return new Statement() { @Override public void evaluate() throws Throwable { injector = injector.createChildInjector(new Module() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void configure(Binder binder) { for (Object each : rules) { binder.bind((Class) each.getClass()).annotatedWith(Names.named(name)).toInstance(each); binder.requestInjection(each); } } }); try { base.evaluate(); } finally { injector = injector.getParent(); } } }; } protected class BindRule implements TestRule, MethodRule { @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { Statement statement = build(base, "method"); for (Object each : rules) { statement = ((MethodRule) each).apply(statement, method, target); } return statement; } @Override public Statement apply(Statement base, Description description) { if (rules.isEmpty()) { return base; } Statement statement = build(base, "test"); for (Object each : rules) { statement = ((TestRule) each).apply(statement, description); } return statement; } } protected final Class<A> annotationType; protected final Class<R> ruleType; protected ArrayList<R> rules = new ArrayList<>(); protected RulesFactory(Class<A> anAnnotationType, Class<R> aRuleType) { annotationType = anAnnotationType; ruleType = aRuleType; } public RulesFactory<A, R> withRules(List<R> someRules) { this.rules.addAll(someRules); return this; } public RulesFactory<A, R> withRule(R aRule) { injector.injectMembers(aRule); rules.add(aRule); return this; } public RulesFactory<A, R> withRules(TestClass aType, Object aTest) { for (R each : aType.getAnnotatedFieldValues(aTest, annotationType, ruleType)) { withRule(each); } for (FrameworkMethod each : aType.getAnnotatedMethods(annotationType)) { if (ruleType.isAssignableFrom(each.getMethod().getReturnType())) { withRule(onMethod(ruleType, each, aTest)); } } return this; } public List<R> build() { return Collections.singletonList(ruleType.cast(new BindRule())); } protected R onMethod(Class<R> aRuleType, FrameworkMethod aMethod, Object aTarget, Object... someParms) { try { return aRuleType.cast(aMethod.invokeExplosively(aTarget, someParms)); } catch (Throwable cause) { throw new RuntimeException("Errors in rules factory " + aMethod, cause); } } } public <T extends RunnerFeature> T getFeature(Class<T> aType) { return loader.getFeature(aType); } }