aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cmd/go/internal/test/test.go71
-rw-r--r--src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt134
-rw-r--r--src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt80
3 files changed, 276 insertions, 9 deletions
diff --git a/src/cmd/go/internal/test/test.go b/src/cmd/go/internal/test/test.go
index 5c3093a58d..59474138ad 100644
--- a/src/cmd/go/internal/test/test.go
+++ b/src/cmd/go/internal/test/test.go
@@ -1415,9 +1415,10 @@ type runTestActor struct {
type runCache struct {
disableCache bool // cache should be disabled for this run
- buf *bytes.Buffer
- id1 cache.ActionID
- id2 cache.ActionID
+ buf *bytes.Buffer
+ id1 cache.ActionID
+ id2 cache.ActionID
+ covMeta cache.ActionID // Hash of writeCoverMetaAct dependencies, for invalidating coverage profiles
}
func coverProfTempFile(a *work.Action) string {
@@ -1810,6 +1811,33 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
return false
}
+ // If we are collecting coverage for out-of-band packages (-coverpkg),
+ // find the writeCoverMetaAct among the run action's dependencies and hash
+ // its deps to ensure the cache invalidates when covered packages change.
+ // Note: the run action's original deps may be wrapped inside a "test barrier"
+ // action, so we search both a.Deps and any barrier's deps.
+ if len(testCoverPkgs) != 0 {
+ searchDeps := a.Deps
+ for _, dep := range a.Deps {
+ if dep.Mode == "test barrier" {
+ searchDeps = dep.Deps
+ break
+ }
+ }
+ for _, dep := range searchDeps {
+ if dep.Mode == "write coverage meta-data file" {
+ h := cache.NewHash("covermeta")
+ for _, metaDep := range dep.Deps {
+ if aid := metaDep.BuildActionID(); aid != "" {
+ fmt.Fprintf(h, "dep %s\n", aid)
+ }
+ }
+ c.covMeta = h.Sum()
+ break
+ }
+ }
+ }
+
var cacheArgs []string
for _, arg := range testArgs {
i := strings.Index(arg, "=")
@@ -1916,7 +1944,7 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
// Merge cached cover profile data to cover profile.
if testCoverProfile != "" {
// Specifically ignore entry as it will be the same as above.
- cpData, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID))
+ cpData, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID, c.covMeta))
if err != nil {
if cache.DebugTest {
fmt.Fprintf(os.Stderr, "testcache: %s: cached cover profile missing: %v\n", a.Package.ImportPath, err)
@@ -1924,6 +1952,18 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
return false
}
mergeCoverProfile(cpData)
+ } else if c.covMeta != (cache.ActionID{}) {
+ // If we have a coverage metadata hash but no testCoverProfile, we're collecting
+ // coverage for out-of-band packages. Check if the coverage profile cache is still
+ // valid. If c.covMeta changed (meaning a covered package changed), the coverage
+ // profile cache will miss and we need to re-run the test.
+ _, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID, c.covMeta))
+ if err != nil {
+ if cache.DebugTest {
+ fmt.Fprintf(os.Stderr, "testcache: %s: coverage metadata changed, re-running test: %v\n", a.Package.ImportPath, err)
+ }
+ return false
+ }
}
if len(data) == 0 || data[len(data)-1] != '\n' {
@@ -2114,9 +2154,15 @@ func testAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
return cache.Subkey(testID, fmt.Sprintf("inputs:%x", testInputsID))
}
-// coverProfileAndInputKey returns the "coverprofile" cache key for the pair (testID, testInputsID).
-func coverProfileAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID {
- return cache.Subkey(testAndInputKey(testID, testInputsID), "coverprofile")
+// coverProfileAndInputKey returns the "coverprofile" cache key.
+// If covMetaID is non-zero, it is included in the hash to ensure coverage profiles are invalidated
+// when the coverage metadata changes (e.g., when source files in covered packages are modified).
+func coverProfileAndInputKey(testID, testInputsID, covMetaID cache.ActionID) cache.ActionID {
+ key := testAndInputKey(testID, testInputsID)
+ if covMetaID != (cache.ActionID{}) {
+ key = cache.Subkey(key, fmt.Sprintf("coverdeps:%x", covMetaID))
+ }
+ return cache.Subkey(key, "coverprofile")
}
func (c *runCache) saveOutput(a *work.Action) {
@@ -2157,7 +2203,11 @@ func (c *runCache) saveOutput(a *work.Action) {
cache.PutNoVerify(cache.Default(), c.id1, bytes.NewReader(testlog))
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id1, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
if coverProfile != nil {
- cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID), bytes.NewReader(coverProfile))
+ cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID, c.covMeta), bytes.NewReader(coverProfile))
+ } else if c.covMeta != (cache.ActionID{}) {
+ // Write a sentinel so the else-if branch in tryCacheWithID can verify
+ // that the covMeta hash has not changed since the last run.
+ cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID, c.covMeta), bytes.NewReader(nil))
}
}
if c.id2 != (cache.ActionID{}) {
@@ -2167,7 +2217,10 @@ func (c *runCache) saveOutput(a *work.Action) {
cache.PutNoVerify(cache.Default(), c.id2, bytes.NewReader(testlog))
cache.PutNoVerify(cache.Default(), testAndInputKey(c.id2, testInputsID), bytes.NewReader(a.TestOutput.Bytes()))
if coverProfile != nil {
- cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID), bytes.NewReader(coverProfile))
+ cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID, c.covMeta), bytes.NewReader(coverProfile))
+ } else if c.covMeta != (cache.ActionID{}) {
+ // Sentinel for covMeta validity; see comment in id1 block above.
+ cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID, c.covMeta), bytes.NewReader(nil))
}
}
}
diff --git a/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt b/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt
new file mode 100644
index 0000000000..7f85d0a680
--- /dev/null
+++ b/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt
@@ -0,0 +1,134 @@
+# Test for bug where cached coverage profiles with -coverpkg can contain
+# outdated line references when source files are modified.
+# This reproduces the issue where coverage data from cache may reference
+# lines that no longer exist in the updated source files.
+
+[short] skip
+[GODEBUG:gocacheverify=1] skip
+
+# We're testing cache behavior, so start with a clean GOCACHE.
+env GOCACHE=$WORK/cache
+
+# Create a project structure with multiple packages
+# proj/
+# some_func.go
+# some_func_test.go
+# sub/
+# sub.go
+# sub_test.go
+# sum/
+# sum.go
+
+# Switch to the proj directory
+cd proj
+
+# Run tests with -coverpkg to collect coverage for all packages
+go test -coverpkg=proj/... -coverprofile=cover1.out ./...
+stdout 'coverage:'
+
+# Verify the first coverage profile exists and has expected content
+exists cover1.out
+grep -q 'proj/sub/sub.go:' cover1.out
+
+# Run again to ensure caching works
+go test -coverpkg=proj/... -coverprofile=cover1_cached.out ./...
+stdout '\(cached\)'
+stdout 'coverage:'
+
+# Note: Due to the bug, cached coverage profiles may have duplicate entries.
+# The duplicate entries are the entries for the previous file structure and the new file structure.
+
+# Now modify sub.go to change line structure - this will invalidate
+# the cache for the sub package but not for the proj package.
+cp ../sub_modified.go sub/sub.go
+
+# After modifying sub.go, we should not have both old and new line references.
+go test -coverpkg=proj/... -coverprofile=cover2.out ./...
+stdout 'coverage:'
+
+# With the bug present, we would see duplicate entries for the same lines.
+# With the bug fixed, there should be no duplicate or stale entries in the coverage profile.
+grep 'proj/sub/sub.go:' cover2.out
+# The fix should ensure that only the new line format exists, not the old one
+grep 'proj/sub/sub.go:4.2,4.35' cover2.out
+# This should fail if the stale coverage line exists (the bug is present)
+! grep 'proj/sub/sub.go:4.2,4.22' cover2.out
+
+-- proj/go.mod --
+module proj
+
+go 1.21
+
+-- proj/some_func.go --
+package proj
+
+import "proj/sum"
+
+func SomeFunc(a, b int) int {
+ if a == 0 && b == 0 {
+ return 0
+ }
+ return sum.Sum(a, b)
+}
+
+-- proj/some_func_test.go --
+package proj
+
+import (
+ "testing"
+)
+
+func Test_SomeFunc(t *testing.T) {
+ t.Run("test1", func(t *testing.T) {
+ result := SomeFunc(1, 1)
+ if result != 2 {
+ t.Errorf("Expected 2, got %d", result)
+ }
+ })
+}
+
+-- proj/sub/sub.go --
+package sub
+
+func Sub(a, b int) int {
+ if a == 0 && b == 0 {
+ return 0
+ }
+ return a - b
+}
+
+-- proj/sub/sub_test.go --
+package sub
+
+import (
+ "testing"
+)
+
+func Test_Sub(t *testing.T) {
+ t.Run("test_sub1", func(t *testing.T) {
+ result := Sub(1, 1)
+ if result != 0 {
+ t.Errorf("Expected 0, got %d", result)
+ }
+ })
+}
+
+-- proj/sum/sum.go --
+package sum
+
+func Sum(a, b int) int {
+ if a == 0 {
+ return b
+ }
+ return a + b
+}
+
+-- sub_modified.go --
+package sub
+
+func Sub(a, b int) int {
+ if a == 0 && b == 0 || a == -100 {
+ return 0
+ }
+ return a - b
+}
diff --git a/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt b/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt
new file mode 100644
index 0000000000..263400a9e6
--- /dev/null
+++ b/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt
@@ -0,0 +1,80 @@
+# Test that the test cache is invalidated when -coverpkg dependencies change,
+# even when -coverprofile is not specified. This exercises the else-if branch
+# in tryCacheWithID that checks covMeta without a cover profile file.
+
+[short] skip
+[GODEBUG:gocacheverify=1] skip
+
+# Start with a clean GOCACHE.
+env GOCACHE=$WORK/cache
+
+cd proj
+
+# Run tests with -cover and -coverpkg but without -coverprofile.
+go test -cover -coverpkg=proj/... ./...
+stdout 'coverage:'
+
+# Run again — should be served from cache.
+go test -cover -coverpkg=proj/... ./...
+stdout '\(cached\)'
+stdout 'coverage:'
+
+# Modify a covered package that is not directly under test.
+cp ../sub_modified.go sub/sub.go
+
+# Run again — the cache must be invalidated because a covered package changed.
+go test -cover -coverpkg=proj/... ./...
+! stdout '\(cached\)'
+stdout 'coverage:'
+
+-- proj/go.mod --
+module proj
+
+go 1.21
+
+-- proj/main.go --
+package proj
+
+import "proj/sub"
+
+func Add(a, b int) int {
+ return sub.Sub(a, -b)
+}
+
+-- proj/main_test.go --
+package proj
+
+import "testing"
+
+func TestAdd(t *testing.T) {
+ if Add(1, 2) != 3 {
+ t.Fatal("expected 3")
+ }
+}
+
+-- proj/sub/sub.go --
+package sub
+
+func Sub(a, b int) int {
+ return a - b
+}
+
+-- proj/sub/sub_test.go --
+package sub
+
+import "testing"
+
+func TestSub(t *testing.T) {
+ if Sub(3, 1) != 2 {
+ t.Fatal("expected 2")
+ }
+}
+
+-- sub_modified.go --
+package sub
+
+// Added a comment to change the source.
+
+func Sub(a, b int) int {
+ return a - b
+}