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

Commit 52002ea6 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[ravenwood] Handle asserts on looper threads gracefully" into main

parents 5cbb8504 f9f47501
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