From 5b2c271edd275f3f0363e241ae1ab838c2a259ac Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Thu, 6 Aug 2026 13:55:39 +0100 Subject: [PATCH 1/2] [MS-1418] Replace WorkManager location collection with in-process coroutine --- .../simprints/feature/setup/SetupModule.kt | 4 +- .../setup/location/CollectLocationUseCase.kt | 50 +++++++++++++++++ .../setup/location/LocationStoreImpl.kt | 23 ++++++++ .../location/LocationStoreWorkerScheduler.kt | 28 ---------- ...oreUserLocationIntoCurrentSessionWorker.kt | 50 ----------------- ...rTest.kt => CollectLocationUseCaseTest.kt} | 49 +++++++--------- .../setup/location/LocationStoreImplTest.kt | 56 +++++++++++++++++++ 7 files changed, 152 insertions(+), 108 deletions(-) create mode 100644 feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt create mode 100644 feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt delete mode 100644 feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreWorkerScheduler.kt delete mode 100644 feature/setup/src/main/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorker.kt rename feature/setup/src/test/java/com/simprints/feature/setup/location/{StoreUserLocationIntoCurrentSessionWorkerTest.kt => CollectLocationUseCaseTest.kt} (50%) create mode 100644 feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/SetupModule.kt b/feature/setup/src/main/java/com/simprints/feature/setup/SetupModule.kt index eda16a45d4..29d667c6fd 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/SetupModule.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/SetupModule.kt @@ -1,6 +1,6 @@ package com.simprints.feature.setup -import com.simprints.feature.setup.location.LocationStoreWorkerScheduler +import com.simprints.feature.setup.location.LocationStoreImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -10,5 +10,5 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) abstract class SetupModule { @Binds - internal abstract fun provideLocationStore(authManager: LocationStoreWorkerScheduler): LocationStore + internal abstract fun provideLocationStore(locationStoreImpl: LocationStoreImpl): LocationStore } diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt new file mode 100644 index 0000000000..a783cf8aa0 --- /dev/null +++ b/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt @@ -0,0 +1,50 @@ +package com.simprints.feature.setup.location + +import com.simprints.core.AppScope +import com.simprints.infra.events.event.domain.models.scope.Location +import com.simprints.infra.logging.Simber +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class CollectLocationUseCase @Inject constructor( + @param:AppScope private val appScope: CoroutineScope, + private val locationManager: LocationManager, + private val updateSessionScopeLocationUseCase: UpdateSessionScopeLocationUseCase, +) { + operator fun invoke(): Job { + val requestStartTimeMs = System.currentTimeMillis() + Simber.i("Started collecting location", tag = TAG) + return appScope.launch { + try { + locationManager + .requestLocation() + .filterNotNull() + .collect { location -> runCatching { saveUserLocation(location, requestStartTimeMs) } } + Simber.d("Finished collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) + } catch (c: CancellationException) { + Simber.d("Stopped collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) + throw c + } catch (t: Throwable) { + Simber.e("Failed to collect location", t, tag = TAG) + } + } + } + + private suspend fun saveUserLocation( + location: Location, + requestStartTimeMs: Long, + ) { + updateSessionScopeLocationUseCase(location) + Simber.d("Saved user's location into the current session (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) + } + + private fun elapsedMs(requestStartTimeMs: Long) = System.currentTimeMillis() - requestStartTimeMs + + private companion object { + private const val TAG = "CollectLocationUseCase" + } +} diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt new file mode 100644 index 0000000000..235b0d2ce4 --- /dev/null +++ b/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt @@ -0,0 +1,23 @@ +package com.simprints.feature.setup.location + +import com.simprints.feature.setup.LocationStore +import kotlinx.coroutines.Job +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class LocationStoreImpl @Inject constructor( + private val collectLocation: CollectLocationUseCase, +) : LocationStore { + private var collectionJob: Job? = null + + override fun collectLocationInBackground() { + collectionJob?.cancel() + collectionJob = collectLocation() + } + + override fun cancelLocationCollection() { + collectionJob?.cancel() + collectionJob = null + } +} diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreWorkerScheduler.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreWorkerScheduler.kt deleted file mode 100644 index bf0216da5d..0000000000 --- a/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreWorkerScheduler.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.simprints.feature.setup.location - -import android.content.Context -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import com.simprints.feature.setup.LocationStore -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -internal class LocationStoreWorkerScheduler @Inject constructor( - @param:ApplicationContext private val appContext: Context, -) : LocationStore { - override fun collectLocationInBackground() { - val request = OneTimeWorkRequest - .Builder(StoreUserLocationIntoCurrentSessionWorker::class.java) - .addTag(STORE_USER_LOCATION_WORKER_TAG) - .build() - WorkManager.getInstance(appContext).enqueue(request) - } - - override fun cancelLocationCollection() { - WorkManager.getInstance(appContext).cancelAllWorkByTag(STORE_USER_LOCATION_WORKER_TAG) - } - - companion object { - private const val STORE_USER_LOCATION_WORKER_TAG = "StoreUserLocationWorkerTag" - } -} diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorker.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorker.kt deleted file mode 100644 index f93065750e..0000000000 --- a/feature/setup/src/main/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorker.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.simprints.feature.setup.location - -import android.content.Context -import androidx.hilt.work.HiltWorker -import androidx.work.WorkerParameters -import com.simprints.core.DispatcherMain -import com.simprints.core.workers.SimCoroutineWorker -import com.simprints.infra.events.event.domain.models.scope.Location -import com.simprints.infra.logging.Simber -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.withContext - -/** - * Worker that collects user's last known location and save it into current session - */ -@HiltWorker -internal class StoreUserLocationIntoCurrentSessionWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val updateSessionScopeLocationUseCase: UpdateSessionScopeLocationUseCase, - private val locationManager: LocationManager, - @param:DispatcherMain private val dispatcher: CoroutineDispatcher, -) : SimCoroutineWorker(context, params) { - override val tag: String = "StoreUserLocationWorker" - - override suspend fun doWork(): Result = withContext(dispatcher) { - showProgressNotification() - crashlyticsLog("Started") - try { - locationManager - .requestLocation() - .filterNotNull() - .collect { location -> runCatching { saveUserLocation(location) } } - } catch (t: Throwable) { - fail(t) - } - success() - } - - private suspend fun saveUserLocation(location: Location) { - if (!isStopped) { - // Only store location if SID didn't yet sent the response to the calling app - updateSessionScopeLocationUseCase(location) - Simber.d("Saving user's location into the current session", tag = tag) - } - } -} diff --git a/feature/setup/src/test/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorkerTest.kt b/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt similarity index 50% rename from feature/setup/src/test/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorkerTest.kt rename to feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt index 95fdfced5f..62547c6dda 100644 --- a/feature/setup/src/test/java/com/simprints/feature/setup/location/StoreUserLocationIntoCurrentSessionWorkerTest.kt +++ b/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt @@ -1,17 +1,19 @@ package com.simprints.feature.setup.location -import android.os.PowerManager import com.simprints.infra.events.event.domain.models.scope.Location import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test -internal class StoreUserLocationIntoCurrentSessionWorkerTest { +internal class CollectLocationUseCaseTest { @get:Rule val testCoroutineRule = TestCoroutineRule() @@ -21,53 +23,44 @@ internal class StoreUserLocationIntoCurrentSessionWorkerTest { @MockK private lateinit var updateSessionScopeLocationUseCase: UpdateSessionScopeLocationUseCase - private lateinit var worker: StoreUserLocationIntoCurrentSessionWorker + private lateinit var collectLocation: CollectLocationUseCase @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - worker = StoreUserLocationIntoCurrentSessionWorker( - mockk(relaxed = true) { - every { getSystemService(any()) } returns mockk { - every { isIgnoringBatteryOptimizations(any()) } returns true - } - }, - mockk(relaxed = true), - updateSessionScopeLocationUseCase, - locationManager, - testCoroutineRule.testCoroutineDispatcher, + collectLocation = CollectLocationUseCase( + appScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), + locationManager = locationManager, + updateSessionScopeLocationUseCase = updateSessionScopeLocationUseCase, ) } @Test - fun storeUserLocationIntoCurrentSession() = runTest { + fun `invoke saves location into current session`() { every { locationManager.requestLocation() } returns flowOf(Location(latitude = 23.0, longitude = 54.0)) - worker.doWork() + + collectLocation() + coVerify(exactly = 1) { updateSessionScopeLocationUseCase.invoke(any()) } } @Test - fun `storeUserLocationIntoCurrentSession requestLocation throw exception`() = runTest { + fun `invoke requestLocation throws exception does not crash`() { every { locationManager.requestLocation() } throws Exception("Location collect exception") - worker.doWork() + + collectLocation() + coVerify(exactly = 0) { updateSessionScopeLocationUseCase.invoke(any()) } } @Test(expected = Test.None::class) - fun `storeUserLocationIntoCurrentSession can't save event should not crash the app`() = runTest { + fun `invoke can't save event should not crash the app`() { every { locationManager.requestLocation() } returns flowOf(Location(latitude = 23.0, longitude = 54.0)) coEvery { updateSessionScopeLocationUseCase.invoke(any()) } throws Exception("No session capture event found") - worker.doWork() - } - @Test - fun `storeUserLocationIntoCurrentSession can't save events if the worker is canceled`() = runTest { - every { locationManager.requestLocation() } returns flowOf(Location(latitude = 23.0, longitude = 54.0)) - worker.stop(0) - worker.doWork() - coVerify(exactly = 0) { updateSessionScopeLocationUseCase.invoke(any()) } + collectLocation() } } diff --git a/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt b/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt new file mode 100644 index 0000000000..894a503319 --- /dev/null +++ b/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt @@ -0,0 +1,56 @@ +package com.simprints.feature.setup.location + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.Job +import org.junit.Before +import org.junit.Test + +internal class LocationStoreImplTest { + @MockK + private lateinit var collectLocation: CollectLocationUseCase + + private lateinit var locationStore: LocationStoreImpl + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + locationStore = LocationStoreImpl(collectLocation) + } + + @Test + fun `collectLocationInBackground starts a new collection job`() { + val job = mockk(relaxed = true) + every { collectLocation() } returns job + + locationStore.collectLocationInBackground() + + verify(exactly = 1) { collectLocation() } + } + + @Test + fun `collectLocationInBackground cancels a previous job before starting a new one`() { + val firstJob = mockk(relaxed = true) + val secondJob = mockk(relaxed = true) + every { collectLocation() } returnsMany listOf(firstJob, secondJob) + + locationStore.collectLocationInBackground() + locationStore.collectLocationInBackground() + + verify(exactly = 1) { firstJob.cancel() } + } + + @Test + fun `cancelLocationCollection cancels the current job`() { + val job = mockk(relaxed = true) + every { collectLocation() } returns job + + locationStore.collectLocationInBackground() + locationStore.cancelLocationCollection() + + verify(exactly = 1) { job.cancel() } + } +} From 36e63a0b2ea781b13f234917026aae1c259df9d1 Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Mon, 10 Aug 2026 13:15:54 +0100 Subject: [PATCH 2/2] [MS-1540] Enable age and gender estimation in face capture processing --- .../setup/location/CollectLocationUseCase.kt | 36 ++++++------ .../setup/location/LocationStoreImpl.kt | 16 ++++-- .../location/CollectLocationUseCaseTest.kt | 9 ++- .../setup/location/LocationStoreImplTest.kt | 57 +++++++++++++------ 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt index a783cf8aa0..edcf982226 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/location/CollectLocationUseCase.kt @@ -1,36 +1,34 @@ package com.simprints.feature.setup.location -import com.simprints.core.AppScope import com.simprints.infra.events.event.domain.models.scope.Location import com.simprints.infra.logging.Simber import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.launch import javax.inject.Inject internal class CollectLocationUseCase @Inject constructor( - @param:AppScope private val appScope: CoroutineScope, private val locationManager: LocationManager, private val updateSessionScopeLocationUseCase: UpdateSessionScopeLocationUseCase, ) { - operator fun invoke(): Job { + /** + * Runs directly in the caller's coroutine context/scope so that cancelling the caller + * (e.g. cancelling its scope's children) cancels this collection too, without needing to + * track and cancel a [kotlinx.coroutines.Job] manually. + */ + suspend operator fun invoke() { val requestStartTimeMs = System.currentTimeMillis() Simber.i("Started collecting location", tag = TAG) - return appScope.launch { - try { - locationManager - .requestLocation() - .filterNotNull() - .collect { location -> runCatching { saveUserLocation(location, requestStartTimeMs) } } - Simber.d("Finished collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) - } catch (c: CancellationException) { - Simber.d("Stopped collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) - throw c - } catch (t: Throwable) { - Simber.e("Failed to collect location", t, tag = TAG) - } + try { + locationManager + .requestLocation() + .filterNotNull() + .collect { location -> runCatching { saveUserLocation(location, requestStartTimeMs) } } + Simber.d("Finished collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) + } catch (c: CancellationException) { + Simber.d("Stopped collecting location (took ${elapsedMs(requestStartTimeMs)}ms)", tag = TAG) + throw c + } catch (t: Throwable) { + Simber.e("Failed to collect location", t, tag = TAG) } } diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt b/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt index 235b0d2ce4..a9d588f866 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/location/LocationStoreImpl.kt @@ -1,23 +1,29 @@ package com.simprints.feature.setup.location +import com.simprints.core.AppScope import com.simprints.feature.setup.LocationStore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton internal class LocationStoreImpl @Inject constructor( + @AppScope appScope: CoroutineScope, private val collectLocation: CollectLocationUseCase, ) : LocationStore { - private var collectionJob: Job? = null + // A child scope of the app scope so that collection coroutines can be cancelled + private val scope = CoroutineScope(appScope.coroutineContext + SupervisorJob(appScope.coroutineContext[Job])) override fun collectLocationInBackground() { - collectionJob?.cancel() - collectionJob = collectLocation() + scope.coroutineContext.cancelChildren() + scope.launch { collectLocation() } } override fun cancelLocationCollection() { - collectionJob?.cancel() - collectionJob = null + scope.coroutineContext.cancelChildren() } } diff --git a/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt b/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt index 62547c6dda..f59af41c86 100644 --- a/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt +++ b/feature/setup/src/test/java/com/simprints/feature/setup/location/CollectLocationUseCaseTest.kt @@ -7,8 +7,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -30,14 +30,13 @@ internal class CollectLocationUseCaseTest { MockKAnnotations.init(this, relaxed = true) collectLocation = CollectLocationUseCase( - appScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), locationManager = locationManager, updateSessionScopeLocationUseCase = updateSessionScopeLocationUseCase, ) } @Test - fun `invoke saves location into current session`() { + fun `invoke saves location into current session`() = runTest { every { locationManager.requestLocation() } returns flowOf(Location(latitude = 23.0, longitude = 54.0)) collectLocation() @@ -46,7 +45,7 @@ internal class CollectLocationUseCaseTest { } @Test - fun `invoke requestLocation throws exception does not crash`() { + fun `invoke requestLocation throws exception does not crash`() = runTest { every { locationManager.requestLocation() } throws Exception("Location collect exception") collectLocation() @@ -55,7 +54,7 @@ internal class CollectLocationUseCaseTest { } @Test(expected = Test.None::class) - fun `invoke can't save event should not crash the app`() { + fun `invoke can't save event should not crash the app`() = runTest { every { locationManager.requestLocation() } returns flowOf(Location(latitude = 23.0, longitude = 54.0)) coEvery { updateSessionScopeLocationUseCase.invoke(any()) diff --git a/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt b/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt index 894a503319..355856914c 100644 --- a/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt +++ b/feature/setup/src/test/java/com/simprints/feature/setup/location/LocationStoreImplTest.kt @@ -1,56 +1,79 @@ package com.simprints.feature.setup.location +import com.google.common.truth.Truth.assertThat +import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation import org.junit.Before +import org.junit.Rule import org.junit.Test internal class LocationStoreImplTest { + @get:Rule + val testCoroutineRule = TestCoroutineRule() + @MockK private lateinit var collectLocation: CollectLocationUseCase + private lateinit var appScope: CoroutineScope private lateinit var locationStore: LocationStoreImpl @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - locationStore = LocationStoreImpl(collectLocation) + appScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher + Job()) + locationStore = LocationStoreImpl(appScope, collectLocation) } @Test - fun `collectLocationInBackground starts a new collection job`() { - val job = mockk(relaxed = true) - every { collectLocation() } returns job + fun `collectLocationInBackground starts a new collection`() { + coEvery { collectLocation() } returns Unit locationStore.collectLocationInBackground() - verify(exactly = 1) { collectLocation() } + coVerify(exactly = 1) { collectLocation() } } @Test - fun `collectLocationInBackground cancels a previous job before starting a new one`() { - val firstJob = mockk(relaxed = true) - val secondJob = mockk(relaxed = true) - every { collectLocation() } returnsMany listOf(firstJob, secondJob) + fun `collectLocationInBackground cancels a previous collection before starting a new one`() { + val firstCollectionCancelled = CompletableDeferred() + coEvery { collectLocation() } coAnswers { + try { + awaitCancellation() + } catch (c: CancellationException) { + firstCollectionCancelled.complete(Unit) + throw c + } + } locationStore.collectLocationInBackground() locationStore.collectLocationInBackground() - verify(exactly = 1) { firstJob.cancel() } + assertThat(firstCollectionCancelled.isCompleted).isTrue() } @Test - fun `cancelLocationCollection cancels the current job`() { - val job = mockk(relaxed = true) - every { collectLocation() } returns job + fun `cancelLocationCollection cancels the current collection`() { + val collectionCancelled = CompletableDeferred() + coEvery { collectLocation() } coAnswers { + try { + awaitCancellation() + } catch (c: CancellationException) { + collectionCancelled.complete(Unit) + throw c + } + } locationStore.collectLocationInBackground() locationStore.cancelLocationCollection() - verify(exactly = 1) { job.cancel() } + assertThat(collectionCancelled.isCompleted).isTrue() } }