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}