Skip to content

Commit cdd481a

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. Note: This is a rebase and squash from the original PRs #50483 and #65657 that was created/closed/abandoned by @jproberts and @macnibblet that I plan to maintain. I made improvements to the PR based on feedback from @bcmills here https://go-review.googlesource.com/c/go/+/563138. From @macnibblet: I don't know if anyone has considered the environmental impact (Yes, of course, dev experience too), but on a team with 3 backend developers, when I replaced our CI Golang version with this build, it reduced the build time by 50%, which would have equated to about 5000 hours of CI reduced in the past year. Fixes #23565 Signed-off-by: Ryan Currah <[email protected]>
1 parent 464aae7 commit cdd481a

File tree

4 files changed

+144
-24
lines changed

4 files changed

+144
-24
lines changed

src/cmd/go/alldocs.go

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ import (
1414
"sync"
1515
)
1616

17+
// coverMerge manages the state for merging test coverage profiles.
18+
// It ensures thread-safe operations on a single coverage profile file
19+
// across multiple test runs and packages.
1720
var coverMerge struct {
18-
f *os.File
19-
sync.Mutex // for f.Write
21+
f *os.File
22+
fsize int64 // Tracks the size of valid data written to f
23+
sync.Mutex
2024
}
2125

2226
// initCoverProfile initializes the test coverage profile.
@@ -36,16 +40,17 @@ func initCoverProfile() {
3640
if err != nil {
3741
base.Fatalf("%v", err)
3842
}
39-
_, err = fmt.Fprintf(f, "mode: %s\n", cfg.BuildCoverMode)
43+
s, err := fmt.Fprintf(f, "mode: %s\n", cfg.BuildCoverMode)
4044
if err != nil {
4145
base.Fatalf("%v", err)
4246
}
4347
coverMerge.f = f
48+
coverMerge.fsize = int64(s)
4449
}
4550

4651
// mergeCoverProfile merges file into the profile stored in testCoverProfile.
4752
// It prints any errors it encounters to ew.
48-
func mergeCoverProfile(ew io.Writer, file string) {
53+
func mergeCoverProfile(file string) {
4954
if coverMerge.f == nil {
5055
return
5156
}
@@ -66,19 +71,25 @@ func mergeCoverProfile(ew io.Writer, file string) {
6671
return
6772
}
6873
if err != nil || string(buf) != expect {
69-
fmt.Fprintf(ew, "error: test wrote malformed coverage profile %s.\n", file)
74+
base.Errorf("error: test wrote malformed coverage profile %s: header %q, expected %q: %v", file, string(buf), expect, err)
7075
return
7176
}
72-
_, err = io.Copy(coverMerge.f, r)
77+
m, err := io.Copy(coverMerge.f, r)
7378
if err != nil {
74-
fmt.Fprintf(ew, "error: saving coverage profile: %v\n", err)
79+
base.Errorf("error: saving coverage profile: %v", err)
80+
return
7581
}
82+
coverMerge.fsize += m
7683
}
7784

7885
func closeCoverProfile() {
7986
if coverMerge.f == nil {
8087
return
8188
}
89+
// Discard any partially written data from a failed merge.
90+
if err := coverMerge.f.Truncate(coverMerge.fsize); err != nil {
91+
base.Errorf("closing coverage profile: %v", err)
92+
}
8293
if err := coverMerge.f.Close(); err != nil {
8394
base.Errorf("closing coverage profile: %v", err)
8495
}

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

+65-15
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ elapsed time in the summary line.
126126
127127
The rule for a match in the cache is that the run involves the same
128128
test binary and the flags on the command line come entirely from a
129-
restricted set of 'cacheable' test flags, defined as -benchtime, -cpu,
130-
-list, -parallel, -run, -short, -timeout, -failfast, -fullpath and -v.
129+
restricted set of 'cacheable' test flags, defined as -benchtime, -cover,
130+
-covermode, -coverprofile, -cpu, -list, -parallel, -run, -short, -timeout,
131+
-failfast, -fullpath and -v.
131132
If a run of go test has any test or non-test flags outside this set,
132133
the result is not cached. To disable test caching, use any test flag
133134
or argument other than the cacheable flags. The idiomatic way to disable
@@ -1338,6 +1339,13 @@ type runCache struct {
13381339
id2 cache.ActionID
13391340
}
13401341

1342+
func coverProfTempFile(a *work.Action) string {
1343+
if a.Objdir == "" {
1344+
panic("internal error: objdir not set in coverProfTempFile")
1345+
}
1346+
return a.Objdir + "_cover_.out"
1347+
}
1348+
13411349
// stdoutMu and lockedStdout provide a locked standard output
13421350
// that guarantees never to interlace writes from multiple
13431351
// goroutines, so that we can have multiple JSON streams writing
@@ -1396,13 +1404,6 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
13961404
return nil
13971405
}
13981406

1399-
coverProfTempFile := func(a *work.Action) string {
1400-
if a.Objdir == "" {
1401-
panic("internal error: objdir not set in coverProfTempFile")
1402-
}
1403-
return a.Objdir + "_cover_.out"
1404-
}
1405-
14061407
if p := a.Package; len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
14071408
reportNoTestFiles := true
14081409
if cfg.BuildCover && cfg.Experiment.CoverageRedesign && p.Internal.Cover.GenMeta {
@@ -1426,7 +1427,7 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
14261427
if err := work.WriteCoverageProfile(b, a, mf, cp, stdout); err != nil {
14271428
return err
14281429
}
1429-
mergeCoverProfile(stdout, cp)
1430+
mergeCoverProfile(cp)
14301431
}
14311432
}
14321433
}
@@ -1616,7 +1617,7 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
16161617
a.TestOutput = &buf
16171618
t := fmt.Sprintf("%.3fs", time.Since(t0).Seconds())
16181619

1619-
mergeCoverProfile(cmd.Stdout, a.Objdir+"_cover_.out")
1620+
mergeCoverProfile(a.Objdir + "_cover_.out")
16201621

16211622
if err == nil {
16221623
norun := ""
@@ -1638,10 +1639,10 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
16381639
cmd.Stdout.Write([]byte("\n"))
16391640
}
16401641
fmt.Fprintf(cmd.Stdout, "ok \t%s\t%s%s%s\n", a.Package.ImportPath, t, coveragePercentage(out), norun)
1641-
r.c.saveOutput(a)
1642+
r.c.saveOutput(a, coverProfTempFile(a))
16421643
} else {
16431644
if testFailFast {
1644-
testShouldFailFast.Store(true)
1645+
r.c.saveOutput(a, coverProfTempFile(a))
16451646
}
16461647

16471648
base.SetExitStatus(1)
@@ -1736,7 +1737,18 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
17361737
// Note that this list is documented above,
17371738
// so if you add to this list, update the docs too.
17381739
cacheArgs = append(cacheArgs, arg)
1739-
1740+
case "-test.coverprofile",
1741+
"-test.outputdir":
1742+
// The `-coverprofile` and `-outputdir` arguments, which
1743+
// only affect the location of profile output, are cacheable. This
1744+
// is based on the process where, upon a cache hit, stored profile
1745+
// data is copied to the specified output location. This mechanism
1746+
// ensures that output location preferences are honored without
1747+
// modifying the profile's content, thereby justifying their
1748+
// cacheability without impacting cache integrity. This allows
1749+
// cached coverage profiles to be written to different files.
1750+
// Note that this list is documented above,
1751+
// so if you add to this list, update the docs too.
17401752
default:
17411753
// nothing else is cacheable
17421754
if cache.DebugTest {
@@ -1848,6 +1860,21 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
18481860
j++
18491861
}
18501862
c.buf.Write(data[j:])
1863+
1864+
// Write coverage data to profile.
1865+
if cfg.BuildCover {
1866+
// The cached coverprofile has the same expiration time as the
1867+
// test result it corresponds to. That time is already checked
1868+
// above, so we can ignore the entry returned by GetFile here.
1869+
f, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID))
1870+
if err != nil {
1871+
if cache.DebugTest {
1872+
fmt.Fprintf(os.Stderr, "testcache: %s: test coverage profile not found: %v\n", a.Package.ImportPath, err)
1873+
}
1874+
return false
1875+
}
1876+
mergeCoverProfile(f)
1877+
}
18511878
return true
18521879
}
18531880

@@ -1996,7 +2023,12 @@ func testAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
19962023
return cache.Subkey(testID, fmt.Sprintf("inputs:%x", testInputsID))
19972024
}
19982025

1999-
func (c *runCache) saveOutput(a *work.Action) {
2026+
// coverProfileAndInputKey returns the "coverprofile" cache key for the pair (testID, testInputsID).
2027+
func coverProfileAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
2028+
return cache.Subkey(testAndInputKey(testID, testInputsID), "coverprofile")
2029+
}
2030+
2031+
func (c *runCache) saveOutput(a *work.Action, coverProfileFile string) {
20002032
if c.id1 == (cache.ActionID{}) && c.id2 == (cache.ActionID{}) {
20012033
return
20022034
}
@@ -2017,19 +2049,37 @@ func (c *runCache) saveOutput(a *work.Action) {
20172049
if err != nil {
20182050
return
20192051
}
2052+
var saveCoverProfile = func(testID cache.ActionID) {}
2053+
if coverProfileFile != "" {
2054+
coverProfile, err := os.Open(coverProfileFile)
2055+
if err == nil {
2056+
saveCoverProfile = func(testID cache.ActionID) {
2057+
cache.Default().Put(coverProfileAndInputKey(testID, testInputsID), coverProfile)
2058+
}
2059+
defer func() {
2060+
if err := coverProfile.Close(); err != nil && cache.DebugTest {
2061+
fmt.Fprintf(os.Stderr, "testcache: %s: closing temporary coverprofile: %v", a.Package.ImportPath, err)
2062+
}
2063+
}()
2064+
} else if cache.DebugTest {
2065+
fmt.Fprintf(os.Stderr, "testcache: %s: failed to open temporary coverprofile: %s", a.Package.ImportPath, err)
2066+
}
2067+
}
20202068
if c.id1 != (cache.ActionID{}) {
20212069
if cache.DebugTest {
20222070
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))
20232071
}
20242072
cache.PutNoVerify(cache.Default(), c.id1, bytes.NewReader(testlog))
20252073
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id1, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
2074+
saveCoverProfile(c.id1)
20262075
}
20272076
if c.id2 != (cache.ActionID{}) {
20282077
if cache.DebugTest {
20292078
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))
20302079
}
20312080
cache.PutNoVerify(cache.Default(), c.id2, bytes.NewReader(testlog))
20322081
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id2, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
2082+
saveCoverProfile(c.id2)
20332083
}
20342084
}
20352085

src/cmd/go/testdata/script/test_cache_inputs.txt

+58
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,40 @@ go test testcache -run=TestOSArgs -fullpath
128128
go test testcache -run=TestOSArgs -fullpath
129129
stdout '\(cached\)'
130130

131+
# Ensure that specifying a cover profile does not prevent test results from being cached.
132+
go test testcache -run=TestCoverageCache -coverprofile=coverage.out
133+
go test testcache -run=TestCoverageCache -coverprofile=coverage.out
134+
stdout '\(cached\)'
135+
136+
# A new -coverprofile file should use the cached coverage profile contents.
137+
go test testcache -run=TestCoverageCache -coverprofile=coverage2.out
138+
stdout '\(cached\)'
139+
cmp coverage.out coverage2.out
140+
141+
# Explicitly setting the default covermode should still use cache
142+
go test testcache -run=TestCoverageCache -coverprofile=coverage3.out -covermode=set
143+
stdout '\(cached\)'
144+
cmp coverage.out coverage3.out
145+
146+
# A new -covermode should not use the cached coverage profile.
147+
go test testcache -run=TestCoverageCache -coverprofile=coverage_set.out -covermode=atomic
148+
! stdout '\(cached\)'
149+
! cmp coverage.out coverage_set.out
150+
151+
# A new -coverpkg should not use the cached coverage profile.
152+
go test testcache -run=TestCoverageCache -coverprofile=coverage_pkg.out -coverpkg=all
153+
! stdout '\(cached\)'
154+
! cmp coverage.out coverage_pkg.out
155+
156+
# Test that -v doesn't prevent caching
157+
go test testcache -v -run=TestCoverageCache -coverprofile=coverage_v.out
158+
go test testcache -v -run=TestCoverageCache -coverprofile=coverage_v2.out
159+
stdout '\(cached\)'
160+
cmp coverage_v.out coverage_v2.out
161+
162+
# Test that -count affects caching
163+
go test testcache -run=TestCoverageCache -coverprofile=coverage_count.out -count=2
164+
! stdout '\(cached\)'
131165

132166
# Executables within GOROOT and GOPATH should affect caching,
133167
# even if the test does not stat them explicitly.
@@ -159,6 +193,18 @@ This file is outside of GOPATH.
159193
-- testcache/script.sh --
160194
#!/bin/sh
161195
exit 0
196+
-- testcache/hello.go --
197+
package testcache
198+
199+
import "fmt"
200+
201+
func HelloWorld(name string) string {
202+
if name == "" {
203+
return "Hello, World!"
204+
}
205+
return fmt.Sprintf("Hello, %s!", name)
206+
}
207+
162208
-- testcache/testcache_test.go --
163209
// Copyright 2017 The Go Authors. All rights reserved.
164210
// Use of this source code is governed by a BSD-style
@@ -262,6 +308,18 @@ func TestOSArgs(t *testing.T) {
262308
func TestBenchtime(t *testing.T) {
263309
}
264310

311+
func TestCoverageCache(t *testing.T) {
312+
result := HelloWorld("")
313+
if result != "Hello, World!" {
314+
t.Errorf("Expected 'Hello, World!', got '%s'", result)
315+
}
316+
317+
result = HelloWorld("Go")
318+
if result != "Hello, Go!" {
319+
t.Errorf("Expected 'Hello, Go!', got '%s'", result)
320+
}
321+
}
322+
265323
-- mkold.go --
266324
package main
267325

0 commit comments

Comments
 (0)