package link.roadio.riff.userstate import com.dynatrace.hash4j.hashing.Hashing import link.roadio.riff.storage.publish.DirectorySync import link.roadio.riff.userstate.model.UserStateCodec import link.roadio.riff.userstate.model.UserStateCommandModel import link.roadio.riff.userstate.model.UserStateWriteModel import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder class UserStateTransactionTest { @get:Rule val temporary = TemporaryFolder() private val hash: (ByteArray) -> Long = { Hashing.xxh3_64().hashBytesToLong(it) } @Test fun invalidSecondPatchRollsBackBothStateAndOriginalDeadline() { var now = 0L val owner = UserStateCoordinator(UserSnapshotStore(temporary.root, 10, hash, DirectorySync {}), ContentIds(hash), { now }) owner.openWriter(11) owner.changeProfile(11, 1, 2) { it.copy(settings = it.settings.copy(theme = 1)) } now = 4000 assertThrows(IllegalArgumentException::class.java) { owner.atomic { changeProfile(11, 1, 1) { it.copy(settings = it.settings.copy(theme = 2)) } changeInstallation(11, 1) { it.copy(activeProfileSlot = 4) } } } assertEquals(0, owner.profile(1).settings.theme) assertEquals( 0L, owner .profile(1) .header.receipt.sequence, ) assertEquals(5000L, owner.deadline()) now = 5000 assertTrue(owner.flushIfDue()) assertEquals(1L, owner.profile(2).header.generation) assertEquals(0L, owner.profile(1).header.generation) assertEquals(0L, owner.installation().header.generation) } @Test fun groupedReplicaKeepsExactEnvelopeThroughPartialSavedAndCommitsOneCut() { val owner = UserStateCoordinator(UserSnapshotStore(temporary.root, 10, hash, DirectorySync {}), ContentIds(hash), { 0L }) owner.openWriter(11) val replica = UserStateReplica(10, 11, owner.installation(), (1..3).associateWith(owner::profile), (1..3).associateWith(owner::history)) assertTrue(replica.changeCombined(1, { it.copy(settings = it.settings.copy(theme = 1)) }, { it.copy(activeProfileSlot = 2) })) val members = checkNotNull(replica.next()).group fun encode() = UserStateCodec.encode( UserStateCommandModel( 1, 10, 11, members.map { UserStateWriteModel(it.kind, it.slot, it.sequence, it.profile, it.installation) }, ), ) val bytes = encode() val decoded = UserStateCodec.decodeUserStateCommand(bytes) assertEquals(2, decoded.writes.size) owner.atomic { decoded.writes.forEach { write -> if (write.kind == 0 ) { changeInstallation(11, write.sequence) { checkNotNull(write.installation) } } else { changeProfile(11, write.sequence, write.profileSlot) { checkNotNull(write.profile) } } } } owner.flush() replica.saved(10, 1, 1, 11, 1, owner.profile(1).header.generation) assertFalse(replica.completion().isDone) assertArrayEquals(bytes, encode()) assertEquals(2, checkNotNull(replica.next()).group.size) replica.saved(10, 0, 0, 11, 1, owner.installation().header.generation) assertTrue(replica.completion().isDone) assertEquals(0, replica.pendingCount()) assertEquals(1L, owner.profile(1).header.generation) assertEquals(1L, owner.installation().header.generation) assertEquals(0L, owner.history(1).header.generation) } }