001/*
002 * (C) Copyright 2014-2015 Nuxeo SA (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 *     Stephane Lacoin, Julien Carsique
018 *
019 */
020package org.nuxeo.runtime.test.runner;
021
022import java.lang.annotation.ElementType;
023import java.lang.annotation.Retention;
024import java.lang.annotation.RetentionPolicy;
025import java.lang.annotation.Target;
026import java.lang.reflect.Field;
027import java.lang.reflect.Method;
028
029import javax.inject.Inject;
030import javax.inject.Named;
031
032import org.apache.commons.lang3.SystemUtils;
033import org.junit.Rule;
034import org.junit.rules.MethodRule;
035import org.junit.rules.TestRule;
036import org.junit.runner.Description;
037import org.junit.runner.notification.RunNotifier;
038import org.junit.runners.model.FrameworkMethod;
039import org.junit.runners.model.Statement;
040
041public class ConditionalIgnoreRule implements TestRule, MethodRule {
042    @Inject
043    private RunNotifier runNotifier;
044
045    @Inject
046    private FeaturesRunner runner;
047
048    public static class Feature implements RunnerFeature {
049        protected static final ConditionalIgnoreRule rule = new ConditionalIgnoreRule();
050
051        @Rule
052        public MethodRule methodRule() {
053            return rule;
054        }
055
056        @Rule
057        public static TestRule testRule() {
058            return rule;
059        }
060
061    }
062
063    @Retention(RetentionPolicy.RUNTIME)
064    @Target({ ElementType.TYPE, ElementType.METHOD })
065    public @interface Ignore {
066        Class<? extends Condition> condition();
067
068        /**
069         * Optional reason why the test is ignored, reported additionally to the condition class simple name.
070         */
071        String cause() default "";
072    }
073
074    public interface Condition {
075        boolean shouldIgnore();
076    }
077
078    public static final class NXP10926H2Upgrade implements Condition {
079        @Override
080        public boolean shouldIgnore() {
081            return false;
082        }
083    }
084
085    public static final class IgnoreIsolated implements Condition {
086        boolean isIsolated = "org.nuxeo.runtime.testsuite.IsolatedClassloader".equals(
087                getClass().getClassLoader().getClass().getName());
088
089        @Override
090        public boolean shouldIgnore() {
091            return isIsolated;
092        }
093    }
094
095    public static final class IgnoreLongRunning implements Condition {
096        @Override
097        public boolean shouldIgnore() {
098            return true;
099        }
100    }
101
102    public static final class IgnoreWindows implements Condition {
103        @Override
104        public boolean shouldIgnore() {
105            return SystemUtils.IS_OS_WINDOWS;
106        }
107    }
108
109    @Override
110    public Statement apply(Statement base, Description description) {
111        Ignore ignore = runner.getConfig(Ignore.class);
112        Class<? extends Condition> conditionType = ignore.condition();
113        if (conditionType == null) {
114            return base;
115        }
116        return new Statement() {
117
118            @Override
119            public void evaluate() throws Throwable {
120                // as this is a TestRule / Rule (see Feature#testRule) runtime was already started
121                // because we are here after BeforeClassStatement (runtime start) and before
122                // the MethodRule / Rule execution which cause processing of method annotations, but here we just want
123                // to check condition on the class which doesn't depend on method annotations
124                if (newCondition(null, null, null, conditionType).shouldIgnore()) {
125                    runNotifier.fireTestIgnored(description);
126                } else {
127                    base.evaluate();
128                }
129            }
130        };
131    }
132
133    @Override
134    public Statement apply(Statement base, FrameworkMethod frameworkMethod, Object target) {
135        Ignore ignore = runner.getConfig(frameworkMethod, Ignore.class);
136        Class<? extends Condition> conditionType = ignore.condition();
137        if (conditionType == null) {
138            return base;
139        }
140        Class<?> type = target.getClass();
141        Method method = frameworkMethod.getMethod();
142        Description description = Description.createTestDescription(type, method.getName(), method.getAnnotations());
143        return new Statement() {
144
145            @Override
146            public void evaluate() throws Throwable {
147                if (newCondition(type, method, target, conditionType).shouldIgnore()) {
148                    runNotifier.fireTestIgnored(description);
149                } else {
150                    base.evaluate();
151                }
152            }
153        };
154    }
155
156    protected Condition newCondition(Class<?> type, Method method, Object target,
157            Class<? extends Condition> conditionType) throws Error {
158        Condition condition;
159        try {
160            condition = conditionType.newInstance();
161        } catch (InstantiationException | IllegalAccessException cause) {
162            throw new Error("Cannot instantiate condition of type " + conditionType, cause);
163        }
164        injectCondition(type, method, target, condition);
165        return condition;
166    }
167
168    protected void injectCondition(Class<?> type, Method method, Object target, Condition condition)
169            throws SecurityException, Error {
170        Error errors = new Error("Cannot inject condition parameters in " + condition.getClass());
171        for (Field eachField : condition.getClass().getDeclaredFields()) {
172            if (!eachField.isAnnotationPresent(Inject.class)) {
173                continue;
174            }
175            Object eachValue = null;
176            if (eachField.isAnnotationPresent(Named.class)) {
177                String name = eachField.getAnnotation(Named.class).value();
178                if ("type".equals(name)) {
179                    eachValue = type;
180                } else if ("target".equals(name)) {
181                    eachValue = target;
182                } else if ("method".equals(name)) {
183                    eachValue = method;
184                }
185            } else {
186                Class<?> eachType = eachField.getType();
187                if (eachType.equals(Class.class)) {
188                    eachValue = type;
189                } else if (eachType.equals(Object.class)) {
190                    eachValue = target;
191                } else if (eachType.equals(Method.class)) {
192                    eachValue = method;
193                }
194            }
195            if (eachValue == null) {
196                continue;
197            }
198            eachField.setAccessible(true);
199            try {
200                eachField.set(condition, eachValue);
201            } catch (IllegalArgumentException | IllegalAccessException cause) {
202                errors.addSuppressed(new Error("Cannot inject " + eachField.getName(), cause));
203            }
204        }
205        if (errors.getSuppressed().length > 0) {
206            throw errors;
207        }
208        runner.getInjector().injectMembers(condition);
209    }
210
211}