Make two tests independent of host load (closes #256) #258

Merged
clawbot merged 1 commits from issue-256-load-independent-tests into next 2026-11-11 19:40:39 +01:00
3 changed files with 72 additions and 43 deletions

No files matched your search

+4
View File
@@ -57,6 +57,10 @@ File an issue for each problem the run finds.
# Completed Steps # Completed Steps
- 2026-11-11: two more tests no longer fail on a busy host. The test of the home
page's card refresh runs on a fake clock. The geo test in which geojs never
answers counts each request as it is sent, not when it reaches the server
(closes https://git.eeqj.de/sneak/routewatch/issues/256).
- 2026-11-10: `github.com/mattn/go-sqlite3` is at v1.14.53, so building on the - 2026-11-10: `github.com/mattn/go-sqlite3` is at v1.14.53, so building on the
host no longer prints its two C compiler warnings (closes host no longer prints its two C compiler warnings (closes
https://git.eeqj.de/sneak/routewatch/issues/245). https://git.eeqj.de/sneak/routewatch/issues/245).
+24 -16
View File
@@ -90,14 +90,12 @@ func parseAddresses(t *testing.T, addresses ...string) []netip.Addr {
return addrs return addrs
} }
// fakeGeoJS is a geojs server. It sends body, or status when that is set, or // fakeGeoJS is a geojs server. It sends body, or status when that is set. It
// waits for the request to end when hang is true. It keeps each request's // keeps each request's address list.
// address list.
type fakeGeoJS struct { type fakeGeoJS struct {
server *httptest.Server server *httptest.Server
body string body string
status int status int
hang bool
mu sync.Mutex mu sync.Mutex
requests []string requests []string
@@ -119,12 +117,6 @@ func (f *fakeGeoJS) serve(w http.ResponseWriter, r *http.Request) {
f.requests = append(f.requests, r.URL.Query().Get("ip")) f.requests = append(f.requests, r.URL.Query().Get("ip"))
f.mu.Unlock() f.mu.Unlock()
if f.hang {
<-r.Context().Done()
return
}
if f.status != 0 { if f.status != 0 {
w.WriteHeader(f.status) w.WriteHeader(f.status)
@@ -142,6 +134,20 @@ func (f *fakeGeoJS) requestsSeen() []string {
return append([]string{}, f.requests...) return append([]string{}, f.requests...)
} }
// hangingTransport never answers: each request waits until its context ends.
// It counts the requests as they are sent, so the count does not depend on
// whether one reaches a server before its time limit runs out.
type hangingTransport struct {
requests atomic.Int32
}
func (h *hangingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
h.requests.Add(1)
<-r.Context().Done()
return nil, r.Context().Err()
}
// openCacheDB opens a new cache.db in a temporary directory. // openCacheDB opens a new cache.db in a temporary directory.
func openCacheDB(t *testing.T, dir string) *sql.DB { func openCacheDB(t *testing.T, dir string) *sql.DB {
t.Helper() t.Helper()
@@ -385,13 +391,15 @@ func TestLookupFallsBackToTheDatabase(t *testing.T) {
fake := newFakeGeoJS(t, tt.body) fake := newFakeGeoJS(t, tt.body)
fake.status = tt.status fake.status = tt.status
fake.hang = tt.hang
dir := t.TempDir() dir := t.TempDir()
locator := newTestLocator(t, fake, openCacheDB(t, dir), dir) locator := newTestLocator(t, fake, openCacheDB(t, dir), dir)
locator.timeLimit = shortTimeLimit requestCount := func() int { return len(fake.requestsSeen()) }
if !tt.hang { if tt.hang {
locator.timeLimit = testTimeLimit hanging := &hangingTransport{}
locator.client = &http.Client{Transport: hanging}
locator.timeLimit = shortTimeLimit
requestCount = func() int { return int(hanging.requests.Load()) }
} }
giveDatabase(t, locator) giveDatabase(t, locator)
@@ -413,8 +421,8 @@ func TestLookupFallsBackToTheDatabase(t *testing.T) {
// asks geojs again. // asks geojs again.
lookupOne(t, locator, testAddress) lookupOne(t, locator, testAddress)
if requests := fake.requestsSeen(); len(requests) != 2 { if requests := requestCount(); requests != 2 {
t.Errorf("%d requests, want 2", len(requests)) t.Errorf("%d requests, want 2", requests)
} }
}) })
} }
+44 -27
View File
@@ -275,49 +275,66 @@ func TestPagesServeTheCopyOfTheCards(t *testing.T) {
} }
} }
// TestKeepHomeCardsFreshWorksThemOutEveryInterval runs keepHomeCardsFresh with // TestKeepHomeCardsFreshWorksThemOutEveryInterval runs keepHomeCardsFresh. It
// a short interval. It works the cards out at once, again after the table // works the cards out at once, again an interval after the table changes, and
// changes, and stops when its context ends. // stops when its context ends. It runs in a synctest bubble, whose clock is
// fake, so a stall on the host cannot fail it.
func TestKeepHomeCardsFreshWorksThemOutEveryInterval(t *testing.T) { func TestKeepHomeCardsFreshWorksThemOutEveryInterval(t *testing.T) {
t.Parallel() t.Parallel()
const ( const interval = time.Minute
interval = 10 * time.Millisecond
maxWait = 15 * time.Second
)
table := emptyTable() table := emptyTable()
announce(table, "1.0.0.0/24", twoPrefixASN, testPeer(1), testRouteTime(0)) announce(table, "1.0.0.0/24", twoPrefixASN, testPeer(1), testRouteTime(0))
s := newServerWithTable(t, table) s := newServerWithTable(t, table)
ctx, cancel := context.WithCancel(t.Context()) // newestRouteOrigin returns the origin AS of the newest route on the
stopped := make(chan struct{}) // cards, or 0 when there is none.
newestRouteOrigin := func() int {
go func() {
s.keepHomeCardsFresh(ctx, interval)
close(stopped)
}()
newestRouteASNIs := func(asn int) bool {
cards := s.cards.Load() cards := s.cards.Load()
if cards == nil || cards.NewestRoute == nil {
return 0
}
return cards != nil && cards.NewestRoute != nil && return cards.NewestRoute.OriginASN
cards.NewestRoute.OriginASN == asn
} }
waitUntil(t, maxWait, func() bool { return newestRouteASNIs(twoPrefixASN) }) synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
stopped := make(chan struct{})
announce(table, "8.8.8.0/24", newestRouteASN, testPeer(2), testRouteTime(1)) go func() {
waitUntil(t, maxWait, func() bool { return newestRouteASNIs(newestRouteASN) }) s.keepHomeCardsFresh(ctx, interval)
close(stopped)
}()
cancel() synctest.Wait()
select { if got := newestRouteOrigin(); got != twoPrefixASN {
case <-stopped: t.Fatalf("at the start, the newest route's AS is %d, want %d",
case <-time.After(maxWait): got, twoPrefixASN)
t.Fatal("keepHomeCardsFresh did not stop when its context ended") }
}
announce(table, "8.8.8.0/24", newestRouteASN, testPeer(2),
testRouteTime(1))
time.Sleep(interval)
synctest.Wait()
if got := newestRouteOrigin(); got != newestRouteASN {
t.Fatalf("an interval after the table changed, the newest "+
"route's AS is %d, want %d", got, newestRouteASN)
}
cancel()
synctest.Wait()
select {
case <-stopped:
default:
t.Fatal("keepHomeCardsFresh did not stop when its context ended")
}
})
} }
// TestStartedServerWorksOutTheCards starts a server and waits until its home // TestStartedServerWorksOutTheCards starts a server and waits until its home