Skip to content

Commit

Permalink
More structures + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
serras committed Nov 23, 2023
1 parent 42e98f9 commit daed4c9
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,50 @@ public interface STM {
}

public fun <A> TVar<A>.read(): A = variable.value
public fun <A> TVar<A>.write(value: A) {
variable.value = value
public fun <A> TVar<A>.write(value: A) { variable.value = value }
public fun <A> TVar<A>.modify(f: (A) -> A) { variable.value = f(variable.value) }

public operator fun <K, V> TMap<K, V>.get(k: K): V? = map[k]
public fun <K, V> TMap<K, V>.lookup(k: K): V? = this[k]

public operator fun <K, V> TMap<K, V>.set(k: K, v: V) { map[k] = v }
public operator fun <K, V> TMap<K, V>.plusAssign(kv: Pair<K, V>) { this[kv.first] = kv.second }
public fun <K, V> TMap<K, V>.insert(k: K, v: V) { this[k] = v }
public fun <K, V> TMap<K, V>.remove(k: K) { map.remove(k) }
public operator fun <K, V> TMap<K, V>.contains(k: K): Boolean = k in map
public fun <K, V> TMap<K, V>.member(k: K): Boolean = k in this
public fun <K, V> TMap<K, V>.update(k: K, f: (V) -> V) {
when (val v = map[k]) {
null -> {}
else -> map[k] = f(v)
}
}
public fun <A> TVar<A>.modify(f: (A) -> A) {
variable.value = f(variable.value)

public operator fun <A> TSet<A>.plusAssign(a: A) { map[a] = true }
public fun <A> TSet<A>.insert(a: A) { this += a }
public fun <A> TSet<A>.remove(a: A) { map.remove(a) }
public operator fun <A> TSet<A>.contains(a: A): Boolean = a in map
public fun <A> TSet<A>.member(a: A): Boolean = a in this

public fun <A> TQueue<A>.isEmpty(): Boolean = list.isEmpty()
public fun <A> TQueue<A>.isNotEmpty(): Boolean = !isEmpty()
public fun <A> TQueue<A>.size(): Int = list.size

public fun <A> TQueue<A>.peek(): A = if (list.isEmpty()) retry() else list.first()
public fun <A> TQueue<A>.tryPeek(): A? = list.firstOrNull()
public fun <A> TQueue<A>.read(): A = if (list.isEmpty()) retry() else list.removeFirst()
public fun <A> TQueue<A>.tryRead(): A? = if (list.isEmpty()) null else list.removeFirst()

public operator fun <A> TQueue<A>.plusAssign(a: A) { list.add(a) }
public fun <A> TQueue<A>.write(a: A) { this += a }
public fun <A> TQueue<A>.writeFront(a: A) { list.add(0, a) }

public fun <A> TQueue<A>.removeAll(predicate: (A) -> Boolean) { list.removeAll { !predicate(it) } }

public fun <A> TQueue<A>.flush(): List<A> {
val current = mutableListOf<A>().also { it.addAll(list) }
list.clear()
return current
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package arrow.fx.stm.compose

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import kotlin.jvm.JvmInline

@JvmInline
Expand All @@ -12,3 +16,31 @@ public value class TVar<A>(internal val variable: MutableState<A>) {
public fun <A> new(value: A): TVar<A> = TVar(mutableStateOf(value))
}
}

@JvmInline
public value class TMap<K, V>(internal val map: SnapshotStateMap<K, V>) {
public companion object {
public fun <K, V> new(): TMap<K, V> = TMap(mutableStateMapOf())
}
}

@JvmInline
public value class TSet<A>(internal val map: SnapshotStateMap<A, Boolean>) {
public companion object {
public fun <A> new(): TSet<A> = TSet(mutableStateMapOf())
}
}

@JvmInline
public value class TList<A>(internal val list: SnapshotStateList<A>) {
public companion object {
public fun <A> new(): TList<A> = TList(mutableStateListOf())
}
}

@JvmInline
public value class TQueue<A>(internal val list: SnapshotStateList<A>) {
public companion object {
public fun <A> new(): TQueue<A> = TQueue(mutableStateListOf())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package arrow.fx.stm.compose

import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.map
import io.kotest.property.checkAll
import kotlinx.coroutines.test.runTest
import kotlin.test.Test

class TMapTest {

@Test fun insertValues() = runTest {
checkAll(Arb.int(), Arb.int()) { k, v ->
val map = TMap.new<Int, Int>()
atomically { map.insert(k, v) }
atomically { map.lookup(k) } shouldBe v
}
}

@Test fun insertMultipleValues() = runTest {
checkAll(Arb.map(Arb.int(), Arb.int())) { pairs ->
val map = TMap.new<Int, Int>()
atomically {
for ((k, v) in pairs) map.insert(k, v)
}
atomically {
for ((k, v) in pairs) map.lookup(k) shouldBe v
}
}
}

@Test fun insertMultipleCollidingValues() = runTest {
checkAll(Arb.map(Arb.int(), Arb.int())) { pairs ->
val map = TMap.new<Int, Int>() // hash function that always returns 0
atomically {
for ((k, v) in pairs) map.insert(k, v)
}
atomically {
for ((k, v) in pairs) map.lookup(k) shouldBe v
}
}
}

@Test fun insertAndRemove() = runTest {
checkAll(Arb.int(), Arb.int()) { k, v ->
val map = TMap.new<Int, Int>()
atomically { map.insert(k, v) }
atomically { map.lookup(k) } shouldBe v
atomically { map.remove(k) }
atomically { map.lookup(k) } shouldBe null
}
}

@Test fun update() = runTest {
checkAll(Arb.int(), Arb.int(), Arb.int()) { k, v, g ->
val map = TMap.new<Int, Int>()
atomically { map.insert(k, v) }
atomically { map.lookup(k) } shouldBe v
atomically { map.update(k) { v + g } }
atomically { map.lookup(k) } shouldBe v + g
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package arrow.fx.stm.compose

import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
import kotlinx.coroutines.test.runTest
import kotlin.random.Random
import kotlin.test.Test

class TQueueTest {

@Test fun writingToAQueueAddsAnElement() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(10) }
atomically { tq.flush() } shouldBe listOf(10)
}

@Test fun readingFromAQueueShouldRetryIfTheQueueIsEmpty() = runTest {
val tq = TQueue.new<Int>()
atomically {
stm { tq.read().let { true } } orElse { false }
} shouldBe false
}

@Test fun readingFromAQueueShouldRemoveThatValue() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(10); tq.write(20) }
atomically { tq.read() } shouldBe 10
atomically { tq.flush() } shouldBe listOf(20)
}

@Test fun tryReadBehavesLikeReadIfThereAreValuesToRead() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(10) }
atomically { tq.tryRead() } shouldBe 10
atomically { tq.flush() } shouldBe emptyList()
}

@Test fun tryReadReturnsNullIfTheQueueIsEmpty() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.tryRead() } shouldBe null
}

@Test fun flushEmptiesTheEntireQueueAndReturnsIt() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(20); tq.write(30); tq.write(40) }
atomically { tq.flush() } shouldBe listOf(20, 30, 40)
atomically { tq.flush() } shouldBe emptyList()
}

@Test fun readingFlushingShouldWorkAfterMixedReadsWrites() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) }
atomically { tq.read() } shouldBe 20
atomically { tq.flush() } shouldBe listOf(30, 40)

atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) }
atomically { tq.flush() } shouldBe listOf(20, 30, 40)
atomically { tq.flush() } shouldBe emptyList()
}

@Test fun peekShouldLeaveTheQueueUnchanged() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(20); tq.write(30); tq.write(40) }
atomically { tq.peek() } shouldBeExactly 20
atomically { tq.flush() } shouldBe listOf(20, 30, 40)
}

@Test fun peekShouldRetryIfTheQueueIsEmpty() = runTest {
val tq = TQueue.new<Int>()
atomically {
stm { tq.peek().let { true } } orElse { false }
} shouldBe false
}

@Test fun tryPeekShouldBehaveLikePeekIfThereAreElements() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.write(20); tq.write(30); tq.write(40) }
atomically { tq.peek() } shouldBeExactly
atomically { tq.tryPeek()!! }
atomically { tq.flush() } shouldBe listOf(20, 30, 40)
}

@Test fun tryPeekShouldReturnNullIfTheQueueIsEmpty() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.tryPeek() } shouldBe null
}

@Test fun isEmptyAndIsNotEmptyShouldWorkCorrectly() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.isEmpty() } shouldBe true
atomically { tq.isNotEmpty() } shouldBe false
atomically { tq.write(20) }
atomically { tq.isEmpty() } shouldBe false
atomically { tq.isNotEmpty() } shouldBe true
atomically { tq.peek(); tq.write(30) }
atomically { tq.isEmpty() } shouldBe false
atomically { tq.isNotEmpty() } shouldBe true
}

@Test fun sizeShouldReturnTheCorrectAmount() = runTest {
checkAll(Arb.int(0..50)) { i ->
val tq = TQueue.new<Int>()
atomically {
for (j in 0..i) {
// read to swap read and write lists randomly
if (Random.nextFloat() > 0.9) tq.tryPeek()
tq.write(j)
}
}
atomically { tq.size() } shouldBeExactly i + 1
}
}

@Test fun writeFrontShouldWorkCorrectly() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.writeFront(203) }
atomically { tq.peek() } shouldBeExactly 203
atomically { tq.writeFront(50) }
atomically { tq.peek() } shouldBeExactly 50
atomically { tq.flush() } shouldBe listOf(50, 203)
}

@Test fun removeAllShouldWork() = runTest {
val tq = TQueue.new<Int>()
atomically { tq.removeAll { true } }
atomically { tq.flush() } shouldBe emptyList()

atomically {
for (i in 0..100) {
tq.write(i)
}
tq.removeAll { it.rem(2) == 0 }
tq.flush()
} shouldBe (0..100).filter { it.rem(2) == 0 }
}
}

0 comments on commit daed4c9

Please sign in to comment.