Fix nondeterminism in NotificationLockscreenUserManagerTest due to use of a real looper
* Adds a Rule that exposes handling of Log.wtf in a way that is safe for tests
* Adds a mockExecutorHandler method which allows using FakeExecutor when testing classes that take Handler
Fixes: 313999630
Test: atest NotificationLockscreenUserManagerTest
Flag: NA
Change-Id: I3eafe50af9fe26286c955c9aa5aa408142cf1dba
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index ae32142..8bc5e70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -26,14 +26,10 @@
import static android.os.UserHandle.USER_ALL;
import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
-
+import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
-
-import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -57,8 +53,6 @@
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -75,6 +69,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogWtfHandlerRule;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.settings.UserTracker;
@@ -85,11 +80,14 @@
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.settings.FakeSettings;
-
+import com.android.systemui.util.time.FakeSystemClock;
import com.google.android.collect.Lists;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -144,7 +142,11 @@
private NotificationEntry mSecondaryUserNotif;
private NotificationEntry mWorkProfileNotif;
private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
- private Executor mBackgroundExecutor = Runnable::run; // Direct executor
+ private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+ private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
+ private final Executor mMainExecutor = Runnable::run; // Direct executor
+
+ @Rule public final LogWtfHandlerRule wtfHandlerRule = new LogWtfHandlerRule();
@Before
public void setUp() {
@@ -175,7 +177,7 @@
when(mUserManager.getProfilesIncludingCommunal(mSecondaryUser.id)).thenReturn(
Lists.newArrayList(mSecondaryUser, mCommunalUser));
mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
- Handler.createAsync(Looper.myLooper()));
+ mockExecutorHandler(mMainExecutor));
Notification notifWithPrivateVisibility = new Notification();
notifWithPrivateVisibility.visibility = VISIBILITY_PRIVATE;
@@ -209,6 +211,14 @@
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
+
+ mBackgroundExecutor.runAllReady();
+ }
+
+ @After
+ public void tearDown() {
+ // Validate that all tests processed all background posted code
+ assertEquals(0, mBackgroundExecutor.numPending());
}
private void changeSetting(String setting) {
@@ -443,28 +453,28 @@
// first call explicitly sets user 0 to not public; notifies
mLockscreenUserManager.updatePublicMode();
- TestableLooper.get(this).processAllMessages();
+ mBackgroundExecutor.runAllReady();
assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener).onNotificationStateChanged();
clearInvocations(listener);
// calling again has no changes; does not notify
mLockscreenUserManager.updatePublicMode();
- TestableLooper.get(this).processAllMessages();
+ mBackgroundExecutor.runAllReady();
assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener, never()).onNotificationStateChanged();
// Calling again with keyguard now showing makes user 0 public; notifies
when(mKeyguardStateController.isShowing()).thenReturn(true);
mLockscreenUserManager.updatePublicMode();
- TestableLooper.get(this).processAllMessages();
+ mBackgroundExecutor.runAllReady();
assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener).onNotificationStateChanged();
clearInvocations(listener);
// calling again has no changes; does not notify
mLockscreenUserManager.updatePublicMode();
- TestableLooper.get(this).processAllMessages();
+ mBackgroundExecutor.runAllReady();
assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener, never()).onNotificationStateChanged();
}
@@ -742,6 +752,9 @@
intent.putExtra(Intent.EXTRA_USER_HANDLE, newUserId);
broadcastReceiver.onReceive(mContext, intent);
+ // One background task to run which will setup the new user
+ assertEquals(1, mBackgroundExecutor.runAllReady());
+
verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), eq(newUserId));
assertTrue(mLockscreenUserManager.userAllowsNotificationsInPublic(newUserId));
@@ -821,10 +834,8 @@
(() -> mOverviewProxyService),
NotificationLockscreenUserManagerTest.this.mKeyguardManager,
mStatusBarStateController,
- Handler.createAsync(TestableLooper.get(
- NotificationLockscreenUserManagerTest.this).getLooper()),
- Handler.createAsync(TestableLooper.get(
- NotificationLockscreenUserManagerTest.this).getLooper()),
+ mockExecutorHandler(mMainExecutor),
+ mockExecutorHandler(mBackgroundExecutor),
mBackgroundExecutor,
mDeviceProvisionedController,
mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
index 87206c5..31bad2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
@@ -132,6 +132,40 @@
* Test FakeExecutor that is told to delay execution on items.
*/
@Test
+ public void testAtTime() {
+ FakeSystemClock clock = new FakeSystemClock();
+ FakeExecutor fakeExecutor = new FakeExecutor(clock);
+ RunnableImpl runnable = new RunnableImpl();
+
+ // Add three delayed runnables.
+ fakeExecutor.executeAtTime(runnable, 10001);
+ fakeExecutor.executeAtTime(runnable, 10050);
+ fakeExecutor.executeAtTime(runnable, 10100);
+ assertEquals(0, runnable.mRunCount);
+ assertEquals(10000, clock.uptimeMillis());
+ assertEquals(3, fakeExecutor.numPending());
+ // Delayed runnables should not advance the clock and therefore should not run.
+ assertFalse(fakeExecutor.runNextReady());
+ assertEquals(0, fakeExecutor.runAllReady());
+ assertEquals(3, fakeExecutor.numPending());
+
+ // Advance the clock to the next runnable. One runnable should execute.
+ assertEquals(1, fakeExecutor.advanceClockToNext());
+ assertEquals(1, fakeExecutor.runAllReady());
+ assertEquals(2, fakeExecutor.numPending());
+ assertEquals(1, runnable.mRunCount);
+ // Advance the clock to the last runnable.
+ assertEquals(99, fakeExecutor.advanceClockToLast());
+ assertEquals(2, fakeExecutor.runAllReady());
+ // Now all remaining runnables should execute.
+ assertEquals(0, fakeExecutor.numPending());
+ assertEquals(3, runnable.mRunCount);
+ }
+
+ /**
+ * Test FakeExecutor that is told to delay execution on items.
+ */
+ @Test
public void testDelayed_AdvanceAndRun() {
FakeSystemClock clock = new FakeSystemClock();
FakeExecutor fakeExecutor = new FakeExecutor(clock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt
new file mode 100644
index 0000000..d1d2598
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2019 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.systemui.util.concurrency
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MockExecutorHandlerTest : SysuiTestCase() {
+ /** Test FakeExecutor that receives non-delayed items to execute. */
+ @Test
+ fun testNoDelay() {
+ val clock = FakeSystemClock()
+ val fakeExecutor = FakeExecutor(clock)
+ val handler = mockExecutorHandler(fakeExecutor)
+ val runnable = RunnableImpl()
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(0, runnable.mRunCount)
+
+ // Execute two runnables. They should not run and should be left pending.
+ handler.post(runnable)
+ assertEquals(0, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(1, fakeExecutor.numPending())
+ handler.post(runnable)
+ assertEquals(0, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(2, fakeExecutor.numPending())
+
+ // Run one pending runnable.
+ assertTrue(fakeExecutor.runNextReady())
+ assertEquals(1, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(1, fakeExecutor.numPending())
+ // Run a second pending runnable.
+ assertTrue(fakeExecutor.runNextReady())
+ assertEquals(2, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(0, fakeExecutor.numPending())
+
+ // No more runnables to run.
+ assertFalse(fakeExecutor.runNextReady())
+
+ // Add two more runnables.
+ handler.post(runnable)
+ handler.post(runnable)
+ assertEquals(2, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(2, fakeExecutor.numPending())
+ // Execute all pending runnables in batch.
+ assertEquals(2, fakeExecutor.runAllReady())
+ assertEquals(4, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(0, fakeExecutor.runAllReady())
+ }
+
+ /** Test FakeExecutor that is told to delay execution on items. */
+ @Test
+ fun testDelayed() {
+ val clock = FakeSystemClock()
+ val fakeExecutor = FakeExecutor(clock)
+ val handler = mockExecutorHandler(fakeExecutor)
+ val runnable = RunnableImpl()
+
+ // Add three delayed runnables.
+ handler.postDelayed(runnable, 1)
+ handler.postDelayed(runnable, 50)
+ handler.postDelayed(runnable, 100)
+ assertEquals(0, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(3, fakeExecutor.numPending())
+ // Delayed runnables should not advance the clock and therefore should not run.
+ assertFalse(fakeExecutor.runNextReady())
+ assertEquals(0, fakeExecutor.runAllReady())
+ assertEquals(3, fakeExecutor.numPending())
+
+ // Advance the clock to the next runnable. One runnable should execute.
+ assertEquals(1, fakeExecutor.advanceClockToNext())
+ assertEquals(1, fakeExecutor.runAllReady())
+ assertEquals(2, fakeExecutor.numPending())
+ assertEquals(1, runnable.mRunCount)
+ // Advance the clock to the last runnable.
+ assertEquals(99, fakeExecutor.advanceClockToLast())
+ assertEquals(2, fakeExecutor.runAllReady())
+ // Now all remaining runnables should execute.
+ assertEquals(0, fakeExecutor.numPending())
+ assertEquals(3, runnable.mRunCount)
+ }
+
+ /** Test FakeExecutor that is told to delay execution on items. */
+ @Test
+ fun testAtTime() {
+ val clock = FakeSystemClock()
+ val fakeExecutor = FakeExecutor(clock)
+ val handler = mockExecutorHandler(fakeExecutor)
+ val runnable = RunnableImpl()
+
+ // Add three delayed runnables.
+ handler.postAtTime(runnable, 10001)
+ handler.postAtTime(runnable, 10050)
+ handler.postAtTime(runnable, 10100)
+ assertEquals(0, runnable.mRunCount)
+ assertEquals(10000, clock.uptimeMillis())
+ assertEquals(3, fakeExecutor.numPending())
+ // Delayed runnables should not advance the clock and therefore should not run.
+ assertFalse(fakeExecutor.runNextReady())
+ assertEquals(0, fakeExecutor.runAllReady())
+ assertEquals(3, fakeExecutor.numPending())
+
+ // Advance the clock to the next runnable. One runnable should execute.
+ assertEquals(1, fakeExecutor.advanceClockToNext())
+ assertEquals(1, fakeExecutor.runAllReady())
+ assertEquals(2, fakeExecutor.numPending())
+ assertEquals(1, runnable.mRunCount)
+ // Advance the clock to the last runnable.
+ assertEquals(99, fakeExecutor.advanceClockToLast())
+ assertEquals(2, fakeExecutor.runAllReady())
+ // Now all remaining runnables should execute.
+ assertEquals(0, fakeExecutor.numPending())
+ assertEquals(3, runnable.mRunCount)
+ }
+
+ /**
+ * Verifies that `Handler.removeMessages`, which doesn't make sense with executor backing,
+ * causes an error in the test (rather than failing silently like most mocks).
+ */
+ @Test(expected = RuntimeException::class)
+ fun testRemoveMessages_fails() {
+ val clock = FakeSystemClock()
+ val fakeExecutor = FakeExecutor(clock)
+ val handler = mockExecutorHandler(fakeExecutor)
+
+ handler.removeMessages(1)
+ }
+
+ private class RunnableImpl : Runnable {
+ var mRunCount = 0
+ override fun run() {
+ mRunCount++
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
new file mode 100644
index 0000000..e639326
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 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.systemui.log
+
+import android.util.Log
+import android.util.Log.TerribleFailureHandler
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class LogWtfHandlerRule : TestRule {
+
+ private var started = false
+ private var handler = ThrowAndFailAtEnd
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ started = true
+ val originalWtfHandler = Log.setWtfHandler(handler)
+ var failure: Throwable? = null
+ try {
+ base.evaluate()
+ } catch (ex: Throwable) {
+ failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) }
+ } finally {
+ failure = failure.runAndAddSuppressed { handler.onTestFinished() }
+ Log.setWtfHandler(originalWtfHandler)
+ }
+ if (failure != null) {
+ throw failure
+ }
+ }
+ }
+ }
+
+ fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? {
+ try {
+ block()
+ } catch (t: Throwable) {
+ if (this == null) {
+ return t
+ }
+ addSuppressed(t)
+ }
+ return this
+ }
+
+ fun setWtfHandler(handler: TerribleFailureTestHandler) {
+ check(!started) { "Should only be called before the test starts" }
+ this.handler = handler
+ }
+
+ fun interface TerribleFailureTestHandler : TerribleFailureHandler {
+ fun onTestFailure(failure: Throwable) {}
+ fun onTestFinished() {}
+ }
+
+ companion object Handlers {
+ val ThrowAndFailAtEnd
+ get() =
+ object : TerribleFailureTestHandler {
+ val failures = mutableListOf<Log.TerribleFailure>()
+
+ override fun onTerribleFailure(
+ tag: String,
+ what: Log.TerribleFailure,
+ system: Boolean
+ ) {
+ failures.add(what)
+ throw what
+ }
+
+ override fun onTestFailure(failure: Throwable) {
+ super.onTestFailure(failure)
+ }
+
+ override fun onTestFinished() {
+ if (failures.isNotEmpty()) {
+ throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
+ }
+ }
+ }
+
+ val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what }
+
+ val JustFailAtEnd
+ get() =
+ object : TerribleFailureTestHandler {
+ val failures = mutableListOf<Log.TerribleFailure>()
+
+ override fun onTerribleFailure(
+ tag: String,
+ what: Log.TerribleFailure,
+ system: Boolean
+ ) {
+ failures.add(what)
+ }
+
+ override fun onTestFinished() {
+ if (failures.isNotEmpty()) {
+ throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt
new file mode 100644
index 0000000..184d4b5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.systemui.util.concurrency
+
+import android.os.Handler
+import java.util.concurrent.Executor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+
+/**
+ * Wrap an [Executor] in a mock [Handler] that execute when [Handler.post] is called, and throws an
+ * exception otherwise. This is useful when a class requires a Handler only because Handlers are
+ * used by ContentObserver, and no other methods are used.
+ */
+fun mockExecutorHandler(executor: Executor): Handler {
+ val handlerMock = Mockito.mock(Handler::class.java, RuntimeExceptionAnswer())
+ doAnswer { invocation: InvocationOnMock ->
+ executor.execute(invocation.getArgument(0))
+ true
+ }
+ .`when`(handlerMock)
+ .post(any())
+ if (executor is DelayableExecutor) {
+ doAnswer { invocation: InvocationOnMock ->
+ val runnable = invocation.getArgument<Runnable>(0)
+ val uptimeMillis = invocation.getArgument<Long>(1)
+ executor.executeAtTime(runnable, uptimeMillis)
+ true
+ }
+ .`when`(handlerMock)
+ .postAtTime(any(), anyLong())
+ doAnswer { invocation: InvocationOnMock ->
+ val runnable = invocation.getArgument<Runnable>(0)
+ val delayInMillis = invocation.getArgument<Long>(1)
+ executor.executeDelayed(runnable, delayInMillis)
+ true
+ }
+ .`when`(handlerMock)
+ .postDelayed(any(), anyLong())
+ }
+ return handlerMock
+}
+
+private class RuntimeExceptionAnswer : Answer<Any> {
+ override fun answer(invocation: InvocationOnMock): Any {
+ throw RuntimeException(invocation.method.name + " is not stubbed")
+ }
+}