1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.testing;
18 
19 import android.testing.TestableLooper.LooperFrameworkMethod;
20 import android.testing.TestableLooper.RunWithLooper;
21 
22 import org.junit.internal.runners.statements.InvokeMethod;
23 import org.junit.rules.MethodRule;
24 import org.junit.runner.RunWith;
25 import org.junit.runners.model.FrameworkMethod;
26 import org.junit.runners.model.Statement;
27 
28 import java.lang.reflect.Field;
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 /*
33  * This rule is meant to be an alternative of using AndroidTestingRunner.
34  * It let tests to start from background thread, and assigns mainLooper or new
35  * Looper for the Statement.
36  */
37 public class TestWithLooperRule implements MethodRule {
38 
39     /*
40      * This rule requires to be the inner most Rule, so the next statement is RunAfters
41      * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)'
42      */
43     @Override
apply(Statement base, FrameworkMethod method, Object target)44     public Statement apply(Statement base, FrameworkMethod method, Object target) {
45         // getting testRunner check, if AndroidTestingRunning then we skip this rule
46         RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class);
47         if (runWithAnnotation != null) {
48             // if AndroidTestingRunner or it's subclass is in use, do nothing
49             if (AndroidTestingRunner.class.isAssignableFrom(runWithAnnotation.value())) {
50                 return base;
51             }
52         }
53 
54         // check if RunWithLooper annotation is used. If not skip this rule
55         RunWithLooper looperAnnotation = method.getAnnotation(RunWithLooper.class);
56         if (looperAnnotation == null) {
57             looperAnnotation = target.getClass().getAnnotation(RunWithLooper.class);
58         }
59         if (looperAnnotation == null) {
60             return base;
61         }
62 
63         try {
64             wrapMethodInStatement(base, method, target);
65         } catch (Exception e) {
66             throw new RuntimeException(e);
67         }
68         return base;
69     }
70 
71     // This method is based on JUnit4 test runner flow. It might need to be revisited when JUnit is
72     // upgraded
73     // TODO(b/277743626): use a cleaner way to wrap each statements; may require some JUnit
74     //  patching to facilitate this.
wrapMethodInStatement(Statement base, FrameworkMethod method, Object target)75     private void wrapMethodInStatement(Statement base, FrameworkMethod method, Object target)
76             throws Exception {
77         Statement next = base;
78         try {
79             while (next != null) {
80                 switch (next.getClass().getSimpleName()) {
81                     case "RunAfters":
82                         this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
83                                 next.getClass(), "afters", method, target);
84                         next = getNextStatement(next, "next");
85                         break;
86                     case "RunBefores":
87                         this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
88                                 next.getClass(), "befores", method, target);
89                         next = getNextStatement(next, "next");
90                         break;
91                     case "FailOnTimeout":
92                         // Note: withPotentialTimeout() from BlockJUnit4ClassRunner might use
93                         // FailOnTimeout which always wraps a new thread during InvokeMethod
94                         // method evaluation.
95                         next = getNextStatement(next, "originalStatement");
96                         break;
97                     case "InvokeMethod":
98                         this.<FrameworkMethod>wrapFieldMethodFor(next,
99                                 InvokeMethod.class, "testMethod", method, target);
100                         return;
101                     default:
102                         throw new Exception(
103                                 String.format("Unexpected Statement received: [%s]",
104                                 next.getClass().getName())
105                         );
106                 }
107             }
108         } catch (Exception e) {
109             throw e;
110         }
111     }
112 
113     // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod
114     // within the statement.
wrapFieldMethodFor(Statement base, Class<?> targetClass, String fieldStr, FrameworkMethod method, Object target)115     private <T> void wrapFieldMethodFor(Statement base, Class<?> targetClass, String fieldStr,
116             FrameworkMethod method, Object target)
117             throws NoSuchFieldException, IllegalAccessException {
118         Field field = targetClass.getDeclaredField(fieldStr);
119         field.setAccessible(true);
120         T fieldInstance = (T) field.get(base);
121         if (fieldInstance instanceof FrameworkMethod) {
122             field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance));
123         } else {
124             // Befores and afters methods lists
125             field.set(base, looperWrap(method, target, (List<FrameworkMethod>) fieldInstance));
126         }
127     }
128 
129     // Retrieve the next wrapped statement based on the selected field string
getNextStatement(Statement base, String fieldStr)130     private Statement getNextStatement(Statement base, String fieldStr)
131             throws NoSuchFieldException, IllegalAccessException {
132         Field nextField = base.getClass().getDeclaredField(fieldStr);
133         nextField.setAccessible(true);
134         Object value = nextField.get(base);
135         return value instanceof Statement ? (Statement) value : null;
136     }
137 
looperWrap(FrameworkMethod method, Object test, FrameworkMethod base)138     protected FrameworkMethod looperWrap(FrameworkMethod method, Object test,
139             FrameworkMethod base) {
140         RunWithLooper annotation = method.getAnnotation(RunWithLooper.class);
141         if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class);
142         if (annotation != null) {
143             return LooperFrameworkMethod.get(base, annotation.setAsMainLooper(), test);
144         }
145         return base;
146     }
147 
looperWrap(FrameworkMethod method, Object test, List<FrameworkMethod> methods)148     protected List<FrameworkMethod> looperWrap(FrameworkMethod method, Object test,
149             List<FrameworkMethod> methods) {
150         RunWithLooper annotation = method.getAnnotation(RunWithLooper.class);
151         if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class);
152         if (annotation != null) {
153             methods = new ArrayList<>(methods);
154             for (int i = 0; i < methods.size(); i++) {
155                 methods.set(i, LooperFrameworkMethod.get(methods.get(i),
156                         annotation.setAsMainLooper(), test));
157             }
158         }
159         return methods;
160     }
161 }
162