Skip to content

cmd/go: test cache uses stale coverage data with -coverpkg #74873

@ryancurrah

Description

@ryancurrah

Go version

go1.26-devel_bb124921e9 Sun Jul 27 12:36:07 2025 -0400 darwin/amd64

Output of go env in your module/workspace:

╰─ go env
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/ryancurrah/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/ryancurrah/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/fk/8lgm5_252mb0ln68l_z9wlz80000gn/T/go-build3317734112=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/ryancurrah/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/ryancurrah/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/ryancurrah/git/github.com/ryancurrah/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/ryancurrah/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/ryancurrah/git/github.com/ryancurrah/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.26-devel_bb124921e9 Sun Jul 27 12:36:07 2025 -0400'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Hello guys,

Thanks a lot for resolving #23565.

I started testing this feature and encountered a problem that occurs when I use the go test command with the -coverpkg flag.

When coverage is collected with the -coverpkg flag and test result caching is enabled, the generated report may include lines that no longer exist.

This happens because:

  • Each test package attempts to collect coverage for all packages matching the -coverpkg pattern.
  • If the test result is loaded from the cache, the coverage data may be outdated—especially if the test package does not directly or indirectly depend on the modified code, leaving the cache uninvalidated.

For example we have project with layout:

Project layout

proj/
  some_func.go
  some_func_test.go
  sub/
    sub.go
    sub_test.go
  sum/
    sum.go

Files Content

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)
}
sub.go
package sub

func Sub(a, b int) int {
	if a == 0 && b == 0 {
		return 0
	}
	return a - b
}
sum.go
package sum

func Sum(a, b int) int {
	if a == 0 {
		return b
	}
	return a + b
}
some_func_test.go
package proj

import (
	"github.com/stretchr/testify/require"
	"testing"
)

func Test_SomeFunc(t *testing.T) {
	t.Run("test1", func(t *testing.T) {
		require.Equal(t, 2, SomeFunc(1, 1))
	})
}
sub_test.go
package sub

import (
	"github.com/stretchr/testify/require"
	"testing"
)

func Test_Sub(t *testing.T) {
	t.Run("test_sub1", func(t *testing.T) {
		require.Equal(t, 0, Sub(1, 1))
	})
}

Coverage result of this tests

go test -coverpkg=proj/... -coverprofile=cover.out ./proj/...
ok  	proj	(cached)	coverage: 44.4% of statements in proj/...
ok  	proj/sub	(cached)	coverage: 22.2% of statements in proj/...
	proj/sum		coverage: 0.0% of statements
mode: set
proj/some_func.go:5.29,6.22 1 1
proj/some_func.go:6.22,8.3 1 0
proj/some_func.go:9.2,9.22 1 1
proj/sub/sub.go:3.24,4.22 1 0
proj/sub/sub.go:4.22,6.3 1 0
proj/sub/sub.go:7.2,7.14 1 0
proj/sum/sum.go:3.24,4.12 1 1
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 1
proj/some_func.go:5.29,6.22 1 0
proj/some_func.go:6.22,8.3 1 0
proj/some_func.go:9.2,9.22 1 0
proj/sub/sub.go:3.24,4.22 1 1
proj/sub/sub.go:4.22,6.3 1 0
proj/sub/sub.go:7.2,7.14 1 1
proj/sum/sum.go:3.24,4.12 1 0
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 0
proj/sum/sum.go:3.24,4.12 1 0
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 0

Change sub.go a bit

sub.go
package sub

func Sub(a, b int) int {
	if a == 0 && b == 0 || a == -100 {
		return 0
	}
	return a - b
}

Coverage result after change

go test -coverpkg=proj/... -coverprofile=cover.out ./proj/...
ok  	proj	(cached)	coverage: 44.4% of statements in proj/...
ok  	proj/sub	0.005s	coverage: 22.2% of statements in proj/...
	proj/sum		coverage: 0.0% of statements
mode: set
proj/some_func.go:5.29,6.22 1 1
proj/some_func.go:6.22,8.3 1 0
proj/some_func.go:9.2,9.22 1 1
proj/sub/sub.go:3.24,4.22 1 0  <- Old (Should have been invalidated and removed)
proj/sub/sub.go:4.22,6.3 1 0
proj/sub/sub.go:7.2,7.14 1 0
proj/sum/sum.go:3.24,4.12 1 1
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 1
proj/some_func.go:5.29,6.22 1 0
proj/some_func.go:6.22,8.3 1 0
proj/some_func.go:9.2,9.22 1 0
proj/sub/sub.go:3.24,4.35 1 1  <- New
proj/sub/sub.go:4.35,6.3 1 0
proj/sub/sub.go:7.2,7.14 1 1
proj/sum/sum.go:3.24,4.12 1 0
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 0
proj/sum/sum.go:3.24,4.12 1 0
proj/sum/sum.go:4.12,6.3 1 0
proj/sum/sum.go:7.2,7.14 1 0

What did you see happen?

The report merges cached and fresh coverage, so you end up with two sub.go:3.24 entries, one that is stale and one that is correct. That duplication trips up tools that emit Cobertura XML.

What did you expect to see?

I expected the report to keep only the latest coverage and invalidate the cached coverage reports.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GoCommandcmd/goNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions