• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 package android.platform.test.ravenwood;
17 
18 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
19 import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod;
20 
21 import static org.junit.Assert.fail;
22 import static org.junit.Assume.assumeTrue;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
27 import android.platform.test.annotations.internal.InnerRunner;
28 import android.util.Log;
29 
30 import com.android.ravenwood.common.RavenwoodCommonUtils;
31 
32 import org.junit.rules.TestRule;
33 import org.junit.runner.Description;
34 import org.junit.runner.Runner;
35 import org.junit.runner.manipulation.Filter;
36 import org.junit.runner.manipulation.Filterable;
37 import org.junit.runner.manipulation.NoTestsRemainException;
38 import org.junit.runner.notification.RunNotifier;
39 import org.junit.runners.BlockJUnit4ClassRunner;
40 import org.junit.runners.Suite;
41 import org.junit.runners.model.Statement;
42 import org.junit.runners.model.TestClass;
43 
44 import java.lang.annotation.Annotation;
45 import java.lang.reflect.InvocationTargetException;
46 import java.util.function.BiConsumer;
47 
48 /**
49  * A test runner used for Ravenwood.
50  *
51  * It will delegate to another runner specified with {@link InnerRunner}
52  * (default = {@link BlockJUnit4ClassRunner}) with the following features.
53  * - Add a called before the inner runner gets a chance to run. This can be used to initialize
54  *   stuff used by the inner runner.
55  * - Add hook points with help from the four test rules such as {@link #sImplicitClassOuterRule},
56  *   which are also injected by the ravenizer tool.
57  *
58  * We use this runner to:
59  * - Initialize the Ravenwood environment.
60  * - Handle {@link android.platform.test.annotations.DisabledOnRavenwood}.
61  */
62 public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase {
63     /** Scope of a hook. */
64     public enum Scope {
65         Class,
66         Instance,
67     }
68 
69     /** Order of a hook. */
70     public enum Order {
71         Outer,
72         Inner,
73     }
74 
75     private record HookRule(Scope scope, Order order) implements TestRule {
76         @Override
apply(Statement base, Description description)77         public Statement apply(Statement base, Description description) {
78             return getCurrentRunner().wrapWithHooks(base, description, scope, order);
79         }
80     }
81 
82     // The following four rule instances will be injected to tests by the Ravenizer tool.
83     public static final TestRule sImplicitClassOuterRule = new HookRule(Scope.Class, Order.Outer);
84     public static final TestRule sImplicitClassInnerRule = new HookRule(Scope.Class, Order.Inner);
85     public static final TestRule sImplicitInstOuterRule = new HookRule(Scope.Instance, Order.Outer);
86     public static final TestRule sImplicitInstInnerRule = new HookRule(Scope.Instance, Order.Inner);
87 
88     /** Keeps track of the runner on the current thread. */
89     private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
90 
getCurrentRunner()91     private static RavenwoodAwareTestRunner getCurrentRunner() {
92         var runner = sCurrentRunner.get();
93         if (runner == null) {
94             throw new RuntimeException("Current test runner not set!");
95         }
96         return runner;
97     }
98 
99     final Class<?> mTestJavaClass;
100     private final Runner mRealRunner;
101     private TestClass mTestClass = null;
102 
103     /**
104      * Stores internal states / methods associated with this runner that's only needed in
105      * junit-impl.
106      */
107     final RavenwoodRunnerState mState = new RavenwoodRunnerState(this);
108 
109     /**
110      * Constructor.
111      */
RavenwoodAwareTestRunner(Class<?> testClass)112     public RavenwoodAwareTestRunner(Class<?> testClass) {
113         RavenwoodRuntimeEnvironmentController.globalInitOnce();
114         mTestJavaClass = testClass;
115 
116         /*
117          * If the class has @DisabledOnRavenwood, then we'll delegate to
118          * ClassSkippingTestRunner, which simply skips it.
119          *
120          * We need to do it before instantiating TestClass for b/367694651.
121          */
122         if (!RavenwoodEnablementChecker.shouldRunClassOnRavenwood(testClass, true)) {
123             mRealRunner = new ClassSkippingTestRunner(testClass);
124             return;
125         }
126 
127         mTestClass = new TestClass(testClass);
128 
129         Log.v(TAG, "RavenwoodAwareTestRunner initializing for " + testClass.getCanonicalName());
130 
131         // Hook point to allow more customization.
132         runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null);
133 
134         mRealRunner = instantiateRealRunner(mTestClass);
135 
136         mState.enterTestRunner();
137     }
138 
139     @Override
getRealRunner()140     Runner getRealRunner() {
141         return mRealRunner;
142     }
143 
runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass, Object instance)144     private void runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass,
145             Object instance) {
146         if (RAVENWOOD_VERBOSE_LOGGING) {
147             Log.v(TAG, "runAnnotatedMethodsOnRavenwood() " + annotationClass.getName());
148         }
149 
150         for (var method : mTestClass.getAnnotatedMethods(annotationClass)) {
151             ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null);
152 
153             var methodDesc = method.getDeclaringClass().getName() + "."
154                     + method.getMethod().toString();
155             try {
156                 method.getMethod().invoke(instance);
157             } catch (IllegalAccessException | InvocationTargetException e) {
158                 throw logAndFail("Caught exception while running method " + methodDesc, e);
159             }
160         }
161     }
162 
163     @Override
run(RunNotifier realNotifier)164     public void run(RunNotifier realNotifier) {
165         final var notifier = new RavenwoodRunNotifier(realNotifier);
166         final var description = getDescription();
167 
168         RavenwoodTestStats.getInstance().attachToRunNotifier(notifier);
169 
170         if (mRealRunner instanceof ClassSkippingTestRunner) {
171             Log.v(TAG, "onClassSkipped: description=" + description);
172             mRealRunner.run(notifier);
173             return;
174         }
175 
176         if (RAVENWOOD_VERBOSE_LOGGING) {
177             Log.v(TAG, "Running " + mTestJavaClass.getCanonicalName());
178         }
179         if (RAVENWOOD_VERBOSE_LOGGING) {
180             dumpDescription(description);
181         }
182 
183         // TODO(b/365976974): handle nested classes better
184         final boolean skipRunnerHook =
185                 mRealRunnerTakesRunnerBuilder && mRealRunner instanceof Suite;
186 
187         sCurrentRunner.set(this);
188         try {
189             if (!skipRunnerHook) {
190                 try {
191                     mState.enterTestClass();
192                 } catch (Throwable th) {
193                     notifier.reportBeforeTestFailure(description, th);
194                     return;
195                 }
196             }
197 
198             // Delegate to the inner runner.
199             mRealRunner.run(notifier);
200         } finally {
201             sCurrentRunner.remove();
202 
203             if (!skipRunnerHook) {
204                 try {
205                     mState.exitTestClass();
206                 } catch (Throwable th) {
207                     notifier.reportAfterTestFailure(th);
208                 }
209             }
210         }
211     }
212 
wrapWithHooks(Statement base, Description description, Scope scope, Order order)213     private Statement wrapWithHooks(Statement base, Description description, Scope scope,
214             Order order) {
215         return new Statement() {
216             @Override
217             public void evaluate() throws Throwable {
218                 runWithHooks(description, scope, order, base);
219             }
220         };
221     }
222 
223     private void runWithHooks(Description description, Scope scope, Order order, Statement s)
224             throws Throwable {
225         assumeTrue(onBefore(description, scope, order));
226         try {
227             s.evaluate();
228             onAfter(description, scope, order, null);
229         } catch (Throwable t) {
230             var shouldReportFailure = RavenwoodCommonUtils.runIgnoringException(
231                     () -> onAfter(description, scope, order, t));
232             if (shouldReportFailure == null || shouldReportFailure) {
233                 throw t;
234             }
235         }
236     }
237 
238     /**
239      * A runner that simply skips a class. It still has to support {@link Filterable}
240      * because otherwise the result still says "SKIPPED" even when it's not included in the
241      * filter.
242      */
243     private static class ClassSkippingTestRunner extends Runner implements Filterable {
244         private final Description mDescription;
245         private boolean mFilteredOut;
246 
247         ClassSkippingTestRunner(Class<?> testClass) {
248             mDescription = Description.createTestDescription(testClass, testClass.getSimpleName());
249             mFilteredOut = false;
250         }
251 
252         @Override
253         public Description getDescription() {
254             return mDescription;
255         }
256 
257         @Override
258         public void run(RunNotifier notifier) {
259             if (mFilteredOut) {
260                 return;
261             }
262             notifier.fireTestSuiteStarted(mDescription);
263             notifier.fireTestIgnored(mDescription);
264             notifier.fireTestSuiteFinished(mDescription);
265         }
266 
267         @Override
268         public void filter(Filter filter) throws NoTestsRemainException {
269             if (filter.shouldRun(mDescription)) {
270                 mFilteredOut = false;
271             } else {
272                 throw new NoTestsRemainException();
273             }
274         }
275     }
276 
277     /**
278      * Called before a test / class.
279      *
280      * Return false if it should be skipped.
281      */
282     private boolean onBefore(Description description, Scope scope, Order order) {
283         Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
284 
285         final var classDescription = getDescription();
286 
287         // Class-level annotations are checked by the runner already, so we only check
288         // method-level annotations here.
289         if (scope == Scope.Instance && order == Order.Outer) {
290             if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood(description, true)) {
291                 return false;
292             }
293         }
294 
295         if (scope == Scope.Instance && order == Order.Outer) {
296             // Start of a test method.
297             mState.enterTestMethod(description);
298         }
299 
300         return true;
301     }
302 
303     /**
304      * Called after a test / class.
305      *
306      * Return false if the exception should be ignored.
307      */
308     private boolean onAfter(Description description, Scope scope, Order order, Throwable th) {
309         Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
310 
311         final var classDescription = getDescription();
312 
313         if (scope == Scope.Instance && order == Order.Outer) {
314             // End of a test method.
315             mState.exitTestMethod(description);
316         }
317 
318         // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
319         if (RavenwoodRule.private$ravenwood().isRunningDisabledTests()
320                 && scope == Scope.Instance && order == Order.Outer) {
321 
322             boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood(
323                     description, false);
324             if (th == null) {
325                 // Test passed. Is the test method supposed to be enabled?
326                 if (isTestEnabled) {
327                     // Enabled and didn't throw, okay.
328                     return true;
329                 } else {
330                     // Disabled and didn't throw. We should report it.
331                     fail("Test wasn't included under Ravenwood, but it actually "
332                             + "passed under Ravenwood; consider updating annotations");
333                     return true; // unreachable.
334                 }
335             } else {
336                 // Test failed.
337                 if (isTestEnabled) {
338                     // Enabled but failed. We should throw the exception.
339                     return true;
340                 } else {
341                     // Disabled and failed. Expected. Don't throw.
342                     return false;
343                 }
344             }
345         }
346         return true;
347     }
348 
349     /**
350      * Called by RavenwoodRule.
351      */
352     static void onRavenwoodRuleEnter(Description description, RavenwoodRule rule) {
353         Log.v(TAG, "onRavenwoodRuleEnter: description=" + description);
354         getCurrentRunner().mState.enterRavenwoodRule(rule);
355     }
356 
357     /**
358      * Called by RavenwoodRule.
359      */
360     static void onRavenwoodRuleExit(Description description, RavenwoodRule rule) {
361         Log.v(TAG, "onRavenwoodRuleExit: description=" + description);
362         getCurrentRunner().mState.exitRavenwoodRule(rule);
363     }
364 
365     private void dumpDescription(Description desc) {
366         dumpDescription(desc, "[TestDescription]=", "  ");
367     }
368 
369     private void dumpDescription(Description desc, String header, String indent) {
370         Log.v(TAG, indent + header + desc);
371 
372         var children = desc.getChildren();
373         var childrenIndent = "  " + indent;
374         for (int i = 0; i < children.size(); i++) {
375             dumpDescription(children.get(i), "#" + i + ": ", childrenIndent);
376         }
377     }
378 
379     static volatile BiConsumer<String, Throwable> sCriticalErrorHandler = null;
380 
381     static void onCriticalError(@NonNull String message, @Nullable Throwable th) {
382         Log.e(TAG, "Critical error! " + message, th);
383         var handler = sCriticalErrorHandler;
384         if (handler == null) {
385             Log.e(TAG, "Ravenwood cannot continue. Killing self process.", th);
386             System.exit(1);
387         }
388         handler.accept(message, th);
389     }
390 }
391