diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle index b985127ec6..d1e82fcb9a 100644 --- a/library/core-utils/build.gradle +++ b/library/core-utils/build.gradle @@ -53,4 +53,12 @@ android { dependencies { implementation libs.jetbrains.coroutinesAndroid + + // TESTS + testImplementation libs.tests.junit + testImplementation libs.tests.kluent + testImplementation libs.mockk.mockk + testImplementation(libs.jetbrains.coroutinesTest) { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } } diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index 8485398ffa..435a8603e8 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -19,22 +19,21 @@ package im.vector.lib.core.utils.timer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicLong @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class CountUpTimer( + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main), private val clock: Clock = DefaultClock(), private val intervalInMs: Long = 1_000, initialTime: Long = 0L, ) { - private val coroutineScope = CoroutineScope(Dispatchers.Main) private var counterJob: Job? = null - private val lastTime: AtomicLong = AtomicLong() + private val lastTime: AtomicLong = AtomicLong(clock.epochMillis()) private val elapsedTime: AtomicLong = AtomicLong(initialTime) private fun startCounter() { @@ -70,7 +69,7 @@ class CountUpTimer( fun stop() { tickListener?.onTick(elapsedTime()) - coroutineScope.cancel() + counterJob?.cancel() counterJob = null } diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt new file mode 100644 index 0000000000..4bad4471f1 --- /dev/null +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 im.vector.lib.core.utils.test.fakes + +import im.vector.lib.core.utils.timer.Clock +import io.mockk.every +import io.mockk.mockk + +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +} diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt new file mode 100644 index 0000000000..df517751ed --- /dev/null +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 im.vector.lib.core.utils.timer + +import im.vector.lib.core.utils.test.fakes.FakeClock +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifySequence +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val AN_INTERVAL = 500L +private const val AN_INITIAL_TIME = 2_333L + +@OptIn(ExperimentalCoroutinesApi::class) +internal class CountUpTimerTest { + + private val fakeClock = FakeClock() + + @Test + fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest { + every { fakeClock.epochMillis() } answers { currentTime } + val tickListener = mockk(relaxed = true) + val timer = CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + initialTime = 0, + ).also { it.tickListener = tickListener } + + timer.resume() + advanceTimeBy(AN_INTERVAL / 2) // no tick + timer.pause() // tick + advanceTimeBy(AN_INTERVAL * 10) // no tick + timer.resume() // no tick + advanceTimeBy(AN_INTERVAL * 4) // tick * 4 + timer.stop() // tick + + verifySequence { + tickListener.onTick(AN_INTERVAL / 2) + tickListener.onTick(AN_INTERVAL) + tickListener.onTick(AN_INTERVAL * 2) + tickListener.onTick(AN_INTERVAL * 3) + tickListener.onTick(AN_INTERVAL * 4) + tickListener.onTick(AN_INTERVAL * 4 + AN_INTERVAL / 2) + } + } + + @Test + fun `given an initial time, the timer ticks the right values at the right moments`() = runTest { + every { fakeClock.epochMillis() } answers { currentTime } + val tickListener = mockk(relaxed = true) + val timer = CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + initialTime = AN_INITIAL_TIME, + ).also { it.tickListener = tickListener } + + timer.resume() + advanceTimeBy(AN_INTERVAL) // tick + timer.pause() // tick + advanceTimeBy(AN_INTERVAL * 10) // no tick + timer.resume() // no tick + advanceTimeBy(AN_INTERVAL * 4) // tick * 4 + timer.stop() // tick + + val offset = AN_INITIAL_TIME % AN_INTERVAL + verifySequence { + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 2 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 3 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 4 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5) + } + } +}