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
}
}
}
-}
+})