From 359b4a323d312080012804037eee996abed8fa7a Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Thu, 3 Sep 2020 14:28:05 -0500 Subject: [PATCH] switch to Kotlin serialization, migrate to kotest --- .idea/compiler.xml | 2 +- .idea/kotlinc.xml | 4 +- .idea/misc.xml | 2 +- .idea/modules.xml | 8 - .idea/shoebox.iml | 9 +- build.gradle | 56 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 51 ++-- gradlew.bat | 18 +- .../kotlin/kweb/shoebox/OrderedViewSet.kt | 6 +- src/main/kotlin/kweb/shoebox/Shoebox.kt | 31 +- src/main/kotlin/kweb/shoebox/View.kt | 5 +- .../kotlin/kweb/shoebox/samples/samples.kt | 12 +- .../kweb/shoebox/stores/DirectoryStore.kt | 28 +- .../kotlin/kweb/shoebox/stores/LmdbStore.kt | 70 +++-- src/main/kotlin/kweb/shoebox/utils.kt | 48 +-- .../kotlin/kweb/shoebox/CountingListener.kt | 19 ++ .../kotlin/kweb/shoebox/OrderedViewSetSpec.kt | 280 +++++++++--------- src/test/kotlin/kweb/shoebox/ShoeboxSpec.kt | 208 ++++++------- src/test/kotlin/kweb/shoebox/TestData.kt | 6 + src/test/kotlin/kweb/shoebox/ViewSpec.kt | 204 ++++++------- .../kweb/shoebox/stores/DirectoryStoreSpec.kt | 114 +++---- 23 files changed, 578 insertions(+), 608 deletions(-) delete mode 100644 .idea/modules.xml create mode 100644 src/test/kotlin/kweb/shoebox/CountingListener.kt create mode 100644 src/test/kotlin/kweb/shoebox/TestData.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b74fec4..2e9f993 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -12,7 +12,7 @@ - + diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 3097f31..57f05c9 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 539dfdb..ed26c18 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -44,7 +44,7 @@ - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 54a22fd..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/shoebox.iml b/.idea/shoebox.iml index d6ebd48..78b2cc5 100644 --- a/.idea/shoebox.iml +++ b/.idea/shoebox.iml @@ -1,9 +1,2 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0fad7ee..529e115 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ group 'kweb' -version '0.3.1' +version '0.4.0' buildscript { - ext.kotlin_version = '1.3.72' - ext.dokka_version = '0.10.1' + ext.kotlin_version = '1.4.0' + ext.dokka_version = '1.4.0-rc' repositories { jcenter() @@ -20,16 +20,16 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.3.0' classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } apply plugin: 'kotlin' +apply plugin: 'org.jetbrains.kotlin.plugin.serialization' apply plugin: 'org.jetbrains.dokka' -apply plugin: "info.solidsoft.pitest" apply plugin: "com.github.ben-manes.versions" repositories { @@ -46,39 +46,27 @@ repositories { } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile 'com.github.salomonbrys.kotson:kotson:2.5.0' - compile 'com.google.guava:guava:27.1-jre' - compile 'net.incongru.watchservice:barbary-watchservice:1.0' - compile 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.1' + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + implementation 'com.google.guava:guava:27.1-jre' + implementation 'net.incongru.watchservice:barbary-watchservice:1.0' implementation 'org.lmdbjava:lmdbjava:0.7.0' + compile "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0-RC" - testCompile 'io.kotlintest:kotlintest:2.0.7' -} -/* -task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { - outputFormat = 'html' - outputDirectory = javadoc.destinationDir - inputs.dir 'src/main/kotlin' - samples = ["src/main/kotlin/com/github/sanity/shoebox/samples/samples.kt"] - includes = ['packages.md'] -} -task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { - classifier = 'javadoc' - from javadoc.destinationDir + testImplementation "io.kotest:kotest-runner-junit5-jvm:4.2.3" + testImplementation "io.kotest:kotest-assertions-core-jvm:4.2.3" + testImplementation "io.kotest:kotest-property:4.2.3" } -*/ -pitest { - targetClasses = ['com.github.sanity.shoebox.*'] //by default "${project.group}.*" - threads = 8 - outputFormats = ['HTML'] - jvmArgs = ['-Xmx1024m'] + +test { + useJUnitPlatform() } -/* -artifacts { - archives javadocJar + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } } - */ \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..f3d88b1c2faf2fc91d853cd5d4242b5547257070 100644 GIT binary patch delta 25668 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

r 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 Hp`iW;?oWjv delta 22507 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 Shoebox(store : Store) = Shoebox(store, T::class) -inline fun Shoebox(dir : Path) = Shoebox(DirectoryStore(dir), T::class) -inline fun Shoebox() = Shoebox(MemoryStore(), T::class) -inline fun LmdbShoebox(name: String) = Shoebox(LmdbStore(name), T::class) +fun shoebox(dir : Path, kSerializer: KSerializer) = Shoebox(DirectoryStore(dir, kSerializer)) /** * Can persistently store and retrieve objects, and notify listeners of changes to those objects @@ -38,7 +29,9 @@ inline fun LmdbShoebox(name: String) = Shoebox(LmdbStore(name) * @param directory The path to a directory in which data will be stored, will be created if it doesn't already exist * @param kc The KClass associated with T. To avoid having to provide this use `Shoebox(directory)` */ -class Shoebox(val store: Store, private val kc: KClass) { +class Shoebox(val store: Store) { + + constructor() : this(MemoryStore()) private val keySpecificChangeListeners = ConcurrentHashMap Unit>>() private val newListeners = ConcurrentHashMap, Source) -> Unit>() @@ -169,8 +162,8 @@ class Shoebox(val store: Store, private val kc: KClass) { val store = when (store) { is MemoryStore -> MemoryStore() is DirectoryStore -> - DirectoryStore(store.directory.parent.resolve("${store.directory.fileName}-$name-view")) - is LmdbStore -> LmdbStore("${store.name}-$name-view") + DirectoryStore(store.directory.parent.resolve("${store.directory.fileName}-$name-view"), Reference.serializer()) + is LmdbStore -> LmdbStore("${store.name}-$name-view", Reference.serializer()) else -> throw RuntimeException("Shoebox doesn't currently support creating a view for store type ${store::class.simpleName}") } return View(Shoebox(store), this, verify, by) diff --git a/src/main/kotlin/kweb/shoebox/View.kt b/src/main/kotlin/kweb/shoebox/View.kt index 9379415..2d0d197 100644 --- a/src/main/kotlin/kweb/shoebox/View.kt +++ b/src/main/kotlin/kweb/shoebox/View.kt @@ -1,7 +1,9 @@ package kweb.shoebox +import kotlinx.serialization.Serializable import kweb.shoebox.Source.LOCAL -import kweb.shoebox.View.VerifyBehavior.* +import kweb.shoebox.View.VerifyBehavior.ASYNC_VERIFY +import kweb.shoebox.View.VerifyBehavior.BLOCKING_VERIFY import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.concurrent.thread @@ -150,6 +152,7 @@ class View(val references: Shoebox, BLOCKING_VERIFY, ASYNC_VERIFY } + @Serializable data class Reference(val keys: Set) { constructor() : this(Collections.emptySet()) diff --git a/src/main/kotlin/kweb/shoebox/samples/samples.kt b/src/main/kotlin/kweb/shoebox/samples/samples.kt index a4b5639..01b924a 100644 --- a/src/main/kotlin/kweb/shoebox/samples/samples.kt +++ b/src/main/kotlin/kweb/shoebox/samples/samples.kt @@ -1,7 +1,7 @@ package kweb.shoebox.samples -import kweb.shoebox.Shoebox -import kweb.shoebox.View +import kotlinx.serialization.Serializable +import kweb.shoebox.shoebox import java.nio.file.Files /** @@ -11,9 +11,9 @@ import java.nio.file.Files fun basic_usage_sample() { val dir = Files.createTempDirectory("sb-") - val userStore = Shoebox(dir.resolve("users")) - val usersByEmail = View(Shoebox(dir.resolve("usersByEmail")), userStore, viewBy = User::email) - val usersByGender = View(Shoebox(dir.resolve("usersByGender")), userStore, viewBy = User::gender) + val userStore = shoebox(dir.resolve("users"), User.serializer()) + val usersByEmail = userStore.view("usersByEmail", User::email) + val usersByGender = userStore.view("usersByGender", User::gender) userStore["ian"] = User("Ian Clarke", "male", "ian@blah.com") userStore["fred"] = User("Fred Smith", "male", "fred@blah.com") @@ -33,4 +33,4 @@ fun basic_usage_sample() { userStore["fred"] = userStore["fred"]!!.copy(gender = "female") // Prints "fred ceased to be male" } -data class User(val name : String, val gender : String, val email : String) \ No newline at end of file +@Serializable data class User(val name : String, val gender : String, val email : String) \ No newline at end of file diff --git a/src/main/kotlin/kweb/shoebox/stores/DirectoryStore.kt b/src/main/kotlin/kweb/shoebox/stores/DirectoryStore.kt index 2700c22..23d2a47 100644 --- a/src/main/kotlin/kweb/shoebox/stores/DirectoryStore.kt +++ b/src/main/kotlin/kweb/shoebox/stores/DirectoryStore.kt @@ -1,30 +1,26 @@ package kweb.shoebox.stores -import com.fatboyindustrial.gsonjavatime.Converters -import com.google.common.cache.* +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader.InvalidCacheLoadException -import com.google.gson.* -import com.google.gson.reflect.TypeToken +import com.google.common.cache.LoadingCache +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json import kweb.shoebox.* import java.net.URLDecoder -import java.nio.file.* +import java.nio.file.Files +import java.nio.file.Path import java.nio.file.attribute.FileTime -import java.time.* +import java.time.Duration +import java.time.Instant import java.util.concurrent.TimeUnit import java.util.regex.Pattern -import kotlin.reflect.KClass /** * Created by ian on 3/22/17. */ -inline fun DirectoryStore(directory : Path) = DirectoryStore(directory, T::class) - -val defaultGson = Converters.registerAll(GsonBuilder()).let { - it.registerTypeAdapter(object : TypeToken() {}.type, DurationConverter()) -}.create() - -class DirectoryStore(val directory: Path, private val kc: KClass, val gson: Gson = defaultGson) : Store { +class DirectoryStore(val directory: Path, private val kSerializer: KSerializer) : Store { companion object { private const val LOCK_FILENAME = "shoebox.lock" private val LOCK_TOUCH_TIME = Duration.ofMillis(100) @@ -42,7 +38,7 @@ class DirectoryStore(val directory: Path, private val kc: KClass, va throw IllegalStateException("File $filePath is a directory, not a file") } val o = filePath.newBufferedReader().use { - gson.fromJson(it, kc.javaObjectType) + Json.decodeFromString(kSerializer, it.readText()) } CachedValueWithTime(o, Files.getLastModifiedTime(filePath).toInstant()) } else { @@ -141,7 +137,7 @@ class DirectoryStore(val directory: Path, private val kc: KClass, va if (!directory.exists()) throw RuntimeException("Parent directory doesn't exist") val filePath = toPath(key) filePath.newBufferedWriter().use { - gson.toJson(value, kc.javaObjectType, it) + it.write(Json.encodeToString(kSerializer, value)) } } return previousValue diff --git a/src/main/kotlin/kweb/shoebox/stores/LmdbStore.kt b/src/main/kotlin/kweb/shoebox/stores/LmdbStore.kt index 4569b91..15f6f2d 100644 --- a/src/main/kotlin/kweb/shoebox/stores/LmdbStore.kt +++ b/src/main/kotlin/kweb/shoebox/stores/LmdbStore.kt @@ -1,32 +1,26 @@ package kweb.shoebox.stores -import com.fatboyindustrial.gsonjavatime.Converters -import com.google.gson.* -import com.google.gson.reflect.TypeToken -import kweb.shoebox.* -import java.nio.file.* -import java.time.* -import kotlin.reflect.KClass - -import org.lmdbjava.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.protobuf.ProtoBuf +import kweb.shoebox.KeyValue +import kweb.shoebox.Shoebox +import kweb.shoebox.Store +import org.lmdbjava.Dbi +import org.lmdbjava.DbiFlags +import org.lmdbjava.Env import java.io.File import java.nio.ByteBuffer import java.nio.ByteBuffer.allocateDirect import java.nio.charset.StandardCharsets.UTF_8 -import kotlin.io.FileSystemException - +import java.nio.file.InvalidPathException -/** - * TODO: remove dependence on gson - */ - -inline fun LmdbStore(name: String) = LmdbStore(name, T::class) /* val defaultGson: Gson = Converters.registerAll(GsonBuilder()).let { it.registerTypeAdapter(object : TypeToken() {}.type, DurationConverter()) }.create() */ -class LmdbStore(val name: String, private val kc: KClass, val gson: Gson = defaultGson) : Store { +class LmdbStore(val name: String, private val kSerializer: KSerializer) : Store { companion object { private val home: String = System.getProperty("user.dir") @@ -60,20 +54,21 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: * * @return The keys and their corresponding values in this [Shoebox] */ - override val entries: Iterable> get() { - val ret = mutableSetOf>() - env.txnRead().use { txn -> - dbi.iterate(txn).use { c -> - c.forEach { - val k = UTF_8.decode(it.key()).toString() - val v = gson.fromJson(UTF_8.decode(it.`val`()).toString(), kc.javaObjectType) - ret.add(KeyValue(k, v)) + override val entries: Iterable> + get() { + val ret = mutableSetOf>() + env.txnRead().use { txn -> + dbi.iterate(txn).use { c -> + c.forEach { + val k = UTF_8.decode(it.key()).toString() + val v = ProtoBuf.decodeFromByteArray(kSerializer, it.`val`().array()) + ret.add(KeyValue(k, v)) + } } + txn.abort() } - txn.abort() + return ret } - return ret - } /** * Retrieve a value, similar to [Map.get] @@ -82,14 +77,14 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: * @return The value associated with the key, or null if no value is associated */ override operator fun get(key: String): T? { - require(key.isNotBlank()) {"key(\"$key\") must not be blank"} + require(key.isNotBlank()) { "key(\"$key\") must not be blank" } val k = allocateDirect(env.maxKeySize) k.put(key.toByteArray(UTF_8)).flip() var ret: T? = null env.txnRead().use { txn -> val v: ByteBuffer? = dbi.get(txn, k) if (v != null) { - ret = gson.fromJson(UTF_8.decode(v).toString(), kc.javaObjectType) + ret = Json.decodeFromString(kSerializer, UTF_8.decode(v).toString()) } txn.abort() } @@ -101,8 +96,8 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: * * @param key The key associated with the value to be removed, similar to [MutableMap.remove] */ - override fun remove(key: String) : T? { - require(key.isNotBlank()) {"key(\"$key\") must not be blank"} + override fun remove(key: String): T? { + require(key.isNotBlank()) { "key(\"$key\") must not be blank" } val k = allocateDirect(env.maxKeySize) k.put(key.toByteArray(UTF_8)).flip() var ret: T? = null @@ -110,7 +105,8 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: // who needs the value? val oldv: ByteBuffer? = dbi.get(txn, k) if (oldv != null) { - ret = gson.fromJson(UTF_8.decode(oldv).toString(), kc.javaObjectType) + // ret = gson.fromJson(UTF_8.decode(oldv).toString(), kc.javaObjectType) + ret = Json.decodeFromString(kSerializer, UTF_8.decode(oldv).toString()) } dbi.delete(txn, k) txn.commit() @@ -124,11 +120,11 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: * @param key The key associated with the value to be set or changed * @param value The new value */ - override operator fun set(key: String, value: T) : T? { - require(key.isNotBlank()) {"key(\"$key\") must not be blank"} + override operator fun set(key: String, value: T): T? { + require(key.isNotBlank()) { "key(\"$key\") must not be blank" } val k = allocateDirect(env.maxKeySize) k.put(key.toByteArray(UTF_8)).flip() - val bytes = gson.toJson(value, kc.javaObjectType).toByteArray(UTF_8) + val bytes = Json.encodeToString(kSerializer, value).toByteArray(UTF_8) val v = allocateDirect(bytes.size) v.put(bytes).flip() var ret: T? = null @@ -136,7 +132,7 @@ class LmdbStore(val name: String, private val kc: KClass, val gson: // is the old value necessary? val oldv: ByteBuffer? = dbi.get(txn, k) if (oldv != null) { - ret = gson.fromJson(UTF_8.decode(oldv).toString(), kc.javaObjectType) + ret = Json.decodeFromString(kSerializer, UTF_8.decode(oldv).toString()) } dbi.put(txn, k, v) txn.commit() diff --git a/src/main/kotlin/kweb/shoebox/utils.kt b/src/main/kotlin/kweb/shoebox/utils.kt index f2cff99..bcb6fec 100644 --- a/src/main/kotlin/kweb/shoebox/utils.kt +++ b/src/main/kotlin/kweb/shoebox/utils.kt @@ -1,11 +1,10 @@ package kweb.shoebox -import com.google.gson.* -import kweb.shoebox.BinarySearchResult.* -import java.lang.reflect.Type -import java.nio.file.* -import java.time.* -import java.time.format.DateTimeFormatter +import kweb.shoebox.BinarySearchResult.Between +import kweb.shoebox.BinarySearchResult.Exact +import java.nio.file.Files +import java.nio.file.OpenOption +import java.nio.file.Path import java.util.* import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicLong @@ -85,40 +84,3 @@ private fun toBinarySearchResult(result: Int): BinarySearchResult { } } - -/** - * GSON serialiser/deserialiser for converting [Instant] objects. - */ -class DurationConverter : JsonSerializer, JsonDeserializer { - - override fun serialize(src: Duration, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(src.toMillis()) - } - - /** - * Gson invokes this call-back method during deserialization when it encounters a field of the - * specified type. - * - * - * - * In the implementation of this call-back method, you should consider invoking - * [JsonDeserializationContext.deserialize] method to defaultGson objects - * for any non-trivial field of the returned object. However, you should never invoke it on the - * the same type passing `json` since that will cause an infinite loop (Gson will call your - * call-back method again). - * - * @param json The Json data being deserialized - * @param typeOfT The type of the Object to deserialize to - * @return a deserialized object of the specified type typeOfT which is a subclass of `T` - * @throws JsonParseException if json is not in the expected format of `typeOfT` - */ - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Duration { - return Duration.ofNanos(json.asLong) - } - - companion object { - /** Formatter. */ - private val FORMATTER = DateTimeFormatter.ISO_INSTANT - } -} \ No newline at end of file diff --git a/src/test/kotlin/kweb/shoebox/CountingListener.kt b/src/test/kotlin/kweb/shoebox/CountingListener.kt new file mode 100644 index 0000000..8acdb51 --- /dev/null +++ b/src/test/kotlin/kweb/shoebox/CountingListener.kt @@ -0,0 +1,19 @@ +package kweb.shoebox + +import java.util.concurrent.atomic.AtomicInteger + +class CountingListener(val correct: KeyValue) { + private val _counter = AtomicInteger(0) + + fun add(kv: KeyValue) { + _counter.incrementAndGet() + if (kv != correct) throw AssertionError("$kv != $correct") + } + + fun remove(kv: KeyValue) { + _counter.incrementAndGet() + if (kv != correct) throw AssertionError("$kv != $correct") + } + + val counter get() = _counter.get() +} \ No newline at end of file diff --git a/src/test/kotlin/kweb/shoebox/OrderedViewSetSpec.kt b/src/test/kotlin/kweb/shoebox/OrderedViewSetSpec.kt index 319161d..120d194 100644 --- a/src/test/kotlin/kweb/shoebox/OrderedViewSetSpec.kt +++ b/src/test/kotlin/kweb/shoebox/OrderedViewSetSpec.kt @@ -1,186 +1,194 @@ package kweb.shoebox -import io.kotlintest.matchers.* -import io.kotlintest.specs.FreeSpec -import kweb.shoebox.data.Gender.* +import io.kotest.core.spec.style.FunSpec +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.launch +import kweb.shoebox.data.Gender.FEMALE +import kweb.shoebox.data.Gender.MALE import kweb.shoebox.data.User import kweb.shoebox.stores.MemoryStore /** * Created by ian on 3/14/17. */ -class OrderedViewSetSpec : FreeSpec() { - init { - "an OrderedViewSet" - { - - "on initialization" - { - val userMap = Shoebox(MemoryStore()) - userMap["zool"] = User("Zool", MALE) - userMap["george"] = User("George", MALE) - userMap["paul"] = User("Paul", MALE) - userMap["xavier"] = User("Xavier", MALE) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - - val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) - val menInOrder = listOf( - KeyValue("george", User("George", MALE)), - KeyValue("jack", User("Jack", MALE)), - KeyValue("paul", User("Paul", MALE)), - KeyValue("xavier", User("Xavier", MALE)), - KeyValue("zool", User("Zool", MALE)) - ) - "keyValueEntries should return men in correct order" { - maleViewSet.keyValueEntries shouldEqual menInOrder - } - "entries should return men in correct order" { - maleViewSet.entries shouldEqual menInOrder.map { it.value } - } - - val femaleViewSet = OrderedViewSet(viewByGender, "FEMALE", compareBy(User::name)) - femaleViewSet.keyValueEntries shouldEqual listOf(KeyValue("jill", User("Jill", FEMALE))) +class OrderedViewSetSpec : FunSpec({ + + context("on initialization") { + val userMap = Shoebox(MemoryStore()) + userMap["zool"] = User("Zool", MALE) + userMap["george"] = User("George", MALE) + userMap["paul"] = User("Paul", MALE) + userMap["xavier"] = User("Xavier", MALE) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + + val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + val menInOrder = listOf( + KeyValue("george", User("George", MALE)), + KeyValue("jack", User("Jack", MALE)), + KeyValue("paul", User("Paul", MALE)), + KeyValue("xavier", User("Xavier", MALE)), + KeyValue("zool", User("Zool", MALE)) + ) + test("keyValueEntries should return men in correct order") { + maleViewSet.keyValueEntries shouldBe menInOrder + } + test("entries should return men in correct order") { + maleViewSet.entries shouldBe menInOrder.map { it.value } } - "when a value is added" - { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["paul"] = User("Paul", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + val femaleViewSet = OrderedViewSet(viewByGender, "FEMALE", compareBy(User::name)) + femaleViewSet.keyValueEntries shouldBe listOf(KeyValue("jill", User("Jill", FEMALE))) + } - val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + context("when a value is added") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["paul"] = User("Paul", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - var callCount = 0 - val insertHandle = maleViewSet.onInsert { ix, keyValue -> - callCount++ - "should call the insert handler with the correct values" { + val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + + var callCount = 0 + val insertHandle = maleViewSet.onInsert { ix, keyValue -> + callCount++ + launch { + test("should call the insert handler with the correct values") { callCount shouldBe 1 ix shouldBe 2 keyValue shouldBe KeyValue("peter", User("Peter", MALE)) } } - userMap["peter"] = User("Peter", MALE) - "should call the insert handler" { - callCount shouldBe 1 - } + } + userMap["peter"] = User("Peter", MALE) + test("should call the insert handler") { + callCount shouldBe 1 + } - "should include newly inserted value in keyValueEntries" { - maleViewSet.keyValueEntries shouldEqual listOf( - KeyValue("jack", User("Jack", MALE)), - KeyValue("paul", User("Paul", MALE)), - KeyValue("peter", User("Peter", MALE)) - ) - } + test("should include newly inserted value in keyValueEntries") { + maleViewSet.keyValueEntries shouldBe listOf( + KeyValue("jack", User("Jack", MALE)), + KeyValue("paul", User("Paul", MALE)), + KeyValue("peter", User("Peter", MALE)) + ) + } - "should not call the insert handler after it has been deleted" { - maleViewSet.deleteInsertListener(insertHandle) - userMap["toby"] = User("Toby", MALE) - callCount shouldBe 1 - } + test("should not call the insert handler after it has been deleted") { + maleViewSet.deleteInsertListener(insertHandle) + userMap["toby"] = User("Toby", MALE) + callCount shouldBe 1 } + } - "when a value is deleted" - { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["paul"] = User("Paul", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + context("when a value is deleted") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["paul"] = User("Paul", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) - var callCount = 0 - val removeHandle = maleViewSet.onRemove { ix, keyValue -> - callCount++ - "should call the delete handler with the correct values" { + var callCount = 0 + val removeHandle = maleViewSet.onRemove { ix, keyValue -> + callCount++ + launch { + test("should call the delete handler with the correct values") { callCount shouldBe 1 ix shouldBe 0 keyValue shouldBe KeyValue("jack", User("Jack", MALE)) } } - userMap.remove("jack") - "should call the remove handler" { - callCount shouldBe 1 - } - - "shouldn't include newly removed value in keyValueEntries" { - maleViewSet.keyValueEntries shouldEqual listOf( - KeyValue("paul", User("Paul", MALE)) - ) - } + } + userMap.remove("jack") + test("should call the remove handler") { + callCount shouldBe 1 + } - "should not call the handler after it has been deleted" { - maleViewSet.deleteRemoveListener(removeHandle) - userMap.remove("paul") - callCount shouldBe 1 - } + test("shouldn't include newly removed value in keyValueEntries") { + maleViewSet.keyValueEntries shouldBe listOf( + KeyValue("paul", User("Paul", MALE)) + ) } - " when a second value is added that is not distinguishable based on the supplied comparator" { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + test("should not call the handler after it has been deleted") { + maleViewSet.deleteRemoveListener(removeHandle) + userMap.remove("paul") + callCount shouldBe 1 + } + } - val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + test("when a second value is added that is not distinguishable based on the supplied comparator") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - maleViewSet.onInsert { _, kv -> + val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) - } + maleViewSet.onInsert { _, kv -> - userMap["paul"] = User("Paul", MALE) } - "should detect a value reorder" - { - val userMap = Shoebox(MemoryStore()) - val jackUser = User("Jack", MALE) - userMap["jack"] = jackUser - userMap["paul"] = User("Paul", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - - val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) - - var callCount = 0 - val renamedJackUser = jackUser.copy(name = "Zeus") - maleViewSet.onInsert { ix, keyValue -> - callCount++ - "should call the insert handler with the correct values" { + userMap["paul"] = User("Paul", MALE) + } + + test("should detect a value reorder") { + val userMap = Shoebox(MemoryStore()) + val jackUser = User("Jack", MALE) + userMap["jack"] = jackUser + userMap["paul"] = User("Paul", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + + val maleViewSet = OrderedViewSet(viewByGender, "MALE", compareBy(User::name)) + + var callCount = 0 + val renamedJackUser = jackUser.copy(name = "Zeus") + maleViewSet.onInsert { ix, keyValue -> + callCount++ + launch { + test("should call the insert handler with the correct values") { ix shouldBe 2 keyValue shouldBe KeyValue("jack", renamedJackUser) } } - maleViewSet.onRemove { ix, keyValue -> - callCount++ - "should call the remove handler with the correct values" { + } + maleViewSet.onRemove { ix, keyValue -> + callCount++ + launch { + test("should call the remove handler with the correct values") { ix shouldBe 0 keyValue shouldBe KeyValue("jack", jackUser) } } - userMap["jack"] = renamedJackUser + } + userMap["jack"] = renamedJackUser - "should call both handlers" { - callCount shouldBe 2 - } + test("should call both handlers") { + callCount shouldBe 2 } + } - "should handle this case discovered while debugging" - { - data class Dog(val name: String, val color: String, val breed: String) - - val dogs = Shoebox() - listOf( - Dog(name = "hot dog", color = "tan", breed = "dachshund"), - Dog(name = "toby", color = "tan", breed = "labrador") - ).forEach { dogs[it.name] = it } - - val viewByColor = dogs.view("dogsByColor", Dog::color) - val tanDogs = viewByColor.orderedSet("tan", compareBy(Dog::color)) - "dogs should be listed with correct test in correct order" { - tanDogs.entries.size shouldBe 2 - tanDogs.entries[0] shouldBe Dog(name = "hot dog", color = "tan", breed = "dachshund") - tanDogs.entries[1] shouldBe Dog(name = "toby", color = "tan", breed = "labrador") - } + context("should handle this case discovered while debugging") { + data class Dog(val name: String, val color: String, val breed: String) + + val dogs = Shoebox(MemoryStore()) + listOf( + Dog(name = "hot dog", color = "tan", breed = "dachshund"), + Dog(name = "toby", color = "tan", breed = "labrador") + ).forEach { dogs[it.name] = it } + + val viewByColor = dogs.view("dogsByColor", Dog::color) + val tanDogs = viewByColor.orderedSet("tan", compareBy(Dog::color)) + test("dogs should be listed with correct test in correct order") { + tanDogs.entries.size shouldBe 2 + tanDogs.entries[0] shouldBe Dog(name = "hot dog", color = "tan", breed = "dachshund") + tanDogs.entries[1] shouldBe Dog(name = "toby", color = "tan", breed = "labrador") } } - } -} + +}) diff --git a/src/test/kotlin/kweb/shoebox/ShoeboxSpec.kt b/src/test/kotlin/kweb/shoebox/ShoeboxSpec.kt index 25e251d..09aa7e5 100644 --- a/src/test/kotlin/kweb/shoebox/ShoeboxSpec.kt +++ b/src/test/kotlin/kweb/shoebox/ShoeboxSpec.kt @@ -1,137 +1,139 @@ package kweb.shoebox -import kweb.shoebox.stores.MemoryStore -import io.kotlintest.matchers.shouldBe -import io.kotlintest.matchers.shouldEqual -import io.kotlintest.specs.FreeSpec +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.launch import kweb.shoebox.Source.LOCAL +import kweb.shoebox.stores.MemoryStore import java.util.concurrent.atomic.AtomicInteger /** * Created by ian on 3/12/17. */ -class ShoeboxSpec : FreeSpec() { +class ShoeboxSpec : FunSpec({ + context("A Shoebox store") { + val object1 = TestData(1, 2) + val object2 = TestData(3, 4) + context("when an item is stored") { + val pm = Shoebox(MemoryStore()) + pm["key1"] = object1 + test("should retrieve the data") { + val retrievedObject: TestData? = pm["key1"] + retrievedObject shouldBe object1 + } + } + context("when an item is removed") { + val pm = Shoebox(MemoryStore()) + pm["key1"] = object1 + pm.remove("key1") + test("should return null for the removed key") { + pm["key1"] shouldBe null + } + } + test("should iterate through data") { + val pm = Shoebox(MemoryStore()) + pm["key1"] = TestData(1, 2) + pm["key2"] = TestData(3, 4) + val entries = pm.entries + entries.map { KeyValue(it.key, it.value) }.toSet() shouldBe setOf(KeyValue("key1", TestData(1, 2)), KeyValue("key2", TestData(3, 4))) + } - init { - "A Shoebox store" - { + context("should trigger appropriate callbacks when") { val object1 = TestData(1, 2) val object2 = TestData(3, 4) - "when an item is stored" - { + val object3 = TestData(5, 4) + context("a new object is created") { val pm = Shoebox(MemoryStore()) - pm["key1"] = object1 - "should retrieve the data" { - val retrievedObject: TestData? = pm["key1"] - retrievedObject shouldEqual object1 + var callCount = AtomicInteger(0) + val handle: Long = pm.onNew { keyValue, source -> + callCount.incrementAndGet() shouldBe 1 + keyValue shouldBe KeyValue("key1", object1) + source shouldBe LOCAL } - } - "when an item is removed" - { - val pm = Shoebox(MemoryStore()) pm["key1"] = object1 - pm.remove("key1") - "should return null for the removed key" { - pm["key1"] shouldEqual null + test("should trigger callback") { callCount.get() shouldBe 1 } + pm.deleteNewListener(handle) + test("should not trigger callback after it has been removed") { + callCount.get() shouldBe 1 + pm["key3"] = object3 + callCount.get() shouldBe 1 } - } - "should iterate through data" { - val pm = Shoebox(MemoryStore()) - pm["key1"] = TestData(1, 2) - pm["key2"] = TestData(3, 4) - val entries = pm.entries - entries.map { KeyValue(it.key, it.value) }.toSet() shouldEqual setOf(KeyValue("key1", TestData(1, 2)), KeyValue("key2", TestData(3, 4))) + pm.remove("key3") } - "should trigger appropriate callbacks when" - { - val object1 = TestData(1, 2) - val object2 = TestData(3, 4) - val object3 = TestData(5, 4) - "a new object is created" - { - val pm = Shoebox(MemoryStore()) - var callCount = AtomicInteger(0) - val handle: Long = pm.onNew { keyValue, source -> - callCount.incrementAndGet() shouldEqual 1 - keyValue shouldEqual KeyValue("key1", object1) - source shouldEqual LOCAL - } - pm["key1"] = object1 - "should trigger callback" { callCount.get() shouldEqual 1 } - pm.deleteNewListener(handle) - "should not trigger callback after it has been removed" { - callCount.get() shouldEqual 1 - pm["key3"] = object3 - callCount.get() shouldEqual 1 - } - pm.remove("key3") - - } - "an object is changed" - { - val pm = Shoebox(MemoryStore()) - pm["key1"] = object1 - var globalCallCount = 0 - var keySpecificCallCount = 0 - val globalChangeHandle = pm.onChange { prev, nextKeyValue, source -> - globalCallCount++ - "global change callback should be called with the correct parameters" { - prev shouldEqual object1 - nextKeyValue shouldEqual KeyValue("key1", object2) - source shouldEqual LOCAL + context("an object is changed") { + val pm = Shoebox(MemoryStore()) + pm["key1"] = object1 + var globalCallCount = 0 + var keySpecificCallCount = 0 + val globalChangeHandle = pm.onChange { prev, nextKeyValue, source -> + globalCallCount++ + launch { + test("global change callback should be called with the correct parameters") { + prev shouldBe object1 + nextKeyValue shouldBe KeyValue("key1", object2) + source shouldBe LOCAL } } - val keySpecificChangeHandle = pm.onChange("key1") { old, new, source -> - keySpecificCallCount++ - "key-specific change callback should be called with the correct parameters" { - old shouldEqual object1 - new shouldEqual object2 + } + val keySpecificChangeHandle = pm.onChange("key1") { old, new, source -> + keySpecificCallCount++ + launch { + test("key-specific change callback should be called with the correct parameters") { + old shouldBe object1 + new shouldBe object2 source shouldBe LOCAL } } - pm["key1"] = object2 - "callbacks should each be called once" { - globalCallCount shouldEqual 1 - keySpecificCallCount shouldEqual 1 - } - pm["key1"] = object2.copy() // Shouldn't trigger the callbacks again - "callbacks shouldn't be called again if the object value hasn't changed" { - globalCallCount shouldEqual 1 - keySpecificCallCount shouldEqual 1 - } + } + pm["key1"] = object2 + test("callbacks should each be called once") { + globalCallCount shouldBe 1 + keySpecificCallCount shouldBe 1 + } + pm["key1"] = object2.copy() // Shouldn't trigger the callbacks again + test("callbacks shouldn't be called again if the object value hasn't changed") { + globalCallCount shouldBe 1 + keySpecificCallCount shouldBe 1 + } - pm.deleteChangeListener(globalChangeHandle) - pm.deleteChangeListener("key1", keySpecificChangeHandle) - pm["key1"] = object3 - "callbacks shouldn't be called after they've been removed" { - globalCallCount shouldEqual 1 - keySpecificCallCount shouldEqual 1 - } + pm.deleteChangeListener(globalChangeHandle) + pm.deleteChangeListener("key1", keySpecificChangeHandle) + pm["key1"] = object3 + test("callbacks shouldn't be called after they've been removed") { + globalCallCount shouldBe 1 + keySpecificCallCount shouldBe 1 + } - } - "should trigger object removal callback" - { - val pm = Shoebox(MemoryStore()) - pm["key1"] = object3 - var callCount = 0 - val onRemoveHandle = pm.onRemove { keyValue, source -> - callCount++ - keyValue shouldEqual KeyValue("key1", object3) - source shouldEqual LOCAL + } + context("should trigger object removal callback") { + val pm = Shoebox(MemoryStore()) + pm["key1"] = object3 + var callCount = 0 + val onRemoveHandle = pm.onRemove { keyValue, source -> + callCount++ + keyValue shouldBe KeyValue("key1", object3) + source shouldBe LOCAL - } - pm.remove("key1") - "callback should be called once" { - callCount shouldEqual 1 - } - pm.deleteRemoveListener(onRemoveHandle) - pm["key3"] = object3 - pm.remove("key3") - "callback shouldn't be called again after it has been removed" { - callCount shouldEqual 1 - } + } + pm.remove("key1") + test("callback should be called once") { + callCount shouldBe 1 + } + pm.deleteRemoveListener(onRemoveHandle) + pm["key3"] = object3 + pm.remove("key3") + test("callback shouldn't be called again after it has been removed") { + callCount shouldBe 1 } } } } - data class TestData(val one: Int, val two: Int) -} \ No newline at end of file +}) + + diff --git a/src/test/kotlin/kweb/shoebox/TestData.kt b/src/test/kotlin/kweb/shoebox/TestData.kt new file mode 100644 index 0000000..fe7aa47 --- /dev/null +++ b/src/test/kotlin/kweb/shoebox/TestData.kt @@ -0,0 +1,6 @@ +package kweb.shoebox + +import kotlinx.serialization.Serializable + +@Serializable +data class TestData(val one: Int, val two: Int) \ No newline at end of file diff --git a/src/test/kotlin/kweb/shoebox/ViewSpec.kt b/src/test/kotlin/kweb/shoebox/ViewSpec.kt index 90579a0..6893481 100644 --- a/src/test/kotlin/kweb/shoebox/ViewSpec.kt +++ b/src/test/kotlin/kweb/shoebox/ViewSpec.kt @@ -1,132 +1,116 @@ package kweb.shoebox -import io.kotlintest.matchers.* -import io.kotlintest.specs.FreeSpec -import kweb.shoebox.data.Gender.* +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import kweb.shoebox.data.Gender.FEMALE +import kweb.shoebox.data.Gender.MALE import kweb.shoebox.data.User import kweb.shoebox.stores.MemoryStore -import java.util.concurrent.atomic.AtomicInteger /** * Created by ian on 3/12/17. */ -class ViewSpec : FreeSpec() { - init { - "on initialization" - { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - "references should be correct" { - viewByGender.references["MALE"]!!.keys shouldEqual setOf("jack") - viewByGender.references["FEMALE"]!!.keys shouldEqual setOf("jill") - } - "should return correctly categorized objects" { - viewByGender["MALE"] shouldEqual setOf(User("Jack", MALE)) - viewByGender["FEMALE"] shouldEqual setOf(User("Jill", FEMALE)) - } +class ViewSpec : FunSpec({ + context("on initialization") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + test("references should be correct") { + viewByGender.references["MALE"]!!.keys shouldBe setOf("jack") + viewByGender.references["FEMALE"]!!.keys shouldBe setOf("jill") } - "on change of a view name after initialization" - { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - - val addListener = CountingListener(KeyValue("jack", User("Jack", FEMALE))) - viewByGender.onAdd("MALE", addListener::add) // Should have no effect - viewByGender.onAdd("FEMALE", addListener::add) - - val removeListener = CountingListener(KeyValue("jack", User("Jack", MALE))) - viewByGender.onRemove("MALE", removeListener::remove) - viewByGender.onRemove("FEMALE", removeListener::remove) // Should have no effect - - userMap["jack"] = User("Jack", FEMALE) - - "references should be correct" { - viewByGender.references["MALE"]!!.keys should beEmpty() - viewByGender.references["FEMALE"]!!.keys shouldEqual setOf("jack", "jill") - - } - "actual values returned should be correct" { - viewByGender["FEMALE"] shouldEqual setOf(User("Jack", FEMALE), User("Jill", FEMALE)) - viewByGender["MALE"] should beEmpty() - } - "listeners should have been called" { - addListener.counter shouldEqual 1 - removeListener.counter shouldEqual 1 - } + test("should return correctly categorized objects") { + viewByGender["MALE"] shouldBe setOf(User("Jack", MALE)) + viewByGender["FEMALE"] shouldBe setOf(User("Jill", FEMALE)) } + } + context("on change of a view name after initialization") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - "should respond to a failure to sync a viewName change correctly" { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - userMap["jack"] = User("Jack", FEMALE) - viewByGender.addValue("MALE", "jack") - viewByGender.references["MALE"]!!.keys shouldEqual setOf("jack") - viewByGender["MALE"] should beEmpty() - } + val addListener = CountingListener(KeyValue("jack", User("Jack", FEMALE))) + viewByGender.onAdd("MALE", addListener::add) // Should have no effect + viewByGender.onAdd("FEMALE", addListener::add) - "should respond to an addition correctly" { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - val addListener = CountingListener(KeyValue("paul", User("Paul", MALE))) - viewByGender.onAdd("MALE", addListener::add) - userMap["paul"] = User("Paul", MALE) - viewByGender.references["MALE"]!!.keys shouldEqual setOf("jack", "paul") - viewByGender["MALE"] shouldEqual setOf(User("Paul", MALE), User("Jack", MALE)) - - viewByGender.references["FEMALE"]!!.keys shouldEqual setOf("jill") - viewByGender["FEMALE"] shouldEqual setOf(User("Jill", FEMALE)) - - addListener.counter shouldEqual 1 - } + val removeListener = CountingListener(KeyValue("jack", User("Jack", MALE))) + viewByGender.onRemove("MALE", removeListener::remove) + viewByGender.onRemove("FEMALE", removeListener::remove) // Should have no effect + + userMap["jack"] = User("Jack", FEMALE) - "should respond to a deletion correctly" { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - val removeListener = CountingListener(KeyValue("jill", User("Jill", FEMALE))) - viewByGender.onRemove("FEMALE", removeListener::remove) - userMap.remove("jill") - viewByGender.references["FEMALE"]!!.keys should beEmpty() - viewByGender["FEMALE"] should beEmpty() - viewByGender.references["MALE"]!!.keys shouldEqual setOf("jack") - viewByGender["MALE"] shouldEqual setOf(User("Jack", MALE)) - - removeListener.counter shouldEqual 1 + test("references should be correct") { + viewByGender.references["MALE"]!!.keys shouldBe emptySet() + viewByGender.references["FEMALE"]!!.keys shouldBe setOf("jack", "jill") + + } + test("actual values returned should be correct") { + viewByGender["FEMALE"] shouldBe setOf(User("Jack", FEMALE), User("Jill", FEMALE)) + viewByGender["MALE"] shouldBe emptySet() } - "should correct for a failure to sync a delete" { - val userMap = Shoebox(MemoryStore()) - userMap["jack"] = User("Jack", MALE) - userMap["jill"] = User("Jill", FEMALE) - val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) - userMap.remove("jill") - viewByGender.addValue("FEMALE", "jill") - viewByGender.references["FEMALE"]!!.keys shouldEqual setOf("jill") - viewByGender["FEMALE"] should beEmpty() + test("listeners should have been called") { + addListener.counter shouldBe 1 + removeListener.counter shouldBe 1 } } - class CountingListener(val correct : KeyValue) { - private val _counter = AtomicInteger(0) - - fun add(kv : KeyValue) { - _counter.incrementAndGet() - if (kv != correct) throw AssertionError("$kv != $correct") - } + test("should respond to a failure to sync a viewName change correctly") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + userMap["jack"] = User("Jack", FEMALE) + viewByGender.addValue("MALE", "jack") + viewByGender.references["MALE"]!!.keys shouldBe setOf("jack") + viewByGender["MALE"] shouldBe emptySet() + } - fun remove(kv : KeyValue) { - _counter.incrementAndGet() - if (kv != correct) throw AssertionError("$kv != $correct") - } + test("should respond to an addition correctly") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + val addListener = CountingListener(KeyValue("paul", User("Paul", MALE))) + viewByGender.onAdd("MALE", addListener::add) + userMap["paul"] = User("Paul", MALE) + viewByGender.references["MALE"]!!.keys shouldBe setOf("jack", "paul") + viewByGender["MALE"] shouldBe setOf(User("Paul", MALE), User("Jack", MALE)) + + viewByGender.references["FEMALE"]!!.keys shouldBe setOf("jill") + viewByGender["FEMALE"] shouldBe setOf(User("Jill", FEMALE)) + + addListener.counter shouldBe 1 + } - val counter get() = _counter.get() + test("should respond to a deletion correctly") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + val removeListener = CountingListener(KeyValue("jill", User("Jill", FEMALE))) + viewByGender.onRemove("FEMALE", removeListener::remove) + userMap.remove("jill") + viewByGender.references["FEMALE"]!!.keys shouldBe emptySet() + viewByGender["FEMALE"] shouldBe emptySet() + viewByGender.references["MALE"]!!.keys shouldBe setOf("jack") + viewByGender["MALE"] shouldBe setOf(User("Jack", MALE)) + + removeListener.counter shouldBe 1 } -} + test("should correct for a failure to sync a delete") { + val userMap = Shoebox(MemoryStore()) + userMap["jack"] = User("Jack", MALE) + userMap["jill"] = User("Jill", FEMALE) + val viewByGender = View(Shoebox(MemoryStore()), viewOf = userMap, viewBy = { it.gender.toString() }) + userMap.remove("jill") + viewByGender.addValue("FEMALE", "jill") + viewByGender.references["FEMALE"]!!.keys shouldBe setOf("jill") + viewByGender["FEMALE"] shouldBe emptySet() + } + + +}) diff --git a/src/test/kotlin/kweb/shoebox/stores/DirectoryStoreSpec.kt b/src/test/kotlin/kweb/shoebox/stores/DirectoryStoreSpec.kt index 35f47b9..91e8ef4 100644 --- a/src/test/kotlin/kweb/shoebox/stores/DirectoryStoreSpec.kt +++ b/src/test/kotlin/kweb/shoebox/stores/DirectoryStoreSpec.kt @@ -1,76 +1,76 @@ package kweb.shoebox.stores -import io.kotlintest.matchers.* -import io.kotlintest.specs.FreeSpec -import kweb.shoebox.ShoeboxSpec -import kweb.shoebox.ShoeboxSpec.TestData +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.comparables.gt +import io.kotest.matchers.shouldBe +import kotlinx.serialization.builtins.serializer +import kweb.shoebox.TestData import java.nio.file.Files import java.nio.file.attribute.FileTime /** * Created by ian on 3/22/17. */ -class DirectoryStoreSpec : FreeSpec() { - init { - "DirectoryStore" - { - "locking" - { - val dir = Files.createTempDirectory("ss-") - val directoryStore = DirectoryStore(dir) - "should defaultGson a lockfile" { - Files.exists(dir.resolve("shoebox.lock")) shouldBe true - } - "should throw an exception if attempting to defaultGson a store for a directory that already has a store" { - shouldThrow { - DirectoryStore(dir) - } - } - "should disregard an old lock" { - val dir = Files.createTempDirectory("ss-") - val lockFilePath = dir.resolve("shoebox.lock") - Files.newBufferedWriter(lockFilePath).use { - it.appendln("lock") - } - Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) - DirectoryStore(dir, TestData::class) - } - - "should update an old lock" { - val dir = Files.createTempDirectory("ss-") - val lockFilePath = dir.resolve("shoebox.lock") - Files.newBufferedWriter(lockFilePath).use { - it.appendln("lock") - } - Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) - DirectoryStore(dir, TestData::class) - Files.getLastModifiedTime(lockFilePath).toMillis() shouldBe gt (System.currentTimeMillis() - 5000) +class DirectoryStoreSpec : FunSpec({ + context("DirectoryStore") { + context("locking") { + val dir = Files.createTempDirectory("ss-") + val directoryStore = DirectoryStore(dir, String.serializer()) + test("should defaultGson a lockfile") { + Files.exists(dir.resolve("shoebox.lock")) shouldBe true + } + test("should throw an exception if attempting to defaultGson a store for a directory that already has a store") { + shouldThrow { + DirectoryStore(dir, TestData.serializer()) } } - val object1 = ShoeboxSpec.TestData(1, 2) - val object2 = ShoeboxSpec.TestData(3, 4) - "when an item is stored" - { - val object1 = ShoeboxSpec.TestData(1, 2) + test("should disregard an old lock") { val dir = Files.createTempDirectory("ss-") - val pm = DirectoryStore(dir) - pm["key1"] = object1 - "should cache the item that was stored" { - pm.cache.get("key1").value shouldEqual object1 + val lockFilePath = dir.resolve("shoebox.lock") + Files.newBufferedWriter(lockFilePath).use { + it.appendLine("lock") } + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) + DirectoryStore(dir, TestData.serializer()) } - "when an item is replaced" - { - val dir = Files.createTempDirectory("ss-") - val pm = DirectoryStore(dir) - pm["key1"] = object1 - pm["key1"] = object2 - "should have cached the replaced data" { - pm.cache.get("key1").value shouldEqual object2 - } - "should retrieve the replaced data without the cache" { - pm.cache.invalidate("key1") - pm["key1"] shouldEqual object2 + test("should update an old lock") { + val dir = Files.createTempDirectory("ss-") + val lockFilePath = dir.resolve("shoebox.lock") + Files.newBufferedWriter(lockFilePath).use { + it.appendLine("lock") } + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) + DirectoryStore(dir, TestData.serializer()) + Files.getLastModifiedTime(lockFilePath).toMillis() shouldBe gt(System.currentTimeMillis() - 5000) + } + } + val object1 = TestData(1, 2) + val object2 = TestData(3, 4) + context("when an item is stored") { + val object1 = TestData(1, 2) + val dir = Files.createTempDirectory("ss-") + val pm = DirectoryStore(dir, TestData.serializer()) + pm["key1"] = object1 + test("should cache the item that was stored") { + pm.cache.get("key1").value shouldBe object1 + } + } + context("when an item is replaced") { + val dir = Files.createTempDirectory("ss-") + val pm = DirectoryStore(dir, TestData.serializer()) + pm["key1"] = object1 + pm["key1"] = object2 + + test("should have cached the replaced data") { + pm.cache.get("key1").value shouldBe object2 + } + test("should retrieve the replaced data without the cache") { + pm.cache.invalidate("key1") + pm["key1"] shouldBe object2 } } } -} +})