mirror of
https://github.com/golang/go.git
synced 2025-10-19 19:13:18 +00:00
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:
parent
1623927730
commit
bb1ca7ae81
11 changed files with 333 additions and 71 deletions
4
api/next/71287.txt
Normal file
4
api/next/71287.txt
Normal 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
|
18
doc/next/6-stdlib/99-minor/testing/71287.md
Normal file
18
doc/next/6-stdlib/99-minor/testing/71287.md
Normal 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
|
||||
```
|
|
@ -3244,6 +3244,10 @@
|
|||
// The following flags are recognized by the 'go test' command and
|
||||
// control the execution of any test:
|
||||
//
|
||||
// -artifacts
|
||||
// Save test artifacts in the directory specified by -outputdir.
|
||||
// See 'go doc testing.T.ArtifactDir'.
|
||||
//
|
||||
// -bench regexp
|
||||
// Run only those benchmarks matching a regular expression.
|
||||
// By default, no benchmarks are run.
|
||||
|
@ -3338,6 +3342,10 @@
|
|||
// This will only list top-level tests. No subtest or subbenchmarks will be
|
||||
// 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
|
||||
// Allow parallel execution of test functions that call t.Parallel, and
|
||||
// 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
|
||||
// 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
|
||||
// Write an execution trace to the specified file before exiting.
|
||||
//
|
||||
|
|
|
@ -649,6 +649,14 @@ func (t *testFuncs) ImportPath() string {
|
|||
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.
|
||||
// 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
|
||||
|
@ -836,6 +844,7 @@ func init() {
|
|||
testdeps.CoverMarkProfileEmittedFunc = cfile.MarkProfileEmitted
|
||||
|
||||
{{end}}
|
||||
testdeps.ModulePath = {{.ModulePath | printf "%q"}}
|
||||
testdeps.ImportPath = {{.ImportPath | printf "%q"}}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ package test
|
|||
// passFlagToTest contains the flags that should be forwarded to
|
||||
// the test binary with the prefix "test.".
|
||||
var passFlagToTest = map[string]bool{
|
||||
"artifacts": true,
|
||||
"bench": true,
|
||||
"benchmem": true,
|
||||
"benchtime": true,
|
||||
|
|
|
@ -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
|
||||
control the execution of any test:
|
||||
|
||||
-artifacts
|
||||
Save test artifacts in the directory specified by -outputdir.
|
||||
See 'go doc testing.T.ArtifactDir'.
|
||||
|
||||
-bench regexp
|
||||
Run only those benchmarks matching a regular expression.
|
||||
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
|
||||
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
|
||||
Allow parallel execution of test functions that call t.Parallel, and
|
||||
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
|
||||
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
|
||||
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 (
|
||||
testArtifacts bool // -artifacts flag
|
||||
testBench string // -bench flag
|
||||
testC bool // -c flag
|
||||
testCoverPkgs []*load.Package // -coverpkg flag
|
||||
|
|
|
@ -44,6 +44,7 @@ func init() {
|
|||
// 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.
|
||||
|
||||
cf.BoolVar(&testArtifacts, "artifacts", false, "")
|
||||
cf.StringVar(&testBench, "bench", "", "")
|
||||
cf.Bool("benchmem", false, "")
|
||||
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'
|
||||
// command. Set it explicitly if it is needed due to some other flag that
|
||||
// requests output.
|
||||
if testProfile() != "" && !outputDirSet {
|
||||
needOutputDir := testProfile() != "" || testArtifacts
|
||||
if needOutputDir && !outputDirSet {
|
||||
injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir.getAbs())
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ type event struct {
|
|||
FailedBuild string `json:",omitempty"`
|
||||
Key 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
|
||||
|
@ -180,6 +181,7 @@ var (
|
|||
[]byte("=== FAIL "),
|
||||
[]byte("=== SKIP "),
|
||||
[]byte("=== ATTR "),
|
||||
[]byte("=== ARTIFACTS "),
|
||||
}
|
||||
|
||||
reports = [][]byte{
|
||||
|
@ -251,7 +253,6 @@ func (c *Converter) handleInputLine(line []byte) {
|
|||
// "=== RUN "
|
||||
// "=== PAUSE "
|
||||
// "=== CONT "
|
||||
actionColon := false
|
||||
origLine := line
|
||||
ok := false
|
||||
indent := 0
|
||||
|
@ -273,7 +274,6 @@ func (c *Converter) handleInputLine(line []byte) {
|
|||
}
|
||||
for _, magic := range reports {
|
||||
if bytes.HasPrefix(line, magic) {
|
||||
actionColon = true
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
|
@ -296,16 +296,11 @@ func (c *Converter) handleInputLine(line []byte) {
|
|||
return
|
||||
}
|
||||
|
||||
// Parse out action and test name.
|
||||
i := 0
|
||||
if actionColon {
|
||||
i = bytes.IndexByte(line, ':') + 1
|
||||
}
|
||||
if i == 0 {
|
||||
i = len(updates[0])
|
||||
}
|
||||
action := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(string(line[4:i])), ":"))
|
||||
name := strings.TrimSpace(string(line[i:]))
|
||||
// Parse out action and test name from "=== ACTION: Name".
|
||||
action, name, _ := strings.Cut(string(line[len("=== "):]), " ")
|
||||
action = strings.TrimSuffix(action, ":")
|
||||
action = strings.ToLower(action)
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
e := &event{Action: action}
|
||||
if line[0] == '-' { // PASS or FAIL report
|
||||
|
@ -336,7 +331,10 @@ func (c *Converter) handleInputLine(line []byte) {
|
|||
c.output.write(origLine)
|
||||
return
|
||||
}
|
||||
if action == "attr" {
|
||||
switch action {
|
||||
case "artifacts":
|
||||
name, e.Path, _ = strings.Cut(name, " ")
|
||||
case "attr":
|
||||
var rest string
|
||||
name, rest, _ = strings.Cut(name, " ")
|
||||
e.Key, e.Value, _ = strings.Cut(rest, " ")
|
||||
|
|
|
@ -66,6 +66,12 @@ func (TestDeps) ImportPath() string {
|
|||
return ImportPath
|
||||
}
|
||||
|
||||
var ModulePath string
|
||||
|
||||
func (TestDeps) ModulePath() string {
|
||||
return ModulePath
|
||||
}
|
||||
|
||||
// testLog implements testlog.Interface, logging actions by package os.
|
||||
type testLog struct {
|
||||
mu sync.Mutex
|
||||
|
|
|
@ -420,7 +420,6 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
_ "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
|
||||
// the "go test" command is run.
|
||||
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.
|
||||
flag.Var(&chatty, "test.v", "verbose: print additional output")
|
||||
count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times")
|
||||
|
@ -489,6 +489,7 @@ var (
|
|||
short *bool
|
||||
failFast *bool
|
||||
outputDir *string
|
||||
artifacts *bool
|
||||
chatty chattyFlag
|
||||
count *uint
|
||||
coverProfile *string
|
||||
|
@ -516,6 +517,7 @@ var (
|
|||
|
||||
cpuList []int
|
||||
testlogFile *os.File
|
||||
artifactDir string
|
||||
|
||||
numFailed atomic.Uint32 // number of test failures
|
||||
|
||||
|
@ -653,15 +655,17 @@ type common struct {
|
|||
runner string // Function name of tRunner running the test.
|
||||
isParallel bool // Whether the test is parallel.
|
||||
|
||||
parent *common
|
||||
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.
|
||||
name string // Name of test or benchmark.
|
||||
start highPrecisionTime // Time test or benchmark started
|
||||
duration time.Duration
|
||||
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.
|
||||
parent *common
|
||||
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.
|
||||
modulePath string
|
||||
importPath string
|
||||
name string // Name of test or benchmark.
|
||||
start highPrecisionTime // Time test or benchmark started
|
||||
duration time.Duration
|
||||
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.
|
||||
raceErrorLogged atomic.Bool
|
||||
|
@ -671,6 +675,10 @@ type common struct {
|
|||
tempDirErr error
|
||||
tempDirSeq int32
|
||||
|
||||
artifactDirOnce sync.Once
|
||||
artifactDir string
|
||||
artifactDirErr error
|
||||
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
}
|
||||
|
@ -879,6 +887,7 @@ func fmtDuration(d time.Duration) string {
|
|||
|
||||
// TB is the interface common to [T], [B], and [F].
|
||||
type TB interface {
|
||||
ArtifactDir() string
|
||||
Attr(key, value string)
|
||||
Cleanup(func())
|
||||
Error(args ...any)
|
||||
|
@ -1313,6 +1322,96 @@ func (c *common) Cleanup(f func()) {
|
|||
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.
|
||||
// The directory is automatically removed when the test and
|
||||
// all its subtests complete.
|
||||
|
@ -1322,6 +1421,14 @@ func (c *common) Cleanup(f func()) {
|
|||
// be created somewhere beneath it.
|
||||
func (c *common) TempDir() string {
|
||||
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
|
||||
// created by a test, each numbered sequentially.
|
||||
c.tempDirMu.Lock()
|
||||
|
@ -1332,7 +1439,7 @@ func (c *common) TempDir() string {
|
|||
_, err := os.Stat(c.tempDir)
|
||||
nonExistent = os.IsNotExist(err)
|
||||
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
|
||||
// characters interacting with globs) from the directory name to
|
||||
// avoid surprising os.MkdirTemp behavior.
|
||||
mapper := func(r rune) rune {
|
||||
if r < utf8.RuneSelf {
|
||||
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)
|
||||
const allowed = "!#$%&()+,-.=@^_{}~ "
|
||||
pattern = removeSymbolsExcept(pattern, allowed)
|
||||
|
||||
c.tempDir, c.tempDirErr = os.MkdirTemp(os.Getenv("GOTMPDIR"), pattern)
|
||||
if c.tempDirErr == nil {
|
||||
c.Cleanup(func() {
|
||||
|
@ -1381,14 +1474,14 @@ func (c *common) TempDir() string {
|
|||
c.tempDirMu.Unlock()
|
||||
|
||||
if c.tempDirErr != nil {
|
||||
c.Fatalf("TempDir: %v", c.tempDirErr)
|
||||
return "", c.tempDirErr
|
||||
}
|
||||
|
||||
dir := fmt.Sprintf("%s%c%03d", c.tempDir, os.PathSeparator, seq)
|
||||
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."
|
||||
|
@ -1971,15 +2064,17 @@ func (t *T) Run(name string, f func(t *T)) bool {
|
|||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
t = &T{
|
||||
common: common{
|
||||
barrier: make(chan bool),
|
||||
signal: make(chan bool, 1),
|
||||
name: testName,
|
||||
parent: &t.common,
|
||||
level: t.level + 1,
|
||||
creator: pc[:n],
|
||||
chatty: t.chatty,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
barrier: make(chan bool),
|
||||
signal: make(chan bool, 1),
|
||||
name: testName,
|
||||
modulePath: t.modulePath,
|
||||
importPath: t.importPath,
|
||||
parent: &t.common,
|
||||
level: t.level + 1,
|
||||
creator: pc[:n],
|
||||
chatty: t.chatty,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
},
|
||||
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) StopCPUProfile() {}
|
||||
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) StartTestLog(io.Writer) {}
|
||||
func (f matchStringOnly) StopTestLog() error { return errMain }
|
||||
|
@ -2193,6 +2289,7 @@ type M struct {
|
|||
// testing/internal/testdeps's TestDeps.
|
||||
type testDeps interface {
|
||||
ImportPath() string
|
||||
ModulePath() string
|
||||
MatchString(pat, str string) (bool, error)
|
||||
SetPanicOnExit0(bool)
|
||||
StartCPUProfile(io.Writer) error
|
||||
|
@ -2336,7 +2433,7 @@ func (m *M) Run() (code int) {
|
|||
if !*isFuzzWorker {
|
||||
deadline := m.startAlarm()
|
||||
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)
|
||||
exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
|
||||
m.stopAlarm()
|
||||
|
@ -2437,14 +2534,14 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
|
|||
if *timeout > 0 {
|
||||
deadline = time.Now().Add(*timeout)
|
||||
}
|
||||
ran, ok := runTests(matchString, tests, deadline)
|
||||
ran, ok := runTests("", "", matchString, tests, deadline)
|
||||
if !ran && !haveExamples {
|
||||
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
|
||||
}
|
||||
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
|
||||
for _, procs := range cpuList {
|
||||
runtime.GOMAXPROCS(procs)
|
||||
|
@ -2463,11 +2560,13 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT
|
|||
tstate.deadline = deadline
|
||||
t := &T{
|
||||
common: common{
|
||||
signal: make(chan bool, 1),
|
||||
barrier: make(chan bool),
|
||||
w: os.Stdout,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
signal: make(chan bool, 1),
|
||||
barrier: make(chan bool),
|
||||
w: os.Stdout,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
modulePath: modulePath,
|
||||
importPath: importPath,
|
||||
},
|
||||
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")
|
||||
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 != "" {
|
||||
// Note: Not using toOutputDir.
|
||||
// This file is for use by cmd/go, not users.
|
||||
|
|
|
@ -469,7 +469,7 @@ func TestTesting(t *testing.T) {
|
|||
|
||||
// runTest runs a helper test with -test.v, ignoring its exit status.
|
||||
// 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()
|
||||
|
||||
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.CleanCmdEnv(cmd)
|
||||
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
|
||||
cmd.Args = append(cmd.Args, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
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) {
|
||||
out := runTest(t, "BenchmarkBLoopPrint")
|
||||
c := bytes.Count(out, []byte("Printing from BenchmarkBLoopPrint"))
|
||||
|
@ -1110,3 +1210,7 @@ func BenchmarkBNPrint(b *testing.B) {
|
|||
b.Logf("Printing from BenchmarkBNPrint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactDir(t *testing.T) {
|
||||
t.Log(t.ArtifactDir())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue