package io.flamenet.e2e import org.json.JSONObject import java.util.Random import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue /** * Running a whole poll between three people, without a relay (ยง6.29). * * The sequencing is the part that goes wrong: a sum published before everybody * has voted is not a partial answer, it is noise, and a piece counted twice is * a wrong count that nothing else would catch. */ class PollBookTest { private val members = listOf(4L, 7L, 9L) private fun book(): PollBook = PollBook().apply { remember("p1", "g1", "Lunch?", listOf("Thai", "Pizza"), PollBook.memberSet(members)) } private fun rng(seed: Long) = Random(seed) /** Everybody votes, the pieces travel, everybody publishes. */ @Test fun `a whole poll between three people`() { val books = members.associateWith { book() } val choices = mapOf(4L to 1, 7L to 0, 9L to 1) for (voter in members) { val outgoing = books[voter]!!.vote("p1", voter, choices[voter]!!, rng(voter)) assertEquals(members.size - 1, outgoing.size, "my own piece never leaves") for ((member, piece) in outgoing) { assertTrue(books[member]!!.receivePiece("p1", voter, piece)) } } val sums = members.associateWith { assertNotNull(books[it]!!.sumToPublish("p1", it)) } for ((publisher, sum) in sums) { for (member in members) { if (member != publisher) { assertTrue(books[member]!!.receiveSum("p1", publisher, sum)) } } } for (member in members) { val standing = books[member]!!.poll("p1")!!.standing assertTrue(standing is PollBook.Standing.Counted, "$member has no tally") assertContentEquals( intArrayOf(1, 2), (standing as PollBook.Standing.Counted).counts, "one Thai, two Pizza", ) } } /** The rule the interface depends on. */ @Test fun `no sum is published before everybody has voted`() { val mine = book() mine.vote("p1", 4L, 0, rng(1)) assertNull(mine.sumToPublish("p1", 4L), "only my own vote is in") val other = book() val from7 = other.vote("p1", 7L, 1, rng(2)) mine.receivePiece("p1", 7L, from7.first { it.first == 4L }.second) assertNull(mine.sumToPublish("p1", 4L), "still nothing from 9") val third = book() val from9 = third.vote("p1", 9L, 1, rng(3)) mine.receivePiece("p1", 9L, from9.first { it.first == 4L }.second) assertNotNull(mine.sumToPublish("p1", 4L), "now every member has voted") } /** And it says who it is waiting on. */ @Test fun `it says who it is waiting for`() { val mine = book() mine.vote("p1", 4L, 0, rng(1)) val standing = mine.poll("p1")!!.standing assertTrue(standing is PollBook.Standing.WaitingForVotes) assertEquals(listOf(7L, 9L), (standing as PollBook.Standing.WaitingForVotes).from) } @Test fun `the sum is published only once`() { val mine = book() mine.vote("p1", 4L, 0, rng(1)) for (voter in listOf(7L, 9L)) { val out = book().vote("p1", voter, 1, rng(voter)) mine.receivePiece("p1", voter, out.first { it.first == 4L }.second) } assertNotNull(mine.sumToPublish("p1", 4L)) assertNull(mine.sumToPublish("p1", 4L), "a second publish would double the count") } /** Redelivery is the failure that shows up as a tally quietly being wrong. */ @Test fun `a redelivered piece is ignored`() { val out = book().vote("p1", 7L, 1, rng(2)) val piece = out.first { it.first == 4L }.second val mine = book() assertTrue(mine.receivePiece("p1", 7L, piece)) assertFalse(mine.receivePiece("p1", 7L, piece), "counted twice") } @Test fun `a redelivered sum is ignored`() { val mine = book() assertTrue(mine.receiveSum("p1", 7L, intArrayOf(3, -3))) assertFalse(mine.receiveSum("p1", 7L, intArrayOf(3, -3))) } @Test fun `a stranger cannot join the arithmetic`() { val mine = book() assertFalse(mine.receivePiece("p1", 999L, intArrayOf(1, 0))) assertFalse(mine.receiveSum("p1", 999L, intArrayOf(1, 0))) } @Test fun `a vector of the wrong width is refused`() { val mine = book() assertFalse(mine.receivePiece("p1", 7L, intArrayOf(1, 0, 0))) assertFalse(mine.receiveSum("p1", 7L, intArrayOf(1))) } /** Both engines index the pieces the same way, whatever order was learned. */ @Test fun `the member set is order independent`() { assertEquals("4,7,9", PollBook.memberSet(listOf(9L, 4L, 7L))) assertEquals("4,7,9", PollBook.memberSet(listOf(9L, 9L, 4L, 7L))) assertEquals(listOf(4L, 7L, 9L), PollBook.members("4,7,9")) } @Test fun `a redelivered poll does not reset it`() { val mine = book() mine.vote("p1", 4L, 0, rng(1)) assertFalse( mine.remember( "p1", "g1", "Lunch?", listOf("Thai", "Pizza"), PollBook.memberSet(members), ), ) assertEquals(0, mine.poll("p1")!!.myChoice, "the vote survived") } @Test fun `voting twice changes nothing`() { val mine = book() val first = mine.vote("p1", 4L, 0, rng(1)) val second = mine.vote("p1", 4L, 1, rng(1)) assertTrue(first.isNotEmpty()) assertTrue(second.isEmpty(), "a second vote must send nothing") assertEquals(0, mine.poll("p1")!!.myChoice) } /** * A poll has to survive being closed, or it could never finish: it waits * for every member, and the pieces already gathered would be gone. */ @Test fun `it survives a round trip through storage`() { val mine = book() mine.vote("p1", 4L, 1, rng(1)) val out = book().vote("p1", 7L, 0, rng(2)) mine.receivePiece("p1", 7L, out.first { it.first == 4L }.second) val back = PollBook() back.restore(JSONObject(mine.archive().toString())) val e = assertNotNull(back.poll("p1")) assertEquals("Lunch?", e.question) assertEquals(listOf("Thai", "Pizza"), e.options) assertEquals(members, e.members) assertEquals(1, e.myChoice) assertContentEquals( mine.poll("p1")!!.piecesHeld[7L], e.piecesHeld[7L], "the piece somebody handed us has to come back exactly", ) assertContentEquals(mine.poll("p1")!!.piecesHeld[4L], e.piecesHeld[4L]) } /** And the negative values in a share have to survive it too. */ @Test fun `storage keeps the sign and the extremes`() { val mine = book() mine.receiveSum("p1", 7L, intArrayOf(Int.MIN_VALUE, Int.MAX_VALUE)) val back = PollBook() back.restore(JSONObject(mine.archive().toString())) assertContentEquals( intArrayOf(Int.MIN_VALUE, Int.MAX_VALUE), back.poll("p1")!!.sumsPublished[7L], ) } /** One member is not a poll. */ @Test fun `a poll needs at least two members`() { val b = PollBook() assertTrue(b.remember("p2", "g1", "?", listOf("a", "b"), "4,7")) assertFalse(b.remember("p3", "g1", "?", listOf("a", "b"), "4")) } }