cmd/go, testing: add TB.ArtifactDir and -artifacts flag

Add TB.ArtifactDir, which returns a directory for a test to store
output files in. Add a -artifacts testflag which enables persistent
storage of artifacts in the output directory (-outputdir, or the
current directory by default).

Fixes #71287

Change-Id: I5f6515a6cd6c103f88588f4c033d5ea11ffd0c3c
Reviewed-on: https://go-review.googlesource.com/c/go/+/696399
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Damien Neil 2025-08-15 15:24:05 -07:00
parent 1623927730
commit bb1ca7ae81
11 changed files with 333 additions and 71 deletions

4
api/next/71287.txt Normal file
View file

@ -0,0 +1,4 @@
pkg testing, method (*B) ArtifactDir() string #71287
pkg testing, method (*F) ArtifactDir() string #71287
pkg testing, method (*T) ArtifactDir() string #71287
pkg testing, type TB interface, ArtifactDir() string #71287

View file

@ -0,0 +1,18 @@
The new methods [T.ArtifactDir], [B.ArtifactDir], and [F.ArtifactDir]
return a directory in which to write test output files (artifacts).
When the `-artifacts` flag is provided to `go test`,
this directory will be located under the output directory
(specified with `-outputdir`, or the current directory by default).
Otherwise, artifacts are stored in a temporary directory
which is removed after the test completes.
The first call to `ArtifactDir` when `-artifacts` is provided
writes the location of the directory to the test log.
For example, in a test named `TestArtifacts`,
`t.ArtifactDir()` emits:
```
=== ARTIFACTS Test /path/to/artifact/dir
```

View file

@ -3244,6 +3244,10 @@
// The following flags are recognized by the 'go test' command and // The following flags are recognized by the 'go test' command and
// control the execution of any test: // control the execution of any test:
// //
// -artifacts
// Save test artifacts in the directory specified by -outputdir.
// See 'go doc testing.T.ArtifactDir'.
//
// -bench regexp // -bench regexp
// Run only those benchmarks matching a regular expression. // Run only those benchmarks matching a regular expression.
// By default, no benchmarks are run. // By default, no benchmarks are run.
@ -3338,6 +3342,10 @@
// This will only list top-level tests. No subtest or subbenchmarks will be // This will only list top-level tests. No subtest or subbenchmarks will be
// shown. // shown.
// //
// -outputdir directory
// Place output files from profiling and test artifacts in the
// specified directory, by default the directory in which "go test" is running.
//
// -parallel n // -parallel n
// Allow parallel execution of test functions that call t.Parallel, and // Allow parallel execution of test functions that call t.Parallel, and
// fuzz targets that call t.Parallel when running the seed corpus. // fuzz targets that call t.Parallel when running the seed corpus.
@ -3449,10 +3457,6 @@
// Sample 1 in n stack traces of goroutines holding a // Sample 1 in n stack traces of goroutines holding a
// contended mutex. // contended mutex.
// //
// -outputdir directory
// Place output files from profiling in the specified directory,
// by default the directory in which "go test" is running.
//
// -trace trace.out // -trace trace.out
// Write an execution trace to the specified file before exiting. // Write an execution trace to the specified file before exiting.
// //

View file

@ -649,6 +649,14 @@ func (t *testFuncs) ImportPath() string {
return pkg return pkg
} }
func (t *testFuncs) ModulePath() string {
m := t.Package.Module
if m == nil {
return ""
}
return m.Path
}
// Covered returns a string describing which packages are being tested for coverage. // Covered returns a string describing which packages are being tested for coverage.
// If the covered package is the same as the tested package, it returns the empty string. // If the covered package is the same as the tested package, it returns the empty string.
// Otherwise it is a comma-separated human-readable list of packages beginning with // Otherwise it is a comma-separated human-readable list of packages beginning with
@ -836,6 +844,7 @@ func init() {
testdeps.CoverMarkProfileEmittedFunc = cfile.MarkProfileEmitted testdeps.CoverMarkProfileEmittedFunc = cfile.MarkProfileEmitted
{{end}} {{end}}
testdeps.ModulePath = {{.ModulePath | printf "%q"}}
testdeps.ImportPath = {{.ImportPath | printf "%q"}} testdeps.ImportPath = {{.ImportPath | printf "%q"}}
} }

View file

@ -9,6 +9,7 @@ package test
// passFlagToTest contains the flags that should be forwarded to // passFlagToTest contains the flags that should be forwarded to
// the test binary with the prefix "test.". // the test binary with the prefix "test.".
var passFlagToTest = map[string]bool{ var passFlagToTest = map[string]bool{
"artifacts": true,
"bench": true, "bench": true,
"benchmem": true, "benchmem": true,
"benchtime": true, "benchtime": true,

View file

@ -192,6 +192,10 @@ and -show_bytes options of pprof control how the information is presented.
The following flags are recognized by the 'go test' command and The following flags are recognized by the 'go test' command and
control the execution of any test: control the execution of any test:
-artifacts
Save test artifacts in the directory specified by -outputdir.
See 'go doc testing.T.ArtifactDir'.
-bench regexp -bench regexp
Run only those benchmarks matching a regular expression. Run only those benchmarks matching a regular expression.
By default, no benchmarks are run. By default, no benchmarks are run.
@ -286,6 +290,10 @@ control the execution of any test:
This will only list top-level tests. No subtest or subbenchmarks will be This will only list top-level tests. No subtest or subbenchmarks will be
shown. shown.
-outputdir directory
Place output files from profiling and test artifacts in the
specified directory, by default the directory in which "go test" is running.
-parallel n -parallel n
Allow parallel execution of test functions that call t.Parallel, and Allow parallel execution of test functions that call t.Parallel, and
fuzz targets that call t.Parallel when running the seed corpus. fuzz targets that call t.Parallel when running the seed corpus.
@ -397,10 +405,6 @@ profile the tests during execution:
Sample 1 in n stack traces of goroutines holding a Sample 1 in n stack traces of goroutines holding a
contended mutex. contended mutex.
-outputdir directory
Place output files from profiling in the specified directory,
by default the directory in which "go test" is running.
-trace trace.out -trace trace.out
Write an execution trace to the specified file before exiting. Write an execution trace to the specified file before exiting.
@ -540,6 +544,7 @@ See the documentation of the testing package for more information.
} }
var ( var (
testArtifacts bool // -artifacts flag
testBench string // -bench flag testBench string // -bench flag
testC bool // -c flag testC bool // -c flag
testCoverPkgs []*load.Package // -coverpkg flag testCoverPkgs []*load.Package // -coverpkg flag

View file

@ -44,6 +44,7 @@ func init() {
// some of them so that cmd/go knows what to do with the test output, or knows // some of them so that cmd/go knows what to do with the test output, or knows
// to build the test in a way that supports the use of the flag. // to build the test in a way that supports the use of the flag.
cf.BoolVar(&testArtifacts, "artifacts", false, "")
cf.StringVar(&testBench, "bench", "", "") cf.StringVar(&testBench, "bench", "", "")
cf.Bool("benchmem", false, "") cf.Bool("benchmem", false, "")
cf.String("benchtime", "", "") cf.String("benchtime", "", "")
@ -392,7 +393,8 @@ func testFlags(args []string) (packageNames, passToTest []string) {
// directory, but 'go test' defaults it to the working directory of the 'go' // directory, but 'go test' defaults it to the working directory of the 'go'
// command. Set it explicitly if it is needed due to some other flag that // command. Set it explicitly if it is needed due to some other flag that
// requests output. // requests output.
if testProfile() != "" && !outputDirSet { needOutputDir := testProfile() != "" || testArtifacts
if needOutputDir && !outputDirSet {
injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir.getAbs()) injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir.getAbs())
} }

View file

@ -38,6 +38,7 @@ type event struct {
FailedBuild string `json:",omitempty"` FailedBuild string `json:",omitempty"`
Key string `json:",omitempty"` Key string `json:",omitempty"`
Value string `json:",omitempty"` Value string `json:",omitempty"`
Path string `json:",omitempty"`
} }
// textBytes is a hack to get JSON to emit a []byte as a string // textBytes is a hack to get JSON to emit a []byte as a string
@ -180,6 +181,7 @@ var (
[]byte("=== FAIL "), []byte("=== FAIL "),
[]byte("=== SKIP "), []byte("=== SKIP "),
[]byte("=== ATTR "), []byte("=== ATTR "),
[]byte("=== ARTIFACTS "),
} }
reports = [][]byte{ reports = [][]byte{
@ -251,7 +253,6 @@ func (c *Converter) handleInputLine(line []byte) {
// "=== RUN " // "=== RUN "
// "=== PAUSE " // "=== PAUSE "
// "=== CONT " // "=== CONT "
actionColon := false
origLine := line origLine := line
ok := false ok := false
indent := 0 indent := 0
@ -273,7 +274,6 @@ func (c *Converter) handleInputLine(line []byte) {
} }
for _, magic := range reports { for _, magic := range reports {
if bytes.HasPrefix(line, magic) { if bytes.HasPrefix(line, magic) {
actionColon = true
ok = true ok = true
break break
} }
@ -296,16 +296,11 @@ func (c *Converter) handleInputLine(line []byte) {
return return
} }
// Parse out action and test name. // Parse out action and test name from "=== ACTION: Name".
i := 0 action, name, _ := strings.Cut(string(line[len("=== "):]), " ")
if actionColon { action = strings.TrimSuffix(action, ":")
i = bytes.IndexByte(line, ':') + 1 action = strings.ToLower(action)
} name = strings.TrimSpace(name)
if i == 0 {
i = len(updates[0])
}
action := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(string(line[4:i])), ":"))
name := strings.TrimSpace(string(line[i:]))
e := &event{Action: action} e := &event{Action: action}
if line[0] == '-' { // PASS or FAIL report if line[0] == '-' { // PASS or FAIL report
@ -336,7 +331,10 @@ func (c *Converter) handleInputLine(line []byte) {
c.output.write(origLine) c.output.write(origLine)
return return
} }
if action == "attr" { switch action {
case "artifacts":
name, e.Path, _ = strings.Cut(name, " ")
case "attr":
var rest string var rest string
name, rest, _ = strings.Cut(name, " ") name, rest, _ = strings.Cut(name, " ")
e.Key, e.Value, _ = strings.Cut(rest, " ") e.Key, e.Value, _ = strings.Cut(rest, " ")

View file

@ -66,6 +66,12 @@ func (TestDeps) ImportPath() string {
return ImportPath return ImportPath
} }
var ModulePath string
func (TestDeps) ModulePath() string {
return ModulePath
}
// testLog implements testlog.Interface, logging actions by package os. // testLog implements testlog.Interface, logging actions by package os.
type testLog struct { type testLog struct {
mu sync.Mutex mu sync.Mutex

View file

@ -420,7 +420,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"unicode" "unicode"
"unicode/utf8"
_ "unsafe" // for linkname _ "unsafe" // for linkname
) )
@ -456,6 +455,7 @@ func Init() {
// this flag lets "go test" tell the binary to write the files in the directory where // this flag lets "go test" tell the binary to write the files in the directory where
// the "go test" command is run. // the "go test" command is run.
outputDir = flag.String("test.outputdir", "", "write profiles to `dir`") outputDir = flag.String("test.outputdir", "", "write profiles to `dir`")
artifacts = flag.Bool("test.artifacts", false, "store test artifacts in test.,outputdir")
// Report as tests are run; default is silent for success. // Report as tests are run; default is silent for success.
flag.Var(&chatty, "test.v", "verbose: print additional output") flag.Var(&chatty, "test.v", "verbose: print additional output")
count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times") count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times")
@ -489,6 +489,7 @@ var (
short *bool short *bool
failFast *bool failFast *bool
outputDir *string outputDir *string
artifacts *bool
chatty chattyFlag chatty chattyFlag
count *uint count *uint
coverProfile *string coverProfile *string
@ -516,6 +517,7 @@ var (
cpuList []int cpuList []int
testlogFile *os.File testlogFile *os.File
artifactDir string
numFailed atomic.Uint32 // number of test failures numFailed atomic.Uint32 // number of test failures
@ -653,15 +655,17 @@ type common struct {
runner string // Function name of tRunner running the test. runner string // Function name of tRunner running the test.
isParallel bool // Whether the test is parallel. isParallel bool // Whether the test is parallel.
parent *common parent *common
level int // Nesting depth of test or benchmark. level int // Nesting depth of test or benchmark.
creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run. creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run.
name string // Name of test or benchmark. modulePath string
start highPrecisionTime // Time test or benchmark started importPath string
duration time.Duration name string // Name of test or benchmark.
barrier chan bool // To signal parallel subtests they may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing). start highPrecisionTime // Time test or benchmark started
signal chan bool // To signal a test is done. duration time.Duration
sub []*T // Queue of subtests to be run in parallel. barrier chan bool // To signal parallel subtests they may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing).
signal chan bool // To signal a test is done.
sub []*T // Queue of subtests to be run in parallel.
lastRaceErrors atomic.Int64 // Max value of race.Errors seen during the test or its subtests. lastRaceErrors atomic.Int64 // Max value of race.Errors seen during the test or its subtests.
raceErrorLogged atomic.Bool raceErrorLogged atomic.Bool
@ -671,6 +675,10 @@ type common struct {
tempDirErr error tempDirErr error
tempDirSeq int32 tempDirSeq int32
artifactDirOnce sync.Once
artifactDir string
artifactDirErr error
ctx context.Context ctx context.Context
cancelCtx context.CancelFunc cancelCtx context.CancelFunc
} }
@ -879,6 +887,7 @@ func fmtDuration(d time.Duration) string {
// TB is the interface common to [T], [B], and [F]. // TB is the interface common to [T], [B], and [F].
type TB interface { type TB interface {
ArtifactDir() string
Attr(key, value string) Attr(key, value string)
Cleanup(func()) Cleanup(func())
Error(args ...any) Error(args ...any)
@ -1313,6 +1322,96 @@ func (c *common) Cleanup(f func()) {
c.cleanups = append(c.cleanups, fn) c.cleanups = append(c.cleanups, fn)
} }
// ArtifactDir returns a directory in which the test should store output files.
// When the -artifacts flag is provided, this directory is located
// under the output directory. Otherwise, ArtifactDir returns a temporary directory
// that is removed after the test completes.
//
// Each test or subtest within each test package has a unique artifact directory.
// Repeated calls to ArtifactDir in the same test or subtest return the same directory.
// Subtest outputs are not located under the parent test's output directory.
func (c *common) ArtifactDir() string {
c.checkFuzzFn("ArtifactDir")
c.artifactDirOnce.Do(func() {
c.artifactDir, c.artifactDirErr = c.makeArtifactDir()
})
if c.artifactDirErr != nil {
c.Fatalf("ArtifactDir: %v", c.artifactDirErr)
}
return c.artifactDir
}
func hashString(s string) (h uint64) {
// FNV, used here to avoid a dependency on maphash.
for i := 0; i < len(s); i++ {
h ^= uint64(s[i])
h *= 1099511628211
}
return
}
// makeArtifactDir creates the artifact directory for a test.
// The artifact directory is:
//
// <output dir>/_artifacts/<test package>/<test name>/<random>
//
// The test package is the package import path with the module name prefix removed.
// The test name is truncated if too long.
// Special characters are removed from the path.
func (c *common) makeArtifactDir() (string, error) {
if !*artifacts {
return c.makeTempDir()
}
// If the test name is longer than maxNameSize, truncate it and replace the last
// hashSize bytes with a hash of the full name.
const maxNameSize = 64
name := strings.ReplaceAll(c.name, "/", "__")
if len(name) > maxNameSize {
h := fmt.Sprintf("%0x", hashString(name))
name = name[:maxNameSize-len(h)] + h
}
// Remove the module path prefix from the import path.
pkg := strings.TrimPrefix(c.importPath, c.modulePath+"/")
// Join with /, not filepath.Join: the import path is /-separated,
// and we don't want removeSymbolsExcept to strip \ separators on Windows.
base := "/" + pkg + "/" + name
base = removeSymbolsExcept(base, "!#$%&()+,-.=@^_{}~ /")
base, err := filepath.Localize(base)
if err != nil {
// This name can't be safely converted into a local filepath.
// Drop it and just use _artifacts/<random>.
base = ""
}
artifactBase := filepath.Join(artifactDir, base)
if err := os.MkdirAll(artifactBase, 0o777); err != nil {
return "", err
}
dir, err := os.MkdirTemp(artifactBase, "")
if err != nil {
return "", err
}
if c.chatty != nil {
c.chatty.Updatef(c.name, "=== ARTIFACTS %s %v\n", c.name, dir)
}
return dir, nil
}
func removeSymbolsExcept(s, allowed string) string {
mapper := func(r rune) rune {
if unicode.IsLetter(r) ||
unicode.IsNumber(r) ||
strings.ContainsRune(allowed, r) {
return r
}
return -1 // disallowed symbol
}
return strings.Map(mapper, s)
}
// TempDir returns a temporary directory for the test to use. // TempDir returns a temporary directory for the test to use.
// The directory is automatically removed when the test and // The directory is automatically removed when the test and
// all its subtests complete. // all its subtests complete.
@ -1322,6 +1421,14 @@ func (c *common) Cleanup(f func()) {
// be created somewhere beneath it. // be created somewhere beneath it.
func (c *common) TempDir() string { func (c *common) TempDir() string {
c.checkFuzzFn("TempDir") c.checkFuzzFn("TempDir")
dir, err := c.makeTempDir()
if err != nil {
c.Fatalf("TempDir: %v", err)
}
return dir
}
func (c *common) makeTempDir() (string, error) {
// Use a single parent directory for all the temporary directories // Use a single parent directory for all the temporary directories
// created by a test, each numbered sequentially. // created by a test, each numbered sequentially.
c.tempDirMu.Lock() c.tempDirMu.Lock()
@ -1332,7 +1439,7 @@ func (c *common) TempDir() string {
_, err := os.Stat(c.tempDir) _, err := os.Stat(c.tempDir)
nonExistent = os.IsNotExist(err) nonExistent = os.IsNotExist(err)
if err != nil && !nonExistent { if err != nil && !nonExistent {
c.Fatalf("TempDir: %v", err) return "", err
} }
} }
@ -1347,23 +1454,9 @@ func (c *common) TempDir() string {
// Drop unusual characters (such as path separators or // Drop unusual characters (such as path separators or
// characters interacting with globs) from the directory name to // characters interacting with globs) from the directory name to
// avoid surprising os.MkdirTemp behavior. // avoid surprising os.MkdirTemp behavior.
mapper := func(r rune) rune { const allowed = "!#$%&()+,-.=@^_{}~ "
if r < utf8.RuneSelf { pattern = removeSymbolsExcept(pattern, allowed)
const allowed = "!#$%&()+,-.=@^_{}~ "
if '0' <= r && r <= '9' ||
'a' <= r && r <= 'z' ||
'A' <= r && r <= 'Z' {
return r
}
if strings.ContainsRune(allowed, r) {
return r
}
} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
return r
}
return -1
}
pattern = strings.Map(mapper, pattern)
c.tempDir, c.tempDirErr = os.MkdirTemp(os.Getenv("GOTMPDIR"), pattern) c.tempDir, c.tempDirErr = os.MkdirTemp(os.Getenv("GOTMPDIR"), pattern)
if c.tempDirErr == nil { if c.tempDirErr == nil {
c.Cleanup(func() { c.Cleanup(func() {
@ -1381,14 +1474,14 @@ func (c *common) TempDir() string {
c.tempDirMu.Unlock() c.tempDirMu.Unlock()
if c.tempDirErr != nil { if c.tempDirErr != nil {
c.Fatalf("TempDir: %v", c.tempDirErr) return "", c.tempDirErr
} }
dir := fmt.Sprintf("%s%c%03d", c.tempDir, os.PathSeparator, seq) dir := fmt.Sprintf("%s%c%03d", c.tempDir, os.PathSeparator, seq)
if err := os.Mkdir(dir, 0o777); err != nil { if err := os.Mkdir(dir, 0o777); err != nil {
c.Fatalf("TempDir: %v", err) return "", err
} }
return dir return dir, nil
} }
// removeAll is like os.RemoveAll, but retries Windows "Access is denied." // removeAll is like os.RemoveAll, but retries Windows "Access is denied."
@ -1971,15 +2064,17 @@ func (t *T) Run(name string, f func(t *T)) bool {
ctx, cancelCtx := context.WithCancel(context.Background()) ctx, cancelCtx := context.WithCancel(context.Background())
t = &T{ t = &T{
common: common{ common: common{
barrier: make(chan bool), barrier: make(chan bool),
signal: make(chan bool, 1), signal: make(chan bool, 1),
name: testName, name: testName,
parent: &t.common, modulePath: t.modulePath,
level: t.level + 1, importPath: t.importPath,
creator: pc[:n], parent: &t.common,
chatty: t.chatty, level: t.level + 1,
ctx: ctx, creator: pc[:n],
cancelCtx: cancelCtx, chatty: t.chatty,
ctx: ctx,
cancelCtx: cancelCtx,
}, },
tstate: t.tstate, tstate: t.tstate,
} }
@ -2140,6 +2235,7 @@ func (f matchStringOnly) MatchString(pat, str string) (bool, error) { return f
func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain } func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain }
func (f matchStringOnly) StopCPUProfile() {} func (f matchStringOnly) StopCPUProfile() {}
func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain } func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain }
func (f matchStringOnly) ModulePath() string { return "" }
func (f matchStringOnly) ImportPath() string { return "" } func (f matchStringOnly) ImportPath() string { return "" }
func (f matchStringOnly) StartTestLog(io.Writer) {} func (f matchStringOnly) StartTestLog(io.Writer) {}
func (f matchStringOnly) StopTestLog() error { return errMain } func (f matchStringOnly) StopTestLog() error { return errMain }
@ -2193,6 +2289,7 @@ type M struct {
// testing/internal/testdeps's TestDeps. // testing/internal/testdeps's TestDeps.
type testDeps interface { type testDeps interface {
ImportPath() string ImportPath() string
ModulePath() string
MatchString(pat, str string) (bool, error) MatchString(pat, str string) (bool, error)
SetPanicOnExit0(bool) SetPanicOnExit0(bool)
StartCPUProfile(io.Writer) error StartCPUProfile(io.Writer) error
@ -2336,7 +2433,7 @@ func (m *M) Run() (code int) {
if !*isFuzzWorker { if !*isFuzzWorker {
deadline := m.startAlarm() deadline := m.startAlarm()
haveExamples = len(m.examples) > 0 haveExamples = len(m.examples) > 0
testRan, testOk := runTests(m.deps.MatchString, m.tests, deadline) testRan, testOk := runTests(m.deps.ModulePath(), m.deps.ImportPath(), m.deps.MatchString, m.tests, deadline)
fuzzTargetsRan, fuzzTargetsOk := runFuzzTests(m.deps, m.fuzzTargets, deadline) fuzzTargetsRan, fuzzTargetsOk := runFuzzTests(m.deps, m.fuzzTargets, deadline)
exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples) exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
m.stopAlarm() m.stopAlarm()
@ -2437,14 +2534,14 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
if *timeout > 0 { if *timeout > 0 {
deadline = time.Now().Add(*timeout) deadline = time.Now().Add(*timeout)
} }
ran, ok := runTests(matchString, tests, deadline) ran, ok := runTests("", "", matchString, tests, deadline)
if !ran && !haveExamples { if !ran && !haveExamples {
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run") fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
} }
return ok return ok
} }
func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) { func runTests(modulePath, importPath string, matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) {
ok = true ok = true
for _, procs := range cpuList { for _, procs := range cpuList {
runtime.GOMAXPROCS(procs) runtime.GOMAXPROCS(procs)
@ -2463,11 +2560,13 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT
tstate.deadline = deadline tstate.deadline = deadline
t := &T{ t := &T{
common: common{ common: common{
signal: make(chan bool, 1), signal: make(chan bool, 1),
barrier: make(chan bool), barrier: make(chan bool),
w: os.Stdout, w: os.Stdout,
ctx: ctx, ctx: ctx,
cancelCtx: cancelCtx, cancelCtx: cancelCtx,
modulePath: modulePath,
importPath: importPath,
}, },
tstate: tstate, tstate: tstate,
} }
@ -2536,6 +2635,18 @@ func (m *M) before() {
fmt.Fprintf(os.Stderr, "testing: cannot use -test.gocoverdir because test binary was not built with coverage enabled\n") fmt.Fprintf(os.Stderr, "testing: cannot use -test.gocoverdir because test binary was not built with coverage enabled\n")
os.Exit(2) os.Exit(2)
} }
if *artifacts {
var err error
artifactDir, err = filepath.Abs(toOutputDir("_artifacts"))
if err != nil {
fmt.Fprintf(os.Stderr, "testing: cannot make -test.outputdir absolute: %v\n", err)
os.Exit(2)
}
if err := os.Mkdir(artifactDir, 0o777); err != nil && !errors.Is(err, os.ErrExist) {
fmt.Fprintf(os.Stderr, "testing: %v\n", err)
os.Exit(2)
}
}
if *testlog != "" { if *testlog != "" {
// Note: Not using toOutputDir. // Note: Not using toOutputDir.
// This file is for use by cmd/go, not users. // This file is for use by cmd/go, not users.

View file

@ -469,7 +469,7 @@ func TestTesting(t *testing.T) {
// runTest runs a helper test with -test.v, ignoring its exit status. // runTest runs a helper test with -test.v, ignoring its exit status.
// runTest both logs and returns the test output. // runTest both logs and returns the test output.
func runTest(t *testing.T, test string) []byte { func runTest(t *testing.T, test string, args ...string) []byte {
t.Helper() t.Helper()
testenv.MustHaveExec(t) testenv.MustHaveExec(t)
@ -477,6 +477,7 @@ func runTest(t *testing.T, test string) []byte {
cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+test+"$", "-test.bench="+test, "-test.v", "-test.parallel=2", "-test.benchtime=2x") cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+test+"$", "-test.bench="+test, "-test.v", "-test.parallel=2", "-test.benchtime=2x")
cmd = testenv.CleanCmdEnv(cmd) cmd = testenv.CleanCmdEnv(cmd)
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
cmd.Args = append(cmd.Args, args...)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
t.Logf("%v: %v\n%s", cmd, err, out) t.Logf("%v: %v\n%s", cmd, err, out)
@ -1055,6 +1056,105 @@ func TestAttrInvalid(t *testing.T) {
} }
} }
const artifactContent = "It belongs in a museum.\n"
func TestArtifactDirExample(t *testing.T) {
os.WriteFile(filepath.Join(t.ArtifactDir(), "artifact"), []byte(artifactContent), 0o666)
}
func TestArtifactDirDefault(t *testing.T) {
tempDir := t.TempDir()
t.Chdir(tempDir)
out := runTest(t, "TestArtifactDirExample", "-test.artifacts")
checkArtifactDir(t, out, "TestArtifactDirExample", tempDir)
}
func TestArtifactDirSpecified(t *testing.T) {
tempDir := t.TempDir()
out := runTest(t, "TestArtifactDirExample", "-test.artifacts", "-test.outputdir="+tempDir)
checkArtifactDir(t, out, "TestArtifactDirExample", tempDir)
}
func TestArtifactDirNoArtifacts(t *testing.T) {
t.Chdir(t.TempDir())
out := string(runTest(t, "TestArtifactDirExample"))
if strings.Contains(out, "=== ARTIFACTS") {
t.Errorf("expected output with no === ARTIFACTS, got\n%q", out)
}
ents, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
for _, e := range ents {
t.Errorf("unexpected file in current directory after test: %v", e.Name())
}
}
func TestArtifactDirSubtestExample(t *testing.T) {
t.Run("Subtest", func(t *testing.T) {
os.WriteFile(filepath.Join(t.ArtifactDir(), "artifact"), []byte(artifactContent), 0o666)
})
}
func TestArtifactDirInSubtest(t *testing.T) {
tempDir := t.TempDir()
out := runTest(t, "TestArtifactDirSubtestExample/Subtest", "-test.artifacts", "-test.outputdir="+tempDir)
checkArtifactDir(t, out, "TestArtifactDirSubtestExample/Subtest", tempDir)
}
func TestArtifactDirLongTestNameExample(t *testing.T) {
name := strings.Repeat("x", 256)
t.Run(name, func(t *testing.T) {
os.WriteFile(filepath.Join(t.ArtifactDir(), "artifact"), []byte(artifactContent), 0o666)
})
}
func TestArtifactDirWithLongTestName(t *testing.T) {
tempDir := t.TempDir()
out := runTest(t, "TestArtifactDirLongTestNameExample", "-test.artifacts", "-test.outputdir="+tempDir)
checkArtifactDir(t, out, `TestArtifactDirLongTestNameExample/\w+`, tempDir)
}
func TestArtifactDirConsistent(t *testing.T) {
a := t.ArtifactDir()
b := t.ArtifactDir()
if a != b {
t.Errorf("t.ArtifactDir is not consistent between calls: %q, %q", a, b)
}
}
func checkArtifactDir(t *testing.T, out []byte, testName, outputDir string) {
t.Helper()
re := regexp.MustCompile(`=== ARTIFACTS ` + testName + ` ([^\n]+)`)
match := re.FindSubmatch(out)
if match == nil {
t.Fatalf("expected output matching %q, got\n%q", re, out)
}
artifactDir := string(match[1])
// Verify that the artifact directory is contained in the expected output directory.
relDir, err := filepath.Rel(outputDir, artifactDir)
if err != nil {
t.Fatal(err)
}
if !filepath.IsLocal(relDir) {
t.Fatalf("want artifact directory contained in %q, got %q", outputDir, artifactDir)
}
for _, part := range strings.Split(relDir, string(os.PathSeparator)) {
const maxSize = 64
if len(part) > maxSize {
t.Errorf("artifact directory %q contains component >%v characters long: %q", relDir, maxSize, part)
}
}
got, err := os.ReadFile(filepath.Join(artifactDir, "artifact"))
if err != nil || string(got) != artifactContent {
t.Errorf("reading artifact in %q: got %q, %v; want %q", artifactDir, got, err, artifactContent)
}
}
func TestBenchmarkBLoopIterationCorrect(t *testing.T) { func TestBenchmarkBLoopIterationCorrect(t *testing.T) {
out := runTest(t, "BenchmarkBLoopPrint") out := runTest(t, "BenchmarkBLoopPrint")
c := bytes.Count(out, []byte("Printing from BenchmarkBLoopPrint")) c := bytes.Count(out, []byte("Printing from BenchmarkBLoopPrint"))
@ -1110,3 +1210,7 @@ func BenchmarkBNPrint(b *testing.B) {
b.Logf("Printing from BenchmarkBNPrint") b.Logf("Printing from BenchmarkBNPrint")
} }
} }
func TestArtifactDir(t *testing.T) {
t.Log(t.ArtifactDir())
}