diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt index 3beae6ac03..ccfb61e52a 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt @@ -10,15 +10,15 @@ class PeriodicTask( private val task: suspend () -> Result, private val onSuccess: (T) -> Unit, private val onError: (Throwable) -> Unit, - private val isDelayFirst: Boolean = false, + private val initialDelay: Long = 0L, ) { private var isActive: AtomicBoolean = AtomicBoolean(false) suspend fun runTaskWithDelay() { isActive.set(true) - if (isDelayFirst) { - delay(delay) + if (initialDelay > 0L) { + delay(initialDelay) } while (isActive.get()) { task.invoke() diff --git a/core/utils/src/test/kotlin/com/tangem/utils/coroutines/PeriodicTaskTest.kt b/core/utils/src/test/kotlin/com/tangem/utils/coroutines/PeriodicTaskTest.kt new file mode 100644 index 0000000000..1f77fe3cce --- /dev/null +++ b/core/utils/src/test/kotlin/com/tangem/utils/coroutines/PeriodicTaskTest.kt @@ -0,0 +1,208 @@ +package com.tangem.utils.coroutines + +import com.google.common.truth.Truth.assertThat +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class PeriodicTaskTest { + + @Test + fun `GIVEN initialDelay 0 and delay 1000 WHEN runTaskWithDelay THEN task is invoked immediately`() = runTest { + val callCount = AtomicInteger(0) + val onSuccess = mockk<(Int) -> Unit>(relaxed = true) + val onError = mockk<(Throwable) -> Unit>(relaxed = true) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = onSuccess, + onError = onError, + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + + assertThat(callCount.get()).isEqualTo(1) + periodicTask.cancel() + } + + @Test + fun `GIVEN initialDelay 1000 WHEN runTaskWithDelay THEN task is not invoked before initialDelay elapses`() = + runTest { + val callCount = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = mockk(relaxed = true), + onError = mockk(relaxed = true), + initialDelay = INITIAL_DELAY, + ) + + launch { periodicTask.runTaskWithDelay() } + advanceTimeBy(INITIAL_DELAY - 1) + + assertThat(callCount.get()).isEqualTo(0) + periodicTask.cancel() + } + + @Test + fun `GIVEN initialDelay 1000 WHEN runTaskWithDelay THEN task is invoked once initialDelay elapses`() = runTest { + val callCount = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = mockk(relaxed = true), + onError = mockk(relaxed = true), + initialDelay = INITIAL_DELAY, + ) + + launch { periodicTask.runTaskWithDelay() } + advanceTimeBy(INITIAL_DELAY) + runCurrent() + + assertThat(callCount.get()).isEqualTo(1) + periodicTask.cancel() + } + + @Test + fun `GIVEN periodic task WHEN runTaskWithDelay THEN task is invoked repeatedly every delay ms`() = runTest { + val callCount = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = mockk(relaxed = true), + onError = mockk(relaxed = true), + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + advanceTimeBy(PERIOD * 3) + runCurrent() + + assertThat(callCount.get()).isEqualTo(4) + periodicTask.cancel() + } + + @Test + fun `GIVEN successful task WHEN runTaskWithDelay THEN onSuccess is called with the result value`() = runTest { + val received = AtomicInteger(-1) + val errors = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { Result.success(VALUE) }, + onSuccess = { received.set(it) }, + onError = { errors.incrementAndGet() }, + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + + assertThat(received.get()).isEqualTo(VALUE) + assertThat(errors.get()).isEqualTo(0) + periodicTask.cancel() + } + + @Test + fun `GIVEN failing task WHEN runTaskWithDelay THEN onError is called with the thrown exception`() = runTest { + val boom = IllegalStateException("boom") + val captured = arrayOfNulls(1) + val successes = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { Result.failure(boom) }, + onSuccess = { successes.incrementAndGet() }, + onError = { captured[0] = it }, + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + + assertThat(captured[0]).isSameInstanceAs(boom) + assertThat(successes.get()).isEqualTo(0) + periodicTask.cancel() + } + + @Test + fun `GIVEN task running WHEN cancel THEN no further invocations happen`() = runTest { + val callCount = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = mockk(relaxed = true), + onError = mockk(relaxed = true), + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + assertThat(callCount.get()).isEqualTo(1) + + periodicTask.cancel() + advanceUntilIdle() + + assertThat(callCount.get()).isEqualTo(1) + } + + @Test + fun `GIVEN initialDelay 1000 and cancel before it elapses WHEN runTaskWithDelay THEN task is never invoked`() = + runTest { + val callCount = AtomicInteger(0) + val periodicTask = PeriodicTask( + delay = PERIOD, + task = { callCount.incrementAndGet(); Result.success(VALUE) }, + onSuccess = mockk(relaxed = true), + onError = mockk(relaxed = true), + initialDelay = INITIAL_DELAY, + ) + + launch { periodicTask.runTaskWithDelay() } + advanceTimeBy(INITIAL_DELAY / 2) + periodicTask.cancel() + advanceUntilIdle() + + assertThat(callCount.get()).isEqualTo(0) + } + + @Test + fun `GIVEN task cancelled during invocation WHEN runTaskWithDelay THEN onSuccess is not called for the pending result`() = + runTest { + val onSuccess = mockk<(Int) -> Unit>(relaxed = true) + val periodicTask = PeriodicTask( + delay = PERIOD, + // Simulates a slow task that completes after the scheduler was cancelled. + task = { + delay(PERIOD) + Result.success(VALUE) + }, + onSuccess = onSuccess, + onError = mockk(relaxed = true), + initialDelay = 0L, + ) + + launch { periodicTask.runTaskWithDelay() } + runCurrent() + periodicTask.cancel() + advanceUntilIdle() + + verify(exactly = 0) { onSuccess.invoke(any()) } + } + + private companion object { + const val PERIOD = 10_000L + const val INITIAL_DELAY = 1_000L + const val VALUE = 42 + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index dbae3fb93d..05354188cf 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -120,7 +120,6 @@ internal class OnrampSuccessComponentModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( modelScope, PeriodicTask( - isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runSuspendCatching { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 3aedd8f612..77a4272f1a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -151,10 +151,14 @@ internal class SwapAmountModel @Inject constructor( } fun onStart() { - val isDelayFirst = params !is SwapAmountComponentParams.AmountBlockParams + val initialDelay = if (params is SwapAmountComponentParams.AmountBlockParams) { + BLOCK_INITIAL_QUOTE_DELAY + } else { + QUOTES_UPDATE_DELAY + } quoteTaskScheduler.scheduleTask( scope = modelScope, - task = loadQuotesTask(isDelayFirst = isDelayFirst), + task = loadQuotesTask(initialDelay = initialDelay), ) subscribeOnAutoupdateEnabling() } @@ -850,10 +854,10 @@ internal class SwapAmountModel @Inject constructor( ) } - private fun loadQuotesTask(isDelayFirst: Boolean = true): PeriodicTask { + private fun loadQuotesTask(initialDelay: Long = QUOTES_UPDATE_DELAY): PeriodicTask { return PeriodicTask( delay = QUOTES_UPDATE_DELAY, - isDelayFirst = isDelayFirst, + initialDelay = initialDelay, task = { runCatching { loadQuotes(isSilentReload = true) } }, @@ -933,5 +937,6 @@ internal class SwapAmountModel @Inject constructor( private companion object { const val DEBOUNCE_AMOUNT_DELAY = 500L const val QUOTES_UPDATE_DELAY = 10000L + const val BLOCK_INITIAL_QUOTE_DELAY = 1000L } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt index bdeb591c42..55ce756ae3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt @@ -195,7 +195,6 @@ internal class ExpressTransactionsModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( scope = modelScope, task = PeriodicTask( - isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { try { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 4cd0017182..8214740b7b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -390,7 +390,6 @@ internal class TokenDetailsModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( scope = modelScope, task = PeriodicTask( - isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runSuspendCatching { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 8597389093..89446f2de8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -398,7 +398,6 @@ internal class WalletModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( modelScope, PeriodicTask( - isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runCatching {