diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 847b8b42a..77b13174e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,8 +96,9 @@ jobs:
script: |
adb shell pm list packages | grep dev.yorkie.test && adb uninstall dev.yorkie.test || true;
./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=dev.yorkie.TreeTest --no-build-cache --no-daemon --stacktrace
- ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.annotation=dev.yorkie.TreeBasicTest --no-build-cache --no-daemon --stacktrace
- ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.annotation=dev.yorkie.TreeSplitMergeTest --no-build-cache --no-daemon --stacktrace
+ ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=dev.yorkie.document.json.JsonTreeTest --no-build-cache --no-daemon --stacktrace
+ ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=dev.yorkie.document.json.JsonTreeSplitMergeTest --no-build-cache --no-daemon --stacktrace
+ ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=dev.yorkie.document.json.JsonTreeConcurrencyTest --no-build-cache --no-daemon --stacktrace
- if: ${{ matrix.api-level == 24 }}
run: ./gradlew yorkie:jacocoDebugTestReport
- if: ${{ matrix.api-level == 24 }}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c353e893d..064c62136 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,12 +7,12 @@ connectKotlin = "0.6.1"
okhttp = "4.12.0"
coroutines = "1.8.1"
androidxActivity = "1.9.0"
-androidxLifecycle = "2.7.0"
+androidxLifecycle = "2.8.0"
androidxBenchmark = "1.2.4"
androidxComposeCompiler = "1.5.13"
[libraries]
-androidx-annotation = { module = "androidx.annotation:annotation", version = "1.7.1" }
+androidx-annotation = { module = "androidx.annotation:annotation", version = "1.8.0" }
connect-kotlin-google-javalite-ext = { module = "com.connectrpc:connect-kotlin-google-javalite-ext", version.ref = "connectKotlin" }
connect-kotlin-okhttp = { module = "com.connectrpc:connect-kotlin-okhttp", version.ref = "connectKotlin" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt
index 779ebb2a7..c0d26b900 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt
@@ -1,13 +1,7 @@
+@file:Suppress("ktlint:standard:filename")
+
package dev.yorkie
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
internal annotation class TreeTest
-
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-internal annotation class TreeBasicTest
-
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-internal annotation class TreeSplitMergeTest
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt
new file mode 100644
index 000000000..1074d036e
--- /dev/null
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt
@@ -0,0 +1,360 @@
+package dev.yorkie.document.json
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dev.yorkie.TreeTest
+import dev.yorkie.core.createClient
+import dev.yorkie.document.json.OpCode.EditOpCode
+import dev.yorkie.document.json.TestOperation.EditOperationType
+import dev.yorkie.document.json.TestOperation.StyleOperationType
+import dev.yorkie.document.json.TreeBuilder.element
+import dev.yorkie.document.json.TreeBuilder.text
+import kotlin.test.assertEquals
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@TreeTest
+@RunWith(AndroidJUnit4::class)
+class JsonTreeConcurrencyTest {
+
+ @Test
+ fun test_concurrent_edit_and_edit() {
+ runBlocking {
+ val root = element("r") {
+ element("p") { text { "abc" } }
+ element("p") { text { "def" } }
+ element("p") { text { "ghi" } }
+ }
+ val initialXml = "abc
def
ghi
"
+ val textNode1 = text { "A" }
+ val textNode2 = text { "B" }
+ val elementNode1 = element("b")
+ val elementNode2 = element("i")
+
+ val ranges = listOf(
+ // intersect-element:
abc
def
- def
ghi
+ makeTwoRanges(Triple(0, 5, 10), Triple(5, 10, 15), "intersect-selement"),
+ // intersect-text: ab - bc
+ makeTwoRanges(Triple(1, 2, 3), Triple(2, 3, 4), "intersect-text"),
+ // contain-element: abc
def
ghi
- def
+ makeTwoRanges(Triple(0, 5, 15), Triple(5, 5, 10), "contain-element"),
+ // contain-text: abc - b
+ makeTwoRanges(Triple(1, 2, 4), Triple(2, 2, 3), "contain-text"),
+ // contain-mixed-type: abc
def
ghi
- def
+ makeTwoRanges(Triple(0, 5, 15), Triple(6, 7, 9), "contain-mixed-type"),
+ // side-by-side-element: abc
- def
+ makeTwoRanges(Triple(0, 5, 5), Triple(5, 5, 10), "side-by-side-element"),
+ // side-by-side-text: a - bc
+ makeTwoRanges(Triple(1, 1, 2), Triple(2, 3, 4), "side-by-side-text"),
+ // equal-element: abc
def
- abc
def
+ makeTwoRanges(Triple(0, 5, 10), Triple(0, 5, 10), "equal-element"),
+ // equal-text: abc - abc
+ makeTwoRanges(Triple(1, 2, 4), Triple(1, 2, 4), "equal-text"),
+ )
+
+ val edit1Operations = listOf(
+ EditOperationType(
+ RangeSelector.RangeFront,
+ EditOpCode.EditUpdate,
+ textNode1,
+ 0,
+ "insertTextFront",
+ ),
+ EditOperationType(
+ RangeSelector.RangeMiddle,
+ EditOpCode.EditUpdate,
+ textNode1,
+ 0,
+ "insertTextMiddle",
+ ),
+ EditOperationType(
+ RangeSelector.RangeBack,
+ EditOpCode.EditUpdate,
+ textNode1,
+ 0,
+ "insertTextBack",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ textNode1,
+ 0,
+ "replaceText",
+ ),
+ EditOperationType(
+ RangeSelector.RangeFront,
+ EditOpCode.EditUpdate,
+ elementNode1,
+ 0,
+ "insertElementFront",
+ ),
+ EditOperationType(
+ RangeSelector.RangeMiddle,
+ EditOpCode.EditUpdate,
+ elementNode1,
+ 0,
+ "insertElementMiddle",
+ ),
+ EditOperationType(
+ RangeSelector.RangeBack,
+ EditOpCode.EditUpdate,
+ elementNode1,
+ 0,
+ "insertElementBack",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ elementNode1,
+ 0,
+ "replaceElement",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ null,
+ 0,
+ "delete",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.MergeUpdate,
+ null,
+ 0,
+ "merge",
+ ),
+ )
+
+ val edit2Operations = listOf(
+ EditOperationType(
+ RangeSelector.RangeFront,
+ EditOpCode.EditUpdate,
+ textNode2,
+ 0,
+ "insertTextFront",
+ ),
+ EditOperationType(
+ RangeSelector.RangeMiddle,
+ EditOpCode.EditUpdate,
+ textNode2,
+ 0,
+ "insertTextMiddle",
+ ),
+ EditOperationType(
+ RangeSelector.RangeBack,
+ EditOpCode.EditUpdate,
+ textNode2,
+ 0,
+ "insertTextBack",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ textNode2,
+ 0,
+ "replaceText",
+ ),
+ EditOperationType(
+ RangeSelector.RangeFront,
+ EditOpCode.EditUpdate,
+ elementNode2,
+ 0,
+ "insertElementFront",
+ ),
+ EditOperationType(
+ RangeSelector.RangeMiddle,
+ EditOpCode.EditUpdate,
+ elementNode2,
+ 0,
+ "insertElementMiddle",
+ ),
+ EditOperationType(
+ RangeSelector.RangeBack,
+ EditOpCode.EditUpdate,
+ elementNode2,
+ 0,
+ "insertElementBack",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ elementNode2,
+ 0,
+ "replaceElement",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ null,
+ 0,
+ "delete",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.MergeUpdate,
+ null,
+ 0,
+ "merge",
+ ),
+ )
+
+ runTestConcurrency(
+ root,
+ initialXml,
+ ranges,
+ edit1Operations,
+ edit2Operations,
+ "concurrently-edit-edit-test",
+ )
+ }
+ }
+
+ @Test
+ fun test_concurrent_edit_and_style() {
+ runBlocking {
+ val root = element("r") {
+ element("p") {
+ text { "a" }
+ attrs { mapOf("color" to "red") }
+ }
+ element("p") {
+ text { "b" }
+ attrs { mapOf("color" to "red") }
+ }
+ element("p") {
+ text { "c" }
+ attrs { mapOf("color" to "red") }
+ }
+ }
+ val initialXml =
+ "a
b
c
"
+ val content = element("p") {
+ text { "d" }
+ attrs { mapOf("italic" to "true") }
+ }
+
+ val ranges = listOf(
+ // equal: b
- b
+ makeTwoRanges(Triple(3, 3, 6), Triple(3, -1, 6), "equal"),
+ // equal multiple: a
b
c
- a
b
c
+ makeTwoRanges(Triple(0, 3, 9), Triple(0, 3, 9), "equal multiple"),
+ // A contains B: a
b
c
- b
+ makeTwoRanges(Triple(0, 3, 9), Triple(3, -1, 6), "A contains B"),
+ // B contains A: b
- a
b
c
+ makeTwoRanges(Triple(3, 3, 6), Triple(0, -1, 9), "B contains A"),
+ // intersect: a
b
- b
c
+ makeTwoRanges(Triple(0, 3, 6), Triple(3, -1, 9), "intersect"),
+ // A -> B: a
- b
+ makeTwoRanges(Triple(0, 3, 3), Triple(3, -1, 6), "A -> B"),
+ // B -> A: b
- a
+ makeTwoRanges(Triple(3, 3, 6), Triple(0, -1, 3), "B -> A"),
+ )
+
+ val editOperations = listOf(
+ EditOperationType(
+ RangeSelector.RangeFront,
+ EditOpCode.EditUpdate,
+ content,
+ 0,
+ "insertFront",
+ ),
+ EditOperationType(
+ RangeSelector.RangeMiddle,
+ EditOpCode.EditUpdate,
+ content,
+ 0,
+ "insertMiddle",
+ ),
+ EditOperationType(
+ RangeSelector.RangeBack,
+ EditOpCode.EditUpdate,
+ content,
+ 0,
+ "insertBack",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ null,
+ 0,
+ "delete",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.EditUpdate,
+ content,
+ 0,
+ "replace",
+ ),
+ EditOperationType(
+ RangeSelector.RangeAll,
+ EditOpCode.MergeUpdate,
+ null,
+ 0,
+ "merge",
+ ),
+ )
+
+ val styleOperations = listOf(
+ StyleOperationType(
+ RangeSelector.RangeAll,
+ OpCode.StyleOpCode.StyleRemove,
+ "color",
+ "",
+ "remove-bold",
+ ),
+ StyleOperationType(
+ RangeSelector.RangeAll,
+ OpCode.StyleOpCode.StyleSet,
+ "bold",
+ "aa",
+ "set-bold-aa",
+ ),
+ )
+
+ runTestConcurrency(
+ root,
+ initialXml,
+ ranges,
+ editOperations,
+ styleOperations,
+ "concurrently-edit-style-test",
+ )
+ }
+ }
+
+ companion object {
+
+ private suspend fun runTestConcurrency(
+ root: JsonTree.ElementNode,
+ initialXml: String,
+ ranges: List,
+ op1s: List,
+ op2s: List,
+ desc: String,
+ ) {
+ val c1 = createClient()
+ val c2 = createClient()
+ c1.activateAsync().await()
+ c2.activateAsync().await()
+
+ ranges.forEach { range ->
+ op1s.forEach { op1 ->
+ op2s.forEach { op2 ->
+ val testDesc = "$desc-${range.desc}(${op1.desc},${op2.desc})"
+ val result = withTimeout(10_000) {
+ runTest(c1, c2, root, initialXml, range, op1, op2, testDesc)
+ }
+ assertEquals(result.after.first, result.after.second)
+ }
+ }
+ }
+
+ c1.deactivateAsync().await()
+ c2.deactivateAsync().await()
+ c1.close()
+ c2.close()
+ }
+ }
+}
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTestHelpers.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTestHelpers.kt
new file mode 100644
index 000000000..461f5f3e6
--- /dev/null
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTestHelpers.kt
@@ -0,0 +1,231 @@
+package dev.yorkie.document.json
+
+import dev.yorkie.core.Client
+import dev.yorkie.core.toDocKey
+import dev.yorkie.document.Document
+import dev.yorkie.document.json.JsonTreeTest.Companion.Updater
+import dev.yorkie.document.json.JsonTreeTest.Companion.assertTreesXmlEquals
+import dev.yorkie.document.json.JsonTreeTest.Companion.rootTree
+import dev.yorkie.document.json.JsonTreeTest.Companion.updateAndSync
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+internal typealias xmlPair = Pair
+
+private val pass = Unit
+
+internal sealed interface TestOperation {
+ val selector: RangeSelector
+ val op: OpCode
+ val desc: String
+ suspend fun run(document: Document, range: RangeWithMiddleType)
+
+ data class StyleOperationType(
+ override val selector: RangeSelector,
+ override val op: OpCode.StyleOpCode,
+ val key: String,
+ val value: String,
+ override val desc: String,
+ ) : TestOperation {
+ override suspend fun run(document: Document, range: RangeWithMiddleType) {
+ val (from, to) = getRange(range, selector)
+ document.updateAsync { root, _ ->
+ when (op) {
+ OpCode.StyleOpCode.StyleRemove -> {
+ root.rootTree().removeStyle(from, to, listOf(key))
+ }
+
+ OpCode.StyleOpCode.StyleSet -> {
+ root.rootTree().style(from, to, mapOf(key to value))
+ }
+
+ else -> pass
+ }
+ }.await()
+ }
+ }
+
+ data class EditOperationType(
+ override val selector: RangeSelector,
+ override val op: OpCode.EditOpCode,
+ val content: JsonTree.TreeNode?,
+ val splitLevel: Int,
+ override val desc: String,
+ ) : TestOperation {
+ override suspend fun run(document: Document, range: RangeWithMiddleType) {
+ val interval = getRange(range, selector)
+ val (from, to) = interval
+ val convertedContent = if (content == null) arrayOf() else arrayOf(content)
+ document.updateAsync { root, _ ->
+ when (op) {
+ OpCode.EditOpCode.EditUpdate -> {
+ root.rootTree().edit(from, to, splitLevel, *convertedContent)
+ }
+
+ OpCode.EditOpCode.MergeUpdate -> {
+ val mergeInterval = getMergeRange(root.rootTree().toXml(), interval)
+ val (st, ed) = mergeInterval
+ if (st != -1 && ed != -1 && st < ed) {
+ root.rootTree().edit(st, ed, splitLevel, *convertedContent)
+ }
+ }
+
+ OpCode.EditOpCode.SplitUpdate -> {
+ assertNotEquals(0, splitLevel)
+ assertEquals(from, to)
+ root.rootTree().edit(from, to, splitLevel, *convertedContent)
+ }
+
+ else -> pass
+ }
+ }.await()
+ }
+ }
+}
+
+internal enum class RangeSelector {
+ RangeUnknown,
+ RangeFront,
+ RangeMiddle,
+ RangeBack,
+ RangeAll,
+ RangeOneQuarter,
+ RangeThreeQuarter,
+}
+
+internal sealed interface OpCode {
+
+ enum class StyleOpCode : OpCode {
+ StyleUndefined,
+ StyleRemove,
+ StyleSet,
+ }
+
+ enum class EditOpCode : OpCode {
+ EditUndefined,
+ EditUpdate,
+ MergeUpdate,
+ SplitUpdate,
+ }
+}
+
+internal data class RangeType(val from: Int, val to: Int)
+
+internal data class RangeWithMiddleType(val from: Int, val mid: Int, val to: Int)
+
+internal data class TwoRangesType(
+ val from: RangeWithMiddleType,
+ val to: RangeWithMiddleType,
+ val desc: String,
+)
+
+internal fun getRange(range: RangeWithMiddleType, selector: RangeSelector): RangeType {
+ return when (selector) {
+ RangeSelector.RangeFront -> RangeType(range.from, range.from)
+ RangeSelector.RangeMiddle -> RangeType(range.mid, range.mid)
+ RangeSelector.RangeBack -> RangeType(range.to, range.to)
+ RangeSelector.RangeAll -> RangeType(range.from, range.to)
+ RangeSelector.RangeOneQuarter -> {
+ val quarter = (range.from + range.mid + 1) shr 1
+ RangeType(quarter, quarter)
+ }
+
+ RangeSelector.RangeThreeQuarter -> {
+ val quarter = (range.mid + range.to) shr 1
+ RangeType(quarter, quarter)
+ }
+
+ RangeSelector.RangeUnknown -> RangeType(-1, -1)
+ }
+}
+
+internal fun makeTwoRanges(
+ from: Triple,
+ to: Triple,
+ desc: String,
+): TwoRangesType {
+ val fromRange = RangeWithMiddleType(from.first, from.second, from.third)
+ val toRange = RangeWithMiddleType(to.first, to.second, to.third)
+ return TwoRangesType(fromRange, toRange, desc)
+}
+
+internal fun getMergeRange(xml: String, interval: RangeType): RangeType {
+ val content = parseSimpleXml(xml)
+ var st = -1
+ var ed = -1
+ for (i in interval.from + 1..interval.to) {
+ if (st == -1 && content[i].startsWith("")) {
+ st = i - 1
+ }
+ if (content[i].startsWith("<") && !content[i].startsWith("")) {
+ ed = i
+ }
+ }
+ return RangeType(st, ed)
+}
+
+private fun parseSimpleXml(s: String): List = buildList {
+ var i = 0
+ while (i < s.length) {
+ var now = ""
+ if (s[i] == '<') {
+ while (i < s.length && s[i] != '>') {
+ now += s[i++]
+ }
+ }
+ now += s[i++]
+ add(now)
+ }
+}
+
+internal suspend fun runTest(
+ c1: Client,
+ c2: Client,
+ initialRoot: JsonTree.ElementNode,
+ initialXml: String,
+ ranges: TwoRangesType,
+ op1: TestOperation,
+ op2: TestOperation,
+ desc: String,
+): TestResult {
+ val docKey = desc.toDocKey()
+ val d1 = Document(docKey)
+ val d2 = Document(docKey)
+
+ c1.attachAsync(d1, syncMode = Client.SyncMode.Manual).await()
+ c2.attachAsync(d2, syncMode = Client.SyncMode.Manual).await()
+
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.setNewTree("t", initialRoot)
+ },
+ Updater(c2, d2),
+ )
+ assertTreesXmlEquals(initialXml, d1, d2)
+
+ op1.run(d1, ranges.from)
+ op2.run(d1, ranges.from)
+
+ val before1 = d1.getRoot().rootTree().toXml()
+ val before2 = d2.getRoot().rootTree().toXml()
+
+ // save own changes and get previous changes
+ c1.syncAsync().await()
+ c2.syncAsync().await()
+
+ // get last client changes
+ c1.syncAsync().await()
+ c2.syncAsync().await()
+
+ val after1 = d1.getRoot().rootTree().toXml()
+ val after2 = d2.getRoot().rootTree().toXml()
+
+ c1.detachAsync(d1).await()
+ c2.detachAsync(d2).await()
+ d1.close()
+ d2.close()
+
+ return TestResult(before1 to before2, after1 to after2)
+}
+
+internal data class TestResult(val before: xmlPair, val after: xmlPair)
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt
index 9d3eaa496..5fdb9c8bb 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt
@@ -1,7 +1,6 @@
package dev.yorkie.document.json
import androidx.test.ext.junit.runners.AndroidJUnit4
-import dev.yorkie.TreeSplitMergeTest
import dev.yorkie.TreeTest
import dev.yorkie.core.Client.SyncMode.Manual
import dev.yorkie.core.withTwoClientsAndDocuments
@@ -11,7 +10,6 @@ import org.junit.Test
import org.junit.runner.RunWith
@TreeTest
-@TreeSplitMergeTest
@RunWith(AndroidJUnit4::class)
class JsonTreeSplitMergeTest {
@@ -35,10 +33,10 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
) {
JsonTreeTest.assertTreesXmlEquals("a
b
", d1, d2)
@@ -67,10 +65,10 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
- root.rootTree().edit(3, 3, splitLevel = 1)
+ root.rootTree().edit(3, 3, 1)
},
) {
JsonTreeTest.assertTreesXmlEquals("a
bc
", d1)
@@ -100,7 +98,7 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
root.rootTree().edit(2, 2, TreeBuilder.text { "c" })
@@ -133,7 +131,7 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
root.rootTree().edit(1, 1, TreeBuilder.text { "c" })
@@ -166,7 +164,7 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
root.rootTree().edit(3, 3, TreeBuilder.text { "c" })
@@ -199,7 +197,7 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.updateAndSync(
JsonTreeTest.Companion.Updater(c1, d1) { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
},
JsonTreeTest.Companion.Updater(c2, d2) { root, _ ->
root.rootTree().edit(2, 3)
@@ -230,7 +228,7 @@ class JsonTreeSplitMergeTest {
JsonTreeTest.assertTreesXmlEquals("ab
", d1)
d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, splitLevel = 1)
+ root.rootTree().edit(1, 1, 1)
}.await()
JsonTreeTest.assertTreesXmlEquals("ab
", d1)
@@ -263,7 +261,7 @@ class JsonTreeSplitMergeTest {
assertEquals("ab
", d1.getRoot().rootTree().toXml())
d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 2)
+ root.rootTree().edit(2, 2, 2)
}.await()
assertEquals(
"ab
",
@@ -297,7 +295,7 @@ class JsonTreeSplitMergeTest {
assertEquals("ab
", d1.getRoot().rootTree().toXml())
d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
}.await()
assertEquals("a
b
", d1.getRoot().rootTree().toXml())
@@ -307,7 +305,7 @@ class JsonTreeSplitMergeTest {
assertEquals("ac
b
", d1.getRoot().rootTree().toXml())
d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, splitLevel = 1)
+ root.rootTree().edit(2, 2, 1)
}.await()
assertEquals("a
c
b
", d1.getRoot().rootTree().toXml())
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
index 9fceda2af..f589f3ecb 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
@@ -2,7 +2,6 @@ package dev.yorkie.document.json
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.gson.reflect.TypeToken
-import dev.yorkie.TreeBasicTest
import dev.yorkie.TreeTest
import dev.yorkie.core.Client
import dev.yorkie.core.Client.SyncMode.Manual
@@ -44,7 +43,6 @@ import org.junit.Test
import org.junit.runner.RunWith
@TreeTest
-@TreeBasicTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class JsonTreeTest {
diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
index f32b09b37..440a7f220 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
@@ -597,9 +597,10 @@ public class Client @VisibleForTesting internal constructor(
}
private suspend fun waitForInitialization(documentKey: Document.Key) {
- val attachment = attachments.value[documentKey] ?: return
- if (attachment.syncMode == SyncMode.Realtime) {
- attachment.document.presences.first { it != UninitializedPresences }
+ val attachment = attachments.first { documentKey in it.keys }[documentKey] ?: return
+ attachment.document.presences.first { it != UninitializedPresences }
+ if (attachment.syncMode != SyncMode.Manual) {
+ attachment.document.events.first { it is Initialized }
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
index 52447a798..a647f9694 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
@@ -131,8 +131,19 @@ public class JsonTree internal constructor(
public fun edit(
fromIndex: Int,
toIndex: Int,
+ vararg content: TreeNode,
+ ) {
+ edit(fromIndex, toIndex, 0, *content)
+ }
+
+ /**
+ * Edits this tree with the given node.
+ */
+ public fun edit(
+ fromIndex: Int,
+ toIndex: Int,
+ splitLevel: Int,
vararg contents: TreeNode,
- splitLevel: Int = 0,
) {
require(fromIndex <= toIndex) {
"from should be less than or equal to to"
diff --git a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
index e38c419c3..9931a5650 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
@@ -75,192 +75,172 @@ class ClientTest {
}
@Test
- fun `should activate and deactivate`() {
- runTest {
- assertFalse(target.isActive)
- val activateRequestCaptor = argumentCaptor()
- assertTrue(target.activateAsync().await().isSuccess)
- verify(service).activateClient(activateRequestCaptor.capture(), any())
- assertEquals(TEST_KEY, activateRequestCaptor.firstValue.clientKey)
- assertTrue(target.isActive)
-
- val activatedStatus = assertIs(target.status.value)
- assertEquals(TEST_ACTOR_ID, activatedStatus.clientId)
-
- val deactivateRequestCaptor = argumentCaptor()
- assertTrue(target.deactivateAsync().await().isSuccess)
- verify(service).deactivateClient(deactivateRequestCaptor.capture(), any())
- assertIsTestActorID(deactivateRequestCaptor.firstValue.clientId)
- assertFalse(target.isActive)
- assertIs(target.status.value)
- }
+ fun `should activate and deactivate`() = runTest {
+ assertFalse(target.isActive)
+ val activateRequestCaptor = argumentCaptor()
+ assertTrue(target.activateAsync().await().isSuccess)
+ verify(service).activateClient(activateRequestCaptor.capture(), any())
+ assertEquals(TEST_KEY, activateRequestCaptor.firstValue.clientKey)
+ assertTrue(target.isActive)
+
+ val activatedStatus = assertIs(target.status.value)
+ assertEquals(TEST_ACTOR_ID, activatedStatus.clientId)
+
+ val deactivateRequestCaptor = argumentCaptor()
+ assertTrue(target.deactivateAsync().await().isSuccess)
+ verify(service).deactivateClient(deactivateRequestCaptor.capture(), any())
+ assertIsTestActorID(deactivateRequestCaptor.firstValue.clientId)
+ assertFalse(target.isActive)
+ assertIs(target.status.value)
}
@Test
- fun `should sync when document is attached and on manual sync requests`() {
- runTest {
- val document = Document(Key(NORMAL_DOCUMENT_KEY))
- target.activateAsync().await()
-
- val attachRequestCaptor = argumentCaptor()
- target.attachAsync(document, syncMode = Manual).await()
- verify(service).attachDocument(attachRequestCaptor.capture(), any())
- assertIsTestActorID(attachRequestCaptor.firstValue.clientId)
- assertIsInitialChangePack(attachRequestCaptor.firstValue.changePack)
- assertJsonContentEquals("""{"k1": 4}""", document.toJson())
-
- val syncRequestCaptor = argumentCaptor()
- target.syncAsync().await()
- verify(service).pushPullChanges(syncRequestCaptor.capture(), any())
- assertIsTestActorID(syncRequestCaptor.firstValue.clientId)
- assertIsInitialChangePack(syncRequestCaptor.firstValue.changePack)
- assertJsonContentEquals("""{"k2": 100.0}""", document.toJson())
-
- val detachRequestCaptor = argumentCaptor()
- target.detachAsync(document).await()
- verify(service).detachDocument(detachRequestCaptor.capture(), any())
- assertIsTestActorID(detachRequestCaptor.firstValue.clientId)
- val detachmentChange =
- detachRequestCaptor.firstValue.changePack.toChangePack().changes.last()
- assertIs(detachmentChange.presenceChange)
- target.deactivateAsync().await()
- }
+ fun `should sync when document is attached and on manual sync requests`() = runTest {
+ val document = Document(Key(NORMAL_DOCUMENT_KEY))
+ target.activateAsync().await()
+
+ val attachRequestCaptor = argumentCaptor()
+ target.attachAsync(document, syncMode = Manual).await()
+ verify(service).attachDocument(attachRequestCaptor.capture(), any())
+ assertIsTestActorID(attachRequestCaptor.firstValue.clientId)
+ assertIsInitialChangePack(attachRequestCaptor.firstValue.changePack)
+ assertJsonContentEquals("""{"k1": 4}""", document.toJson())
+
+ val syncRequestCaptor = argumentCaptor()
+ target.syncAsync().await()
+ verify(service).pushPullChanges(syncRequestCaptor.capture(), any())
+ assertIsTestActorID(syncRequestCaptor.firstValue.clientId)
+ assertIsInitialChangePack(syncRequestCaptor.firstValue.changePack)
+ assertJsonContentEquals("""{"k2": 100.0}""", document.toJson())
+
+ val detachRequestCaptor = argumentCaptor()
+ target.detachAsync(document).await()
+ verify(service).detachDocument(detachRequestCaptor.capture(), any())
+ assertIsTestActorID(detachRequestCaptor.firstValue.clientId)
+ val detachmentChange =
+ detachRequestCaptor.firstValue.changePack.toChangePack().changes.last()
+ assertIs(detachmentChange.presenceChange)
+ target.deactivateAsync().await()
}
@Test
- fun `should run watch and sync when document is attached`() {
- runTest {
- val document = Document(Key(NORMAL_DOCUMENT_KEY))
- target.activateAsync().await()
-
- target.attachAsync(document).await()
-
- val syncRequestCaptor = argumentCaptor()
- assertIs(
- document.events.filterIsInstance().first(),
- )
- verify(service, atLeastOnce()).pushPullChanges(syncRequestCaptor.capture(), any())
- assertIsTestActorID(syncRequestCaptor.firstValue.clientId)
- assertJsonContentEquals("""{"k2": 100.0}""", document.toJson())
-
- target.detachAsync(document).await()
- target.deactivateAsync().await()
- }
+ fun `should run watch and sync when document is attached`() = runTest {
+ val document = Document(Key(NORMAL_DOCUMENT_KEY))
+ target.activateAsync().await()
+
+ target.attachAsync(document).await()
+
+ val syncRequestCaptor = argumentCaptor()
+ assertIs(
+ document.events.filterIsInstance().first(),
+ )
+ verify(service, atLeastOnce()).pushPullChanges(syncRequestCaptor.capture(), any())
+ assertIsTestActorID(syncRequestCaptor.firstValue.clientId)
+ assertJsonContentEquals("""{"k2": 100.0}""", document.toJson())
+
+ target.detachAsync(document).await()
+ target.deactivateAsync().await()
}
@Test
- fun `should emit according event when watch stream fails`() {
- runTest {
- val document = Document(Key(WATCH_SYNC_ERROR_DOCUMENT_KEY))
- target.activateAsync().await()
- target.attachAsync(document).await()
-
- val syncEventDeferred = async(start = CoroutineStart.UNDISPATCHED) {
- document.events.filterIsInstance().first()
- }
- val connectionEventDeferred = async(start = CoroutineStart.UNDISPATCHED) {
- document.events.filterIsInstance().first()
- }
-
- document.updateAsync { root, _ ->
- root["k1"] = 1
- }.await()
-
- assertIs(syncEventDeferred.await())
- assertIs(connectionEventDeferred.await())
-
- target.detachAsync(document).await()
- target.deactivateAsync().await()
+ fun `should emit according event when watch stream fails`() = runTest {
+ val document = Document(Key(WATCH_SYNC_ERROR_DOCUMENT_KEY))
+ target.activateAsync().await()
+ target.attachAsync(document).await()
+
+ val syncEventDeferred = async(start = CoroutineStart.UNDISPATCHED) {
+ document.events.filterIsInstance().first()
}
+ val connectionEventDeferred = async(start = CoroutineStart.UNDISPATCHED) {
+ document.events.filterIsInstance().first()
+ }
+
+ document.updateAsync { root, _ ->
+ root["k1"] = 1
+ }.await()
+
+ assertIs(syncEventDeferred.await())
+ assertIs(connectionEventDeferred.await())
+
+ target.detachAsync(document).await()
+ target.deactivateAsync().await()
}
@Test
- fun `should return sync result according to server response`() {
- runTest {
- val success = Document(Key(NORMAL_DOCUMENT_KEY))
- target.activateAsync().await()
- target.attachAsync(success).await()
+ fun `should return sync result according to server response`() = runTest {
+ val success = Document(Key(NORMAL_DOCUMENT_KEY))
+ target.activateAsync().await()
+ target.attachAsync(success).await()
- assertTrue(target.syncAsync().await().isSuccess)
- target.detachAsync(success).await()
+ assertTrue(target.syncAsync().await().isSuccess)
+ target.detachAsync(success).await()
- val failing = Document(Key(WATCH_SYNC_ERROR_DOCUMENT_KEY))
- target.attachAsync(failing).await()
- assertFalse(target.syncAsync().await().isSuccess)
+ val failing = Document(Key(WATCH_SYNC_ERROR_DOCUMENT_KEY))
+ target.attachAsync(failing).await()
+ assertFalse(target.syncAsync().await().isSuccess)
- target.detachAsync(failing).await()
- target.deactivateAsync().await()
- }
+ target.detachAsync(failing).await()
+ target.deactivateAsync().await()
}
@Test
- fun `should return false on attach failure without exceptions`() {
- runTest {
- val document = Document(Key(ATTACH_ERROR_DOCUMENT_KEY))
- target.activateAsync().await()
+ fun `should return false on attach failure without exceptions`() = runTest {
+ val document = Document(Key(ATTACH_ERROR_DOCUMENT_KEY))
+ target.activateAsync().await()
- assertFalse(target.attachAsync(document).await().isSuccess)
+ assertFalse(target.attachAsync(document).await().isSuccess)
- target.deactivateAsync().await()
- }
+ target.deactivateAsync().await()
}
@Test
- fun `should return false on detach failure without exceptions`() {
- runTest {
- val document = Document(Key(DETACH_ERROR_DOCUMENT_KEY))
- target.activateAsync().await()
- target.attachAsync(document).await()
+ fun `should return false on detach failure without exceptions`() = runTest {
+ val document = Document(Key(DETACH_ERROR_DOCUMENT_KEY))
+ target.activateAsync().await()
+ target.attachAsync(document).await()
- assertFalse(target.detachAsync(document).await().isSuccess)
+ assertFalse(target.detachAsync(document).await().isSuccess)
- target.deactivateAsync().await()
- }
+ target.deactivateAsync().await()
}
@Test
- fun `should handle activating and deactivating multiple times`() {
- runTest {
- assertTrue(target.activateAsync().await().isSuccess)
- assertTrue(target.activateAsync().await().isSuccess)
- delay(500)
- assertTrue(target.deactivateAsync().await().isSuccess)
- assertTrue(target.deactivateAsync().await().isSuccess)
- }
+ fun `should handle activating and deactivating multiple times`() = runTest {
+ assertTrue(target.activateAsync().await().isSuccess)
+ assertTrue(target.activateAsync().await().isSuccess)
+ delay(500)
+ assertTrue(target.deactivateAsync().await().isSuccess)
+ assertTrue(target.deactivateAsync().await().isSuccess)
}
@Test
- fun `should remove document`() {
- runTest {
- val document = Document(Key(NORMAL_DOCUMENT_KEY))
- target.activateAsync().await()
- target.attachAsync(document).await()
-
- val removeDocumentRequestCaptor = argumentCaptor()
- target.removeAsync(document).await()
- verify(service).removeDocument(removeDocumentRequestCaptor.capture(), any())
- assertIsTestActorID(removeDocumentRequestCaptor.firstValue.clientId)
- assertEquals(
- InitialChangePack.copy(isRemoved = true),
- removeDocumentRequestCaptor.firstValue.changePack.toChangePack(),
- )
-
- target.deactivateAsync().await()
- }
+ fun `should remove document`() = runTest {
+ val document = Document(Key(NORMAL_DOCUMENT_KEY))
+ target.activateAsync().await()
+ target.attachAsync(document).await()
+
+ val removeDocumentRequestCaptor = argumentCaptor()
+ target.removeAsync(document).await()
+ verify(service).removeDocument(removeDocumentRequestCaptor.capture(), any())
+ assertIsTestActorID(removeDocumentRequestCaptor.firstValue.clientId)
+ assertEquals(
+ InitialChangePack.copy(isRemoved = true),
+ removeDocumentRequestCaptor.firstValue.changePack.toChangePack(),
+ )
+
+ target.deactivateAsync().await()
}
@Test
- fun `should return false on remove document error without exceptions`() {
- runTest {
- val document = Document(Key(REMOVE_ERROR_DOCUMENT_KEY))
- target.activateAsync().await()
- target.attachAsync(document).await()
+ fun `should return false on remove document error without exceptions`() = runTest {
+ val document = Document(Key(REMOVE_ERROR_DOCUMENT_KEY))
+ target.activateAsync().await()
+ target.attachAsync(document).await()
- assertFalse(target.removeAsync(document).await().isSuccess)
+ assertFalse(target.removeAsync(document).await().isSuccess)
- target.detachAsync(document).await()
- target.deactivateAsync().await()
- }
+ target.detachAsync(document).await()
+ target.deactivateAsync().await()
}
private fun assertIsTestActorID(clientId: String) {
diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/DocumentTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/DocumentTest.kt
index d434fc90d..949a6aede 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/DocumentTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/DocumentTest.kt
@@ -38,80 +38,75 @@ class DocumentTest {
}
@Test
- fun `should not throw error when trying to delete missing key`() {
- runTest {
- var result = target.updateAsync { root, _ ->
- root["k1"] = "1"
- root["k2"] = "2"
- root["k2"] = "3"
- with(root.setNewArray("k3")) {
- put(1)
- put(2)
- }
- }.await()
- assertTrue(result.isSuccess)
-
- result = target.updateAsync { root, _ ->
- root.remove("k1")
- val array = root.getAs("k3")
- array.removeAt(0)
- root.remove("k2")
- array.removeAt(2)
- }.await()
- assertTrue(result.isSuccess)
- }
+ fun `should not throw error when trying to delete missing key`() = runTest {
+ var result = target.updateAsync { root, _ ->
+ root["k1"] = "1"
+ root["k2"] = "2"
+ root["k2"] = "3"
+ with(root.setNewArray("k3")) {
+ put(1)
+ put(2)
+ }
+ }.await()
+ assertTrue(result.isSuccess)
+
+ result = target.updateAsync { root, _ ->
+ root.remove("k1")
+ val array = root.getAs("k3")
+ array.removeAt(0)
+ root.remove("k2")
+ array.removeAt(2)
+ }.await()
+ assertTrue(result.isSuccess)
}
@Test
- fun `should handle null values`() {
- runTest {
- target.updateAsync { root, _ ->
- val data = root.setNewObject("data")
- data["null"] = null
- data[""] = null
- }.await()
-
- assertJsonContentEquals("""{"data":{"":null,"null":null}}""", target.toJson())
- }
+ fun `should handle null values`() = runTest {
+ target.updateAsync { root, _ ->
+ val data = root.setNewObject("data")
+ data["null"] = null
+ data[""] = null
+ }.await()
+
+ assertJsonContentEquals("""{"data":{"":null,"null":null}}""", target.toJson())
}
@Test
- fun `should handle all types`() {
- runTest {
- target.updateAsync { root, _ ->
- val obj = root.setNewObject("data")
- obj["k1"] = true
- obj["k2"] = false
- obj["k3"] = 11
- obj["k4"] = 100L
- obj["k5"] = 111.11
- obj["k6"] = "test string\n\n"
- obj["k7"] = "bytes".toByteArray()
- obj["k8"] = Date(1_000)
-
- val array = obj.setNewArray("k9")
- array.put(true)
- array.put(1)
- array.put(100L)
- array.put(111.111)
- array.put("test")
- array.put("bytes".toByteArray())
- array.put(Date(10_000))
- val arrayObj = array.putNewObject()
- arrayObj["k1"] = 1
- val arrayArray = array.putNewArray()
- arrayArray.put(1)
- arrayArray.put(2)
-
- obj.setNewCounter("int", 100)
- obj.setNewCounter("long", 100L)
-
- obj.setNewText("text").edit(0, 0, "Hello World")
- obj.getAs("text").style(0, 1, mapOf("b" to "1"))
- }.await()
-
- assertJsonContentEquals(
- """{
+ fun `should handle all types`() = runTest {
+ target.updateAsync { root, _ ->
+ val obj = root.setNewObject("data")
+ obj["k1"] = true
+ obj["k2"] = false
+ obj["k3"] = 11
+ obj["k4"] = 100L
+ obj["k5"] = 111.11
+ obj["k6"] = "test string\n\n"
+ obj["k7"] = "bytes".toByteArray()
+ obj["k8"] = Date(1_000)
+
+ val array = obj.setNewArray("k9")
+ array.put(true)
+ array.put(1)
+ array.put(100L)
+ array.put(111.111)
+ array.put("test")
+ array.put("bytes".toByteArray())
+ array.put(Date(10_000))
+ val arrayObj = array.putNewObject()
+ arrayObj["k1"] = 1
+ val arrayArray = array.putNewArray()
+ arrayArray.put(1)
+ arrayArray.put(2)
+
+ obj.setNewCounter("int", 100)
+ obj.setNewCounter("long", 100L)
+
+ obj.setNewText("text").edit(0, 0, "Hello World")
+ obj.getAs("text").style(0, 1, mapOf("b" to "1"))
+ }.await()
+
+ assertJsonContentEquals(
+ """{
"data": {
"k1": true,
"k2": false,
@@ -127,77 +122,72 @@ class DocumentTest {
"text": [{"attrs":{"b":"1"},"val":"H"},{"val":"ello World"}]
}
}""",
- target.toJson(),
- )
- }
+ target.toJson(),
+ )
}
@Test
- fun `should emit local change events when document properties are changed`() {
- runTest {
- val events = mutableListOf()
- val collectJob = launch(UnconfinedTestDispatcher()) {
- target.events.collect(events::add)
- }
+ fun `should emit local change events when document properties are changed`() = runTest {
+ val events = mutableListOf()
+ val collectJob = launch(UnconfinedTestDispatcher()) {
+ target.events.collect(events::add)
+ }
- assertFalse(target.hasLocalChanges)
+ assertFalse(target.hasLocalChanges)
- target.updateAsync { root, _ ->
- root["k1"] = 1
- root["k2"] = true
- }.await()
+ target.updateAsync { root, _ ->
+ root["k1"] = 1
+ root["k2"] = true
+ }.await()
- assertEquals(1, events.size)
- var event = events.first()
- assertIs(event)
- var operations = event.changeInfo.operations
- assertEquals(2, operations.size)
- assertTrue(operations.all { it is SetOpInfo })
+ assertEquals(1, events.size)
+ var event = events.first()
+ assertIs(event)
+ var operations = event.changeInfo.operations
+ assertEquals(2, operations.size)
+ assertTrue(operations.all { it is SetOpInfo })
- val firstSet = operations.first() as SetOpInfo
- assertEquals("k1", firstSet.key)
+ val firstSet = operations.first() as SetOpInfo
+ assertEquals("k1", firstSet.key)
- val secondSet = operations.last() as SetOpInfo
- assertEquals("k2", secondSet.key)
+ val secondSet = operations.last() as SetOpInfo
+ assertEquals("k2", secondSet.key)
- target.updateAsync { root, _ ->
- root.remove("k2")
- root.remove("k1")
- }.await()
+ target.updateAsync { root, _ ->
+ root.remove("k2")
+ root.remove("k1")
+ }.await()
- assertEquals(2, events.size)
- event = events.last()
- assertIs(event)
- operations = event.changeInfo.operations
- assertEquals(2, operations.size)
- assertTrue(operations.all { it is RemoveOpInfo })
+ assertEquals(2, events.size)
+ event = events.last()
+ assertIs(event)
+ operations = event.changeInfo.operations
+ assertEquals(2, operations.size)
+ assertTrue(operations.all { it is RemoveOpInfo })
- val firstRemove = operations.first() as RemoveOpInfo
- assertEquals(secondSet.executedAt, firstRemove.executedAt)
+ val firstRemove = operations.first() as RemoveOpInfo
+ assertEquals(secondSet.executedAt, firstRemove.executedAt)
- val secondRemove = operations.last() as RemoveOpInfo
- assertEquals(firstSet.executedAt, secondRemove.executedAt)
+ val secondRemove = operations.last() as RemoveOpInfo
+ assertEquals(firstSet.executedAt, secondRemove.executedAt)
- assertTrue(target.hasLocalChanges)
+ assertTrue(target.hasLocalChanges)
- collectJob.cancel()
- }
+ collectJob.cancel()
}
@Test
- fun `should clear clone when error occurs on update`() {
- runTest {
- target.updateAsync { root, _ ->
- root["k1"] = 1
- }.await()
- assertNotNull(target.clone)
-
- target.updateAsync { root, _ ->
- root["k2"] = 2
- error("error test")
- }.await()
- assertNull(target.clone)
- }
+ fun `should clear clone when error occurs on update`() = runTest {
+ target.updateAsync { root, _ ->
+ root["k1"] = 1
+ }.await()
+ assertNotNull(target.clone)
+
+ target.updateAsync { root, _ ->
+ root["k2"] = 2
+ error("error test")
+ }.await()
+ assertNull(target.clone)
}
@Test
@@ -211,102 +201,94 @@ class DocumentTest {
@Suppress("ktlint:standard:max-line-length")
@Test
- fun `should get value from paths`() {
- runTest {
- target.updateAsync { root, _ ->
- root.setNewArray("todos").putNewObject().apply {
- set("text", "todo1")
- set("completed", false)
- }
- root.setNewObject("obj").setNewObject("c1").apply {
- set("name", "josh")
- set("age", 14)
- }
- root["str"] = "string"
- }.await()
-
- assertEquals(
- """{"todos":[{"text":"todo1","completed":false}],"obj":{"c1":{"name":"josh","age":14}},"str":"string"}""",
- target.getValueByPath("$")?.toJson(),
- )
- assertEquals(
- """[{"text":"todo1","completed":false}]""",
- target.getValueByPath("$.todos")?.toJson(),
- )
- assertEquals(
- """{"text":"todo1","completed":false}""",
- target.getValueByPath("$.todos.0")?.toJson(),
- )
- assertEquals(
- """{"c1":{"name":"josh","age":14}}""",
- target.getValueByPath("$.obj")?.toJson(),
- )
- assertEquals(
- """{"name":"josh","age":14}""",
- target.getValueByPath("$.obj.c1")?.toJson(),
- )
- assertEquals(
- """"josh"""",
- target.getValueByPath("$.obj.c1.name")?.toJson(),
- )
- assertEquals(
- """"string"""",
- target.getValueByPath("$.str")?.toJson(),
- )
- assertNull(target.getValueByPath("$..."))
- }
+ fun `should get value from paths`() = runTest {
+ target.updateAsync { root, _ ->
+ root.setNewArray("todos").putNewObject().apply {
+ set("text", "todo1")
+ set("completed", false)
+ }
+ root.setNewObject("obj").setNewObject("c1").apply {
+ set("name", "josh")
+ set("age", 14)
+ }
+ root["str"] = "string"
+ }.await()
+
+ assertEquals(
+ """{"todos":[{"text":"todo1","completed":false}],"obj":{"c1":{"name":"josh","age":14}},"str":"string"}""",
+ target.getValueByPath("$")?.toJson(),
+ )
+ assertEquals(
+ """[{"text":"todo1","completed":false}]""",
+ target.getValueByPath("$.todos")?.toJson(),
+ )
+ assertEquals(
+ """{"text":"todo1","completed":false}""",
+ target.getValueByPath("$.todos.0")?.toJson(),
+ )
+ assertEquals(
+ """{"c1":{"name":"josh","age":14}}""",
+ target.getValueByPath("$.obj")?.toJson(),
+ )
+ assertEquals(
+ """{"name":"josh","age":14}""",
+ target.getValueByPath("$.obj.c1")?.toJson(),
+ )
+ assertEquals(
+ """"josh"""",
+ target.getValueByPath("$.obj.c1.name")?.toJson(),
+ )
+ assertEquals(
+ """"string"""",
+ target.getValueByPath("$.str")?.toJson(),
+ )
+ assertNull(target.getValueByPath("$..."))
}
@Test
- fun `should remove previously inserted elements in heap when running GC`() {
- runTest {
- target.updateAsync { root, _ ->
- root["a"] = 1
- root["a"] = 2
- root.remove("a")
- }.await()
- assertEquals("{}", target.toJson())
- assertEquals(2, target.garbageLength)
-
- target.garbageCollect(TimeTicket.MaxTimeTicket)
- assertEquals(0, target.garbageLength)
- }
+ fun `should remove previously inserted elements in heap when running GC`() = runTest {
+ target.updateAsync { root, _ ->
+ root["a"] = 1
+ root["a"] = 2
+ root.remove("a")
+ }.await()
+ assertEquals("{}", target.toJson())
+ assertEquals(2, target.garbageLength)
+
+ target.garbageCollect(TimeTicket.MaxTimeTicket)
+ assertEquals(0, target.garbageLength)
}
@Test
- fun `should handle escape string for strings containing single quotes`() {
- runTest {
- target.updateAsync { root, _ ->
- root["str"] = "I'm Yorkie"
- }.await()
- assertEquals("""{"str":"I'm Yorkie"}""", target.toJson())
- assertEquals(
- "I'm Yorkie",
- JsonParser.parseString(target.toJson()).asJsonObject.get("str").asString,
- )
-
- target.updateAsync { root, _ ->
- root["str"] = """I\\'m Yorkie"""
- }.await()
- assertEquals("""{"str":"I\\\\'m Yorkie"}""", target.toJson())
- assertEquals(
- """I\\'m Yorkie""",
- JsonParser.parseString(target.toJson()).asJsonObject.get("str").asString,
- )
- }
+ fun `should handle escape string for strings containing single quotes`() = runTest {
+ target.updateAsync { root, _ ->
+ root["str"] = "I'm Yorkie"
+ }.await()
+ assertEquals("""{"str":"I'm Yorkie"}""", target.toJson())
+ assertEquals(
+ "I'm Yorkie",
+ JsonParser.parseString(target.toJson()).asJsonObject.get("str").asString,
+ )
+
+ target.updateAsync { root, _ ->
+ root["str"] = """I\\'m Yorkie"""
+ }.await()
+ assertEquals("""{"str":"I\\\\'m Yorkie"}""", target.toJson())
+ assertEquals(
+ """I\\'m Yorkie""",
+ JsonParser.parseString(target.toJson()).asJsonObject.get("str").asString,
+ )
}
@Test
- fun `should handle escape string for object keys`() {
- runTest {
- target.updateAsync { root, _ ->
- root["""it"s"""] = "Yorkie"
- }.await()
- assertEquals("""{"it\"s":"Yorkie"}""", target.toJson())
- assertEquals(
- "Yorkie",
- JsonParser.parseString(target.toJson()).asJsonObject.get("""it"s""").asString,
- )
- }
+ fun `should handle escape string for object keys`() = runTest {
+ target.updateAsync { root, _ ->
+ root["""it"s"""] = "Yorkie"
+ }.await()
+ assertEquals("""{"it\"s":"Yorkie"}""", target.toJson())
+ assertEquals(
+ "Yorkie",
+ JsonParser.parseString(target.toJson()).asJsonObject.get("""it"s""").asString,
+ )
}
}
diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonCounterTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonCounterTest.kt
index a49860ed3..a0c98440c 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonCounterTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonCounterTest.kt
@@ -11,15 +11,13 @@ class JsonCounterTest {
private val document = Document(Document.Key(""))
@Test
- fun `verify increase with Int type`() {
- runTest {
- document.updateAsync { root, _ ->
- val obj = root.setNewObject("k1")
- val age = obj.setNewCounter("age", 1)
- age.increase(1000)
- age.increase(100L)
- }.await()
- }
+ fun `verify increase with Int type`() = runTest {
+ document.updateAsync { root, _ ->
+ val obj = root.setNewObject("k1")
+ val age = obj.setNewCounter("age", 1)
+ age.increase(1000)
+ age.increase(100L)
+ }.await()
assertJsonContentEquals(
"""{ "k1": { "age": 1101 } }""",
@@ -28,15 +26,13 @@ class JsonCounterTest {
}
@Test
- fun `verify increase with Long type`() {
- runTest {
- document.updateAsync { root, _ ->
- val obj = root.setNewObject("k1")
- val length = obj.setNewCounter("length", 1L)
- length.increase(1000L)
- length.increase(100)
- }.await()
- }
+ fun `verify increase with Long type`() = runTest {
+ document.updateAsync { root, _ ->
+ val obj = root.setNewObject("k1")
+ val length = obj.setNewCounter("length", 1L)
+ length.increase(1000L)
+ length.increase(100)
+ }.await()
assertJsonContentEquals(
"""{ "k1": { "length": 1101 } }""",
@@ -45,18 +41,16 @@ class JsonCounterTest {
}
@Test
- fun `verify whether increase input type is casted to counter type`() {
- runTest {
- document.updateAsync { root, _ ->
- val obj = root.setNewObject("k1")
- val lengthLong = obj.setNewCounter("lengthLong", 1L)
- lengthLong.increase(1000)
- assertEquals(CounterType.IntegerCnt, lengthLong.target.type)
-
- val lengthInt = obj.setNewCounter("lengthInt", 1)
- lengthInt.increase(1000L)
- assertEquals(CounterType.LongCnt, lengthInt.target.type)
- }.await()
- }
+ fun `verify whether increase input type is casted to counter type`() = runTest {
+ document.updateAsync { root, _ ->
+ val obj = root.setNewObject("k1")
+ val lengthLong = obj.setNewCounter("lengthLong", 1L)
+ lengthLong.increase(1000)
+ assertEquals(CounterType.IntegerCnt, lengthLong.target.type)
+
+ val lengthInt = obj.setNewCounter("lengthInt", 1)
+ lengthInt.increase(1000L)
+ assertEquals(CounterType.LongCnt, lengthInt.target.type)
+ }.await()
}
}