Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b1631c5b authored by Makoto Onuki's avatar Makoto Onuki
Browse files

Better handle exceptions from BG threads

- Enable TOLERATE_UNHANDLED_EXCEPTIONS by default
- When we detect an exception from worker threads, we prevent subsequent
  messages from getting executed, until the end of the current test.

- Add RavenwoodUtils.waitForLooperDone() and waitForMainLooperDone(),
  which will wait for a looper to be idle, and then throws the pending
  exception, if any.

Fix: 424511030
Flag: TEST_ONLY
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh

Change-Id: I421e0715f888d2cdd5eac2380da40416cde2cefe
parent ef00818c
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -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;

@@ -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
@@ -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();

+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);
    }
}
+75 −23
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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);
@@ -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 =
@@ -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;
@@ -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);

@@ -498,7 +506,7 @@ public class RavenwoodDriver {

        SystemProperties.clearChangeCallbacksForTest();

        maybeThrowPendingRecoverableUncaughtException();
        maybeThrowPendingRecoverableUncaughtExceptionAndClear();
    }

    /**
@@ -523,7 +531,7 @@ public class RavenwoodDriver {
     */
    public static void exitTestMethod(Description description) {
        cancelTimeout();
        maybeThrowPendingRecoverableUncaughtException();
        maybeThrowPendingRecoverableUncaughtExceptionAndClear();
        maybeThrowUnrecoverableUncaughtExceptionIfDetected();
    }

@@ -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;
        }
@@ -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) {
@@ -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;
@@ -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.
     */
@@ -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(
+63 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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}).
@@ -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);
        }
@@ -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;
            }
        }
    }

    /**
@@ -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;
    }
}
+30 −94
Original line number Diff line number Diff line
@@ -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