Cover the loss window, rate window, error taxonomy and formatters with tests
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func newWindow() *lossWindow {
|
||||||
|
w := newLossWindows(1)
|
||||||
|
return &w[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLossWindowContiguousLosesNothing(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
for seq := uint64(0); seq < lossSlots+1000; seq++ {
|
||||||
|
w.observe(seq)
|
||||||
|
}
|
||||||
|
if got := w.lost.Load(); got != 0 {
|
||||||
|
t.Errorf("lost = %d, want 0", got)
|
||||||
|
}
|
||||||
|
if got := w.late.Load(); got != 0 {
|
||||||
|
t.Errorf("late = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLossWindowCountsGapOnceEvicted(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
for seq := uint64(0); seq < lossSlots+1000; seq++ {
|
||||||
|
if seq == 100 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.observe(seq)
|
||||||
|
}
|
||||||
|
if got := w.lost.Load(); got != 1 {
|
||||||
|
t.Errorf("lost = %d, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arriving out of order inside the window is not loss: a sequence is only
|
||||||
|
// judged once it falls out the far end.
|
||||||
|
func TestLossWindowOutOfOrderIsNotLoss(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
for seq := uint64(99); ; seq-- {
|
||||||
|
w.observe(seq)
|
||||||
|
if seq == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for seq := uint64(100); seq < lossSlots+1000; seq++ {
|
||||||
|
w.observe(seq)
|
||||||
|
}
|
||||||
|
if got := w.lost.Load(); got != 0 {
|
||||||
|
t.Errorf("lost = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other branch of evict: a jump past a whole window writes off everything
|
||||||
|
// the window held plus the sequences that never landed in it at all.
|
||||||
|
func TestLossWindowJumpBeyondWindow(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
w.observe(0)
|
||||||
|
w.observe(200000)
|
||||||
|
|
||||||
|
// Everything below the new base except the one sequence that was seen.
|
||||||
|
want := uint64(200000 - lossSlots + 1 - 1)
|
||||||
|
if got := w.lost.Load(); got != want {
|
||||||
|
t.Errorf("lost = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got := w.late.Load(); got != 0 {
|
||||||
|
t.Errorf("late = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLossWindowBelowBaseIsLate(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
w.observe(100000)
|
||||||
|
w.observe(1000)
|
||||||
|
if got := w.late.Load(); got != 1 {
|
||||||
|
t.Errorf("late = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got := w.lost.Load(); got != 0 {
|
||||||
|
t.Errorf("lost = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first sequence seen starts the window half a span below it, so frames
|
||||||
|
// another worker is still holding land inside rather than arriving late.
|
||||||
|
func TestLossWindowStartsHalfAWindowBack(t *testing.T) {
|
||||||
|
w := newWindow()
|
||||||
|
w.observe(100000)
|
||||||
|
if w.base != 100000-lossSlots/2 {
|
||||||
|
t.Errorf("base = %d, want %d", w.base, 100000-lossSlots/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
-1
@@ -1,6 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRateWindowLatestNeedsTwoBuckets(t *testing.T) {
|
||||||
|
w := newRateWindow(4)
|
||||||
|
if got := w.latest(rxRatePPS); got != 0 {
|
||||||
|
t.Errorf("empty ring gave %v, want 0", got)
|
||||||
|
}
|
||||||
|
w.push(counterSet{t: time.Now(), s: sample{rxFrames: 100}})
|
||||||
|
if got := w.latest(rxRatePPS); got != 0 {
|
||||||
|
t.Errorf("one bucket gave %v, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The newest pair alone, so a step in the rate shows at once instead of being
|
||||||
|
// averaged against everything still in the ring.
|
||||||
|
func TestRateWindowLatestUsesNewestPair(t *testing.T) {
|
||||||
|
w := newRateWindow(4)
|
||||||
|
t0 := time.Now()
|
||||||
|
w.push(counterSet{t: t0, s: sample{rxFrames: 100}})
|
||||||
|
w.push(counterSet{t: t0.Add(time.Second), s: sample{rxFrames: 300}})
|
||||||
|
if got := w.latest(rxRatePPS); got != 200 {
|
||||||
|
t.Errorf("rate = %v, want 200", got)
|
||||||
|
}
|
||||||
|
w.push(counterSet{t: t0.Add(2 * time.Second), s: sample{rxFrames: 400}})
|
||||||
|
if got := w.latest(rxRatePPS); got != 100 {
|
||||||
|
t.Errorf("rate = %v, want 100 rather than the mean of the ring", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRateWindowLatestIgnoresZeroSpan(t *testing.T) {
|
||||||
|
w := newRateWindow(4)
|
||||||
|
t0 := time.Now()
|
||||||
|
w.push(counterSet{t: t0, s: sample{rxFrames: 100}})
|
||||||
|
w.push(counterSet{t: t0, s: sample{rxFrames: 300}})
|
||||||
|
if got := w.latest(rxRatePPS); got != 0 {
|
||||||
|
t.Errorf("rate = %v, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which counter feeds which bucket is the whole taxonomy the panel reports, so
|
||||||
|
// it is pinned here rather than left to whoever reads errsBetween next.
|
||||||
|
func TestErrsBetweenBuckets(t *testing.T) {
|
||||||
|
n := counterSet{
|
||||||
|
s: sample{
|
||||||
|
lost: 1, late: 7,
|
||||||
|
crcErr: 2, badMagic: 3, badLen: 4,
|
||||||
|
txErrs: 6, rxErrs: 5,
|
||||||
|
},
|
||||||
|
drops: 9,
|
||||||
|
nic: 8,
|
||||||
|
}
|
||||||
|
got := errsBetween(counterSet{}, n)
|
||||||
|
want := errs{lost: 1, corrupt: 2 + 3 + 4, link: 8 + 5, internal: 9 + 7 + 6}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("errsBetween = %+v, want %+v", got, want)
|
||||||
|
}
|
||||||
|
if got.total() != 45 {
|
||||||
|
t.Errorf("total = %d, want 45", got.total())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A reset re-bases from a fresh capture while the ring still holds buckets from
|
// A reset re-bases from a fresh capture while the ring still holds buckets from
|
||||||
// just before it, so the newest bucket must not be left behind the new origin.
|
// just before it, so the newest bucket must not be left behind the new origin.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func stamped(min, floor int64) view {
|
||||||
|
return view{cable: cableView{min: min, floor: floor, ok: true}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Averaged across directions, since one direction alone carries a phy asymmetry
|
||||||
|
// that swamps the cable.
|
||||||
|
func TestCableMetresAveragesDirections(t *testing.T) {
|
||||||
|
m, ok := cableMetres([]view{stamped(1000, 900), stamped(1100, 900)}, 5)
|
||||||
|
if !ok || m != 30 {
|
||||||
|
t.Errorf("cableMetres = %v, %v; want 30, true", m, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCableMetresNeedsEveryDirection(t *testing.T) {
|
||||||
|
if _, ok := cableMetres(nil, 5); ok {
|
||||||
|
t.Error("no views should not yield a length")
|
||||||
|
}
|
||||||
|
if _, ok := cableMetres([]view{stamped(1000, 900), {}}, 5); ok {
|
||||||
|
t.Error("a direction with no stamp yet should not yield a length")
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScaleSI(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
in float64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, "0.00"},
|
||||||
|
{999, "999.00"},
|
||||||
|
{1000, "1.00 k"},
|
||||||
|
{1234567, "1.23 M"},
|
||||||
|
{1e12, "1.00 T"},
|
||||||
|
{1e15, "1.00 P"},
|
||||||
|
} {
|
||||||
|
if got := scaleSI(c.in); got != c.want {
|
||||||
|
t.Errorf("scaleSI(%v) = %q, want %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below a thousand a count is the count, since two decimals on a quantity that
|
||||||
|
// cannot have them read as precision that is not there.
|
||||||
|
func TestScaleCount(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
in uint64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, "0"},
|
||||||
|
{999, "999"},
|
||||||
|
{1000, "1.00 k"},
|
||||||
|
} {
|
||||||
|
if got := scaleCount(c.in); got != c.want {
|
||||||
|
t.Errorf("scaleCount(%d) = %q, want %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleTime(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
in time.Duration
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{1500 * time.Millisecond, "1.50 s"},
|
||||||
|
{90 * time.Second, "1.50 m"},
|
||||||
|
{90 * time.Minute, "1.50 h"},
|
||||||
|
{36 * time.Hour, "1.50 d"},
|
||||||
|
} {
|
||||||
|
if got := scaleTime(c.in); got != c.want {
|
||||||
|
t.Errorf("scaleTime(%v) = %q, want %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommas(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
in uint64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, "0"},
|
||||||
|
{999, "999"},
|
||||||
|
{1000, "1,000"},
|
||||||
|
{1234567, "1,234,567"},
|
||||||
|
} {
|
||||||
|
if got := commas(c.in); got != c.want {
|
||||||
|
t.Errorf("commas(%d) = %q, want %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The table only lines up if colour codes are not counted as width.
|
||||||
|
func TestPadIgnoresColour(t *testing.T) {
|
||||||
|
if got := visWidth(paint("ok", cGreen)); got != 2 {
|
||||||
|
t.Errorf("visWidth = %d, want 2", got)
|
||||||
|
}
|
||||||
|
if got := visWidth(pad(paint("ok", cGreen), 6, true)); got != 6 {
|
||||||
|
t.Errorf("padded width = %d, want 6", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The break between the two halves of the row has to be the same width on the
|
||||||
|
// header, the rule and every value line, or the columns drift apart.
|
||||||
|
func TestStreamTableBreakAligns(t *testing.T) {
|
||||||
|
tbl := &streamTable{cols: intervalCols}
|
||||||
|
lines := tbl.headerLines()
|
||||||
|
row := tbl.emit(make([]string, len(intervalCols)))
|
||||||
|
widths := map[string]int{}
|
||||||
|
for _, l := range append(lines, row...) {
|
||||||
|
widths[l] = visWidth(l)
|
||||||
|
}
|
||||||
|
var first int
|
||||||
|
for _, w := range widths {
|
||||||
|
if first == 0 {
|
||||||
|
first = w
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if w != first {
|
||||||
|
t.Fatalf("header and value lines disagree on width: %v", widths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if first != tbl.width() {
|
||||||
|
t.Errorf("lines are %d wide, width() reports %d", first, tbl.width())
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGridCellFullRow(t *testing.T) {
|
||||||
|
x0, w0 := gridCell(0, 2, 0, 100)
|
||||||
|
x1, w1 := gridCell(1, 2, 0, 100)
|
||||||
|
if w0 != w1 {
|
||||||
|
t.Errorf("cells differ in width: %d vs %d", w0, w1)
|
||||||
|
}
|
||||||
|
if x0 != 0 {
|
||||||
|
t.Errorf("first cell x = %d, want 0", x0)
|
||||||
|
}
|
||||||
|
if x1+w1 != 100 {
|
||||||
|
t.Errorf("row ends at %d, want 100", x1+w1)
|
||||||
|
}
|
||||||
|
if got := x1 - (x0 + w0); got != chipGap {
|
||||||
|
t.Errorf("gap between cells = %d, want %d", got, chipGap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A last row that does not fill the grid is centred.
|
||||||
|
func TestGridCellShortLastRow(t *testing.T) {
|
||||||
|
cx, cw := gridCell(2, 3, 0, 100)
|
||||||
|
if left, right := cx, 100-(cx+cw); left != right {
|
||||||
|
t.Errorf("lone cell has %d left and %d right, want centred", left, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user