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