tstest/natlab/vmtest: add helpers for fatal step errors (#19753)

In a lot of places, we construct an error to End a step, then immediately log
it to the governing test as test fatal. Save ourselves a bit of boilerplate by
putting methods on Step for that.

There are a couple cases this doesn't cover, e.g., where we construct the Step
outside a subtest that wants to fail individually, but it helps enough to pay
for its lines.

Updates #13038

Change-Id: I71f9900942962de16609b6b198d3ba13d6958a5f
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
This commit is contained in:
M. J. Fromberger
2026-05-14 09:24:47 -07:00
committed by GitHub
parent 8203edc099
commit 4eb977413a
2 changed files with 40 additions and 47 deletions
+18
View File
@@ -129,6 +129,24 @@ func (s *Step) End(err error) {
s.env.publishStepChange(s) s.env.publishStepChange(s)
} }
// Fatalf marks the step as failed (as [Step.End]), and then logs a test
// failure to the environment's test, with an error constructed from the given
// arguments.
func (s *Step) Fatalf(msg string, args ...any) {
s.Fatal(fmt.Errorf(msg, args...))
}
// Fatal marks the step as failed (as [Step.End]), and then logs a test failure
// to the environment's test, with the specified (non-nil) error. It will panic
// if err == nil.
func (s *Step) Fatal(err error) {
if err == nil {
panic(fmt.Sprintf("Step %q: Fatal called with a nil error", s.name))
}
s.End(err)
s.env.t.Fatal(err)
}
// EventType identifies the kind of event published to the EventBus. // EventType identifies the kind of event published to the EventBus.
type EventType string type EventType string
+22 -47
View File
@@ -110,8 +110,7 @@ func testSubnetRouterForOS(t testing.TB, srOS vmtest.OSImage) {
httpStep.Begin() httpStep.Begin()
body := env.HTTPGet(client, fmt.Sprintf("http://%s:8080/", backend.LanIP(internalNet))) body := env.HTTPGet(client, fmt.Sprintf("http://%s:8080/", backend.LanIP(internalNet)))
if !strings.Contains(body, "Hello world I am backend") { if !strings.Contains(body, "Hello world I am backend") {
httpStep.End(fmt.Errorf("got %q", body)) httpStep.Fatalf("got %q", body)
t.Fatalf("got %q", body)
} }
httpStep.End(nil) httpStep.End(nil)
} }
@@ -198,16 +197,14 @@ func testSiteToSite(t *testing.T, srOS vmtest.OSImage) {
t.Logf("response: %s", body) t.Logf("response: %s", body)
if !strings.Contains(body, "Hello world I am backend-b") { if !strings.Contains(body, "Hello world I am backend-b") {
httpStep.End(fmt.Errorf("expected response from backend-b, got %q", body)) httpStep.Fatalf("expected response from backend-b, got %q", body)
t.Fatalf("expected response from backend-b, got %q", body)
} }
// Verify the source IP was preserved. With --snat-subnet-routes=false, // Verify the source IP was preserved. With --snat-subnet-routes=false,
// backend-b should see backend-a's LAN IP as the source, not sr-b's LAN IP. // backend-b should see backend-a's LAN IP as the source, not sr-b's LAN IP.
backendAIP := backendA.LanIP(lanA).String() backendAIP := backendA.LanIP(lanA).String()
if !strings.Contains(body, "from "+backendAIP) { if !strings.Contains(body, "from "+backendAIP) {
httpStep.End(fmt.Errorf("source IP not preserved: expected %q in response, got %q", backendAIP, body)) httpStep.Fatalf("source IP not preserved: expected %q in response, got %q", backendAIP, body)
t.Fatalf("source IP not preserved: expected %q in response, got %q", backendAIP, body)
} }
httpStep.End(nil) httpStep.End(nil)
} }
@@ -244,12 +241,10 @@ func TestInterNetworkTCP(t *testing.T) {
body := env.HTTPGet(client, fmt.Sprintf("http://%s:8080/", webWAN)) body := env.HTTPGet(client, fmt.Sprintf("http://%s:8080/", webWAN))
t.Logf("response: %s", body) t.Logf("response: %s", body)
if !strings.Contains(body, "Hello world I am webserver") { if !strings.Contains(body, "Hello world I am webserver") {
httpStep.End(fmt.Errorf("unexpected response: %q", body)) httpStep.Fatalf("unexpected response: %q", body)
t.Fatalf("unexpected response: %q", body)
} }
if !strings.Contains(body, "from "+clientWAN) { if !strings.Contains(body, "from "+clientWAN) {
httpStep.End(fmt.Errorf("expected source %q in response, got %q", clientWAN, body)) httpStep.Fatalf("expected source %q in response, got %q", clientWAN, body)
t.Fatalf("expected source %q in response, got %q", clientWAN, body)
} }
httpStep.End(nil) httpStep.End(nil)
} }
@@ -308,12 +303,10 @@ func TestSubnetRouterPublicIP(t *testing.T) {
body := env.HTTPGet(client, webURL) body := env.HTTPGet(client, webURL)
t.Logf("[%s] response: %s", label, body) t.Logf("[%s] response: %s", label, body)
if !strings.Contains(body, "Hello world I am webserver") { if !strings.Contains(body, "Hello world I am webserver") {
step.End(fmt.Errorf("[%s] unexpected webserver response: %q", label, body)) step.Fatalf("[%s] unexpected webserver response: %q", label, body)
t.Fatalf("[%s] unexpected webserver response: %q", label, body)
} }
if !strings.Contains(body, "from "+wantSrc) { if !strings.Contains(body, "from "+wantSrc) {
step.End(fmt.Errorf("[%s] expected source %q in response, got %q", label, wantSrc, body)) step.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
t.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
} }
step.End(nil) step.End(nil)
} }
@@ -458,15 +451,11 @@ func TestTaildrop(t *testing.T) {
verifyStep.Begin() verifyStep.Begin()
if gotName != filename { if gotName != filename {
err := fmt.Errorf("received name = %q; want %q", gotName, filename) verifyStep.Fatalf("received name = %q; want %q", gotName, filename)
verifyStep.End(err)
t.Error(err)
return return
} }
if !bytes.Equal(gotContent, want) { if !bytes.Equal(gotContent, want) {
err := fmt.Errorf("received content = %q; want %q", gotContent, want) verifyStep.Fatalf("received content = %q; want %q", gotContent, want)
verifyStep.End(err)
t.Error(err)
return return
} }
verifyStep.End(nil) verifyStep.End(nil)
@@ -542,12 +531,10 @@ func TestExitNode(t *testing.T) {
body := env.HTTPGet(client, webURL) body := env.HTTPGet(client, webURL)
t.Logf("response: %s", body) t.Logf("response: %s", body)
if !strings.Contains(body, "Hello world I am webserver") { if !strings.Contains(body, "Hello world I am webserver") {
tt.step.End(fmt.Errorf("unexpected webserver response: %q", body)) tt.step.Fatalf("unexpected webserver response: %q", body)
t.Fatalf("unexpected webserver response: %q", body)
} }
if !strings.Contains(body, "from "+tt.wantSrc) { if !strings.Contains(body, "from "+tt.wantSrc) {
tt.step.End(fmt.Errorf("expected source %q in response, got %q", tt.wantSrc, body)) tt.step.Fatalf("expected source %q in response, got %q", tt.wantSrc, body)
t.Fatalf("expected source %q in response, got %q", tt.wantSrc, body)
} }
tt.step.End(nil) tt.step.End(nil)
}) })
@@ -674,8 +661,7 @@ func TestDiscoKeyChange(t *testing.T) {
pingABStep.Begin() pingABStep.Begin()
if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil { if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil {
pingABStep.End(err) pingABStep.Fatal(err)
t.Fatal(err)
} }
pingABStep.End(nil) pingABStep.End(nil)
@@ -719,9 +705,7 @@ func checkDiscoRotated(t *testing.T, env *vmtest.Env, a, b, pingFrom, pingTo *vm
verifyStep.Begin() verifyStep.Begin()
bSt := env.Status(b) bSt := env.Status(b)
if got := bSt.Self.PublicKey; got != bNodeKey { if got := bSt.Self.PublicKey; got != bNodeKey {
err := fmt.Errorf("[%s] b's node key changed: %v -> %v", label, bNodeKey, got) verifyStep.Fatalf("[%s] b's node key changed: %v -> %v", label, bNodeKey, got)
verifyStep.End(err)
t.Fatal(err)
} }
var newDisco key.DiscoPublic var newDisco key.DiscoPublic
if err := tstest.WaitFor(15*time.Second, func() error { if err := tstest.WaitFor(15*time.Second, func() error {
@@ -735,8 +719,7 @@ func checkDiscoRotated(t *testing.T, env *vmtest.Env, a, b, pingFrom, pingTo *vm
newDisco = n.DiscoKey newDisco = n.DiscoKey
return nil return nil
}); err != nil { }); err != nil {
verifyStep.End(err) verifyStep.Fatalf("[%s] %v", label, err)
t.Fatalf("[%s] %v", label, err)
} }
t.Logf("[b] after %s: nodekey=%s discokey=%s", label, bNodeKey.ShortString(), newDisco.ShortString()) t.Logf("[b] after %s: nodekey=%s discokey=%s", label, bNodeKey.ShortString(), newDisco.ShortString())
verifyStep.End(nil) verifyStep.End(nil)
@@ -897,12 +880,10 @@ func TestMullvadExitNode(t *testing.T) {
body := env.HTTPGet(client, webURL) body := env.HTTPGet(client, webURL)
t.Logf("[%s] response: %s", label, body) t.Logf("[%s] response: %s", label, body)
if !strings.Contains(body, "Hello world I am webserver") { if !strings.Contains(body, "Hello world I am webserver") {
step.End(fmt.Errorf("[%s] unexpected webserver response: %q", label, body)) step.Fatalf("[%s] unexpected webserver response: %q", label, body)
t.Fatalf("[%s] unexpected webserver response: %q", label, body)
} }
if !strings.Contains(body, "from "+wantSrc) { if !strings.Contains(body, "from "+wantSrc) {
step.End(fmt.Errorf("[%s] expected source %q in response, got %q", label, wantSrc, body)) step.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
t.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
} }
step.End(nil) step.End(nil)
} }
@@ -965,8 +946,7 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
connectStep.Begin() connectStep.Begin()
if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil { if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil {
connectStep.End(err) connectStep.Fatal(err)
t.Fatal(err)
} }
connectStep.End(nil) connectStep.End(nil)
@@ -988,12 +968,10 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
for _, node := range []*vmtest.Node{a, b} { for _, node := range []*vmtest.Node{a, b} {
nm, err := local.GetDebugResultJSON[netmap.NetworkMap](t.Context(), node.Agent().Client, "current-netmap") nm, err := local.GetDebugResultJSON[netmap.NetworkMap](t.Context(), node.Agent().Client, "current-netmap")
if err != nil { if err != nil {
netmapCheckStep.End(fmt.Errorf("[%s] got err fetching netmap %q", node.Name(), err)) netmapCheckStep.Fatalf("[%s] got err fetching netmap %q", node.Name(), err)
t.Fatalf("[%s] got err fetching netmap %q", node.Name(), err)
} }
if !nm.Cached { if !nm.Cached {
netmapCheckStep.End(fmt.Errorf("[%s] expected netmap.Cached = true, got: %t", node.Name(), nm.Cached)) netmapCheckStep.Fatalf("[%s] expected netmap.Cached = true, got: %t", node.Name(), nm.Cached)
t.Fatalf("[%s] expected netmap.Cached = true, got: %t", node.Name(), nm.Cached)
} }
} }
netmapCheckStep.End(nil) netmapCheckStep.End(nil)
@@ -1010,8 +988,7 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
// because every timer fires multiple times under scheduling jitter. // because every timer fires multiple times under scheduling jitter.
pingStep.Begin() pingStep.Begin()
if err := env.Ping(a, b, tailcfg.PingTSMP, 90*time.Second); err != nil { if err := env.Ping(a, b, tailcfg.PingTSMP, 90*time.Second); err != nil {
pingStep.End(err) pingStep.Fatal(err)
t.Fatal(err)
} }
pingStep.End(nil) pingStep.End(nil)
} }
@@ -1066,15 +1043,13 @@ func TestDirectConnectionWithCachedNetmapOnOneNode(t *testing.T) {
tsmpPingStep.Begin() tsmpPingStep.Begin()
if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil { if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil {
tsmpPingStep.End(err) tsmpPingStep.Fatal(err)
t.Fatal(err)
} }
tsmpPingStep.End(nil) tsmpPingStep.End(nil)
discoPingStep.Begin() discoPingStep.Begin()
if err := env.PingExpect(a, b, vmtest.PingRouteDirect, 30*time.Second); err != nil { if err := env.PingExpect(a, b, vmtest.PingRouteDirect, 30*time.Second); err != nil {
discoPingStep.End(err) discoPingStep.Fatal(err)
t.Fatal(err)
} }
discoPingStep.End(nil) discoPingStep.End(nil)