diff --git a/api/next/62516.txt b/api/next/62516.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a09b950490c971f6cf73d35a04308cb2099eddd --- /dev/null +++ b/api/next/62516.txt @@ -0,0 +1,4 @@ +pkg testing, method (*B) Chdir(string) #62516 +pkg testing, method (*F) Chdir(string) #62516 +pkg testing, method (*T) Chdir(string) #62516 +pkg testing, type TB interface, Chdir(string) #62516 diff --git a/doc/next/6-stdlib/99-minor/testing/62516.md b/doc/next/6-stdlib/99-minor/testing/62516.md new file mode 100644 index 0000000000000000000000000000000000000000..a7a90cdbcd407a95a561681bdf142083394f61ae --- /dev/null +++ b/doc/next/6-stdlib/99-minor/testing/62516.md @@ -0,0 +1,2 @@ +The new [T.Chdir] and [B.Chdir] methods can be used to change the working +directory for the duration of a test or benchmark. diff --git a/src/testing/export_test.go b/src/testing/export_test.go index 10a5b04aee38e295f614314750fc94f13d8bf48b..a2dddc79b68f6562cd0f52b4ca2296e69e64fb49 100644 --- a/src/testing/export_test.go +++ b/src/testing/export_test.go @@ -9,3 +9,5 @@ var PrettyPrint = prettyPrint type HighPrecisionTime = highPrecisionTime var HighPrecisionTimeNow = highPrecisionTimeNow + +const ParallelConflict = parallelConflict diff --git a/src/testing/testing.go b/src/testing/testing.go index 526cba39f8fc92563f425c0cdf92d147563d9322..49d14f5f66eda87d3c95d6e7aba084f3ca4571d2 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -379,6 +379,7 @@ import ( "io" "math/rand" "os" + "path/filepath" "reflect" "runtime" "runtime/debug" @@ -891,6 +892,7 @@ type TB interface { Logf(format string, args ...any) Name() string Setenv(key, value string) + Chdir(dir string) Skip(args ...any) SkipNow() Skipf(format string, args ...any) @@ -917,8 +919,8 @@ var _ TB = (*B)(nil) // may be called simultaneously from multiple goroutines. type T struct { common - isEnvSet bool - context *testContext // For running tests and subtests. + denyParallel bool + context *testContext // For running tests and subtests. } func (c *common) private() {} @@ -1307,6 +1309,48 @@ func (c *common) Setenv(key, value string) { } } +// Chdir calls os.Chdir(dir) and uses Cleanup to restore the current +// working directory to its original value after the test. On Unix, it +// also sets PWD environment variable for the duration of the test. +// +// Because Chdir affects the whole process, it cannot be used +// in parallel tests or tests with parallel ancestors. +func (c *common) Chdir(dir string) { + c.checkFuzzFn("Chdir") + oldwd, err := os.Open(".") + if err != nil { + c.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + c.Fatal(err) + } + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory, we should also set or update PWD to reflect that. + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable. + default: + if !filepath.IsAbs(dir) { + dir, err = os.Getwd() + if err != nil { + c.Fatal(err) + } + } + c.Setenv("PWD", dir) + } + c.Cleanup(func() { + err := oldwd.Chdir() + oldwd.Close() + if err != nil { + // It's not safe to continue with tests if we can't + // get back to the original working directory. Since + // we are holding a dirfd, this is highly unlikely. + panic("testing.Chdir: " + err.Error()) + } + }) +} + // panicHandling controls the panic handling used by runCleanup. type panicHandling int @@ -1436,6 +1480,8 @@ func pcToName(pc uintptr) string { return frame.Function } +const parallelConflict = `testing: test using t.Setenv or t.Chdir can not use t.Parallel` + // Parallel signals that this test is to be run in parallel with (and only with) // other parallel tests. When a test is run multiple times due to use of // -test.count or -test.cpu, multiple instances of a single test never run in @@ -1444,8 +1490,8 @@ func (t *T) Parallel() { if t.isParallel { panic("testing: t.Parallel called multiple times") } - if t.isEnvSet { - panic("testing: t.Parallel called after t.Setenv; cannot set environment variables in parallel tests") + if t.denyParallel { + panic(parallelConflict) } t.isParallel = true if t.parent.barrier == nil { @@ -1500,34 +1546,43 @@ func (t *T) Parallel() { t.lastRaceErrors.Store(int64(race.Errors())) } -// Setenv calls os.Setenv(key, value) and uses Cleanup to -// restore the environment variable to its original value -// after the test. -// -// Because Setenv affects the whole process, it cannot be used -// in parallel tests or tests with parallel ancestors. -func (t *T) Setenv(key, value string) { +func (t *T) checkParallel() { // Non-parallel subtests that have parallel ancestors may still // run in parallel with other tests: they are only non-parallel // with respect to the other subtests of the same parent. - // Since SetEnv affects the whole process, we need to disallow it - // if the current test or any parent is parallel. - isParallel := false + // Since calls like SetEnv or Chdir affects the whole process, we need + // to deny those if the current test or any parent is parallel. for c := &t.common; c != nil; c = c.parent { if c.isParallel { - isParallel = true - break + panic(parallelConflict) } } - if isParallel { - panic("testing: t.Setenv called after t.Parallel; cannot set environment variables in parallel tests") - } - t.isEnvSet = true + t.denyParallel = true +} +// Setenv calls os.Setenv(key, value) and uses Cleanup to +// restore the environment variable to its original value +// after the test. +// +// Because Setenv affects the whole process, it cannot be used +// in parallel tests or tests with parallel ancestors. +func (t *T) Setenv(key, value string) { + t.checkParallel() t.common.Setenv(key, value) } +// Chdir calls os.Chdir(dir) and uses Cleanup to restore the current +// working directory to its original value after the test. On Unix, it +// also sets PWD environment variable for the duration of the test. +// +// Because Chdir affects the whole process, it cannot be used +// in parallel tests or tests with parallel ancestors. +func (t *T) Chdir(dir string) { + t.checkParallel() + t.common.Chdir(dir) +} + // InternalTest is an internal type but exported because it is cross-package; // it is part of the implementation of the "go test" command. type InternalTest struct { diff --git a/src/testing/testing_test.go b/src/testing/testing_test.go index 4a9303952e79caadee427e6e36d589afb74db885..af6035fd2797aa50d29ba0381fd340ff67edd38a 100644 --- a/src/testing/testing_test.go +++ b/src/testing/testing_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "slices" "strings" "sync" @@ -200,64 +201,168 @@ func TestSetenv(t *testing.T) { } } -func TestSetenvWithParallelAfterSetenv(t *testing.T) { - defer func() { - want := "testing: t.Parallel called after t.Setenv; cannot set environment variables in parallel tests" - if got := recover(); got != want { - t.Fatalf("expected panic; got %#v want %q", got, want) - } - }() +func expectParallelConflict(t *testing.T) { + want := testing.ParallelConflict + if got := recover(); got != want { + t.Fatalf("expected panic; got %#v want %q", got, want) + } +} - t.Setenv("GO_TEST_KEY_1", "value") +func testWithParallelAfter(t *testing.T, fn func(*testing.T)) { + defer expectParallelConflict(t) + fn(t) t.Parallel() } -func TestSetenvWithParallelBeforeSetenv(t *testing.T) { - defer func() { - want := "testing: t.Setenv called after t.Parallel; cannot set environment variables in parallel tests" - if got := recover(); got != want { - t.Fatalf("expected panic; got %#v want %q", got, want) - } - }() +func testWithParallelBefore(t *testing.T, fn func(*testing.T)) { + defer expectParallelConflict(t) t.Parallel() - - t.Setenv("GO_TEST_KEY_1", "value") + fn(t) } -func TestSetenvWithParallelParentBeforeSetenv(t *testing.T) { +func testWithParallelParentBefore(t *testing.T, fn func(*testing.T)) { t.Parallel() t.Run("child", func(t *testing.T) { - defer func() { - want := "testing: t.Setenv called after t.Parallel; cannot set environment variables in parallel tests" - if got := recover(); got != want { - t.Fatalf("expected panic; got %#v want %q", got, want) - } - }() + defer expectParallelConflict(t) - t.Setenv("GO_TEST_KEY_1", "value") + fn(t) }) } -func TestSetenvWithParallelGrandParentBeforeSetenv(t *testing.T) { +func testWithParallelGrandParentBefore(t *testing.T, fn func(*testing.T)) { t.Parallel() t.Run("child", func(t *testing.T) { t.Run("grand-child", func(t *testing.T) { - defer func() { - want := "testing: t.Setenv called after t.Parallel; cannot set environment variables in parallel tests" - if got := recover(); got != want { - t.Fatalf("expected panic; got %#v want %q", got, want) - } - }() + defer expectParallelConflict(t) - t.Setenv("GO_TEST_KEY_1", "value") + fn(t) }) }) } +func tSetenv(t *testing.T) { + t.Setenv("GO_TEST_KEY_1", "value") +} + +func TestSetenvWithParallelAfter(t *testing.T) { + testWithParallelAfter(t, tSetenv) +} + +func TestSetenvWithParallelBefore(t *testing.T) { + testWithParallelBefore(t, tSetenv) +} + +func TestSetenvWithParallelParentBefore(t *testing.T) { + testWithParallelParentBefore(t, tSetenv) +} + +func TestSetenvWithParallelGrandParentBefore(t *testing.T) { + testWithParallelGrandParentBefore(t, tSetenv) +} + +func tChdir(t *testing.T) { + t.Chdir(t.TempDir()) +} + +func TestChdirWithParallelAfter(t *testing.T) { + testWithParallelAfter(t, tChdir) +} + +func TestChdirWithParallelBefore(t *testing.T) { + testWithParallelBefore(t, tChdir) +} + +func TestChdirWithParallelParentBefore(t *testing.T) { + testWithParallelParentBefore(t, tChdir) +} + +func TestChdirWithParallelGrandParentBefore(t *testing.T) { + testWithParallelGrandParentBefore(t, tChdir) +} + +func TestChdir(t *testing.T) { + oldDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(oldDir) + + tmp := t.TempDir() + rel, err := filepath.Rel(oldDir, tmp) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name, dir, pwd string + extraChdir bool + }{ + { + name: "absolute", + dir: tmp, + pwd: tmp, + }, + { + name: "relative", + dir: rel, + pwd: tmp, + }, + { + name: "current (absolute)", + dir: oldDir, + pwd: oldDir, + }, + { + name: "current (relative) with extra os.Chdir", + dir: ".", + pwd: oldDir, + + extraChdir: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if !filepath.IsAbs(tc.pwd) { + t.Fatalf("Bad tc.pwd: %q (must be absolute)", tc.pwd) + } + + t.Chdir(tc.dir) + + newDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if newDir != tc.pwd { + t.Fatalf("failed to chdir to %q: getwd: got %q, want %q", tc.dir, newDir, tc.pwd) + } + + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable. + default: + if pwd := os.Getenv("PWD"); pwd != tc.pwd { + t.Fatalf("PWD: got %q, want %q", pwd, tc.pwd) + } + } + + if tc.extraChdir { + os.Chdir("..") + } + }) + + newDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if newDir != oldDir { + t.Fatalf("failed to restore wd to %s: getwd: %s", oldDir, newDir) + } + } +} + // testingTrueInInit is part of TestTesting. var testingTrueInInit = false