Skip to content

Commit 6a4bc8d

Browse files
ryancurrahmatloob
authored andcommitted
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 below that was created/closed/abandoned by @jproberts and @macnibblet that I plan to maintain. - #50483 - #65657 I made improvements to the change based on feedback from @bcmills in Gerrit 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 Change-Id: I59a20af5ea156f990a17544cf06dc667ae7f8aa3 GitHub-Last-Rev: a5a1d1b GitHub-Pull-Request: #69339 Reviewed-on: https://go-review.googlesource.com/c/go/+/610564 Reviewed-by: Michael Matloob <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Reviewed-by: Junyang Shao <[email protected]>
1 parent 938b6c1 commit 6a4bc8d

File tree

4 files changed

+124
-18
lines changed

4 files changed

+124
-18
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/internal/test/cover.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ func initCoverProfile() {
4444
}
4545

4646
// mergeCoverProfile merges file into the profile stored in testCoverProfile.
47-
// It prints any errors it encounters to ew.
48-
func mergeCoverProfile(ew io.Writer, file string) {
47+
// Errors encountered are logged and cause a non-zero exit status.
48+
func mergeCoverProfile(file string) {
4949
if coverMerge.f == nil {
5050
return
5151
}
@@ -66,12 +66,13 @@ func mergeCoverProfile(ew io.Writer, file string) {
6666
return
6767
}
6868
if err != nil || string(buf) != expect {
69-
fmt.Fprintf(ew, "error: test wrote malformed coverage profile %s.\n", file)
69+
base.Errorf("test wrote malformed coverage profile %s: header %q, expected %q: %v", file, string(buf), expect, err)
7070
return
7171
}
7272
_, err = io.Copy(coverMerge.f, r)
7373
if err != nil {
74-
fmt.Fprintf(ew, "error: saving coverage profile: %v\n", err)
74+
base.Errorf("saving coverage profile: %v", err)
75+
return
7576
}
7677
}
7778

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

Lines changed: 52 additions & 12 deletions
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, -skip, -timeout, -failfast, -fullpath and -v.
129+
restricted set of 'cacheable' test flags, defined as -benchtime,
130+
-coverprofile, -cpu, -failfast, -fullpath, -list, -outputdir, -parallel,
131+
-run, -short, -skip, -timeout 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
@@ -1375,6 +1376,13 @@ type runCache struct {
13751376
id2 cache.ActionID
13761377
}
13771378

1379+
func coverProfTempFile(a *work.Action) string {
1380+
if a.Objdir == "" {
1381+
panic("internal error: objdir not set in coverProfTempFile")
1382+
}
1383+
return a.Objdir + "_cover_.out"
1384+
}
1385+
13781386
// stdoutMu and lockedStdout provide a locked standard output
13791387
// that guarantees never to interlace writes from multiple
13801388
// goroutines, so that we can have multiple JSON streams writing
@@ -1476,13 +1484,6 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
14761484
return nil
14771485
}
14781486

1479-
coverProfTempFile := func(a *work.Action) string {
1480-
if a.Objdir == "" {
1481-
panic("internal error: objdir not set in coverProfTempFile")
1482-
}
1483-
return a.Objdir + "_cover_.out"
1484-
}
1485-
14861487
if p := a.Package; len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
14871488
reportNoTestFiles := true
14881489
if cfg.BuildCover && p.Internal.Cover.GenMeta {
@@ -1506,7 +1507,7 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
15061507
if err := work.WriteCoverageProfile(b, a, mf, cp, stdout); err != nil {
15071508
return err
15081509
}
1509-
mergeCoverProfile(stdout, cp)
1510+
mergeCoverProfile(cp)
15101511
}
15111512
}
15121513
}
@@ -1669,7 +1670,7 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
16691670
a.TestOutput = &buf
16701671
t := fmt.Sprintf("%.3fs", time.Since(t0).Seconds())
16711672

1672-
mergeCoverProfile(cmd.Stdout, a.Objdir+"_cover_.out")
1673+
mergeCoverProfile(coverProfTempFile(a))
16731674

16741675
if err == nil {
16751676
norun := ""
@@ -1790,7 +1791,11 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
17901791
// Note that this list is documented above,
17911792
// so if you add to this list, update the docs too.
17921793
cacheArgs = append(cacheArgs, arg)
1793-
1794+
case "-test.coverprofile",
1795+
"-test.outputdir":
1796+
// These are cacheable and do not invalidate the cache when they change.
1797+
// Note that this list is documented above,
1798+
// so if you add to this list, update the docs too.
17941799
default:
17951800
// nothing else is cacheable
17961801
if cache.DebugTest {
@@ -1862,6 +1867,20 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
18621867
// Parse cached result in preparation for changing run time to "(cached)".
18631868
// If we can't parse the cached result, don't use it.
18641869
data, entry, err = cache.GetBytes(cache.Default(), testAndInputKey(testID, testInputsID))
1870+
1871+
// Merge cached cover profile data to cover profile.
1872+
if testCoverProfile != "" {
1873+
// Specifically ignore entry as it will be the same as above.
1874+
cpData, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID))
1875+
if err != nil {
1876+
if cache.DebugTest {
1877+
fmt.Fprintf(os.Stderr, "testcache: %s: cached cover profile missing: %v\n", a.Package.ImportPath, err)
1878+
}
1879+
return false
1880+
}
1881+
mergeCoverProfile(cpData)
1882+
}
1883+
18651884
if len(data) == 0 || data[len(data)-1] != '\n' {
18661885
if cache.DebugTest {
18671886
if err != nil {
@@ -2050,6 +2069,11 @@ func testAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
20502069
return cache.Subkey(testID, fmt.Sprintf("inputs:%x", testInputsID))
20512070
}
20522071

2072+
// coverProfileAndInputKey returns the "coverprofile" cache key for the pair (testID, testInputsID).
2073+
func coverProfileAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
2074+
return cache.Subkey(testAndInputKey(testID, testInputsID), "coverprofile")
2075+
}
2076+
20532077
func (c *runCache) saveOutput(a *work.Action) {
20542078
if c.id1 == (cache.ActionID{}) && c.id2 == (cache.ActionID{}) {
20552079
return
@@ -2071,19 +2095,35 @@ func (c *runCache) saveOutput(a *work.Action) {
20712095
if err != nil {
20722096
return
20732097
}
2098+
var coverProfile []byte
2099+
if testCoverProfile != "" {
2100+
coverProfile, err = os.ReadFile(coverProfTempFile(a))
2101+
if err != nil {
2102+
if cache.DebugTest {
2103+
fmt.Fprintf(os.Stderr, "testcache: %s: reading cover profile: %v\n", a.Package.ImportPath, err)
2104+
}
2105+
return
2106+
}
2107+
}
20742108
if c.id1 != (cache.ActionID{}) {
20752109
if cache.DebugTest {
20762110
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))
20772111
}
20782112
cache.PutNoVerify(cache.Default(), c.id1, bytes.NewReader(testlog))
20792113
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id1, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
2114+
if coverProfile != nil {
2115+
cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID), bytes.NewReader(coverProfile))
2116+
}
20802117
}
20812118
if c.id2 != (cache.ActionID{}) {
20822119
if cache.DebugTest {
20832120
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))
20842121
}
20852122
cache.PutNoVerify(cache.Default(), c.id2, bytes.NewReader(testlog))
20862123
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id2, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
2124+
if coverProfile != nil {
2125+
cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID), bytes.NewReader(coverProfile))
2126+
}
20872127
}
20882128
}
20892129

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,46 @@ go test testcache -run=TestOdd -skip=TestOddFile
134134
go test testcache -run=TestOdd -skip=TestOddFile
135135
stdout '\(cached\)'
136136

137+
# Ensure that coverage profiles are being cached.
138+
go test testcache -run=TestCoverageCache -coverprofile=coverage.out
139+
go test testcache -run=TestCoverageCache -coverprofile=coverage.out
140+
stdout '\(cached\)'
141+
exists coverage.out
142+
grep -q 'mode: set' coverage.out
143+
grep -q 'testcache/hello.go:' coverage.out
144+
145+
# A new -coverprofile file should use the cached coverage profile contents.
146+
go test testcache -run=TestCoverageCache -coverprofile=coverage2.out
147+
stdout '\(cached\)'
148+
cmp coverage.out coverage2.out
149+
150+
# Explicitly setting the default covermode should still use cache.
151+
go test testcache -run=TestCoverageCache -coverprofile=coverage_set.out -covermode=set
152+
stdout '\(cached\)'
153+
cmp coverage.out coverage_set.out
154+
155+
# A new -covermode should not use the cached coverage profile.
156+
go test testcache -run=TestCoverageCache -coverprofile=coverage_atomic.out -covermode=atomic
157+
! stdout '\(cached\)'
158+
! cmp coverage.out coverage_atomic.out
159+
grep -q 'mode: atomic' coverage_atomic.out
160+
grep -q 'testcache/hello.go:' coverage_atomic.out
161+
162+
# A new -coverpkg should not use the cached coverage profile.
163+
go test testcache -run=TestCoverageCache -coverprofile=coverage_pkg.out -coverpkg=all
164+
! stdout '\(cached\)'
165+
! cmp coverage.out coverage_pkg.out
166+
167+
# Test that -v doesn't prevent caching.
168+
go test testcache -v -run=TestCoverageCache -coverprofile=coverage_v.out
169+
go test testcache -v -run=TestCoverageCache -coverprofile=coverage_v2.out
170+
stdout '\(cached\)'
171+
cmp coverage_v.out coverage_v2.out
172+
173+
# Test that -count affects caching.
174+
go test testcache -run=TestCoverageCache -coverprofile=coverage_count.out -count=2
175+
! stdout '\(cached\)'
176+
137177
# Executables within GOROOT and GOPATH should affect caching,
138178
# even if the test does not stat them explicitly.
139179

@@ -164,6 +204,18 @@ This file is outside of GOPATH.
164204
-- testcache/script.sh --
165205
#!/bin/sh
166206
exit 0
207+
-- testcache/hello.go --
208+
package testcache
209+
210+
import "fmt"
211+
212+
func HelloWorld(name string) string {
213+
if name == "" {
214+
return "Hello, World!"
215+
}
216+
return fmt.Sprintf("Hello, %s!", name)
217+
}
218+
167219
-- testcache/testcache_test.go --
168220
// Copyright 2017 The Go Authors. All rights reserved.
169221
// Use of this source code is governed by a BSD-style
@@ -267,6 +319,18 @@ func TestOSArgs(t *testing.T) {
267319
func TestBenchtime(t *testing.T) {
268320
}
269321

322+
func TestCoverageCache(t *testing.T) {
323+
result := HelloWorld("")
324+
if result != "Hello, World!" {
325+
t.Errorf("Expected 'Hello, World!', got '%s'", result)
326+
}
327+
328+
result = HelloWorld("Go")
329+
if result != "Hello, Go!" {
330+
t.Errorf("Expected 'Hello, Go!', got '%s'", result)
331+
}
332+
}
333+
270334
-- mkold.go --
271335
package main
272336

0 commit comments

Comments
 (0)