/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.testing; import android.testing.TestableLooper.LooperFrameworkMethod; import android.testing.TestableLooper.RunWithLooper; import org.junit.internal.runners.statements.InvokeMethod; import org.junit.rules.MethodRule; import org.junit.runner.RunWith; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; /* * This rule is meant to be an alternative of using AndroidTestingRunner. * It let tests to start from background thread, and assigns mainLooper or new * Looper for the Statement. */ public class TestWithLooperRule implements MethodRule { /* * This rule requires to be the inner most Rule, so the next statement is RunAfters * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)' */ @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { // getting testRunner check, if AndroidTestingRunning then we skip this rule RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class); if (runWithAnnotation != null) { // if AndroidTestingRunner or it's subclass is in use, do nothing if (AndroidTestingRunner.class.isAssignableFrom(runWithAnnotation.value())) { return base; } } // check if RunWithLooper annotation is used. If not skip this rule RunWithLooper looperAnnotation = method.getAnnotation(RunWithLooper.class); if (looperAnnotation == null) { looperAnnotation = target.getClass().getAnnotation(RunWithLooper.class); } if (looperAnnotation == null) { return base; } try { wrapMethodInStatement(base, method, target); } catch (Exception e) { throw new RuntimeException(e); } return base; } // This method is based on JUnit4 test runner flow. It might need to be revisited when JUnit is // upgraded // TODO(b/277743626): use a cleaner way to wrap each statements; may require some JUnit // patching to facilitate this. private void wrapMethodInStatement(Statement base, FrameworkMethod method, Object target) throws Exception { Statement next = base; try { while (next != null) { switch (next.getClass().getSimpleName()) { case "RunAfters": this.>wrapFieldMethodFor(next, next.getClass(), "afters", method, target); next = getNextStatement(next, "next"); break; case "RunBefores": this.>wrapFieldMethodFor(next, next.getClass(), "befores", method, target); next = getNextStatement(next, "next"); break; case "FailOnTimeout": // Note: withPotentialTimeout() from BlockJUnit4ClassRunner might use // FailOnTimeout which always wraps a new thread during InvokeMethod // method evaluation. next = getNextStatement(next, "originalStatement"); break; case "InvokeMethod": this.wrapFieldMethodFor(next, InvokeMethod.class, "testMethod", method, target); return; default: throw new Exception( String.format("Unexpected Statement received: [%s]", next.getClass().getName()) ); } } } catch (Exception e) { throw e; } } // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod // within the statement. private void wrapFieldMethodFor(Statement base, Class targetClass, String fieldStr, FrameworkMethod method, Object target) throws NoSuchFieldException, IllegalAccessException { Field field = targetClass.getDeclaredField(fieldStr); field.setAccessible(true); T fieldInstance = (T) field.get(base); if (fieldInstance instanceof FrameworkMethod) { field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance)); } else { // Befores and afters methods lists field.set(base, looperWrap(method, target, (List) fieldInstance)); } } // Retrieve the next wrapped statement based on the selected field string private Statement getNextStatement(Statement base, String fieldStr) throws NoSuchFieldException, IllegalAccessException { Field nextField = base.getClass().getDeclaredField(fieldStr); nextField.setAccessible(true); Object value = nextField.get(base); return value instanceof Statement ? (Statement) value : null; } protected FrameworkMethod looperWrap(FrameworkMethod method, Object test, FrameworkMethod base) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class); if (annotation != null) { return LooperFrameworkMethod.get(base, annotation.setAsMainLooper(), test); } return base; } protected List looperWrap(FrameworkMethod method, Object test, List methods) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = test.getClass().getAnnotation(RunWithLooper.class); if (annotation != null) { methods = new ArrayList<>(methods); for (int i = 0; i < methods.size(); i++) { methods.set(i, LooperFrameworkMethod.get(methods.get(i), annotation.setAsMainLooper(), test)); } } return methods; } }