From 94af1b00fb5320864d848847870e9d6d10f5a37c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 May 2026 19:35:04 +0000 Subject: [PATCH] cmd/testwrapper, tstest: move test sharding out of test code Previously, sharding required tests to opt in by calling tstest.Shard, which used a process-global counter to assign each test to a shard. This had two problems: most tests didn't call it, so they ran on every shard (defeating the purpose), and shard assignments were unstable (depended on call order, so adding a test could reshuffle others). Remove tstest.Shard and tstest.SkipOnUnshardedCI entirely. Instead, have testwrapper implement sharding automatically for all tests: when TS_TEST_SHARD=N/M is set, it uses "go list -json" (no compilation) to find test source files, scans them for top-level Test/Benchmark/ Example/Fuzz function names, and filters by fnv32a(name) % M == N-1. The filtered names are passed as an anchored -run regex to go test. Using go list instead of "go test -list" avoids linking the test binary twice (Go's build cache does not cache test binary linking). Fixes #19886 Change-Id: I62ab7b3d757324d4c5fd0b5de50c1e3742681791 Signed-off-by: Brad Fitzpatrick --- cmd/testwrapper/testwrapper.go | 115 ++++++++++++++++++++++++- tsnet/packet_filter_test.go | 2 - tsnet/tsnet_test.go | 20 ----- tstest/integration/capmap_test.go | 2 - tstest/integration/integration_test.go | 35 -------- tstest/integration/whois_test.go | 1 - tstest/largetailnet/delta_test.go | 3 +- tstest/tstest.go | 37 -------- 8 files changed, 112 insertions(+), 103 deletions(-) diff --git a/cmd/testwrapper/testwrapper.go b/cmd/testwrapper/testwrapper.go index 4aa0f486a..201ac2aa1 100644 --- a/cmd/testwrapper/testwrapper.go +++ b/cmd/testwrapper/testwrapper.go @@ -15,12 +15,16 @@ import ( "encoding/json" "errors" "fmt" + "hash/fnv" "io" "log" "os" "os/exec" + "path/filepath" + "regexp" "slices" "sort" + "strconv" "strings" "time" @@ -83,6 +87,94 @@ type goTestOutput struct { var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != "" +// testsForShard returns the test names in pkg that belong to the given shard +// spec (e.g. "2/3"). It uses "go list -json" to find test source files (no +// compilation) and scans them for top-level test function names, assigning +// each to a shard by hashing. Returns nil if the spec is invalid or if +// listing fails (the main run will surface the error). +func testsForShard(ctx context.Context, pkg, shardSpec string) ([]string, error) { + a, b, ok := strings.Cut(shardSpec, "/") + if !ok { + return nil, nil + } + wantShard, err := strconv.Atoi(a) + if err != nil || wantShard < 1 { + return nil, nil + } + shards, err := strconv.Atoi(b) + if err != nil || shards < 1 { + return nil, nil + } + + out, err := exec.CommandContext(ctx, "go", "list", "-json", pkg).Output() + if err != nil { + // Errors will be surfaced by the main test run. + return nil, nil + } + + type pkgJSON struct { + Dir string + TestGoFiles []string + XTestGoFiles []string + } + + seen := map[string]bool{} + var result []string + + dec := json.NewDecoder(bytes.NewReader(out)) + for dec.More() { + var p pkgJSON + if err := dec.Decode(&p); err != nil { + break + } + for _, f := range append(p.TestGoFiles, p.XTestGoFiles...) { + names, err := testFuncNames(filepath.Join(p.Dir, f)) + if err != nil { + continue + } + for _, name := range names { + if seen[name] { + continue + } + seen[name] = true + h := fnv.New32a() + io.WriteString(h, name) + if int(h.Sum32()%uint32(shards)) == wantShard-1 { + result = append(result, name) + } + } + } + } + return result, nil +} + +// testFuncNames scans a Go source file and returns the names of all top-level +// test functions (Test*, Benchmark*, Example*, Fuzz*). +func testFuncNames(path string) ([]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var names []string + sc := bufio.NewScanner(f) + for sc.Scan() { + rest, ok := strings.CutPrefix(sc.Text(), "func ") + if !ok { + continue + } + for _, prefix := range []string{"Test", "Benchmark", "Example", "Fuzz"} { + if strings.HasPrefix(rest, prefix) { + if i := strings.IndexByte(rest, '('); i > 0 { + names = append(names, rest[:i]) + } + break + } + } + } + return names, sc.Err() +} + // runTests runs the tests in pt and sends the results on ch. It sends a // testAttempt for each test and a final testAttempt per pkg with pkgFinished // set to true. Package build errors will not emit a testAttempt (as no valid @@ -94,8 +186,24 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te args = append(args, goTestArgs...) args = append(args, pt.Pattern) if len(pt.Tests) > 0 { + // Specific tests requested (e.g. flaky test retry). runArg := strings.Join(pt.Tests, "|") args = append(args, "--run", runArg) + } else if shardSpec := os.Getenv("TS_TEST_SHARD"); shardSpec != "" { + // Automatic test-name sharding: list tests and filter by hash. + shardTests, err := testsForShard(ctx, pt.Pattern, shardSpec) + if err != nil { + return err + } + if len(shardTests) == 0 { + ch <- &testAttempt{pkg: pt.Pattern, outcome: "skip", pkgFinished: true} + return nil + } + quoted := make([]string, len(shardTests)) + for i, name := range shardTests { + quoted[i] = regexp.QuoteMeta(name) + } + args = append(args, "--run", "^("+strings.Join(quoted, "|")+")$") } args = append(args, testArgs...) args = append(args, "-json") @@ -103,9 +211,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te fmt.Println("running", strings.Join(args, " ")) } cmd := exec.CommandContext(ctx, "go", args...) - if len(pt.Tests) > 0 { - cmd.Env = append(os.Environ(), "TS_TEST_SHARD=") // clear test shard; run all tests we say to run - } r, err := cmd.StdoutPipe() if err != nil { log.Printf("error creating stdout pipe: %v", err) @@ -113,7 +218,9 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te defer r.Close() cmd.Stderr = os.Stderr - cmd.Env = os.Environ() + cmd.Env = slices.DeleteFunc(os.Environ(), func(s string) bool { + return strings.HasPrefix(s, "TS_TEST_SHARD=") + }) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt)) if err := cmd.Start(); err != nil { diff --git a/tsnet/packet_filter_test.go b/tsnet/packet_filter_test.go index 342210f4b..69fb7bb1e 100644 --- a/tsnet/packet_filter_test.go +++ b/tsnet/packet_filter_test.go @@ -12,7 +12,6 @@ import ( "tailscale.com/ipn" "tailscale.com/tailcfg" - "tailscale.com/tstest" "tailscale.com/types/ipproto" "tailscale.com/types/key" "tailscale.com/types/netmap" @@ -45,7 +44,6 @@ func waitFor(t testing.TB, ctx context.Context, s *Server, f func(*netmap.Networ // netmaps and turning them into packet filters together. Only the control-plane // side is mocked out. func TestPacketFilterFromNetmap(t *testing.T) { - tstest.Shard(t) t.Parallel() var key key.NodePublic diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index b6677b5ec..62b83b070 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -340,7 +340,6 @@ func startServer(t *testing.T, ctx context.Context, controlURL, hostname string) } func TestDialBlocks(t *testing.T) { - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -387,7 +386,6 @@ func TestDialBlocks(t *testing.T) { // - s2 can dial through the subnet router functionality (getting a synthetic RST // that we verify we generated & saw) func TestConn(t *testing.T) { - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -520,7 +518,6 @@ func TestConn(t *testing.T) { func TestLoopbackLocalAPI(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557") - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -596,7 +593,6 @@ func TestLoopbackLocalAPI(t *testing.T) { func TestLoopbackSOCKS5(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8198") - tstest.Shard(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -647,7 +643,6 @@ func TestLoopbackSOCKS5(t *testing.T) { } func TestTailscaleIPs(t *testing.T) { - tstest.Shard(t) controlURL, _ := startControl(t) tmp := t.TempDir() @@ -690,7 +685,6 @@ func TestTailscaleIPs(t *testing.T) { // TestListenerCleanup is a regression test to verify that s.Close doesn't // deadlock if a listener is still open. func TestListenerCleanup(t *testing.T) { - tstest.Shard(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -733,7 +727,6 @@ func (wc *closeTrackConn) Close() error { // tests https://github.com/tailscale/tailscale/issues/6973 -- that we can start a tsnet server, // stop it, and restart it, even on Windows. func TestStartStopStartGetsSameIP(t *testing.T) { - tstest.Shard(t) controlURL, _ := startControl(t) tmp := t.TempDir() @@ -783,7 +776,6 @@ func TestStartStopStartGetsSameIP(t *testing.T) { } func TestFunnel(t *testing.T) { - tstest.Shard(t) ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) defer dialCancel() @@ -848,7 +840,6 @@ func TestFunnel(t *testing.T) { // after itself when closed. Specifically, changes made to the serve config // should be cleared. func TestFunnelClose(t *testing.T) { - tstest.Shard(t) marshalServeConfig := func(t *testing.T, sc ipn.ServeConfigView) string { t.Helper() @@ -1034,7 +1025,6 @@ func setUpServiceState(t *testing.T, name, ip string, host, client *Server, } func TestListenService(t *testing.T) { - tstest.Shard(t) type dialFn func(context.Context, string, string) (net.Conn, error) @@ -1430,7 +1420,6 @@ func TestListenService(t *testing.T) { } func TestListenServiceClose(t *testing.T) { - tstest.Shard(t) const serviceName = "svc:foo" diffServeConfig := func(a, b ipn.ServeConfigView) string { @@ -1586,7 +1575,6 @@ func TestListenServiceClose(t *testing.T) { } func TestListenerClose(t *testing.T) { - tstest.Shard(t) ctx := context.Background() controlURL, _ := startControl(t) @@ -1666,7 +1654,6 @@ func (c *bufferedConn) Read(b []byte) (int, error) { } func TestFallbackTCPHandler(t *testing.T) { - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -1709,7 +1696,6 @@ func TestFallbackTCPHandler(t *testing.T) { } func TestCapturePcap(t *testing.T) { - tstest.Shard(t) const timeLimit = 120 ctx, cancel := context.WithTimeout(context.Background(), timeLimit*time.Second) defer cancel() @@ -1763,7 +1749,6 @@ func TestCapturePcap(t *testing.T) { } func TestUDPConn(t *testing.T) { - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -1955,7 +1940,6 @@ func sendData(logf func(format string, args ...any), ctx context.Context, bytesC } func TestUserMetricsByteCounters(t *testing.T) { - tstest.Shard(t) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() @@ -2070,7 +2054,6 @@ func TestUserMetricsByteCounters(t *testing.T) { } func TestUserMetricsRouteGauges(t *testing.T) { - tstest.Shard(t) // Windows does not seem to support or report back routes when running in // userspace via tsnet. So, we skip this check on Windows. // TODO(kradalby): Figure out if this is correct. @@ -2306,7 +2289,6 @@ type listenTest struct { // If useTUN is true, s2 uses a chanTUN; otherwise it uses netstack only. func setupTwoClientTest(t *testing.T, useTUN bool) *listenTest { t.Helper() - tstest.Shard(t) tstest.ResourceCheck(t) ctx := t.Context() controlURL, control := startControl(t) @@ -2904,7 +2886,6 @@ func buildDNSQuery(name string, srcIP netip.Addr) []byte { } func TestDeps(t *testing.T) { - tstest.Shard(t) deptest.DepChecker{ GOOS: "linux", GOARCH: "amd64", @@ -3168,7 +3149,6 @@ func TestResolveAuthKey(t *testing.T) { // packets were sent to WireGuard (which has no peer for the node's own IP) // and silently dropped, causing Dial to hang indefinitely. func TestSelfDial(t *testing.T) { - tstest.Shard(t) tstest.ResourceCheck(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/tstest/integration/capmap_test.go b/tstest/integration/capmap_test.go index aea4a210b..48e9dae49 100644 --- a/tstest/integration/capmap_test.go +++ b/tstest/integration/capmap_test.go @@ -14,7 +14,6 @@ import ( // TestPeerCapMap tests that the node capability map (CapMap) is included in peer information. func TestPeerCapMap(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -97,7 +96,6 @@ func TestPeerCapMap(t *testing.T) { // TestSetNodeCapMap tests that SetNodeCapMap updates are propagated to peers. func TestSetNodeCapMap(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index 6c1dfcecb..cabb50cc7 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -79,7 +79,6 @@ func fetchNetMapForTest(ctx context.Context, lc *local.Client) (*netmap.NetworkM // Tests that tailscaled starts up in TUN mode, and also without data races: // https://github.com/tailscale/tailscale/issues/7894 func TestTUNMode(t *testing.T) { - tstest.Shard(t) tstest.RequireRoot(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -97,7 +96,6 @@ func TestTUNMode(t *testing.T) { } func TestOneNodeUpNoAuth(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -115,7 +113,6 @@ func TestOneNodeUpNoAuth(t *testing.T) { } func TestOneNodeExpiredKey(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -152,7 +149,6 @@ func TestOneNodeExpiredKey(t *testing.T) { } func TestControlKnobs(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -183,7 +179,6 @@ func TestControlKnobs(t *testing.T) { } func TestExpectedFeaturesLinked(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -205,7 +200,6 @@ func TestExpectedFeaturesLinked(t *testing.T) { } func TestCollectPanic(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n := NewTestNode(t, env) @@ -248,7 +242,6 @@ func TestCollectPanic(t *testing.T) { } func TestControlTimeLogLine(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) env.LogCatcher.StoreRawJSON() @@ -272,7 +265,6 @@ func TestControlTimeLogLine(t *testing.T) { // test Issue 2321: Start with UpdatePrefs should save prefs to disk func TestStateSavedOnStart(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -473,7 +465,6 @@ func TestOneNodeUpAuth(t *testing.T) { }, }, } { - tstest.Shard(t) t.Run(tt.name, func(t *testing.T) { tstest.Parallel(t) @@ -559,7 +550,6 @@ func isNonZeroExitCode(err error) bool { // If we interrupt `tailscale up` and then run it again, we should only // print a single auth URL. func TestOneNodeUpInterruptedAuth(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t, ConfigureControl( @@ -638,7 +628,6 @@ func TestOneNodeUpInterruptedAuth(t *testing.T) { // complete the device approval, we should see the device approval URL // when we run `tailscale up` a second time. func TestOneNodeUpInterruptedDeviceApproval(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t, ConfigureControl( @@ -718,8 +707,6 @@ func TestOneNodeUpInterruptedDeviceApproval(t *testing.T) { } func TestConfigFileAuthKey(t *testing.T) { - tstest.SkipOnUnshardedCI(t) - tstest.Shard(t) t.Parallel() const authKey = "opensesame" env := NewTestEnv(t, ConfigureControl(func(control *testcontrol.Server) { @@ -745,7 +732,6 @@ func TestConfigFileAuthKey(t *testing.T) { } func TestTwoNodes(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -831,7 +817,6 @@ func TestTwoNodes(t *testing.T) { // tests two nodes where the first gets a incremental MapResponse (with only // PeersRemoved set) saying that the second node disappeared. func TestIncrementalMapUpdatePeersRemoved(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -919,7 +904,6 @@ func TestIncrementalMapUpdatePeersRemoved(t *testing.T) { // This covers VIP additions at runtime, where the VIP route is not reachable // before the map mutation but is reachable over TSMP afterward. func TestIncrementalMapUpdatePeerAllowedIPsReachability(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -1005,7 +989,6 @@ func TestIncrementalMapUpdatePeerAllowedIPsReachability(t *testing.T) { } func TestNodeAddressIPFields(t *testing.T) { - tstest.Shard(t) flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/7008") tstest.Parallel(t) env := NewTestEnv(t) @@ -1033,7 +1016,6 @@ func TestNodeAddressIPFields(t *testing.T) { } func TestAddPingRequest(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -1086,7 +1068,6 @@ func TestAddPingRequest(t *testing.T) { } func TestC2NPingRequest(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) @@ -1148,7 +1129,6 @@ func TestC2NPingRequest(t *testing.T) { // Issue 2434: when "down" (WantRunning false), tailscaled shouldn't // be connected to control. func TestNoControlConnWhenDown(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -1197,7 +1177,6 @@ func TestNoControlConnWhenDown(t *testing.T) { // Issue 2137: make sure Windows tailscaled works with the CLI alone, // without the GUI to kick off a Start. func TestOneNodeUpWindowsStyle(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) @@ -1218,7 +1197,6 @@ func TestOneNodeUpWindowsStyle(t *testing.T) { // node can initiate connections to the jailed node. func TestClientSideJailing(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/17419") - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) registerNode := func() (*TestNode, key.NodePublic) { @@ -1331,7 +1309,6 @@ func TestClientSideJailing(t *testing.T) { // tries to do bi-directional pings between them. func TestNATPing(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/12169") - tstest.Shard(t) tstest.Parallel(t) for _, v6 := range []bool{false, true} { env := NewTestEnv(t) @@ -1459,7 +1436,6 @@ func TestNATPing(t *testing.T) { } func TestLogoutRemovesAllPeers(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) // Spin up some nodes. @@ -1521,7 +1497,6 @@ func TestAutoUpdateDefaults_cap(t *testing.T) { testAutoUpdateDefaults(t, true) func testAutoUpdateDefaults(t *testing.T, useCap bool) { t.Cleanup(feature.HookCanAutoUpdate.SetForTest(func() bool { return true })) - tstest.Shard(t) env := NewTestEnv(t) var ( @@ -1654,7 +1629,6 @@ func testAutoUpdateDefaults(t *testing.T, useCap bool) { // gVisor/netstack. // https://github.com/tailscale/corp/issues/22511 func TestDNSOverTCPIntervalResolver(t *testing.T) { - tstest.Shard(t) tstest.RequireRoot(t) env := NewTestEnv(t) env.tunMode = true @@ -1724,7 +1698,6 @@ func TestDNSOverTCPIntervalResolver(t *testing.T) { // TestNetstackTCPLoopback tests netstack loopback of a TCP stream, in both // directions. func TestNetstackTCPLoopback(t *testing.T) { - tstest.Shard(t) tstest.RequireRoot(t) env := NewTestEnv(t) @@ -1864,7 +1837,6 @@ func TestNetstackTCPLoopback(t *testing.T) { // TestNetstackUDPLoopback tests netstack loopback of UDP packets, in both // directions. func TestNetstackUDPLoopback(t *testing.T) { - tstest.Shard(t) tstest.RequireRoot(t) env := NewTestEnv(t) @@ -2013,7 +1985,6 @@ func TestEncryptStateMigration(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "windows" { t.Skip("--encrypt-state for tailscaled state not supported on this platform") } - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n := NewTestNode(t, env) @@ -2069,7 +2040,6 @@ func TestEncryptStateMigration(t *testing.T) { // expected values. func TestPeerRelayPing(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/17251") - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t, ConfigureControl(func(server *testcontrol.Server) { @@ -2209,7 +2179,6 @@ func TestPeerRelayPing(t *testing.T) { } func TestC2NDebugNetmap(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t, ConfigureControl(func(s *testcontrol.Server) { s.CollectServices = opt.False @@ -2352,7 +2321,6 @@ func TestTailnetLock(t *testing.T) { // If you run `tailscale lock log` on a node where Tailnet Lock isn't // enabled, you get an error explaining that. t.Run("log-when-not-enabled", func(t *testing.T) { - tstest.Shard(t) t.Parallel() env := NewTestEnv(t) @@ -2390,7 +2358,6 @@ func TestTailnetLock(t *testing.T) { // the signed nodes can talk to each other but the unsigned node cannot // talk to anybody. t.Run("node-connectivity", func(t *testing.T) { - tstest.Shard(t) t.Parallel() env := NewTestEnv(t) @@ -2466,7 +2433,6 @@ func TestTailnetLock(t *testing.T) { t.Run("no-keys-is-error", func(t *testing.T) { for _, verb := range []string{"add", "remove", "revoke-keys"} { t.Run(verb, func(t *testing.T) { - tstest.Shard(t) t.Parallel() env := NewTestEnv(t) @@ -2493,7 +2459,6 @@ func TestTailnetLock(t *testing.T) { } func TestNodeWithBadStateFile(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) n1 := NewTestNode(t, env) diff --git a/tstest/integration/whois_test.go b/tstest/integration/whois_test.go index b4e99a547..258a22987 100644 --- a/tstest/integration/whois_test.go +++ b/tstest/integration/whois_test.go @@ -21,7 +21,6 @@ import ( // netstack forwards the connection to localhost, and the listener // calls WhoIs on n2's LocalAPI to identify the remote peer as n1. func TestUserspaceWhoIsProxyMap(t *testing.T) { - tstest.Shard(t) tstest.Parallel(t) env := NewTestEnv(t) diff --git a/tstest/largetailnet/delta_test.go b/tstest/largetailnet/delta_test.go index cb9958c93..056394af2 100644 --- a/tstest/largetailnet/delta_test.go +++ b/tstest/largetailnet/delta_test.go @@ -57,7 +57,6 @@ func metricByName(t testing.TB, name string) *clientmetric.Metric { // [tstest/largetailnet/BenchmarkGiantTailnet], which only measures cost // of the same fast path — this test verifies correctness. func TestNetmapDeltaFastPath(t *testing.T) { - tstest.Shard(t) logf := logger.Discard if testing.Verbose() { @@ -105,7 +104,7 @@ func TestNetmapDeltaFastPath(t *testing.T) { // Snapshot baseline metric values; we'll assert deltas against // these. Globals make per-test isolation impossible, but deltas // are robust against interleaving (assuming no other test runs in - // parallel here, hence tstest.Shard above). + // parallel here). mFast := metricByName(t, "controlclient_map_response_handled_incrementally") mFull := metricByName(t, "controlclient_map_response_handled_full_rebuild") mUpsert := metricByName(t, "localbackend_netmap_delta_peer_upserted") diff --git a/tstest/tstest.go b/tstest/tstest.go index 7e25ce8a0..676d56f8d 100644 --- a/tstest/tstest.go +++ b/tstest/tstest.go @@ -8,16 +8,12 @@ import ( "context" "fmt" "os" - "strconv" - "strings" - "sync/atomic" "testing" "time" "tailscale.com/envknob" "tailscale.com/types/logger" "tailscale.com/util/backoff" - "tailscale.com/util/cibuild" ) // AssertNotParallel asserts that t has not been marked as parallel. @@ -48,7 +44,6 @@ func Replace[T any](t testing.TB, target *T, val T) { }) *target = val - return } // WaitFor retries try for up to maxWait. @@ -68,38 +63,6 @@ func WaitFor(maxWait time.Duration, try func() error) error { return err } -var testNum atomic.Int32 - -// Shard skips t if it's not running if the TS_TEST_SHARD test shard is set to -// "n/m" and this test execution number in the process mod m is not equal to n-1. -// That is, to run with 4 shards, set TS_TEST_SHARD=1/4, ..., TS_TEST_SHARD=4/4 -// for the four jobs. -func Shard(t testing.TB) { - e := os.Getenv("TS_TEST_SHARD") - a, b, ok := strings.Cut(e, "/") - if !ok { - return - } - wantShard, _ := strconv.ParseInt(a, 10, 32) - shards, _ := strconv.ParseInt(b, 10, 32) - if wantShard == 0 || shards == 0 { - return - } - - shard := ((testNum.Add(1) - 1) % int32(shards)) + 1 - if shard != int32(wantShard) { - t.Skipf("skipping shard %d/%d (process has TS_TEST_SHARD=%q)", shard, shards, e) - } -} - -// SkipOnUnshardedCI skips t if we're in CI and the TS_TEST_SHARD -// environment variable isn't set. -func SkipOnUnshardedCI(t testing.TB) { - if cibuild.On() && os.Getenv("TS_TEST_SHARD") == "" { - t.Skip("skipping on CI without TS_TEST_SHARD") - } -} - var serializeParallel = envknob.RegisterBool("TS_SERIAL_TESTS") // Parallel calls t.Parallel, unless TS_SERIAL_TESTS is set true.