diff options
| author | 2024-01-29 10:55:45 -0700 | |
|---|---|---|
| committer | 2024-01-29 12:38:58 -0700 | |
| commit | c91451a692f7f385b9aeb6783dc735d73656ce50 (patch) | |
| tree | ddedce0ec8ca72b80ecf96163bb34ee0787d6b99 | |
| parent | 7189df1e5e84b2930fea93d0756c65389f64ffd3 (diff) | |
Offer `TestLooperManager` on Ravenwood.
Common test utilities like `TestableLooper` rely on being able to
drive `Looper` interactions via `TestLooperManager` which can only
be obtained via `Instrumentation`.
This change integrates with the common `InstrumentationRegistry`
library which is how most tests obtain their `Instrumentation`
instance. We play an odd dance with `java_host_for_device` and
`java_device_for_host` to get a version of `androidx.test.monitor`
that we can link against our runtime. (The build system otherwise
gets excited that we're trying to compile an Android-linked library
into what appears to be a Java-linked library, but we know that we're
offering the Android runtime.)
Fix subtle bug in `MessageQueue_host` where a requested wake might
race with the `for (;;)` loop in `MessageQueue.next()`; if a wake is
pending, it's a valid reason to return from `nativePollOnce()`.
Add timeout logic to `RavenwoodRuleImpl` which can be enabled to
dump all our stack traces just before Tradefed times out, to aid in
debugging deadlocks; disabled by default.
Bug: 319647875
Test: atest SystemUiRoboTests
Test: atest SystemUiRavenTests
Test: atest FrameworksCoreTestsRavenwood:TestLooperManagerTest
Change-Id: I13605d32df0c3e6758f541d5e75a6bdaf44368db
8 files changed, 188 insertions, 1 deletions
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 2162e3a77f15..68512b8bd771 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -79,6 +79,7 @@ import java.util.concurrent.TimeoutException; * implementation is described to the system through an AndroidManifest.xml's * <instrumentation> tag. */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class Instrumentation { /** @@ -132,6 +133,7 @@ public class Instrumentation { private UiAutomation mUiAutomation; private final Object mAnimationCompleteLock = new Object(); + @android.ravenwood.annotation.RavenwoodKeep public Instrumentation() { } @@ -142,6 +144,7 @@ public class Instrumentation { * reflection, but it will serve as noticeable discouragement from * doing such a thing. */ + @android.ravenwood.annotation.RavenwoodReplace private void checkInstrumenting(String method) { // Check if we have an instrumentation context, as init should only get called by // the system in startup processes that are being instrumented. @@ -151,6 +154,11 @@ public class Instrumentation { } } + private void checkInstrumenting$ravenwood(String method) { + // At the moment, Ravenwood doesn't attach a Context, but we're only ever + // running code as part of tests, so we continue quietly + } + /** * Returns if it is being called in an instrumentation environment. * @@ -2504,6 +2512,7 @@ public class Instrumentation { * Takes control of the execution of messages on the specified looper until * {@link TestLooperManager#release} is called. */ + @android.ravenwood.annotation.RavenwoodKeep public TestLooperManager acquireLooperManager(Looper looper) { checkInstrumenting("acquireLooperManager"); return new TestLooperManager(looper); diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 5e7549fa67d8..4b16c1dce463 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -28,6 +28,7 @@ import java.util.concurrent.LinkedBlockingQueue; * The test code may use {@link #next()} to acquire messages that have been queued to this * {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class TestLooperManager { private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>(); diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index e7b5dff60110..93c2e0e40593 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -121,6 +122,14 @@ public class BundleTest { } @Test + public void testEmpty() throws Exception { + assertNotNull(Bundle.EMPTY); + assertEquals(0, Bundle.EMPTY.size()); + + new Bundle(Bundle.EMPTY); + } + + @Test @IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class) public void testCreateFromParcel() throws Exception { boolean withFd; diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java new file mode 100644 index 000000000000..5959444e49cc --- /dev/null +++ b/core/tests/coretests/src/android/os/TestLooperManagerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public class TestLooperManagerTest { + private static final String TAG = "TestLooperManagerTest"; + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Test + public void testMainThread() throws Exception { + doTest(Looper.getMainLooper()); + } + + @Test + public void testCustomThread() throws Exception { + final HandlerThread thread = new HandlerThread(TAG); + thread.start(); + doTest(thread.getLooper()); + } + + private void doTest(Looper looper) throws Exception { + final TestLooperManager tlm = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper); + + final Handler handler = new Handler(looper); + final CountDownLatch latch = new CountDownLatch(1); + + assertFalse(tlm.hasMessages(handler, null, 42)); + + handler.sendEmptyMessage(42); + handler.post(() -> { + latch.countDown(); + }); + assertTrue(tlm.hasMessages(handler, null, 42)); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + + final Message first = tlm.next(); + assertEquals(42, first.what); + assertNull(first.callback); + tlm.execute(first); + assertFalse(tlm.hasMessages(handler, null, 42)); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + tlm.recycle(first); + + final Message second = tlm.next(); + assertNotNull(second.callback); + tlm.execute(second); + assertFalse(tlm.hasMessages(handler, null, 42)); + assertTrue(latch.await(100, TimeUnit.MILLISECONDS)); + tlm.recycle(second); + + tlm.release(); + } +} diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index e013a3e41896..1ac69f6c4fc8 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -30,6 +30,9 @@ java_library { "junit-src/**/*.java", "junit-impl-src/**/*.java", ], + static_libs: [ + "androidx.test.monitor-for-device", + ], libs: [ "framework-minus-apex.ravenwood", "junit", @@ -61,3 +64,17 @@ java_host_for_device { "core-xml-for-host", ], } + +java_host_for_device { + name: "androidx.test.monitor-for-device", + libs: [ + "androidx.test.monitor-for-host", + ], +} + +java_device_for_host { + name: "androidx.test.monitor-for-host", + libs: [ + "androidx.test.monitor", + ], +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index b3dbcde9d324..a797b1d61b2a 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,12 +16,35 @@ package android.platform.test.ravenwood; +import android.app.Instrumentation; +import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; +import androidx.test.platform.app.InstrumentationRegistry; + +import java.io.PrintStream; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; + /** + * When enabled, attempt to dump all thread stacks just before we hit the + * overall Tradefed timeout, to aid in debugging deadlocks. + */ + private static final boolean ENABLE_TIMEOUT_STACKS = false; + private static final int TIMEOUT_MILLIS = 9_000; + + private static final ScheduledExecutorService sTimeoutExecutor = + Executors.newScheduledThreadPool(1); + + private static ScheduledFuture<?> sPendingTimeout; + public static boolean isOnRavenwood() { return true; } @@ -41,9 +64,22 @@ public class RavenwoodRuleImpl { main.start(); Looper.setMainLooperForTest(main.getLooper()); } + + InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY); + + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks, + TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } } public static void reset(RavenwoodRule rule) { + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout.cancel(false); + } + + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + if (rule.mProvideMainThread) { Looper.getMainLooper().quit(); Looper.clearMainLooperForTest(); @@ -55,4 +91,19 @@ public class RavenwoodRuleImpl { android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); } + + 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); + } + } + out.println("-----END ALL THREAD STACKS-----"); + } } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index eaf01a32592e..b775f9ad47ad 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -72,6 +72,7 @@ android.os.Process android.os.ServiceSpecificException android.os.SystemClock android.os.SystemProperties +android.os.TestLooperManager android.os.ThreadLocalWorkSource android.os.TimestampedValue android.os.Trace @@ -141,6 +142,8 @@ android.graphics.RectF android.content.ContentProvider +android.app.Instrumentation + android.metrics.LogMaker android.view.Display$HdrCapabilities diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java index 2e47d48f4fa0..65da4a144160 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java @@ -28,6 +28,7 @@ public class MessageQueue_host { private final Object mPoller = new Object(); private volatile boolean mPolling; + private volatile boolean mPendingWake; private void validate() { if (mDeleted) { @@ -62,7 +63,9 @@ public class MessageQueue_host { synchronized (q.mPoller) { q.mPolling = true; try { - if (timeoutMillis == 0) { + if (q.mPendingWake) { + // Calling with pending wake returns immediately + } else if (timeoutMillis == 0) { // Calling epoll_wait() with 0 returns immediately } else if (timeoutMillis == -1) { q.mPoller.wait(); @@ -72,6 +75,8 @@ public class MessageQueue_host { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + // Any reason for returning counts as a "wake", so clear pending + q.mPendingWake = false; q.mPolling = false; } } @@ -79,6 +84,7 @@ public class MessageQueue_host { public static void nativeWake(long ptr) { var q = getInstance(ptr); synchronized (q.mPoller) { + q.mPendingWake = true; q.mPoller.notifyAll(); } } |