Files
cabletest/main_test.go
T

295 lines
9.2 KiB
Go

package main
import (
"testing"
"time"
)
// Batches land in the bucket of the epoch they were read in, summed, and
// newest follows the highest epoch committed.
func TestRxCommitBucketsByEpoch(t *testing.T) {
var s rxStats
s.commit(0, 1, 100)
s.commit(0, 1, 200)
s.commit(1, 1, 300)
if f, b := s.bucket(0); f != 2 || b != 300 {
t.Errorf("epoch 0 = %d frames, %d bytes; want 2, 300", f, b)
}
if f, b := s.bucket(1); f != 1 || b != 300 {
t.Errorf("epoch 1 = %d frames, %d bytes; want 1, 300", f, b)
}
if got := s.newest.Load(); got != 1 {
t.Errorf("newest = %d, want 1", got)
}
}
// A slot coming round again belongs to its new epoch, and the epoch it replaced
// reports nothing rather than the stale counts.
func TestRxBucketWraps(t *testing.T) {
var s rxStats
s.commit(0, 1, 100)
s.commit(rateBuckets, 1, 200)
if f, b := s.bucket(0); f != 0 || b != 0 {
t.Errorf("evicted epoch = %d frames, %d bytes; want 0, 0", f, b)
}
if f, b := s.bucket(rateBuckets); f != 1 || b != 200 {
t.Errorf("new epoch = %d frames, %d bytes; want 1, 200", f, b)
}
}
// Every stream's frontier is past the epoch being read, so the bucket is
// complete across the board and the rate comes from the window behind the
// lowest frontier.
func TestDirectionRateReadsOneBucketBack(t *testing.T) {
d := &direction{rxStats: []*rxStats{{}, {}}}
d.rxStats[0].commit(1, 1, 500)
d.rxStats[1].commit(1, 1, 700)
d.rxStats[0].commit(2, 1, 900)
d.rxStats[1].commit(2, 1, 1100)
d.readRateBucket(time.Now())
if d.rateFrames != 2 || d.rateBytes != 1200 {
t.Errorf("rate bucket = %d frames, %d bytes; want the completed epoch, 2 and 1200",
d.rateFrames, d.rateBytes)
}
}
// Reading a bucket is a claim that every stream has delivered through it, so a
// stream still short of the epoch holds the read back rather than having its
// half-filled bucket summed.
func TestDirectionRateWaitsForSlowestStream(t *testing.T) {
d := &direction{rxStats: []*rxStats{{}, {}}}
d.rxStats[0].commit(1, 1, 500)
d.rxStats[0].commit(2, 1, 900)
// One stream has never delivered at all, so there is no epoch every stream
// has reached and nothing to read.
d.readRateBucket(time.Now())
if d.rateFrames != 0 || d.rateBytes != 0 {
t.Errorf("rate = %d frames, %d bytes; want nothing while a stream has no frontier",
d.rateFrames, d.rateBytes)
}
// The straggler is still filling the epoch the leader finished, so its
// bucket must not be read yet.
d.rxStats[1].commit(1, 1, 700)
d.readRateBucket(time.Now())
if d.rateFrames != 0 || d.rateBytes != 0 {
t.Errorf("rate = %d frames, %d bytes; want nothing while a stream is still filling the epoch",
d.rateFrames, d.rateBytes)
}
// Once it moves past, the bucket is complete for both streams and is read
// whole.
d.rxStats[1].commit(2, 1, 1100)
d.readRateBucket(time.Now())
if d.rateFrames != 2 || d.rateBytes != 1200 {
t.Errorf("rate = %d frames, %d bytes; want the whole completed epoch, 2 and 1200",
d.rateFrames, d.rateBytes)
}
}
// An epoch only advances when a frame is read, so a frontier that stops moving
// is a quiet wire and must not keep reporting the last bucket.
func TestDirectionRateGoesStale(t *testing.T) {
d := &direction{rxStats: []*rxStats{{}}}
d.rxStats[0].commit(1, 1, 500)
d.rxStats[0].commit(2, 1, 900)
now := time.Now()
d.readRateBucket(now)
if d.rateFrames == 0 {
t.Fatal("expected a rate while the epoch was still moving")
}
d.readRateBucket(now.Add(2 * rateStale))
if d.rateFrames != 0 || d.rateBytes != 0 {
t.Errorf("stale rate = %d frames, %d bytes; want 0, 0", d.rateFrames, d.rateBytes)
}
}
// Each bucket entering the settled window donates once, as it arrives.
func settleAll(frames, bytes []uint64) {
for i := 1; i <= len(frames); i++ {
fillBack(frames[:i], bytes[:i])
}
}
// A donated byte must never display twice: once a burst's excess has refilled
// an earlier deficit, the burst bucket itself reaches the display slot
// trimmed. The pre-settled-window implementation recomputed the smear from
// the raw ring per sample and displayed the excess again — 2C here, and >20G
// on the wire.
func TestDirectionRateNeverRedisplaysDonatedExcess(t *testing.T) {
d := &direction{rxStats: []*rxStats{{}}}
c := bucketWireCap
seq := []uint64{c, 0, 2 * c, c, c, c, c, c}
now := time.Now()
for i, b := range seq {
d.rxStats[0].commit(int64(i+1), 0, b)
d.readRateBucket(now)
if d.rateBytes > c {
t.Errorf("after epoch %d: displayed %d bytes, over line rate %d", i+1, d.rateBytes, c)
}
}
// Jitter settled: the display holds flat line rate, not just under it.
if d.rateBytes != c {
t.Errorf("settled display = %d bytes, want exactly %d", d.rateBytes, c)
}
}
// A stall's deficit is refilled exactly by the burst that drains its backlog,
// so host jitter flattens to line rate.
func TestSmearRepairsStallBurst(t *testing.T) {
c := bucketWireCap
bytes := []uint64{c, 0, 0, 3 * c}
frames := make([]uint64, 4)
settleAll(frames, bytes)
for i, want := range []uint64{c, c, c, c} {
if bytes[i] != want {
t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want)
}
}
}
// Excess refills the nearest earlier deficit, so a genuine wire dip — a
// deficit with no matching excess — keeps its full size in its own bucket.
func TestSmearLeavesRealLossInPlace(t *testing.T) {
c := bucketWireCap
bytes := []uint64{0, c, 0, 2 * c}
frames := make([]uint64, 4)
settleAll(frames, bytes)
for i, want := range []uint64{0, c, c, c} {
if bytes[i] != want {
t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want)
}
}
}
// A deficit after the excess is the host not having read those frames yet, not
// jitter to repair: nothing ever moves forward.
func TestSmearNeverMovesForward(t *testing.T) {
c := bucketWireCap
bytes := []uint64{c, 2 * c, 0}
frames := make([]uint64, 3)
settleAll(frames, bytes)
for i, want := range []uint64{c, 2 * c, 0} {
if bytes[i] != want {
t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want)
}
}
}
// Truly below line rate has nothing to move and displays as-is.
func TestSmearLeavesBelowRateAlone(t *testing.T) {
c := bucketWireCap
bytes := []uint64{c / 2, c / 4, c / 2}
frames := []uint64{100, 50, 100}
want := []uint64{c / 2, c / 4, c / 2}
settleAll(frames, bytes)
for i := range bytes {
if bytes[i] != want[i] {
t.Errorf("bucket %d = %d bytes, want untouched %d", i, bytes[i], want[i])
}
}
}
// Frames ride along with the bytes in the donor's own proportion, and nothing
// is invented or lost in the move.
func TestSmearConservesFramesAndBytes(t *testing.T) {
c := bucketWireCap
donorFrames := uint64(1000)
donorBytes := 2*c - donorFrames*wireOverhead
frames := []uint64{0, donorFrames}
bytes := []uint64{0, donorBytes}
settleAll(frames, bytes)
if tf := frames[0] + frames[1]; tf != donorFrames {
t.Errorf("total frames = %d, want %d conserved", tf, donorFrames)
}
if tb := bytes[0] + bytes[1]; tb != donorBytes {
t.Errorf("total bytes = %d, want %d conserved", tb, donorBytes)
}
if got := bytes[0] + frames[0]*wireOverhead; got != c {
t.Errorf("refilled bucket = %d wire bytes, want exactly %d", got, c)
}
if frames[0] != donorFrames/2 {
t.Errorf("moved frames = %d, want the donor's proportion %d", frames[0], donorFrames/2)
}
if got := bytes[1] + frames[1]*wireOverhead; got != c {
t.Errorf("donor left = %d wire bytes, want trimmed to %d", got, c)
}
}
// Indexed oldest first, so the error window still spans the whole ring once it
// has wrapped.
func TestRateWindowIndexesOldestFirst(t *testing.T) {
w := newRateWindow(3)
for i := 1; i <= 5; i++ {
w.push(counterSet{s: sample{rxFrames: uint64(i)}})
}
if got := w.count(); got != 3 {
t.Fatalf("count = %d, want 3", got)
}
if got := w.at(0).s.rxFrames; got != 3 {
t.Errorf("oldest = %d, want 3", got)
}
if got := w.at(2).s.rxFrames; got != 5 {
t.Errorf("newest = %d, want 5", 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, badHdr: 10,
txErrs: 6, rxErrs: 5,
},
drops: 9,
nic: 8,
}
got := errsBetween(counterSet{}, n)
want := errs{lost: 1, corrupt: 2 + 3 + 4 + 10, link: 8 + 5, internal: 9 + 7 + 6}
if got != want {
t.Errorf("errsBetween = %+v, want %+v", got, want)
}
if got.total() != 55 {
t.Errorf("total = %d, want 55", got.total())
}
}
// 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.
func TestResetDoesNotUnderflowTotals(t *testing.T) {
d := &direction{
win: newRateWindow(8),
rxStats: []*rxStats{{}},
}
d.rxStats[0].frames.Store(100)
d.rxStats[0].bytes.Store(6400)
d.sample()
d.rxStats[0].frames.Store(150)
d.rxStats[0].bytes.Store(9600)
d.reset()
v := d.displayView()
if v.rxFrames != 0 {
t.Errorf("rxFrames = %d, want 0", v.rxFrames)
}
if v.rxBytes != 0 {
t.Errorf("rxBytes = %d, want 0", v.rxBytes)
}
// The rolling window and the rates are about now rather than since the
// reset, so the buckets from before it have to survive.
if got := d.win.count(); got < 2 {
t.Errorf("ring holds %d buckets after reset, want the pre-reset history kept", got)
}
}