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 87b738c..f3d88b1 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b71deac..12d38de 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Apr 23 08:58:13 CDT 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708f..2fe81a7 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -138,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 0f8d593..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/src/main/kotlin/kweb/shoebox/OrderedViewSet.kt b/src/main/kotlin/kweb/shoebox/OrderedViewSet.kt index fa36822..2de6c4d 100644 --- a/src/main/kotlin/kweb/shoebox/OrderedViewSet.kt +++ b/src/main/kotlin/kweb/shoebox/OrderedViewSet.kt @@ -1,7 +1,9 @@ package kweb.shoebox -import kweb.shoebox.BinarySearchResult.* -import java.util.concurrent.* +import kweb.shoebox.BinarySearchResult.Between +import kweb.shoebox.BinarySearchResult.Exact +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList /** * Created by ian on 3/14/17. diff --git a/src/main/kotlin/kweb/shoebox/Shoebox.kt b/src/main/kotlin/kweb/shoebox/Shoebox.kt index ec269a7..63fc0bd 100644 --- a/src/main/kotlin/kweb/shoebox/Shoebox.kt +++ b/src/main/kotlin/kweb/shoebox/Shoebox.kt @@ -1,12 +1,15 @@ package kweb.shoebox +import kotlinx.serialization.KSerializer import kweb.shoebox.Source.LOCAL -import kweb.shoebox.View.* +import kweb.shoebox.View.Reference +import kweb.shoebox.View.VerifyBehavior import kweb.shoebox.View.VerifyBehavior.BLOCKING_VERIFY -import kweb.shoebox.stores.* +import kweb.shoebox.stores.DirectoryStore +import kweb.shoebox.stores.LmdbStore +import kweb.shoebox.stores.MemoryStore import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap -import kotlin.reflect.KClass /* @@ -16,19 +19,7 @@ import kotlin.reflect.KClass * TODO: (then remove the previous lockfile mechanism) */ -/** - * Create a [Shoebox], use this in preference to the Shoebox constructor to avoid having to provide a `KClass` - * - * @param T The type of the objects to store, these must be serializable with [Gson](https://github.com/google/gson), - * - * @param directory The path to a directory in which data will be stored, will be created if it doesn't already exist - * - * @sample com.github.sanity.shoebox.samples.basic usage sample - **/ -inline fun 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 } } } -} +})