From f9a0841244862f5c5495e050be27938d5a5c572e Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 22 Apr 2026 11:13:00 +0200 Subject: [PATCH] tests: fix unit tests --- app/build.gradle | 1 + .../install/updates/UpdatesWorkManager.kt | 63 +++++++++++++++---- .../install/workmanager/InstallWorkManager.kt | 47 +++++++++++--- .../install/updates/UpdatesWorkManagerTest.kt | 13 ++-- .../data/install/updates/UpdatesWorkerTest.kt | 24 ++++--- .../workmanager/InstallWorkManagerTest.kt | 29 +++++---- .../playstore/OauthAuthDataBuilderTest.kt | 6 ++ .../workmanager/InstallOrchestratorTest.kt | 1 + .../ui/search/v2/SearchViewModelV2Test.kt | 10 +++ 9 files changed, 147 insertions(+), 47 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c72a56209..0b9421d18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,7 @@ android { includeAndroidResources = true all { systemProperty 'robolectric.usePreinstrumentedJars', 'false' + maxParallelForks = 1 } } } diff --git a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorkManager.kt b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorkManager.kt index 3b44ca8ff..fc3a177c8 100644 --- a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorkManager.kt @@ -18,6 +18,8 @@ package foundation.e.apps.data.install.updates import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy @@ -45,25 +47,26 @@ object UpdatesWorkManager { } private fun buildOneTimeWorkRequest(): OneTimeWorkRequest { + val spec = buildOneTimeRequestSpec() return OneTimeWorkRequest.Builder(UpdatesWorker::class.java).apply { - setConstraints(WorkRequestConstraints.build(WorkType.UpdateOneTime)) - setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - addTag(TAG_WORK_USER_INITIATED_UPDATE) - }.setInputData( - Data.Builder() - .putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false) - .build() - ).build() + setConstraints(spec.constraints) + if (spec.expedited) { + setExpedited(spec.outOfQuotaPolicy) + } + spec.tags.forEach { addTag(it) } + setInputData(spec.inputData) + }.build() } private fun buildPeriodicWorkRequest(interval: Long): PeriodicWorkRequest { + val spec = buildPeriodicRequestSpec(interval) return PeriodicWorkRequest.Builder( UpdatesWorker::class.java, - interval, + spec.intervalHours, TimeUnit.HOURS ).apply { - setConstraints(WorkRequestConstraints.build(WorkType.UpdatePeriodic)) - addTag(TAG_WORK_PERIODIC_UPDATE) + setConstraints(spec.constraints) + spec.tags.forEach { addTag(it) } }.build() } @@ -80,4 +83,42 @@ object UpdatesWorkManager { ) Timber.i("UpdatesWorker started") } + + @VisibleForTesting + internal data class OneTimeRequestSpec( + val inputData: Data, + val constraints: Constraints, + val tags: Set, + val expedited: Boolean, + val outOfQuotaPolicy: OutOfQuotaPolicy + ) + + @VisibleForTesting + internal data class PeriodicRequestSpec( + val intervalHours: Long, + val constraints: Constraints, + val tags: Set + ) + + @VisibleForTesting + internal fun buildOneTimeRequestSpec(): OneTimeRequestSpec { + return OneTimeRequestSpec( + inputData = Data.Builder() + .putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false) + .build(), + constraints = WorkRequestConstraints.build(WorkType.UpdateOneTime), + tags = setOf(TAG_WORK_USER_INITIATED_UPDATE), + expedited = true, + outOfQuotaPolicy = OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST + ) + } + + @VisibleForTesting + internal fun buildPeriodicRequestSpec(interval: Long): PeriodicRequestSpec { + return PeriodicRequestSpec( + intervalHours = interval, + constraints = WorkRequestConstraints.build(WorkType.UpdatePeriodic), + tags = setOf(TAG_WORK_PERIODIC_UPDATE) + ) + } } diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallWorkManager.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallWorkManager.kt index 27bccf90b..b3944e20c 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallWorkManager.kt @@ -1,6 +1,8 @@ package foundation.e.apps.data.install.workmanager import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder @@ -14,17 +16,16 @@ object InstallWorkManager { const val INSTALL_WORK_NAME = "APP_LOUNGE_INSTALL_APP" fun enqueueWork(context: Context, appInstall: AppInstall, isUpdateWork: Boolean = false): Operation { - val inputData = Data.Builder() - .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, appInstall.id) - .putBoolean(InstallAppWorker.IS_UPDATE_WORK, isUpdateWork) - .build() - + val spec = buildInstallRequestSpec(appInstall, isUpdateWork) val request = OneTimeWorkRequestBuilder() - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .setInputData(inputData) - .addTag(appInstall.id) - .addTag(INSTALL_WORK_NAME) - .setConstraints(WorkRequestConstraints.build(WorkType.InstallOneTime)) + .setInputData(spec.inputData) + .setConstraints(spec.constraints) + .apply { + if (spec.expedited) { + setExpedited(spec.outOfQuotaPolicy) + } + spec.tags.forEach { addTag(it) } + } .build() val operation = WorkManager.getInstance(context) @@ -52,4 +53,30 @@ object InstallWorkManager { } fun getUniqueWorkName(appPackageName: String): String = "${INSTALL_WORK_NAME}/$appPackageName" + + @VisibleForTesting + internal data class InstallRequestSpec( + val inputData: Data, + val constraints: Constraints, + val tags: Set, + val expedited: Boolean, + val outOfQuotaPolicy: OutOfQuotaPolicy + ) + + @VisibleForTesting + internal fun buildInstallRequestSpec( + appInstall: AppInstall, + isUpdateWork: Boolean + ): InstallRequestSpec { + return InstallRequestSpec( + inputData = Data.Builder() + .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, appInstall.id) + .putBoolean(InstallAppWorker.IS_UPDATE_WORK, isUpdateWork) + .build(), + constraints = WorkRequestConstraints.build(WorkType.InstallOneTime), + tags = setOf(appInstall.id, INSTALL_WORK_NAME), + expedited = true, + outOfQuotaPolicy = OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST + ) + } } diff --git a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkManagerTest.kt b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkManagerTest.kt index 065ef7a32..909819eac 100644 --- a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkManagerTest.kt +++ b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkManagerTest.kt @@ -90,7 +90,7 @@ class UpdatesWorkManagerTest { @Test fun enqueueWork_buildsExpectedPeriodicRequest() { - UpdatesWorkManager.enqueueWork(context, interval = 6, ExistingPeriodicWorkPolicy.REPLACE) + UpdatesWorkManager.enqueueWork(context, interval = 6, ExistingPeriodicWorkPolicy.KEEP) val workInfo = getActiveUniqueWorkInfo("updates_work") val workSpec = getWorkSpec(workInfo.id) @@ -105,21 +105,22 @@ class UpdatesWorkManagerTest { fun enqueueWork_respectsExistingPeriodicWorkPolicy() { UpdatesWorkManager.enqueueWork(context, interval = 6, ExistingPeriodicWorkPolicy.KEEP) val initialWorkInfo = getActiveUniqueWorkInfo("updates_work") - val initialWorkSpec = getWorkSpec(initialWorkInfo.id) + val initialSpec = getWorkSpec(initialWorkInfo.id) + assertThat(initialSpec.intervalDuration).isEqualTo(TimeUnit.HOURS.toMillis(6)) UpdatesWorkManager.enqueueWork(context, interval = 12, ExistingPeriodicWorkPolicy.KEEP) val keptWorkInfo = getActiveUniqueWorkInfo("updates_work") - val keptWorkSpec = getWorkSpec(keptWorkInfo.id) + val keptSpec = getWorkSpec(keptWorkInfo.id) assertThat(keptWorkInfo.id).isEqualTo(initialWorkInfo.id) - assertThat(keptWorkSpec.intervalDuration).isEqualTo(initialWorkSpec.intervalDuration) + assertThat(keptSpec.intervalDuration).isEqualTo(TimeUnit.HOURS.toMillis(6)) UpdatesWorkManager.enqueueWork(context, interval = 24, ExistingPeriodicWorkPolicy.REPLACE) val replacedWorkInfo = getActiveUniqueWorkInfo("updates_work") - val replacedWorkSpec = getWorkSpec(replacedWorkInfo.id) + val replacedSpec = getWorkSpec(replacedWorkInfo.id) assertThat(replacedWorkInfo.id).isNotEqualTo(initialWorkInfo.id) - assertThat(replacedWorkSpec.intervalDuration).isEqualTo(TimeUnit.HOURS.toMillis(24)) + assertThat(replacedSpec.intervalDuration).isEqualTo(TimeUnit.HOURS.toMillis(24)) } private fun getActiveUniqueWorkInfo(uniqueWorkName: String): WorkInfo { diff --git a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt index 4376c7dfa..933ef0aac 100644 --- a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt +++ b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt @@ -296,8 +296,12 @@ class UpdatesWorkerTest { systemAppsUpdatesRepository ) - val response = worker.checkManualUpdateRunning() - assertFalse(response) + try { + val response = worker.checkManualUpdateRunning() + assertFalse(response) + } finally { + WorkManager.getInstance(appContext).cancelAllWork().result.get() + } } @Test @@ -398,13 +402,17 @@ class UpdatesWorkerTest { systemAppsUpdatesRepository ) - val result = worker.doWork() + try { + val result = worker.doWork() - assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success()) - verify(blockedAppRepository, never()).fetchUpdateOfAppWarningList() - verify(systemAppsUpdatesRepository, never()).fetchUpdatableSystemApps(true) - verify(updatesManagerRepository, never()).getUpdates() - verify(updatesManagerRepository, never()).getUpdatesOSS() + assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success()) + verify(blockedAppRepository, never()).fetchUpdateOfAppWarningList() + verify(systemAppsUpdatesRepository, never()).fetchUpdatableSystemApps(true) + verify(updatesManagerRepository, never()).getUpdates() + verify(updatesManagerRepository, never()).getUpdatesOSS() + } finally { + WorkManager.getInstance(appContext).cancelAllWork().result.get() + } } @Test diff --git a/app/src/test/java/foundation/e/apps/data/install/workmanager/InstallWorkManagerTest.kt b/app/src/test/java/foundation/e/apps/data/install/workmanager/InstallWorkManagerTest.kt index 422ca6804..490d343c8 100644 --- a/app/src/test/java/foundation/e/apps/data/install/workmanager/InstallWorkManagerTest.kt +++ b/app/src/test/java/foundation/e/apps/data/install/workmanager/InstallWorkManagerTest.kt @@ -24,6 +24,7 @@ import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.Worker @@ -67,10 +68,8 @@ class InstallWorkManagerTest { fun enqueueWork_buildsExpectedRequestForInstall() { val appInstall = AppInstall(id = "id.install", packageName = "foundation.e.install") - InstallWorkManager.enqueueWork(context, appInstall).result.get() - - val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) - val workInfo = workManager.getWorkInfosForUniqueWork(uniqueWorkName).get().single() + InstallWorkManager.enqueueWork(context, appInstall, isUpdateWork = false).result.get() + val workInfo = getActiveUniqueWorkInfo(InstallWorkManager.getUniqueWorkName(appInstall.packageName)) val workSpec = getWorkSpec(workInfo.id) assertThat(workInfo.tags).containsAtLeast(appInstall.id, InstallWorkManager.INSTALL_WORK_NAME) @@ -79,6 +78,9 @@ class InstallWorkManagerTest { assertThat(workSpec.input.getBoolean(InstallAppWorker.IS_UPDATE_WORK, true)).isFalse() assertThat(workSpec.constraints.requiredNetworkType).isEqualTo(NetworkType.CONNECTED) assertThat(workSpec.constraints.requiresStorageNotLow()).isTrue() + assertThat(workSpec.expedited).isTrue() + assertThat(workSpec.outOfQuotaPolicy) + .isEqualTo(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) } @Test @@ -86,9 +88,7 @@ class InstallWorkManagerTest { val appInstall = AppInstall(id = "id.update", packageName = "foundation.e.update") InstallWorkManager.enqueueWork(context, appInstall, isUpdateWork = true).result.get() - - val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) - val workInfo = workManager.getWorkInfosForUniqueWork(uniqueWorkName).get().single() + val workInfo = getActiveUniqueWorkInfo(InstallWorkManager.getUniqueWorkName(appInstall.packageName)) val workSpec = getWorkSpec(workInfo.id) assertThat(workSpec.input.getBoolean(InstallAppWorker.IS_UPDATE_WORK, false)).isTrue() @@ -161,11 +161,6 @@ class InstallWorkManagerTest { .isEqualTo("${InstallWorkManager.INSTALL_WORK_NAME}/") } - private fun getWorkSpec(workId: UUID): WorkSpec { - val workManagerImpl = WorkManagerImpl.getInstance(context) - return requireNotNull(workManagerImpl.workDatabase.workSpecDao().getWorkSpec(workId.toString())) - } - private fun waitUntilFinished(workId: UUID): WorkInfo.State { repeat(30) { val state = requireNotNull(workManager.getWorkInfoById(workId).get()).state @@ -177,6 +172,16 @@ class InstallWorkManagerTest { return requireNotNull(workManager.getWorkInfoById(workId).get()).state } + private fun getActiveUniqueWorkInfo(uniqueWorkName: String): WorkInfo { + return workManager.getWorkInfosForUniqueWork(uniqueWorkName).get() + .single { !it.state.isFinished } + } + + private fun getWorkSpec(workId: UUID): WorkSpec { + val workManagerImpl = WorkManagerImpl.getInstance(context) + return requireNotNull(workManagerImpl.workDatabase.workSpecDao().getWorkSpec(workId.toString())) + } + class NoOpWorker( appContext: Context, workerParams: WorkerParameters diff --git a/app/src/test/java/foundation/e/apps/data/login/playstore/OauthAuthDataBuilderTest.kt b/app/src/test/java/foundation/e/apps/data/login/playstore/OauthAuthDataBuilderTest.kt index be2d274d2..90d0f2695 100644 --- a/app/src/test/java/foundation/e/apps/data/login/playstore/OauthAuthDataBuilderTest.kt +++ b/app/src/test/java/foundation/e/apps/data/login/playstore/OauthAuthDataBuilderTest.kt @@ -1,5 +1,6 @@ package foundation.e.apps.data.login.playstore +import android.os.Build import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AuthHelper import com.google.common.truth.Truth.assertThat @@ -13,8 +14,13 @@ import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import java.util.Properties +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.R, Build.VERSION_CODES.TIRAMISU]) class OauthAuthDataBuilderTest { @Before diff --git a/app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt b/app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt index a453e3ed4..8ede690ac 100644 --- a/app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt +++ b/app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt @@ -89,6 +89,7 @@ class InstallOrchestratorTest { unmockkObject(InstallWorkManager) isInstallWorkManagerMocked = false } + WorkManager.getInstance(context).cancelAllWork().result.get() } @Test diff --git a/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt b/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt index 6a6f79aa8..3d2befd2b 100644 --- a/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt +++ b/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt @@ -21,6 +21,7 @@ package foundation.e.apps.ui.search.v2 import androidx.paging.AsyncPagingDataDiffer import androidx.paging.PagingData import androidx.recyclerview.widget.ListUpdateCallback +import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.App import foundation.e.apps.data.Stores import foundation.e.apps.data.application.data.Application @@ -55,6 +56,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch +import kotlinx.coroutines.cancel import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -62,6 +64,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.After import org.junit.Rule import org.junit.Test @@ -134,6 +137,13 @@ class SearchViewModelV2Test { buildViewModel() } + @After + fun tearDown() { + if (::viewModel.isInitialized) { + viewModel.viewModelScope.cancel() + } + } + private fun buildStores(): Stores { val playStoreRepository = mockk(relaxed = true) val cleanApkAppsRepository = mockk(relaxed = true) -- GitLab