blob: 99b303e0c43ab62bfe0673c9f4de5ae027121a99 [file] [log] [blame]
/*
* 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.<List<FrameworkMethod>>wrapFieldMethodFor(next,
next.getClass(), "afters", method, target);
next = getNextStatement(next, "next");
break;
case "RunBefores":
this.<List<FrameworkMethod>>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.<FrameworkMethod>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 <T> 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<FrameworkMethod>) 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<FrameworkMethod> looperWrap(FrameworkMethod method, Object test,
List<FrameworkMethod> 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;
}
}