"io"
"math/rand"
"os"
+ "path/filepath"
"reflect"
"runtime"
"runtime/debug"
Logf(format string, args ...any)
Name() string
Setenv(key, value string)
+ Chdir(dir string)
Skip(args ...any)
SkipNow()
Skipf(format string, args ...any)
// 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() {}
}
}
+// 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
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
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 {
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 {
"os/exec"
"path/filepath"
"regexp"
+ "runtime"
"slices"
"strings"
"sync"
}
}
-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