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

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

Merge "Multiple improvements" into main

parents b95729a4 c8dac37e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -189,6 +189,7 @@ public class Instrumentation {
     * @param arguments Any additional arguments that were supplied when the 
     *                  instrumentation was started.
     */
    @android.ravenwood.annotation.RavenwoodKeep
    public void onCreate(Bundle arguments) {
    }

+7 −10
Original line number Diff line number Diff line
@@ -23,13 +23,10 @@ import static org.junit.Assume.assumeTrue;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
import android.platform.test.annotations.internal.InnerRunner;
import android.util.Log;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.ravenwood.common.RavenwoodCommonUtils;

import org.junit.rules.TestRule;
@@ -285,11 +282,6 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase
    private boolean onBefore(Description description, Scope scope, Order order) {
        Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);

        if (scope == Scope.Instance && order == Order.Outer) {
            // Start of a test method.
            mState.enterTestMethod(description);
        }

        final var classDescription = getDescription();

        // Class-level annotations are checked by the runner already, so we only check
@@ -299,6 +291,12 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase
                return false;
            }
        }

        if (scope == Scope.Instance && order == Order.Outer) {
            // Start of a test method.
            mState.enterTestMethod(description);
        }

        return true;
    }

@@ -314,8 +312,7 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase

        if (scope == Scope.Instance && order == Order.Outer) {
            // End of a test method.
            mState.exitTestMethod();

            mState.exitTestMethod(description);
        }

        // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
+5 −2
Original line number Diff line number Diff line
@@ -81,12 +81,15 @@ public final class RavenwoodRunnerState {
        RavenwoodRuntimeEnvironmentController.exitTestClass();
    }

    /** Called when a test method is about to start */
    public void enterTestMethod(Description description) {
        mMethodDescription = description;
        RavenwoodRuntimeEnvironmentController.initForMethod();
        RavenwoodRuntimeEnvironmentController.enterTestMethod(description);
    }

    public void exitTestMethod() {
    /** Called when a test method finishes */
    public void exitTestMethod(Description description) {
        RavenwoodRuntimeEnvironmentController.exitTestMethod(description);
        mMethodDescription = null;
    }

+205 −60
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process_ravenwood;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
@@ -74,6 +75,7 @@ import com.android.ravenwood.common.SneakyThrow;
import com.android.server.LocalServices;
import com.android.server.compat.PlatformCompat;

import org.junit.AssumptionViolatedException;
import org.junit.internal.management.ManagementFactory;
import org.junit.runner.Description;

@@ -81,6 +83,7 @@ import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -93,6 +96,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Responsible for initializing and the environment.
@@ -107,32 +111,60 @@ public class RavenwoodRuntimeEnvironmentController {
    @SuppressWarnings("UnusedVariable")
    private static final PrintStream sStdErr = System.err;

    private static final String MAIN_THREAD_NAME = "RavenwoodMain";
    private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
    private static final String TESTS_THREAD_NAME = "Ravenwood:Test";

    private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
    private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";

    private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
    private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;

    static volatile Thread sTestThread;
    static volatile Thread sMainThread;

    /**
     * When enabled, attempt to dump all thread stacks just before we hit the
     * overall Tradefed timeout, to aid in debugging deadlocks.
     *
     * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
     * It'll merely print stacktraces.
     */
    private static final boolean ENABLE_TIMEOUT_STACKS =
            "1".equals(System.getenv("RAVENWOOD_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"));

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

    static int getTimeoutSeconds() {
        var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
        if (e == null || e.isEmpty()) {
            return DEFAULT_TIMEOUT_SECONDS;
        }
        return Integer.parseInt(e);
    }

    private static final int TIMEOUT_MILLIS = 9_000;

    private static final ScheduledExecutorService sTimeoutExecutor =
            Executors.newScheduledThreadPool(1);
            Executors.newScheduledThreadPool(1, (Runnable r) -> {
                Thread t = Executors.defaultThreadFactory().newThread(r);
                t.setName("Ravenwood:TimeoutMonitor");
                t.setDaemon(true);
                return t;
            });

    private static ScheduledFuture<?> sPendingTimeout;
    private static volatile ScheduledFuture<?> sPendingTimeout;

    /**
     * When enabled, attempt to detect uncaught exceptions from background threads.
     */
    private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
            "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
            !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));

    private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;

    /**
     * When set, an unhandled exception was discovered (typically on a background thread), and we
@@ -141,12 +173,6 @@ public class RavenwoodRuntimeEnvironmentController {
    private static final AtomicReference<Throwable> sPendingUncaughtException =
            new AtomicReference<>();

    private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
            (thread, throwable) -> {
                // Remember the first exception we discover
                sPendingUncaughtException.compareAndSet(null, throwable);
            };

    // TODO: expose packCallingIdentity function in libbinder and use it directly
    // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
    private static long packBinderIdentityToken(
@@ -187,6 +213,8 @@ public class RavenwoodRuntimeEnvironmentController {
     * Initialize the global environment.
     */
    public static void globalInitOnce() {
        sTestThread = Thread.currentThread();
        Thread.currentThread().setName(TESTS_THREAD_NAME);
        synchronized (sInitializationLock) {
            if (!sInitialized) {
                // globalInitOnce() is called from class initializer, which cause
@@ -194,6 +222,7 @@ public class RavenwoodRuntimeEnvironmentController {
                sInitialized = true;

                // This is the first call.
                final long start = System.currentTimeMillis();
                try {
                    globalInitInner();
                } catch (Throwable th) {
@@ -202,6 +231,9 @@ public class RavenwoodRuntimeEnvironmentController {
                    sExceptionFromGlobalInit = th;
                    SneakyThrow.sneakyThrow(th);
                }
                final long end = System.currentTimeMillis();
                // TODO Show user/system time too
                Log.e(TAG, "globalInit() took " + (end - start) + "ms");
            } else {
                // Subsequent calls. If the first call threw, just throw the same error, to prevent
                // the test from running.
@@ -220,7 +252,8 @@ public class RavenwoodRuntimeEnvironmentController {
        RavenwoodCommonUtils.log(TAG, "globalInitInner()");

        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
            Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
            Thread.setDefaultUncaughtExceptionHandler(
                    RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
        }

        // Some process-wide initialization:
@@ -304,6 +337,7 @@ public class RavenwoodRuntimeEnvironmentController {
        ActivityManager.init$ravenwood(SYSTEM.getIdentifier());

        final var main = new HandlerThread(MAIN_THREAD_NAME);
        sMainThread = main;
        main.start();
        Looper.setMainLooperForTest(main.getLooper());

@@ -350,9 +384,20 @@ public class RavenwoodRuntimeEnvironmentController {
        var systemServerContext =
                new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);

        sInstrumentation = new Instrumentation();
        var instArgs = Bundle.EMPTY;
        RavenwoodUtils.runOnMainThreadSync(() -> {
            try {
                // TODO We should get the instrumentation class name from the build file or
                // somewhere.
                var InstClass = Class.forName("android.app.Instrumentation");
                sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
                sInstrumentation.basicInit(instContext, targetContext, null);
        InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY);
                sInstrumentation.onCreate(instArgs);
            } catch (Exception e) {
                SneakyThrow.sneakyThrow(e);
            }
        });
        InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);

        RavenwoodSystemServer.init(systemServerContext);

@@ -399,22 +444,46 @@ public class RavenwoodRuntimeEnvironmentController {

        SystemProperties.clearChangeCallbacksForTest();

        if (ENABLE_TIMEOUT_STACKS) {
            sPendingTimeout = sTimeoutExecutor.schedule(
                    RavenwoodRuntimeEnvironmentController::dumpStacks,
                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
        }
        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
            maybeThrowPendingUncaughtException(false);
        }
        maybeThrowPendingUncaughtException();
    }

    /**
     * Partially reset and initialize before each test method invocation
     * Called when a test method is about to be started.
     */
    public static void initForMethod() {
    public static void enterTestMethod(Description description) {
        // TODO(b/375272444): this is a hacky workaround to ensure binder identity
        Binder.restoreCallingIdentity(sCallingIdentity);

        scheduleTimeout();
    }

    /**
     * Called when a test method finished.
     */
    public static void exitTestMethod(Description description) {
        cancelTimeout();
        maybeThrowPendingUncaughtException();
    }

    private static void scheduleTimeout() {
        if (!ENABLE_TIMEOUT_STACKS) {
            return;
        }
        cancelTimeout();

        sPendingTimeout = sTimeoutExecutor.schedule(
                RavenwoodRuntimeEnvironmentController::onTestTimedOut,
                TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    }

    private static void cancelTimeout() {
        if (!ENABLE_TIMEOUT_STACKS) {
            return;
        }
        var pt = sPendingTimeout;
        if (pt != null) {
            pt.cancel(false);
        }
    }

    private static void initializeCompatIds() {
@@ -473,15 +542,36 @@ public class RavenwoodRuntimeEnvironmentController {
    }

    /**
     * A callback when a test class finishes its execution, mostly only for debugging.
     * Return if an exception is benign and okay to continue running the main looper even
     * if we detect it.
     */
    public static void exitTestClass() {
        if (ENABLE_TIMEOUT_STACKS) {
            sPendingTimeout.cancel(false);
    private static boolean isThrowableBenign(Throwable th) {
        return th instanceof AssertionError || th instanceof AssumptionViolatedException;
    }

    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 && isThrowableBenign(th)) {
                sStdErr.printf("*** Continuing the test because it's %s ***\n",
                        th.getClass().getSimpleName());
                var e = new Exception(desc, th);
                sPendingUncaughtException.compareAndSet(null, e);
                return;
            }
            throw th;
        }
        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
            maybeThrowPendingUncaughtException(true);
    }

    /**
     * A callback when a test class finishes its execution, mostly only for debugging.
     */
    public static void exitTestClass() {
        maybeThrowPendingUncaughtException();
    }

    public static void logTestRunner(String label, Description description) {
@@ -491,35 +581,70 @@ public class RavenwoodRuntimeEnvironmentController {
                + "(" + description.getTestClass().getName() + ")");
    }

    private static void dumpStacks() {
        final PrintStream out = System.err;
        out.println("-----BEGIN ALL THREAD STACKS-----");
        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
            out.println();
            Thread t = stack.getKey();
            out.println(t.toString() + " ID=" + t.getId());
            for (StackTraceElement e : stack.getValue()) {
                out.println("\tat " + e);
    private static void maybeThrowPendingUncaughtException() {
        final Throwable pending = sPendingUncaughtException.getAndSet(null);
        if (pending != null) {
            throw new IllegalStateException("Found an uncaught exception", pending);
        }
    }
        out.println("-----END ALL THREAD STACKS-----");

    /**
     * Prints the stack trace from all threads.
     */
    private static void onTestTimedOut() {
        sStdErr.println("********* SLOW TEST DETECTED ********");
        dumpStacks(null, null);
    }

    private static final Object sDumpStackLock = new Object();

    /**
     * If there's a pending uncaught exception, consume and throw it now. Typically used to
     * report an exception on a background thread as a failure for the currently running test.
     * Prints the stack trace from all threads.
     */
    private static void maybeThrowPendingUncaughtException(boolean duringReset) {
        final Throwable pending = sPendingUncaughtException.getAndSet(null);
        if (pending != null) {
            if (duringReset) {
                throw new IllegalStateException(
                        "Found an uncaught exception during this test", pending);
            } else {
                throw new IllegalStateException(
                        "Found an uncaught exception before this test started", pending);
    private static void dumpStacks(
            @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
        cancelTimeout();
        synchronized (sDumpStackLock) {
            final PrintStream out = sStdErr;
            out.println("-----BEGIN ALL THREAD STACKS-----");

            var stacks = Thread.getAllStackTraces();
            var threads = stacks.keySet().stream().sorted(
                    Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());

            // Put the test and the main thread at the top.
            var testThread = sTestThread;
            var mainThread = sMainThread;
            if (mainThread != null) {
                threads.remove(mainThread);
                threads.add(0, mainThread);
            }
            if (testThread != null) {
                threads.remove(testThread);
                threads.add(0, testThread);
            }
            // Put the exception thread at the top.
            // Also inject the stacktrace from the exception.
            if (exceptionThread != null) {
                threads.remove(exceptionThread);
                threads.add(0, exceptionThread);
                stacks.put(exceptionThread, throwable.getStackTrace());
            }
            for (var th : threads) {
                out.println();

                out.print("Thread");
                if (th == exceptionThread) {
                    out.print(" [** EXCEPTION THREAD **]");
                }
                out.print(": " + th.getName() + " / " + th);
                out.println();

                for (StackTraceElement e :  stacks.get(th)) {
                    out.println("\tat " + e);
                }
            }
            out.println("-----END ALL THREAD STACKS-----");
        }
    }

@@ -545,13 +670,17 @@ public class RavenwoodRuntimeEnvironmentController {
                () -> Class.forName("org.mockito.Matchers"));
    }

    // TODO: use the real UiAutomation class instead of a mock
    private static UiAutomation createMockUiAutomation() {
        sAdoptedPermissions = Collections.emptySet();
        var mock = mock(UiAutomation.class, inv -> {
    static <T> T makeDefaultThrowMock(Class<T> clazz) {
        return mock(clazz, inv -> {
            HostTestUtils.onThrowMethodCalled();
            return null;
        });
    }

    // TODO: use the real UiAutomation class instead of a mock
    private static UiAutomation createMockUiAutomation() {
        sAdoptedPermissions = Collections.emptySet();
        var mock = makeDefaultThrowMock(UiAutomation.class);
        doAnswer(inv -> {
            sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
            return null;
@@ -586,6 +715,23 @@ public class RavenwoodRuntimeEnvironmentController {
        }
    }

    private static void reportUncaughtExceptions(Thread th, Throwable e) {
        sStdErr.printf("Uncaught exception detected: %s: %s\n",
                th, RavenwoodCommonUtils.getStackTraceString(e));

        doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
    }

    private static void doBugreport(
            @Nullable Thread exceptionThread, @Nullable Throwable throwable,
            boolean killSelf) {
        // TODO: Print more information
        dumpStacks(exceptionThread, throwable);
        if (killSelf) {
            System.exit(13);
        }
    }

    private static void dumpJavaProperties() {
        Log.v(TAG, "JVM properties:");
        dumpMap(System.getProperties());
@@ -601,7 +747,6 @@ public class RavenwoodRuntimeEnvironmentController {
            Log.v(TAG, "  " + key + "=" + map.get(key));
        }
    }

    private static void dumpOtherInfo() {
        Log.v(TAG, "Other key information:");
        var jloc = Locale.getDefault();
+45 −38
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -45,6 +46,9 @@ public class RavenwoodSystemProperties {
    /** The default values. */
    static final Map<String, String> sDefaultValues = new HashMap<>();

    static final Set<String> sReadableKeys = new HashSet<>();
    static final Set<String> sWritableKeys = new HashSet<>();

    private static final String[] PARTITIONS = {
            "bootimage",
            "odm",
@@ -88,9 +92,24 @@ public class RavenwoodSystemProperties {
        ravenwoodProps.forEach((key, origValue) -> {
            final String value;

            // If a value starts with "$$$", then this is a reference to the device-side value.
            if (origValue.startsWith("$$$")) {
                // If a value starts with "$$$", then:
                // - If it's "$$$r", the key is allowed to read.
                // - If it's "$$$w", the key is allowed to write.
                // - Otherwise, it's a reference to the device-side value.
                // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded
                // as a prefix match.
                var deviceKey = origValue.substring(3);
                if ("r".equals(deviceKey)) {
                    sReadableKeys.add(key);
                    Log.v(TAG, key + " (readable)");
                    return;
                } else if ("w".equals(deviceKey)) {
                    sWritableKeys.add(key);
                    Log.v(TAG, key + " (writable)");
                    return;
                }

                var deviceValue = deviceProps.get(deviceKey);
                if (deviceValue == null) {
                    throw new RuntimeException("Failed to initialize system properties. Key '"
@@ -131,50 +150,38 @@ public class RavenwoodSystemProperties {
        sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty);
    }

    private static boolean isKeyReadable(String key) {
        // All writable keys are also readable
        if (isKeyWritable(key)) return true;
    private static boolean checkAllowedInner(String key, Set<String> allowed) {
        if (allowed.contains(key)) {
            return true;
        }

        final String root = getKeyRoot(key);
        // Also search for a prefix match.
        for (var k : allowed) {
            if (k.endsWith(".") && key.startsWith(k)) {
                return true;
            }
        }
        return false;
    }

        // This set is carefully curated to help identify situations where a test may
        // accidentally depend on a default value of an obscure property whose owner hasn't
        // decided how Ravenwood should behave.
        if (root.startsWith("boot.")) return true;
        if (root.startsWith("build.")) return true;
        if (root.startsWith("product.")) return true;
        if (root.startsWith("soc.")) return true;
        if (root.startsWith("system.")) return true;
    private static boolean checkAllowed(String key, Set<String> allowed) {
        return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed);
    }

    private static boolean isKeyReadable(String key) {
        // All core values should be readable
        if (sDefaultValues.containsKey(key)) return true;

        // Hardcoded allowlist
        return switch (key) {
            case "gsm.version.baseband",
                 "no.such.thing",
                 "qemu.sf.lcd_density",
                 "ro.bootloader",
                 "ro.hardware",
                 "ro.hw_timeout_multiplier",
                 "ro.odm.build.media_performance_class",
                 "ro.sf.lcd_density",
                 "ro.treble.enabled",
                 "ro.vndk.version",
                 "ro.icu.data.path" -> true;
            default -> false;
        };
        if (sDefaultValues.containsKey(key)) {
            return true;
        }
        if (checkAllowed(key, sReadableKeys)) {
            return true;
        }
        // All writable keys are also readable
        return isKeyWritable(key);
    }

    private static boolean isKeyWritable(String key) {
        final String root = getKeyRoot(key);

        if (root.startsWith("debug.")) return true;

        // For PropertyInvalidatedCache
        if (root.startsWith("cache_key.")) return true;

        return false;
        return checkAllowed(key, sWritableKeys);
    }

    static boolean isKeyAccessible(String key, boolean write) {
Loading