mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
read queue view rows on the main dispatcher in the view model tests
`QueueViewModelTest` read `viewModel.rows` (a `mutableStateListOf` mutated on the main dispatcher) from the test thread, racing the view model: CI saw `ConcurrentModificationException` in `removingWhileShuffledReconcilesTheRows` and a torn read in `removingATrackUnderTheBlockReloadsRows`. Copy the rows on the dispatcher via a `rowsSnapshot()` helper before asserting.
This commit is contained in:
@@ -24,6 +24,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -146,8 +147,9 @@ class QueueViewModelTest {
|
|||||||
|
|
||||||
await {
|
await {
|
||||||
val snapshot = runBlocking { queues.snapshot() }
|
val snapshot = runBlocking { queues.snapshot() }
|
||||||
viewModel.rows.isNotEmpty() &&
|
val rows = rowsSnapshot()
|
||||||
viewModel.rows.all { runBlocking { queues.itemAt(snapshot, it.position)?.song?.id } == it.song.song.id }
|
rows.isNotEmpty() &&
|
||||||
|
runBlocking { rows.all { queues.itemAt(snapshot, it.position)?.song?.id == it.song.song.id } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,17 +312,17 @@ class QueueViewModelTest {
|
|||||||
|
|
||||||
viewModel.remove(2)
|
viewModel.remove(2)
|
||||||
await {
|
await {
|
||||||
val positions = viewModel.rows.map { it.position }
|
val positions = rowsSnapshot().map { it.position }
|
||||||
positions.isNotEmpty() && positions == (positions.first()..positions.last()).toList()
|
positions.isNotEmpty() && positions == (positions.first()..positions.last()).toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val snapshot = runBlocking { queues.snapshot() }
|
val snapshot = runBlocking { queues.snapshot() }
|
||||||
|
val rows = rowsSnapshot()
|
||||||
val expected =
|
val expected =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
(viewModel.rows.first().position..viewModel.rows.last().position)
|
(rows.first().position..rows.last().position).mapNotNull { queues.itemAt(snapshot, it)?.song?.id }
|
||||||
.mapNotNull { queues.itemAt(snapshot, it)?.song?.id }
|
|
||||||
}
|
}
|
||||||
assertEquals(expected, viewModel.rows.map { it.song.song.id })
|
assertEquals(expected, rows.map { it.song.song.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -440,6 +442,10 @@ class QueueViewModelTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `rows` is a snapshot list mutated on the main dispatcher, so read it there rather than from
|
||||||
|
// the test thread (otherwise iterating it races the view model and throws or reads torn state).
|
||||||
|
private fun rowsSnapshot(): List<QueueRow> = runBlocking { withContext(dispatcher) { viewModel.rows.toList() } }
|
||||||
|
|
||||||
private fun await(predicate: () -> Boolean) {
|
private fun await(predicate: () -> Boolean) {
|
||||||
repeat(500) {
|
repeat(500) {
|
||||||
if (predicate()) return
|
if (predicate()) return
|
||||||
|
|||||||
Reference in New Issue
Block a user