xref: /aosp_15_r20/frameworks/base/tests/testables/src/android/testing/TestWithLooperRule.java (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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.rules.MethodRule;
23 import org.junit.runner.RunWith;
24 import org.junit.runners.model.FrameworkMethod;
25 import org.junit.runners.model.Statement;
26 
27 import java.lang.reflect.Field;
28 import java.util.ArrayList;
29 import java.util.List;
30 
31 /*
32  * This rule is meant to be an alternative of using AndroidTestingRunner.
33  * It let tests to start from background thread, and assigns mainLooper or new
34  * Looper for the Statement.
35  */
36 public class TestWithLooperRule implements MethodRule {
37     /*
38      * This rule requires to be the inner most Rule, so the next statement is RunAfters
39      * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)'
40      */
41     @Override
apply(Statement base, FrameworkMethod method, Object target)42     public Statement apply(Statement base, FrameworkMethod method, Object target) {
43 
44         // getting testRunner check, if AndroidTestingRunning then we skip this rule
45         RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class);
46         if (runWithAnnotation != null) {
47             // if AndroidTestingRunner or it's subclass is in use, do nothing
48             if (AndroidTestingRunner.class.isAssignableFrom(runWithAnnotation.value())) {
49                 return base;
50             }
51         }
52 
53         // check if RunWithLooper annotation is used. If not skip this rule
54         RunWithLooper looperAnnotation = method.getAnnotation(RunWithLooper.class);
55         if (looperAnnotation == null) {
56             looperAnnotation = target.getClass().getAnnotation(RunWithLooper.class);
57         }
58         if (looperAnnotation == null) {
59             return base;
60         }
61 
62         try {
63             wrapMethodInStatement(base, method, target);
64         } catch (Exception e) {
65             throw new RuntimeException(e);
66         }
67         return base;
68     }
69 
70     // This method is based on JUnit4 test runner flow. It might need to be revisited when JUnit is
71     // upgraded
72     // TODO(b/277743626): use a cleaner way to wrap each statements; may require some JUnit
73     //  patching to facilitate this.
wrapMethodInStatement(Statement base, FrameworkMethod method, Object target)74     private void wrapMethodInStatement(Statement base, FrameworkMethod method, Object target)
75             throws Exception {
76         Statement next = base;
77         try {
78             while (next != null) {
79                 switch (next.getClass().getSimpleName()) {
80                     case "RunAfters":
81                         this.wrapFieldMethodFor(next, "afters", method, target);
82                         next = getNextStatement(next, "next");
83                         break;
84                     case "RunBefores":
85                         this.wrapFieldMethodFor(next, "befores", method, target);
86                         next = getNextStatement(next, "next");
87                         break;
88                     case "FailOnTimeout":
89                         // Note: withPotentialTimeout() from BlockJUnit4ClassRunner might use
90                         // FailOnTimeout which always wraps a new thread during InvokeMethod
91                         // method evaluation.
92                         next = getNextStatement(next, "originalStatement");
93                         break;
94                     case "InvokeMethod":
95                         this.wrapFieldMethodFor(next, "testMethod", method, target);
96                         return;
97                     case "InvokeParameterizedMethod":
98                         this.wrapFieldMethodFor(next, "frameworkMethod", method, target);
99                         return;
100                     case "ExpectException":
101                         next = this.getNextStatement(next, "next");
102                         break;
103                     case "UiThreadStatement":
104                         next = this.getNextStatement(next, "base");
105                         break;
106                     default:
107                         throw new Exception(
108                                 String.format("Unexpected Statement received: [%s]",
109                                 next.getClass().getName())
110                         );
111                 }
112             }
113         } catch (Exception e) {
114             throw e;
115         }
116     }
117 
118     // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod
119     // within the statement.
wrapFieldMethodFor(Statement base, String fieldStr, FrameworkMethod method, Object target)120     private void wrapFieldMethodFor(Statement base, String fieldStr, FrameworkMethod method,
121             Object target) throws NoSuchFieldException, IllegalAccessException {
122         Field field = base.getClass().getDeclaredField(fieldStr);
123         field.setAccessible(true);
124         Object fieldInstance = field.get(base);
125         if (fieldInstance instanceof FrameworkMethod) {
126             field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance));
127         } else {
128             // Befores and afters methods lists
129             field.set(base, looperWrap(method, target, (List<FrameworkMethod>) fieldInstance));
130         }
131     }
132 
133     // Retrieve the next wrapped statement based on the selected field string
getNextStatement(Statement base, String fieldStr)134     private Statement getNextStatement(Statement base, String fieldStr)
135             throws NoSuchFieldException, IllegalAccessException {
136         Field nextField = base.getClass().getDeclaredField(fieldStr);
137         nextField.setAccessible(true);
138         Object value = nextField.get(base);
139         return value instanceof Statement ? (Statement) value : null;
140     }
141 
looperWrap(FrameworkMethod method, Object test, FrameworkMethod base)142     protected FrameworkMethod looperWrap(FrameworkMethod method, Object test,
143             FrameworkMethod base) {
144         RunWithLooper annotation = method.getAnnotation(RunWithLooper.class);
145         if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class);
146         if (annotation != null) {
147             return LooperFrameworkMethod.get(base, annotation.setAsMainLooper(), test);
148         }
149         return base;
150     }
151 
looperWrap(FrameworkMethod method, Object test, List<FrameworkMethod> methods)152     protected List<FrameworkMethod> looperWrap(FrameworkMethod method, Object test,
153             List<FrameworkMethod> methods) {
154         RunWithLooper annotation = method.getAnnotation(RunWithLooper.class);
155         if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class);
156         if (annotation != null) {
157             methods = new ArrayList<>(methods);
158             for (int i = 0; i < methods.size(); i++) {
159                 methods.set(i, LooperFrameworkMethod.get(methods.get(i),
160                         annotation.setAsMainLooper(), test));
161             }
162         }
163         return methods;
164     }
165 }
166