diff options
author | 2025-02-21 14:13:40 -0800 | |
---|---|---|
committer | 2025-02-24 12:54:47 -0800 | |
commit | c8dac37e3e93fc73df332e1723888344655fe413 (patch) | |
tree | 06185ec61e481981f9921a4ad3710246f0929e5a | |
parent | ca8ecbebc7e95f3dfec071fc68f43f0d388f7b7c (diff) |
Multiple improvements
- Sysprops allowlist now fully moved to the text file.
- Always enable the test timeout w/ improved stack traces
- Add a method to "run on main thread"
- Always enable the uncaught exception handler.
- Tolerate assertion failures on the main looper. (rather than letting
it crash the thread)
- Also some other minor changes needed in my main work.
Flag: EXEMPT host test change only
Bug: 292141694
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
Change-Id: I47b060021ce46b6e9fbe4aca63835745311a4b06
18 files changed, 765 insertions, 124 deletions
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index eb9feb95bf3d..8af5b1bd40f8 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -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) { } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 3ebef02284d6..b6167665c985 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -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. diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java index 70bc52bdaa12..705186edba00 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java @@ -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; } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java index f205d238c693..d935626c34df 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -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(); - sInstrumentation.basicInit(instContext, targetContext, null); - InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY); + 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); + 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 { } /** + * Return if an exception is benign and okay to continue running the main looper even + * if we detect it. + */ + 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; + } + } + + /** * A callback when a test class finishes its execution, mostly only for debugging. */ public static void exitTestClass() { - if (ENABLE_TIMEOUT_STACKS) { - sPendingTimeout.cancel(false); - } - if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - maybeThrowPendingUncaughtException(true); - } + 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-----"); } /** - * 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 onTestTimedOut() { + sStdErr.println("********* SLOW TEST DETECTED ********"); + dumpStacks(null, null); + } + + private static final Object sDumpStackLock = new Object(); + + /** + * Prints the stack trace from all threads. + */ + 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(); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java index 70c161c1f19a..819d93a9c336 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java @@ -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) { diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java index 19c1bffaebcd..3e2c4051b792 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java @@ -15,7 +15,20 @@ */ 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; +import android.os.Looper; + import com.android.ravenwood.common.RavenwoodCommonUtils; +import com.android.ravenwood.common.SneakyThrow; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; /** * Utilities for writing (bivalent) ravenwood tests. @@ -47,4 +60,129 @@ public class RavenwoodUtils { public static void loadJniLibrary(String libname) { RavenwoodCommonUtils.loadJniLibrary(libname); } + + private class MainHandlerHolder { + static Handler sMainHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Returns the main thread handler. + */ + public static Handler getMainHandler() { + return MainHandlerHolder.sMainHandler; + } + + /** + * Run a Callable on Handler and wait for it to complete. + */ + @Nullable + public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) { + var result = new AtomicReference<T>(); + var thrown = new AtomicReference<Throwable>(); + var latch = new CountDownLatch(1); + h.post(() -> { + try { + result.set(c.call()); + } catch (Throwable th) { + thrown.set(th); + } + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while waiting on the Runnable", e); + } + var th = thrown.get(); + if (th != null) { + SneakyThrow.sneakyThrow(th); + } + return result.get(); + } + + + /** + * Run a Runnable on Handler and wait for it to complete. + */ + @Nullable + public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) { + runOnHandlerSync(h, () -> { + r.run(); + return null; + }); + } + + /** + * Run a Callable on main thread and wait for it to complete. + */ + @Nullable + public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) { + return runOnHandlerSync(getMainHandler(), c); + } + + /** + * Run a Runnable on main thread and wait for it to complete. + */ + @Nullable + public static void runOnMainThreadSync(@NonNull Runnable r) { + runOnHandlerSync(getMainHandler(), r); + } + + public static class MockitoHelper { + private MockitoHelper() { + } + + /** + * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different + * method on. (Maybe we should do it in Ravenizer.) + */ + public static void verifyZeroInteractions(Object... mocks) { + if (RavenwoodRule.isOnRavenwood()) { + // Mockito 4 or later + reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class) + .callStatic(new Object[]{mocks}); + } else { + // Mockito 2 + reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class) + .callStatic(new Object[]{mocks}); + } + } + } + + + /** + * Wrap the given {@link Supplier} to become memoized. + * + * The underlying {@link Supplier} will only be invoked once, and that result will be cached + * and returned for any future requests. + */ + static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) { + return new Supplier<>() { + private T mInstance; + + @Override + public T get() { + synchronized (this) { + if (mInstance == null) { + mInstance = create(); + } + return mInstance; + } + } + + private T create() { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + /** Used by {@link #memoize(ThrowingSupplier)} */ + public interface ThrowingSupplier<T> { + /** */ + T get() throws Exception; + } } diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index a967a3fff0d7..893b354d4645 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -26,10 +26,12 @@ import java.io.FileInputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Objects; import java.util.function.Supplier; public class RavenwoodCommonUtils { @@ -329,4 +331,70 @@ public class RavenwoodCommonUtils { public static <T> T withDefault(@Nullable T value, @Nullable T def) { return value != null ? value : def; } + + /** + * Utility for calling a method with reflections. Used to call a method by name. + * Note, this intentionally does _not_ support non-public methods, as we generally + * shouldn't violate java visibility in ravenwood. + * + * @param <TTHIS> class owning the method. + */ + public static class ReflectedMethod<TTHIS> { + private final Class<TTHIS> mThisClass; + private final Method mMethod; + + private ReflectedMethod(Class<TTHIS> thisClass, Method method) { + mThisClass = thisClass; + mMethod = method; + } + + /** Factory method. */ + @SuppressWarnings("unchecked") + public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod( + @NonNull Class<TTHIS> clazz, @NonNull String methodName, + @NonNull Class<?>... argTypes) { + try { + return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** Factory method. */ + @SuppressWarnings("unchecked") + public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod( + @NonNull String className, @NonNull String methodName, + @NonNull Class<?>... argTypes) { + try { + return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** Call the instance method */ + @SuppressWarnings("unchecked") + public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) { + try { + return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** Call the static method */ + @SuppressWarnings("unchecked") + public <RET> RET callStatic(@NonNull Object... args) { + try { + return (RET) mMethod.invoke(null, args); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + /** Handy method to create an array */ + public static <T> T[] arr(@NonNull T... objects) { + return objects; + } } diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java index 7ab9cda378b7..855a4ff21671 100644 --- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java +++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java @@ -21,7 +21,6 @@ import android.util.Log.Level; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.RuntimeInit; import com.android.ravenwood.RavenwoodRuntimeNative; -import com.android.ravenwood.common.RavenwoodCommonUtils; import java.io.PrintStream; import java.text.SimpleDateFormat; @@ -164,7 +163,7 @@ public class Log_ravenwood { * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so * that we don't end up in a recursive loop. */ - private static PrintStream getRealOut() { + public static PrintStream getRealOut() { if (RuntimeInit.sOut$ravenwood != null) { return RuntimeInit.sOut$ravenwood; } else { diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java index eaadac6a8b92..50cfd3bbe863 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java +++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java @@ -57,4 +57,12 @@ public class VMRuntime { public int getTargetSdkVersion() { return RavenwoodRuntimeState.sTargetSdkLevel; } + + /** Ignored on ravenwood. */ + public void registerNativeAllocation(long bytes) { + } + + /** Ignored on ravenwood. */ + public void registerNativeFree(long bytes) { + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java index cf1a5138cbc6..985e00e8641d 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java +++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java @@ -97,6 +97,9 @@ public class NativeAllocationRegistry { if (referent == null) { throw new IllegalArgumentException("referent is null"); } + if (mFreeFunction == 0) { + return () -> {}; // do nothing + } if (nativePtr == 0) { throw new IllegalArgumentException("nativePtr is null"); } diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh index 3e86037d7c7b..8c394f51d8c4 100755 --- a/ravenwood/scripts/add-annotations.sh +++ b/ravenwood/scripts/add-annotations.sh @@ -35,7 +35,7 @@ set -e # We add this line to each methods found. # Note, if we used a single @, that'd be handled as an at file. Use # the double-at instead. -annotation="@@android.platform.test.annotations.DisabledOnRavenwood" +annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")" while getopts "t:" opt; do case "$opt" in t) diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp index 9dd7cc683719..182a7cf3d3de 100644 --- a/ravenwood/tests/coretest/Android.bp +++ b/ravenwood/tests/coretest/Android.bp @@ -33,3 +33,34 @@ android_ravenwood_test { }, auto_gen_config: true, } + +// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib, +// because that modules has too many dependencies and slow to build incrementally. +android_ravenwood_test { + name: "RavenwoodCoreTest-light", + + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "androidx.test.rules", + + // This library should be removed by Ravenizer + "mockito-target-minus-junit4", + ], + libs: [ + // We access internal private classes + "ravenwood-junit-impl", + ], + srcs: [ + "test/**/*.java", + "test/**/*.kt", + ], + + exclude_srcs: [ + "test/com/android/ravenwoodtest/runnercallbacktests/*", + ], + ravenizer: { + strip_mockito: true, + }, + auto_gen_config: true, +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java new file mode 100644 index 000000000000..68387d76b675 --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java @@ -0,0 +1,82 @@ +/* + * 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.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.platform.test.ravenwood.RavenwoodUtils; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +public class RavenwoodMainThreadTest { + private static final boolean RUN_UNSAFE_TESTS = + "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS")); + + @Test + public void testRunOnMainThread() { + AtomicReference<Thread> thr = new AtomicReference<>(); + RavenwoodUtils.runOnMainThreadSync(() -> { + thr.set(Thread.currentThread()); + }); + var th = thr.get(); + assertThat(th).isNotNull(); + assertThat(th).isNotEqualTo(Thread.currentThread()); + } + + /** + * Sleep a long time on the main thread. This test would then "pass", but Ravenwood + * should show the stack traces. + * + * This is "unsafe" because this test is slow. + */ + @Test + public void testUnsafeMainThreadHang() { + assumeTrue(RUN_UNSAFE_TESTS); + + // The test should time out. + RavenwoodUtils.runOnMainThreadSync(() -> { + try { + Thread.sleep(30_000); + } catch (InterruptedException e) { + fail("Interrupted"); + } + }); + } + + /** + * 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) + * + * This is "unsafe" only because this feature can be disabled via the env var. + */ + @Test + public void testUnsafeAssertFailureOnMainThread() { + assumeTrue(RUN_UNSAFE_TESTS); + + assertThrows(AssertionError.class, () -> { + RavenwoodUtils.runOnMainThreadSync(() -> { + fail(); + }); + }); + } +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java new file mode 100644 index 000000000000..421fb50e0c9a --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java @@ -0,0 +1,63 @@ +/* + * 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.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod; + +import org.junit.Test; + +/** + * Tests for {@link ReflectedMethod}. + */ +public class RavenwoodReflectorTest { + /** test target */ + public class Target { + private final int mVar; + + /** test target */ + public Target(int var) { + mVar = var; + } + + /** test target */ + public int foo(int x) { + return x + mVar; + } + + /** test target */ + public static int bar(int x) { + return x + 1; + } + } + + /** Test for a non-static method call */ + @Test + public void testNonStatic() { + var obj = new Target(5); + + var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class); + assertThat((int) m.call(obj, 2)).isEqualTo(7); + } + + /** Test for a static method call */ + @Test + public void testStatic() { + var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class); + assertThat((int) m.callStatic(1)).isEqualTo(2); + } +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java new file mode 100644 index 000000000000..454f5a9576d9 --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java @@ -0,0 +1,65 @@ +/* + * 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.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.os.SystemProperties; + +import org.junit.Test; + +public class RavenwoodSystemPropertiesTest { + @Test + public void testRead() { + assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1"); + } + + @Test + public void testWrite() { + SystemProperties.set("debug.xxx", "5"); + assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5"); + } + + private static void assertException(String expectedMessage, Runnable r) { + try { + r.run(); + fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown"); + } catch (RuntimeException e) { + if (e.getMessage().contains(expectedMessage)) { + return; + } + fail("Excepted exception with message '" + expectedMessage + "' but was '" + + e.getMessage() + "'"); + } + } + + + @Test + public void testReadDisallowed() { + assertException("Read access to system property 'nonexisitent' denied", () -> { + SystemProperties.get("nonexisitent"); + }); + } + + @Test + public void testWriteDisallowed() { + assertException("failed to set system property \"ro.board.first_api_level\" ", () -> { + SystemProperties.set("ro.board.first_api_level", "2"); + }); + } +} diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java index 30abaa2e7d38..b1a40f082656 100644 --- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java +++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java @@ -16,28 +16,27 @@ package com.android.ravenwoodtest; import android.platform.test.annotations.IgnoreUnderRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Assert; -import org.junit.Rule; +import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RavenwoodMinimumTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() - .setProcessApp() - .build(); - @Test public void testSimple() { Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid())); } @Test + public void testAssumeNot() { + Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid())); + } + + @Test @IgnoreUnderRavenwood public void testIgnored() { throw new RuntimeException("Shouldn't be executed under ravenwood"); diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop index 37c50f11f73f..512b459113da 100644 --- a/ravenwood/texts/ravenwood-build.prop +++ b/ravenwood/texts/ravenwood-build.prop @@ -8,9 +8,41 @@ ro.soc.manufacturer=Android ro.soc.model=Ravenwood ro.debuggable=1 -# For the graphics stack -ro.hwui.max_texture_allocation_size=104857600 persist.sys.locale=en-US +ro.product.locale=en-US + +ro.hwui.max_texture_allocation_size=104857600 + +# Allowlist control: +# 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. + +boot.=$$$r +build.=$$$r +product.=$$$r +soc.=$$$r +system.=$$$r +wm.debug.=$$$r +wm.extensions.=$$$r + +gsm.version.baseband=$$$r +no.such.thing=$$$r +qemu.sf.lcd_density=$$$r +ro.bootloader=$$$r +ro.hardware=$$$r +ro.hw_timeout_multiplier=$$$r +ro.odm.build.media_performance_class=$$$r +ro.sf.lcd_density=$$$r +ro.treble.enabled=$$$r +ro.vndk.version=$$$r +ro.icu.data.path=$$$r + +# Writable keys +debug.=$$$w + +# For PropertyInvalidatedCache +cache_key.=$$$w # The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too. # See RavenwoodSystemProperties. diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt index 8fdd3408f74d..64a0e2548e2e 100644 --- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt +++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt @@ -5,7 +5,7 @@ rule com.android.server.pm.pkg.AndroidPackageSplit @0 # Rename all other service internals so that tests can continue to statically # link services code when owners aren't ready to support on Ravenwood -rule com.android.server.** repackaged.@0 +rule com.android.server.** repackaged.services.@0 # TODO: support AIDL generated Parcelables via hoststubgen -rule android.hardware.power.stats.** repackaged.@0 +rule android.hardware.power.stats.** repackaged.services.@0 |