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:
austinried
2026-10-04 00:30:01 +00:00
parent 7ee7dc474a
commit 033ac56cf1
@@ -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