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. 9package cfile 10 11import ( 12 "crypto/md5" 13 "fmt" 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 37func 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'. 59type emitState struct { 60 mfname string // path of final meta-data output file 61 mftmp string // path to meta-data temp file (if needed) 62 mf *os.File // open os.File for meta-data temp file 63 cfname string // path of final counter data file 64 cftmp string // path to counter data temp file 65 cf *os.File // open os.File for counter data file 66 outdir string // 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 81var ( 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 string 98 // Copy of os.Args made at init time, converted into map format. 99 capturedOsArgs map[string]string 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. 106type fileType int 107 108const ( 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. 117func 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 147func 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 158func 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. 169func 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 := md5.New() 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. 241func emitMetaDataToDirectory(outdir string, 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(). 275func 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. 288func emitCounterDataToDirectory(outdir string) 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. 335func (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. 347func (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. 368func (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. 392func (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). 417func (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. 437func (s *emitState) needMetaDataFile() bool { 438 return s.mf != nil 439} 440 441func 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 452func (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. 551func captureOsArgs() map[string]string { 552 m := make(map[string]string) 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'). 564func (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. 577func MarkProfileEmitted(val bool) { 578 covProfileAlreadyEmitted = val 579} 580 581func 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