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

Commit 89091e95 authored by Makoto Onuki's avatar Makoto Onuki
Browse files

Consistent uncaught exception handling

- Moved the TOLERATE_* flag check into the isRecoverableException() method for consistent behavior.

- Also cleaned up the flag and env-var names.
- Added some more tests.

Fix: 33530960
Test: atest RavenwoodCoreTest with manual testing with the TOLERATE_*
    flags disabled.
Flag: EXEMPT host test change only
Change-Id: I6e299081286c3b5b6ebfae661dcd82256f8d43e0
parent 00395891
Loading
Loading
Loading
Loading
+20 −12
Original line number Original line Diff line number Diff line
@@ -131,11 +131,13 @@ public class RavenwoodRuntimeEnvironmentController {
    private static final boolean ENABLE_TIMEOUT_STACKS =
    private static final boolean ENABLE_TIMEOUT_STACKS =
            !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
            !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));


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


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


    static final int DEFAULT_TIMEOUT_SECONDS = 10;
    static final int DEFAULT_TIMEOUT_SECONDS = 10;
    private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
    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
     * Return if an exception is benign and okay to continue running the remaining tests.
     * if we detect it.
     */
     */
    private static boolean isThrowableRecoverable(Throwable th) {
    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) {
    private static Exception makeRecoverableExceptionInstance(Throwable inner) {
        var outer = new Exception(String.format("Exception detected on thread %s: "
        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);
                Thread.currentThread().getName()), inner);
        Log.e(TAG, outer.getMessage(), outer);
        Log.e(TAG, outer.getMessage(), outer);
        return outer;
        return outer;
@@ -618,8 +626,7 @@ public class RavenwoodRuntimeEnvironmentController {
            var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
            var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
                    Thread.currentThread());
                    Thread.currentThread());
            sStdErr.println(desc);
            sStdErr.println(desc);
            if (TOLERATE_LOOPER_EXCEPTIONS
            if (isThrowableRecoverable(th)) {
                    || (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th))) {
                sPendingRecoverableUncaughtException.compareAndSet(null,
                sPendingRecoverableUncaughtException.compareAndSet(null,
                        makeRecoverableExceptionInstance(th));
                        makeRecoverableExceptionInstance(th));
                return;
                return;
@@ -767,7 +774,6 @@ public class RavenwoodRuntimeEnvironmentController {
    }
    }


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

        if (isThrowableRecoverable(inner)) {
        if (isThrowableRecoverable(inner)) {
            sPendingRecoverableUncaughtException.compareAndSet(null,
            sPendingRecoverableUncaughtException.compareAndSet(null,
                    makeRecoverableExceptionInstance(inner));
                    makeRecoverableExceptionInstance(inner));
@@ -775,7 +781,9 @@ public class RavenwoodRuntimeEnvironmentController {
        }
        }
        var msg = String.format(
        var msg = String.format(
                "Uncaught exception detected on thread %s, test=%s:"
                "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));
                thread, sCurrentDescription, RavenwoodCommonUtils.getStackTraceString(inner));


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


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

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


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


@@ -89,7 +88,7 @@ public class RavenwoodUtils {
            latch.countDown();
            latch.countDown();
        });
        });
        try {
        try {
            latch.await();
            latch.await(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while waiting on the Runnable", e);
            throw new RuntimeException("Interrupted while waiting on the Runnable", e);
        }
        }
+0 −39
Original line number Original line Diff line number Diff line
@@ -15,20 +15,13 @@
 */
 */
package com.android.ravenwoodtest.runnercallbacktests;
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 static org.junit.Assume.assumeTrue;


import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.NoRavenizer;
import android.platform.test.annotations.NoRavenizer;
import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
import android.platform.test.ravenwood.RavenwoodUtils;


import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;


import org.junit.AfterClass;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Assert;
@@ -45,7 +38,6 @@ import org.junit.runners.model.Statement;


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


import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
import platform.test.runner.parameterized.Parameters;
@@ -463,35 +455,4 @@ public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase {
        public void test2() {
        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 Original line 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);
            }
        }
    }
}