001/*
002 * (C) Copyright 2014-2019 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 *     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.ClassRule;
034import org.junit.Rule;
035import org.junit.rules.MethodRule;
036import org.junit.rules.TestRule;
037import org.junit.runner.Description;
038import org.junit.runner.notification.RunNotifier;
039import org.junit.runners.model.FrameworkMethod;
040import org.junit.runners.model.Statement;
041import org.nuxeo.runtime.RuntimeServiceException;
042import org.nuxeo.runtime.test.runner.FeaturesRunner.BeforeClassStatement;
043
044public class ConditionalIgnoreRule implements TestRule, MethodRule {
045    @Inject
046    private RunNotifier runNotifier;
047
048    @Inject
049    private FeaturesRunner runner;
050
051    public static class Feature implements RunnerFeature {
052        protected static final ConditionalIgnoreRule rule = new ConditionalIgnoreRule();
053
054        @Rule
055        public MethodRule methodRule() {
056            return rule;
057        }
058
059        @ClassRule
060        public static TestRule testRule() {
061            return rule;
062        }
063
064    }
065
066    @Retention(RetentionPolicy.RUNTIME)
067    @Target({ ElementType.TYPE, ElementType.METHOD })
068    public @interface Ignore {
069        Class<? extends Condition> condition();
070
071        /**
072         * Optional reason why the test is ignored, reported additionally to the condition class simple name.
073         */
074        String cause() default "";
075    }
076
077    public interface Condition {
078        boolean shouldIgnore();
079
080        /**
081         * Returns whether this condition supports check at class level. Note: A condition supporting the class rule
082         * behavior will be called before the {@link BeforeClassStatement}, at this moment Nuxeo Runtime is not fully
083         * initialized and injection is not performed yet.
084         * <p>
085         * By default, conditions don't support it in order to keep backward compatibility.
086         *
087         * @since 11.1
088         */
089        default boolean supportsClassRule() {
090            return false;
091        }
092    }
093
094    /**
095     * @deprecated since 11.1, {@code IsolatedClassloader} doesn't exist anymore
096     */
097    @Deprecated(since = "11.1")
098    public static final class IgnoreIsolated implements Condition {
099        boolean isIsolated = "org.nuxeo.runtime.testsuite.IsolatedClassloader".equals(
100                getClass().getClassLoader().getClass().getName());
101
102        @Override
103        public boolean shouldIgnore() {
104            return isIsolated;
105        }
106    }
107
108    public static final class IgnoreLongRunning implements Condition {
109        @Override
110        public boolean shouldIgnore() {
111            return true;
112        }
113    }
114
115    public static final class IgnoreWindows implements Condition {
116        @Override
117        public boolean shouldIgnore() {
118            return SystemUtils.IS_OS_WINDOWS;
119        }
120
121        @Override
122        public boolean supportsClassRule() {
123            return true;
124        }
125    }
126
127    @Override
128    public Statement apply(Statement base, Description description) {
129        Ignore ignore = runner.getConfig(Ignore.class);
130        Class<? extends Condition> conditionType = ignore.condition();
131        if (conditionType == null) {
132            return base;
133        }
134        return new Statement() {
135
136            @Override
137            public void evaluate() throws Throwable {
138                // as this is a @ClassRule / TestRule, built statement is evaluated just before @BeforeClass annotations
139                // and thus before Nuxeo Runtime initialization. Condition should explicitly support the ClassRule
140                // behavior to ignore tests there. If it doesn't, check will be done before test (former behavior)
141                Condition condition = instantiateCondition(conditionType);
142                if (condition.supportsClassRule() && condition.shouldIgnore()) {
143                    runNotifier.fireTestIgnored(description);
144                } else {
145                    base.evaluate();
146                }
147            }
148        };
149    }
150
151    @Override
152    public Statement apply(Statement base, FrameworkMethod frameworkMethod, Object target) {
153        Ignore ignore = runner.getConfig(frameworkMethod, Ignore.class);
154        Class<? extends Condition> conditionType = ignore.condition();
155        if (conditionType == null) {
156            return base;
157        }
158        Class<?> type = target.getClass();
159        Method method = frameworkMethod.getMethod();
160        Description description = Description.createTestDescription(type, method.getName(), method.getAnnotations());
161        return new Statement() {
162
163            @Override
164            public void evaluate() throws Throwable {
165                if (newCondition(type, method, target, conditionType).shouldIgnore()) {
166                    runNotifier.fireTestIgnored(description);
167                } else {
168                    base.evaluate();
169                }
170            }
171        };
172    }
173
174    protected Condition newCondition(Class<?> type, Method method, Object target,
175            Class<? extends Condition> conditionType) {
176        Condition condition = instantiateCondition(conditionType);
177        injectCondition(type, method, target, condition);
178        return condition;
179    }
180
181    protected Condition instantiateCondition(Class<? extends Condition> conditionType) {
182        Condition condition;
183        try {
184            condition = conditionType.getDeclaredConstructor().newInstance();
185        } catch (ReflectiveOperationException cause) {
186            throw new RuntimeServiceException("Cannot instantiate condition of type " + conditionType, cause);
187        }
188        return condition;
189    }
190
191    protected void injectCondition(Class<?> type, Method method, Object target, Condition condition) {
192        var errors = new RuntimeServiceException("Cannot inject condition parameters in " + condition.getClass());
193        for (Field eachField : condition.getClass().getDeclaredFields()) {
194            if (!eachField.isAnnotationPresent(Inject.class)) {
195                continue;
196            }
197            Object eachValue = null;
198            if (eachField.isAnnotationPresent(Named.class)) {
199                String name = eachField.getAnnotation(Named.class).value();
200                if ("type".equals(name)) {
201                    eachValue = type;
202                } else if ("target".equals(name)) {
203                    eachValue = target;
204                } else if ("method".equals(name)) {
205                    eachValue = method;
206                }
207            } else {
208                Class<?> eachType = eachField.getType();
209                if (eachType.equals(Class.class)) {
210                    eachValue = type;
211                } else if (eachType.equals(Object.class)) {
212                    eachValue = target;
213                } else if (eachType.equals(Method.class)) {
214                    eachValue = method;
215                }
216            }
217            if (eachValue == null) {
218                continue;
219            }
220            eachField.setAccessible(true);
221            try {
222                eachField.set(condition, eachValue);
223            } catch (IllegalArgumentException | IllegalAccessException cause) {
224                errors.addSuppressed(new RuntimeServiceException("Cannot inject " + eachField.getName(), cause));
225            }
226        }
227        if (errors.getSuppressed().length > 0) {
228            throw errors;
229        }
230        runner.getInjector().injectMembers(condition);
231    }
232
233}