1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
22  */
23 package org.opengrok.indexer.condition;
24 
25 import java.lang.reflect.Method;
26 import java.lang.reflect.Modifier;
27 import java.util.ArrayList;
28 import java.util.LinkedList;
29 import java.util.List;
30 import org.junit.Assume;
31 import org.junit.rules.TestRule;
32 import org.junit.runner.Description;
33 import org.junit.runners.model.Statement;
34 
35 /**
36  * This rule can be added to a Junit test and will look for the annotation
37  * {@link ConditionalRun} on either the test class or method. The test is then
38  * skipped through Junit's {@link Assume} capabilities if the
39  * {@link RunCondition} provided in the annotation is not satisfied.
40  *
41  * Cobbled together from:
42  * http://www.codeaffine.com/2013/11/18/a-junit-rule-to-conditionally-ignore-tests/
43  * https://gist.github.com/yinzara/9980184
44  * http://cwd.dhemery.com/2010/12/junit-rules/
45  * http://stackoverflow.com/questions/28145735/androidjunit4-class-org-junit-assume-assumetrue-assumptionviolatedexception/
46  * https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html
47  */
48 public class ConditionalRunRule implements TestRule {
49 
50     @Override
apply(Statement aStatement, Description aDescription)51     public Statement apply(Statement aStatement, Description aDescription) {
52         if (hasConditionalIgnoreAnnotationOnMethod(aDescription)) {
53             RunCondition condition = getIgnoreConditionOnMethod(aDescription);
54             if (!condition.isForcedOrSatisfied()) {
55                 return new IgnoreStatement(condition);
56             }
57         }
58 
59         if (hasConditionalIgnoreAnnotationOnClass(aDescription)) {
60             RunCondition condition = getIgnoreConditionOnClass(aDescription);
61             if (!condition.isForcedOrSatisfied()) {
62                 return new IgnoreStatement(condition);
63             }
64         }
65 
66         return aStatement;
67     }
68 
hasConditionalIgnoreAnnotationOnClass(Description aDescription)69     private static boolean hasConditionalIgnoreAnnotationOnClass(Description aDescription) {
70         return aDescription.getTestClass().getAnnotationsByType(ConditionalRun.class).length > 0;
71     }
72 
getIgnoreConditionOnClass(Description aDescription)73     private static RunCondition getIgnoreConditionOnClass(Description aDescription) {
74         ConditionalRun[] annotations = aDescription.getTestClass().getAnnotationsByType(ConditionalRun.class);
75         return new IgnoreConditionCreator(aDescription.getTestClass(), annotations).create();
76     }
77 
hasConditionalIgnoreAnnotationOnMethod(Description aDescription)78     private static boolean hasConditionalIgnoreAnnotationOnMethod(Description aDescription) {
79         if (aDescription.getMethodName() == null) { // if @ClassRule is used
80             return false;
81         }
82         try {
83             // this is possible because test methods must not have any argument
84             Method testMethod = aDescription.getTestClass().getMethod(aDescription.getMethodName());
85             return testMethod.getAnnotationsByType(ConditionalRun.class).length > 0;
86         } catch (NoSuchMethodException | SecurityException ex) {
87             throw new RuntimeException(ex);
88         }
89     }
90 
getIgnoreConditionOnMethod(Description aDescription)91     private static RunCondition getIgnoreConditionOnMethod(Description aDescription) {
92         try {
93             // this is possible because test methods must not have any argument
94             ConditionalRun[] annotations = aDescription.getTestClass().getMethod(aDescription.getMethodName()).getAnnotationsByType(ConditionalRun.class);
95             return new IgnoreConditionCreator(aDescription.getTestClass(), annotations).create();
96         } catch (NoSuchMethodException | SecurityException ex) {
97             throw new RuntimeException(ex);
98         }
99     }
100 
101     /**
102      * Container for several conditions joined by an AND operator.
103      */
104     protected static class CompositeCondition implements RunCondition {
105 
106         List<RunCondition> conditions = new LinkedList<>();
107 
add(RunCondition e)108         public boolean add(RunCondition e) {
109             return conditions.add(e);
110         }
111 
112         @Override
isSatisfied()113         public boolean isSatisfied() {
114             for (RunCondition condition : conditions) {
115                 if (!condition.isSatisfied()) {
116                     return false;
117                 }
118             }
119             return true;
120         }
121     }
122 
123     protected static class IgnoreConditionCreator {
124 
125         private final Class<?> mTestClass;
126         private final List<Class<? extends RunCondition>> conditionTypes;
127 
IgnoreConditionCreator(Class<?> aTestClass, ConditionalRun[] annotation)128         public IgnoreConditionCreator(Class<?> aTestClass, ConditionalRun[] annotation) {
129             this.mTestClass = aTestClass;
130             this.conditionTypes = new ArrayList<>(annotation.length);
131             for (int i = 0; i < annotation.length; i++) {
132                 this.conditionTypes.add(i, annotation[i].value());
133             }
134         }
135 
create()136         public RunCondition create() {
137             checkConditionType();
138             try {
139                 return createCondition();
140             } catch (RuntimeException re) {
141                 throw re;
142             } catch (Exception e) {
143                 throw new RuntimeException(e);
144             }
145         }
146 
createCondition()147         private RunCondition createCondition() throws Exception {
148             CompositeCondition result = null;
149             /**
150              * Run through the list of classes implementing RunCondition and
151              * create a new class from it.
152              */
153             for (Class<? extends RunCondition> clazz : conditionTypes) {
154                 if (result == null) {
155                     result = new CompositeCondition();
156                 }
157                 if (isConditionTypeStandalone(clazz)) {
158                     result.add(clazz.getDeclaredConstructor().newInstance());
159                 } else {
160                     result.add(clazz.getDeclaredConstructor(mTestClass).newInstance(mTestClass));
161                 }
162             }
163             return result;
164         }
165 
checkConditionType()166         private void checkConditionType() {
167             for (Class<? extends RunCondition> clazz : conditionTypes) {
168                 if (!isConditionTypeStandalone(clazz) && !isConditionTypeDeclaredInTarget(clazz)) {
169                     String msg
170                             = "Conditional class '%s' is a member class "
171                             + "but was not declared inside the test case using it.\n"
172                             + "Either make this class a static class, "
173                             + "standalone class (by declaring it in it's own file) "
174                             + "or move it inside the test case using it";
175                     throw new IllegalArgumentException(String.format(msg, clazz.getName()));
176                 }
177             }
178         }
179 
isConditionTypeStandalone(Class<? extends RunCondition> clazz)180         private boolean isConditionTypeStandalone(Class<? extends RunCondition> clazz) {
181             return !clazz.isMemberClass()
182                     || Modifier.isStatic(clazz.getModifiers());
183         }
184 
isConditionTypeDeclaredInTarget(Class<? extends RunCondition> clazz)185         private boolean isConditionTypeDeclaredInTarget(Class<? extends RunCondition> clazz) {
186             return mTestClass.getClass().isAssignableFrom(clazz.getDeclaringClass());
187         }
188     }
189 
190     protected static class IgnoreStatement extends Statement {
191 
192         private final RunCondition condition;
193 
IgnoreStatement(RunCondition condition)194         IgnoreStatement(RunCondition condition) {
195             this.condition = condition;
196         }
197 
198         @Override
evaluate()199         public void evaluate() {
200             Assume.assumeTrue("Ignored by " + condition.getClass().getSimpleName(), false);
201         }
202     }
203 }
204