Skip to content

Commit e356aa6

Browse files
committed
cmd/cover: add new "emit meta file" mode for packages without tests
Introduce a new mode of execution for instrumenting packages that have no test files. Instead of just skipping packages with no test files (during "go test -cover" runs), the go command will invoke cmd/cover on the package passing in an option in the config file indicating that it should emit a coverage meta-data file directly for the package (if the package has no functions, an empty file is emitted). Note that this CL doesn't actually wire up this functionality in the Go command, that will come in a later patch. Updates #27261. Updates #58770 Updates #24570. Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest Change-Id: I01e8a3edb62441698c7246596e4bacbd966591c3 Reviewed-on: https://go-review.googlesource.com/c/go/+/495446 Reviewed-by: Bryan Mills <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 0b07bbd commit e356aa6

File tree

8 files changed

+220
-94
lines changed

8 files changed

+220
-94
lines changed

src/cmd/cover/cfg_test.go

+119-36
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ func writeFile(t *testing.T, path string, contents []byte) {
2121
}
2222
}
2323

24-
func writePkgConfig(t *testing.T, outdir, tag, ppath, pname string, gran string) string {
24+
func writePkgConfig(t *testing.T, outdir, tag, ppath, pname string, gran string, mpath string) string {
2525
incfg := filepath.Join(outdir, tag+"incfg.txt")
2626
outcfg := filepath.Join(outdir, "outcfg.txt")
2727
p := covcmd.CoverPkgConfig{
28-
PkgPath: ppath,
29-
PkgName: pname,
30-
Granularity: gran,
31-
OutConfig: outcfg,
28+
PkgPath: ppath,
29+
PkgName: pname,
30+
Granularity: gran,
31+
OutConfig: outcfg,
32+
EmitMetaFile: mpath,
3233
}
3334
data, err := json.Marshal(p)
3435
if err != nil {
@@ -74,40 +75,14 @@ func runPkgCover(t *testing.T, outdir string, tag string, incfg string, mode str
7475
}
7576
}
7677

77-
// Set to true when debugging unit test (to inspect debris, etc).
78-
// Note that this functionality does not work on windows.
79-
const debugWorkDir = false
80-
8178
func TestCoverWithCfg(t *testing.T) {
8279
testenv.MustHaveGoRun(t)
8380

8481
t.Parallel()
8582

8683
// Subdir in testdata that has our input files of interest.
8784
tpath := filepath.Join("testdata", "pkgcfg")
88-
89-
// Helper to collect input paths (go files) for a subdir in 'pkgcfg'
90-
pfiles := func(subdir string) []string {
91-
de, err := os.ReadDir(filepath.Join(tpath, subdir))
92-
if err != nil {
93-
t.Fatalf("reading subdir %s: %v", subdir, err)
94-
}
95-
paths := []string{}
96-
for _, e := range de {
97-
if !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") {
98-
continue
99-
}
100-
paths = append(paths, filepath.Join(tpath, subdir, e.Name()))
101-
}
102-
return paths
103-
}
104-
10585
dir := tempDir(t)
106-
if debugWorkDir {
107-
dir = "/tmp/qqq"
108-
os.RemoveAll(dir)
109-
os.Mkdir(dir, 0777)
110-
}
11186
instdira := filepath.Join(dir, "insta")
11287
if err := os.Mkdir(instdira, 0777); err != nil {
11388
t.Fatal(err)
@@ -131,6 +106,7 @@ func TestCoverWithCfg(t *testing.T) {
131106
}
132107

133108
var incfg string
109+
apkgfiles := []string{filepath.Join(tpath, "a", "a.go")}
134110
for _, scenario := range scenarios {
135111
// Instrument package "a", producing a set of instrumented output
136112
// files and an 'output config' file to pass on to the compiler.
@@ -139,9 +115,9 @@ func TestCoverWithCfg(t *testing.T) {
139115
mode := scenario.mode
140116
gran := scenario.gran
141117
tag := mode + "_" + gran
142-
incfg = writePkgConfig(t, instdira, tag, ppath, pname, gran)
118+
incfg = writePkgConfig(t, instdira, tag, ppath, pname, gran, "")
143119
ofs, outcfg, _ := runPkgCover(t, instdira, tag, incfg, mode,
144-
pfiles("a"), false)
120+
apkgfiles, false)
145121
t.Logf("outfiles: %+v\n", ofs)
146122

147123
// Run the compiler on the files to make sure the result is
@@ -161,7 +137,7 @@ func TestCoverWithCfg(t *testing.T) {
161137
errExpected := true
162138
tag := "errors"
163139
_, _, errmsg := runPkgCover(t, instdira, tag, "/not/a/file", mode,
164-
pfiles("a"), errExpected)
140+
apkgfiles, errExpected)
165141
want := "error reading pkgconfig file"
166142
if !strings.Contains(errmsg, want) {
167143
t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg)
@@ -171,7 +147,7 @@ func TestCoverWithCfg(t *testing.T) {
171147
t.Logf("mangling in config")
172148
writeFile(t, incfg, []byte("blah=foo\n"))
173149
_, _, errmsg = runPkgCover(t, instdira, tag, incfg, mode,
174-
pfiles("a"), errExpected)
150+
apkgfiles, errExpected)
175151
want = "error reading pkgconfig file"
176152
if !strings.Contains(errmsg, want) {
177153
t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg)
@@ -181,8 +157,115 @@ func TestCoverWithCfg(t *testing.T) {
181157
t.Logf("writing empty config")
182158
writeFile(t, incfg, []byte("\n"))
183159
_, _, errmsg = runPkgCover(t, instdira, tag, incfg, mode,
184-
pfiles("a"), errExpected)
160+
apkgfiles, errExpected)
185161
if !strings.Contains(errmsg, want) {
186162
t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg)
187163
}
188164
}
165+
166+
func TestCoverOnPackageWithNoTestFiles(t *testing.T) {
167+
testenv.MustHaveGoRun(t)
168+
169+
// For packages with no test files, the new "go test -cover"
170+
// strategy is to run cmd/cover on the package in a special
171+
// "EmitMetaFile" mode. When running in this mode, cmd/cover walks
172+
// the package doing instrumention, but when finished, instead of
173+
// writing out instrumented source files, it directly emits a
174+
// meta-data file for the package in question, essentially
175+
// simulating the effect that you would get if you added a dummy
176+
// "no-op" x_test.go file and then did a build and run of the test.
177+
178+
t.Run("YesFuncsNoTests", func(t *testing.T) {
179+
testCoverNoTestsYesFuncs(t)
180+
})
181+
t.Run("NoFuncsNoTests", func(t *testing.T) {
182+
testCoverNoTestsNoFuncs(t)
183+
})
184+
}
185+
186+
func testCoverNoTestsYesFuncs(t *testing.T) {
187+
t.Parallel()
188+
dir := tempDir(t)
189+
190+
// Run the cover command with "emit meta" enabled on a package
191+
// with functions but no test files.
192+
tpath := filepath.Join("testdata", "pkgcfg")
193+
pkg1files := []string{filepath.Join(tpath, "yesFuncsNoTests", "yfnt.go")}
194+
ppath := "cfg/yesFuncsNoTests"
195+
pname := "yesFuncsNoTests"
196+
mode := "count"
197+
gran := "perblock"
198+
tag := mode + "_" + gran
199+
instdir := filepath.Join(dir, "inst")
200+
if err := os.Mkdir(instdir, 0777); err != nil {
201+
t.Fatal(err)
202+
}
203+
mdir := filepath.Join(dir, "meta")
204+
if err := os.Mkdir(mdir, 0777); err != nil {
205+
t.Fatal(err)
206+
}
207+
mpath := filepath.Join(mdir, "covmeta.xxx")
208+
incfg := writePkgConfig(t, instdir, tag, ppath, pname, gran, mpath)
209+
_, _, errmsg := runPkgCover(t, instdir, tag, incfg, mode,
210+
pkg1files, false)
211+
if errmsg != "" {
212+
t.Fatalf("runPkgCover err: %q", errmsg)
213+
}
214+
215+
// Check for existence of meta-data file.
216+
if inf, err := os.Open(mpath); err != nil {
217+
t.Fatalf("meta-data file not created: %v", err)
218+
} else {
219+
inf.Close()
220+
}
221+
222+
// Make sure it is digestible.
223+
cdargs := []string{"tool", "covdata", "percent", "-i", mdir}
224+
cmd := testenv.Command(t, testenv.GoToolPath(t), cdargs...)
225+
run(cmd, t)
226+
}
227+
228+
func testCoverNoTestsNoFuncs(t *testing.T) {
229+
t.Parallel()
230+
dir := tempDir(t)
231+
232+
// Run the cover command with "emit meta" enabled on a package
233+
// with no functions and no test files.
234+
tpath := filepath.Join("testdata", "pkgcfg")
235+
pkgfiles := []string{filepath.Join(tpath, "noFuncsNoTests", "nfnt.go")}
236+
pname := "noFuncsNoTests"
237+
mode := "count"
238+
gran := "perblock"
239+
ppath := "cfg/" + pname
240+
tag := mode + "_" + gran
241+
instdir := filepath.Join(dir, "inst2")
242+
if err := os.Mkdir(instdir, 0777); err != nil {
243+
t.Fatal(err)
244+
}
245+
mdir := filepath.Join(dir, "meta2")
246+
if err := os.Mkdir(mdir, 0777); err != nil {
247+
t.Fatal(err)
248+
}
249+
mpath := filepath.Join(mdir, "covmeta.yyy")
250+
incfg := writePkgConfig(t, instdir, tag, ppath, pname, gran, mpath)
251+
_, _, errmsg := runPkgCover(t, instdir, tag, incfg, mode,
252+
pkgfiles, false)
253+
if errmsg != "" {
254+
t.Fatalf("runPkgCover err: %q", errmsg)
255+
}
256+
257+
// We expect to see an empty meta-data file in this case.
258+
if inf, err := os.Open(mpath); err != nil {
259+
t.Fatalf("opening meta-data file: error %v", err)
260+
} else {
261+
defer inf.Close()
262+
fi, err := inf.Stat()
263+
if err != nil {
264+
t.Fatalf("stat meta-data file: %v", err)
265+
}
266+
if fi.Size() != 0 {
267+
t.Fatalf("want zero-sized meta-data file got size %d",
268+
fi.Size())
269+
}
270+
}
271+
}

src/cmd/cover/cover.go

+73-24
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,22 @@ func usage() {
6464
}
6565

6666
var (
67-
mode = flag.String("mode", "", "coverage mode: set, count, atomic")
68-
varVar = flag.String("var", "GoCover", "name of coverage variable to generate")
69-
output = flag.String("o", "", "file for output")
70-
outfilelist = flag.String("outfilelist", "", "file containing list of output files (one per line) if -pkgcfg is in use")
71-
htmlOut = flag.String("html", "", "generate HTML representation of coverage profile")
72-
funcOut = flag.String("func", "", "output coverage profile information for each function")
73-
pkgcfg = flag.String("pkgcfg", "", "enable full-package instrumentation mode using params from specified config file")
67+
mode = flag.String("mode", "", "coverage mode: set, count, atomic")
68+
varVar = flag.String("var", "GoCover", "name of coverage variable to generate")
69+
output = flag.String("o", "", "file for output")
70+
outfilelist = flag.String("outfilelist", "", "file containing list of output files (one per line) if -pkgcfg is in use")
71+
htmlOut = flag.String("html", "", "generate HTML representation of coverage profile")
72+
funcOut = flag.String("func", "", "output coverage profile information for each function")
73+
pkgcfg = flag.String("pkgcfg", "", "enable full-package instrumentation mode using params from specified config file")
74+
pkgconfig covcmd.CoverPkgConfig
75+
outputfiles []string // list of *.cover.go instrumented outputs to write, one per input (set when -pkgcfg is in use)
76+
profile string // The profile to read; the value of -html or -func
77+
counterStmt func(*File, string) string
78+
covervarsoutfile string // an additional Go source file into which we'll write definitions of coverage counter variables + meta data variables (set when -pkgcfg is in use).
79+
cmode coverage.CounterMode
80+
cgran coverage.CounterGranularity
7481
)
7582

76-
var pkgconfig covcmd.CoverPkgConfig
77-
78-
// outputfiles is the list of *.cover.go instrumented outputs to write,
79-
// one per input (set when -pkgcfg is in use)
80-
var outputfiles []string
81-
82-
// covervarsoutfile is an additional Go source file into which we'll
83-
// write definitions of coverage counter variables + meta data variables
84-
// (set when -pkgcfg is in use).
85-
var covervarsoutfile string
86-
87-
var profile string // The profile to read; the value of -html or -func
88-
89-
var counterStmt func(*File, string) string
90-
9183
const (
9284
atomicPackagePath = "sync/atomic"
9385
atomicPackageName = "_cover_atomic_"
@@ -152,12 +144,19 @@ func parseFlags() error {
152144
switch *mode {
153145
case "set":
154146
counterStmt = setCounterStmt
147+
cmode = coverage.CtrModeSet
155148
case "count":
156149
counterStmt = incCounterStmt
150+
cmode = coverage.CtrModeCount
157151
case "atomic":
158152
counterStmt = atomicCounterStmt
159-
case "regonly", "testmain":
153+
cmode = coverage.CtrModeAtomic
154+
case "regonly":
155+
counterStmt = nil
156+
cmode = coverage.CtrModeRegOnly
157+
case "testmain":
160158
counterStmt = nil
159+
cmode = coverage.CtrModeTestMain
161160
default:
162161
return fmt.Errorf("unknown -mode %v", *mode)
163162
}
@@ -215,7 +214,12 @@ func readPackageConfig(path string) error {
215214
if err := json.Unmarshal(data, &pkgconfig); err != nil {
216215
return fmt.Errorf("error reading pkgconfig file %q: %v", path, err)
217216
}
218-
if pkgconfig.Granularity != "perblock" && pkgconfig.Granularity != "perfunc" {
217+
switch pkgconfig.Granularity {
218+
case "perblock":
219+
cgran = coverage.CtrGranularityPerBlock
220+
case "perfunc":
221+
cgran = coverage.CtrGranularityPerFunc
222+
default:
219223
return fmt.Errorf(`%s: pkgconfig requires perblock/perfunc value`, path)
220224
}
221225
return nil
@@ -1088,6 +1092,14 @@ func (p *Package) emitMetaData(w io.Writer) {
10881092
return
10891093
}
10901094

1095+
// If the "EmitMetaFile" path has been set, invoke a helper
1096+
// that will write out a pre-cooked meta-data file for this package
1097+
// to the specified location, in effect simulating the execution
1098+
// of a test binary that doesn't do any testing to speak of.
1099+
if pkgconfig.EmitMetaFile != "" {
1100+
p.emitMetaFile(pkgconfig.EmitMetaFile)
1101+
}
1102+
10911103
// Something went wrong if regonly/testmain mode is in effect and
10921104
// we have instrumented functions.
10931105
if counterStmt == nil && len(p.counterLengths) != 0 {
@@ -1158,3 +1170,40 @@ func atomicPackagePrefix() string {
11581170
}
11591171
return atomicPackageName + "."
11601172
}
1173+
1174+
func (p *Package) emitMetaFile(outpath string) {
1175+
// Open output file.
1176+
of, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
1177+
if err != nil {
1178+
log.Fatalf("opening covmeta %s: %v", outpath, err)
1179+
}
1180+
1181+
if len(p.counterLengths) == 0 {
1182+
// This corresponds to the case where we have no functions
1183+
// in the package to instrument. Leave the file empty file if
1184+
// this happens.
1185+
if err = of.Close(); err != nil {
1186+
log.Fatalf("closing meta-data file: %v", err)
1187+
}
1188+
return
1189+
}
1190+
1191+
// Encode meta-data.
1192+
var sws slicewriter.WriteSeeker
1193+
digest, err := p.mdb.Emit(&sws)
1194+
if err != nil {
1195+
log.Fatalf("encoding meta-data: %v", err)
1196+
}
1197+
payload := sws.BytesWritten()
1198+
blobs := [][]byte{payload}
1199+
1200+
// Write meta-data file directly.
1201+
mfw := encodemeta.NewCoverageMetaFileWriter(outpath, of)
1202+
err = mfw.Write(digest, blobs, cmode, cgran)
1203+
if err != nil {
1204+
log.Fatalf("writing meta-data file: %v", err)
1205+
}
1206+
if err = of.Close(); err != nil {
1207+
log.Fatalf("closing meta-data file: %v", err)
1208+
}
1209+
}

src/cmd/cover/testdata/pkgcfg/b/b.go

-10
This file was deleted.

src/cmd/cover/testdata/pkgcfg/b/b_test.go

-9
This file was deleted.

src/cmd/cover/testdata/pkgcfg/main/main.go

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package noFuncsNoTests
2+
3+
const foo = 1
4+
5+
var G struct {
6+
x int
7+
y bool
8+
}

0 commit comments

Comments
 (0)