| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package coverage | |
| import ( | |
| "crypto/md5" | |
| "fmt" | |
| "internal/coverage" | |
| "internal/coverage/encodecounter" | |
| "internal/coverage/encodemeta" | |
| "internal/coverage/rtcov" | |
| "io" | |
| "os" | |
| "path/filepath" | |
| "reflect" | |
| "runtime" | |
| "time" | |
| "unsafe" | |
| ) | |
| // This file contains functions that support the writing of data files | |
| // emitted at the end of code coverage testing runs, from instrumented | |
| // executables. | |
| // getCovMetaList returns a list of meta-data blobs registered | |
| // for the currently executing instrumented program. It is defined in the | |
| // runtime. | |
| func getCovMetaList() []rtcov.CovMetaBlob | |
| // getCovCounterList returns a list of counter-data blobs registered | |
| // for the currently executing instrumented program. It is defined in the | |
| // runtime. | |
| func getCovCounterList() []rtcov.CovCounterBlob | |
| // getCovPkgMap returns a map storing the remapped package IDs for | |
| // hard-coded runtime packages (see internal/coverage/pkgid.go for | |
| // more on why hard-coded package IDs are needed). This function | |
| // is defined in the runtime. | |
| func getCovPkgMap() map[int]int | |
| // emitState holds useful state information during the emit process. | |
| // | |
| // When an instrumented program finishes execution and starts the | |
| // process of writing out coverage data, it's possible that an | |
| // existing meta-data file already exists in the output directory. In | |
| // this case openOutputFiles() below will leave the 'mf' field below | |
| // as nil. If a new meta-data file is needed, field 'mfname' will be | |
| // the final desired path of the meta file, 'mftmp' will be a | |
| // temporary file, and 'mf' will be an open os.File pointer for | |
| // 'mftmp'. The meta-data file payload will be written to 'mf', the | |
| // temp file will be then closed and renamed (from 'mftmp' to | |
| // 'mfname'), so as to insure that the meta-data file is created | |
| // atomically; we want this so that things work smoothly in cases | |
| // where there are several instances of a given instrumented program | |
| // all terminating at the same time and trying to create meta-data | |
| // files simultaneously. | |
| // | |
| // For counter data files there is less chance of a collision, hence | |
| // the openOutputFiles() stores the counter data file in 'cfname' and | |
| // then places the *io.File into 'cf'. | |
| type emitState struct { | |
| mfname string // path of final meta-data output file | |
| mftmp string // path to meta-data temp file (if needed) | |
| mf *os.File // open os.File for meta-data temp file | |
| cfname string // path of final counter data file | |
| cftmp string // path to counter data temp file | |
| cf *os.File // open os.File for counter data file | |
| outdir string // output directory | |
| // List of meta-data symbols obtained from the runtime | |
| metalist []rtcov.CovMetaBlob | |
| // List of counter-data symbols obtained from the runtime | |
| counterlist []rtcov.CovCounterBlob | |
| // Table to use for remapping hard-coded pkg ids. | |
| pkgmap map[int]int | |
| // emit debug trace output | |
| debug bool | |
| } | |
| var ( | |
| // finalHash is computed at init time from the list of meta-data | |
| // symbols registered during init. It is used both for writing the | |
| // meta-data file and counter-data files. | |
| finalHash [16]byte | |
| // Set to true when we've computed finalHash + finalMetaLen. | |
| finalHashComputed bool | |
| // Total meta-data length. | |
| finalMetaLen uint64 | |
| // Records whether we've already attempted to write meta-data. | |
| metaDataEmitAttempted bool | |
| // Counter mode for this instrumented program run. | |
| cmode coverage.CounterMode | |
| // Counter granularity for this instrumented program run. | |
| cgran coverage.CounterGranularity | |
| // Cached value of GOCOVERDIR environment variable. | |
| goCoverDir string | |
| // Copy of os.Args made at init time, converted into map format. | |
| capturedOsArgs map[string]string | |
| // Flag used in tests to signal that coverage data already written. | |
| covProfileAlreadyEmitted bool | |
| ) | |
| // fileType is used to select between counter-data files and | |
| // meta-data files. | |
| type fileType int | |
| const ( | |
| noFile = 1 << iota | |
| metaDataFile | |
| counterDataFile | |
| ) | |
| // emitMetaData emits the meta-data output file for this coverage run. | |
| // This entry point is intended to be invoked by the compiler from | |
| // an instrumented program's main package init func. | |
| func emitMetaData() { | |
| if covProfileAlreadyEmitted { | |
| return | |
| } | |
| ml, err := prepareForMetaEmit() | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err) | |
| if os.Getenv("GOCOVERDEBUG") != "" { | |
| panic("meta-data write failure") | |
| } | |
| } | |
| if len(ml) == 0 { | |
| fmt.Fprintf(os.Stderr, "program not built with -cover\n") | |
| return | |
| } | |
| goCoverDir = os.Getenv("GOCOVERDIR") | |
| if goCoverDir == "" { | |
| fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n") | |
| return | |
| } | |
| if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil { | |
| fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err) | |
| if os.Getenv("GOCOVERDEBUG") != "" { | |
| panic("meta-data write failure") | |
| } | |
| } | |
| } | |
| func modeClash(m coverage.CounterMode) bool { | |
| if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain { | |
| return false | |
| } | |
| if cmode == coverage.CtrModeInvalid { | |
| cmode = m | |
| return false | |
| } | |
| return cmode != m | |
| } | |
| func granClash(g coverage.CounterGranularity) bool { | |
| if cgran == coverage.CtrGranularityInvalid { | |
| cgran = g | |
| return false | |
| } | |
| return cgran != g | |
| } | |
| // prepareForMetaEmit performs preparatory steps needed prior to | |
| // emitting a meta-data file, notably computing a final hash of | |
| // all meta-data blobs and capturing os args. | |
| func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) { | |
| // Ask the runtime for the list of coverage meta-data symbols. | |
| ml := getCovMetaList() | |
| // In the normal case (go build -o prog.exe ... ; ./prog.exe) | |
| // len(ml) will always be non-zero, but we check here since at | |
| // some point this function will be reachable via user-callable | |
| // APIs (for example, to write out coverage data from a server | |
| // program that doesn't ever call os.Exit). | |
| if len(ml) == 0 { | |
| return nil, nil | |
| } | |
| s := &emitState{ | |
| metalist: ml, | |
| debug: os.Getenv("GOCOVERDEBUG") != "", | |
| } | |
| // Capture os.Args() now so as to avoid issues if args | |
| // are rewritten during program execution. | |
| capturedOsArgs = captureOsArgs() | |
| if s.debug { | |
| fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR")) | |
| fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n") | |
| for k, b := range ml { | |
| fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath) | |
| if b.PkgID != -1 { | |
| fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID) | |
| } | |
| fmt.Fprintf(os.Stderr, "\n") | |
| } | |
| pm := getCovPkgMap() | |
| fmt.Fprintf(os.Stderr, "=+= remap table:\n") | |
| for from, to := range pm { | |
| fmt.Fprintf(os.Stderr, "=+= from %d to %d\n", | |
| uint32(from), uint32(to)) | |
| } | |
| } | |
| h := md5.New() | |
| tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{})) | |
| for _, entry := range ml { | |
| if _, err := h.Write(entry.Hash[:]); err != nil { | |
| return nil, err | |
| } | |
| tlen += uint64(entry.Len) | |
| ecm := coverage.CounterMode(entry.CounterMode) | |
| if modeClash(ecm) { | |
| 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) | |
| } | |
| ecg := coverage.CounterGranularity(entry.CounterGranularity) | |
| if granClash(ecg) { | |
| 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) | |
| } | |
| } | |
| // Hash mode and granularity as well. | |
| h.Write([]byte(cmode.String())) | |
| h.Write([]byte(cgran.String())) | |
| // Compute final digest. | |
| fh := h.Sum(nil) | |
| copy(finalHash[:], fh) | |
| finalHashComputed = true | |
| finalMetaLen = tlen | |
| return ml, nil | |
| } | |
| // emitMetaData emits the meta-data output file to the specified | |
| // directory, returning an error if something went wrong. | |
| func emitMetaDataToDirectory(outdir string, ml []rtcov.CovMetaBlob) error { | |
| ml, err := prepareForMetaEmit() | |
| if err != nil { | |
| return err | |
| } | |
| if len(ml) == 0 { | |
| return nil | |
| } | |
| metaDataEmitAttempted = true | |
| s := &emitState{ | |
| metalist: ml, | |
| debug: os.Getenv("GOCOVERDEBUG") != "", | |
| outdir: outdir, | |
| } | |
| // Open output files. | |
| if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil { | |
| return err | |
| } | |
| // Emit meta-data file only if needed (may already be present). | |
| if s.needMetaDataFile() { | |
| if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // emitCounterData emits the counter data output file for this coverage run. | |
| // This entry point is intended to be invoked by the runtime when an | |
| // instrumented program is terminating or calling os.Exit(). | |
| func emitCounterData() { | |
| if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted { | |
| return | |
| } | |
| if err := emitCounterDataToDirectory(goCoverDir); err != nil { | |
| fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err) | |
| if os.Getenv("GOCOVERDEBUG") != "" { | |
| panic("counter-data write failure") | |
| } | |
| } | |
| } | |
| // emitMetaData emits the counter-data output file for this coverage run. | |
| func emitCounterDataToDirectory(outdir string) error { | |
| // Ask the runtime for the list of coverage counter symbols. | |
| cl := getCovCounterList() | |
| if len(cl) == 0 { | |
| // no work to do here. | |
| return nil | |
| } | |
| if !finalHashComputed { | |
| return fmt.Errorf("error: meta-data not available (binary not built with -cover?)") | |
| } | |
| // Ask the runtime for the list of coverage counter symbols. | |
| pm := getCovPkgMap() | |
| s := &emitState{ | |
| counterlist: cl, | |
| pkgmap: pm, | |
| outdir: outdir, | |
| debug: os.Getenv("GOCOVERDEBUG") != "", | |
| } | |
| // Open output file. | |
| if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil { | |
| return err | |
| } | |
| if s.cf == nil { | |
| return fmt.Errorf("counter data output file open failed (no additional info") | |
| } | |
| // Emit counter data file. | |
| if err := s.emitCounterDataFile(finalHash, s.cf); err != nil { | |
| return err | |
| } | |
| if err := s.cf.Close(); err != nil { | |
| return fmt.Errorf("closing counter data file: %v", err) | |
| } | |
| // Counter file has now been closed. Rename the temp to the | |
| // final desired path. | |
| if err := os.Rename(s.cftmp, s.cfname); err != nil { | |
| return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err) | |
| } | |
| return nil | |
| } | |
| // openMetaFile determines whether we need to emit a meta-data output | |
| // file, or whether we can reuse the existing file in the coverage out | |
| // dir. It updates mfname/mftmp/mf fields in 's', returning an error | |
| // if something went wrong. See the comment on the emitState type | |
| // definition above for more on how file opening is managed. | |
| func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error { | |
| // Open meta-outfile for reading to see if it exists. | |
| fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash) | |
| s.mfname = filepath.Join(s.outdir, fn) | |
| fi, err := os.Stat(s.mfname) | |
| if err != nil || fi.Size() != int64(metaLen) { | |
| // We need a new meta-file. | |
| tname := "tmp." + fn + fmt.Sprintf("%d", time.Now().UnixNano()) | |
| s.mftmp = filepath.Join(s.outdir, tname) | |
| s.mf, err = os.Create(s.mftmp) | |
| if err != nil { | |
| return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err) | |
| } | |
| } | |
| return nil | |
| } | |
| // openCounterFile opens an output file for the counter data portion | |
| // of a test coverage run. If updates the 'cfname' and 'cf' fields in | |
| // 's', returning an error if something went wrong. | |
| func (s *emitState) openCounterFile(metaHash [16]byte) error { | |
| processID := os.Getpid() | |
| fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano()) | |
| s.cfname = filepath.Join(s.outdir, fn) | |
| s.cftmp = filepath.Join(s.outdir, "tmp."+fn) | |
| var err error | |
| s.cf, err = os.Create(s.cftmp) | |
| if err != nil { | |
| return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err) | |
| } | |
| return nil | |
| } | |
| // openOutputFiles opens output files in preparation for emitting | |
| // coverage data. In the case of the meta-data file, openOutputFiles | |
| // may determine that we can reuse an existing meta-data file in the | |
| // outdir, in which case it will leave the 'mf' field in the state | |
| // struct as nil. If a new meta-file is needed, the field 'mfname' | |
| // will be the final desired path of the meta file, 'mftmp' will be a | |
| // temporary file, and 'mf' will be an open os.File pointer for | |
| // 'mftmp'. The idea is that the client/caller will write content into | |
| // 'mf', close it, and then rename 'mftmp' to 'mfname'. This function | |
| // also opens the counter data output file, setting 'cf' and 'cfname' | |
| // in the state struct. | |
| func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error { | |
| fi, err := os.Stat(s.outdir) | |
| if err != nil { | |
| return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err) | |
| } | |
| if !fi.IsDir() { | |
| return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir) | |
| } | |
| if (which & metaDataFile) != 0 { | |
| if err := s.openMetaFile(metaHash, metaLen); err != nil { | |
| return err | |
| } | |
| } | |
| if (which & counterDataFile) != 0 { | |
| if err := s.openCounterFile(metaHash); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // emitMetaDataFile emits coverage meta-data to a previously opened | |
| // temporary file (s.mftmp), then renames the generated file to the | |
| // final path (s.mfname). | |
| func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error { | |
| if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil { | |
| return fmt.Errorf("writing %s: %v\n", s.mftmp, err) | |
| } | |
| if err := s.mf.Close(); err != nil { | |
| return fmt.Errorf("closing meta data temp file: %v", err) | |
| } | |
| // Temp file has now been flushed and closed. Rename the temp to the | |
| // final desired path. | |
| if err := os.Rename(s.mftmp, s.mfname); err != nil { | |
| return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err) | |
| } | |
| return nil | |
| } | |
| // needMetaDataFile returns TRUE if we need to emit a meta-data file | |
| // for this program run. It should be used only after | |
| // openOutputFiles() has been invoked. | |
| func (s *emitState) needMetaDataFile() bool { | |
| return s.mf != nil | |
| } | |
| func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error { | |
| mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w) | |
| // Note: "sd" is re-initialized on each iteration of the loop | |
| // below, and would normally be declared inside the loop, but | |
| // placed here escape analysis since we capture it in bufHdr. | |
| var sd []byte | |
| bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) | |
| var blobs [][]byte | |
| for _, e := range metalist { | |
| bufHdr.Data = uintptr(unsafe.Pointer(e.P)) | |
| bufHdr.Len = int(e.Len) | |
| bufHdr.Cap = int(e.Len) | |
| blobs = append(blobs, sd) | |
| } | |
| return mfw.Write(finalHash, blobs, cmode, gran) | |
| } | |
| func (s *emitState) NumFuncs() (int, error) { | |
| var sd []uint32 | |
| bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) | |
| totalFuncs := 0 | |
| for _, c := range s.counterlist { | |
| bufHdr.Data = uintptr(unsafe.Pointer(c.Counters)) | |
| bufHdr.Len = int(c.Len) | |
| bufHdr.Cap = int(c.Len) | |
| for i := 0; i < len(sd); i++ { | |
| // Skip ahead until the next non-zero value. | |
| if sd[i] == 0 { | |
| continue | |
| } | |
| // We found a function that was executed. | |
| nCtrs := sd[i] | |
| totalFuncs++ | |
| // Skip over this function. | |
| i += coverage.FirstCtrOffset + int(nCtrs) - 1 | |
| } | |
| } | |
| return totalFuncs, nil | |
| } | |
| func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error { | |
| var sd []uint32 | |
| bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) | |
| dpkg := uint32(0) | |
| for _, c := range s.counterlist { | |
| bufHdr.Data = uintptr(unsafe.Pointer(c.Counters)) | |
| bufHdr.Len = int(c.Len) | |
| bufHdr.Cap = int(c.Len) | |
| for i := 0; i < len(sd); i++ { | |
| // Skip ahead until the next non-zero value. | |
| if sd[i] == 0 { | |
| continue | |
| } | |
| // We found a function that was executed. | |
| nCtrs := sd[i+coverage.NumCtrsOffset] | |
| pkgId := sd[i+coverage.PkgIdOffset] | |
| funcId := sd[i+coverage.FuncIdOffset] | |
| cst := i + coverage.FirstCtrOffset | |
| counters := sd[cst : cst+int(nCtrs)] | |
| if s.debug { | |
| if pkgId != dpkg { | |
| dpkg = pkgId | |
| fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn", | |
| i, pkgId) | |
| } | |
| fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs) | |
| } | |
| // Vet and/or fix up package ID. A package ID of zero | |
| // indicates that there is some new package X that is a | |
| // runtime dependency, and this package has code that | |
| // executes before its corresponding init package runs. | |
| // This is a fatal error that we should only see during | |
| // Go development (e.g. tip). | |
| ipk := int32(pkgId) | |
| if ipk == 0 { | |
| fmt.Fprintf(os.Stderr, "\n") | |
| reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs) | |
| } else if ipk < 0 { | |
| if newId, ok := s.pkgmap[int(ipk)]; ok { | |
| pkgId = uint32(newId) | |
| } else { | |
| fmt.Fprintf(os.Stderr, "\n") | |
| reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs) | |
| } | |
| } else { | |
| // The package ID value stored in the counter array | |
| // has 1 added to it (so as to preclude the | |
| // possibility of a zero value ; see | |
| // runtime.addCovMeta), so subtract off 1 here to form | |
| // the real package ID. | |
| pkgId-- | |
| } | |
| if err := f(pkgId, funcId, counters); err != nil { | |
| return err | |
| } | |
| // Skip over this function. | |
| i += coverage.FirstCtrOffset + int(nCtrs) - 1 | |
| } | |
| if s.debug { | |
| fmt.Fprintf(os.Stderr, "\n") | |
| } | |
| } | |
| return nil | |
| } | |
| // captureOsArgs converts os.Args() into the format we use to store | |
| // this info in the counter data file (counter data file "args" | |
| // section is a generic key-value collection). See the 'args' section | |
| // in internal/coverage/defs.go for more info. The args map | |
| // is also used to capture GOOS + GOARCH values as well. | |
| func captureOsArgs() map[string]string { | |
| m := make(map[string]string) | |
| m["argc"] = fmt.Sprintf("%d", len(os.Args)) | |
| for k, a := range os.Args { | |
| m[fmt.Sprintf("argv%d", k)] = a | |
| } | |
| m["GOOS"] = runtime.GOOS | |
| m["GOARCH"] = runtime.GOARCH | |
| return m | |
| } | |
| // emitCounterDataFile emits the counter data portion of a | |
| // coverage output file (to the file 's.cf'). | |
| func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error { | |
| cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128) | |
| if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // markProfileEmitted signals the runtime/coverage machinery that | |
| // coverate data output files have already been written out, and there | |
| // is no need to take any additional action at exit time. This | |
| // function is called (via linknamed reference) from the | |
| // coverage-related boilerplate code in _testmain.go emitted for go | |
| // unit tests. | |
| func markProfileEmitted(val bool) { | |
| covProfileAlreadyEmitted = val | |
| } | |
| func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) { | |
| metaList := getCovMetaList() | |
| pkgMap := getCovPkgMap() | |
| println("internal error in coverage meta-data tracking:") | |
| println("encountered bad pkgID:", pkgID, " at slot:", slot, | |
| " fnID:", fnID, " numCtrs:", nCtrs) | |
| println("list of hard-coded runtime package IDs needs revising.") | |
| println("[see the comment on the 'rtPkgs' var in ") | |
| println(" <goroot>/src/internal/coverage/pkid.go]") | |
| println("registered list:") | |
| for k, b := range metaList { | |
| print("slot: ", k, " path='", b.PkgPath, "' ") | |
| if b.PkgID != -1 { | |
| print(" hard-coded id: ", b.PkgID) | |
| } | |
| println("") | |
| } | |
| println("remap table:") | |
| for from, to := range pkgMap { | |
| println("from ", from, " to ", to) | |
| } | |
| } |