From 18a460f69b8b4ea5dd833f9ed0111f619710aebc Mon Sep 17 00:00:00 2001 From: Jeounghui Nah Date: Thu, 25 Jan 2024 13:11:01 +0900 Subject: [PATCH] Refactor concurrency tests for basic Tree.Edit (#772) Insert/Delete/Replace by 2 users (9 * 9 * 9 = 729 cases): - Ranges(9) - intersecting, element - intersecting, text - A contains B, element - A contains B, text - A contains B, A is element, B is text - side by side, element - side by side, text - A = B, element - A = B, text - Operations for both A and B(9) - Insert text front of range - Insert text middle of range - Insert text back of range - Replace to text node - Insert element front of range - Insert element middle of range - Insert element back of range - Replace to element node - Delete --- test/integration/tree_concurrency_test.go | 329 +++++++++++++--------- 1 file changed, 203 insertions(+), 126 deletions(-) diff --git a/test/integration/tree_concurrency_test.go b/test/integration/tree_concurrency_test.go index 848527eb3..b62e41706 100644 --- a/test/integration/tree_concurrency_test.go +++ b/test/integration/tree_concurrency_test.go @@ -30,6 +30,21 @@ import ( "github.com/yorkie-team/yorkie/test/helper" ) +type testResult struct { + flag bool + resultDesc string +} + +type rangeSelector int + +const ( + RangeUnknown rangeSelector = iota + RangeFront + RangeMiddle + RangeBack + RangeAll +) + type rangeType struct { from, to int } @@ -43,16 +58,6 @@ type twoRangesType struct { desc string } -type rangeSelector int - -const ( - RangeUnknown rangeSelector = iota - RangeFront - RangeMiddle - RangeBack - RangeAll -) - func getRange(ranges twoRangesType, selector rangeSelector, user int) rangeType { if selector == RangeFront { return rangeType{ranges.ranges[user].from, ranges.ranges[user].from} @@ -66,19 +71,10 @@ func getRange(ranges twoRangesType, selector rangeSelector, user int) rangeType return rangeType{-1, -1} } -type styleOperationType struct { - selector rangeSelector - op styleOpCode - key, value string - desc string -} - -type editOperationType struct { - selector rangeSelector - op editOpCode - content *json.TreeNode - splitLevel int - desc string +func makeTwoRanges(from1, mid1, to1 int, from2, mid2, to2 int, desc string) twoRangesType { + range0 := rangeWithMiddleType{from1, mid1, to1} + range1 := rangeWithMiddleType{from2, mid2, to2} + return twoRangesType{[2]rangeWithMiddleType{range0, range1}, desc} } type styleOpCode int @@ -95,79 +91,67 @@ const ( EditUpdate ) -func makeTwoRanges(from1, mid1, to1 int, from2, mid2, to2 int, desc string) twoRangesType { - range0 := rangeWithMiddleType{from1, mid1, to1} - range1 := rangeWithMiddleType{from2, mid2, to2} - return twoRangesType{[2]rangeWithMiddleType{range0, range1}, desc} +type operationInterface interface { + run(t *testing.T, doc *document.Document, user int, ranges twoRangesType) + getDesc() string } -var rangesToTestSameTypeOperation = []twoRangesType{ - makeTwoRanges(3, -1, 6, 3, -1, 6, `equal`), // (3, 6) - (3, 6) - makeTwoRanges(0, -1, 9, 3, -1, 6, `contain`), // (0, 9) - (3, 6) - makeTwoRanges(0, -1, 6, 3, -1, 9, `intersect`), // (0, 6) - (3, 9) - makeTwoRanges(0, -1, 3, 3, -1, 6, `side-by-side`), // (0, 3) - (3, 6) +type styleOperationType struct { + selector rangeSelector + op styleOpCode + key, value string + desc string } -var rangesToTestMixedTypeOperation = []twoRangesType{ - makeTwoRanges(3, 3, 6, 3, -1, 6, `equal`), // (3, 6) - (3, 6) - makeTwoRanges(0, 3, 9, 3, -1, 6, `A contains B`), // (0, 9) - (3, 6) - makeTwoRanges(3, 3, 6, 0, -1, 9, `B contains A`), // (0, 9) - (3, 6) - makeTwoRanges(0, 3, 6, 3, -1, 9, `intersect`), // (0, 6) - (3, 9) - makeTwoRanges(0, 3, 3, 3, -1, 6, `A -> B`), // (0, 3) - (3, 6) - makeTwoRanges(3, 3, 6, 0, -1, 3, `B -> A`), // (0, 3) - (3, 6) +type editOperationType struct { + selector rangeSelector + op editOpCode + content *json.TreeNode + splitLevel int + desc string +} + +func (op styleOperationType) getDesc() string { + return op.desc +} + +func (op editOperationType) getDesc() string { + return op.desc } -func runStyleOperation(t *testing.T, doc *document.Document, user int, ranges twoRangesType, operation styleOperationType) { - interval := getRange(ranges, operation.selector, user) +func (op styleOperationType) run(t *testing.T, doc *document.Document, user int, ranges twoRangesType) { + interval := getRange(ranges, op.selector, user) from, to := interval.from, interval.to assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { - if operation.op == StyleRemove { - root.GetTree("t").RemoveStyle(from, to, []string{operation.key}) - } else if operation.op == StyleSet { - root.GetTree("t").Style(from, to, map[string]string{operation.key: operation.value}) + if op.op == StyleRemove { + root.GetTree("t").RemoveStyle(from, to, []string{op.key}) + } else if op.op == StyleSet { + root.GetTree("t").Style(from, to, map[string]string{op.key: op.value}) } return nil })) } -func runEditOperation(t *testing.T, doc *document.Document, user int, ranges twoRangesType, operation editOperationType) { - interval := getRange(ranges, operation.selector, user) +func (op editOperationType) run(t *testing.T, doc *document.Document, user int, ranges twoRangesType) { + interval := getRange(ranges, op.selector, user) 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, operation.content, operation.splitLevel) + root.GetTree("t").Edit(from, to, op.content, op.splitLevel) return nil })) } -func TestTreeConcurrencyStyle(t *testing.T) { - // 0 1 2 3 4 5 6 7 8 9 - //

a

b

c

- // 0,3 : |----------| - // 3,6 : |----------| - // 6,9 : |----------| - - 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"}}}, - }, - } - initialXML := `

a

b

c

` - - styleOperationsToTest := []styleOperationType{ - {RangeAll, StyleRemove, "bold", "", `remove-bold`}, - {RangeAll, StyleSet, "bold", "aa", `set-bold-aa`}, - {RangeAll, StyleSet, "bold", "bb", `set-bold-bb`}, - {RangeAll, StyleRemove, "italic", "", `remove-italic`}, - {RangeAll, StyleSet, "italic", "aa", `set-italic-aa`}, - {RangeAll, StyleSet, "italic", "bb", `set-italic-bb`}, - } +// testDesc: description of test set +// initialState, initialXML: initial state of document +// rangeArr: ranges to perform operation +// opArr1: operations to perform by first user +// opArr2: operations to perform by second user +func RunTestTreeConcurrency(testDesc string, t *testing.T, initialState json.TreeNode, initialXML string, + rangesArr []twoRangesType, opArr1, opArr2 []operationInterface) { - runStyleTest := func(ranges twoRangesType, op1, op2 styleOperationType) { + runTest := func(ranges twoRangesType, op1, op2 operationInterface) testResult { clients := activeClients(t, 2) c1, c2 := clients[0], clients[1] defer deactivateAndCloseClients(t, clients) @@ -187,24 +171,100 @@ func TestTreeConcurrencyStyle(t *testing.T) { assert.Equal(t, initialXML, d1.Root().GetTree("t").ToXML()) assert.Equal(t, initialXML, d2.Root().GetTree("t").ToXML()) - runStyleOperation(t, d1, 0, ranges, op1) - runStyleOperation(t, d2, 1, ranges, op2) - syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}}) + op1.run(t, d1, 0, ranges) + op2.run(t, d2, 1, ranges) + + flag := syncClientsThenCheckEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}}) + if flag { + return testResult{flag, `pass`} + } + return testResult{flag, `different result`} } - for _, interval := range rangesToTestSameTypeOperation { - for _, op1 := range styleOperationsToTest { - for _, op2 := range styleOperationsToTest { - desc := "concurrently style-style test " + interval.desc + "(" + op1.desc + " " + op2.desc + ")" + for _, interval := range rangesArr { + for _, op1 := range opArr1 { + for _, op2 := range opArr2 { + desc := testDesc + "-" + interval.desc + desc += "(" + op1.getDesc() + "," + op2.getDesc() + ")" t.Run(desc, func(t *testing.T) { - runStyleTest(interval, op1, op2) + result := runTest(interval, op1, op2) + if !result.flag { + t.Skip(result.resultDesc) + } }) } } } } -func TestTreeConcurrencyEditAndStyle(t *testing.T) { +func TestTreeConcurrencyEditEdit(t *testing.T) { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + //

a b c

d e f

g h i

+ + initialState := json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "abc"}}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "def"}}}, + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ghi"}}}, + }, + } + initialXML := `

abc

def

ghi

` + + textNode1 := &json.TreeNode{Type: "text", Value: "A"} + textNode2 := &json.TreeNode{Type: "text", Value: "B"} + elementNode1 := &json.TreeNode{Type: "b", Children: []json.TreeNode{}} + elementNode2 := &json.TreeNode{Type: "i", Children: []json.TreeNode{}} + + ranges := []twoRangesType{ + // 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`), + } + + editOperations1 := []operationInterface{ + 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`}, + } + + editOperations2 := []operationInterface{ + 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`}, + } + + RunTestTreeConcurrency("concurrently-edit-edit-test", t, initialState, initialXML, ranges, editOperations1, editOperations2) +} + +func TestTreeConcurrencyStyleStyle(t *testing.T) { // 0 1 2 3 4 5 6 7 8 9 //

a

b

c

// 0,3 : |----------| @@ -221,58 +281,75 @@ func TestTreeConcurrencyEditAndStyle(t *testing.T) { } initialXML := `

a

b

c

` - content := &json.TreeNode{Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: `d`}}} - - editOperationsToTest := []editOperationType{ - {RangeFront, EditUpdate, content, 0, `insertFront`}, - {RangeMiddle, EditUpdate, content, 0, `insertMiddle`}, - {RangeBack, EditUpdate, content, 0, `insertBack`}, - {RangeAll, EditUpdate, nil, 0, `erase`}, - {RangeAll, EditUpdate, content, 0, `change`}, + ranges := []twoRangesType{ + // 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`), } - styleOperationsToTest := []styleOperationType{ - {RangeAll, StyleRemove, "bold", "", `remove-bold`}, - {RangeAll, StyleSet, "bold", "aa", `set-bold-aa`}, + styleOperations := []operationInterface{ + 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`}, } - runEditStyleTest := func(ranges twoRangesType, op1 editOperationType, op2 styleOperationType) bool { - clients := activeClients(t, 2) - c1, c2 := clients[0], clients[1] - defer deactivateAndCloseClients(t, clients) + RunTestTreeConcurrency("concurrently-style-style-test", t, initialState, initialXML, ranges, styleOperations, styleOperations) +} - ctx := context.Background() - d1 := document.New(helper.TestDocKey(t)) - assert.NoError(t, c1.Attach(ctx, d1)) - d2 := document.New(helper.TestDocKey(t)) - assert.NoError(t, c2.Attach(ctx, d2)) +func TestTreeConcurrencyEditStyle(t *testing.T) { + // 0 1 2 3 4 5 6 7 8 9 + //

a

b

c

+ // 0,3 : |----------| + // 3,6 : |----------| + // 6,9 : |----------| - assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error { - root.SetNewTree("t", &initialState) - return nil - })) - assert.NoError(t, c1.Sync(ctx)) - assert.NoError(t, c2.Sync(ctx)) - assert.Equal(t, initialXML, d1.Root().GetTree("t").ToXML()) - assert.Equal(t, initialXML, d2.Root().GetTree("t").ToXML()) + 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"}}}, + }, + } + initialXML := `

a

b

c

` + + content := &json.TreeNode{Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: `d`}}} - runEditOperation(t, d1, 0, ranges, op1) - runStyleOperation(t, d2, 1, ranges, op2) + ranges := []twoRangesType{ + // equal:

b

-

b

+ makeTwoRanges(3, 3, 6, 3, -1, 6, `equal`), + // 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`), + } - return syncClientsThenCheckEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}}) + editOperations := []operationInterface{ + 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`}, } - for _, interval := range rangesToTestMixedTypeOperation { - for _, op1 := range editOperationsToTest { - for _, op2 := range styleOperationsToTest { - desc := "concurrently edit-style test-" + interval.desc + "(" - desc += op1.desc + "," + op2.desc + ")" - t.Run(desc, func(t *testing.T) { - if !runEditStyleTest(interval, op1, op2) { - t.Skip() - } - }) - } - } + styleOperations := []operationInterface{ + styleOperationType{RangeAll, StyleRemove, "bold", "", `remove-bold`}, + styleOperationType{RangeAll, StyleSet, "bold", "aa", `set-bold-aa`}, } + + RunTestTreeConcurrency("concurrently-edit-style-test", t, initialState, initialXML, ranges, editOperations, styleOperations) }