From e3096888140fb07f515130aec4466d2815cb3766 Mon Sep 17 00:00:00 2001
From: Jung gyun Ahn
Date: Tue, 21 May 2024 15:51:58 +0900
Subject: [PATCH] Add Tree concurrency tests (#169)
* Add Tree concurrency tests
* Update swift-integration.yml
* Update swift-integration.yml
* Update swift-integration.yml
---
.github/workflows/swift-integration.yml | 12 +-
Sources/Core/Client.swift | 3 +-
Sources/Document/Change/ChangeContext.swift | 9 +
Sources/Document/Json/JSONTree.swift | 9 +
Tests/Integration/TreeConcurrencyTests.swift | 919 ++++++++++++++++++
Yorkie.xcodeproj/project.pbxproj | 4 +
.../xcshareddata/xcschemes/Yorkie.xcscheme | 2 +-
7 files changed, 949 insertions(+), 9 deletions(-)
create mode 100644 Tests/Integration/TreeConcurrencyTests.swift
diff --git a/.github/workflows/swift-integration.yml b/.github/workflows/swift-integration.yml
index 4fadfa28..ecec2012 100644
--- a/.github/workflows/swift-integration.yml
+++ b/.github/workflows/swift-integration.yml
@@ -8,15 +8,15 @@ on:
jobs:
build:
- runs-on: macos-13
+ runs-on: macos-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- - uses: actions/checkout@v3
- - name: Setup Docker on macOS using Colima, Lima-VM, and Homebrew.
- uses: douglascamata/setup-docker-macos-action@main
- id: docker
- - run: docker-compose -f docker/docker-compose-ci.yml up --build -d
+ - uses: actions/checkout@v4
+ - name: Setup yorkie server
+ run: brew install yorkie
+ - name: Run yorkie server
+ run: yorkie server &
- name: Run tests
run: swift test --enable-code-coverage -v --filter YorkieIntegrationTests
diff --git a/Sources/Core/Client.swift b/Sources/Core/Client.swift
index 43b44a61..e63ded4e 100644
--- a/Sources/Core/Client.swift
+++ b/Sources/Core/Client.swift
@@ -197,7 +197,7 @@ public actor Client {
let builder: ClientConnection.Builder
if rpcAddress.isSecured {
- builder = ClientConnection.usingTLSBackedByNetworkFramework(on: self.group)
+ builder = ClientConnection.usingPlatformAppropriateTLS(for: self.group)
} else {
builder = ClientConnection.insecure(group: self.group)
}
@@ -226,7 +226,6 @@ public actor Client {
}
deinit {
- try? self.group.syncShutdownGracefully()
try? self.rpcClient.channel.close().wait()
}
diff --git a/Sources/Document/Change/ChangeContext.swift b/Sources/Document/Change/ChangeContext.swift
index ff4256d0..2b257fbf 100644
--- a/Sources/Document/Change/ChangeContext.swift
+++ b/Sources/Document/Change/ChangeContext.swift
@@ -37,6 +37,15 @@ class ChangeContext {
self.delimiter = TimeTicket.Values.initialDelimiter
}
+ func deepcopy() -> ChangeContext {
+ let clone = ChangeContext(id: self.id, root: self.root.deepcopy(), message: self.message)
+
+ clone.operations = self.operations
+ clone.delimiter = self.delimiter
+
+ return clone
+ }
+
/**
* `push` pushes the given operation to this context.
*/
diff --git a/Sources/Document/Json/JSONTree.swift b/Sources/Document/Json/JSONTree.swift
index 9764c06c..6b595e96 100644
--- a/Sources/Document/Json/JSONTree.swift
+++ b/Sources/Document/Json/JSONTree.swift
@@ -259,6 +259,15 @@ public class JSONTree {
self.tree = tree
}
+ func deepcopy() -> JSONTree {
+ let clone = JSONTree(initialRoot: self.initialRoot)
+
+ clone.context = self.context?.deepcopy()
+ clone.tree = self.tree?.deepcopy() as? CRDTTree
+
+ return clone
+ }
+
/**
* `id` returns the ID of this tree.
*/
diff --git a/Tests/Integration/TreeConcurrencyTests.swift b/Tests/Integration/TreeConcurrencyTests.swift
new file mode 100644
index 00000000..a75d7ad1
--- /dev/null
+++ b/Tests/Integration/TreeConcurrencyTests.swift
@@ -0,0 +1,919 @@
+/*
+ * Copyright 2024 The Yorkie Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import XCTest
+@testable import Yorkie
+
+func parseSimpleXML(_ string: String) -> [String] {
+ var res: [String] = []
+ var index = string.startIndex
+
+ while index < string.endIndex {
+ var now = ""
+
+ if string[index] == "<" {
+ while index < string.endIndex, string[index] != ">" {
+ now.append(string[index])
+ index = string.index(after: index)
+ }
+ }
+
+ if index < string.endIndex {
+ now.append(string[index])
+ index = string.index(after: index)
+ }
+
+ res.append(now)
+ }
+
+ return res
+}
+
+struct TestResult {
+ let before: (String, String)
+ let after: (String, String)
+}
+
+enum RangeSelector {
+ case rangeUnknown
+ case rangeFront
+ case rangeMiddle
+ case rangeBack
+ case rangeAll
+ case rangeOneQuarter
+ case rangeThreeQuarter
+}
+
+struct RangeType {
+ let from: Int
+ let to: Int
+}
+
+struct RangeWithMiddleType {
+ let from: Int
+ let mid: Int
+ let to: Int
+}
+
+struct TwoRangesType {
+ let ranges: (RangeWithMiddleType, RangeWithMiddleType)
+ let desc: String
+}
+
+func getRange(_ ranges: TwoRangesType, _ selector: RangeSelector, _ user: Int) -> RangeType {
+ let selectedRange = user == 0 ? ranges.ranges.0 : ranges.ranges.1
+
+ let q1 = (selectedRange.from + selectedRange.mid + 1) >> 1 // Math.floor(x/2)
+ let q3 = (selectedRange.mid + selectedRange.to) >> 1
+
+ switch selector {
+ case .rangeFront:
+ return RangeType(from: selectedRange.from, to: selectedRange.from)
+ case .rangeMiddle:
+ return RangeType(from: selectedRange.mid, to: selectedRange.mid)
+ case .rangeBack:
+ return RangeType(from: selectedRange.to, to: selectedRange.to)
+ case .rangeAll:
+ return RangeType(from: selectedRange.from, to: selectedRange.to)
+ case .rangeOneQuarter:
+ return RangeType(from: q1, to: q1)
+ case .rangeThreeQuarter:
+ return RangeType(from: q3, to: q3)
+ default:
+ return RangeType(from: -1, to: -1)
+ }
+}
+
+// swiftlint: disable function_parameter_count
+func makeTwoRanges(_ from1: Int, _ mid1: Int, _ to1: Int, _ from2: Int, _ mid2: Int, _ to2: Int, _ desc: String) -> TwoRangesType {
+ let range0 = RangeWithMiddleType(from: from1, mid: mid1, to: to1)
+ let range1 = RangeWithMiddleType(from: from2, mid: mid2, to: to2)
+ return TwoRangesType(ranges: (range0, range1), desc: desc)
+}
+
+// swiftlint: enable function_parameter_count
+
+func getMergeRange(_ xml: String, _ interval: RangeType) -> RangeType {
+ let content = parseSimpleXML(xml)
+ var st = -1
+ var ed = -1
+
+ for index in (interval.from + 1) ... interval.to {
+ if st == -1 && content[index].hasPrefix("") {
+ st = index - 1
+ }
+ if content[index].hasPrefix("<") && !content[index].hasPrefix("") {
+ ed = index
+ }
+ }
+
+ return RangeType(from: st, to: ed)
+}
+
+enum StyleOpCode {
+ case styleUndefined
+ case styleRemove
+ case styleSet
+}
+
+enum EditOpCode {
+ case editUndefined
+ case editUpdate
+ case mergeUpdate
+ case splitUpdate
+}
+
+protocol OperationInterface {
+ func run(_ doc: Document, _ user: Int, _ ranges: TwoRangesType) async throws
+ func getDesc() -> String
+}
+
+class StyleOperationType: OperationInterface {
+ private let selector: RangeSelector
+ private let op: StyleOpCode
+ private let key: String
+ private let value: String
+ private let desc: String
+
+ init(_ selector: RangeSelector, _ op: StyleOpCode, _ key: String, _ value: String, _ desc: String) {
+ self.selector = selector
+ self.op = op
+ self.key = key
+ self.value = value
+ self.desc = desc
+ }
+
+ func getDesc() -> String {
+ self.desc
+ }
+
+ func run(_ doc: Document, _ user: Int, _ ranges: TwoRangesType) async throws {
+ let interval = getRange(ranges, self.selector, user)
+ let from = interval.from
+ let to = interval.to
+
+ try await doc.update { root, _ in
+ if self.op == .styleRemove {
+ try (root.t as? JSONTree)?.removeStyle(from, to, [self.key])
+ } else if self.op == .styleSet {
+ try (root.t as? JSONTree)?.style(from, to, [key: self.value])
+ }
+ }
+ }
+}
+
+class EditOperationType: OperationInterface {
+ private let selector: RangeSelector
+ private let op: EditOpCode
+ private let content: (any JSONTreeNode)?
+ private let splitLevel: Int32
+ private let desc: String
+
+ init(_ selector: RangeSelector, _ op: EditOpCode, _ content: (any JSONTreeNode)?, _ splitLevel: Int32, _ desc: String) {
+ self.selector = selector
+ self.op = op
+ self.content = content
+ self.splitLevel = splitLevel
+ self.desc = desc
+ }
+
+ func getDesc() -> String {
+ self.desc
+ }
+
+ func run(_ doc: Document, _ user: Int, _ ranges: TwoRangesType) async throws {
+ let interval = getRange(ranges, self.selector, user)
+ let from = interval.from
+ let to = interval.to
+
+ try await doc.update { root, _ in
+ if self.op == .editUpdate {
+ try (root.t as? JSONTree)?.edit(from, to, self.content, self.splitLevel)
+ } else if self.op == .mergeUpdate {
+ let xml = (root.t as? JSONTree)!.toXML()
+ let mergeInterval = getMergeRange(xml, interval)
+ let st = mergeInterval.from, ed = mergeInterval.to
+ if st != -1 && ed != -1 && st < ed {
+ try (root.t as? JSONTree)?.edit(st, ed, self.content, self.splitLevel)
+ }
+ } else if self.op == .splitUpdate {
+ XCTAssertNotEqual(0, self.splitLevel)
+ XCTAssertEqual(from, to)
+ try (root.t as? JSONTree)?.edit(from, to, self.content, self.splitLevel)
+ }
+ }
+ }
+}
+
+final class TreeConcurrencyTests: XCTestCase {
+ // swiftlint: disable function_parameter_count
+ func runTest(initialState: JSONTree,
+ initialXML: String,
+ ranges: TwoRangesType,
+ op1: OperationInterface,
+ op2: OperationInterface,
+ desc: String) async throws -> TestResult
+ {
+ let rpcAddress = RPCAddress(host: "localhost", port: 8080)
+
+ let docKey = "\(Date().description)-\(desc)".toDocKey
+
+ let c1 = Client(rpcAddress)
+ let c2 = Client(rpcAddress)
+
+ try await c1.activate()
+ try await c2.activate()
+
+ let d1 = Document(key: docKey)
+ let d2 = Document(key: docKey)
+
+ try await c1.attach(d1, [:], .manual)
+ try await c2.attach(d2, [:], .manual)
+
+ try await d1.update { root, _ in
+ root.t = initialState
+ }
+ try await c1.sync()
+ try await c2.sync()
+ print("====== \(desc) ====== ")
+ let d1XML = await(d1.getRoot().t as? JSONTree)?.toXML()
+ let d2XML = await(d2.getRoot().t as? JSONTree)?.toXML()
+ XCTAssertEqual(d1XML, initialXML)
+ XCTAssertEqual(d2XML, initialXML)
+
+ try await op1.run(d1, 0, ranges)
+ try await op2.run(d2, 0, ranges)
+
+ let before1 = await(d1.getRoot().t as? JSONTree)?.toXML() ?? ""
+ let before2 = await(d2.getRoot().t as? JSONTree)?.toXML() ?? ""
+
+ // save own changes and get previous changes
+ try await c1.sync()
+ try await c2.sync()
+
+ // get last client changes
+ try await c1.sync()
+ try await c2.sync()
+
+ let after1 = await(d1.getRoot().t as? JSONTree)?.toXML() ?? ""
+ let after2 = await(d2.getRoot().t as? JSONTree)?.toXML() ?? ""
+
+ try await c1.detach(d1)
+ try await c2.detach(d2)
+ try await c1.deactivate()
+ try await c2.deactivate()
+
+ return TestResult(before: (before1, before2), after: (after1, after2))
+ }
+
+ func runTestConcurrency(_ testDesc: String,
+ _ initialState: JSONTree,
+ _ initialXML: String,
+ _ rangesArr: [TwoRangesType],
+ _ op1Arr: [OperationInterface],
+ _ op2Arr: [OperationInterface]) async throws
+ {
+ for ranges in rangesArr {
+ for op1 in op1Arr {
+ var exps = [XCTestExpectation]()
+
+ for op2 in op2Arr {
+ let desc = "\(testDesc)-[\(ranges.desc)](\(op1.getDesc()),\(op2.getDesc()))"
+ let exp = expectation(description: "\(desc)")
+
+ exps.append(exp)
+
+ DispatchQueue.global().async {
+ Task {
+ let result = try await self.runTest(initialState: initialState.deepcopy(), initialXML: initialXML, ranges: ranges, op1: op1, op2: op2, desc: desc)
+
+ print("====== before d1: \(result.before.0)")
+ print("====== before d2: \(result.before.1)")
+ print("====== after d1: \(result.after.0)")
+ print("====== after d2: \(result.after.1)")
+ XCTAssertEqual(result.after.0, result.after.1)
+
+ exp.fulfill()
+ }
+ }
+ }
+ await fulfillment(of: exps, timeout: 10)
+ }
+ }
+ }
+
+ // swiftlint: enable function_parameter_count
+
+ // swiftlint: disable function_body_length
+ func test_concurrently_edit_edit_test() async throws {
+ let initialTree = JSONTree(initialRoot:
+ JSONTreeElementNode(type: "r",
+ children: [
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abc")]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "def")]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "ghi")])
+ ])
+ )
+ let initialXML = "abc
def
ghi
"
+
+ let textNode1 = JSONTreeTextNode(value: "A")
+ let textNode2 = JSONTreeTextNode(value: "B")
+ let elementNode1 = JSONTreeElementNode(type: "b")
+ let elementNode2 = JSONTreeElementNode(type: "i")
+
+ let rangesArr = [
+ // intersect-element: abc
def
- def
ghi
+ makeTwoRanges(0, 5, 10, 5, 10, 15, "intersect-element"),
+ // intersect-text: ab - bc
+ makeTwoRanges(1, 2, 3, 2, 3, 4, "intersect-text"),
+ // contain-element: abc
def
ghi
- def
+ makeTwoRanges(0, 5, 15, 5, 5, 10, "contain-element"),
+ // contain-text: abc - b
+ makeTwoRanges(1, 2, 4, 2, 2, 3, "contain-text"),
+ // contain-mixed-type: abc
def
ghi
- def
+ makeTwoRanges(0, 5, 15, 6, 7, 9, "contain-mixed-type"),
+ // side-by-side-element: abc
- def
+ makeTwoRanges(0, 5, 5, 5, 5, 10, "side-by-side-element"),
+ // side-by-side-text: a - bc
+ makeTwoRanges(1, 1, 2, 2, 3, 4, "side-by-side-text"),
+ // equal-element: abc
def
- abc
def
+ makeTwoRanges(0, 5, 10, 0, 5, 10, "equal-element"),
+ // equal-text: abc - abc
+ makeTwoRanges(1, 2, 4, 1, 2, 4, "equal-text")
+ ]
+
+ let edit1Operations: [EditOperationType] = [
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ textNode1,
+ 0,
+ "insertTextFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ textNode1,
+ 0,
+ "insertTextMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ textNode1,
+ 0,
+ "insertTextBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ textNode1,
+ 0,
+ "replaceText"
+ ),
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ elementNode1,
+ 0,
+ "insertElementFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ elementNode1,
+ 0,
+ "insertElementMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ elementNode1,
+ 0,
+ "insertElementBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ elementNode1,
+ 0,
+ "replaceElement"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ nil,
+ 0,
+ "delete"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .mergeUpdate,
+ nil,
+ 0,
+ "merge"
+ )
+ ]
+
+ let edit2Operations: [EditOperationType] = [
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ textNode2,
+ 0,
+ "insertTextFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ textNode2,
+ 0,
+ "insertTextMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ textNode2,
+ 0,
+ "insertTextBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ textNode2,
+ 0,
+ "replaceText"
+ ),
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ elementNode2,
+ 0,
+ "insertElementFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ elementNode2,
+ 0,
+ "insertElementMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ elementNode2,
+ 0,
+ "insertElementBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ elementNode2,
+ 0,
+ "replaceElement"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ nil,
+ 0,
+ "delete"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .mergeUpdate,
+ nil,
+ 0,
+ "merge"
+ )
+ ]
+
+ try await runTestConcurrency(
+ "concurrently-edit-edit-test",
+ initialTree,
+ initialXML,
+ rangesArr,
+ edit1Operations,
+ edit2Operations
+ )
+ }
+
+ func skip_test_concurrently_style_style_test() async throws {
+ let initialTree = JSONTree(initialRoot:
+ JSONTreeElementNode(type: "r",
+ children: [
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "a")]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "b")]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "c")])
+ ])
+ )
+ let initialXML = "a
b
c
"
+
+ let rangesArr = [
+ // equal: b
- b
+ makeTwoRanges(3, -1, 6, 3, -1, 6, "equal"),
+ // contain: a
b
c
- b
+ makeTwoRanges(0, -1, 9, 3, -1, 6, "contain"),
+ // intersect: a
b
- b
c
+ makeTwoRanges(0, -1, 6, 3, -1, 9, "intersect"),
+ // side-by-side: a
- b
+ makeTwoRanges(0, -1, 3, 3, -1, 6, "side-by-side")
+ ]
+
+ let styleOperations: [StyleOperationType] = [
+ StyleOperationType(
+ .rangeAll,
+ .styleRemove,
+ "bold",
+ "",
+ "remove-bold"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "bold",
+ "aa",
+ "set-bold-aa"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "bold",
+ "bb",
+ "set-bold-bb"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleRemove,
+ "italic",
+ "",
+ "remove-italic"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "italic",
+ "aa",
+ "set-italic-aa"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "italic",
+ "bb",
+ "set-italic-bb"
+ )
+ ]
+
+ // Define range & style operations
+ try await runTestConcurrency(
+ "concurrently-style-style-test",
+ initialTree,
+ initialXML,
+ rangesArr,
+ styleOperations,
+ styleOperations
+ )
+ }
+
+ func test_concurrently_edit_style_test() async throws {
+ let initialTree = JSONTree(initialRoot:
+ JSONTreeElementNode(type: "r",
+ children: [
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "a")], attributes: ["color": "red"]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "b")], attributes: ["color": "red"]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "c")], attributes: ["color": "red"])
+ ])
+ )
+ let initialXML = "a
b
c
"
+
+ let content = JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "d")], attributes: ["italic": true])
+
+ let rangesArr = [
+ // equal: b
- b
+ makeTwoRanges(3, 3, 6, 3, -1, 6, "equal"),
+ // equal multiple: a
b
c
- a
b
c
+ makeTwoRanges(0, 3, 9, 0, 3, 9, "equal multiple"),
+ // A contains B: a
b
c
- b
+ makeTwoRanges(0, 3, 9, 3, -1, 6, "A contains B"),
+ // B contains A: b
- a
b
c
+ makeTwoRanges(3, 3, 6, 0, -1, 9, "B contains A"),
+ // intersect: a
b
- b
c
+ makeTwoRanges(0, 3, 6, 3, -1, 9, "intersect"),
+ // A -> B: a
- b
+ makeTwoRanges(0, 3, 3, 3, -1, 6, "A -> B"),
+ // B -> A: b
- a
+ makeTwoRanges(3, 3, 6, 0, -1, 3, "B -> A")
+ ]
+
+ let editOperations: [EditOperationType] = [
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ content,
+ 0,
+ "insertFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ content,
+ 0,
+ "insertMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ content,
+ 0,
+ "insertBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ nil,
+ 0,
+ "delete"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ content,
+ 0,
+ "replace"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .mergeUpdate,
+ nil,
+ 0,
+ "merge"
+ )
+ ]
+
+ let styleOperations: [StyleOperationType] = [
+ StyleOperationType(
+ .rangeAll,
+ .styleRemove,
+ "color",
+ "",
+ "remove-bold"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "bold",
+ "aa",
+ "set-bold-aa"
+ )
+ ]
+
+ try await runTestConcurrency(
+ "concurrently-edit-style-test",
+ initialTree,
+ initialXML,
+ rangesArr,
+ editOperations,
+ styleOperations
+ )
+ }
+
+ func skip_test_concurrently_split_split_test() async throws {
+ let initialTree = JSONTree(initialRoot:
+ JSONTreeElementNode(type: "r",
+ children: [
+ JSONTreeElementNode(type: "p", children: [
+ JSONTreeElementNode(type: "p", children: [
+ JSONTreeElementNode(type: "p", children: [
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abcd")]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abcd")])
+ ]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "ijkl")])
+ ])
+ ])
+ ])
+ )
+
+ let initialXML = "abcd
efgh
ijkl
"
+
+ let rangesArr = [
+ // equal-single-element: abcd
+ makeTwoRanges(3, 6, 9, 3, 6, 9, "equal-single"),
+ // equal-multiple-element: abcd
efgh
+ makeTwoRanges(3, 9, 15, 3, 9, 15, "equal-multiple"),
+ // A contains B same level: abcd
efgh
- efgh
+ makeTwoRanges(3, 9, 15, 9, 12, 15, "A contains B same level"),
+ // A contains B multiple level: abcd
efgh
ijkl
- efgh
+ makeTwoRanges(2, 16, 22, 9, 12, 15, "A contains B multiple level"),
+ // side by side
+ makeTwoRanges(3, 6, 9, 9, 12, 15, "B is next to A")
+ ]
+
+ let splitOperations: [EditOperationType] = [
+ EditOperationType(
+ .rangeFront,
+ .splitUpdate,
+ nil,
+ 1,
+ "split-front-1"
+ ),
+ EditOperationType(
+ .rangeOneQuarter,
+ .splitUpdate,
+ nil,
+ 1,
+ "split-one-quarter-1"
+ ),
+ EditOperationType(
+ .rangeThreeQuarter,
+ .splitUpdate,
+ nil,
+ 1,
+ "split-three-quarter-1"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .splitUpdate,
+ nil,
+ 1,
+ "split-back-1"
+ ),
+ EditOperationType(
+ .rangeFront,
+ .splitUpdate,
+ nil,
+ 2,
+ "split-front-2"
+ ),
+ EditOperationType(
+ .rangeOneQuarter,
+ .splitUpdate,
+ nil,
+ 2,
+ "split-one-quarter-2"
+ ),
+ EditOperationType(
+ .rangeThreeQuarter,
+ .splitUpdate,
+ nil,
+ 2,
+ "split-three-quarter-2"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .splitUpdate,
+ nil,
+ 2,
+ "split-back-2"
+ )
+ ]
+
+ try await runTestConcurrency(
+ "concurrently-split-split-test",
+ initialTree,
+ initialXML,
+ rangesArr,
+ splitOperations,
+ splitOperations
+ )
+ }
+
+ func skip_test_concurrently_split_edit_test() async throws {
+ let initialTree = JSONTree(initialRoot:
+ JSONTreeElementNode(type: "r",
+ children: [
+ JSONTreeElementNode(type: "p", children: [
+ JSONTreeElementNode(type: "p", children: [
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abcd")], attributes: ["italic": "a"]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abcd")], attributes: ["italic": "a"])
+ ]),
+ JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "ijkl")], attributes: ["italic": "a"])
+ ])
+ ])
+ )
+
+ let initialXML = "abcd
efgh
ijkl
"
+
+ let content = JSONTreeElementNode(type: "i")
+
+ let rangesArr = [
+ // equal: ab"cd
+ makeTwoRanges(2, 5, 8, 2, 5, 8, "equal"),
+ // A contains B: ab"cd
- bc
+ makeTwoRanges(2, 5, 8, 4, 5, 6, "A contains B"),
+ // B contains A: ab"cd
- abcd
efgh
+ makeTwoRanges(2, 5, 8, 2, 8, 14, "B contains A"),
+ // left node(text): ab"cd
- ab
+ makeTwoRanges(2, 5, 8, 3, 4, 5, "left node(text)"),
+ // right node(text): ab"cd
- cd
+ makeTwoRanges(2, 5, 8, 5, 6, 7, "right node(text)"),
+ // left node(element): abcd
"efgh
- abcd
+ makeTwoRanges(2, 8, 14, 2, 5, 8, "left node(element)"),
+ // right node(element): abcd
"efgh
- efgh
+ makeTwoRanges(2, 8, 14, 8, 11, 14, "right node(element)"),
+ // A -> B: ab"cd
- efgh
+ makeTwoRanges(2, 5, 8, 8, 11, 14, "A -> B"),
+ // B -> A: ef"gh
- abcd
+ makeTwoRanges(8, 11, 14, 2, 5, 8, "B -> A")
+ ]
+
+ let splitOperations: [EditOperationType] = [
+ EditOperationType(
+ .rangeMiddle,
+ .splitUpdate,
+ nil,
+ 1,
+ "split-1"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .splitUpdate,
+ nil,
+ 2,
+ "split-2"
+ )
+ ]
+
+ let editOperations: [OperationInterface] = [
+ EditOperationType(
+ .rangeFront,
+ .editUpdate,
+ content,
+ 0,
+ "insertFront"
+ ),
+ EditOperationType(
+ .rangeMiddle,
+ .editUpdate,
+ content,
+ 0,
+ "insertMiddle"
+ ),
+ EditOperationType(
+ .rangeBack,
+ .editUpdate,
+ content,
+ 0,
+ "insertBack"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ content,
+ 0,
+ "replace"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .editUpdate,
+ nil,
+ 0,
+ "delete"
+ ),
+ EditOperationType(
+ .rangeAll,
+ .mergeUpdate,
+ nil,
+ 0,
+ "merge"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleSet,
+ "bold",
+ "aa",
+ "style"
+ ),
+ StyleOperationType(
+ .rangeAll,
+ .styleRemove,
+ "italic",
+ "",
+ "remove-style"
+ )
+ ]
+
+ try await runTestConcurrency(
+ "concurrently-split-edit-test",
+ initialTree,
+ initialXML,
+ rangesArr,
+ splitOperations,
+ editOperations
+ )
+ }
+ // swiftlint: enable function_body_length
+}
diff --git a/Yorkie.xcodeproj/project.pbxproj b/Yorkie.xcodeproj/project.pbxproj
index cd607225..c0a6300b 100644
--- a/Yorkie.xcodeproj/project.pbxproj
+++ b/Yorkie.xcodeproj/project.pbxproj
@@ -39,6 +39,7 @@
9AB38FD42A9337DF006FA879 /* PresenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB38FD32A9337DF006FA879 /* PresenceTests.swift */; };
9AB532232A52DB450098FD25 /* CRDTTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7E88BB2A495F7F00AF8997 /* CRDTTree.swift */; };
9AB6744D2A525BA800EBD282 /* IndexTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB6744C2A525BA800EBD282 /* IndexTreeTests.swift */; };
+ 9AC481502BFB164900C51338 /* TreeConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC4814F2BFB164900C51338 /* TreeConcurrencyTests.swift */; };
9AD11BF32A544353005C84D2 /* TreeEditOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD11BF22A544353005C84D2 /* TreeEditOperation.swift */; };
9AD11BF52A544362005C84D2 /* TreeSytleOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD11BF42A544362005C84D2 /* TreeSytleOperation.swift */; };
9AD6E7E329C190E7001A1F89 /* resources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD6E7DE29C190E7001A1F89 /* resources.pb.swift */; };
@@ -167,6 +168,7 @@
9A97C18B2A8F32CE002AEFC5 /* Presence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = ""; };
9AB38FD32A9337DF006FA879 /* PresenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresenceTests.swift; sourceTree = ""; };
9AB6744C2A525BA800EBD282 /* IndexTreeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexTreeTests.swift; sourceTree = ""; };
+ 9AC4814F2BFB164900C51338 /* TreeConcurrencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreeConcurrencyTests.swift; sourceTree = ""; };
9AD11BF22A544353005C84D2 /* TreeEditOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeEditOperation.swift; sourceTree = ""; };
9AD11BF42A544362005C84D2 /* TreeSytleOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeSytleOperation.swift; sourceTree = ""; };
9AD6E7DE29C190E7001A1F89 /* resources.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = resources.pb.swift; sourceTree = ""; };
@@ -366,6 +368,7 @@
9ADECA002919FC1A006BA331 /* Integration */ = {
isa = PBXGroup;
children = (
+ 9AC4814F2BFB164900C51338 /* TreeConcurrencyTests.swift */,
96DA809028C5B7B400E2C1DA /* GRPCTests.swift */,
9A4DC742292B5E0500C89478 /* CounterIntegrationTests.swift */,
9ADECA012919FC1A006BA331 /* ClientIntegrationTests.swift */,
@@ -889,6 +892,7 @@
CEA2DA4428F672AD00431B61 /* AddOperationTests.swift in Sources */,
CE8BB74028FE2F000020F62A /* ChangeContextTests.swift in Sources */,
CE8ED31F28F566DE009A5419 /* RemoveOperationTests.swift in Sources */,
+ 9AC481502BFB164900C51338 /* TreeConcurrencyTests.swift in Sources */,
CE8BB74128FE2F030020F62A /* CheckpointTests.swift in Sources */,
9A4BC6042AA08D6A00A9D436 /* String+Extensions.swift in Sources */,
9A3CF3EF2966A7370024E3DD /* CRDTTextTests.swift in Sources */,
diff --git a/Yorkie.xcodeproj/xcshareddata/xcschemes/Yorkie.xcscheme b/Yorkie.xcodeproj/xcshareddata/xcschemes/Yorkie.xcscheme
index 46f64980..58ff3b2b 100644
--- a/Yorkie.xcodeproj/xcshareddata/xcschemes/Yorkie.xcscheme
+++ b/Yorkie.xcodeproj/xcshareddata/xcschemes/Yorkie.xcscheme
@@ -23,7 +23,7 @@