Make player history failures non-fatal
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package com.jsjdesigns.poker
|
||||
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PlayerHistoryStoreTest {
|
||||
|
||||
private class FakePersistence(
|
||||
var stored: PlayerHistory = PlayerHistory(),
|
||||
var readFailure: RuntimeException? = null,
|
||||
var writeResult: Boolean = true,
|
||||
var writeFailure: RuntimeException? = null,
|
||||
) : PlayerHistoryPersistence {
|
||||
var reads = 0
|
||||
var writes = 0
|
||||
|
||||
override fun read(): PlayerHistory {
|
||||
reads++
|
||||
readFailure?.let { throw it }
|
||||
return stored
|
||||
}
|
||||
|
||||
override fun write(history: PlayerHistory): Boolean {
|
||||
writes++
|
||||
writeFailure?.let { throw it }
|
||||
if (writeResult) stored = history
|
||||
return writeResult
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `schema dispatch has an explicit migration branch`() {
|
||||
assertEquals(HistorySchemaPlan.FRESH, historySchemaPlan(0))
|
||||
assertEquals(HistorySchemaPlan.READ_VERSION_1, historySchemaPlan(1))
|
||||
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(2))
|
||||
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(99))
|
||||
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(-1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `newer schema falls back in memory without overwrite or crash`() = runTest {
|
||||
val persistence = FakePersistence(
|
||||
readFailure = UnsupportedPlayerHistorySchema(99),
|
||||
)
|
||||
val failures = mutableListOf<String>()
|
||||
val store = ResilientPlayerHistoryStore(persistence) { message, _ ->
|
||||
failures += message
|
||||
}
|
||||
|
||||
assertEquals(PlayerHistory(), store.load())
|
||||
val currentSession = store.recordSession("Jay", coachEnabled = true)
|
||||
|
||||
assertEquals("Jay", currentSession.playerName)
|
||||
assertTrue(currentSession.coachEnabled)
|
||||
assertEquals(1L, currentSession.sessionsStarted)
|
||||
assertEquals("fallback remains useful in this process", currentSession, store.load())
|
||||
assertEquals(1, persistence.reads)
|
||||
assertEquals("an older build must not overwrite newer data", 0, persistence.writes)
|
||||
assertEquals(1, failures.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `corrupt history also degrades to an in-memory session`() = runTest {
|
||||
val persistence = FakePersistence(
|
||||
readFailure = IllegalArgumentException("corrupt counter"),
|
||||
)
|
||||
val store = ResilientPlayerHistoryStore(persistence)
|
||||
|
||||
assertEquals(PlayerHistory(), store.load())
|
||||
assertEquals(1L, store.recordCompletedHand().handsCompleted)
|
||||
assertEquals(0, persistence.writes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed commit disables further writes but keeps current counters`() = runTest {
|
||||
val persistence = FakePersistence(writeResult = false)
|
||||
val failures = mutableListOf<String>()
|
||||
val store = ResilientPlayerHistoryStore(persistence) { message, _ ->
|
||||
failures += message
|
||||
}
|
||||
|
||||
assertEquals(1L, store.recordCompletedHand().handsCompleted)
|
||||
assertEquals(2L, store.recordCompletedHand().handsCompleted)
|
||||
|
||||
assertEquals("do not repeatedly hammer failed storage", 1, persistence.writes)
|
||||
assertEquals(1, failures.size)
|
||||
assertTrue("commit" in failures.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write exception is contained like a false commit`() = runTest {
|
||||
val persistence = FakePersistence(
|
||||
writeFailure = IllegalStateException("disk unavailable"),
|
||||
)
|
||||
val store = ResilientPlayerHistoryStore(persistence)
|
||||
|
||||
assertEquals(1L, store.recordCompletedHand().handsCompleted)
|
||||
assertEquals(2L, store.recordCompletedHand().handsCompleted)
|
||||
assertEquals(1, persistence.writes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `concurrent hand updates cannot clobber one another`() = runTest {
|
||||
val persistence = FakePersistence()
|
||||
val store = ResilientPlayerHistoryStore(persistence)
|
||||
|
||||
coroutineScope {
|
||||
repeat(100) {
|
||||
launch { store.recordCompletedHand() }
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(100L, store.load().handsCompleted)
|
||||
assertEquals(100, persistence.writes)
|
||||
assertFalse(persistence.stored.coachEnabled)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user