Loading core/java/android/os/Handler.java +12 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,9 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.ravenwood.annotation.RavenwoodRedirect; import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; Loading Loading @@ -66,7 +69,8 @@ import java.lang.reflect.Modifier; * your new thread. The given Runnable or Message will then be scheduled * in the Handler's message queue and processed when appropriate. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass @RavenwoodKeepWholeClass @RavenwoodRedirectionClass("Handler_ravenwood") public class Handler { /* * Set this flag to true to detect anonymous, local or member classes Loading Loading @@ -789,8 +793,15 @@ public class Handler { return sendMessage(msg); } @RavenwoodRedirect private static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // Ravenwood will check for a pending exception, and throw it if any. } private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { onBeforeEnqueue(queue, msg, uptimeMillis); msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); Loading core/java/android/os/Handler_ravenwood.java 0 → 100644 +34 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.os; import android.annotation.NonNull; import com.android.internal.util.function.TriFunction; public class Handler_ravenwood { private Handler_ravenwood() { } public static volatile TriFunction<MessageQueue, Message, Long, Void> sPendingExceptionThrower = (a, b, c) -> null; static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // Check for a pendign exception, and throw it if any. sPendingExceptionThrower.apply(queue, msg, uptimeMillis); } } ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodDriver.java +75 −23 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Environment_ravenwood; import android.os.HandlerThread; import android.os.Handler_ravenwood; import android.os.Looper; import android.os.Looper_ravenwood; import android.os.Message; Loading Loading @@ -133,12 +134,12 @@ public class RavenwoodDriver { !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS")); /** RavenwoodCoreTest modifies it, so not final. */ public static volatile boolean TOLERATE_UNHANDLED_ASSERTS = public static final boolean TOLERATE_UNHANDLED_ASSERTS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_ASSERTS")); /** RavenwoodCoreTest modifies it, so not final. */ public static volatile boolean TOLERATE_UNHANDLED_EXCEPTIONS = "1".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS")); public static final boolean TOLERATE_UNHANDLED_EXCEPTIONS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS")); static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000; Loading @@ -151,7 +152,6 @@ public class RavenwoodDriver { return Integer.parseInt(e); } private static final ScheduledExecutorService sTimeoutExecutor = Executors.newScheduledThreadPool(1, (Runnable r) -> { Thread t = Executors.defaultThreadFactory().newThread(r); Loading @@ -171,7 +171,7 @@ public class RavenwoodDriver { private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = false; /** * This is an "recoverable" uncaught exception from a BG thread. When we detect one, * This is a "recoverable" uncaught exception from a BG thread. When we detect one, * we just make the current test failed, but continue running the subsequent tests normally. */ private static final AtomicReference<Throwable> sPendingRecoverableUncaughtException = Loading Loading @@ -367,10 +367,12 @@ public class RavenwoodDriver { RavenwoodRuntimeState.sPid = sMyPid; RavenwoodRuntimeState.sTargetSdkLevel = sTargetSdkLevel; ServiceManager.init$ravenwood(); LocalServices.removeAllServicesForTest(); ActivityManager.init$ravenwood(SYSTEM.getIdentifier()); RavenwoodUtils.sPendingExceptionThrower = RavenwoodDriver::maybeThrowPendingRecoverableUncaughtExceptionNoClear; Handler_ravenwood.sPendingExceptionThrower = (a, b, c) -> { maybeThrowPendingRecoverableUncaughtExceptionNoClear(); return null; }; final var main = new HandlerThread(MAIN_THREAD_NAME); sMainThread = main; Loading @@ -378,6 +380,12 @@ public class RavenwoodDriver { Looper_ravenwood.sDispatcher = RavenwoodDriver::dispatchMessage; Looper.setMainLooperForTest(main.getLooper()); ServiceManager.init$ravenwood(); LocalServices.removeAllServicesForTest(); ActivityManager.init$ravenwood(SYSTEM.getIdentifier()); final boolean isSelfInstrumenting = Objects.equals(sTestPackageName, sTargetPackageName); Loading Loading @@ -498,7 +506,7 @@ public class RavenwoodDriver { SystemProperties.clearChangeCallbacksForTest(); maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); } /** Loading @@ -523,7 +531,7 @@ public class RavenwoodDriver { */ public static void exitTestMethod(Description description) { cancelTimeout(); maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); maybeThrowUnrecoverableUncaughtExceptionIfDetected(); } Loading Loading @@ -607,6 +615,9 @@ public class RavenwoodDriver { * Return if an exception is benign and okay to continue running the remaining tests. */ private static boolean isThrowableRecoverable(Throwable th) { if (th instanceof RavenwoodRecoverableExceptionWrapper) { return true; } if (TOLERATE_UNHANDLED_EXCEPTIONS) { return true; } Loading @@ -617,15 +628,33 @@ public class RavenwoodDriver { return false; } private static Exception makeRecoverableExceptionInstance(Throwable inner) { var outer = new Exception(String.format("Exception detected on thread %s: " + " *** Continuing running the remaining tests ***", Thread.currentThread().getName()), inner); private static class RavenwoodRecoverableExceptionWrapper extends Exception { RavenwoodRecoverableExceptionWrapper(String message, Throwable cause) { super(message, cause); } @Override public String getMessage() { return super.getMessage() + " : " + getCause().getMessage(); } } private static Throwable makeRecoverableExceptionInstance(Throwable th) { if (th instanceof RavenwoodRecoverableExceptionWrapper) { return th; } var outer = new RavenwoodRecoverableExceptionWrapper( "Exception detected on thread " + Thread.currentThread().getName() + ": " + " *** Continuing running the remaining test ***", th); Log.e(TAG, outer.getMessage(), outer); return outer; } private static void dispatchMessage(Message msg) { // If there's already an exception caught and pending, don't run any more messages. if (hasPendingRecoverableUncaughtException()) { return; } try { msg.getTarget().dispatchMessage(msg); } catch (Throwable th) { Loading @@ -633,8 +662,7 @@ public class RavenwoodDriver { Thread.currentThread()); sStdErr.println(desc); if (isThrowableRecoverable(th)) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); setPendingRecoverableUncaughtException(th); return; } throw th; Loading @@ -642,19 +670,44 @@ public class RavenwoodDriver { } /** * A callback when a test class finishes its execution, mostly only for debugging. * A callback when a test class finishes its execution. */ public static void exitTestClass() { maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); } private static void setPendingRecoverableUncaughtException(Throwable th) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); } private static boolean hasPendingRecoverableUncaughtException() { return sPendingRecoverableUncaughtException.get() != null; } private static void maybeThrowPendingRecoverableUncaughtException() { final Throwable pending = sPendingRecoverableUncaughtException.getAndSet(null); private static Throwable getPendingRecoverableUncaughtException(boolean clear) { if (clear) { return sPendingRecoverableUncaughtException.getAndSet(null); } else { return sPendingRecoverableUncaughtException.get(); } } private static void maybeThrowPendingRecoverableUncaughtException(boolean clear) { final Throwable pending = getPendingRecoverableUncaughtException(clear); if (pending != null) { SneakyThrow.sneakyThrow(pending); } } private static void maybeThrowPendingRecoverableUncaughtExceptionAndClear() { maybeThrowPendingRecoverableUncaughtException(true); } private static void maybeThrowPendingRecoverableUncaughtExceptionNoClear() { maybeThrowPendingRecoverableUncaughtException(false); } /** * Prints the stack trace from all threads. */ Loading Loading @@ -753,8 +806,7 @@ public class RavenwoodDriver { private static void onUncaughtException(Thread thread, Throwable inner) { if (isThrowableRecoverable(inner)) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(inner)); setPendingRecoverableUncaughtException(inner); return; } var msg = String.format( Loading ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +63 −4 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; import android.util.Log; import com.android.ravenwood.common.RavenwoodCommonUtils; import com.android.ravenwood.common.SneakyThrow; Loading @@ -36,6 +38,8 @@ public class RavenwoodUtils { private RavenwoodUtils() { } private static final int DEFAULT_TIMEOUT_SECONDS = 10; /** * Load a JNI library respecting {@code java.library.path} * (which reflects {@code LD_LIBRARY_PATH}). Loading Loading @@ -88,7 +92,7 @@ public class RavenwoodUtils { latch.countDown(); }); try { latch.await(30, TimeUnit.SECONDS); latch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException("Interrupted while waiting on the Runnable", e); } Loading Loading @@ -123,8 +127,57 @@ public class RavenwoodUtils { * Run a Runnable on main thread and wait for it to complete. */ @Nullable public static void runOnMainThreadSync(@NonNull Runnable r) { runOnHandlerSync(getMainHandler(), r); public static void runOnMainThreadSync(@NonNull ThrowingRunnable r) { runOnHandlerSync(getMainHandler(), () -> { r.run(); return null; }); } /** * Set by {@link RavenwoodDriver} to run code before {@link #waitForLooperDone(Looper)}. */ static volatile Runnable sPendingExceptionThrower = () -> {}; /** * Wait for a looper to be idle. * * When running on Ravenwood, this will also throw the pending exception, if any. */ public static void waitForLooperDone(Looper looper) { var idler = new Idler(); looper.getQueue().addIdleHandler(idler); idler.waitForIdle(); sPendingExceptionThrower.run(); } /** * Wait for a looper to be idle. * * When running on Ravenwood, this will also throw the pending exception, if any. */ public static void waitForMainLooperDone() { waitForLooperDone(Looper.getMainLooper()); } private static class Idler implements MessageQueue.IdleHandler { private final CountDownLatch mLatch = new CountDownLatch(1); @Override public boolean queueIdle() { mLatch.countDown(); return false; // One-shot idle handler returns true. } public boolean waitForIdle() { try { return mLatch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.w("Idler", "Interrupted"); return false; } } } /** Loading Loading @@ -157,9 +210,15 @@ public class RavenwoodUtils { }; } /** Used by {@link #memoize(ThrowingSupplier)} */ public interface ThrowingRunnable { /** run the code. */ void run() throws Exception; } /** Used by {@link #memoize(ThrowingSupplier)} */ public interface ThrowingSupplier<T> { /** */ /** run the code. */ T get() throws Exception; } } ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerExecutionTest.java +30 −94 Original line number Diff line number Diff line Loading @@ -15,150 +15,86 @@ */ package com.android.ravenwoodtest.runnercallbacktests; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.NoRavenizer; import android.platform.test.ravenwood.RavenwoodDriver; import android.platform.test.ravenwood.RavenwoodUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import java.util.concurrent.atomic.AtomicInteger; @NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. public class RavenwoodRunnerExecutionTest extends RavenwoodRunnerTestBase { private static boolean sOrigTolerateUnhandledAsserts; private static boolean sOrigTolerateUnhandledExceptions; /** Save the TOLERATE_* flags and set them to false. */ private static void initTolerateFlags() { sOrigTolerateUnhandledAsserts = RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS; sOrigTolerateUnhandledExceptions = RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS; RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = false; RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = false; } /** Restore the original TOLERATE_* flags. */ private static void restoreTolerateFlags() { RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = sOrigTolerateUnhandledAsserts; RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = sOrigTolerateUnhandledExceptions; } private static void ensureMainThreadAlive() { var value = new AtomicInteger(0); RavenwoodUtils.runOnMainThreadSync(() -> value.set(1)); assertThat(value.get()).isEqualTo(1); } /** * Make sure TOLERATE_UNHANDLED_ASSERTS works. * Test around exceptions on handler threads. */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining tests *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining test *** : Intentional exception on the main thread! testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadAssertionFailureTest { @BeforeClass public static void beforeClass() { initTolerateFlags(); // Comment it out to test the false case. RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = true; } @AfterClass public static void afterClass() { restoreTolerateFlags(); } public static class MainThreadExceptionAndwaitForMainLooperDoneTest { @Test public void test1() throws Exception { var h = new Handler(Looper.getMainLooper()); h.post(() -> fail("failed on the man thread")); h.post(() -> { throw new RuntimeException("Intentional exception on the main thread!"); }); // If the flag isn't set to true, then the looper would be dead, so don't do it. if (RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS) { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); ensureMainThreadAlive(); } else { // waitForIdleSync() won't work, so just wait for a bit... Thread.sleep(5_000); } // This will wait for the looper idle, and checks for a pending exception and throws // if any. So the remaining code shouldn't be executed. RavenwoodUtils.waitForMainLooperDone(); fail("Shouldn't reach here"); } } /** * Make sure TOLERATE_UNHANDLED_EXCEPTIONS works. * Test around exceptions on handler threads. */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining tests *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining test *** : Intentional exception on the main thread! testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadRuntimeExceptionTest { @BeforeClass public static void beforeClass() { initTolerateFlags(); // Comment it out to test the false case. RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = true; } @AfterClass public static void afterClass() { restoreTolerateFlags(); } public static class MainThreadExceptionAndPostTest { @Test public void test1() throws Exception { var h = new Handler(Looper.getMainLooper()); h.post(() -> { throw new RuntimeException("exception on the man thread"); throw new RuntimeException("Intentional exception on the main thread!"); }); // If the flag isn't set to true, then the looper would be dead, so don't do it. if (RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS) { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); ensureMainThreadAlive(); } else { // waitForIdleSync() won't work, so just wait for a bit... Thread.sleep(5_000); } // Because the above exception happens first, this message shouldn't be // executed, because before Looper executes each message, we check for a pending // exception and prevents running farther messages. h.post(() -> { setError(new RuntimeException("Shouldn't reach here")); }); RavenwoodUtils.waitForMainLooperDone(); } } } Loading
core/java/android/os/Handler.java +12 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,9 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.ravenwood.annotation.RavenwoodRedirect; import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; Loading Loading @@ -66,7 +69,8 @@ import java.lang.reflect.Modifier; * your new thread. The given Runnable or Message will then be scheduled * in the Handler's message queue and processed when appropriate. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass @RavenwoodKeepWholeClass @RavenwoodRedirectionClass("Handler_ravenwood") public class Handler { /* * Set this flag to true to detect anonymous, local or member classes Loading Loading @@ -789,8 +793,15 @@ public class Handler { return sendMessage(msg); } @RavenwoodRedirect private static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // Ravenwood will check for a pending exception, and throw it if any. } private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { onBeforeEnqueue(queue, msg, uptimeMillis); msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); Loading
core/java/android/os/Handler_ravenwood.java 0 → 100644 +34 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.os; import android.annotation.NonNull; import com.android.internal.util.function.TriFunction; public class Handler_ravenwood { private Handler_ravenwood() { } public static volatile TriFunction<MessageQueue, Message, Long, Void> sPendingExceptionThrower = (a, b, c) -> null; static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // Check for a pendign exception, and throw it if any. sPendingExceptionThrower.apply(queue, msg, uptimeMillis); } }
ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodDriver.java +75 −23 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Environment_ravenwood; import android.os.HandlerThread; import android.os.Handler_ravenwood; import android.os.Looper; import android.os.Looper_ravenwood; import android.os.Message; Loading Loading @@ -133,12 +134,12 @@ public class RavenwoodDriver { !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS")); /** RavenwoodCoreTest modifies it, so not final. */ public static volatile boolean TOLERATE_UNHANDLED_ASSERTS = public static final boolean TOLERATE_UNHANDLED_ASSERTS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_ASSERTS")); /** RavenwoodCoreTest modifies it, so not final. */ public static volatile boolean TOLERATE_UNHANDLED_EXCEPTIONS = "1".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS")); public static final boolean TOLERATE_UNHANDLED_EXCEPTIONS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS")); static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000; Loading @@ -151,7 +152,6 @@ public class RavenwoodDriver { return Integer.parseInt(e); } private static final ScheduledExecutorService sTimeoutExecutor = Executors.newScheduledThreadPool(1, (Runnable r) -> { Thread t = Executors.defaultThreadFactory().newThread(r); Loading @@ -171,7 +171,7 @@ public class RavenwoodDriver { private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = false; /** * This is an "recoverable" uncaught exception from a BG thread. When we detect one, * This is a "recoverable" uncaught exception from a BG thread. When we detect one, * we just make the current test failed, but continue running the subsequent tests normally. */ private static final AtomicReference<Throwable> sPendingRecoverableUncaughtException = Loading Loading @@ -367,10 +367,12 @@ public class RavenwoodDriver { RavenwoodRuntimeState.sPid = sMyPid; RavenwoodRuntimeState.sTargetSdkLevel = sTargetSdkLevel; ServiceManager.init$ravenwood(); LocalServices.removeAllServicesForTest(); ActivityManager.init$ravenwood(SYSTEM.getIdentifier()); RavenwoodUtils.sPendingExceptionThrower = RavenwoodDriver::maybeThrowPendingRecoverableUncaughtExceptionNoClear; Handler_ravenwood.sPendingExceptionThrower = (a, b, c) -> { maybeThrowPendingRecoverableUncaughtExceptionNoClear(); return null; }; final var main = new HandlerThread(MAIN_THREAD_NAME); sMainThread = main; Loading @@ -378,6 +380,12 @@ public class RavenwoodDriver { Looper_ravenwood.sDispatcher = RavenwoodDriver::dispatchMessage; Looper.setMainLooperForTest(main.getLooper()); ServiceManager.init$ravenwood(); LocalServices.removeAllServicesForTest(); ActivityManager.init$ravenwood(SYSTEM.getIdentifier()); final boolean isSelfInstrumenting = Objects.equals(sTestPackageName, sTargetPackageName); Loading Loading @@ -498,7 +506,7 @@ public class RavenwoodDriver { SystemProperties.clearChangeCallbacksForTest(); maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); } /** Loading @@ -523,7 +531,7 @@ public class RavenwoodDriver { */ public static void exitTestMethod(Description description) { cancelTimeout(); maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); maybeThrowUnrecoverableUncaughtExceptionIfDetected(); } Loading Loading @@ -607,6 +615,9 @@ public class RavenwoodDriver { * Return if an exception is benign and okay to continue running the remaining tests. */ private static boolean isThrowableRecoverable(Throwable th) { if (th instanceof RavenwoodRecoverableExceptionWrapper) { return true; } if (TOLERATE_UNHANDLED_EXCEPTIONS) { return true; } Loading @@ -617,15 +628,33 @@ public class RavenwoodDriver { return false; } private static Exception makeRecoverableExceptionInstance(Throwable inner) { var outer = new Exception(String.format("Exception detected on thread %s: " + " *** Continuing running the remaining tests ***", Thread.currentThread().getName()), inner); private static class RavenwoodRecoverableExceptionWrapper extends Exception { RavenwoodRecoverableExceptionWrapper(String message, Throwable cause) { super(message, cause); } @Override public String getMessage() { return super.getMessage() + " : " + getCause().getMessage(); } } private static Throwable makeRecoverableExceptionInstance(Throwable th) { if (th instanceof RavenwoodRecoverableExceptionWrapper) { return th; } var outer = new RavenwoodRecoverableExceptionWrapper( "Exception detected on thread " + Thread.currentThread().getName() + ": " + " *** Continuing running the remaining test ***", th); Log.e(TAG, outer.getMessage(), outer); return outer; } private static void dispatchMessage(Message msg) { // If there's already an exception caught and pending, don't run any more messages. if (hasPendingRecoverableUncaughtException()) { return; } try { msg.getTarget().dispatchMessage(msg); } catch (Throwable th) { Loading @@ -633,8 +662,7 @@ public class RavenwoodDriver { Thread.currentThread()); sStdErr.println(desc); if (isThrowableRecoverable(th)) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); setPendingRecoverableUncaughtException(th); return; } throw th; Loading @@ -642,19 +670,44 @@ public class RavenwoodDriver { } /** * A callback when a test class finishes its execution, mostly only for debugging. * A callback when a test class finishes its execution. */ public static void exitTestClass() { maybeThrowPendingRecoverableUncaughtException(); maybeThrowPendingRecoverableUncaughtExceptionAndClear(); } private static void setPendingRecoverableUncaughtException(Throwable th) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); } private static boolean hasPendingRecoverableUncaughtException() { return sPendingRecoverableUncaughtException.get() != null; } private static void maybeThrowPendingRecoverableUncaughtException() { final Throwable pending = sPendingRecoverableUncaughtException.getAndSet(null); private static Throwable getPendingRecoverableUncaughtException(boolean clear) { if (clear) { return sPendingRecoverableUncaughtException.getAndSet(null); } else { return sPendingRecoverableUncaughtException.get(); } } private static void maybeThrowPendingRecoverableUncaughtException(boolean clear) { final Throwable pending = getPendingRecoverableUncaughtException(clear); if (pending != null) { SneakyThrow.sneakyThrow(pending); } } private static void maybeThrowPendingRecoverableUncaughtExceptionAndClear() { maybeThrowPendingRecoverableUncaughtException(true); } private static void maybeThrowPendingRecoverableUncaughtExceptionNoClear() { maybeThrowPendingRecoverableUncaughtException(false); } /** * Prints the stack trace from all threads. */ Loading Loading @@ -753,8 +806,7 @@ public class RavenwoodDriver { private static void onUncaughtException(Thread thread, Throwable inner) { if (isThrowableRecoverable(inner)) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(inner)); setPendingRecoverableUncaughtException(inner); return; } var msg = String.format( Loading
ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +63 −4 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; import android.util.Log; import com.android.ravenwood.common.RavenwoodCommonUtils; import com.android.ravenwood.common.SneakyThrow; Loading @@ -36,6 +38,8 @@ public class RavenwoodUtils { private RavenwoodUtils() { } private static final int DEFAULT_TIMEOUT_SECONDS = 10; /** * Load a JNI library respecting {@code java.library.path} * (which reflects {@code LD_LIBRARY_PATH}). Loading Loading @@ -88,7 +92,7 @@ public class RavenwoodUtils { latch.countDown(); }); try { latch.await(30, TimeUnit.SECONDS); latch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException("Interrupted while waiting on the Runnable", e); } Loading Loading @@ -123,8 +127,57 @@ public class RavenwoodUtils { * Run a Runnable on main thread and wait for it to complete. */ @Nullable public static void runOnMainThreadSync(@NonNull Runnable r) { runOnHandlerSync(getMainHandler(), r); public static void runOnMainThreadSync(@NonNull ThrowingRunnable r) { runOnHandlerSync(getMainHandler(), () -> { r.run(); return null; }); } /** * Set by {@link RavenwoodDriver} to run code before {@link #waitForLooperDone(Looper)}. */ static volatile Runnable sPendingExceptionThrower = () -> {}; /** * Wait for a looper to be idle. * * When running on Ravenwood, this will also throw the pending exception, if any. */ public static void waitForLooperDone(Looper looper) { var idler = new Idler(); looper.getQueue().addIdleHandler(idler); idler.waitForIdle(); sPendingExceptionThrower.run(); } /** * Wait for a looper to be idle. * * When running on Ravenwood, this will also throw the pending exception, if any. */ public static void waitForMainLooperDone() { waitForLooperDone(Looper.getMainLooper()); } private static class Idler implements MessageQueue.IdleHandler { private final CountDownLatch mLatch = new CountDownLatch(1); @Override public boolean queueIdle() { mLatch.countDown(); return false; // One-shot idle handler returns true. } public boolean waitForIdle() { try { return mLatch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.w("Idler", "Interrupted"); return false; } } } /** Loading Loading @@ -157,9 +210,15 @@ public class RavenwoodUtils { }; } /** Used by {@link #memoize(ThrowingSupplier)} */ public interface ThrowingRunnable { /** run the code. */ void run() throws Exception; } /** Used by {@link #memoize(ThrowingSupplier)} */ public interface ThrowingSupplier<T> { /** */ /** run the code. */ T get() throws Exception; } }
ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerExecutionTest.java +30 −94 Original line number Diff line number Diff line Loading @@ -15,150 +15,86 @@ */ package com.android.ravenwoodtest.runnercallbacktests; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.NoRavenizer; import android.platform.test.ravenwood.RavenwoodDriver; import android.platform.test.ravenwood.RavenwoodUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import java.util.concurrent.atomic.AtomicInteger; @NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. public class RavenwoodRunnerExecutionTest extends RavenwoodRunnerTestBase { private static boolean sOrigTolerateUnhandledAsserts; private static boolean sOrigTolerateUnhandledExceptions; /** Save the TOLERATE_* flags and set them to false. */ private static void initTolerateFlags() { sOrigTolerateUnhandledAsserts = RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS; sOrigTolerateUnhandledExceptions = RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS; RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = false; RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = false; } /** Restore the original TOLERATE_* flags. */ private static void restoreTolerateFlags() { RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = sOrigTolerateUnhandledAsserts; RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = sOrigTolerateUnhandledExceptions; } private static void ensureMainThreadAlive() { var value = new AtomicInteger(0); RavenwoodUtils.runOnMainThreadSync(() -> value.set(1)); assertThat(value.get()).isEqualTo(1); } /** * Make sure TOLERATE_UNHANDLED_ASSERTS works. * Test around exceptions on handler threads. */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining tests *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining test *** : Intentional exception on the main thread! testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadAssertionFailureTest { @BeforeClass public static void beforeClass() { initTolerateFlags(); // Comment it out to test the false case. RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = true; } @AfterClass public static void afterClass() { restoreTolerateFlags(); } public static class MainThreadExceptionAndwaitForMainLooperDoneTest { @Test public void test1() throws Exception { var h = new Handler(Looper.getMainLooper()); h.post(() -> fail("failed on the man thread")); h.post(() -> { throw new RuntimeException("Intentional exception on the main thread!"); }); // If the flag isn't set to true, then the looper would be dead, so don't do it. if (RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS) { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); ensureMainThreadAlive(); } else { // waitForIdleSync() won't work, so just wait for a bit... Thread.sleep(5_000); } // This will wait for the looper idle, and checks for a pending exception and throws // if any. So the remaining code shouldn't be executed. RavenwoodUtils.waitForMainLooperDone(); fail("Shouldn't reach here"); } } /** * Make sure TOLERATE_UNHANDLED_EXCEPTIONS works. * Test around exceptions on handler threads. */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining tests *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing running the remaining test *** : Intentional exception on the main thread! testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadRuntimeExceptionTest { @BeforeClass public static void beforeClass() { initTolerateFlags(); // Comment it out to test the false case. RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = true; } @AfterClass public static void afterClass() { restoreTolerateFlags(); } public static class MainThreadExceptionAndPostTest { @Test public void test1() throws Exception { var h = new Handler(Looper.getMainLooper()); h.post(() -> { throw new RuntimeException("exception on the man thread"); throw new RuntimeException("Intentional exception on the main thread!"); }); // If the flag isn't set to true, then the looper would be dead, so don't do it. if (RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS) { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); ensureMainThreadAlive(); } else { // waitForIdleSync() won't work, so just wait for a bit... Thread.sleep(5_000); } // Because the above exception happens first, this message shouldn't be // executed, because before Looper executes each message, we check for a pending // exception and prevents running farther messages. h.post(() -> { setError(new RuntimeException("Shouldn't reach here")); }); RavenwoodUtils.waitForMainLooperDone(); } } }