Skip to content

Commit 9ada311

Browse files
committed
cmd/go: cache coverage profile with tests
This CL stores coverage profile data in the GOCACHE under the 'coverprofile' subkey alongside tests. This makes tests which use coverage profiles cacheable. The values of the -coverprofile and -outputdir flags are not included in the cache key to allow cached profile data to be written to any output file. Fixes #23565
1 parent 6178d25 commit 9ada311

File tree

4 files changed

+135
-17
lines changed

4 files changed

+135
-17
lines changed

src/cmd/go/alldocs.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/go_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,6 +2320,68 @@ func TestCacheCoverage(t *testing.T) {
23202320
tg.run("test", "-cover", "-short", "math", "strings")
23212321
}
23222322

2323+
func TestCacheCoverageProfile(t *testing.T) {
2324+
tooSlow(t)
2325+
2326+
if godebug.Get("gocacheverify") == "1" {
2327+
t.Skip("GODEBUG gocacheverify")
2328+
}
2329+
2330+
tg := testgo(t)
2331+
defer tg.cleanup()
2332+
tg.parallel()
2333+
tg.setenv("GOPATH", filepath.Join(tg.pwd(), "testdata"))
2334+
tg.makeTempdir()
2335+
tg.setenv("GOCACHE", tg.path("c1"))
2336+
2337+
// checkProfile asserts that the given profile contains the given mode
2338+
// and coverage lines for all given files.
2339+
checkProfile := func(t *testing.T, profile, mode string, files ...string) {
2340+
t.Helper()
2341+
if out, err := os.ReadFile(profile); err != nil {
2342+
t.Fatalf("failed to open coverprofile: %v", err)
2343+
} else {
2344+
if n := bytes.Count(out, []byte("mode: "+mode)); n != 1 {
2345+
if n == 0 {
2346+
t.Fatalf("missing mode: %s", mode)
2347+
} else {
2348+
t.Fatalf("too many mode: %s", mode)
2349+
}
2350+
}
2351+
for _, fname := range files {
2352+
if !bytes.Contains(out, []byte(fname)) {
2353+
t.Fatalf("missing file in coverprofile: %s", fname)
2354+
}
2355+
}
2356+
}
2357+
}
2358+
2359+
tg.run("test", "-coverprofile="+tg.path("cover.out"), "-x", "-v", "-short", "strings")
2360+
tg.grepStdout(`ok \t`, "expected strings test to succeed")
2361+
checkProfile(t, tg.path("cover.out"), "set", "strings/strings.go")
2362+
// Repeat commands should use the cache.
2363+
tg.run("test", "-coverprofile="+tg.path("cover.out"), "-x", "-v", "-short", "strings")
2364+
tg.grepStdout(`ok \tstrings\t\(cached\)`, "expected strings test results to be cached")
2365+
checkProfile(t, tg.path("cover.out"), "set", "strings/strings.go")
2366+
// Cover profiles should be cached independently. Since strings is already cached,
2367+
// only math should need to run.
2368+
tg.run("test", "-coverprofile="+tg.path("cover.out"), "-x", "-v", "-short", "strings", "math")
2369+
tg.grepStdout(`ok \tstrings\t\(cached\)`, "expected strings test results to be cached")
2370+
checkProfile(t, tg.path("cover.out"), "set", "strings/strings.go", "math/mod.go")
2371+
// A new -coverprofile file should use the cached coverage profile contents.
2372+
tg.run("test", "-coverprofile="+tg.path("cover1.out"), "-x", "-v", "-short", "strings")
2373+
tg.grepStdout(`ok \tstrings\t\(cached\)`, "expected cached strings test results to be used regardless of -coverprofile")
2374+
checkProfile(t, tg.path("cover1.out"), "set", "strings/strings.go")
2375+
// A new -covermode should not use the cached coverage profile, since the covermode changes
2376+
// the profile output.
2377+
tg.run("test", "-covermode=count", "-coverprofile="+tg.path("cover.out"), "-x", "-v", "-short", "strings")
2378+
tg.grepStdoutNot(`ok \tstrings\t\(cached\)`, "cached strings test results should not be used with different -covermode")
2379+
// A new -coverpkg should not use the cached coverage profile, since the coverpkg changes
2380+
// the profile output.
2381+
tg.run("test", "-coverpkg=math", "-coverprofile="+tg.path("cover.out"), "-x", "-v", "-short", "strings")
2382+
tg.grepStdoutNot(`ok \tstrings\t\(cached\)`, "cached strings test results should not be used with different -coverpkg")
2383+
}
2384+
23232385
func TestIssue22588(t *testing.T) {
23242386
// Don't get confused by stderr coming from tools.
23252387
tg := testgo(t)

src/cmd/go/internal/test/cover.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package test
66

77
import (
88
"cmd/go/internal/base"
9+
"errors"
910
"fmt"
1011
"io"
1112
"os"
@@ -43,10 +44,9 @@ func initCoverProfile() {
4344
}
4445

4546
// mergeCoverProfile merges file into the profile stored in testCoverProfile.
46-
// It prints any errors it encounters to ew.
47-
func mergeCoverProfile(ew io.Writer, file string) {
47+
func mergeCoverProfile(file string) error {
4848
if coverMerge.f == nil {
49-
return
49+
return nil
5050
}
5151
coverMerge.Lock()
5252
defer coverMerge.Unlock()
@@ -56,22 +56,22 @@ func mergeCoverProfile(ew io.Writer, file string) {
5656
r, err := os.Open(file)
5757
if err != nil {
5858
// Test did not create profile, which is OK.
59-
return
59+
return nil
6060
}
6161
defer r.Close()
6262

6363
n, err := io.ReadFull(r, buf)
6464
if n == 0 {
65-
return
65+
return nil
6666
}
6767
if err != nil || string(buf) != expect {
68-
fmt.Fprintf(ew, "error: test wrote malformed coverage profile.\n")
69-
return
68+
return errMalformedCoverProfile
7069
}
7170
_, err = io.Copy(coverMerge.f, r)
7271
if err != nil {
73-
fmt.Fprintf(ew, "error: saving coverage profile: %v\n", err)
72+
return fmt.Errorf("saving coverage profile: %v\n", err)
7473
}
74+
return nil
7575
}
7676

7777
func closeCoverProfile() {
@@ -82,3 +82,5 @@ func closeCoverProfile() {
8282
base.Errorf("closing coverage profile: %v", err)
8383
}
8484
}
85+
86+
var errMalformedCoverProfile = errors.New("test wrote malformed coverage profile")

src/cmd/go/internal/test/test.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ elapsed time in the summary line.
123123
124124
The rule for a match in the cache is that the run involves the same
125125
test binary and the flags on the command line come entirely from a
126-
restricted set of 'cacheable' test flags, defined as -benchtime, -cpu,
127-
-list, -parallel, -run, -short, -timeout, -failfast, and -v.
126+
restricted set of 'cacheable' test flags, defined as -benchtime, -cover,
127+
-covermode, -coverprofile, -cpu, -list, -parallel, -run, -short, -timeout,
128+
-failfast, -v, -vet and -outputdir.
128129
If a run of go test has any test or non-test flags outside this set,
129130
the result is not cached. To disable test caching, use any test flag
130131
or argument other than the cacheable flags. The idiomatic way to disable
@@ -1336,11 +1337,13 @@ func (c *runCache) builderRunTest(b *work.Builder, ctx context.Context, a *work.
13361337
}
13371338
args := str.StringList(execCmd, a.Deps[0].BuiltTarget(), testlogArg, panicArg, fuzzArg, testArgs)
13381339

1340+
var coverprofileFile string
13391341
if testCoverProfile != "" {
13401342
// Write coverage to temporary profile, for merging later.
1343+
coverprofileFile = a.Objdir + "_cover_.out"
13411344
for i, arg := range args {
13421345
if strings.HasPrefix(arg, "-test.coverprofile=") {
1343-
args[i] = "-test.coverprofile=" + a.Objdir + "_cover_.out"
1346+
args[i] = "-test.coverprofile=" + coverprofileFile
13441347
}
13451348
}
13461349
}
@@ -1417,7 +1420,9 @@ func (c *runCache) builderRunTest(b *work.Builder, ctx context.Context, a *work.
14171420
a.TestOutput = &buf
14181421
t := fmt.Sprintf("%.3fs", time.Since(t0).Seconds())
14191422

1420-
mergeCoverProfile(cmd.Stdout, a.Objdir+"_cover_.out")
1423+
if coverErr := mergeCoverProfile(coverprofileFile); coverErr != nil {
1424+
fmt.Fprintf(cmd.Stdout, "error: %v\n", coverErr)
1425+
}
14211426

14221427
if err == nil {
14231428
norun := ""
@@ -1439,7 +1444,7 @@ func (c *runCache) builderRunTest(b *work.Builder, ctx context.Context, a *work.
14391444
cmd.Stdout.Write([]byte("\n"))
14401445
}
14411446
fmt.Fprintf(cmd.Stdout, "ok \t%s\t%s%s%s\n", a.Package.ImportPath, t, coveragePercentage(out), norun)
1442-
c.saveOutput(a)
1447+
c.saveOutput(a, coverprofileFile)
14431448
} else {
14441449
base.SetExitStatus(1)
14451450
if len(out) == 0 {
@@ -1520,7 +1525,14 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
15201525
// Note that this list is documented above,
15211526
// so if you add to this list, update the docs too.
15221527
cacheArgs = append(cacheArgs, arg)
1523-
1528+
case "-test.coverprofile",
1529+
"-test.outputdir":
1530+
// The -coverprofile and -outputdir arguments are cacheable but
1531+
// only change where profiles are written. They don't change the
1532+
// profile contents, so they aren't added to the cacheArgs. This
1533+
// allows cached coverage profiles to be written to different files.
1534+
// Note that this list is documented above,
1535+
// so if you add to this list, update the docs too.
15241536
default:
15251537
// nothing else is cacheable
15261538
if cache.DebugTest {
@@ -1640,6 +1652,26 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
16401652
j++
16411653
}
16421654
c.buf.Write(data[j:])
1655+
1656+
// Write coverage data to profile.
1657+
if testCover {
1658+
// coverprofile cache expiration time should be coupled with the test data above, so
1659+
// the entry can be ignored.
1660+
f, _, err := cache.Default().GetFile(testCoverProfileKey(testID, testInputsID))
1661+
if err != nil {
1662+
if cache.DebugTest {
1663+
fmt.Fprintf(os.Stderr, "testcache: %s: test coverage profile not found: %v\n", a.Package.ImportPath, err)
1664+
}
1665+
return false
1666+
}
1667+
if err := mergeCoverProfile(f); err != nil {
1668+
if cache.DebugTest {
1669+
fmt.Fprintf(os.Stderr, "testcache: %s: test coverage profile not merged: %v\n", a.Package.ImportPath, err)
1670+
}
1671+
return false
1672+
}
1673+
}
1674+
16431675
return true
16441676
}
16451677

@@ -1788,7 +1820,12 @@ func testAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
17881820
return cache.Subkey(testID, fmt.Sprintf("inputs:%x", testInputsID))
17891821
}
17901822

1791-
func (c *runCache) saveOutput(a *work.Action) {
1823+
// testCoverProfileKey returns the "coverprofile" cache key for the pair (testID, testInputsID).
1824+
func testCoverProfileKey(testID, testInputsID cache.ActionID) cache.ActionID {
1825+
return cache.Subkey(testAndInputKey(testID, testInputsID), "coverprofile")
1826+
}
1827+
1828+
func (c *runCache) saveOutput(a *work.Action, coverprofileFile string) {
17921829
if c.id1 == (cache.ActionID{}) && c.id2 == (cache.ActionID{}) {
17931830
return
17941831
}
@@ -1809,19 +1846,35 @@ func (c *runCache) saveOutput(a *work.Action) {
18091846
if err != nil {
18101847
return
18111848
}
1849+
saveCoverProfile := func(testID cache.ActionID) {
1850+
if coverprofileFile == "" {
1851+
return
1852+
}
1853+
coverprof, err := os.Open(coverprofileFile)
1854+
if err != nil {
1855+
if cache.DebugTest {
1856+
fmt.Fprintf(os.Stderr, "testcache: %s: failed to open temporary coverprofile file: %s", a.Package.ImportPath, err)
1857+
}
1858+
return
1859+
}
1860+
defer coverprof.Close()
1861+
cache.Default().Put(testCoverProfileKey(testID, testInputsID), coverprof)
1862+
}
18121863
if c.id1 != (cache.ActionID{}) {
18131864
if cache.DebugTest {
18141865
fmt.Fprintf(os.Stderr, "testcache: %s: save test ID %x => input ID %x => %x\n", a.Package.ImportPath, c.id1, testInputsID, testAndInputKey(c.id1, testInputsID))
18151866
}
18161867
cache.Default().PutNoVerify(c.id1, bytes.NewReader(testlog))
18171868
cache.Default().PutNoVerify(testAndInputKey(c.id1, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
1869+
saveCoverProfile(c.id1)
18181870
}
18191871
if c.id2 != (cache.ActionID{}) {
18201872
if cache.DebugTest {
18211873
fmt.Fprintf(os.Stderr, "testcache: %s: save test ID %x => input ID %x => %x\n", a.Package.ImportPath, c.id2, testInputsID, testAndInputKey(c.id2, testInputsID))
18221874
}
18231875
cache.Default().PutNoVerify(c.id2, bytes.NewReader(testlog))
18241876
cache.Default().PutNoVerify(testAndInputKey(c.id2, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
1877+
saveCoverProfile(c.id2)
18251878
}
18261879
}
18271880

0 commit comments

Comments
 (0)