diff --git a/tstest/allocs.go b/tstest/allocs.go index 6c2a1a22b..fdbc31b31 100644 --- a/tstest/allocs.go +++ b/tstest/allocs.go @@ -6,8 +6,9 @@ import ( "fmt" "runtime" - "testing" "time" + + "tailscale.com/util/testenv" ) // MinAllocsPerRun asserts that f can run with no more than target allocations. @@ -16,7 +17,7 @@ // // MinAllocsPerRun sets GOMAXPROCS to 1 during its measurement and restores // it before returning. -func MinAllocsPerRun(t *testing.T, target uint64, f func()) error { +func MinAllocsPerRun(t testenv.TB, target uint64, f func()) error { defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) var memstats runtime.MemStats diff --git a/tstest/log.go b/tstest/log.go index 73e973d23..3b271a166 100644 --- a/tstest/log.go +++ b/tstest/log.go @@ -9,7 +9,6 @@ "log" "os" "sync" - "testing" "go4.org/mem" "tailscale.com/types/logger" @@ -17,7 +16,7 @@ ) type testLogWriter struct { - t *testing.T + t testenv.TB } func (w *testLogWriter) Write(b []byte) (int, error) { @@ -26,12 +25,12 @@ func (w *testLogWriter) Write(b []byte) (int, error) { return len(b), nil } -func FixLogs(t *testing.T) { +func FixLogs(t testenv.TB) { log.SetFlags(log.Ltime | log.Lshortfile) log.SetOutput(&testLogWriter{t}) } -func UnfixLogs(t *testing.T) { +func UnfixLogs(t testenv.TB) { defer log.SetOutput(os.Stderr) } diff --git a/tstest/reflect.go b/tstest/reflect.go index 4ba1f96c3..19142af58 100644 --- a/tstest/reflect.go +++ b/tstest/reflect.go @@ -6,8 +6,9 @@ import ( "net/netip" "reflect" - "testing" "time" + + "tailscale.com/util/testenv" ) // IsZeroable is the interface for things with an IsZero method. @@ -30,7 +31,7 @@ type IsZeroable interface { // The nonzeroValues map should contain non-zero values for each type that // exists in the type T or any contained types. Basic types like string, bool, // and numeric types are handled automatically. -func CheckIsZero[T IsZeroable](t testing.TB, nonzeroValues map[reflect.Type]any) { +func CheckIsZero[T IsZeroable](t testenv.TB, nonzeroValues map[reflect.Type]any) { t.Helper() var zero T diff --git a/tstest/resource.go b/tstest/resource.go index 867925b7d..72397c2d0 100644 --- a/tstest/resource.go +++ b/tstest/resource.go @@ -9,8 +9,9 @@ "runtime/pprof" "slices" "strings" - "testing" "time" + + "tailscale.com/util/testenv" ) // ResourceCheck takes a snapshot of the current goroutines and registers a @@ -19,7 +20,7 @@ // can look at specific routines). // // It panics if called from a parallel test. -func ResourceCheck(tb testing.TB) { +func ResourceCheck(tb testenv.TB) { tb.Helper() // Set an environment variable (anything at all) just for the @@ -48,7 +49,7 @@ func ResourceCheck(tb testing.TB) { // Parse and print goroutines. start := parseGoroutines(startStacks) end := parseGoroutines(endStacks) - if testing.Verbose() { + if testenv.Verbose() { tb.Logf("goroutines start:\n%s", printGoroutines(start)) tb.Logf("goroutines end:\n%s", printGoroutines(end)) } diff --git a/tstest/tstest.go b/tstest/tstest.go index 676d56f8d..10316e4a5 100644 --- a/tstest/tstest.go +++ b/tstest/tstest.go @@ -2,18 +2,23 @@ // SPDX-License-Identifier: BSD-3-Clause // Package tstest provides utilities for use in unit tests. +// +// It intentionally does not depend on the "testing" package (using +// [testenv.TB] instead of testing.TB) so that importing it from +// non-test code doesn't link the testing package and its flag +// registration side effects into the binary. package tstest import ( "context" "fmt" "os" - "testing" "time" "tailscale.com/envknob" "tailscale.com/types/logger" "tailscale.com/util/backoff" + "tailscale.com/util/testenv" ) // AssertNotParallel asserts that t has not been marked as parallel. @@ -21,7 +26,7 @@ // // Use this when a test modifies package-level globals or other shared // state that would be unsafe to modify concurrently with other tests. -func AssertNotParallel(t testing.TB) { +func AssertNotParallel(t testenv.TB) { t.Helper() t.Setenv("ASSERT_NOT_PARALLEL_TEST", "1") // panics if t.Parallel was called } @@ -32,7 +37,7 @@ func AssertNotParallel(t testing.TB) { // When target is a package-level variable, the caller should also call // [AssertNotParallel] to ensure the test is not running in parallel with // other tests that may access the same variable. -func Replace[T any](t testing.TB, target *T, val T) { +func Replace[T any](t testenv.TB, target *T, val T) { t.Helper() if target == nil { t.Fatalf("Replace: nil pointer") @@ -66,14 +71,14 @@ func WaitFor(maxWait time.Duration, try func() error) error { var serializeParallel = envknob.RegisterBool("TS_SERIAL_TESTS") // Parallel calls t.Parallel, unless TS_SERIAL_TESTS is set true. -func Parallel(t *testing.T) { +func Parallel(t interface{ Parallel() }) { if !serializeParallel() { t.Parallel() } } // RequireRoot skips the test if the current user is not root. -func RequireRoot(tb testing.TB) { +func RequireRoot(tb testenv.TB) { tb.Helper() if os.Getuid() != 0 { tb.Skip("skipping test; requires root") @@ -82,7 +87,7 @@ func RequireRoot(tb testing.TB) { // SkipOnKernelVersions skips the test if the current // kernel version is in the specified list. -func SkipOnKernelVersions(t testing.TB, issue string, versions ...string) { +func SkipOnKernelVersions(t testenv.TB, issue string, versions ...string) { major, minor, patch := KernelVersion() if major == 0 && minor == 0 && patch == 0 { t.Logf("could not determine kernel version") diff --git a/tstest/tstest_test.go b/tstest/tstest_test.go index 0c281d235..e0e7b2e54 100644 --- a/tstest/tstest_test.go +++ b/tstest/tstest_test.go @@ -6,8 +6,18 @@ import ( "runtime" "testing" + + "tailscale.com/tstest/deptest" ) +func TestDeps(t *testing.T) { + deptest.DepChecker{ + BadDeps: map[string]string{ + "testing": "do not link the testing package and its flag side effects into non-test binaries; use testenv.TB", + }, + }.Check(t) +} + func TestReplace(t *testing.T) { before := "before" done := false diff --git a/util/testenv/testenv.go b/util/testenv/testenv.go index c6792bb4f..d234b7d62 100644 --- a/util/testenv/testenv.go +++ b/util/testenv/testenv.go @@ -22,6 +22,19 @@ func InTest() bool { }) } +// Verbose reports whether the test binary is running with verbose +// output (the -test.v flag), like testing.Verbose but without +// importing the testing package. +func Verbose() bool { + f := flag.Lookup("test.v") + if f == nil { + return false + } + // The flag's String value is "false" when off, and "true" or + // "test2json" when on. + return f.Value.String() != "false" +} + // TB is testing.TB, to avoid importing "testing" in non-test code. type TB interface { ArtifactDir() string