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 41a6730b6..b7d4dd86f 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
@@ -3,17 +3,34 @@ package dev.yorkie.document.json
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import dev.yorkie.core.Client
+import dev.yorkie.core.GENERAL_TIMEOUT
import dev.yorkie.core.Presence
import dev.yorkie.core.withTwoClientsAndDocuments
import dev.yorkie.document.Document
+import dev.yorkie.document.Document.Event.LocalChange
+import dev.yorkie.document.Document.Event.RemoteChange
import dev.yorkie.document.json.TreeBuilder.element
import dev.yorkie.document.json.TreeBuilder.text
+import dev.yorkie.document.operation.OperationInfo.TreeEditOpInfo
import kotlin.test.assertEquals
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class JsonTreeTest {
@@ -478,64 +495,67 @@ class JsonTreeTest {
}
@Test
- fun test_insert_side_by_side_elements_into_left_concurrently() {
+ fun test_contained_split_and_split_at_the_same_position() {
withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
updateAndSync(
Updater(c1, d1) { root, _ ->
root.setNewTree(
"t",
element("r") {
- element("p")
+ element("p") {
+ text { "ab" }
+ }
},
)
},
Updater(c2, d2),
)
- assertTreesXmlEquals("", d1, d2)
+ assertTreesXmlEquals("ab
", d1, d2)
updateAndSync(
Updater(c1, d1) { root, _ ->
- root.rootTree().edit(0, 0, element("b"))
+ root.rootTree().edit(2, 2, splitLevel = 1)
},
Updater(c2, d2) { root, _ ->
- root.rootTree().edit(0, 0, element("i"))
+ root.rootTree().edit(2, 2, splitLevel = 1)
},
) {
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
+ assertTreesXmlEquals("a
b
", d1, d2)
}
- assertTreesXmlEquals("", d1, d2)
+ assertTreesXmlEquals("a
b
", d1, d2)
}
}
@Test
- fun test_insert_side_by_side_elements_into_middle_concurrently() {
+ fun test_contained_split_and_split_at_different_positions_on_the_same_node() {
withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
updateAndSync(
Updater(c1, d1) { root, _ ->
root.setNewTree(
"t",
element("r") {
- element("p")
+ element("p") {
+ text { "abc" }
+ }
},
)
},
Updater(c2, d2),
)
- assertTreesXmlEquals("", d1, d2)
+ assertTreesXmlEquals("abc
", d1, d2)
updateAndSync(
Updater(c1, d1) { root, _ ->
- root.rootTree().edit(1, 1, element("b"))
+ root.rootTree().edit(2, 2, splitLevel = 1)
},
Updater(c2, d2) { root, _ ->
- root.rootTree().edit(1, 1, element("i"))
+ root.rootTree().edit(3, 3, splitLevel = 1)
},
) {
- assertEquals("
", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
+ assertTreesXmlEquals("a
bc
", d1)
+ assertTreesXmlEquals("ab
c
", d2)
}
- assertTreesXmlEquals("
", d1, d2)
+ assertTreesXmlEquals("a
b
c
", d1, d2)
}
}
@@ -571,7 +591,7 @@ class JsonTreeTest {
}
@Test
- fun test_insert_and_delete_side_by_side_elements_concurrently() {
+ fun test_contained_split_and_insert_into_the_split_position() {
withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
updateAndSync(
Updater(c1, d1) { root, _ ->
@@ -579,32 +599,32 @@ class JsonTreeTest {
"t",
element("r") {
element("p") {
- element("b")
+ text { "ab" }
}
},
)
},
Updater(c2, d2),
)
- assertTreesXmlEquals("
", d1, d2)
+ assertTreesXmlEquals("ab
", d1, d2)
updateAndSync(
Updater(c1, d1) { root, _ ->
- root.rootTree().edit(1, 3)
+ root.rootTree().edit(2, 2, splitLevel = 1)
},
Updater(c2, d2) { root, _ ->
- root.rootTree().edit(1, 1, element("i"))
+ root.rootTree().edit(2, 2, text { "c" })
},
) {
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
+ assertTreesXmlEquals("a
b
", d1)
+ assertTreesXmlEquals("acb
", d2)
}
- assertTreesXmlEquals("
", d1, d2)
+ assertTreesXmlEquals("ac
b
", d1, d2)
}
}
@Test
- fun test_delete_and_insert_side_by_side_elements_concurrently() {
+ fun test_contained_split_and_insert_into_original_node() {
withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
updateAndSync(
Updater(c1, d1) { root, _ ->
@@ -612,27 +632,94 @@ class JsonTreeTest {
"t",
element("r") {
element("p") {
- element("b")
+ text { "ab" }
}
},
)
},
Updater(c2, d2),
)
- assertTreesXmlEquals("
", d1, d2)
+ assertTreesXmlEquals("ab
", d1, d2)
updateAndSync(
Updater(c1, d1) { root, _ ->
- root.rootTree().edit(1, 3)
+ root.rootTree().edit(2, 2, splitLevel = 1)
},
Updater(c2, d2) { root, _ ->
- root.rootTree().edit(3, 3, element("i"))
+ root.rootTree().edit(1, 1, text { "c" })
},
) {
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
+ assertTreesXmlEquals("a
b
", d1)
+ assertTreesXmlEquals("cab
", d2)
}
- assertTreesXmlEquals("
", d1, d2)
+ assertTreesXmlEquals("ca
b
", d1, d2)
+ }
+ }
+
+ @Test
+ fun test_contained_split_and_insert_into_split_node() {
+ withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.setNewTree(
+ "t",
+ element("r") {
+ element("p") {
+ text { "ab" }
+ }
+ },
+ )
+ },
+ Updater(c2, d2),
+ )
+ assertTreesXmlEquals("ab
", d1, d2)
+
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.rootTree().edit(2, 2, splitLevel = 1)
+ },
+ Updater(c2, d2) { root, _ ->
+ root.rootTree().edit(3, 3, text { "c" })
+ },
+ ) {
+ assertTreesXmlEquals("a
b
", d1)
+ assertTreesXmlEquals("abc
", d2)
+ }
+ assertTreesXmlEquals("a
bc
", d2)
+ }
+ }
+
+ @Test
+ fun test_contained_split_and_delete_contents_in_split_node() {
+ withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.setNewTree(
+ "t",
+ element("r") {
+ element("p") {
+ text { "ab" }
+ }
+ },
+ )
+ },
+ Updater(c2, d2),
+ )
+ assertTreesXmlEquals("ab
", d1, d2)
+
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.rootTree().edit(2, 2, splitLevel = 1)
+ },
+ Updater(c2, d2) { root, _ ->
+ root.rootTree().edit(2, 3)
+ },
+ ) {
+ assertTreesXmlEquals("a
b
", d1)
+ assertTreesXmlEquals("a
", d2)
+ }
+
+ assertTreesXmlEquals("a
", d1, d2)
}
}
@@ -1458,7 +1545,7 @@ class JsonTreeTest {
@Test
fun test_split_and_merge_with_empty_paragraph_left_and_multiple_split_level_left() {
- withTwoClientsAndDocuments { c1, c2, d1, d2, _ ->
+ withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
d1.updateAsync { root, _ ->
root.setNewTree(
"t",
@@ -1552,6 +1639,10 @@ class JsonTreeTest {
assertTreesXmlEquals("ab
", d1)
}
+ val ops1 = mutableListOf()
+ val ops2 = mutableListOf()
+ val jobs = listOf(collectTreeEditOpInfos(d1, ops1), collectTreeEditOpInfos(d2, ops2))
+
updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().edit(0, 4)
@@ -1564,6 +1655,13 @@ class JsonTreeTest {
assertTreesXmlEquals("b
", d2)
}
assertEquals(d1.getRoot().rootTree().toXml(), d2.getRoot().rootTree().toXml())
+
+ assertTreeEditOpInfosEquals(listOf(SimpleTreeEditOpInfo(0, 4)), ops1)
+ assertTreeEditOpInfosEquals(
+ listOf(SimpleTreeEditOpInfo(1, 2), SimpleTreeEditOpInfo(0, 3)),
+ ops2,
+ )
+ jobs.forEach(Job::cancel)
}
}
@@ -1586,6 +1684,10 @@ class JsonTreeTest {
assertTreesXmlEquals("ab
", d1)
}
+ val ops1 = mutableListOf()
+ val ops2 = mutableListOf()
+ val jobs = listOf(collectTreeEditOpInfos(d1, ops1), collectTreeEditOpInfos(d2, ops2))
+
updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().edit(1, 3)
@@ -1598,6 +1700,23 @@ class JsonTreeTest {
assertTreesXmlEquals("acb
", d2)
}
assertEquals(d1.getRoot().rootTree().toXml(), d2.getRoot().rootTree().toXml())
+
+ assertTreeEditOpInfosEquals(
+ listOf(
+ SimpleTreeEditOpInfo(1, 3),
+ SimpleTreeEditOpInfo(1, 1, text { "c" }),
+ ),
+ ops1,
+ )
+ assertTreeEditOpInfosEquals(
+ listOf(
+ SimpleTreeEditOpInfo(2, 2, text { "c" }),
+ SimpleTreeEditOpInfo(1, 2),
+ SimpleTreeEditOpInfo(3, 4),
+ ),
+ ops2,
+ )
+ jobs.forEach(Job::cancel)
}
}
@@ -1620,6 +1739,10 @@ class JsonTreeTest {
assertTreesXmlEquals("ab
", d1)
}
+ val ops1 = mutableListOf()
+ val ops2 = mutableListOf()
+ val jobs = listOf(collectTreeEditOpInfos(d1, ops1), collectTreeEditOpInfos(d2, ops2))
+
updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().edit(0, 4)
@@ -1632,6 +1755,13 @@ class JsonTreeTest {
assertTreesXmlEquals("acb
", d2)
}
assertEquals(d1.getRoot().rootTree().toXml(), d2.getRoot().rootTree().toXml())
+
+ assertTreeEditOpInfosEquals(listOf(SimpleTreeEditOpInfo(0, 4)), ops1)
+ assertTreeEditOpInfosEquals(
+ listOf(SimpleTreeEditOpInfo(2, 2, text { "c" }), SimpleTreeEditOpInfo(0, 5)),
+ ops2,
+ )
+ jobs.forEach(Job::cancel)
}
}
@@ -1654,6 +1784,10 @@ class JsonTreeTest {
assertTreesXmlEquals("a
", d1)
}
+ val ops1 = mutableListOf()
+ val ops2 = mutableListOf()
+ val jobs = listOf(collectTreeEditOpInfos(d1, ops1), collectTreeEditOpInfos(d2, ops2))
+
updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().edit(1, 2, text { "b" })
@@ -1666,6 +1800,61 @@ class JsonTreeTest {
assertTreesXmlEquals("ac
", d2)
}
assertEquals(d1.getRoot().rootTree().toXml(), d2.getRoot().rootTree().toXml())
+
+ assertTreeEditOpInfosEquals(
+ listOf(
+ SimpleTreeEditOpInfo(1, 2, text { "b" }),
+ SimpleTreeEditOpInfo(2, 2, text { "c" }),
+ ),
+ ops1,
+ )
+ assertTreeEditOpInfosEquals(
+ listOf(
+ SimpleTreeEditOpInfo(2, 2, text { "c" }),
+ SimpleTreeEditOpInfo(1, 2, text { "b" }),
+ ),
+ ops2,
+ )
+ jobs.forEach(Job::cancel)
+ }
+ }
+
+ @Test
+ fun test_overlapping_merge_and_merge() {
+ withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.setNewTree(
+ "t",
+ element("r") {
+ element("p") {
+ text { "a" }
+ }
+ element("p") {
+ text { "b" }
+ }
+ element("p") {
+ text { "c" }
+ }
+ },
+ )
+ },
+ Updater(c2, d2),
+ )
+ assertTreesXmlEquals("a
b
c
", d1, d2)
+
+ updateAndSync(
+ Updater(c1, d1) { root, _ ->
+ root.rootTree().edit(2, 4)
+ },
+ Updater(c2, d2) { root, _ ->
+ root.rootTree().edit(5, 7)
+ },
+ ) {
+ assertTreesXmlEquals("ab
c
", d1)
+ assertTreesXmlEquals("a
bc
", d2)
+ }
+ assertTreesXmlEquals("abc
", d1, d2)
}
}
@@ -1706,10 +1895,45 @@ class JsonTreeTest {
}
}
+ fun CoroutineScope.collectTreeEditOpInfos(
+ document: Document,
+ ops: MutableList,
+ ) = launch(start = CoroutineStart.UNDISPATCHED) {
+ document.events("$.t")
+ .flatMapConcat { event ->
+ when (event) {
+ is LocalChange -> event.changeInfo.operations.asFlow()
+ is RemoteChange -> event.changeInfo.operations.asFlow()
+ else -> emptyFlow()
+ }
+ }
+ .filterIsInstance()
+ .map { SimpleTreeEditOpInfo(it.from, it.to, it.nodes?.firstOrNull()) }
+ .collect(ops::add)
+ }
+
+ suspend fun assertTreeEditOpInfosEquals(
+ expected: List,
+ actual: List,
+ ) {
+ withTimeout(GENERAL_TIMEOUT) {
+ while (actual.size < expected.size) {
+ delay(50)
+ }
+ }
+ assertEquals(expected, actual)
+ }
+
data class Updater(
val client: Client,
val document: Document,
val updater: (suspend (JsonObject, Presence) -> Unit)? = null,
)
+
+ data class SimpleTreeEditOpInfo(
+ val from: Int,
+ val to: Int,
+ val nodes: JsonTree.TreeNode? = null,
+ )
}
}