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