1 // Copyright 2022 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 5 // Package cfile implements management of coverage files.
6 // It provides functionality exported in runtime/coverage as well as
7 // additional functionality used directly by package testing
8 // through testing/internal/testdeps.
9 package cfile
10 11 import (
12 "fmt"
13 "hash/fnv"
14 "internal/coverage"
15 "internal/coverage/encodecounter"
16 "internal/coverage/encodemeta"
17 "internal/coverage/rtcov"
18 "io"
19 "os"
20 "path/filepath"
21 "runtime"
22 "strconv"
23 "sync/atomic"
24 "time"
25 "unsafe"
26 )
27 28 // This file contains functions that support the writing of data files
29 // emitted at the end of code coverage testing runs, from instrumented
30 // executables.
31 32 // getCovCounterList returns a list of counter-data blobs registered
33 // for the currently executing instrumented program. It is defined in the
34 // runtime.
35 //
36 //go:linkname getCovCounterList
37 func getCovCounterList() []rtcov.CovCounterBlob
38 39 // emitState holds useful state information during the emit process.
40 //
41 // When an instrumented program finishes execution and starts the
42 // process of writing out coverage data, it's possible that an
43 // existing meta-data file already exists in the output directory. In
44 // this case openOutputFiles() below will leave the 'mf' field below
45 // as nil. If a new meta-data file is needed, field 'mfname' will be
46 // the final desired path of the meta file, 'mftmp' will be a
47 // temporary file, and 'mf' will be an open os.File pointer for
48 // 'mftmp'. The meta-data file payload will be written to 'mf', the
49 // temp file will be then closed and renamed (from 'mftmp' to
50 // 'mfname'), so as to insure that the meta-data file is created
51 // atomically; we want this so that things work smoothly in cases
52 // where there are several instances of a given instrumented program
53 // all terminating at the same time and trying to create meta-data
54 // files simultaneously.
55 //
56 // For counter data files there is less chance of a collision, hence
57 // the openOutputFiles() stores the counter data file in 'cfname' and
58 // then places the *io.File into 'cf'.
59 type emitState struct {
60 mfname []byte // path of final meta-data output file
61 mftmp []byte // path to meta-data temp file (if needed)
62 mf *os.File // open os.File for meta-data temp file
63 cfname []byte // path of final counter data file
64 cftmp []byte // path to counter data temp file
65 cf *os.File // open os.File for counter data file
66 outdir []byte // output directory
67 68 // List of meta-data symbols obtained from the runtime
69 metalist []rtcov.CovMetaBlob
70 71 // List of counter-data symbols obtained from the runtime
72 counterlist []rtcov.CovCounterBlob
73 74 // Table to use for remapping hard-coded pkg ids.
75 pkgmap map[int]int
76 77 // emit debug trace output
78 debug bool
79 }
80 81 var (
82 // finalHash is computed at init time from the list of meta-data
83 // symbols registered during init. It is used both for writing the
84 // meta-data file and counter-data files.
85 finalHash [16]byte
86 // Set to true when we've computed finalHash + finalMetaLen.
87 finalHashComputed bool
88 // Total meta-data length.
89 finalMetaLen uint64
90 // Records whether we've already attempted to write meta-data.
91 metaDataEmitAttempted bool
92 // Counter mode for this instrumented program run.
93 cmode coverage.CounterMode
94 // Counter granularity for this instrumented program run.
95 cgran coverage.CounterGranularity
96 // Cached value of GOCOVERDIR environment variable.
97 goCoverDir []byte
98 // Copy of os.Args made at init time, converted into map format.
99 capturedOsArgs map[string][]byte
100 // Flag used in tests to signal that coverage data already written.
101 covProfileAlreadyEmitted bool
102 )
103 104 // fileType is used to select between counter-data files and
105 // meta-data files.
106 type fileType int
107 108 const (
109 noFile = 1 << iota
110 metaDataFile
111 counterDataFile
112 )
113 114 // emitMetaData emits the meta-data output file for this coverage run.
115 // This entry point is intended to be invoked by the compiler from
116 // an instrumented program's main package init func.
117 func emitMetaData() {
118 if covProfileAlreadyEmitted {
119 return
120 }
121 ml, err := prepareForMetaEmit()
122 if err != nil {
123 fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err)
124 if os.Getenv("GOCOVERDEBUG") != "" {
125 panic("meta-data write failure")
126 }
127 }
128 if len(ml) == 0 {
129 fmt.Fprintf(os.Stderr, "program not built with -cover\n")
130 return
131 }
132 133 goCoverDir = os.Getenv("GOCOVERDIR")
134 if goCoverDir == "" {
135 fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n")
136 return
137 }
138 139 if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil {
140 fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err)
141 if os.Getenv("GOCOVERDEBUG") != "" {
142 panic("meta-data write failure")
143 }
144 }
145 }
146 147 func modeClash(m coverage.CounterMode) bool {
148 if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain {
149 return false
150 }
151 if cmode == coverage.CtrModeInvalid {
152 cmode = m
153 return false
154 }
155 return cmode != m
156 }
157 158 func granClash(g coverage.CounterGranularity) bool {
159 if cgran == coverage.CtrGranularityInvalid {
160 cgran = g
161 return false
162 }
163 return cgran != g
164 }
165 166 // prepareForMetaEmit performs preparatory steps needed prior to
167 // emitting a meta-data file, notably computing a final hash of
168 // all meta-data blobs and capturing os args.
169 func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) {
170 // Ask the runtime for the list of coverage meta-data symbols.
171 ml := rtcov.Meta.List
172 173 // In the normal case (go build -o prog.exe ... ; ./prog.exe)
174 // len(ml) will always be non-zero, but we check here since at
175 // some point this function will be reachable via user-callable
176 // APIs (for example, to write out coverage data from a server
177 // program that doesn't ever call os.Exit).
178 if len(ml) == 0 {
179 return nil, nil
180 }
181 182 s := &emitState{
183 metalist: ml,
184 debug: os.Getenv("GOCOVERDEBUG") != "",
185 }
186 187 // Capture os.Args() now so as to avoid issues if args
188 // are rewritten during program execution.
189 capturedOsArgs = captureOsArgs()
190 191 if s.debug {
192 fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR"))
193 fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n")
194 for k, b := range ml {
195 fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath)
196 if b.PkgID != -1 {
197 fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID)
198 }
199 fmt.Fprintf(os.Stderr, "\n")
200 }
201 pm := rtcov.Meta.PkgMap
202 fmt.Fprintf(os.Stderr, "=+= remap table:\n")
203 for from, to := range pm {
204 fmt.Fprintf(os.Stderr, "=+= from %d to %d\n",
205 uint32(from), uint32(to))
206 }
207 }
208 209 h := fnv.New128a()
210 tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))
211 for _, entry := range ml {
212 if _, err := h.Write(entry.Hash[:]); err != nil {
213 return nil, err
214 }
215 tlen += uint64(entry.Len)
216 ecm := coverage.CounterMode(entry.CounterMode)
217 if modeClash(ecm) {
218 return nil, fmt.Errorf("coverage counter mode clash: package %s uses mode=%d, but package %s uses mode=%s\n", ml[0].PkgPath, cmode, entry.PkgPath, ecm)
219 }
220 ecg := coverage.CounterGranularity(entry.CounterGranularity)
221 if granClash(ecg) {
222 return nil, fmt.Errorf("coverage counter granularity clash: package %s uses gran=%d, but package %s uses gran=%s\n", ml[0].PkgPath, cgran, entry.PkgPath, ecg)
223 }
224 }
225 226 // Hash mode and granularity as well.
227 h.Write([]byte(cmode.String()))
228 h.Write([]byte(cgran.String()))
229 230 // Compute final digest.
231 fh := h.Sum(nil)
232 copy(finalHash[:], fh)
233 finalHashComputed = true
234 finalMetaLen = tlen
235 236 return ml, nil
237 }
238 239 // emitMetaDataToDirectory emits the meta-data output file to the specified
240 // directory, returning an error if something went wrong.
241 func emitMetaDataToDirectory(outdir []byte, ml []rtcov.CovMetaBlob) error {
242 ml, err := prepareForMetaEmit()
243 if err != nil {
244 return err
245 }
246 if len(ml) == 0 {
247 return nil
248 }
249 250 metaDataEmitAttempted = true
251 252 s := &emitState{
253 metalist: ml,
254 debug: os.Getenv("GOCOVERDEBUG") != "",
255 outdir: outdir,
256 }
257 258 // Open output files.
259 if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil {
260 return err
261 }
262 263 // Emit meta-data file only if needed (may already be present).
264 if s.needMetaDataFile() {
265 if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil {
266 return err
267 }
268 }
269 return nil
270 }
271 272 // emitCounterData emits the counter data output file for this coverage run.
273 // This entry point is intended to be invoked by the runtime when an
274 // instrumented program is terminating or calling os.Exit().
275 func emitCounterData() {
276 if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted {
277 return
278 }
279 if err := emitCounterDataToDirectory(goCoverDir); err != nil {
280 fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err)
281 if os.Getenv("GOCOVERDEBUG") != "" {
282 panic("counter-data write failure")
283 }
284 }
285 }
286 287 // emitCounterDataToDirectory emits the counter-data output file for this coverage run.
288 func emitCounterDataToDirectory(outdir []byte) error {
289 // Ask the runtime for the list of coverage counter symbols.
290 cl := getCovCounterList()
291 if len(cl) == 0 {
292 // no work to do here.
293 return nil
294 }
295 296 if !finalHashComputed {
297 return fmt.Errorf("error: meta-data not available (binary not built with -cover?)")
298 }
299 300 // Ask the runtime for the list of coverage counter symbols.
301 pm := rtcov.Meta.PkgMap
302 s := &emitState{
303 counterlist: cl,
304 pkgmap: pm,
305 outdir: outdir,
306 debug: os.Getenv("GOCOVERDEBUG") != "",
307 }
308 309 // Open output file.
310 if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil {
311 return err
312 }
313 if s.cf == nil {
314 return fmt.Errorf("counter data output file open failed (no additional info")
315 }
316 317 // Emit counter data file.
318 if err := s.emitCounterDataFile(finalHash, s.cf); err != nil {
319 return err
320 }
321 if err := s.cf.Close(); err != nil {
322 return fmt.Errorf("closing counter data file: %v", err)
323 }
324 325 // Counter file has now been closed. Rename the temp to the
326 // final desired path.
327 if err := os.Rename(s.cftmp, s.cfname); err != nil {
328 return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err)
329 }
330 331 return nil
332 }
333 334 // emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.
335 func (s *emitState) emitCounterDataToWriter(w io.Writer) error {
336 if err := s.emitCounterDataFile(finalHash, w); err != nil {
337 return err
338 }
339 return nil
340 }
341 342 // openMetaFile determines whether we need to emit a meta-data output
343 // file, or whether we can reuse the existing file in the coverage out
344 // dir. It updates mfname/mftmp/mf fields in 's', returning an error
345 // if something went wrong. See the comment on the emitState type
346 // definition above for more on how file opening is managed.
347 func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error {
348 349 // Open meta-outfile for reading to see if it exists.
350 fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash)
351 s.mfname = filepath.Join(s.outdir, fn)
352 fi, err := os.Stat(s.mfname)
353 if err != nil || fi.Size() != int64(metaLen) {
354 // We need a new meta-file.
355 tname := "tmp." + fn + strconv.FormatInt(time.Now().UnixNano(), 10)
356 s.mftmp = filepath.Join(s.outdir, tname)
357 s.mf, err = os.Create(s.mftmp)
358 if err != nil {
359 return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err)
360 }
361 }
362 return nil
363 }
364 365 // openCounterFile opens an output file for the counter data portion
366 // of a test coverage run. If updates the 'cfname' and 'cf' fields in
367 // 's', returning an error if something went wrong.
368 func (s *emitState) openCounterFile(metaHash [16]byte) error {
369 processID := os.Getpid()
370 fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano())
371 s.cfname = filepath.Join(s.outdir, fn)
372 s.cftmp = filepath.Join(s.outdir, "tmp."+fn)
373 var err error
374 s.cf, err = os.Create(s.cftmp)
375 if err != nil {
376 return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err)
377 }
378 return nil
379 }
380 381 // openOutputFiles opens output files in preparation for emitting
382 // coverage data. In the case of the meta-data file, openOutputFiles
383 // may determine that we can reuse an existing meta-data file in the
384 // outdir, in which case it will leave the 'mf' field in the state
385 // struct as nil. If a new meta-file is needed, the field 'mfname'
386 // will be the final desired path of the meta file, 'mftmp' will be a
387 // temporary file, and 'mf' will be an open os.File pointer for
388 // 'mftmp'. The idea is that the client/caller will write content into
389 // 'mf', close it, and then rename 'mftmp' to 'mfname'. This function
390 // also opens the counter data output file, setting 'cf' and 'cfname'
391 // in the state struct.
392 func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error {
393 fi, err := os.Stat(s.outdir)
394 if err != nil {
395 return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err)
396 }
397 if !fi.IsDir() {
398 return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir)
399 }
400 401 if (which & metaDataFile) != 0 {
402 if err := s.openMetaFile(metaHash, metaLen); err != nil {
403 return err
404 }
405 }
406 if (which & counterDataFile) != 0 {
407 if err := s.openCounterFile(metaHash); err != nil {
408 return err
409 }
410 }
411 return nil
412 }
413 414 // emitMetaDataFile emits coverage meta-data to a previously opened
415 // temporary file (s.mftmp), then renames the generated file to the
416 // final path (s.mfname).
417 func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error {
418 if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil {
419 return fmt.Errorf("writing %s: %v\n", s.mftmp, err)
420 }
421 if err := s.mf.Close(); err != nil {
422 return fmt.Errorf("closing meta data temp file: %v", err)
423 }
424 425 // Temp file has now been flushed and closed. Rename the temp to the
426 // final desired path.
427 if err := os.Rename(s.mftmp, s.mfname); err != nil {
428 return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err)
429 }
430 431 return nil
432 }
433 434 // needMetaDataFile returns TRUE if we need to emit a meta-data file
435 // for this program run. It should be used only after
436 // openOutputFiles() has been invoked.
437 func (s *emitState) needMetaDataFile() bool {
438 return s.mf != nil
439 }
440 441 func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error {
442 mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w)
443 444 var blobs [][]byte
445 for _, e := range metalist {
446 sd := unsafe.Slice(e.P, int(e.Len))
447 blobs = append(blobs, sd)
448 }
449 return mfw.Write(finalHash, blobs, cmode, gran)
450 }
451 452 func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
453 var tcounters []uint32
454 455 rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 {
456 ctrs = ctrs[:0]
457 for i := range actrs {
458 ctrs = append(ctrs, actrs[i].Load())
459 }
460 return ctrs
461 }
462 463 dpkg := uint32(0)
464 for _, c := range s.counterlist {
465 sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
466 for i := 0; i < len(sd); i++ {
467 // Skip ahead until the next non-zero value.
468 sdi := sd[i].Load()
469 if sdi == 0 {
470 continue
471 }
472 473 // We found a function that was executed.
474 nCtrs := sd[i+coverage.NumCtrsOffset].Load()
475 pkgId := sd[i+coverage.PkgIdOffset].Load()
476 funcId := sd[i+coverage.FuncIdOffset].Load()
477 cst := i + coverage.FirstCtrOffset
478 counters := sd[cst : cst+int(nCtrs)]
479 480 // Check to make sure that we have at least one live
481 // counter. See the implementation note in ClearCoverageCounters
482 // for a description of why this is needed.
483 isLive := false
484 for i := 0; i < len(counters); i++ {
485 if counters[i].Load() != 0 {
486 isLive = true
487 break
488 }
489 }
490 if !isLive {
491 // Skip this function.
492 i += coverage.FirstCtrOffset + int(nCtrs) - 1
493 continue
494 }
495 496 if s.debug {
497 if pkgId != dpkg {
498 dpkg = pkgId
499 fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn",
500 i, pkgId)
501 }
502 fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs)
503 }
504 505 // Vet and/or fix up package ID. A package ID of zero
506 // indicates that there is some new package X that is a
507 // runtime dependency, and this package has code that
508 // executes before its corresponding init package runs.
509 // This is a fatal error that we should only see during
510 // Go development (e.g. tip).
511 ipk := int32(pkgId)
512 if ipk == 0 {
513 fmt.Fprintf(os.Stderr, "\n")
514 reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
515 } else if ipk < 0 {
516 if newId, ok := s.pkgmap[int(ipk)]; ok {
517 pkgId = uint32(newId)
518 } else {
519 fmt.Fprintf(os.Stderr, "\n")
520 reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
521 }
522 } else {
523 // The package ID value stored in the counter array
524 // has 1 added to it (so as to preclude the
525 // possibility of a zero value ; see
526 // runtime.addCovMeta), so subtract off 1 here to form
527 // the real package ID.
528 pkgId--
529 }
530 531 tcounters = rdCounters(counters, tcounters)
532 if err := f(pkgId, funcId, tcounters); err != nil {
533 return err
534 }
535 536 // Skip over this function.
537 i += coverage.FirstCtrOffset + int(nCtrs) - 1
538 }
539 if s.debug {
540 fmt.Fprintf(os.Stderr, "\n")
541 }
542 }
543 return nil
544 }
545 546 // captureOsArgs converts os.Args() into the format we use to store
547 // this info in the counter data file (counter data file "args"
548 // section is a generic key-value collection). See the 'args' section
549 // in internal/coverage/defs.go for more info. The args map
550 // is also used to capture GOOS + GOARCH values as well.
551 func captureOsArgs() map[string][]byte {
552 m := map[string][]byte{}
553 m["argc"] = strconv.Itoa(len(os.Args))
554 for k, a := range os.Args {
555 m[fmt.Sprintf("argv%d", k)] = a
556 }
557 m["GOOS"] = runtime.GOOS
558 m["GOARCH"] = runtime.GOARCH
559 return m
560 }
561 562 // emitCounterDataFile emits the counter data portion of a
563 // coverage output file (to the file 's.cf').
564 func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error {
565 cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128)
566 if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil {
567 return err
568 }
569 return nil
570 }
571 572 // MarkProfileEmitted signals the coverage machinery that
573 // coverage data output files have already been written out, and there
574 // is no need to take any additional action at exit time. This
575 // function is called from the coverage-related boilerplate code in _testmain.go
576 // emitted for go unit tests.
577 func MarkProfileEmitted(val bool) {
578 covProfileAlreadyEmitted = val
579 }
580 581 func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) {
582 metaList := rtcov.Meta.List
583 pkgMap := rtcov.Meta.PkgMap
584 585 println("internal error in coverage meta-data tracking:")
586 println("encountered bad pkgID:", pkgID, " at slot:", slot,
587 " fnID:", fnID, " numCtrs:", nCtrs)
588 println("list of hard-coded runtime package IDs needs revising.")
589 println("[see the comment on the 'rtPkgs' var in ")
590 println(" <goroot>/src/internal/coverage/pkid.go]")
591 println("registered list:")
592 for k, b := range metaList {
593 print("slot: ", k, " path='", b.PkgPath, "' ")
594 if b.PkgID != -1 {
595 print(" hard-coded id: ", b.PkgID)
596 }
597 println("")
598 }
599 println("remap table:")
600 for from, to := range pkgMap {
601 println("from ", from, " to ", to)
602 }
603 }
604