servertest: retry via compat netmap comparison until converged

Peer count arrives before routes; compare-once raced route delivery.
This commit is contained in:
Kristoffer Dalby authored and Kristoffer Dalby committed 2026-10-07 11:08:58 +02:00
1 parent 3e00a42bd2
commit 74121aa339
2 files changed
+45 -18

No files matched your search

+34 -15
View File
@@ -240,6 +240,9 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
saasAddrs := saasAddrsByNode(c, clients) saasAddrs := saasAddrsByNode(c, clients)
// Compare each viewer's [tailcfg.MapResponse] against the golden [netmap.NetworkMap]. // Compare each viewer's [tailcfg.MapResponse] against the golden [netmap.NetworkMap].
// Route approvals reach clients asynchronously, so the peer-count wait
// above says nothing about routes, primaries or filters: retry the
// whole comparison until the viewer's netmap has caught up.
for viewerName, cl := range clients { for viewerName, cl := range clients {
capture := c.Captures[viewerName] capture := c.Captures[viewerName]
if capture.Netmap == nil { if capture.Netmap == nil {
@@ -247,10 +250,12 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
} }
t.Run(viewerName, func(t *testing.T) { t.Run(viewerName, func(t *testing.T) {
nm := cl.Netmap() require.EventuallyWithT(t, func(collect *assert.CollectT) {
require.NotNil(t, nm, "netmap is nil") nm := cl.Netmap()
require.NotNil(collect, nm, "netmap is nil")
compareNetmap(t, nm, capture, clients, saasAddrs) compareNetmap(collect, nm, capture, clients, saasAddrs)
}, 30*time.Second, 100*time.Millisecond)
}) })
} }
} }
@@ -262,14 +267,18 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
// - Number of Tailscale IPs per peer (should be 2: one v4 + one v6) // - Number of Tailscale IPs per peer (should be 2: one v4 + one v6)
// - PrimaryRoutes per peer // - PrimaryRoutes per peer
// - PacketFilter (source, destination, ports) triples // - PacketFilter (source, destination, ports) triples
//
// It takes [require.TestingT] so it can run inside [require.EventuallyWithT].
func compareNetmap( func compareNetmap(
t *testing.T, t require.TestingT,
got *netmap.NetworkMap, got *netmap.NetworkMap,
want testcapture.Node, want testcapture.Node,
clients map[string]*servertest.TestClient, clients map[string]*servertest.TestClient,
saasAddrs map[netip.Addr]string, saasAddrs map[netip.Addr]string,
) { ) {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
require.NotNil(t, want.Netmap, "golden Netmap is nil") require.NotNil(t, want.Netmap, "golden Netmap is nil")
@@ -409,13 +418,15 @@ func compareNetmap(
// Tailscale IPs differ between SaaS and headscale allocation, so both // Tailscale IPs differ between SaaS and headscale allocation, so both
// sides are re-keyed by peer identity. // sides are re-keyed by peer identity.
func comparePacketFilter( func comparePacketFilter(
t *testing.T, t require.TestingT,
got *netmap.NetworkMap, got *netmap.NetworkMap,
want testcapture.Node, want testcapture.Node,
clients map[string]*servertest.TestClient, clients map[string]*servertest.TestClient,
saasAddrs map[netip.Addr]string, saasAddrs map[netip.Addr]string,
) { ) {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
hsAddrs := hsAddrsByPeer(clients) hsAddrs := hsAddrsByPeer(clients)
@@ -483,12 +494,14 @@ func capTriple(srcs, dst string, c peercap.Cap) string {
// peerIdents resolves a prefix into sorted canonical identity tokens, // peerIdents resolves a prefix into sorted canonical identity tokens,
// see [addIdentsForSrc]. // see [addIdentsForSrc].
func peerIdents( func peerIdents(
t *testing.T, t require.TestingT,
pfx netip.Prefix, pfx netip.Prefix,
addrToPeer map[netip.Addr]string, addrToPeer map[netip.Addr]string,
ruleIndex int, ruleIndex int,
) []string { ) []string {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
seen := map[string]struct{}{} seen := map[string]struct{}{}
addIdentsForSrc(t, pfx, addrToPeer, ruleIndex, seen) addIdentsForSrc(t, pfx, addrToPeer, ruleIndex, seen)
@@ -558,12 +571,14 @@ func hsAddrsByPeer(clients map[string]*servertest.TestClient) map[netip.Addr]str
// or /128 (IPv6) expands to the union of its contained peers. // or /128 (IPv6) expands to the union of its contained peers.
// Unresolvable Tailscale-range sources fail the test. // Unresolvable Tailscale-range sources fail the test.
func canonicaliseSrcStrings( func canonicaliseSrcStrings(
t *testing.T, t require.TestingT,
srcs []string, srcs []string,
addrToPeer map[netip.Addr]string, addrToPeer map[netip.Addr]string,
ruleIndex int, ruleIndex int,
) []string { ) []string {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
seen := map[string]struct{}{} seen := map[string]struct{}{}
@@ -588,12 +603,14 @@ func canonicaliseSrcStrings(
// [canonicaliseSrcStrings], reading already-parsed [netip.Prefix] values // [canonicaliseSrcStrings], reading already-parsed [netip.Prefix] values
// from [tailcfg.Match.Srcs]. // from [tailcfg.Match.Srcs].
func canonicaliseSrcPrefixes( func canonicaliseSrcPrefixes(
t *testing.T, t require.TestingT,
srcs []netip.Prefix, srcs []netip.Prefix,
addrToPeer map[netip.Addr]string, addrToPeer map[netip.Addr]string,
ruleIndex int, ruleIndex int,
) []string { ) []string {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
seen := map[string]struct{}{} seen := map[string]struct{}{}
@@ -615,13 +632,15 @@ func canonicaliseSrcPrefixes(
// through literally; a Tailscale-range prefix expands to the union // through literally; a Tailscale-range prefix expands to the union
// of peer names whose addresses fall within it. // of peer names whose addresses fall within it.
func addIdentsForSrc( func addIdentsForSrc(
t *testing.T, t require.TestingT,
pfx netip.Prefix, pfx netip.Prefix,
addrToPeer map[netip.Addr]string, addrToPeer map[netip.Addr]string,
ruleIndex int, ruleIndex int,
seen map[string]struct{}, seen map[string]struct{},
) { ) {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
if !prefixInTailscaleRange(pfx) { if !prefixInTailscaleRange(pfx) {
seen[pfx.String()] = struct{}{} seen[pfx.String()] = struct{}{}
+11 -3
View File
@@ -186,6 +186,9 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
} }
// Compare each viewer's [tailcfg.MapResponse] against golden [netmap.NetworkMap]. // Compare each viewer's [tailcfg.MapResponse] against golden [netmap.NetworkMap].
// Route approvals reach clients asynchronously, so the peer-count wait
// above says nothing about routes or primaries: retry the whole
// comparison until the viewer's netmap has caught up.
for viewerName, cl := range clients { for viewerName, cl := range clients {
capture := c.Captures[viewerName] capture := c.Captures[viewerName]
if capture.Netmap == nil { if capture.Netmap == nil {
@@ -193,7 +196,9 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
} }
t.Run(viewerName, func(t *testing.T) { t.Run(viewerName, func(t *testing.T) {
compareCaptureNetmap(t, cl, capture, clients) require.EventuallyWithT(t, func(collect *assert.CollectT) {
compareCaptureNetmap(collect, cl, capture, clients)
}, 30*time.Second, 100*time.Millisecond)
}) })
} }
} }
@@ -201,13 +206,16 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
// compareCaptureNetmap compares headscale's [tailcfg.MapResponse] against a // compareCaptureNetmap compares headscale's [tailcfg.MapResponse] against a
// [testcapture.Node]'s [netmap.NetworkMap] data. Same logic as [compareNetmap] but // [testcapture.Node]'s [netmap.NetworkMap] data. Same logic as [compareNetmap] but
// reads from typed [testcapture] fields instead of goldenFile strings. // reads from typed [testcapture] fields instead of goldenFile strings.
// It takes [require.TestingT] so it can run inside [require.EventuallyWithT].
func compareCaptureNetmap( func compareCaptureNetmap(
t *testing.T, t require.TestingT,
viewer *servertest.TestClient, viewer *servertest.TestClient,
want testcapture.Node, want testcapture.Node,
clients map[string]*servertest.TestClient, clients map[string]*servertest.TestClient,
) { ) {
t.Helper() if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
nm := viewer.Netmap() nm := viewer.Netmap()
require.NotNil(t, nm, "viewer has no netmap") require.NotNil(t, nm, "viewer has no netmap")