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

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

[ravenwood] Handle asserts on looper threads gracefully

Assertion failures on the main thread is *usually* recoverable.
So let's catch them without killing the looper, report them as
test failures, but continue running.

Without it, the "recoverable exception" check in RREC is actually
broken because even if we catch the exceptions, the looper would still
quit.

Fix: 417275682
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -s
Flag: EXEMPT host test change only
Change-Id: I2b5af4a847889bd6570fc8146d65bd9383428373
parent 0d188614
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import java.util.Objects;
  *  }</pre>
  */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
@android.ravenwood.annotation.RavenwoodRedirectionClass("Looper_ravenwood")
public final class Looper {
    /*
     * API Implementation Note:
@@ -247,7 +248,7 @@ public final class Looper {
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);
            dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
@@ -308,6 +309,12 @@ public final class Looper {
        return true;
    }

    /** Allow ravenwood to hook any "dispatch". */
    @android.ravenwood.annotation.RavenwoodRedirect
    private static void dispatchMessage(Message msg) {
        msg.target.dispatchMessage(msg);
    }

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
+30 −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 java.util.function.Consumer;

@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class Looper_ravenwood {
    public static volatile Consumer<Message> sDispatcher = (msg) -> {
        // Default implementation
        msg.target.dispatchMessage(msg);
    };

    public static void dispatchMessage(Message msg) {
        sDispatcher.accept(msg);
    }
}
+8 −3
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Looper_ravenwood;
import android.os.Message;
import android.os.Process_ravenwood;
import android.os.ServiceManager;
@@ -133,6 +134,9 @@ public class RavenwoodRuntimeEnvironmentController {
    private static final boolean TOLERATE_LOOPER_ASSERTS =
            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));

    private static final boolean TOLERATE_LOOPER_EXCEPTIONS =
            "1".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_EXCEPTIONS"));

    static final int DEFAULT_TIMEOUT_SECONDS = 10;
    private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;

@@ -360,6 +364,7 @@ public class RavenwoodRuntimeEnvironmentController {
        final var main = new HandlerThread(MAIN_THREAD_NAME);
        sMainThread = main;
        main.start();
        Looper_ravenwood.sDispatcher = RavenwoodRuntimeEnvironmentController::dispatchMessage;
        Looper.setMainLooperForTest(main.getLooper());

        final boolean isSelfInstrumenting =
@@ -606,15 +611,15 @@ public class RavenwoodRuntimeEnvironmentController {
        return outer;
    }

    // TODO: use it to tolerate assert failures on the main thread
    static void dispatchMessage(Message msg) {
    private static void dispatchMessage(Message msg) {
        try {
            msg.getTarget().dispatchMessage(msg);
        } catch (Throwable th) {
            var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
                    Thread.currentThread());
            sStdErr.println(desc);
            if (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th)) {
            if (TOLERATE_LOOPER_EXCEPTIONS
                    || (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th))) {
                sPendingRecoverableUncaughtException.compareAndSet(null,
                        makeRecoverableExceptionInstance(th));
                return;
+20 −16
Original line number Diff line number Diff line
@@ -27,9 +27,14 @@ import org.junit.Test;

import java.util.concurrent.atomic.AtomicReference;

/**
 * Tests related to the main thread.
 *
 * Some tests require $RAVENWOOD_RUN_SLOW_TESTS to set to "1".
 */
public class RavenwoodMainThreadTest {
    private static final boolean RUN_UNSAFE_TESTS =
            "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS"));
    private static final boolean RUN_SLOW_TESTS =
            "1".equals(System.getenv("RAVENWOOD_RUN_SLOW_TESTS"));

    @Test
    public void testRunOnMainThread() {
@@ -44,18 +49,18 @@ public class RavenwoodMainThreadTest {

    /**
     * Sleep a long time on the main thread. This test would then "pass", but Ravenwood
     * should show the stack traces.
     * should show the "SLOW TEST DETECTED" stack traces.
     *
     * This is "unsafe" because this test is slow.
     * This test requires `RAVENWOOD_RUN_SLOW_TESTS=1`.
     */
    @Test
    public void testUnsafeMainThreadHang() {
        assumeTrue(RUN_UNSAFE_TESTS);
    @androidx.test.filters.LargeTest
    public void testMainThreadSlow() {
        assumeTrue(RUN_SLOW_TESTS);

        // The test should time out.
        RavenwoodUtils.runOnMainThreadSync(() -> {
            try {
                Thread.sleep(30_000);
                Thread.sleep(12_000);
            } catch (InterruptedException e) {
                fail("Interrupted");
            }
@@ -63,19 +68,18 @@ public class RavenwoodMainThreadTest {
    }

    /**
     * AssertionError on the main thread would be swallowed and reported "normally".
     * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills
     * the process)
     * runOnMainThreadSync() should report back the inner exception, if any.
     *
     * This is "unsafe" only because this feature can be disabled via the env var.
     * Note this test does _not_ involves "recoverable exception" check in
     * RavenwoodRuntimeEnvironmentController because the exception is caught in side the
     * Runnable that's executed on the main handler. This purely tests runOnMainThreadSync()'s
     * exception propagation.
     */
    @Test
    public void testUnsafeAssertFailureOnMainThread() {
        assumeTrue(RUN_UNSAFE_TESTS);

    public void testRunOnMainThreadSync() {
        assertThrows(AssertionError.class, () -> {
            RavenwoodUtils.runOnMainThreadSync(() -> {
                fail();
                fail("Assertion failure on main thread!");
            });
        });
    }
+39 −0
Original line number Diff line number Diff line
@@ -15,13 +15,20 @@
 */
package com.android.ravenwoodtest.runnercallbacktests;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.NoRavenizer;
import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
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.Assert;
@@ -38,6 +45,7 @@ import org.junit.runners.model.Statement;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
@@ -455,4 +463,35 @@ public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase {
        public void test2() {
        }
    }

    /**
     */
    @RunWith(AndroidJUnit4.class)
    // CHECKSTYLE:OFF
    @Expected("""
    testRunStarted: classes
    testSuiteStarted: classes
    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest
    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest)
    testFailure: Exception detected on thread Ravenwood:Main:  *** Continuing the test because it's recoverable ***
    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest)
    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest
    testSuiteFinished: classes
    testRunFinished: 1,1,0,0
    """)
    // CHECKSTYLE:ON
    public static class MainThreadAssertionFailureTest {
        @Test
        public void test1() {
            var h = new Handler(Looper.getMainLooper());
            h.post(() -> fail("If testMainThreadAssertionFailure() fails with this, that's expected."));
            InstrumentationRegistry.getInstrumentation().waitForIdleSync();

            // The looper should still be alive, so this should work. (if the looper has finished,
            // this would hang.)
            var value = new AtomicInteger(0);
            RavenwoodUtils.runOnMainThreadSync(() -> value.set(1) );
            assertThat(value.get()).isEqualTo(1);
        }
    }
}
Loading