From 9a2e4403bd1fb51eeb9fab130a17c33459eda2cc Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 13:06:28 +0900 Subject: [PATCH 1/9] encapsulate Document --- yorkie/src/main/kotlin/dev/yorkie/core/Client.kt | 4 ++-- yorkie/src/main/kotlin/dev/yorkie/document/Document.kt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index f32b09b37..17d2f4a9e 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt @@ -489,7 +489,7 @@ public class Client @VisibleForTesting internal constructor( return@async SUCCESS } - document.status = DocumentStatus.Attached + document.setStatus(DocumentStatus.Attached) attachments.value += document.key to Attachment( document, response.documentId, @@ -535,7 +535,7 @@ public class Client @VisibleForTesting internal constructor( val pack = response.changePack.toChangePack() document.applyChangePack(pack) if (document.status != DocumentStatus.Removed) { - document.status = DocumentStatus.Detached + document.setStatus(DocumentStatus.Detached) attachments.value -= document.key } SUCCESS diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt index 5b87a47db..808e64703 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt @@ -94,7 +94,7 @@ public class Document( @Volatile public var status = DocumentStatus.Detached - internal set + private set @VisibleForTesting public val garbageLength: Int @@ -451,6 +451,10 @@ public class Document( onlineClients.value -= actorID } + internal fun setStatus(newStatus: DocumentStatus) { + this.status = newStatus + } + /** * Deletes elements that were removed before the given time. */ From 1d36f1d1c96c03d4c3b4f50be00cb1da35a796de Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 13:18:31 +0900 Subject: [PATCH 2/9] bump up libraries to latest --- .github/workflows/ci.yml | 5 +++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) 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" } From 873edde699908a5e2a5c936a623a14bc8d1b5fb5 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 13:18:54 +0900 Subject: [PATCH 3/9] code clean up --- .../test/kotlin/dev/yorkie/core/ClientTest.kt | 278 ++++++------ .../dev/yorkie/document/DocumentTest.kt | 402 +++++++++--------- .../yorkie/document/json/JsonCounterTest.kt | 56 ++- 3 files changed, 346 insertions(+), 390 deletions(-) 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() } } From c079ade1849086d68f785eafc3ede23da17073a7 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 13:20:18 +0900 Subject: [PATCH 4/9] Add Tree concurrency tests --- .../kotlin/dev/yorkie/TestAnnotations.kt | 8 - .../document/json/JsonTreeConcurrencyTest.kt | 363 ++++++++++++++++++ .../json/JsonTreeConcurrencyTestHelpers.kt | 231 +++++++++++ .../document/json/JsonTreeSplitMergeTest.kt | 26 +- .../dev/yorkie/document/json/JsonTreeTest.kt | 2 - .../dev/yorkie/document/json/JsonTree.kt | 9 +- 6 files changed, 614 insertions(+), 25 deletions(-) create mode 100644 yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt create mode 100644 yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTestHelpers.kt diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt index 779ebb2a7..176ee5664 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt @@ -3,11 +3,3 @@ 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..0fa3c5ca7 --- /dev/null +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt @@ -0,0 +1,363 @@ +package dev.yorkie.document.json + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dev.yorkie.TreeTest +import dev.yorkie.core.GENERAL_TIMEOUT +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.delay +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(GENERAL_TIMEOUT) { + 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(" = 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/document/json/JsonTree.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt index 52447a798..43af4affa 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt @@ -125,14 +125,21 @@ public class JsonTree internal constructor( editInternal(fromPos, toPos, contents.toList(), splitLevel) } + /** + * Edits this tree with the given node. + */ + 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" From 6bec8097ad30f7f9411f1a251123ef9897d3a430 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 13:23:43 +0900 Subject: [PATCH 5/9] fix lint --- yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt | 2 ++ .../dev/yorkie/document/json/JsonTreeConcurrencyTest.kt | 2 -- yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt index 176ee5664..c0d26b900 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/TestAnnotations.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:filename") + package dev.yorkie @Target(AnnotationTarget.CLASS) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt index 0fa3c5ca7..57b50d3d5 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt @@ -10,7 +10,6 @@ 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.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import org.junit.Test @@ -236,7 +235,6 @@ class JsonTreeConcurrencyTest { attrs { mapOf("italic" to "true") } } - val ranges = listOf( // equal:

b

-

b

makeTwoRanges(Triple(3, 3, 6), Triple(3, -1, 6), "equal"), 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 43af4affa..a647f9694 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt @@ -128,7 +128,11 @@ public class JsonTree internal constructor( /** * Edits this tree with the given node. */ - public fun edit(fromIndex: Int, toIndex: Int, vararg content: TreeNode) { + public fun edit( + fromIndex: Int, + toIndex: Int, + vararg content: TreeNode, + ) { edit(fromIndex, toIndex, 0, *content) } From dcb49d4d941c06e873ab3640dc33fa4af32e1f8b Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 14:23:04 +0900 Subject: [PATCH 6/9] use H2_PRIOR_KNOWLEDGE --- yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt index cc40a5d77..dd2f25c0a 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt @@ -12,7 +12,7 @@ const val GENERAL_TIMEOUT = 3_000L fun createClient() = Client( "http://10.0.2.2:8080", unaryClient = OkHttpClient.Builder() - .protocols(listOf(Protocol.HTTP_1_1)) + .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) .build(), ) From 9f86bbf04ce7fcd82274a65316996c9a0fba4a69 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Mon, 27 May 2024 15:10:39 +0900 Subject: [PATCH 7/9] increase timeout time --- .../kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt index 57b50d3d5..1074d036e 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeConcurrencyTest.kt @@ -2,7 +2,6 @@ package dev.yorkie.document.json import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.yorkie.TreeTest -import dev.yorkie.core.GENERAL_TIMEOUT import dev.yorkie.core.createClient import dev.yorkie.document.json.OpCode.EditOpCode import dev.yorkie.document.json.TestOperation.EditOperationType @@ -344,7 +343,7 @@ class JsonTreeConcurrencyTest { op1s.forEach { op1 -> op2s.forEach { op2 -> val testDesc = "$desc-${range.desc}(${op1.desc},${op2.desc})" - val result = withTimeout(GENERAL_TIMEOUT) { + val result = withTimeout(10_000) { runTest(c1, c2, root, initialXml, range, op1, op2, testDesc) } assertEquals(result.after.first, result.after.second) From 286eca6a6e1277e7c5f6b0cc15021f0784f7a4d4 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Tue, 28 May 2024 11:20:11 +0900 Subject: [PATCH 8/9] make document wait for its initialization --- yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt | 2 +- yorkie/src/main/kotlin/dev/yorkie/core/Client.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt index dd2f25c0a..cc40a5d77 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt @@ -12,7 +12,7 @@ const val GENERAL_TIMEOUT = 3_000L fun createClient() = Client( "http://10.0.2.2:8080", unaryClient = OkHttpClient.Builder() - .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) + .protocols(listOf(Protocol.HTTP_1_1)) .build(), ) diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index 17d2f4a9e..7f9a964d1 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 } } } From 8febcf6c7fdf75aa47a009bf59db7f22a756bc62 Mon Sep 17 00:00:00 2001 From: jee-hyun-kim Date: Tue, 28 May 2024 13:01:52 +0900 Subject: [PATCH 9/9] Revert "encapsulate Document" This reverts commit 9a2e4403bd1fb51eeb9fab130a17c33459eda2cc. --- yorkie/src/main/kotlin/dev/yorkie/core/Client.kt | 4 ++-- yorkie/src/main/kotlin/dev/yorkie/document/Document.kt | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index 7f9a964d1..440a7f220 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt @@ -489,7 +489,7 @@ public class Client @VisibleForTesting internal constructor( return@async SUCCESS } - document.setStatus(DocumentStatus.Attached) + document.status = DocumentStatus.Attached attachments.value += document.key to Attachment( document, response.documentId, @@ -535,7 +535,7 @@ public class Client @VisibleForTesting internal constructor( val pack = response.changePack.toChangePack() document.applyChangePack(pack) if (document.status != DocumentStatus.Removed) { - document.setStatus(DocumentStatus.Detached) + document.status = DocumentStatus.Detached attachments.value -= document.key } SUCCESS diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt index 808e64703..5b87a47db 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt @@ -94,7 +94,7 @@ public class Document( @Volatile public var status = DocumentStatus.Detached - private set + internal set @VisibleForTesting public val garbageLength: Int @@ -451,10 +451,6 @@ public class Document( onlineClients.value -= actorID } - internal fun setStatus(newStatus: DocumentStatus) { - this.status = newStatus - } - /** * Deletes elements that were removed before the given time. */