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

Commit 4f4f6dbd authored by Jeff DeCew's avatar Jeff DeCew
Browse files

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
parent 705b7cd8
Loading
Loading
Loading
Loading
+29 −18
Original line number Diff line number Diff line
@@ -26,14 +26,10 @@ import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
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.content.pm.UserInfo;
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.broadcast.BroadcastDispatcher;
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.notifcollection.Co
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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
    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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
        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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {

        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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {

        // 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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
        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 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
                    (() -> 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,
+34 −0
Original line number Diff line number Diff line
@@ -128,6 +128,40 @@ public class FakeExecutorTest extends SysuiTestCase {
        assertEquals(3, runnable.mRunCount);
    }

    /**
     * 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.
     */
+163 −0
Original line number Diff line number Diff line
/*
 * 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++
        }
    }
}
+121 −0
Original line number Diff line number Diff line
/*
 * 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])
                        }
                    }
                }
    }
}
+66 −0
Original line number Diff line number Diff line
/*
 * 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")
    }
}