From d94681b39442d7575e0cd381dda330e09bbce48d Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Thu, 24 Nov 2022 15:26:27 +0100 Subject: [PATCH 1/2] Introduce TestScope.collectLastValue(Flow) This CL introduces a helper function to easily collect value from an infinite Flow from a test. See ag/20562448 for an example. Bug: 242040009 Test: atest FooterActionsViewModelTest Change-Id: Ie87220a7382424dc9c25728fca03c601952da5c0 --- .../com/android/systemui/coroutines/Flow.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt new file mode 100644 index 0000000000000..b7a8d2e9f6844 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.coroutines + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent + +/** Collect [flow] in a new [Job] and return a getter for the last collected value. */ +fun TestScope.collectLastValue( + flow: Flow, + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, +): () -> T? { + var lastValue: T? = null + backgroundScope.launch(context, start) { flow.collect { lastValue = it } } + return { + runCurrent() + lastValue + } +} From 09e8750a91ebfd4ac9a77b5fc882536fbdb53374 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Thu, 24 Nov 2022 15:27:36 +0100 Subject: [PATCH 2/2] Clean-up FooterActionsViewModelTest This CL cleans up FooterActionsViewModelTest and FooterActionsTestUtils by making sure that the same TestCoroutineScheduler is used by all TestDispatchers, and by using the new collectLastValue(Flow) API instead of manually collecting each Flow. Bug: 242040009 Test: atest FooterActionsViewModelTest Change-Id: Ia3b1ae8fba05c2ed9cf3a46d3cc7546161092f42 --- .../interactor/FooterActionsInteractorTest.kt | 3 +- .../viewmodel/FooterActionsViewModelTest.kt | 65 +++++-------------- .../qs/footer/FooterActionsTestUtils.kt | 10 +-- 3 files changed, 24 insertions(+), 54 deletions(-) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 645b1cde632f5..23466cc20f449 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestCoroutineScheduler import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +56,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), TestCoroutineScheduler()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 081a2181cfe5c..47afa70fa84b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.FooterActionsTestUtils @@ -44,12 +45,9 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,16 +60,20 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) @RunWithLooper class FooterActionsViewModelTest : SysuiTestCase() { + private val testScope = TestScope() private lateinit var utils: FooterActionsTestUtils - private val testDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()) @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) + } + + private fun runTest(block: suspend TestScope.() -> Unit) { + testScope.runTest(testBody = block) } @Test - fun settingsButton() = runBlockingTest { + fun settingsButton() = runTest { val underTest = utils.footerActionsViewModel(showPowerButton = false) val settings = underTest.settings @@ -87,7 +89,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun powerButton() = runBlockingTest { + fun powerButton() = runTest { // Without power button. val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false) assertThat(underTestWithoutPower.power).isNull() @@ -114,7 +116,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun userSwitcher() = runBlockingTest { + fun userSwitcher() = runTest { val picture: Drawable = mock() val userInfoController = FakeUserInfoController(FakeInfo(picture = picture)) val settings = FakeSettings() @@ -135,7 +137,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { showPowerButton = false, footerActionsInteractor = utils.footerActionsInteractor( - bgDispatcher = testDispatcher, userSwitcherRepository = utils.userSwitcherRepository( userTracker = userTracker, @@ -143,22 +144,12 @@ class FooterActionsViewModelTest : SysuiTestCase() { userManager = userManager, userInfoController = userInfoController, userSwitcherController = userSwitcherControllerWrapper.controller, - bgDispatcher = testDispatcher, ), ) ) // Collect the user switcher into currentUserSwitcher. - var currentUserSwitcher: FooterActionsButtonViewModel? = null - val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } } - fun currentUserSwitcher(): FooterActionsButtonViewModel? { - // Make sure we finish collecting the current user switcher. This is necessary because - // combined flows launch multiple coroutines in the current scope so we need to make - // sure we process all coroutines triggered by our flow collection before we make - // assertions on the current buttons. - advanceUntilIdle() - return currentUserSwitcher - } + val currentUserSwitcher = collectLastValue(underTest.userSwitcher) // The user switcher is disabled. assertThat(currentUserSwitcher()).isNull() @@ -203,12 +194,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { // in guest mode. userInfoController.updateInfo { this.picture = mock() } assertThat(iconTint()).isNull() - - job.cancel() } @Test - fun security() = runBlockingTest { + fun security() = runTest { val securityController = FakeSecurityController() val qsSecurityFooterUtils = mock() @@ -224,22 +213,15 @@ class FooterActionsViewModelTest : SysuiTestCase() { footerActionsInteractor = utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, - bgDispatcher = testDispatcher, securityRepository = utils.securityRepository( securityController = securityController, - bgDispatcher = testDispatcher, ), ), ) // Collect the security model into currentSecurity. - var currentSecurity: FooterActionsSecurityButtonViewModel? = null - val job = launch { underTest.security.collect { currentSecurity = it } } - fun currentSecurity(): FooterActionsSecurityButtonViewModel? { - advanceUntilIdle() - return currentSecurity - } + val currentSecurity = collectLastValue(underTest.security) // By default, we always return a null SecurityButtonConfig. assertThat(currentSecurity()).isNull() @@ -270,12 +252,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { security = currentSecurity() assertThat(security).isNotNull() assertThat(security!!.onClick).isNull() - - job.cancel() } @Test - fun foregroundServices() = runBlockingTest { + fun foregroundServices() = runTest { val securityController = FakeSecurityController() val fgsManagerController = FakeFgsManagerController( @@ -300,21 +280,14 @@ class FooterActionsViewModelTest : SysuiTestCase() { securityRepository = utils.securityRepository( securityController, - bgDispatcher = testDispatcher, ), foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), - bgDispatcher = testDispatcher, ), ) // Collect the security model into currentSecurity. - var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null - val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } } - fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? { - advanceUntilIdle() - return currentForegroundServices - } + val currentForegroundServices = collectLastValue(underTest.foregroundServices) // We don't show the foreground services button if the number of running packages is not // > 1. @@ -356,12 +329,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { } securityController.updateState {} assertThat(currentForegroundServices()?.displayText).isFalse() - - job.cancel() } @Test - fun observeDeviceMonitoringDialogRequests() = runBlockingTest { + fun observeDeviceMonitoringDialogRequests() = runTest { val qsSecurityFooterUtils = mock() val broadcastDispatcher = mock() @@ -390,7 +361,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, broadcastDispatcher = broadcastDispatcher, - bgDispatcher = testDispatcher, ), ) @@ -415,7 +385,4 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onVisibilityChangeRequested(visible = true) assertThat(underTest.isVisible.value).isTrue() } - - private fun runBlockingTest(block: suspend TestScope.() -> Unit) = - runTest(testDispatcher) { block() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 63448e236867c..1a893f8c523c0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -56,7 +56,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler /** * Util class to create real implementations of the FooterActions repositories, viewModel and @@ -65,6 +66,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, + private val scheduler: TestCoroutineScheduler, ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { @@ -105,7 +107,7 @@ class FooterActionsTestUtils( foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), broadcastDispatcher: BroadcastDispatcher = mock(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): FooterActionsInteractor { return FooterActionsInteractorImpl( activityStarter, @@ -126,7 +128,7 @@ class FooterActionsTestUtils( /** Create a [SecurityRepository] to be used in tests. */ fun securityRepository( securityController: SecurityController = FakeSecurityController(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): SecurityRepository { return SecurityRepositoryImpl( securityController, @@ -145,7 +147,7 @@ class FooterActionsTestUtils( fun userSwitcherRepository( @Application context: Context = this.context.applicationContext, bgHandler: Handler = Handler(testableLooper.looper), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), userManager: UserManager = mock(), userTracker: UserTracker = FakeUserTracker(), userSwitcherController: UserSwitcherController = mock(),