2022-08-31 13:43:09 +01:00
|
|
|
package sync3
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-04-05 14:11:05 +01:00
|
|
|
"context"
|
2022-08-31 13:43:09 +01:00
|
|
|
"encoding/json"
|
2022-09-05 13:43:12 +01:00
|
|
|
"math/rand"
|
2022-08-31 13:43:09 +01:00
|
|
|
"testing"
|
2022-09-05 13:43:12 +01:00
|
|
|
|
2022-12-15 11:08:50 +00:00
|
|
|
"github.com/matrix-org/sliding-sync/testutils"
|
2022-08-31 13:43:09 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestCalculateListOps_BasicOperations(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
before []string
|
|
|
|
after []string
|
|
|
|
ranges SliceRanges
|
|
|
|
roomID string
|
|
|
|
listOp ListOp
|
|
|
|
wantOps []ResponseOp
|
|
|
|
wantSubs []string
|
|
|
|
}{
|
2022-08-31 17:54:07 +01:00
|
|
|
{
|
|
|
|
name: "basic addition from nothing",
|
|
|
|
before: []string{},
|
|
|
|
after: []string{"a"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(0)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(0), RoomID: "a"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"a"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion to nothing",
|
|
|
|
before: []string{"a"},
|
|
|
|
after: []string{},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(0)},
|
|
|
|
},
|
|
|
|
},
|
2022-08-31 13:43:09 +01:00
|
|
|
{
|
|
|
|
name: "basic move from bottom to top of list",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"d", "a", "b", "c"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "d",
|
|
|
|
listOp: ListOpChange, // e.g new message
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(0), RoomID: "d"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic move from top to bottom of list",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"b", "c", "d", "a"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpChange, // e.g new message
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(0)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "a"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "no-op move",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"a", "b", "c", "d"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "b",
|
|
|
|
listOp: ListOpChange, // e.g new message
|
|
|
|
wantOps: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic addition to middle of list",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"a", "b", "bc", "c", "d"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "bc",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(4)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(2), RoomID: "bc"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"bc"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion from middle of list",
|
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`.
2022-08-31 18:45:22 +01:00
|
|
|
before: []string{"a", "b", "c", "d", "e"},
|
|
|
|
after: []string{"a", "c", "d", "e"},
|
2022-08-31 13:43:09 +01:00
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "b",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion from end of list",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"a", "b", "c"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "d",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2022-08-31 17:54:07 +01:00
|
|
|
name: "basic addition to end of list",
|
2022-08-31 13:43:09 +01:00
|
|
|
before: []string{"a", "b", "c"},
|
|
|
|
after: []string{"a", "b", "c", "d"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "d",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
2022-08-31 17:54:07 +01:00
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
2022-08-31 13:43:09 +01:00
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "d"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"d"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "one element swap",
|
|
|
|
before: []string{"a", "b", "c", "d"},
|
|
|
|
after: []string{"b", "a", "c", "d"},
|
|
|
|
ranges: SliceRanges{{0, 20}},
|
|
|
|
roomID: "b",
|
|
|
|
listOp: ListOpChange, // e.g new message
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(0), RoomID: "b"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
|
|
sl := newStringList(tc.before)
|
|
|
|
sl.sortedRoomIDs = tc.after
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
2022-08-31 13:43:09 +01:00
|
|
|
Ranges: tc.ranges,
|
|
|
|
}, sl, tc.roomID, tc.listOp)
|
|
|
|
assertEqualOps(t, tc.name, gotOps, tc.wantOps)
|
|
|
|
assertEqualSlices(t, tc.name, gotSubs, tc.wantSubs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCalculateListOps_SingleWindowOperations(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
before []string
|
|
|
|
after []string
|
|
|
|
ranges SliceRanges
|
|
|
|
roomID string
|
|
|
|
listOp ListOp
|
|
|
|
wantOps []ResponseOp
|
|
|
|
wantSubs []string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "basic move into a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"f", "a", "b", "c", "d", "e"},
|
|
|
|
ranges: SliceRanges{{0, 2}},
|
|
|
|
roomID: "f",
|
|
|
|
listOp: ListOpChange,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(2)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(0), RoomID: "f"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"f"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic move out of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"b", "c", "d", "e", "a", "f"},
|
|
|
|
ranges: SliceRanges{{0, 2}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpChange,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(0)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(2), RoomID: "d"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"d"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic move over a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"b", "c", "d", "e", "a", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpChange,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"e"},
|
2022-08-31 17:54:07 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion in front of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"b", "c", "d", "e", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "a",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"e"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion at start of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "c", "d", "e", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "b",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(1)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"e"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion at end of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "b", "c", "e", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "d",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "e"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"e"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic deletion in behind a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "b", "c", "d", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "e",
|
|
|
|
listOp: ListOpDel,
|
|
|
|
wantOps: nil,
|
|
|
|
wantSubs: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic addition in front of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "ab", "b", "c", "d", "e", "f"},
|
|
|
|
ranges: SliceRanges{{2, 4}},
|
|
|
|
roomID: "ab",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(4)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(2), RoomID: "b"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"b"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic addition at start of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "ab", "b", "c", "d", "e", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "ab",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "ab"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"ab"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic addition at end of a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "b", "c", "cd", "d", "e", "f"},
|
|
|
|
ranges: SliceRanges{{1, 3}},
|
|
|
|
roomID: "cd",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(3), RoomID: "cd"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"cd"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "basic addition behind a window",
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f"},
|
|
|
|
after: []string{"a", "b", "c", "d", "e", "f", "g"},
|
|
|
|
ranges: SliceRanges{{2, 4}},
|
|
|
|
roomID: "g",
|
|
|
|
listOp: ListOpAdd,
|
|
|
|
wantOps: nil,
|
|
|
|
wantSubs: nil,
|
|
|
|
},
|
2022-08-31 13:43:09 +01:00
|
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
|
|
sl := newStringList(tc.before)
|
|
|
|
sl.sortedRoomIDs = tc.after
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
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`.
2022-08-31 18:45:22 +01:00
|
|
|
Ranges: tc.ranges,
|
|
|
|
}, sl, tc.roomID, tc.listOp)
|
|
|
|
assertEqualOps(t, tc.name, gotOps, tc.wantOps)
|
|
|
|
assertEqualSlices(t, tc.name, gotSubs, tc.wantSubs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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"},
|
|
|
|
},
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
{
|
|
|
|
name: "jump from lower window edge to upper window edge",
|
|
|
|
// 0 1 2 3 4 5 6 7 8
|
|
|
|
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
|
|
|
|
after: []string{"a", "f", "b", "c", "d", "e", "g", "h", "i"},
|
|
|
|
ranges: SliceRanges{{1, 3}, {5, 7}},
|
|
|
|
roomID: "f",
|
|
|
|
listOp: ListOpChange,
|
|
|
|
wantOps: []ResponseOp{
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(5)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(5), RoomID: "e"},
|
|
|
|
&ResponseOpSingle{Operation: OpDelete, Index: ptr(3)},
|
|
|
|
&ResponseOpSingle{Operation: OpInsert, Index: ptr(1), RoomID: "f"},
|
|
|
|
},
|
|
|
|
wantSubs: []string{"e"},
|
|
|
|
},
|
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`.
2022-08-31 18:45:22 +01:00
|
|
|
{
|
|
|
|
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
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
2022-08-31 13:43:09 +01:00
|
|
|
Ranges: tc.ranges,
|
|
|
|
}, sl, tc.roomID, tc.listOp)
|
|
|
|
assertEqualOps(t, tc.name, gotOps, tc.wantOps)
|
|
|
|
assertEqualSlices(t, tc.name, gotSubs, tc.wantSubs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-05 13:43:12 +01:00
|
|
|
func TestCalculateListOps_TortureSingleWindow_Move(t *testing.T) {
|
|
|
|
rand.Seed(42)
|
|
|
|
ranges := SliceRanges{{0, 5}}
|
|
|
|
inRangeIDs := map[string]bool{
|
|
|
|
"a": true, "b": true, "c": true, "d": true, "e": true, "f": true,
|
|
|
|
}
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
|
|
before := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}
|
|
|
|
after, roomID, fromIndex, toIndex := testutils.MoveRandomElement(before)
|
|
|
|
t.Logf("move %s from %v to %v -> %v", roomID, fromIndex, toIndex, after)
|
|
|
|
|
|
|
|
sl := newStringList(before)
|
|
|
|
sl.sortedRoomIDs = after
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
2022-09-05 13:43:12 +01:00
|
|
|
Ranges: ranges,
|
|
|
|
}, sl, roomID, ListOpChange)
|
|
|
|
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if inRangeIDs[sub] {
|
|
|
|
t.Errorf("CalculateListOps: got sub %v but it was already in the range", sub)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if int64(fromIndex) > ranges[0][1] && int64(toIndex) > ranges[0][1] {
|
|
|
|
// the swap happens outside the range, so we expect nothing
|
|
|
|
if len(gotOps) != 0 {
|
|
|
|
t.Errorf("CalculateLisOps: swap outside range got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if fromIndex == toIndex {
|
|
|
|
// we want 0 ops
|
|
|
|
if len(gotOps) > 0 {
|
|
|
|
t.Errorf("CalculateLisOps: from==to got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(gotOps) != 2 {
|
|
|
|
t.Fatalf("CalculateLisOps: wanted 2 ops, got %+v", gotOps)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if int64(fromIndex) > ranges[0][1] {
|
|
|
|
// should inject a different room at the start of the range
|
|
|
|
fromIndex = int(ranges[0][1])
|
|
|
|
}
|
|
|
|
assertSingleOp(t, gotOps[0], "DELETE", fromIndex, "")
|
|
|
|
if int64(toIndex) > ranges[0][1] {
|
|
|
|
// should inject a different room at the end of the range
|
|
|
|
toIndex = int(ranges[0][1])
|
|
|
|
roomID = after[toIndex]
|
|
|
|
}
|
2022-09-05 13:59:14 +01:00
|
|
|
if !inRangeIDs[roomID] {
|
|
|
|
// it should now be in-range
|
|
|
|
inRange := false
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if sub == roomID {
|
|
|
|
inRange = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !inRange {
|
|
|
|
t.Errorf("got subs %v missing room %v", gotSubs, roomID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
assertSingleOp(t, gotOps[1], "INSERT", toIndex, roomID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCalculateListOps_TortureSingleWindowMiddle_Move(t *testing.T) {
|
|
|
|
rand.Seed(41)
|
|
|
|
ranges := SliceRanges{{2, 5}}
|
|
|
|
inRangeIDs := map[string]bool{
|
|
|
|
"c": true, "d": true, "e": true, "f": true,
|
|
|
|
}
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
|
|
before := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}
|
|
|
|
after, roomID, fromIndex, toIndex := testutils.MoveRandomElement(before)
|
|
|
|
t.Logf("move %s from %v to %v -> %v", roomID, fromIndex, toIndex, after)
|
|
|
|
|
|
|
|
sl := newStringList(before)
|
|
|
|
sl.sortedRoomIDs = after
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
2022-09-05 13:59:14 +01:00
|
|
|
Ranges: ranges,
|
|
|
|
}, sl, roomID, ListOpChange)
|
|
|
|
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if inRangeIDs[sub] {
|
|
|
|
t.Errorf("CalculateListOps: got sub %v but it was already in the range", sub)
|
|
|
|
}
|
|
|
|
}
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
|
2022-09-05 13:59:14 +01:00
|
|
|
if (int64(fromIndex) > ranges[0][1] && int64(toIndex) > ranges[0][1]) || (int64(fromIndex) < ranges[0][0] && int64(toIndex) < ranges[0][0]) {
|
|
|
|
// the swap happens outside the range, so we expect nothing
|
|
|
|
if len(gotOps) != 0 {
|
|
|
|
t.Errorf("CalculateLisOps: swap outside range got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if fromIndex == toIndex {
|
|
|
|
// we want 0 ops
|
|
|
|
if len(gotOps) > 0 {
|
|
|
|
t.Errorf("CalculateLisOps: from==to got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(gotOps) != 2 {
|
|
|
|
t.Fatalf("CalculateListOps: wanted 2 ops, got %+v", gotOps)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if int64(fromIndex) > ranges[0][1] {
|
|
|
|
// should inject a different room at the start of the range
|
|
|
|
fromIndex = int(ranges[0][1])
|
|
|
|
} else if int64(fromIndex) < ranges[0][0] {
|
|
|
|
// should inject a different room at the start of the range
|
|
|
|
fromIndex = int(ranges[0][0])
|
|
|
|
}
|
|
|
|
assertSingleOp(t, gotOps[0], "DELETE", fromIndex, "")
|
|
|
|
if int64(toIndex) > ranges[0][1] {
|
|
|
|
// should inject a different room at the end of the range
|
|
|
|
toIndex = int(ranges[0][1])
|
|
|
|
roomID = after[toIndex]
|
|
|
|
} else if int64(toIndex) < ranges[0][0] {
|
|
|
|
// should inject a different room at the end of the range
|
|
|
|
toIndex = int(ranges[0][0])
|
|
|
|
roomID = after[toIndex]
|
|
|
|
}
|
|
|
|
if !inRangeIDs[roomID] {
|
|
|
|
// it should now be in-range
|
|
|
|
inRange := false
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if sub == roomID {
|
|
|
|
inRange = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !inRange {
|
|
|
|
t.Errorf("got subs %v missing room %v", gotSubs, roomID)
|
|
|
|
}
|
|
|
|
}
|
2022-09-05 13:43:12 +01:00
|
|
|
assertSingleOp(t, gotOps[1], "INSERT", toIndex, roomID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
func TestCalculateListOpsTortureMultipleWindowsMove(t *testing.T) {
|
|
|
|
rand.Seed(40)
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
ranges := SliceRanges{{0, 5}, {9, 11}}
|
|
|
|
inRangeIDs := map[string]bool{
|
|
|
|
"a": true, "b": true, "c": true, "d": true, "e": true, "f": true, "j": true, "k": true, "l": true,
|
|
|
|
}
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
|
|
before := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n"}
|
|
|
|
after, roomID, fromIndex, toIndex := testutils.MoveRandomElement(before)
|
|
|
|
t.Logf("move %s from %v to %v -> %v", roomID, fromIndex, toIndex, after)
|
|
|
|
|
|
|
|
sl := newStringList(before)
|
|
|
|
sl.sortedRoomIDs = after
|
2023-04-05 14:11:05 +01:00
|
|
|
gotOps, gotSubs := CalculateListOps(context.Background(), &RequestList{
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
Ranges: ranges,
|
|
|
|
}, sl, roomID, ListOpChange)
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if inRangeIDs[sub] {
|
|
|
|
t.Fatalf("CalculateListOps: got sub %v but it was already in the range", sub)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if there is a swap between the middle of the window OR at the end of the window, we expect no ops
|
|
|
|
isSwapInBetweenWindows := (fromIndex > int(ranges[0][1]) && fromIndex < int(ranges[1][0]) &&
|
|
|
|
toIndex > int(ranges[0][1]) && toIndex < int(ranges[1][0]))
|
|
|
|
isSwapEndOfWindow := (fromIndex > int(ranges[1][1])) && (toIndex > int(ranges[1][1]))
|
|
|
|
if isSwapInBetweenWindows || isSwapEndOfWindow {
|
|
|
|
// the swap happens outside the range, so we expect nothing
|
|
|
|
if len(gotOps) != 0 {
|
|
|
|
t.Errorf("CalculateLisOps: swap outside range got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if fromIndex == toIndex {
|
|
|
|
// we want 0 ops
|
|
|
|
if len(gotOps) > 0 {
|
|
|
|
t.Errorf("CalculateLisOps: from==to got ops wanted none: %+v", gotOps)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// if the swap occurs over multiple windows we expect 4 ops, not 2
|
|
|
|
insideFirstWindow := fromIndex <= int(ranges[0][1]) || toIndex <= int(ranges[0][1])
|
|
|
|
insideSecondWindow := (fromIndex >= int(ranges[1][0])) || (toIndex >= int(ranges[1][0]))
|
|
|
|
wantOps := 2
|
|
|
|
if insideFirstWindow && insideSecondWindow {
|
|
|
|
wantOps = 4
|
|
|
|
}
|
|
|
|
if len(gotOps) != wantOps {
|
|
|
|
t.Fatalf("CalculateListOps: wanted %v ops, got %+v", wantOps, gotOps)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
if wantOps == 4 {
|
|
|
|
continue // TODO: find a way to check multi DELETE/INSERT
|
|
|
|
}
|
|
|
|
rng := ranges[0]
|
|
|
|
if insideSecondWindow {
|
|
|
|
rng = ranges[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
if int64(fromIndex) > rng[1] {
|
|
|
|
// should inject a different room at the start of the range
|
|
|
|
fromIndex = int(rng[1])
|
|
|
|
} else if int64(fromIndex) < rng[0] {
|
|
|
|
// should inject a different room at the start of the range
|
|
|
|
fromIndex = int(rng[0])
|
|
|
|
}
|
|
|
|
assertSingleOp(t, gotOps[0], "DELETE", fromIndex, "")
|
|
|
|
|
|
|
|
if int64(toIndex) > rng[1] {
|
|
|
|
// should inject a different room at the end of the range
|
|
|
|
toIndex = int(rng[1])
|
|
|
|
roomID = after[toIndex]
|
|
|
|
} else if int64(toIndex) < rng[0] {
|
|
|
|
// should inject a different room at the end of the range
|
|
|
|
toIndex = int(rng[0])
|
|
|
|
roomID = after[toIndex]
|
|
|
|
}
|
|
|
|
if !inRangeIDs[roomID] {
|
|
|
|
// it should now be in-range
|
|
|
|
inRange := false
|
|
|
|
for _, sub := range gotSubs {
|
|
|
|
if sub == roomID {
|
|
|
|
inRange = true
|
|
|
|
break
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
}
|
|
|
|
}
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
if !inRange {
|
|
|
|
t.Errorf("got subs %v missing room %v", gotSubs, roomID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
assertSingleOp(t, gotOps[1], "INSERT", toIndex, roomID)
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
}
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
}
|
bugfix: honour the windows that the ranges represent to avoid mismatched DELETE/INSERT operations
Previously, we would just focus on finding _any_ window boundary and then
assume that was the boundary which matched the window for the purposes of
DELETE/INSERT move operations. However, this wasn't always true, especially
in the following case:
```
0..9 [10..20] 21...29 [30...40]
then move 30 to 10
0..9 [30,10...19] 20...28 [29,31...40]
expect:
- DELETE 30, INSERT 30 (val=29)
- DELETE 20, INSERT 10 (val=30)
but we would get:
- DELETE 30, INSERT 20 (val=19)
- DELETE 20, INSERT 10 (val=30)
because the code assumed that there was a window range [20,30] which there wasn't.
```
2022-09-05 15:58:56 +01:00
|
|
|
|
2022-09-05 13:43:12 +01:00
|
|
|
func assertSingleOp(t *testing.T, op ResponseOp, opName string, index int, optRoomID string) {
|
|
|
|
t.Helper()
|
|
|
|
singleOp, ok := op.(*ResponseOpSingle)
|
|
|
|
if !ok {
|
|
|
|
t.Errorf("op was not a single operation, got %+v", op)
|
|
|
|
}
|
|
|
|
if singleOp.Operation != opName {
|
|
|
|
t.Errorf("op name got %v want %v", singleOp.Operation, opName)
|
|
|
|
}
|
|
|
|
if *singleOp.Index != index {
|
|
|
|
t.Errorf("op index got %v want %v", *singleOp.Index, index)
|
|
|
|
}
|
|
|
|
if optRoomID != "" && singleOp.RoomID != optRoomID {
|
|
|
|
t.Errorf("op room ID got %v want %v", singleOp.RoomID, optRoomID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-31 13:43:09 +01:00
|
|
|
func assertEqualOps(t *testing.T, name string, gotOps, wantOps []ResponseOp) {
|
|
|
|
t.Helper()
|
|
|
|
got, err := json.Marshal(gotOps)
|
|
|
|
want, err2 := json.Marshal(wantOps)
|
|
|
|
if err != nil || err2 != nil {
|
|
|
|
t.Fatalf("%s: failed to marshal response ops: %v %v", name, err, err2)
|
|
|
|
}
|
|
|
|
if !bytes.Equal(got, want) {
|
|
|
|
t.Fatalf("%s: assertEqualOps, got != want\n%s\n%s", name, string(got), string(want))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type stringList struct {
|
|
|
|
roomIDs []string
|
|
|
|
roomIDToIndex map[string]int
|
|
|
|
sortedRoomIDs []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newStringList(roomIDs []string) *stringList {
|
|
|
|
s := &stringList{
|
|
|
|
sortedRoomIDs: roomIDs,
|
|
|
|
}
|
|
|
|
s.Sort(nil)
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func ptr(i int) *int {
|
|
|
|
return &i
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *stringList) IndexOf(roomID string) (int, bool) {
|
|
|
|
i, ok := s.roomIDToIndex[roomID]
|
|
|
|
return i, ok
|
|
|
|
}
|
|
|
|
func (s *stringList) Len() int64 {
|
|
|
|
return int64(len(s.roomIDs))
|
|
|
|
}
|
|
|
|
func (s *stringList) Sort(sortBy []string) error {
|
|
|
|
s.roomIDToIndex = make(map[string]int, len(s.sortedRoomIDs))
|
|
|
|
s.roomIDs = s.sortedRoomIDs
|
|
|
|
for i := range s.roomIDs {
|
|
|
|
s.roomIDToIndex[s.roomIDs[i]] = i
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (s *stringList) Add(roomID string) bool {
|
|
|
|
_, ok := s.roomIDToIndex[roomID]
|
|
|
|
if ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
s.roomIDToIndex[roomID] = len(s.roomIDs)
|
|
|
|
s.roomIDs = append(s.roomIDs, roomID)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
func (s *stringList) Remove(roomID string) int {
|
|
|
|
i, ok := s.roomIDToIndex[roomID]
|
|
|
|
if !ok {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
gotRoomID := s.roomIDs[i]
|
|
|
|
if gotRoomID != roomID {
|
|
|
|
panic("roomIDToIndex|roomIDs out of sync, got " + gotRoomID + " want " + roomID)
|
|
|
|
}
|
|
|
|
delete(s.roomIDToIndex, roomID)
|
|
|
|
// splice out index
|
|
|
|
s.roomIDs = append(s.roomIDs[:i], s.roomIDs[i+1:]...)
|
|
|
|
// re-update the map
|
|
|
|
for index := i; index < len(s.roomIDs); index++ {
|
|
|
|
s.roomIDToIndex[s.roomIDs[index]] = index
|
|
|
|
}
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
func (s *stringList) Get(index int) string {
|
|
|
|
return s.roomIDs[index]
|
|
|
|
}
|