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) } } // 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) } }