check / check (push) Canceled after 0s
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test are Dockerfile phases that script/lint and script/test build alone with a new CACHEBUST value, keeping Go's and golangci-lint's caches; the image build depends on both. make fmt and make fmt-check also format the markdown with prettier, pinned in yarn.lock. The agent guidance moves to AGENTS.md. The Go code is fixed for what the new configuration finds, and the AS and prefix detail reads now return the rows' error after their loops. Model: opus-5-5
176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
const (
|
|
testASNA = 64500
|
|
testASNB = 64501
|
|
testASNC = 64502
|
|
)
|
|
|
|
// recordingStore wraps mockStore to count every RecordPeering call, so a
|
|
// test can tell how many times a peering was written across separate runs.
|
|
type recordingStore struct {
|
|
*mockStore
|
|
|
|
mu sync.Mutex
|
|
calls int
|
|
}
|
|
|
|
func (r *recordingStore) RecordPeering(asA, asB int, ts time.Time) error {
|
|
r.mu.Lock()
|
|
r.calls++
|
|
r.mu.Unlock()
|
|
|
|
return r.mockStore.RecordPeering(asA, asB, ts)
|
|
}
|
|
|
|
func (r *recordingStore) callCount() int {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
return r.calls
|
|
}
|
|
|
|
// newTestHandler builds a PeeringHandler without starting the periodic
|
|
// processing goroutine, so tests drive processing explicitly.
|
|
func newTestHandler(db database.Store) *PeeringHandler {
|
|
return &PeeringHandler{
|
|
db: db,
|
|
logger: logger.New(),
|
|
asPaths: make(map[string]time.Time),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// snapshot returns how many paths the handler tracks and how many it dropped.
|
|
func snapshot(h *PeeringHandler) (int, int) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
return len(h.asPaths), h.droppedPaths
|
|
}
|
|
|
|
func pathKey(t *testing.T, asns ...int) string {
|
|
t.Helper()
|
|
|
|
b, err := json.Marshal(ristypes.ASPath(asns))
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal path: %v", err)
|
|
}
|
|
|
|
return string(b)
|
|
}
|
|
|
|
func handle(h *PeeringHandler, ts time.Time, asns ...int) {
|
|
h.HandleMessage(&ristypes.RISMessage{
|
|
Path: ristypes.ASPath(asns),
|
|
ParsedTimestamp: ts,
|
|
})
|
|
}
|
|
|
|
// TestPeeringHandlerProcessesEachRunAndEmpties verifies that a processing run
|
|
// empties the path map (the swap) and that a path seen again after a run is
|
|
// recorded in the next run too.
|
|
func TestPeeringHandlerProcessesEachRunAndEmpties(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := &recordingStore{mockStore: newMockStore()}
|
|
h := newTestHandler(store)
|
|
|
|
now := time.Now().UTC()
|
|
|
|
// Run 1: one path, one peering recorded, map emptied afterwards.
|
|
handle(h, now, testASNA, testASNB)
|
|
h.ProcessPeeringsNow()
|
|
|
|
if tracked, _ := snapshot(h); tracked != 0 {
|
|
t.Fatalf("map not empty after first run: %d paths remain", tracked)
|
|
}
|
|
|
|
if got := store.callCount(); got != 1 {
|
|
t.Fatalf("want 1 RecordPeering call after first run, got %d", got)
|
|
}
|
|
|
|
// Run 2: the same path again is recorded again (RecordPeering upserts).
|
|
handle(h, now.Add(time.Second), testASNA, testASNB)
|
|
h.ProcessPeeringsNow()
|
|
|
|
if tracked, _ := snapshot(h); tracked != 0 {
|
|
t.Fatalf("map not empty after second run: %d paths remain", tracked)
|
|
}
|
|
|
|
if got := store.callCount(); got != 2 {
|
|
t.Fatalf("want 2 RecordPeering calls after second run, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestPeeringHandlerCapDropsAndCounts verifies that a full map drops new paths
|
|
// and counts them, while a path already tracked is refreshed rather than
|
|
// dropped.
|
|
func TestPeeringHandlerCapDropsAndCounts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := &recordingStore{mockStore: newMockStore()}
|
|
h := newTestHandler(store)
|
|
|
|
now := time.Now().UTC()
|
|
|
|
// Fill the map to exactly maxTrackedPaths, including one real path key so
|
|
// the "already tracked" branch can be exercised. The filler keys are never
|
|
// processed in this test, so their contents do not matter.
|
|
existing := pathKey(t, testASNA, testASNB)
|
|
|
|
h.mu.Lock()
|
|
h.asPaths[existing] = now
|
|
|
|
for i := 0; len(h.asPaths) < maxTrackedPaths; i++ {
|
|
h.asPaths[strconv.Itoa(i)] = now
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
// A new path is dropped and counted because the map is full.
|
|
handle(h, now.Add(time.Second), testASNA, testASNC)
|
|
|
|
tracked, dropped := snapshot(h)
|
|
if tracked != maxTrackedPaths {
|
|
t.Fatalf("want map size %d after drop, got %d", maxTrackedPaths, tracked)
|
|
}
|
|
|
|
if dropped != 1 {
|
|
t.Fatalf("want dropped count 1, got %d", dropped)
|
|
}
|
|
|
|
// A path already tracked is refreshed, not dropped.
|
|
refreshed := now.Add(2 * time.Second)
|
|
handle(h, refreshed, testASNA, testASNB)
|
|
|
|
tracked, dropped = snapshot(h)
|
|
if tracked != maxTrackedPaths {
|
|
t.Fatalf("want map size %d after refresh, got %d", maxTrackedPaths, tracked)
|
|
}
|
|
|
|
if dropped != 1 {
|
|
t.Fatalf("want dropped count still 1 after refresh, got %d", dropped)
|
|
}
|
|
|
|
h.mu.Lock()
|
|
gotTS := h.asPaths[existing]
|
|
h.mu.Unlock()
|
|
|
|
if !gotTS.Equal(refreshed) {
|
|
t.Fatalf("existing path timestamp not refreshed: want %v, got %v",
|
|
refreshed, gotTS)
|
|
}
|
|
}
|