Fix StackOverFlow exception when stop action is called within the tick event

This commit is contained in:
Florian Renaud 2023-02-14 12:16:24 +01:00
parent 8667797246
commit 68712513b3
2 changed files with 97 additions and 13 deletions

View File

@ -35,6 +35,7 @@ class CountUpTimer(
private val elapsedTime: AtomicLong = AtomicLong(0) private val elapsedTime: AtomicLong = AtomicLong(0)
private fun startCounter() { private fun startCounter() {
counterJob?.cancel()
counterJob = coroutineScope.launch { counterJob = coroutineScope.launch {
while (true) { while (true) {
delay(intervalInMs - elapsedTime() % intervalInMs) delay(intervalInMs - elapsedTime() % intervalInMs)
@ -54,27 +55,52 @@ class CountUpTimer(
} }
} }
/**
* Start a new timer with the initial given time, if any.
* If the timer is already started, it will be restarted.
*/
fun start(initialTime: Long = 0L) { fun start(initialTime: Long = 0L) {
elapsedTime.set(initialTime) elapsedTime.set(initialTime)
resume()
}
fun pause() {
tickListener?.onTick(elapsedTime())
counterJob?.cancel()
counterJob = null
}
fun resume() {
lastTime.set(clock.epochMillis()) lastTime.set(clock.epochMillis())
startCounter() startCounter()
} }
/**
* Pause the timer at the current time.
*/
fun pause() {
pauseAndTick()
}
/**
* Resume the timer from the current time.
* Does nothing if the timer is already running.
*/
fun resume() {
if (counterJob?.isActive != true) {
lastTime.set(clock.epochMillis())
startCounter()
}
}
/**
* Stop and reset the timer.
*/
fun stop() { fun stop() {
tickListener?.onTick(elapsedTime()) pauseAndTick()
elapsedTime.set(0L)
}
private fun pauseAndTick() {
if (counterJob?.isActive == true) {
// get the elapsed time before cancelling the timer
val elapsedTime = elapsedTime()
// cancel the timer before ticking
counterJob?.cancel() counterJob?.cancel()
counterJob = null counterJob = null
elapsedTime.set(0L) // tick with the computed elapsed time
tickListener?.onTick(elapsedTime)
}
} }
fun interface TickListener { fun interface TickListener {

View File

@ -19,6 +19,8 @@ package im.vector.lib.core.utils.timer
import im.vector.lib.core.utils.test.fakes.FakeClock import im.vector.lib.core.utils.test.fakes.FakeClock
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import io.mockk.verifySequence import io.mockk.verifySequence
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@ -36,6 +38,7 @@ internal class CountUpTimerTest {
@Test @Test
fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest { fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest {
// Given
every { fakeClock.epochMillis() } answers { currentTime } every { fakeClock.epochMillis() } answers { currentTime }
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true) val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
val timer = CountUpTimer( val timer = CountUpTimer(
@ -44,6 +47,7 @@ internal class CountUpTimerTest {
intervalInMs = AN_INTERVAL, intervalInMs = AN_INTERVAL,
).also { it.tickListener = tickListener } ).also { it.tickListener = tickListener }
// When
timer.start() timer.start()
advanceTimeBy(AN_INTERVAL / 2) // no tick advanceTimeBy(AN_INTERVAL / 2) // no tick
timer.pause() // tick timer.pause() // tick
@ -52,6 +56,7 @@ internal class CountUpTimerTest {
advanceTimeBy(AN_INTERVAL * 4) // tick * 4 advanceTimeBy(AN_INTERVAL * 4) // tick * 4
timer.stop() // tick timer.stop() // tick
// Then
verifySequence { verifySequence {
tickListener.onTick(AN_INTERVAL / 2) tickListener.onTick(AN_INTERVAL / 2)
tickListener.onTick(AN_INTERVAL) tickListener.onTick(AN_INTERVAL)
@ -64,6 +69,7 @@ internal class CountUpTimerTest {
@Test @Test
fun `given an initial time, the timer ticks the right values at the right moments`() = runTest { fun `given an initial time, the timer ticks the right values at the right moments`() = runTest {
// Given
every { fakeClock.epochMillis() } answers { currentTime } every { fakeClock.epochMillis() } answers { currentTime }
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true) val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
val timer = CountUpTimer( val timer = CountUpTimer(
@ -72,6 +78,7 @@ internal class CountUpTimerTest {
intervalInMs = AN_INTERVAL, intervalInMs = AN_INTERVAL,
).also { it.tickListener = tickListener } ).also { it.tickListener = tickListener }
// When
timer.start(AN_INITIAL_TIME) timer.start(AN_INITIAL_TIME)
advanceTimeBy(AN_INTERVAL) // tick advanceTimeBy(AN_INTERVAL) // tick
timer.pause() // tick timer.pause() // tick
@ -80,6 +87,7 @@ internal class CountUpTimerTest {
advanceTimeBy(AN_INTERVAL * 4) // tick * 4 advanceTimeBy(AN_INTERVAL * 4) // tick * 4
timer.stop() // tick timer.stop() // tick
// Then
val offset = AN_INITIAL_TIME % AN_INTERVAL val offset = AN_INITIAL_TIME % AN_INTERVAL
verifySequence { verifySequence {
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset) tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset)
@ -91,4 +99,54 @@ internal class CountUpTimerTest {
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5) tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5)
} }
} }
@Test
fun `when stopping the timer on tick, the stop action is called twice and the timer ticks twice`() = runTest {
// Given
every { fakeClock.epochMillis() } answers { currentTime }
val timer = spyk(
CountUpTimer(
coroutineScope = this,
clock = fakeClock,
intervalInMs = AN_INTERVAL,
)
)
val tickListener = mockk<CountUpTimer.TickListener> {
every { onTick(any()) } answers { timer.stop() }
}
timer.tickListener = tickListener
// When
timer.start()
advanceTimeBy(AN_INTERVAL * 10)
// Then
verify(exactly = 2) { timer.stop() } // one call at the first tick, a second time because of the tick on the previous stop action
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the stop action
}
@Test
fun `when pausing the timer on tick, the pause action is called twice and the timer ticks twice`() = runTest {
// Given
every { fakeClock.epochMillis() } answers { currentTime }
val timer = spyk(
CountUpTimer(
coroutineScope = this,
clock = fakeClock,
intervalInMs = AN_INTERVAL,
)
)
val tickListener = mockk<CountUpTimer.TickListener> {
every { onTick(any()) } answers { timer.pause() }
}
timer.tickListener = tickListener
// When
timer.start()
advanceTimeBy(AN_INTERVAL * 10)
// Then
verify(exactly = 2) { timer.pause() } // one call at the first tick, a second time because of the tick on the previous pause action
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the pause action
}
} }