sliding-sync/sync3/range_test.go
Kegan Dougal b33e1d2b9f bugfixes: fix 2 issues which could cause invalid ranges to be returned
- When a range is shrinked from both ends, we would incorrectly send back
  INVALIDATE twice for the same range (the higher end). This was caused by
  improper usage of Go for .. range loops
- When a range is changed to a window completely outside the room list
  (e.g 5 rooms, range=[10,15]) we would incorrectly send back SYNC 10,15
  with a single room ID, the last room in the list. This was caused by
  boundary check code in SliceInto capping ranges to the last element if
  the index positions are > room list. However in this case, this causes
  the last room to be returned.

With regression tests (UT and E2E)
2023-01-03 13:39:46 +00:00

664 lines
13 KiB
Go

package sync3
import (
"reflect"
"testing"
)
type stringSlice []string
func (s stringSlice) Len() int64 {
return int64(len(s))
}
func (s stringSlice) Subslice(i, j int64) Subslicer {
return s[i:j]
}
func TestRangeValid(t *testing.T) {
testCases := []struct {
input SliceRanges
valid bool
}{
{
input: SliceRanges([][2]int64{
{0, 9},
}),
valid: true,
},
{
input: SliceRanges([][2]int64{
{9, 0},
}),
valid: false,
},
{
input: SliceRanges([][2]int64{
{9, 9},
}),
valid: true,
},
{
input: SliceRanges([][2]int64{
{-3, 3},
}),
valid: false,
},
{
input: SliceRanges([][2]int64{
{0, 20}, {20, 40}, // 20 overlaps
}),
valid: false,
},
{
input: SliceRanges([][2]int64{
{0, 20}, {40, 60}, {30, 35},
}),
valid: true,
},
{
input: SliceRanges([][2]int64{
{0, 20}, {40, 60}, {10, 15},
}),
valid: false,
},
{
input: SliceRanges([][2]int64{
{10, 15}, {40, 60}, {0, 20},
}),
valid: false,
},
}
for _, tc := range testCases {
gotValid := tc.input.Valid()
if gotValid != tc.valid {
t.Errorf("test case %+v returned valid=%v", tc, gotValid)
}
}
}
func TestRange(t *testing.T) {
alphabet := []string{
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
}
testCases := []struct {
input SliceRanges
want [][]string
}{
{
input: SliceRanges([][2]int64{
{0, 9},
}),
want: [][]string{
alphabet[0:10],
},
},
{
input: SliceRanges([][2]int64{
{0, 99},
}),
want: [][]string{
alphabet,
},
},
{
input: SliceRanges([][2]int64{
{0, 0}, {1, 1},
}),
want: [][]string{
alphabet[0:1], alphabet[1:2],
},
},
{
input: SliceRanges([][2]int64{
{30, 99},
}),
want: [][]string{},
},
}
for _, tc := range testCases {
result := tc.input.SliceInto(stringSlice(alphabet))
if len(result) != len(tc.want) {
t.Errorf("%+v subslice mismatch: got %v want %v \ngot: %+v\nwant: %+v\n", tc, len(result), len(tc.want), result, tc.want)
continue
}
for i := range result {
got := result[i].(stringSlice)
want := tc.want[i]
if !reflect.DeepEqual([]string(got), want) {
t.Errorf("%+v wrong subslice returned, got %v want %v", tc, got, want)
}
}
}
}
func TestRangeInside(t *testing.T) {
testCases := []struct {
testRange SliceRanges
i int64
inside bool
}{
{
testRange: SliceRanges([][2]int64{
{0, 9},
}),
i: 6,
inside: true,
},
{
testRange: SliceRanges([][2]int64{
{0, 9},
}),
i: 0,
inside: true,
},
{
testRange: SliceRanges([][2]int64{
{0, 9},
}),
i: 9,
inside: true,
},
{
testRange: SliceRanges([][2]int64{
{0, 9},
}),
i: -1,
inside: false,
},
{
testRange: SliceRanges([][2]int64{
{0, 9},
}),
i: 10,
inside: false,
},
{
testRange: SliceRanges([][2]int64{
{0, 9}, {10, 19},
}),
i: 10,
inside: true,
},
{
testRange: SliceRanges([][2]int64{
{0, 9}, {20, 29},
}),
i: 10,
inside: false,
},
}
for _, tc := range testCases {
_, gotInside := tc.testRange.Inside(tc.i)
if gotInside != tc.inside {
t.Errorf("%+v got Inside:%v want %v", tc, gotInside, tc.inside)
}
}
}
func TestRangeClosestInDirection(t *testing.T) {
testCases := []struct {
ranges SliceRanges
i int64
towardsZero bool
wantClosestIndex int64
}{
{
ranges: [][2]int64{{0, 20}},
i: 15,
towardsZero: true,
wantClosestIndex: 0,
},
{
ranges: [][2]int64{{0, 20}},
i: 0,
towardsZero: false,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{0, 20}},
i: 20,
towardsZero: true,
wantClosestIndex: 0,
},
{
ranges: [][2]int64{{0, 20}},
i: 0,
towardsZero: true,
wantClosestIndex: 0,
},
{
ranges: [][2]int64{{0, 20}},
i: 20,
towardsZero: false,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{0, 20}},
i: 15,
towardsZero: false,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{10, 20}},
i: 5,
towardsZero: true,
wantClosestIndex: -1,
},
{
ranges: [][2]int64{{10, 20}},
i: 5,
towardsZero: false,
wantClosestIndex: 10,
},
{
ranges: [][2]int64{{10, 20}},
i: 25,
towardsZero: false,
wantClosestIndex: -1,
},
{
ranges: [][2]int64{{10, 20}},
i: 25,
towardsZero: true,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{10, 20}, {30, 40}},
i: 25,
towardsZero: true,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{10, 20}, {30, 40}},
i: 25,
towardsZero: false,
wantClosestIndex: 30,
},
{
ranges: [][2]int64{{10, 20}, {30, 40}},
i: 35,
towardsZero: true,
wantClosestIndex: 30,
},
{
ranges: [][2]int64{{10, 20}, {30, 40}},
i: 35,
towardsZero: false,
wantClosestIndex: 40,
},
{
ranges: [][2]int64{{0, 0}},
i: 0,
towardsZero: true,
wantClosestIndex: 0,
},
{
ranges: [][2]int64{{0, 0}},
i: 0,
towardsZero: false,
wantClosestIndex: 0,
},
{
ranges: [][2]int64{{10, 10}},
i: 10,
towardsZero: false,
wantClosestIndex: 10,
},
{
ranges: [][2]int64{{10, 10}},
i: 10,
towardsZero: true,
wantClosestIndex: 10,
},
{
ranges: [][2]int64{{20, 40}},
i: 40,
towardsZero: true,
wantClosestIndex: 20,
},
{
ranges: [][2]int64{{0, 20}, {40, 60}},
i: 40,
towardsZero: true,
wantClosestIndex: 40,
},
{
ranges: [][2]int64{{0, 20}, {40, 60}},
i: 20,
towardsZero: false,
wantClosestIndex: 20,
},
}
for _, tc := range testCases {
gotClosest := tc.ranges.ClosestInDirection(tc.i, tc.towardsZero)
if gotClosest != tc.wantClosestIndex {
t.Errorf("%+v: got %v want %v", tc, gotClosest, tc.wantClosestIndex)
}
}
}
func TestRangeDelta(t *testing.T) {
testCases := []struct {
oldRange SliceRanges
newRange SliceRanges
wantAdded SliceRanges
wantSames SliceRanges
wantRemoved SliceRanges
}{
// added
{
oldRange: SliceRanges([][2]int64{}),
newRange: SliceRanges([][2]int64{
{0, 9},
}),
wantAdded: SliceRanges([][2]int64{
{0, 9},
}),
},
// removed
{
oldRange: SliceRanges([][2]int64{
{0, 9},
}),
newRange: SliceRanges([][2]int64{}),
wantRemoved: SliceRanges([][2]int64{
{0, 9},
}),
},
// same
{
oldRange: SliceRanges([][2]int64{
{0, 9},
}),
newRange: SliceRanges([][2]int64{
{0, 9},
}),
wantSames: SliceRanges([][2]int64{
{0, 9},
}),
},
// typical range increase
{
oldRange: SliceRanges([][2]int64{
{0, 9},
}),
newRange: SliceRanges([][2]int64{
{0, 9}, {10, 19},
}),
wantSames: SliceRanges([][2]int64{
{0, 9},
}),
wantAdded: SliceRanges([][2]int64{
{10, 19},
}),
},
// typical range swap
{
oldRange: SliceRanges([][2]int64{
{0, 9}, {10, 19},
}),
newRange: SliceRanges([][2]int64{
{0, 9}, {20, 29},
}),
wantSames: SliceRanges([][2]int64{
{0, 9},
}),
wantAdded: SliceRanges([][2]int64{
{20, 29},
}),
wantRemoved: SliceRanges([][2]int64{
{10, 19},
}),
},
// overlaps are handled intelligently
{
oldRange: SliceRanges([][2]int64{
{0, 9},
}),
newRange: SliceRanges([][2]int64{
{0, 10},
}),
wantSames: SliceRanges([][2]int64{
{0, 9},
}),
wantAdded: SliceRanges([][2]int64{
{10, 10},
}),
wantRemoved: [][2]int64{},
},
{
oldRange: SliceRanges([][2]int64{
{0, 9},
}),
newRange: SliceRanges([][2]int64{
{5, 15},
}),
wantSames: SliceRanges([][2]int64{
{5, 9},
}),
wantAdded: SliceRanges([][2]int64{
{10, 15},
}),
wantRemoved: SliceRanges([][2]int64{
{0, 4},
}),
},
{
oldRange: [][2]int64{{5, 15}},
newRange: [][2]int64{{0, 9}},
wantSames: [][2]int64{{5, 9}},
wantAdded: [][2]int64{{0, 4}},
wantRemoved: [][2]int64{{10, 15}},
},
{
oldRange: [][2]int64{{0, 20}, {35, 45}},
newRange: [][2]int64{{0, 20}, {36, 46}},
wantSames: [][2]int64{{0, 20}, {36, 45}},
wantAdded: [][2]int64{{46, 46}},
wantRemoved: [][2]int64{{35, 35}},
},
{
oldRange: [][2]int64{{0, 20}, {35, 45}},
newRange: [][2]int64{{0, 20}, {34, 44}},
wantSames: [][2]int64{{0, 20}, {35, 44}},
wantAdded: [][2]int64{{34, 34}},
wantRemoved: [][2]int64{{45, 45}},
},
// torture window, multiple same, add, remove
{
oldRange: [][2]int64{{10, 20}, {40, 50}, {70, 80}, {100, 110}},
newRange: [][2]int64{{0, 15}, {40, 50}, {80, 90}, {111, 120}},
wantSames: [][2]int64{{10, 15}, {40, 50}, {80, 80}},
wantAdded: [][2]int64{{0, 9}, {81, 90}, {111, 120}},
wantRemoved: [][2]int64{{16, 20}, {70, 79}, {100, 110}},
},
// swallowed range (one getting smaller, one getting bigger)
{
oldRange: [][2]int64{{0, 20}},
newRange: [][2]int64{{5, 15}},
wantSames: [][2]int64{{5, 15}},
wantRemoved: [][2]int64{{0, 4}, {16, 20}},
},
{
oldRange: [][2]int64{{5, 15}},
newRange: [][2]int64{{0, 20}},
wantSames: [][2]int64{{5, 15}},
wantAdded: [][2]int64{{0, 4}, {16, 20}},
},
// regression test
{
oldRange: [][2]int64{{0, 20}, {20, 24}},
newRange: [][2]int64{{0, 20}, {20, 24}},
wantSames: [][2]int64{{0, 20}, {20, 24}},
wantAdded: [][2]int64{},
wantRemoved: [][2]int64{},
},
// another regression test
{
oldRange: [][2]int64{{0, 0}},
newRange: [][2]int64{{0, 0}},
wantSames: [][2]int64{{0, 0}},
wantAdded: [][2]int64{},
wantRemoved: [][2]int64{},
},
// regression test, 1 element overlap
{
oldRange: [][2]int64{{10, 20}},
newRange: [][2]int64{{20, 30}},
wantSames: [][2]int64{{20, 20}},
wantAdded: [][2]int64{{21, 30}},
wantRemoved: [][2]int64{{10, 19}},
},
}
for _, tc := range testCases {
gotAdd, gotRm, gotSame := tc.oldRange.Delta(tc.newRange)
if tc.wantAdded != nil && !reflect.DeepEqual(gotAdd, tc.wantAdded) {
t.Errorf("%v -> %v got added %+v want %+v", tc.oldRange, tc.newRange, gotAdd, tc.wantAdded)
}
if tc.wantRemoved != nil && !reflect.DeepEqual(gotRm, tc.wantRemoved) {
t.Errorf("%v -> %v got remove %+v want %+v", tc.oldRange, tc.newRange, gotRm, tc.wantRemoved)
}
if tc.wantSames != nil && !reflect.DeepEqual(gotSame, tc.wantSames) {
t.Errorf("%v -> %v got same %+v want %+v", tc.oldRange, tc.newRange, gotSame, tc.wantSames)
}
}
}
func TestSortPoints(t *testing.T) {
testCases := []struct {
name string
input []pointInfo
want []pointInfo
}{
{
name: "two element",
input: []pointInfo{
{
x: 5,
isOldRange: true,
isOpen: true,
},
{
x: 2,
isOldRange: true,
isOpen: true,
},
},
want: []pointInfo{
{
x: 2,
isOldRange: true,
isOpen: true,
},
{
x: 5,
isOldRange: true,
isOpen: true,
},
},
},
{
name: "no dupes, sort by x",
input: []pointInfo{
{
x: 4,
isOldRange: true,
isOpen: true,
},
{
x: 1,
isOldRange: true,
isOpen: false,
},
{
x: 3,
isOldRange: false,
isOpen: true,
},
{
x: 2,
isOldRange: false,
isOpen: false,
},
},
want: []pointInfo{
{
x: 1,
isOldRange: true,
isOpen: false,
},
{
x: 2,
isOldRange: false,
isOpen: false,
},
{
x: 3,
isOldRange: false,
isOpen: true,
},
{
x: 4,
isOldRange: true,
isOpen: true,
},
},
},
{
name: "all dupes, sort by open(tie=old), close(tie=old)",
input: []pointInfo{
{
x: 4,
isOldRange: true,
isOpen: true,
},
{
x: 4,
isOldRange: false,
isOpen: true,
},
{
x: 4,
isOldRange: false,
isOpen: false,
},
{
x: 4,
isOldRange: true,
isOpen: false,
},
},
want: []pointInfo{
{
x: 4,
isOldRange: true,
isOpen: true,
},
{
x: 4,
isOldRange: false,
isOpen: true,
},
{
x: 4,
isOldRange: true,
isOpen: false,
},
{
x: 4,
isOldRange: false,
isOpen: false,
},
},
},
}
for _, tc := range testCases {
sortPoints(tc.input)
if !reflect.DeepEqual(tc.input, tc.want) {
t.Errorf("%s: got %+v\nwant %+v", tc.name, tc.input, tc.want)
}
}
}