diff options
Diffstat (limited to 'src/cmd')
| -rw-r--r-- | src/cmd/go/internal/test/test.go | 71 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt | 134 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt | 80 |
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 +} |
