From d84527a730d06461df69f84d6cf37c67adf58502 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 12 Feb 2020 16:57:40 +0000 Subject: [PATCH] lib/ranges: ranges libary for dealing with partially downloaded files --- lib/ranges/ranges.go | 297 +++++++++++++ lib/ranges/ranges_test.go | 861 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1158 insertions(+) create mode 100644 lib/ranges/ranges.go create mode 100644 lib/ranges/ranges_test.go diff --git a/lib/ranges/ranges.go b/lib/ranges/ranges.go new file mode 100644 index 000000000..8fac3e39e --- /dev/null +++ b/lib/ranges/ranges.go @@ -0,0 +1,297 @@ +// Package ranges provides the Ranges type for keeping track of byte +// ranges which may or may not be present in an object. +package ranges + +import ( + "sort" +) + +// Range describes a single byte range +type Range struct { + Pos int64 + Size int64 +} + +// End returns the end of the Range +func (r Range) End() int64 { + return r.Pos + r.Size +} + +// IsEmpty true if the range has no size +func (r Range) IsEmpty() bool { + return r.Size <= 0 +} + +// Clip ensures r.End() <= offset by modifying r.Size if necessary +// +// if r.Pos > offset then a Range{Pos:0, Size:0} will be returned. +func (r *Range) Clip(offset int64) { + if r.End() <= offset { + return + } + r.Size -= r.End() - offset + if r.Size < 0 { + r.Pos = 0 + r.Size = 0 + } +} + +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} + +// Intersection returns the common Range for two Range~s +// +// If there is no intersection then the Range returned will have +// IsEmpty() true +func (r Range) Intersection(b Range) (intersection Range) { + if (r.Pos >= b.Pos && r.Pos < b.End()) || (b.Pos >= r.Pos && b.Pos < r.End()) { + intersection.Pos = max(r.Pos, b.Pos) + intersection.Size = min(r.End(), b.End()) - intersection.Pos + } + return +} + +// Ranges describes a number of Range segments. These should only be +// added with the Ranges.Insert function. The Ranges are kept sorted +// and coalesced to the minimum size. +type Ranges []Range + +// merge the Range new into dest if possible +// +// dst.Pos must be >= src.Pos +// +// return true if merged +func merge(new, dst *Range) bool { + if new.End() < dst.Pos { + return false + } + if new.End() > dst.End() { + dst.Size = new.Size + } else { + dst.Size += dst.Pos - new.Pos + } + dst.Pos = new.Pos + return true +} + +// coalesce ranges assuming an element has been inserted at i +func (rs *Ranges) coalesce(i int) { + ranges := *rs + var j int + startChop := i + endChop := i + // look at previous element too + if i > 0 && merge(&ranges[i-1], &ranges[i]) { + startChop = i - 1 + } + for j = i; j < len(ranges)-1; j++ { + if !merge(&ranges[j], &ranges[j+1]) { + break + } + endChop = j + 1 + } + if endChop > startChop { + // chop the uneeded ranges out + copy(ranges[startChop:], ranges[endChop:]) + *rs = ranges[:len(ranges)-endChop+startChop] + } +} + +// search finds the first Range in rs that has Pos >= r.Pos +// +// The return takes on values 0..len(rs) so may point beyond the end +// of the slice. +func (rs Ranges) search(r Range) int { + return sort.Search(len(rs), func(i int) bool { + return rs[i].Pos >= r.Pos + }) +} + +// Insert the new Range into a sorted and coalesced slice of +// Ranges. The result will be sorted and coalesced. +func (rs *Ranges) Insert(r Range) { + if r.IsEmpty() { + return + } + ranges := *rs + if len(ranges) == 0 { + ranges = append(ranges, r) + *rs = ranges + return + } + i := ranges.search(r) + if i == len(ranges) || !merge(&r, &ranges[i]) { + // insert into the range + ranges = append(ranges, Range{}) + copy(ranges[i+1:], ranges[i:]) + ranges[i] = r + *rs = ranges + } + rs.coalesce(i) +} + +// Find searches for r in rs and returns the next present or absent +// Range. It returns: +// +// curr which is the Range found +// next is the Range which should be presented to Find next +// present shows whether curr is present or absent +// +// if !next.IsEmpty() then Find should be called again with r = next +// to retrieve the next Range. +// +// Note that r.Pos == curr.Pos always +func (rs Ranges) Find(r Range) (curr, next Range, present bool) { + if r.IsEmpty() { + return r, next, false + } + var intersection Range + i := rs.search(r) + if i > 0 { + prev := rs[i-1] + // we know prev.Pos < r.Pos so intersection.Pos == r.Pos + intersection = prev.Intersection(r) + if !intersection.IsEmpty() { + r.Pos = intersection.End() + r.Size -= intersection.Size + return intersection, r, true + } + } + if i >= len(rs) { + return r, Range{}, false + } + found := rs[i] + intersection = found.Intersection(r) + if intersection.IsEmpty() { + return r, Range{}, false + } + if r.Pos < intersection.Pos { + curr = Range{ + Pos: r.Pos, + Size: intersection.Pos - r.Pos, + } + r.Pos = curr.End() + r.Size -= curr.Size + return curr, r, false + } + r.Pos = intersection.End() + r.Size -= intersection.Size + return intersection, r, true +} + +// FoundRange is returned from FindAll +// +// It contains a Range and a boolean as to whether the range was +// Present or not. +type FoundRange struct { + R Range + Present bool +} + +// FindAll repeatedly calls Find searching for r in rs and returning +// present or absent ranges. +// +// It returns a slice of FoundRange. Each element has a range and an +// indication of whether it was present or not. +func (rs Ranges) FindAll(r Range) (frs []FoundRange) { + for !r.IsEmpty() { + var fr FoundRange + fr.R, r, fr.Present = rs.Find(r) + frs = append(frs, fr) + } + return frs +} + +// Present returns whether r can be satisfied by rs +func (rs Ranges) Present(r Range) (present bool) { + if r.IsEmpty() { + return true + } + _, next, present := rs.Find(r) + if !present { + return false + } + if next.IsEmpty() { + return true + } + return false +} + +// Intersection works out which ranges out of rs are entirely +// contained within r and returns a new Ranges +func (rs Ranges) Intersection(r Range) (newRs Ranges) { + if len(rs) == 0 { + return rs + } + for !r.IsEmpty() { + var curr Range + var found bool + curr, r, found = rs.Find(r) + if found { + newRs.Insert(curr) + } + } + return newRs +} + +// Equal returns true if rs == bs +func (rs Ranges) Equal(bs Ranges) bool { + if len(rs) != len(bs) { + return false + } + if rs == nil || bs == nil { + return true + } + for i := range rs { + if rs[i] != bs[i] { + return false + } + } + return true +} + +// Size returns the total size of all the segments +func (rs Ranges) Size() (size int64) { + for _, r := range rs { + size += r.Size + } + return size +} + +// FindMissing finds the initial part of r that is not in rs +// +// If r is entirely present in rs then r an empty block will be returned. +// +// If r is not present in rs then the block returned will have IsEmpty +// return true. +// +// If r is partially present in rs then a new block will be returned +// which starts with the first part of rs that isn't present in r. The +// End() for this block will be the same as originally passed in. +// +// For all returns rout.End() == r.End() +func (rs Ranges) FindMissing(r Range) (rout Range) { + rout = r + if r.IsEmpty() { + return rout + } + curr, _, present := rs.Find(r) + if !present { + // Initial block is not present + return rout + } + rout.Size -= curr.End() - rout.Pos + rout.Pos = curr.End() + return rout +} diff --git a/lib/ranges/ranges_test.go b/lib/ranges/ranges_test.go new file mode 100644 index 000000000..f71322ac3 --- /dev/null +++ b/lib/ranges/ranges_test.go @@ -0,0 +1,861 @@ +package ranges + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRangeEnd(t *testing.T) { + assert.Equal(t, int64(3), Range{Pos: 1, Size: 2}.End()) +} + +func TestRangeIsEmpty(t *testing.T) { + assert.Equal(t, false, Range{Pos: 1, Size: 2}.IsEmpty()) + assert.Equal(t, true, Range{Pos: 1, Size: 0}.IsEmpty()) + assert.Equal(t, true, Range{Pos: 1, Size: -1}.IsEmpty()) +} + +func TestRangeClip(t *testing.T) { + r := Range{Pos: 1, Size: 2} + r.Clip(5) + assert.Equal(t, Range{Pos: 1, Size: 2}, r) + + r = Range{Pos: 1, Size: 6} + r.Clip(5) + assert.Equal(t, Range{Pos: 1, Size: 4}, r) + + r = Range{Pos: 5, Size: 6} + r.Clip(5) + assert.Equal(t, Range{Pos: 5, Size: 0}, r) + + r = Range{Pos: 7, Size: 6} + r.Clip(5) + assert.Equal(t, Range{Pos: 0, Size: 0}, r) +} + +func TestRangeIntersection(t *testing.T) { + for _, test := range []struct { + r Range + b Range + want Range + }{ + { + r: Range{1, 1}, + b: Range{3, 1}, + want: Range{}, + }, + { + r: Range{1, 1}, + b: Range{1, 1}, + want: Range{1, 1}, + }, + { + r: Range{1, 9}, + b: Range{3, 2}, + want: Range{3, 2}, + }, + { + r: Range{1, 5}, + b: Range{3, 5}, + want: Range{3, 3}, + }, + } { + what := fmt.Sprintf("test r=%v, b=%v", test.r, test.b) + got := test.r.Intersection(test.b) + assert.Equal(t, test.want, got, what) + got = test.b.Intersection(test.r) + assert.Equal(t, test.want, got, what) + } +} + +func TestRangeMerge(t *testing.T) { + for _, test := range []struct { + new Range + dst Range + want Range + wantMerged bool + }{ + { + new: Range{Pos: 1, Size: 1}, // .N....... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 3, Size: 3}, // ...DDD... + wantMerged: false, + }, + { + new: Range{Pos: 1, Size: 2}, // .NN...... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 1, Size: 5}, // .XXXXX... + wantMerged: true, + }, + { + new: Range{Pos: 1, Size: 3}, // .NNN..... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 1, Size: 5}, // .XXXXX... + wantMerged: true, + }, + { + new: Range{Pos: 1, Size: 5}, // .NNNNN... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 1, Size: 5}, // .XXXXX... + wantMerged: true, + }, + { + new: Range{Pos: 1, Size: 6}, // .NNNNNN.. + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 1, Size: 6}, // .XXXXXX.. + wantMerged: true, + }, + { + new: Range{Pos: 3, Size: 3}, // ...NNN... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 3, Size: 3}, // ...XXX... + wantMerged: true, + }, + { + new: Range{Pos: 3, Size: 2}, // ...NN.... + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 3, Size: 3}, // ...XXX... + wantMerged: true, + }, + { + new: Range{Pos: 3, Size: 4}, // ...NNNN.. + dst: Range{Pos: 3, Size: 3}, // ...DDD... + want: Range{Pos: 3, Size: 4}, // ...XXXX.. + wantMerged: true, + }, + } { + what := fmt.Sprintf("test new=%v, dst=%v", test.new, test.dst) + gotMerged := merge(&test.new, &test.dst) + assert.Equal(t, test.wantMerged, gotMerged) + assert.Equal(t, test.want, test.dst, what) + } +} + +func checkRanges(t *testing.T, rs Ranges, what string) bool { + if len(rs) < 2 { + return true + } + ok := true + for i := 0; i < len(rs)-1; i++ { + a := rs[i] + b := rs[i+1] + if a.Pos >= b.Pos { + assert.Failf(t, "%s: Ranges in wrong order at %d in: %v", what, i, rs) + ok = false + } + if a.End() > b.Pos { + assert.Failf(t, "%s: Ranges overlap at %d in: %v", what, i, rs) + ok = false + } + if a.End() == b.Pos { + assert.Failf(t, "%s: Ranges not coalesced at %d in: %v", what, i, rs) + ok = false + } + } + return ok +} + +func TestRangeCoalesce(t *testing.T) { + for _, test := range []struct { + rs Ranges + i int + want Ranges + }{ + { + rs: Ranges{}, + want: Ranges{}, + }, + { + rs: Ranges{ + {Pos: 1, Size: 1}, + }, + want: Ranges{ + {Pos: 1, Size: 1}, + }, + i: 0, + }, + { + rs: Ranges{ + {Pos: 1, Size: 1}, + {Pos: 2, Size: 1}, + {Pos: 3, Size: 1}, + }, + want: Ranges{ + {Pos: 1, Size: 3}, + }, + i: 0, + }, + { + rs: Ranges{ + {Pos: 1, Size: 1}, + {Pos: 3, Size: 1}, + {Pos: 4, Size: 1}, + {Pos: 5, Size: 1}, + }, + want: Ranges{ + {Pos: 1, Size: 1}, + {Pos: 3, Size: 3}, + }, + i: 2, + }, + { + rs: Ranges{{38, 8}, {51, 10}, {60, 3}}, + want: Ranges{{38, 8}, {51, 12}}, + i: 1, + }, + } { + got := append(Ranges{}, test.rs...) + got.coalesce(test.i) + what := fmt.Sprintf("test rs=%v, i=%d", test.rs, test.i) + assert.Equal(t, test.want, got, what) + checkRanges(t, got, what) + } +} + +func TestRangeInsert(t *testing.T) { + for _, test := range []struct { + new Range + rs Ranges + want Ranges + }{ + { + new: Range{Pos: 1, Size: 0}, + rs: Ranges{}, + want: Ranges(nil), + }, + { + new: Range{Pos: 1, Size: 1}, // .N....... + rs: Ranges{}, // ......... + want: Ranges{ // .N....... + {Pos: 1, Size: 1}, + }, + }, + { + new: Range{Pos: 1, Size: 1}, // .N....... + rs: Ranges{{Pos: 5, Size: 1}}, // .....R... + want: Ranges{ // .N...R... + {Pos: 1, Size: 1}, + {Pos: 5, Size: 1}, + }, + }, + { + new: Range{Pos: 5, Size: 1}, // .....R... + rs: Ranges{{Pos: 1, Size: 1}}, // .N....... + want: Ranges{ // .N...R... + {Pos: 1, Size: 1}, + {Pos: 5, Size: 1}, + }, + }, + { + new: Range{Pos: 1, Size: 1}, // .N....... + rs: Ranges{{Pos: 2, Size: 1}}, // ..R...... + want: Ranges{ // .XX...... + {Pos: 1, Size: 2}, + }, + }, + { + new: Range{Pos: 2, Size: 1}, // ..N....... + rs: Ranges{{Pos: 1, Size: 1}}, // .R...... + want: Ranges{ // .XX...... + {Pos: 1, Size: 2}, + }, + }, + { + new: Range{Pos: 51, Size: 10}, + rs: Ranges{{38, 8}, {57, 2}, {60, 3}}, + want: Ranges{{38, 8}, {51, 12}}, + }, + } { + got := append(Ranges(nil), test.rs...) + got.Insert(test.new) + what := fmt.Sprintf("test new=%v, rs=%v", test.new, test.rs) + assert.Equal(t, test.want, got, what) + checkRanges(t, test.rs, what) + checkRanges(t, got, what) + } +} + +func TestRangeInsertRandom(t *testing.T) { + for i := 0; i < 100; i++ { + var rs Ranges + for j := 0; j < 100; j++ { + var r = Range{ + Pos: rand.Int63n(100), + Size: rand.Int63n(10) + 1, + } + what := fmt.Sprintf("inserting %v into %v\n", r, rs) + rs.Insert(r) + if !checkRanges(t, rs, what) { + break + } + //fmt.Printf("%d: %d: %v\n", i, j, rs) + } + } +} + +func TestRangeFind(t *testing.T) { + for _, test := range []struct { + rs Ranges + r Range + wantCurr Range + wantNext Range + wantPresent bool + }{ + { + r: Range{Pos: 1, Size: 0}, + rs: Ranges{}, + wantCurr: Range{Pos: 1, Size: 0}, + wantNext: Range{}, + wantPresent: false, + }, + { + r: Range{Pos: 1, Size: 1}, + rs: Ranges{}, + wantCurr: Range{Pos: 1, Size: 1}, + wantNext: Range{}, + wantPresent: false, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 1, Size: 10}, + }, + wantCurr: Range{Pos: 1, Size: 2}, + wantNext: Range{Pos: 3, Size: 0}, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 10}, + rs: Ranges{ + Range{Pos: 1, Size: 2}, + }, + wantCurr: Range{Pos: 1, Size: 2}, + wantNext: Range{Pos: 3, Size: 8}, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 5, Size: 2}, + }, + wantCurr: Range{Pos: 1, Size: 2}, + wantNext: Range{Pos: 0, Size: 0}, + wantPresent: false, + }, + { + r: Range{Pos: 2, Size: 10}, + rs: Ranges{ + Range{Pos: 1, Size: 2}, + }, + wantCurr: Range{Pos: 2, Size: 1}, + wantNext: Range{Pos: 3, Size: 9}, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 9}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantCurr: Range{Pos: 1, Size: 1}, + wantNext: Range{Pos: 2, Size: 8}, + wantPresent: false, + }, + { + r: Range{Pos: 2, Size: 8}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantCurr: Range{Pos: 2, Size: 1}, + wantNext: Range{Pos: 3, Size: 7}, + wantPresent: true, + }, + { + r: Range{Pos: 3, Size: 7}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantCurr: Range{Pos: 3, Size: 1}, + wantNext: Range{Pos: 4, Size: 6}, + wantPresent: false, + }, + { + r: Range{Pos: 4, Size: 6}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantCurr: Range{Pos: 4, Size: 1}, + wantNext: Range{Pos: 5, Size: 5}, + wantPresent: true, + }, + { + r: Range{Pos: 5, Size: 5}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantCurr: Range{Pos: 5, Size: 5}, + wantNext: Range{Pos: 0, Size: 0}, + wantPresent: false, + }, + } { + what := fmt.Sprintf("test r=%v, rs=%v", test.r, test.rs) + checkRanges(t, test.rs, what) + gotCurr, gotNext, gotPresent := test.rs.Find(test.r) + assert.Equal(t, test.r.Pos, gotCurr.Pos, what) + assert.Equal(t, test.wantCurr, gotCurr, what) + assert.Equal(t, test.wantNext, gotNext, what) + assert.Equal(t, test.wantPresent, gotPresent, what) + } +} + +func TestRangeFindAll(t *testing.T) { + for _, test := range []struct { + rs Ranges + r Range + want []FoundRange + wantNext Range + wantPresent bool + }{ + { + r: Range{Pos: 1, Size: 0}, + rs: Ranges{}, + want: []FoundRange(nil), + }, + { + r: Range{Pos: 1, Size: 1}, + rs: Ranges{}, + want: []FoundRange{ + { + R: Range{Pos: 1, Size: 1}, + Present: false, + }, + }, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 1, Size: 10}, + }, + want: []FoundRange{ + { + R: Range{Pos: 1, Size: 2}, + Present: true, + }, + }, + }, + { + r: Range{Pos: 1, Size: 10}, + rs: Ranges{ + Range{Pos: 1, Size: 2}, + }, + want: []FoundRange{ + { + R: Range{Pos: 1, Size: 2}, + Present: true, + }, + { + R: Range{Pos: 3, Size: 8}, + Present: false, + }, + }, + }, + { + r: Range{Pos: 5, Size: 5}, + rs: Ranges{ + Range{Pos: 4, Size: 2}, + Range{Pos: 7, Size: 1}, + Range{Pos: 9, Size: 2}, + }, + want: []FoundRange{ + { + R: Range{Pos: 5, Size: 1}, + Present: true, + }, + { + R: Range{Pos: 6, Size: 1}, + Present: false, + }, + { + R: Range{Pos: 7, Size: 1}, + Present: true, + }, + { + R: Range{Pos: 8, Size: 1}, + Present: false, + }, + { + R: Range{Pos: 9, Size: 1}, + Present: true, + }, + }, + }, + } { + what := fmt.Sprintf("test r=%v, rs=%v", test.r, test.rs) + checkRanges(t, test.rs, what) + got := test.rs.FindAll(test.r) + assert.Equal(t, test.want, got, what) + } +} + +func TestRangePresent(t *testing.T) { + for _, test := range []struct { + rs Ranges + r Range + wantPresent bool + }{ + { + r: Range{Pos: 1, Size: 0}, + rs: Ranges{}, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 0}, + rs: Ranges(nil), + wantPresent: true, + }, + { + r: Range{Pos: 0, Size: 1}, + rs: Ranges{}, + wantPresent: false, + }, + { + r: Range{Pos: 0, Size: 1}, + rs: Ranges(nil), + wantPresent: false, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 1, Size: 1}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 1, Size: 2}, + }, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 1, Size: 10}, + }, + wantPresent: true, + }, + { + r: Range{Pos: 1, Size: 2}, + rs: Ranges{ + Range{Pos: 5, Size: 2}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 1, Size: 9}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 2, Size: 8}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 3, Size: 7}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 4, Size: 6}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantPresent: false, + }, + { + r: Range{Pos: 5, Size: 5}, + rs: Ranges{ + Range{Pos: 2, Size: 1}, + Range{Pos: 4, Size: 1}, + }, + wantPresent: false, + }, + } { + what := fmt.Sprintf("test r=%v, rs=%v", test.r, test.rs) + checkRanges(t, test.rs, what) + gotPresent := test.rs.Present(test.r) + assert.Equal(t, test.wantPresent, gotPresent, what) + checkRanges(t, test.rs, what) + } +} + +func TestRangesIntersection(t *testing.T) { + for _, test := range []struct { + rs Ranges + r Range + want Ranges + }{ + { + rs: Ranges(nil), + r: Range{}, + want: Ranges(nil), + }, + { + rs: Ranges{}, + r: Range{}, + want: Ranges{}, + }, + { + rs: Ranges{}, + r: Range{Pos: 1, Size: 0}, + want: Ranges{}, + }, + { + rs: Ranges{}, + r: Range{Pos: 1, Size: 1}, + want: Ranges{}, + }, + { + rs: Ranges{{Pos: 1, Size: 5}}, + r: Range{Pos: 1, Size: 3}, + want: Ranges{ + {Pos: 1, Size: 3}, + }, + }, + { + rs: Ranges{{Pos: 1, Size: 5}}, + r: Range{Pos: 1, Size: 10}, + want: Ranges{ + {Pos: 1, Size: 5}, + }, + }, + { + rs: Ranges{{Pos: 1, Size: 5}}, + r: Range{Pos: 3, Size: 10}, + want: Ranges{ + {Pos: 3, Size: 3}, + }, + }, + { + rs: Ranges{{Pos: 1, Size: 5}}, + r: Range{Pos: 6, Size: 10}, + want: Ranges(nil), + }, + { + rs: Ranges{ + {Pos: 1, Size: 2}, + {Pos: 11, Size: 2}, + {Pos: 21, Size: 2}, + {Pos: 31, Size: 2}, + {Pos: 41, Size: 2}, + }, + r: Range{Pos: 12, Size: 20}, + want: Ranges{ + {Pos: 12, Size: 1}, + {Pos: 21, Size: 2}, + {Pos: 31, Size: 1}, + }, + }, + } { + got := test.rs.Intersection(test.r) + what := fmt.Sprintf("test ra=%v, r=%v", test.rs, test.r) + assert.Equal(t, test.want, got, what) + checkRanges(t, test.rs, what) + checkRanges(t, got, what) + } +} + +func TestRangesEqual(t *testing.T) { + for _, test := range []struct { + rs Ranges + bs Ranges + want bool + }{ + { + rs: Ranges(nil), + bs: Ranges(nil), + want: true, + }, + { + rs: Ranges{}, + bs: Ranges(nil), + want: true, + }, + { + rs: Ranges(nil), + bs: Ranges{}, + want: true, + }, + { + rs: Ranges{}, + bs: Ranges{}, + want: true, + }, + { + rs: Ranges{ + {Pos: 0, Size: 1}, + }, + bs: Ranges{}, + want: false, + }, + { + rs: Ranges{ + {Pos: 0, Size: 1}, + }, + bs: Ranges{ + {Pos: 0, Size: 1}, + }, + want: true, + }, + { + rs: Ranges{ + {Pos: 0, Size: 1}, + {Pos: 10, Size: 9}, + {Pos: 20, Size: 21}, + }, + bs: Ranges{ + {Pos: 0, Size: 1}, + {Pos: 10, Size: 9}, + {Pos: 20, Size: 22}, + }, + want: false, + }, + { + rs: Ranges{ + {Pos: 0, Size: 1}, + {Pos: 10, Size: 9}, + {Pos: 20, Size: 21}, + }, + bs: Ranges{ + {Pos: 0, Size: 1}, + {Pos: 10, Size: 9}, + {Pos: 20, Size: 21}, + }, + want: true, + }, + } { + got := test.rs.Equal(test.bs) + what := fmt.Sprintf("test rs=%v, bs=%v", test.rs, test.bs) + assert.Equal(t, test.want, got, what) + checkRanges(t, test.bs, what) + checkRanges(t, test.rs, what) + } +} + +func TestRangesSize(t *testing.T) { + for _, test := range []struct { + rs Ranges + want int64 + }{ + { + rs: Ranges(nil), + want: 0, + }, + { + rs: Ranges{}, + want: 0, + }, + { + rs: Ranges{ + {Pos: 7, Size: 11}, + }, + want: 11, + }, + { + rs: Ranges{ + {Pos: 0, Size: 1}, + {Pos: 10, Size: 9}, + {Pos: 20, Size: 21}, + }, + want: 31, + }, + } { + got := test.rs.Size() + what := fmt.Sprintf("test rs=%v", test.rs) + assert.Equal(t, test.want, got, what) + checkRanges(t, test.rs, what) + } +} + +func TestFindMissing(t *testing.T) { + for _, test := range []struct { + r Range + rs Ranges + want Range + }{ + { + r: Range{}, + rs: Ranges(nil), + want: Range{}, + }, + { + r: Range{}, + rs: Ranges{}, + want: Range{}, + }, + { + r: Range{Pos: 3, Size: 5}, + rs: Ranges{ + {Pos: 10, Size: 5}, + {Pos: 20, Size: 5}, + }, + want: Range{Pos: 3, Size: 5}, + }, + { + r: Range{Pos: 3, Size: 15}, + rs: Ranges{ + {Pos: 10, Size: 5}, + {Pos: 20, Size: 5}, + }, + want: Range{Pos: 3, Size: 15}, + }, + { + r: Range{Pos: 10, Size: 5}, + rs: Ranges{ + {Pos: 10, Size: 5}, + {Pos: 20, Size: 5}, + }, + want: Range{Pos: 15, Size: 0}, + }, + { + r: Range{Pos: 10, Size: 7}, + rs: Ranges{ + {Pos: 10, Size: 5}, + {Pos: 20, Size: 5}, + }, + want: Range{Pos: 15, Size: 2}, + }, + { + r: Range{Pos: 11, Size: 7}, + rs: Ranges{ + {Pos: 10, Size: 5}, + {Pos: 20, Size: 5}, + }, + want: Range{Pos: 15, Size: 3}, + }, + } { + got := test.rs.FindMissing(test.r) + what := fmt.Sprintf("test r=%v, rs=%v", test.r, test.rs) + assert.Equal(t, test.want, got, what) + assert.Equal(t, test.r.End(), got.End()) + checkRanges(t, test.rs, what) + } +}