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

Commit f50c2064 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

Merge "Consistent uncaught exception handling" into main

parents 06161d6c 89091e95
Loading
Loading
Loading
Loading
+20 −12
Original line number Diff line number Diff line
@@ -131,11 +131,13 @@ public class RavenwoodRuntimeEnvironmentController {
    private static final boolean ENABLE_TIMEOUT_STACKS =
            !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));

    private static final boolean TOLERATE_LOOPER_ASSERTS =
            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
    /** RavenwoodCoreTest modifies it, so not final. */
    public static volatile boolean TOLERATE_UNHANDLED_ASSERTS =
            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_ASSERTS"));

    private static final boolean TOLERATE_LOOPER_EXCEPTIONS =
            "1".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_EXCEPTIONS"));
    /** RavenwoodCoreTest modifies it, so not final. */
    public static volatile boolean TOLERATE_UNHANDLED_EXCEPTIONS =
            "1".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS"));

    static final int DEFAULT_TIMEOUT_SECONDS = 10;
    private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
@@ -596,16 +598,22 @@ public class RavenwoodRuntimeEnvironmentController {
    }

    /**
     * Return if an exception is benign and okay to continue running the main looper even
     * if we detect it.
     * Return if an exception is benign and okay to continue running the remaining tests.
     */
    private static boolean isThrowableRecoverable(Throwable th) {
        return th instanceof AssertionError || th instanceof AssumptionViolatedException;
        if (TOLERATE_UNHANDLED_EXCEPTIONS) {
            return true;
        }
        if (TOLERATE_UNHANDLED_ASSERTS
                && (th instanceof AssertionError || th instanceof AssumptionViolatedException)) {
            return true;
        }
        return false;
    }

    private static Exception makeRecoverableExceptionInstance(Throwable inner) {
        var outer = new Exception(String.format("Exception detected on thread %s: "
                + " *** Continuing the test because it's recoverable ***",
                + " *** Continuing running the remaining tests ***",
                Thread.currentThread().getName()), inner);
        Log.e(TAG, outer.getMessage(), outer);
        return outer;
@@ -618,8 +626,7 @@ public class RavenwoodRuntimeEnvironmentController {
            var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
                    Thread.currentThread());
            sStdErr.println(desc);
            if (TOLERATE_LOOPER_EXCEPTIONS
                    || (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th))) {
            if (isThrowableRecoverable(th)) {
                sPendingRecoverableUncaughtException.compareAndSet(null,
                        makeRecoverableExceptionInstance(th));
                return;
@@ -767,7 +774,6 @@ public class RavenwoodRuntimeEnvironmentController {
    }

    private static void onUncaughtException(Thread thread, Throwable inner) {

        if (isThrowableRecoverable(inner)) {
            sPendingRecoverableUncaughtException.compareAndSet(null,
                    makeRecoverableExceptionInstance(inner));
@@ -775,7 +781,9 @@ public class RavenwoodRuntimeEnvironmentController {
        }
        var msg = String.format(
                "Uncaught exception detected on thread %s, test=%s:"
                + " %s; Failing all subsequent tests",
                + " %s; Failing all subsequent tests."
                + " (Run with `RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS=1 atest...` to "
                + "force run the subsequent tests)",
                thread, sCurrentDescription, RavenwoodCommonUtils.getStackTraceString(inner));

        var outer = new Exception(msg, inner);
+2 −3
Original line number Diff line number Diff line
@@ -15,8 +15,6 @@
 */
package android.platform.test.ravenwood;

import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
@@ -27,6 +25,7 @@ import com.android.ravenwood.common.SneakyThrow;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

@@ -89,7 +88,7 @@ public class RavenwoodUtils {
            latch.countDown();
        });
        try {
            latch.await();
            latch.await(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while waiting on the Runnable", e);
        }
+0 −39
Original line number Diff line number Diff line
@@ -15,20 +15,13 @@
 */
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;
@@ -45,7 +38,6 @@ 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;
@@ -463,35 +455,4 @@ 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);
        }
    }
}
+164 −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 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.RavenwoodRuntimeEnvironmentController;
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 =
                RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_ASSERTS;
        sOrigTolerateUnhandledExceptions =
                RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_EXCEPTIONS;

        RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_ASSERTS = false;
        RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_EXCEPTIONS = false;
    }

    /** Restore the original TOLERATE_* flags. */
    private static void restoreTolerateFlags() {
        RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_ASSERTS =
                sOrigTolerateUnhandledAsserts;
        RavenwoodRuntimeEnvironmentController.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.
     */
    @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
    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.
            RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_ASSERTS = true;
        }

        @AfterClass
        public static void afterClass() {
            restoreTolerateFlags();
        }

        @Test
        public void test1() throws Exception {
            var h = new Handler(Looper.getMainLooper());
            h.post(() -> fail("failed on the man thread"));

            // If the flag isn't set to true, then the looper would be dead, so don't do it.
            if (RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_ASSERTS) {
                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
                ensureMainThreadAlive();
            } else {
                // waitForIdleSync() won't work, so just wait for a bit...
                Thread.sleep(5_000);
            }
        }
    }

    /**
     * Make sure TOLERATE_UNHANDLED_EXCEPTIONS works.
     */
    @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
    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.
            RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_EXCEPTIONS = true;
        }

        @AfterClass
        public static void afterClass() {
            restoreTolerateFlags();
        }

        @Test
        public void test1() throws Exception {
            var h = new Handler(Looper.getMainLooper());
            h.post(() -> {
                throw new RuntimeException("exception on the man thread");
            });

            // If the flag isn't set to true, then the looper would be dead, so don't do it.
            if (RavenwoodRuntimeEnvironmentController.TOLERATE_UNHANDLED_EXCEPTIONS) {
                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
                ensureMainThreadAlive();
            } else {
                // waitForIdleSync() won't work, so just wait for a bit...
                Thread.sleep(5_000);
            }
        }
    }
}