Skip to content

Commit

Permalink
use deferred as test service completion
Browse files Browse the repository at this point in the history
  • Loading branch information
StageGuard committed Feb 9, 2025
1 parent 4097569 commit fa75515
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

package me.him188.ani.app.domain.torrent

import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
Expand All @@ -22,18 +22,20 @@ import kotlin.test.BeforeTest
abstract class AbstractTorrentServiceConnectionTest {
protected val fakeBinder = "FAKE_BINDER_OBJECT"

protected val startServiceWithSuccess = object : TorrentServiceStarter<String> {
override suspend fun start(): String {
delay(200)
return fakeBinder
}
}

protected val startServiceWithFail = object : TorrentServiceStarter<String> {
override suspend fun start(): String {
delay(100)
throw ServiceStartException.NullBinder()
}
protected fun createStarter(
expectSuccess: Boolean
): Pair<TorrentServiceStarter<String>, CompletableDeferred<Unit>> {
val deferred = CompletableDeferred<Unit>()
return object : TorrentServiceStarter<String> {
override suspend fun start(): String {
deferred.await()
if (expectSuccess) {
return fakeBinder
} else {
throw ServiceStartException.NullBinder()
}
}
} to deferred
}

@BeforeTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import app.cash.turbine.test
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
Expand All @@ -31,17 +30,19 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `service starts on resume - success`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

connection.connected.test {
assertFalse(awaitItem(), "Initially, connected should be false.")

// trigger on resumed
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
backgroundScope.launch { deferred.complete(Unit) }
assertTrue(awaitItem(), "After service is connected, connected should become true.")

// completed
Expand All @@ -54,10 +55,11 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `service starts on resume - fails to start service`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = false)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithFail,
starter = starter,
).also { it.startLifecycleLoop() }

// The .connected flow should remain false, even after we move to resumed,
Expand All @@ -67,7 +69,7 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect

// Move to RESUMED
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)

backgroundScope.launch { deferred.complete(Unit) }
// Because startService() fails repeatedly in the retry loop, connected never becomes true
// We'll watch for a short while and confirm it does not become true
repeat(3) {
Expand All @@ -82,10 +84,11 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `getBinder suspends until service is connected`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

// Start a coroutine that calls getBinder
Expand All @@ -98,6 +101,7 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
assertTrue(!binderDeferred.isCompleted)

testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
backgroundScope.launch { deferred.complete(Unit) }

// Once connected, getBinder should complete with the fake binder
val binder = binderDeferred.await()
Expand All @@ -109,16 +113,18 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `service disconnect triggers automatic restart if lifecycle is RESUMED`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

connection.connected.test {
assertFalse(awaitItem(), "Initially, connected should be false.")
// Wait for the startService invocation
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
backgroundScope.launch { deferred.complete(Unit) }
advanceUntilIdle()
// Now it’s connected
assertTrue(awaitItem(), "Service should be connected.")
Expand All @@ -128,6 +134,7 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
advanceUntilIdle()
assertFalse(awaitItem(), "Service should be disconnected since we triggered disconnection.")

backgroundScope.launch { deferred.complete(Unit) }
// Because the lifecycle is still in RESUMED,
// it should attempt to startService again automatically
// We can wait a bit, then connect again:
Expand All @@ -143,17 +150,19 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `service disconnect does not restart if lifecycle is only CREATED`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

connection.connected.test {
assertFalse(awaitItem(), "Initially, connected should be false.")

// Wait for the startService invocation
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
backgroundScope.launch { deferred.complete(Unit) }
advanceUntilIdle()
// Now it’s connected
assertTrue(awaitItem(), "Service should be connected.")
Expand All @@ -177,10 +186,11 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `lifecycle move to STARTED while starting service`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

connection.connected.test {
Expand All @@ -190,7 +200,6 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
// Wait for the startService invocation
// connect 成功需要 200ms, 100ms 后就 move to STARTED
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
advanceTimeBy(100)
testLifecycle.setCurrentState(Lifecycle.State.STARTED)

// 启动中途切到后台 (lifecycle state => STARTED) 不会 emit true
Expand All @@ -204,10 +213,11 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
@Test
fun `fast path for service disconnected and also does not affect binder getter`() = runTest {
val testLifecycle = TestLifecycleOwner()
val (starter, deferred) = createStarter(expectSuccess = true)
val connection = LifecycleAwareTorrentServiceConnection(
parentCoroutineContext = backgroundScope.coroutineContext,
lifecycle = testLifecycle.lifecycle,
starter = startServiceWithSuccess,
starter = starter,
).also { it.startLifecycleLoop() }

advanceUntilIdle()
Expand All @@ -230,7 +240,8 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect
// connect 成功需要 200ms, 100ms 后就 trigger disconnect
// 此时 connected 不会被设置为 true, 因为通过 fast path 检测到了 disconnect
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
delay(100)

advanceUntilIdle()
connection.onServiceDisconnected()

advanceUntilIdle()
Expand All @@ -242,16 +253,15 @@ class LifecycleAwareTorrentServiceConnectionTest : AbstractTorrentServiceConnect

advanceUntilIdle()
testLifecycle.setCurrentState(Lifecycle.State.RESUMED)
delay(500)
testLifecycle.setCurrentState(Lifecycle.State.STARTED)
connection.onServiceDisconnected()

backgroundScope.launch { deferred.complete(Unit) }
advanceUntilIdle()
// Now it’s connected
assertTrue(awaitItem(), "Service should be connected.")
assertFalse(awaitItem(), "Service should be disconnected.")

// Disconnect:
connection.onServiceDisconnected()
advanceUntilIdle()
expectNoEvents()
assertFalse(awaitItem(), "Service should be disconnected since we triggered disconnection.")
}

connection.close()
Expand Down

0 comments on commit fa75515

Please sign in to comment.