// 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 cfile implements management of coverage files.// It provides functionality exported in runtime/coverage as well as// additional functionality used directly by package testing// through testing/internal/testdeps.
package cfileimport ()// This file contains functions that support the writing of data files// emitted at the end of code coverage testing runs, from instrumented// executables.// getCovCounterList returns a list of counter-data blobs registered// for the currently executing instrumented program. It is defined in the// runtime.////go:linkname getCovCounterListfunc getCovCounterList() []rtcov.CovCounterBlob// 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 intconst ( 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() {ifcovProfileAlreadyEmitted {return } , := prepareForMetaEmit()if != nil {fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", )ifos.Getenv("GOCOVERDEBUG") != "" {panic("meta-data write failure") } }iflen() == 0 {fmt.Fprintf(os.Stderr, "program not built with -cover\n")return }goCoverDir = os.Getenv("GOCOVERDIR")ifgoCoverDir == "" {fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n")return }if := emitMetaDataToDirectory(goCoverDir, ); != nil {fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", )ifos.Getenv("GOCOVERDEBUG") != "" {panic("meta-data write failure") } }}func modeClash( coverage.CounterMode) bool {if == coverage.CtrModeRegOnly || == coverage.CtrModeTestMain {returnfalse }ifcmode == coverage.CtrModeInvalid {cmode = returnfalse }returncmode != }func granClash( coverage.CounterGranularity) bool {ifcgran == coverage.CtrGranularityInvalid {cgran = returnfalse }returncgran != }// 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. := rtcov.Meta.List// 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).iflen() == 0 {returnnil, nil } := &emitState{metalist: ,debug: os.Getenv("GOCOVERDEBUG") != "", }// Capture os.Args() now so as to avoid issues if args // are rewritten during program execution.capturedOsArgs = captureOsArgs()if .debug {fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR"))fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n")for , := range {fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", , .PkgPath)if .PkgID != -1 {fmt.Fprintf(os.Stderr, " hcid: %d", .PkgID) }fmt.Fprintf(os.Stderr, "\n") } := rtcov.Meta.PkgMapfmt.Fprintf(os.Stderr, "=+= remap table:\n")for , := range {fmt.Fprintf(os.Stderr, "=+= from %d to %d\n",uint32(), uint32()) } } := md5.New() := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))for , := range {if , := .Write(.Hash[:]); != nil {returnnil, } += uint64(.Len) := coverage.CounterMode(.CounterMode)ifmodeClash() {returnnil, fmt.Errorf("coverage counter mode clash: package %s uses mode=%d, but package %s uses mode=%s\n", [0].PkgPath, cmode, .PkgPath, ) } := coverage.CounterGranularity(.CounterGranularity)ifgranClash() {returnnil, fmt.Errorf("coverage counter granularity clash: package %s uses gran=%d, but package %s uses gran=%s\n", [0].PkgPath, cgran, .PkgPath, ) } }// Hash mode and granularity as well. .Write([]byte(cmode.String())) .Write([]byte(cgran.String()))// Compute final digest. := .Sum(nil)copy(finalHash[:], )finalHashComputed = truefinalMetaLen = return , nil}// emitMetaDataToDirectory emits the meta-data output file to the specified// directory, returning an error if something went wrong.func emitMetaDataToDirectory( string, []rtcov.CovMetaBlob) error { , := prepareForMetaEmit()if != nil {return }iflen() == 0 {returnnil }metaDataEmitAttempted = true := &emitState{metalist: ,debug: os.Getenv("GOCOVERDEBUG") != "",outdir: , }// Open output files.if := .openOutputFiles(finalHash, finalMetaLen, metaDataFile); != nil {return }// Emit meta-data file only if needed (may already be present).if .needMetaDataFile() {if := .emitMetaDataFile(finalHash, finalMetaLen); != nil {return } }returnnil}// 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() {ifgoCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted {return }if := emitCounterDataToDirectory(goCoverDir); != nil {fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", )ifos.Getenv("GOCOVERDEBUG") != "" {panic("counter-data write failure") } }}// emitCounterDataToDirectory emits the counter-data output file for this coverage run.func emitCounterDataToDirectory( string) error {// Ask the runtime for the list of coverage counter symbols. := getCovCounterList()iflen() == 0 {// no work to do here.returnnil }if !finalHashComputed {returnfmt.Errorf("error: meta-data not available (binary not built with -cover?)") }// Ask the runtime for the list of coverage counter symbols. := rtcov.Meta.PkgMap := &emitState{counterlist: ,pkgmap: ,outdir: ,debug: os.Getenv("GOCOVERDEBUG") != "", }// Open output file.if := .openOutputFiles(finalHash, finalMetaLen, counterDataFile); != nil {return }if .cf == nil {returnfmt.Errorf("counter data output file open failed (no additional info") }// Emit counter data file.if := .emitCounterDataFile(finalHash, .cf); != nil {return }if := .cf.Close(); != nil {returnfmt.Errorf("closing counter data file: %v", ) }// Counter file has now been closed. Rename the temp to the // final desired path.if := os.Rename(.cftmp, .cfname); != nil {returnfmt.Errorf("writing %s: rename from %s failed: %v\n", .cfname, .cftmp, ) }returnnil}// emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.func ( *emitState) ( io.Writer) error {if := .emitCounterDataFile(finalHash, ); != nil {return }returnnil}// 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 ( *emitState) ( [16]byte, uint64) error {// Open meta-outfile for reading to see if it exists. := fmt.Sprintf("%s.%x", coverage.MetaFilePref, ) .mfname = filepath.Join(.outdir, ) , := os.Stat(.mfname)if != nil || .Size() != int64() {// We need a new meta-file. := "tmp." + + strconv.FormatInt(time.Now().UnixNano(), 10) .mftmp = filepath.Join(.outdir, ) .mf, = os.Create(.mftmp)if != nil {returnfmt.Errorf("creating meta-data file %s: %v", .mftmp, ) } }returnnil}// 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 ( *emitState) ( [16]byte) error { := os.Getpid() := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, , , time.Now().UnixNano()) .cfname = filepath.Join(.outdir, ) .cftmp = filepath.Join(.outdir, "tmp."+)varerror .cf, = os.Create(.cftmp)if != nil {returnfmt.Errorf("creating counter data file %s: %v", .cftmp, ) }returnnil}// 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 ( *emitState) ( [16]byte, uint64, fileType) error { , := os.Stat(.outdir)if != nil {returnfmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", .outdir, ) }if !.IsDir() {returnfmt.Errorf("output directory %q not a directory; no coverage data written", .outdir) }if ( & metaDataFile) != 0 {if := .openMetaFile(, ); != nil {return } }if ( & counterDataFile) != 0 {if := .openCounterFile(); != nil {return } }returnnil}// 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 ( *emitState) ( [16]byte, uint64) error {if := writeMetaData(.mf, .metalist, cmode, cgran, ); != nil {returnfmt.Errorf("writing %s: %v\n", .mftmp, ) }if := .mf.Close(); != nil {returnfmt.Errorf("closing meta data temp file: %v", ) }// Temp file has now been flushed and closed. Rename the temp to the // final desired path.if := os.Rename(.mftmp, .mfname); != nil {returnfmt.Errorf("writing %s: rename from %s failed: %v\n", .mfname, .mftmp, ) }returnnil}// 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 ( *emitState) () bool {return .mf != nil}func writeMetaData( io.Writer, []rtcov.CovMetaBlob, coverage.CounterMode, coverage.CounterGranularity, [16]byte) error { := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", )var [][]bytefor , := range { := unsafe.Slice(.P, int(.Len)) = append(, ) }return .Write(, , , )}func ( *emitState) ( encodecounter.CounterVisitorFn) error {var []uint32 := func( []atomic.Uint32, []uint32) []uint32 { = [:0]for := range { = append(, [].Load()) }return } := uint32(0)for , := range .counterlist { := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(.Counters)), int(.Len))for := 0; < len(); ++ {// Skip ahead until the next non-zero value. := [].Load()if == 0 {continue }// We found a function that was executed. := [+coverage.NumCtrsOffset].Load() := [+coverage.PkgIdOffset].Load() := [+coverage.FuncIdOffset].Load() := + coverage.FirstCtrOffset := [ : +int()]// Check to make sure that we have at least one live // counter. See the implementation note in ClearCoverageCounters // for a description of why this is needed. := falsefor := 0; < len(); ++ {if [].Load() != 0 { = truebreak } }if ! {// Skip this function. += coverage.FirstCtrOffset + int() - 1continue }if .debug {if != { = fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn", , ) }fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", , , ) }// 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). := int32()if == 0 {fmt.Fprintf(os.Stderr, "\n")reportErrorInHardcodedList(int32(), , , ) } elseif < 0 {if , := .pkgmap[int()]; { = uint32() } else {fmt.Fprintf(os.Stderr, "\n")reportErrorInHardcodedList(int32(), , , ) } } 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. -- } = (, )if := (, , ); != nil {return }// Skip over this function. += coverage.FirstCtrOffset + int() - 1 }if .debug {fmt.Fprintf(os.Stderr, "\n") } }returnnil}// 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 { := make(map[string]string) ["argc"] = strconv.Itoa(len(os.Args))for , := rangeos.Args { [fmt.Sprintf("argv%d", )] = } ["GOOS"] = runtime.GOOS ["GOARCH"] = runtime.GOARCHreturn}// emitCounterDataFile emits the counter data portion of a// coverage output file (to the file 's.cf').func ( *emitState) ( [16]byte, io.Writer) error { := encodecounter.NewCoverageDataWriter(, coverage.CtrULeb128)if := .Write(, capturedOsArgs, ); != nil {return }returnnil}// MarkProfileEmitted signals the coverage machinery that// coverage 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 from the coverage-related boilerplate code in _testmain.go// emitted for go unit tests.func ( bool) {covProfileAlreadyEmitted = }func reportErrorInHardcodedList(, int32, , uint32) { := rtcov.Meta.List := rtcov.Meta.PkgMapprintln("internal error in coverage meta-data tracking:")println("encountered bad pkgID:", , " at slot:", ," fnID:", , " numCtrs:", )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 , := range {print("slot: ", , " path='", .PkgPath, "' ")if .PkgID != -1 {print(" hard-coded id: ", .PkgID) }println("") }println("remap table:")for , := range {println("from ", , " to ", ) }}
The pages are generated with Goldsv0.7.0-preview. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.