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