bugfix: ensure we always INSERT a shifted room when handling a deletion

Previously, this would fail:
```
			//                0    1    2    3    4    5    6    7    8
			before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
			after:  []string{"b", "c", "d", "e", "f", "g", "h", "i"},
			ranges: SliceRanges{{1, 3}, {5, 7}},
```

because the 2nd window range perfectly matched the list size, it would
ignore the `INSERT,7,i`.
This commit is contained in:
Kegan Dougal 2022-08-31 18:45:22 +01:00
parent dcad80f51f
commit cff1be0f1e
2 changed files with 196 additions and 4 deletions

View File

@ -81,8 +81,16 @@ func CalculateListOps(reqList *RequestList, list List, roomID string, listOp Lis
if wasUpdatedRoomInserted && listOp == ListOpDel {
// ignore this insert, as deletions algorithmically just jump to the end of the array.
// we do this so we can calculate jumps over ranges using the same codepaths as moves.
ops = append(ops, reqList.WriteDeleteOp(listFromIndex))
continue
isInsertingDeletedRoom := toRoomID == roomID
// The algorithm needs to know if it should issue an INSERT for the `to` room. The whole
// "treat deletes as inserts at the end of the array" thing makes it hard to know if it
// should or should not. This check ensures that we only skip the INSERT if the to-be shifted
// room was not already sent to the client.
_, isShiftedRoomAlreadySent := reqList.Ranges.Inside(list.Len())
if isInsertingDeletedRoom || (listToIndex == int(list.Len()-1) && isShiftedRoomAlreadySent) {
ops = append(ops, reqList.WriteDeleteOp(listFromIndex))
continue
}
}
swapOp := reqList.WriteSwapOp(toRoomID, listFromIndex, listToIndex)

View File

@ -89,8 +89,8 @@ func TestCalculateListOps_BasicOperations(t *testing.T) {
},
{
name: "basic deletion from middle of list",
before: []string{"a", "b", "c", "d"},
after: []string{"a", "c", "d"},
before: []string{"a", "b", "c", "d", "e"},
after: []string{"a", "c", "d", "e"},
ranges: SliceRanges{{0, 20}},
roomID: "b",
listOp: ListOpDel,
@ -306,6 +306,190 @@ func TestCalculateListOps_SingleWindowOperations(t *testing.T) {
}
}
func TestCalculateListOps_MultipleWindowOperations(t *testing.T) {
testCases := []struct {
name string
before []string
after []string
ranges SliceRanges
roomID string
listOp ListOp
wantOps []ResponseOp
wantSubs []string
}{
{
name: "move within lower window",
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"a", "c", "b", "d", "e", "f", "g", "h", "i"},
ranges: SliceRanges{{0, 2}, {5, 7}},
roomID: "c",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(2)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "c"},
},
},
{
name: "move within upper window",
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"a", "b", "c", "d", "e", "f", "h", "g", "i"},
ranges: SliceRanges{{0, 2}, {5, 7}},
roomID: "h",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(7)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(6), RoomID: "h"},
},
},
{
name: "move in the gap between windows",
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"a", "b", "c", "e", "d", "f", "g", "h", "i"},
ranges: SliceRanges{{0, 2}, {5, 7}},
roomID: "e",
listOp: ListOpChange,
wantOps: nil,
},
{
name: "move from upper window to lower window",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"a", "g", "b", "c", "d", "e", "f", "h", "i"},
ranges: SliceRanges{{0, 2}, {5, 7}},
roomID: "g",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(6)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(5), RoomID: "e"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(2)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "g"},
},
wantSubs: []string{"e"},
},
{
name: "move from lower window to upper window",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"a", "c", "d", "e", "f", "g", "b", "h", "i"},
ranges: SliceRanges{{0, 2}, {5, 7}},
roomID: "b",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(2), RoomID: "d"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(5)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(6), RoomID: "b"},
},
wantSubs: []string{"d"},
},
{
name: "jump over multiple windows to lower",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"i", "a", "b", "c", "d", "e", "f", "g", "h"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "i",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "a"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(7)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(5), RoomID: "e"},
},
wantSubs: []string{"a", "e"},
},
{
name: "jump over multiple windows to upper",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"b", "c", "d", "e", "f", "g", "h", "i", "a"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "a",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(5)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(7), RoomID: "i"},
},
wantSubs: []string{"i", "e"},
},
{
name: "jump from upper window over lower window",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"g", "a", "b", "c", "d", "e", "f", "h", "i"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "g",
listOp: ListOpChange,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(6)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(5), RoomID: "e"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "a"},
},
wantSubs: []string{"a", "e"},
},
{
name: "addition moving multiple windows",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"_", "a", "b", "c", "d", "e", "f", "g", "h", "i"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "_",
listOp: ListOpAdd,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "a"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(7)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(5), RoomID: "e"},
},
wantSubs: []string{"a", "e"},
},
{
name: "deletion moving multiple windows",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
after: []string{"b", "c", "d", "e", "f", "g", "h", "i", "j"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "a",
listOp: ListOpDel,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(5)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(7), RoomID: "i"},
},
wantSubs: []string{"i", "e"},
},
{
name: "deletion moving multiple windows window matches end of list",
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"b", "c", "d", "e", "f", "g", "h", "i"},
ranges: SliceRanges{{1, 3}, {5, 7}},
roomID: "a",
listOp: ListOpDel,
wantOps: []ResponseOp{
&ResponseOpSingle{Operation: OpDelete, Index: ptr(5)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(7), RoomID: "i"},
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
},
wantSubs: []string{"i", "e"},
},
}
for _, tc := range testCases {
sl := newStringList(tc.before)
sl.sortedRoomIDs = tc.after
gotOps, gotSubs := CalculateListOps(&RequestList{
Ranges: tc.ranges,
}, sl, tc.roomID, tc.listOp)
assertEqualOps(t, tc.name, gotOps, tc.wantOps)
assertEqualSlices(t, tc.name, gotSubs, tc.wantSubs)
}
}
func assertEqualOps(t *testing.T, name string, gotOps, wantOps []ResponseOp) {
t.Helper()
got, err := json.Marshal(gotOps)