Files
routewatch/internal/routetable/originchanges_test.go
T
sneak 22783eb425
check / check (push) Failing after 15m27s
Fix the second review of the origin changes
Stop now counts the origin changes inside a running write, in a new
writingOriginChanges field, and adds them to the count it logs. The prune test
checks which rows survive, including a prefix with rows on both sides of 72
hours. The prefix page tests cover 5,000 and 5,001 rows and a row's time and
peer count. New tests cover the dropped-rows warning and the dropped count's
reset. The prune floor is named minOriginChangesPerPrefix. The README, TODO,
package doc and doc comments are corrected to match.

Model: deepseek-v4-pro
2026-11-05 05:08:27 +00:00

306 lines
8.5 KiB
Go

package routetable_test
import (
"math"
"net/netip"
"reflect"
"testing"
"time"
"github.com/bits-and-blooms/bitset"
"git.eeqj.de/sneak/routewatch/internal/routetable"
)
// originChange returns the origin change the table records for prefix and asn
// at the clock's time, announced or withdrawn, with peerCount peers.
func originChange(
prefix netip.Prefix, asn int, announced bool, peerCount int, at time.Time,
) routetable.OriginChange {
return routetable.OriginChange{
Prefix: prefix,
OriginASN: asn,
Announced: announced,
PeerCount: peerCount,
Time: at,
}
}
// TestOriginChangesRecordAnnouncedAndWithdrawn checks that a prefix and origin
// AS pair records an announced row when it gains its first peer, nothing for a
// second peer, and a withdrawn row when it loses its last peer.
func TestOriginChangesRecordAnnouncedAndWithdrawn(t *testing.T) {
t.Parallel()
now := at(0)
table := routetable.New(routetable.Params{
Now: func() time.Time { return now },
})
prefix := netip.MustParsePrefix(prefixV4)
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
table.Announce(prefix, asA, routeOf(peerB, asA, 20))
checkOriginChanges(t, table, []routetable.OriginChange{
originChange(prefix, asA, true, 1, now),
})
table.Withdraw(prefix, peerA)
table.Withdraw(prefix, peerB)
checkOriginChanges(t, table, []routetable.OriginChange{
originChange(prefix, asA, false, 0, now),
})
}
// TestOriginChangesRecordAPeerMovingOrigin checks that a peer moving from one
// origin AS to another records a withdrawn row for the old one and an
// announced row for the new one.
func TestOriginChangesRecordAPeerMovingOrigin(t *testing.T) {
t.Parallel()
now := at(0)
table := routetable.New(routetable.Params{
Now: func() time.Time { return now },
})
prefix := netip.MustParsePrefix(prefixV4)
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
table.Announce(prefix, asB, routeOf(peerA, asB, 20))
checkOriginChanges(t, table, []routetable.OriginChange{
originChange(prefix, asA, true, 1, now),
originChange(prefix, asA, false, 0, now),
originChange(prefix, asB, true, 1, now),
})
}
// TestOriginChangesRecordPeersDown checks that PeersDown records a withdrawn
// row for each prefix and origin AS pair it empties.
func TestOriginChangesRecordPeersDown(t *testing.T) {
t.Parallel()
now := at(0)
table := routetable.New(routetable.Params{
Now: func() time.Time { return now },
})
v4 := netip.MustParsePrefix(prefixV4)
v6 := netip.MustParsePrefix(prefixV6)
table.Announce(v4, asA, routeOf(peerA, asA, 10))
table.Announce(v6, asA, routeOf(peerA, asA, 10))
table.Announce(v4, asB, routeOf(peerB, asB, 20))
table.PeersDown([]string{peerA})
// peerA carried v4 and v6 with asA; both pairs are emptied. peerB still
// carries v4 with asB, so that pair is not.
checkOriginChanges(t, table, []routetable.OriginChange{
originChange(v4, asA, true, 1, now),
originChange(v6, asA, true, 1, now),
originChange(v4, asB, true, 2, now),
originChange(v4, asA, false, 1, now),
originChange(v6, asA, false, 0, now),
})
}
// TestOriginChangesLoadRecordsNothing checks that Load records no origin
// change.
func TestOriginChangesLoadRecordsNothing(t *testing.T) {
t.Parallel()
table := routetable.New(routetable.Params{
PeerNumbers: map[string]uint{peerA: 0},
})
prefix := netip.MustParsePrefix(prefixV4)
peers := new(bitset.BitSet)
peers.Set(0)
table.Load(routetable.Change{
Prefix: prefix,
OriginASN: asA,
Peers: peers,
Routes: []routetable.Route{routeOf(peerA, asA, 10)},
})
changes, dropped := table.TakeOriginChanges(math.MaxInt)
if dropped != 0 || len(changes) != 0 {
t.Errorf("TakeOriginChanges = %+v, %d; want none", changes, dropped)
}
}
// TestOriginChangesTakeReturnsOldestFirst checks that TakeOriginChanges
// returns the changes oldest first and at most max.
func TestOriginChangesTakeReturnsOldestFirst(t *testing.T) {
t.Parallel()
table := routetable.New(routetable.Params{})
prefix := netip.MustParsePrefix(prefixV4)
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
table.Announce(prefix, asB, routeOf(peerB, asB, 20))
table.Announce(prefix, asC, routeOf(peerC, asC, 30))
first, dropped := table.TakeOriginChanges(2)
if dropped != 0 {
t.Errorf("dropped = %d, want 0", dropped)
}
if len(first) != 2 || first[0].OriginASN != asA || first[1].OriginASN != asB {
t.Errorf("first TakeOriginChanges = %+v, want the first two", first)
}
second, _ := table.TakeOriginChanges(math.MaxInt)
if len(second) != 1 || second[0].OriginASN != asC {
t.Errorf("second TakeOriginChanges = %+v, want the third", second)
}
}
// TestOriginChangesPutBackPutsRowsInFront checks that PutBackOriginChanges
// puts rows back in front, so the next TakeOriginChanges returns them first.
func TestOriginChangesPutBackPutsRowsInFront(t *testing.T) {
t.Parallel()
table := routetable.New(routetable.Params{})
prefix := netip.MustParsePrefix(prefixV4)
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
table.Announce(prefix, asB, routeOf(peerB, asB, 20))
taken, _ := table.TakeOriginChanges(math.MaxInt)
table.Announce(prefix, asC, routeOf(peerC, asC, 30))
table.PutBackOriginChanges(taken)
changes, _ := table.TakeOriginChanges(math.MaxInt)
if len(changes) != 3 || changes[0].OriginASN != asA ||
changes[1].OriginASN != asB || changes[2].OriginASN != asC {
t.Errorf("TakeOriginChanges = %+v, want the put-back rows first", changes)
}
}
// TestOriginChangesDropPastTheLimit checks that a change past 100,000 waiting
// rows is dropped and counted.
func TestOriginChangesDropPastTheLimit(t *testing.T) {
t.Parallel()
const limit = 100000
table := routetable.New(routetable.Params{})
addr := netip.MustParseAddr("10.0.0.0")
for range limit + 1 {
prefix := netip.PrefixFrom(addr, 32)
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
addr = addr.Next()
}
changes, dropped := table.TakeOriginChanges(limit)
if dropped != 1 {
t.Errorf("dropped = %d, want 1", dropped)
}
if len(changes) != limit {
t.Errorf("len(changes) = %d, want %d", len(changes), limit)
}
}
// TestOriginChangesTakeResetsTheDroppedCount checks that a TakeOriginChanges
// that reports dropped rows leaves the next one reporting none, so each drop
// is logged once, not on every later write.
func TestOriginChangesTakeResetsTheDroppedCount(t *testing.T) {
t.Parallel()
const limit = 100000
table := routetable.New(routetable.Params{})
addr := netip.MustParseAddr("10.0.0.0")
for range limit + 1 {
table.Announce(netip.PrefixFrom(addr, 32), asA, routeOf(peerA, asA, 10))
addr = addr.Next()
}
_, dropped := table.TakeOriginChanges(limit)
if dropped != 1 {
t.Fatalf("dropped = %d, want 1", dropped)
}
_, dropped = table.TakeOriginChanges(0)
if dropped != 0 {
t.Errorf("the next take's dropped = %d, want 0", dropped)
}
}
// TestOriginChangesPutBackTrimsToTheLimit checks that putting back what a
// failed write took, after new changes filled the table again, drops the
// oldest changes past the cap and counts them, so the cap still holds. The
// clock advances one second per change, so a change's time says its age.
func TestOriginChangesPutBackTrimsToTheLimit(t *testing.T) {
t.Parallel()
const (
limit = 100000
taken = 20000
added = 20000
)
now := time.Unix(0, 0)
table := routetable.New(routetable.Params{
Now: func() time.Time {
now = now.Add(time.Second)
return now
},
})
addr := netip.MustParseAddr("10.0.0.0")
announce := func(n int) {
for range n {
table.Announce(netip.PrefixFrom(addr, 32), asA, routeOf(peerA, asA, 10))
addr = addr.Next()
}
}
announce(limit)
takenChanges, _ := table.TakeOriginChanges(taken)
announce(added)
table.PutBackOriginChanges(takenChanges)
changes, dropped := table.TakeOriginChanges(math.MaxInt)
if dropped != added {
t.Errorf("dropped = %d, want %d", dropped, added)
}
if len(changes) != limit {
t.Errorf("len(changes) = %d, want %d", len(changes), limit)
}
// The put-back rows are the oldest, so they are the ones dropped. The
// first remaining change is the one that followed them.
if want := time.Unix(int64(added+1), 0); !changes[0].Time.Equal(want) {
t.Errorf("first change's time = %v, want %v", changes[0].Time, want)
}
}
// checkOriginChanges checks that TakeOriginChanges, taking every change,
// returns want.
func checkOriginChanges(
t *testing.T, table *routetable.Table, want []routetable.OriginChange,
) {
t.Helper()
changes, dropped := table.TakeOriginChanges(math.MaxInt)
if dropped != 0 {
t.Errorf("dropped = %d, want 0", dropped)
}
if !reflect.DeepEqual(changes, want) {
t.Errorf("TakeOriginChanges = %+v, want %+v", changes, want)
}
}