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() table := routetable.New(routetable.Params{}) addr := netip.MustParseAddr("10.0.0.0") for range routetable.MaxWaitingOriginChanges + 1 { prefix := netip.PrefixFrom(addr, 32) table.Announce(prefix, asA, routeOf(peerA, asA, 10)) addr = addr.Next() } changes, dropped := table.TakeOriginChanges(routetable.MaxWaitingOriginChanges) if dropped != 1 { t.Errorf("dropped = %d, want 1", dropped) } if len(changes) != routetable.MaxWaitingOriginChanges { t.Errorf("len(changes) = %d, want %d", len(changes), routetable.MaxWaitingOriginChanges) } } // 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() table := routetable.New(routetable.Params{}) addr := netip.MustParseAddr("10.0.0.0") for range routetable.MaxWaitingOriginChanges + 1 { table.Announce(netip.PrefixFrom(addr, 32), asA, routeOf(peerA, asA, 10)) addr = addr.Next() } _, dropped := table.TakeOriginChanges(routetable.MaxWaitingOriginChanges) 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 ( 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(routetable.MaxWaitingOriginChanges) 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) != routetable.MaxWaitingOriginChanges { t.Errorf("len(changes) = %d, want %d", len(changes), routetable.MaxWaitingOriginChanges) } // 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) } }