diff --git a/test/integration/tree_concurrency_test.go b/test/integration/tree_concurrency_test.go index b62e41706..09c0a55b5 100644 --- a/test/integration/tree_concurrency_test.go +++ b/test/integration/tree_concurrency_test.go @@ -30,6 +30,24 @@ import ( "github.com/yorkie-team/yorkie/test/helper" ) +func parseSimpleXML(s string) []string { + res := []string{} + for i := 0; i < len(s); i++ { + now := `` + if s[i] == '<' { + for i < len(s) && s[i] != '>' { + now += string(s[i]) + i++ + } + now += string(s[i]) + } else { + now += string(s[i]) + } + res = append(res, now) + } + return res +} + type testResult struct { flag bool resultDesc string @@ -43,6 +61,8 @@ const ( RangeMiddle RangeBack RangeAll + RangeOneQuarter + RangeThreeQuarter ) type rangeType struct { @@ -59,14 +79,22 @@ type twoRangesType struct { } func getRange(ranges twoRangesType, selector rangeSelector, user int) rangeType { + interval := ranges.ranges[user] + from, mid, to := interval.from, interval.mid, interval.to if selector == RangeFront { - return rangeType{ranges.ranges[user].from, ranges.ranges[user].from} + return rangeType{from, from} } else if selector == RangeMiddle { - return rangeType{ranges.ranges[user].mid, ranges.ranges[user].mid} + return rangeType{mid, mid} } else if selector == RangeBack { - return rangeType{ranges.ranges[user].to, ranges.ranges[user].to} + return rangeType{to, to} } else if selector == RangeAll { - return rangeType{ranges.ranges[user].from, ranges.ranges[user].to} + return rangeType{from, to} + } else if selector == RangeOneQuarter { + pos := (from + mid + 1) / 2 + return rangeType{pos, pos} + } else if selector == RangeThreeQuarter { + pos := (mid + to) / 2 + return rangeType{pos, pos} } return rangeType{-1, -1} } @@ -77,6 +105,20 @@ func makeTwoRanges(from1, mid1, to1 int, from2, mid2, to2 int, desc string) twoR return twoRangesType{[2]rangeWithMiddleType{range0, range1}, desc} } +func getMergeRange(xml string, interval rangeType) rangeType { + content := parseSimpleXML(xml) + st, ed := -1, -1 + for i := interval.from + 1; i <= interval.to; i++ { + if st == -1 && len(content[i]) >= 2 && content[i][0] == '<' && content[i][1] == '/' { + st = i - 1 + } + if len(content[i]) >= 2 && content[i][0] == '<' && content[i][1] != '/' { + ed = i + } + } + return rangeType{st, ed} +} + type styleOpCode int type editOpCode int @@ -89,6 +131,8 @@ const ( const ( EditUndefined editOpCode = iota EditUpdate + MergeUpdate + SplitUpdate ) type operationInterface interface { @@ -138,7 +182,19 @@ func (op editOperationType) run(t *testing.T, doc *document.Document, user int, from, to := interval.from, interval.to assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { - root.GetTree("t").Edit(from, to, op.content, op.splitLevel) + if op.op == EditUpdate { + root.GetTree("t").Edit(from, to, op.content, op.splitLevel) + } else if op.op == MergeUpdate { + mergeInterval := getMergeRange(root.GetTree("t").ToXML(), interval) + from, to = mergeInterval.from, mergeInterval.to + if from != -1 && to != -1 && from < to { + root.GetTree("t").Edit(mergeInterval.from, mergeInterval.to, op.content, op.splitLevel) + } + } else if op.op == SplitUpdate { + assert.NotEqual(t, 0, op.splitLevel) + assert.Equal(t, from, to) + root.GetTree("t").Edit(from, to, op.content, op.splitLevel) + } return nil })) } @@ -247,6 +303,7 @@ func TestTreeConcurrencyEditEdit(t *testing.T) { editOperationType{RangeBack, EditUpdate, elementNode1, 0, `insertElementBack`}, editOperationType{RangeAll, EditUpdate, elementNode1, 0, `replaceElement`}, editOperationType{RangeAll, EditUpdate, nil, 0, `delete`}, + editOperationType{RangeAll, MergeUpdate, nil, 0, `merge`}, } editOperations2 := []operationInterface{ @@ -259,11 +316,119 @@ func TestTreeConcurrencyEditEdit(t *testing.T) { editOperationType{RangeBack, EditUpdate, elementNode2, 0, `insertElementBack`}, editOperationType{RangeAll, EditUpdate, elementNode2, 0, `replaceElement`}, editOperationType{RangeAll, EditUpdate, nil, 0, `delete`}, + editOperationType{RangeAll, MergeUpdate, nil, 0, `merge`}, } RunTestTreeConcurrency("concurrently-edit-edit-test", t, initialState, initialXML, ranges, editOperations1, editOperations2) } +func TestTreeConcurrencySplitSplit(t *testing.T) { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + //

a b c d

e f g h

i j k l

+ + initialState := json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "abcd"}}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "efgh"}}}, + }}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ijkl"}}}, + }}, + }}, + }, + } + initialXML := `

abcd

efgh

ijkl

` + + ranges := []twoRangesType{ + // 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`), + } + + splitOperations := []operationInterface{ + 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`}, + } + + RunTestTreeConcurrency("concurrently-split-split-test", t, initialState, initialXML, ranges, splitOperations, splitOperations) +} + +func TestTreeConcurrencySplitEdit(t *testing.T) { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + //

a b c d

e f g h

i j k l

+ + initialState := json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "abcd"}}, Attributes: map[string]string{"italic": "true"}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "efgh"}}, Attributes: map[string]string{"italic": "true"}}, + }, Attributes: map[string]string{"italic": "true"}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ijkl"}}, Attributes: map[string]string{"italic": "true"}}, + }}, + }, + } + initialXML := `

abcd

efgh

ijkl

` + + content := &json.TreeNode{Type: "i", Children: []json.TreeNode{}} + + ranges := []twoRangesType{ + // 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`), + } + + splitOperations := []operationInterface{ + editOperationType{RangeMiddle, SplitUpdate, nil, 1, `split-1`}, + editOperationType{RangeMiddle, SplitUpdate, nil, 2, `split-2`}, + } + + 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`}, + } + + RunTestTreeConcurrency("concurrently-split-edit-test", t, initialState, initialXML, ranges, splitOperations, editOperations) +} + func TestTreeConcurrencyStyleStyle(t *testing.T) { // 0 1 2 3 4 5 6 7 8 9 //

a

b

c

@@ -314,18 +479,20 @@ func TestTreeConcurrencyEditStyle(t *testing.T) { initialState := json.TreeNode{ Type: "root", Children: []json.TreeNode{ - {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "a"}}}, - {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "b"}}}, - {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "c"}}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "a"}}, Attributes: map[string]string{"color": "red"}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "b"}}, Attributes: map[string]string{"color": "red"}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "c"}}, Attributes: map[string]string{"color": "red"}}, }, } - initialXML := `

a

b

c

` + initialXML := `

a

b

c

` content := &json.TreeNode{Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: `d`}}} ranges := []twoRangesType{ // 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

@@ -344,10 +511,11 @@ func TestTreeConcurrencyEditStyle(t *testing.T) { editOperationType{RangeBack, EditUpdate, content, 0, `insertBack`}, editOperationType{RangeAll, EditUpdate, nil, 0, `delete`}, editOperationType{RangeAll, EditUpdate, content, 0, `replace`}, + editOperationType{RangeAll, MergeUpdate, nil, 0, `merge`}, } styleOperations := []operationInterface{ - styleOperationType{RangeAll, StyleRemove, "bold", "", `remove-bold`}, + styleOperationType{RangeAll, StyleRemove, "color", "", `remove-bold`}, styleOperationType{RangeAll, StyleSet, "bold", "aa", `set-bold-aa`}, } diff --git a/test/integration/tree_test.go b/test/integration/tree_test.go index 38df94629..5afb68c8d 100644 --- a/test/integration/tree_test.go +++ b/test/integration/tree_test.go @@ -1343,7 +1343,7 @@ func TestTree(t *testing.T) { assert.Equal(t, "

a

b

", d1.Root().GetTree("t").ToXML()) }) - t.Run("contained-split-and-split-at-diffrent-positions-on-the-same-node", func(t *testing.T) { + t.Run("contained-split-and-split-at-different-positions-on-the-same-node", func(t *testing.T) { ctx := context.Background() d1 := document.New(helper.TestDocKey(t)) assert.NoError(t, c1.Attach(ctx, d1)) @@ -2623,7 +2623,7 @@ func TestTree(t *testing.T) { assert.Equal(t, "

a

b

", d1.Root().GetTree("t").ToXML()) }) - t.Run("side-by-side-split-and-delete", func(t *testing.T) { + t.Run("side-by-side-split-and-merge", func(t *testing.T) { ctx := context.Background() d1 := document.New(helper.TestDocKey(t)) assert.NoError(t, c1.Attach(ctx, d1))