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}