diff options
Diffstat (limited to 'src/testing')
-rw-r--r-- | src/testing/benchmark.go | 12 | ||||
-rw-r--r-- | src/testing/benchmark_test.go | 11 | ||||
-rw-r--r-- | src/testing/example.go | 4 | ||||
-rw-r--r-- | src/testing/export_test.go | 4 | ||||
-rw-r--r-- | src/testing/fstest/mapfs.go | 6 | ||||
-rw-r--r-- | src/testing/fstest/testfs.go | 16 | ||||
-rw-r--r-- | src/testing/fstest/testfs_test.go | 7 | ||||
-rw-r--r-- | src/testing/fuzz.go | 10 | ||||
-rw-r--r-- | src/testing/internal/testdeps/deps.go | 40 | ||||
-rw-r--r-- | src/testing/newcover.go | 15 | ||||
-rw-r--r-- | src/testing/testing.go | 47 | ||||
-rw-r--r-- | src/testing/testing_other.go | 18 | ||||
-rw-r--r-- | src/testing/testing_test.go | 3 | ||||
-rw-r--r-- | src/testing/testing_windows.go | 38 | ||||
-rw-r--r-- | src/testing/testing_windows_test.go | 25 |
15 files changed, 194 insertions, 62 deletions
diff --git a/src/testing/benchmark.go b/src/testing/benchmark.go index 9491213ef1..80a1b7de77 100644 --- a/src/testing/benchmark.go +++ b/src/testing/benchmark.go @@ -12,7 +12,7 @@ import ( "math" "os" "runtime" - "sort" + "slices" "strconv" "strings" "sync" @@ -123,7 +123,7 @@ func (b *B) StartTimer() { runtime.ReadMemStats(&memStats) b.startAllocs = memStats.Mallocs b.startBytes = memStats.TotalAlloc - b.start = time.Now() + b.start = highPrecisionTimeNow() b.timerOn = true } } @@ -133,7 +133,7 @@ func (b *B) StartTimer() { // want to measure. func (b *B) StopTimer() { if b.timerOn { - b.duration += time.Since(b.start) + b.duration += highPrecisionTimeSince(b.start) runtime.ReadMemStats(&memStats) b.netAllocs += memStats.Mallocs - b.startAllocs b.netBytes += memStats.TotalAlloc - b.startBytes @@ -156,7 +156,7 @@ func (b *B) ResetTimer() { runtime.ReadMemStats(&memStats) b.startAllocs = memStats.Mallocs b.startBytes = memStats.TotalAlloc - b.start = time.Now() + b.start = highPrecisionTimeNow() } b.duration = 0 b.netAllocs = 0 @@ -325,7 +325,7 @@ func (b *B) launch() { func (b *B) Elapsed() time.Duration { d := b.duration if b.timerOn { - d += time.Since(b.start) + d += highPrecisionTimeSince(b.start) } return d } @@ -443,7 +443,7 @@ func (r BenchmarkResult) String() string { } extraKeys = append(extraKeys, k) } - sort.Strings(extraKeys) + slices.Sort(extraKeys) for _, k := range extraKeys { buf.WriteByte('\t') prettyPrint(buf, r.Extra[k], k) diff --git a/src/testing/benchmark_test.go b/src/testing/benchmark_test.go index 2987170827..66f555d1f1 100644 --- a/src/testing/benchmark_test.go +++ b/src/testing/benchmark_test.go @@ -6,8 +6,9 @@ package testing_test import ( "bytes" + "cmp" "runtime" - "sort" + "slices" "strings" "sync/atomic" "testing" @@ -168,9 +169,9 @@ func ExampleB_ReportMetric() { var compares int64 for i := 0; i < b.N; i++ { s := []int{5, 4, 3, 2, 1} - sort.Slice(s, func(i, j int) bool { + slices.SortFunc(s, func(a, b int) int { compares++ - return s[i] < s[j] + return cmp.Compare(a, b) }) } // This metric is per-operation, so divide by b.N and @@ -190,12 +191,12 @@ func ExampleB_ReportMetric_parallel() { b.RunParallel(func(pb *testing.PB) { for pb.Next() { s := []int{5, 4, 3, 2, 1} - sort.Slice(s, func(i, j int) bool { + slices.SortFunc(s, func(a, b int) int { // Because RunParallel runs the function many // times in parallel, we must increment the // counter atomically to avoid racing writes. compares.Add(1) - return s[i] < s[j] + return cmp.Compare(a, b) }) } }) diff --git a/src/testing/example.go b/src/testing/example.go index 07aa5cb66c..b14477a406 100644 --- a/src/testing/example.go +++ b/src/testing/example.go @@ -6,7 +6,7 @@ package testing import ( "fmt" - "sort" + "slices" "strings" "time" ) @@ -47,7 +47,7 @@ func runExamples(matchString func(pat, str string) (bool, error), examples []Int func sortLines(output string) string { lines := strings.Split(output, "\n") - sort.Strings(lines) + slices.Sort(lines) return strings.Join(lines, "\n") } diff --git a/src/testing/export_test.go b/src/testing/export_test.go index 0022491ecd..10a5b04aee 100644 --- a/src/testing/export_test.go +++ b/src/testing/export_test.go @@ -5,3 +5,7 @@ package testing var PrettyPrint = prettyPrint + +type HighPrecisionTime = highPrecisionTime + +var HighPrecisionTimeNow = highPrecisionTimeNow diff --git a/src/testing/fstest/mapfs.go b/src/testing/fstest/mapfs.go index f7f8ccd9ec..5e3720b0ed 100644 --- a/src/testing/fstest/mapfs.go +++ b/src/testing/fstest/mapfs.go @@ -8,7 +8,7 @@ import ( "io" "io/fs" "path" - "sort" + "slices" "strings" "time" ) @@ -100,8 +100,8 @@ func (fsys MapFS) Open(name string) (fs.File, error) { for name := range need { list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir | 0555}}) } - sort.Slice(list, func(i, j int) bool { - return list[i].name < list[j].name + slices.SortFunc(list, func(a, b mapFileInfo) int { + return strings.Compare(a.name, b.name) }) if file == nil { diff --git a/src/testing/fstest/testfs.go b/src/testing/fstest/testfs.go index 0fd7d4806f..080bcdd65f 100644 --- a/src/testing/fstest/testfs.go +++ b/src/testing/fstest/testfs.go @@ -12,7 +12,7 @@ import ( "io/fs" "path" "reflect" - "sort" + "slices" "strings" "testing/iotest" ) @@ -78,7 +78,7 @@ func testFS(fsys fs.FS, expected ...string) error { list = append(list, k) } } - sort.Strings(list) + slices.Sort(list) if len(list) > 15 { list = append(list[:10], "...") } @@ -362,9 +362,9 @@ func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) { return } - if !sort.StringsAreSorted(names) { + if !slices.IsSorted(names) { t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n")) - sort.Strings(names) + slices.Sort(names) } var problems []string @@ -488,11 +488,11 @@ func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) { return } - sort.Slice(diffs, func(i, j int) bool { - fi := strings.Fields(diffs[i]) - fj := strings.Fields(diffs[j]) + slices.SortFunc(diffs, func(a, b string) int { + fa := strings.Fields(a) + fb := strings.Fields(b) // sort by name (i < j) and then +/- (j < i, because + < -) - return fi[1]+" "+fj[0] < fj[1]+" "+fi[0] + return strings.Compare(fa[1]+" "+fb[0], fb[1]+" "+fa[0]) }) t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t")) diff --git a/src/testing/fstest/testfs_test.go b/src/testing/fstest/testfs_test.go index b9f10c613a..2ef1053a01 100644 --- a/src/testing/fstest/testfs_test.go +++ b/src/testing/fstest/testfs_test.go @@ -10,7 +10,8 @@ import ( "io/fs" "os" "path/filepath" - "sort" + "slices" + "strings" "testing" ) @@ -61,8 +62,8 @@ func (f *shuffledFile) ReadDir(n int) ([]fs.DirEntry, error) { // // We do this to make sure that the TestFS test suite is not affected by the // order of directory entries. - sort.Slice(dirents, func(i, j int) bool { - return dirents[i].Name() > dirents[j].Name() + slices.SortFunc(dirents, func(a, b fs.DirEntry) int { + return strings.Compare(b.Name(), a.Name()) }) return dirents, err } diff --git a/src/testing/fuzz.go b/src/testing/fuzz.go index baf1c7243c..d561225b3c 100644 --- a/src/testing/fuzz.go +++ b/src/testing/fuzz.go @@ -674,7 +674,7 @@ func fRunner(f *F, fn func(*F)) { } for root := &f.common; root.parent != nil; root = root.parent { root.mu.Lock() - root.duration += time.Since(root.start) + root.duration += highPrecisionTimeSince(root.start) d := root.duration root.mu.Unlock() root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d)) @@ -687,7 +687,7 @@ func fRunner(f *F, fn func(*F)) { } // No panic or inappropriate Goexit. - f.duration += time.Since(f.start) + f.duration += highPrecisionTimeSince(f.start) if len(f.sub) > 0 { // Unblock inputs that called T.Parallel while running the seed corpus. @@ -700,9 +700,9 @@ func fRunner(f *F, fn func(*F)) { for _, sub := range f.sub { <-sub.signal } - cleanupStart := time.Now() + cleanupStart := highPrecisionTimeNow() err := f.runCleanup(recoverAndReturnPanic) - f.duration += time.Since(cleanupStart) + f.duration += highPrecisionTimeSince(cleanupStart) if err != nil { doPanic(err) } @@ -719,7 +719,7 @@ func fRunner(f *F, fn func(*F)) { } }() - f.start = time.Now() + f.start = highPrecisionTimeNow() f.resetRaces() fn(f) diff --git a/src/testing/internal/testdeps/deps.go b/src/testing/internal/testdeps/deps.go index 868307550e..3b5dc7198f 100644 --- a/src/testing/internal/testdeps/deps.go +++ b/src/testing/internal/testdeps/deps.go @@ -26,6 +26,9 @@ import ( "time" ) +// Cover indicates whether coverage is enabled. +var Cover bool + // TestDeps is an implementation of the testing.testDeps interface, // suitable for passing to [testing.MainStart]. type TestDeps struct{} @@ -197,3 +200,40 @@ func (TestDeps) ResetCoverage() { func (TestDeps) SnapshotCoverage() { fuzz.SnapshotCoverage() } + +var CoverMode string +var Covered string + +// These variables below are set at runtime (via code in testmain) to point +// to the equivalent functions in package internal/coverage/cfile; doing +// things this way allows us to have tests import internal/coverage/cfile +// only when -cover is in effect (as opposed to importing for all tests). +var ( + CoverSnapshotFunc func() float64 + CoverProcessTestDirFunc func(dir string, cfile string, cm string, cpkg string, w io.Writer) error + CoverMarkProfileEmittedFunc func(val bool) +) + +func (TestDeps) InitRuntimeCoverage() (mode string, tearDown func(string, string) (string, error), snapcov func() float64) { + if CoverMode == "" { + return + } + return CoverMode, coverTearDown, CoverSnapshotFunc +} + +func coverTearDown(coverprofile string, gocoverdir string) (string, error) { + var err error + if gocoverdir == "" { + gocoverdir, err = os.MkdirTemp("", "gocoverdir") + if err != nil { + return "error setting GOCOVERDIR: bad os.MkdirTemp return", err + } + defer os.RemoveAll(gocoverdir) + } + CoverMarkProfileEmittedFunc(true) + cmode := CoverMode + if err := CoverProcessTestDirFunc(gocoverdir, coverprofile, cmode, Covered, os.Stdout); err != nil { + return "error generating coverage report", err + } + return "", nil +} diff --git a/src/testing/newcover.go b/src/testing/newcover.go index 6199f3bd7b..ad2f622640 100644 --- a/src/testing/newcover.go +++ b/src/testing/newcover.go @@ -10,6 +10,7 @@ import ( "fmt" "internal/goexperiment" "os" + _ "unsafe" // for linkname ) // cover2 variable stores the current coverage mode and a @@ -20,10 +21,13 @@ var cover2 struct { snapshotcov func() float64 } -// registerCover2 is invoked during "go test -cover" runs by the test harness -// code in _testmain.go; it is used to record a 'tear down' function +// registerCover2 is invoked during "go test -cover" runs. +// It is used to record a 'tear down' function // (to be called when the test is complete) and the coverage mode. func registerCover2(mode string, tearDown func(coverprofile string, gocoverdir string) (string, error), snapcov func() float64) { + if mode == "" { + return + } cover2.mode = mode cover2.tearDown = tearDown cover2.snapshotcov = snapcov @@ -42,13 +46,6 @@ func coverReport2() { } } -// testGoCoverDir returns the value passed to the -test.gocoverdir -// flag by the Go command, if goexperiment.CoverageRedesign is -// in effect. -func testGoCoverDir() string { - return *gocoverdir -} - // coverage2 returns a rough "coverage percentage so far" // number to support the testing.Coverage() function. func coverage2() float64 { diff --git a/src/testing/testing.go b/src/testing/testing.go index 9c1325a609..200fa659b8 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -72,14 +72,15 @@ // A sample benchmark function looks like this: // // func BenchmarkRandInt(b *testing.B) { -// for i := 0; i < b.N; i++ { +// for range b.N { // rand.Int() // } // } // // The benchmark function must run the target code b.N times. -// During benchmark execution, b.N is adjusted until the benchmark function lasts -// long enough to be timed reliably. The output +// It is called multiple times with b.N adjusted until the +// benchmark function lasts long enough to be timed reliably. +// The output // // BenchmarkRandInt-8 68453040 17.8 ns/op // @@ -91,7 +92,7 @@ // func BenchmarkBigLen(b *testing.B) { // big := NewBig() // b.ResetTimer() -// for i := 0; i < b.N; i++ { +// for range b.N { // big.Len() // } // } @@ -382,7 +383,7 @@ import ( "runtime" "runtime/debug" "runtime/trace" - "sort" + "slices" "strconv" "strings" "sync" @@ -615,10 +616,10 @@ type common struct { 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 time.Time // Time test or benchmark started + 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. @@ -1457,7 +1458,7 @@ func (t *T) Parallel() { // We don't want to include the time we spend waiting for serial tests // in the test duration. Record the elapsed time thus far and reset the // timer afterwards. - t.duration += time.Since(t.start) + t.duration += highPrecisionTimeSince(t.start) // Add to the list of tests to be released by the parent. t.parent.sub = append(t.parent.sub, t) @@ -1486,8 +1487,8 @@ func (t *T) Parallel() { if t.chatty != nil { t.chatty.Updatef(t.name, "=== CONT %s\n", t.name) } - running.Store(t.name, time.Now()) - t.start = time.Now() + running.Store(t.name, highPrecisionTimeNow()) + t.start = highPrecisionTimeNow() // Reset the local race counter to ignore any races that happened while this // goroutine was blocked, such as in the parent test or in other parallel @@ -1619,7 +1620,7 @@ func tRunner(t *T, fn func(t *T)) { // Flush the output log up to the root before dying. for root := &t.common; root.parent != nil; root = root.parent { root.mu.Lock() - root.duration += time.Since(root.start) + root.duration += highPrecisionTimeSince(root.start) d := root.duration root.mu.Unlock() root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d)) @@ -1634,7 +1635,7 @@ func tRunner(t *T, fn func(t *T)) { doPanic(err) } - t.duration += time.Since(t.start) + t.duration += highPrecisionTimeSince(t.start) if len(t.sub) > 0 { // Run parallel subtests. @@ -1652,10 +1653,10 @@ func tRunner(t *T, fn func(t *T)) { // Run any cleanup callbacks, marking the test as running // in case the cleanup hangs. - cleanupStart := time.Now() + cleanupStart := highPrecisionTimeNow() running.Store(t.name, cleanupStart) err := t.runCleanup(recoverAndReturnPanic) - t.duration += time.Since(cleanupStart) + t.duration += highPrecisionTimeSince(cleanupStart) if err != nil { doPanic(err) } @@ -1684,7 +1685,7 @@ func tRunner(t *T, fn func(t *T)) { } }() - t.start = time.Now() + t.start = highPrecisionTimeNow() t.resetRaces() fn(t) @@ -1732,7 +1733,7 @@ func (t *T) Run(name string, f func(t *T)) bool { if t.chatty != nil { t.chatty.Updatef(t.name, "=== RUN %s\n", t.name) } - running.Store(t.name, time.Now()) + running.Store(t.name, highPrecisionTimeNow()) // Instead of reducing the running count of this test before calling the // tRunner and increasing it afterwards, we rely on tRunner keeping the @@ -1854,6 +1855,10 @@ func (f matchStringOnly) CheckCorpus([]any, []reflect.Type) error { return nil } func (f matchStringOnly) ResetCoverage() {} func (f matchStringOnly) SnapshotCoverage() {} +func (f matchStringOnly) InitRuntimeCoverage() (mode string, tearDown func(string, string) (string, error), snapcov func() float64) { + return +} + // Main is an internal function, part of the implementation of the "go test" command. // It was exported because it is cross-package and predates "internal" packages. // It is no longer used by "go test" but preserved, as much as possible, for other @@ -1901,12 +1906,14 @@ type testDeps interface { CheckCorpus([]any, []reflect.Type) error ResetCoverage() SnapshotCoverage() + InitRuntimeCoverage() (mode string, tearDown func(coverprofile string, gocoverdir string) (string, error), snapcov func() float64) } // MainStart is meant for use by tests generated by 'go test'. // It is not meant to be called directly and is not subject to the Go 1 compatibility document. // It may change signature from release to release. func MainStart(deps testDeps, tests []InternalTest, benchmarks []InternalBenchmark, fuzzTargets []InternalFuzzTarget, examples []InternalExample) *M { + registerCover2(deps.InitRuntimeCoverage()) Init() return &M{ deps: deps, @@ -2372,10 +2379,10 @@ func (m *M) startAlarm() time.Time { func runningList() []string { var list []string running.Range(func(k, v any) bool { - list = append(list, fmt.Sprintf("%s (%v)", k.(string), time.Since(v.(time.Time)).Round(time.Second))) + list = append(list, fmt.Sprintf("%s (%v)", k.(string), highPrecisionTimeSince(v.(highPrecisionTime)).Round(time.Second))) return true }) - sort.Strings(list) + slices.Sort(list) return list } diff --git a/src/testing/testing_other.go b/src/testing/testing_other.go index 99a6276a4a..f91e3b4a2c 100644 --- a/src/testing/testing_other.go +++ b/src/testing/testing_other.go @@ -6,8 +6,26 @@ package testing +import "time" + // isWindowsRetryable reports whether err is a Windows error code // that may be fixed by retrying a failed filesystem operation. func isWindowsRetryable(err error) bool { return false } + +// highPrecisionTime represents a single point in time. +// On all systems except Windows, using time.Time is fine. +type highPrecisionTime struct { + now time.Time +} + +// highPrecisionTimeNow returns high precision time for benchmarking. +func highPrecisionTimeNow() highPrecisionTime { + return highPrecisionTime{now: time.Now()} +} + +// highPrecisionTimeSince returns duration since b. +func highPrecisionTimeSince(b highPrecisionTime) time.Duration { + return time.Since(b.now) +} diff --git a/src/testing/testing_test.go b/src/testing/testing_test.go index d3822dfd57..4a9303952e 100644 --- a/src/testing/testing_test.go +++ b/src/testing/testing_test.go @@ -762,7 +762,8 @@ func parseRunningTests(out []byte) (runningTests []string, ok bool) { inRunningTests := false for _, line := range strings.Split(string(out), "\n") { if inRunningTests { - if trimmed, ok := strings.CutPrefix(line, "\t"); ok { + // Package testing adds one tab, the panic printer adds another. + if trimmed, ok := strings.CutPrefix(line, "\t\t"); ok { if name, _, ok := strings.Cut(trimmed, " "); ok { runningTests = append(runningTests, name) continue diff --git a/src/testing/testing_windows.go b/src/testing/testing_windows.go index fd48ae9579..ebe4e01d23 100644 --- a/src/testing/testing_windows.go +++ b/src/testing/testing_windows.go @@ -9,7 +9,9 @@ package testing import ( "errors" "internal/syscall/windows" + "math/bits" "syscall" + "time" ) // isWindowsRetryable reports whether err is a Windows error code @@ -30,3 +32,39 @@ func isWindowsRetryable(err error) bool { } return false } + +// highPrecisionTime represents a single point in time with query performance counter. +// time.Time on Windows has low system granularity, which is not suitable for +// measuring short time intervals. +// +// TODO: If Windows runtime implements high resolution timing then highPrecisionTime +// can be removed. +type highPrecisionTime struct { + now int64 +} + +// highPrecisionTimeNow returns high precision time for benchmarking. +func highPrecisionTimeNow() highPrecisionTime { + var t highPrecisionTime + // This should always succeed for Windows XP and above. + t.now = windows.QueryPerformanceCounter() + return t +} + +func (a highPrecisionTime) sub(b highPrecisionTime) time.Duration { + delta := a.now - b.now + + if queryPerformanceFrequency == 0 { + queryPerformanceFrequency = windows.QueryPerformanceFrequency() + } + hi, lo := bits.Mul64(uint64(delta), uint64(time.Second)/uint64(time.Nanosecond)) + quo, _ := bits.Div64(hi, lo, uint64(queryPerformanceFrequency)) + return time.Duration(quo) +} + +var queryPerformanceFrequency int64 + +// highPrecisionTimeSince returns duration since a. +func highPrecisionTimeSince(a highPrecisionTime) time.Duration { + return highPrecisionTimeNow().sub(a) +} diff --git a/src/testing/testing_windows_test.go b/src/testing/testing_windows_test.go new file mode 100644 index 0000000000..e75232dede --- /dev/null +++ b/src/testing/testing_windows_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testing_test + +import ( + "testing" + "time" +) + +var sink time.Time +var sinkHPT testing.HighPrecisionTime + +func BenchmarkTimeNow(b *testing.B) { + for i := 0; i < b.N; i++ { + sink = time.Now() + } +} + +func BenchmarkHighPrecisionTimeNow(b *testing.B) { + for i := 0; i < b.N; i++ { + sinkHPT = testing.HighPrecisionTimeNow() + } +} |