mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
runtime/coverage: remove uses of //go:linkname
Move code to internal/coverage/cfile, making it possible to access directly from testing/internal/testdeps, so that we can avoid needing //go:linkname hacks. For #67401. Change-Id: I10b23a9970164afd2165e718ef3b2d9e86783883 Reviewed-on: https://go-review.googlesource.com/c/go/+/585820 Auto-Submit: Russ Cox <rsc@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Than McIntosh <thanm@google.com> Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
parent
647870becc
commit
180ea45566
20 changed files with 160 additions and 132 deletions
153
src/internal/coverage/cfile/apis.go
Normal file
153
src/internal/coverage/cfile/apis.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"internal/coverage"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// WriteMetaDir implements [runtime/coverage.WriteMetaDir].
|
||||
func WriteMetaDir(dir string) error {
|
||||
if !finalHashComputed {
|
||||
return fmt.Errorf("error: no meta-data available (binary not built with -cover?)")
|
||||
}
|
||||
return emitMetaDataToDirectory(dir, getCovMetaList())
|
||||
}
|
||||
|
||||
// WriteMeta implements [runtime/coverage.WriteMeta].
|
||||
func WriteMeta(w io.Writer) error {
|
||||
if w == nil {
|
||||
return fmt.Errorf("error: nil writer in WriteMeta")
|
||||
}
|
||||
if !finalHashComputed {
|
||||
return fmt.Errorf("error: no meta-data available (binary not built with -cover?)")
|
||||
}
|
||||
ml := getCovMetaList()
|
||||
return writeMetaData(w, ml, cmode, cgran, finalHash)
|
||||
}
|
||||
|
||||
// WriteCountersDir implements [runtime/coverage.WriteCountersDir].
|
||||
func WriteCountersDir(dir string) error {
|
||||
if cmode != coverage.CtrModeAtomic {
|
||||
return fmt.Errorf("WriteCountersDir invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
|
||||
}
|
||||
return emitCounterDataToDirectory(dir)
|
||||
}
|
||||
|
||||
// WriteCounters implements [runtime/coverage.WriteCounters].
|
||||
func WriteCounters(w io.Writer) error {
|
||||
if w == nil {
|
||||
return fmt.Errorf("error: nil writer in WriteCounters")
|
||||
}
|
||||
if cmode != coverage.CtrModeAtomic {
|
||||
return fmt.Errorf("WriteCounters invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
|
||||
}
|
||||
// Ask the runtime for the list of coverage counter symbols.
|
||||
cl := getCovCounterList()
|
||||
if len(cl) == 0 {
|
||||
return fmt.Errorf("program not built with -cover")
|
||||
}
|
||||
if !finalHashComputed {
|
||||
return fmt.Errorf("meta-data not written yet, unable to write counter data")
|
||||
}
|
||||
|
||||
pm := getCovPkgMap()
|
||||
s := &emitState{
|
||||
counterlist: cl,
|
||||
pkgmap: pm,
|
||||
}
|
||||
return s.emitCounterDataToWriter(w)
|
||||
}
|
||||
|
||||
// ClearCounters implements [runtime/coverage.ClearCounters].
|
||||
func ClearCounters() error {
|
||||
cl := getCovCounterList()
|
||||
if len(cl) == 0 {
|
||||
return fmt.Errorf("program not built with -cover")
|
||||
}
|
||||
if cmode != coverage.CtrModeAtomic {
|
||||
return fmt.Errorf("ClearCounters invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
|
||||
}
|
||||
|
||||
// Implementation note: this function would be faster and simpler
|
||||
// if we could just zero out the entire counter array, but for the
|
||||
// moment we go through and zero out just the slots in the array
|
||||
// corresponding to the counter values. We do this to avoid the
|
||||
// following bad scenario: suppose that a user builds their Go
|
||||
// program with "-cover", and that program has a function (call it
|
||||
// main.XYZ) that invokes ClearCounters:
|
||||
//
|
||||
// func XYZ() {
|
||||
// ... do some stuff ...
|
||||
// coverage.ClearCounters()
|
||||
// if someCondition { <<--- HERE
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// At the point where ClearCounters executes, main.XYZ has not yet
|
||||
// finished running, thus as soon as the call returns the line
|
||||
// marked "HERE" above will trigger the writing of a non-zero
|
||||
// value into main.XYZ's counter slab. However since we've just
|
||||
// finished clearing the entire counter segment, we will have lost
|
||||
// the values in the prolog portion of main.XYZ's counter slab
|
||||
// (nctrs, pkgid, funcid). This means that later on at the end of
|
||||
// program execution as we walk through the entire counter array
|
||||
// for the program looking for executed functions, we'll zoom past
|
||||
// main.XYZ's prolog (which was zero'd) and hit the non-zero
|
||||
// counter value corresponding to the "HERE" block, which will
|
||||
// then be interpreted as the start of another live function.
|
||||
// Things will go downhill from there.
|
||||
//
|
||||
// This same scenario is also a potential risk if the program is
|
||||
// running on an architecture that permits reordering of
|
||||
// writes/stores, since the inconsistency described above could
|
||||
// arise here. Example scenario:
|
||||
//
|
||||
// func ABC() {
|
||||
// ... // prolog
|
||||
// if alwaysTrue() {
|
||||
// XYZ() // counter update here
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In the instrumented version of ABC, the prolog of the function
|
||||
// will contain a series of stores to the initial portion of the
|
||||
// counter array to write number-of-counters, pkgid, funcid. Later
|
||||
// in the function there is also a store to increment a counter
|
||||
// for the block containing the call to XYZ(). If the CPU is
|
||||
// allowed to reorder stores and decides to issue the XYZ store
|
||||
// before the prolog stores, this could be observable as an
|
||||
// inconsistency similar to the one above. Hence the requirement
|
||||
// for atomic counter mode: according to package atomic docs,
|
||||
// "...operations that happen in a specific order on one thread,
|
||||
// will always be observed to happen in exactly that order by
|
||||
// another thread". Thus we can be sure that there will be no
|
||||
// inconsistency when reading the counter array from the thread
|
||||
// running ClearCounters.
|
||||
|
||||
for _, c := range cl {
|
||||
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
|
||||
for i := 0; i < len(sd); i++ {
|
||||
// Skip ahead until the next non-zero value.
|
||||
sdi := sd[i].Load()
|
||||
if sdi == 0 {
|
||||
continue
|
||||
}
|
||||
// We found a function that was executed; clear its counters.
|
||||
nCtrs := sdi
|
||||
for j := 0; j < int(nCtrs); j++ {
|
||||
sd[i+coverage.FirstCtrOffset+j].Store(0)
|
||||
}
|
||||
// Move to next function.
|
||||
i += coverage.FirstCtrOffset + int(nCtrs) - 1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
615
src/internal/coverage/cfile/emit.go
Normal file
615
src/internal/coverage/cfile/emit.go
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
// 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 cfile
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"internal/coverage"
|
||||
"internal/coverage/encodecounter"
|
||||
"internal/coverage/encodemeta"
|
||||
"internal/coverage/rtcov"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"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.
|
||||
//go:linkname getCovMetaList
|
||||
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.
|
||||
//go:linkname getCovCounterList
|
||||
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.
|
||||
//go:linkname getCovPkgMap
|
||||
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
|
||||
}
|
||||
|
||||
// emitMetaDataToDirectory 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emitCounterDataToDirectory 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
|
||||
}
|
||||
|
||||
// emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.
|
||||
func (s *emitState) emitCounterDataToWriter(w io.Writer) error {
|
||||
if err := s.emitCounterDataFile(finalHash, w); err != nil {
|
||||
return 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 + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
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)
|
||||
|
||||
var blobs [][]byte
|
||||
for _, e := range metalist {
|
||||
sd := unsafe.Slice(e.P, int(e.Len))
|
||||
blobs = append(blobs, sd)
|
||||
}
|
||||
return mfw.Write(finalHash, blobs, cmode, gran)
|
||||
}
|
||||
|
||||
func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
|
||||
var tcounters []uint32
|
||||
|
||||
rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 {
|
||||
ctrs = ctrs[:0]
|
||||
for i := range actrs {
|
||||
ctrs = append(ctrs, actrs[i].Load())
|
||||
}
|
||||
return ctrs
|
||||
}
|
||||
|
||||
dpkg := uint32(0)
|
||||
for _, c := range s.counterlist {
|
||||
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
|
||||
for i := 0; i < len(sd); i++ {
|
||||
// Skip ahead until the next non-zero value.
|
||||
sdi := sd[i].Load()
|
||||
if sdi == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// We found a function that was executed.
|
||||
nCtrs := sd[i+coverage.NumCtrsOffset].Load()
|
||||
pkgId := sd[i+coverage.PkgIdOffset].Load()
|
||||
funcId := sd[i+coverage.FuncIdOffset].Load()
|
||||
cst := i + coverage.FirstCtrOffset
|
||||
counters := sd[cst : cst+int(nCtrs)]
|
||||
|
||||
// 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.
|
||||
isLive := false
|
||||
for i := 0; i < len(counters); i++ {
|
||||
if counters[i].Load() != 0 {
|
||||
isLive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isLive {
|
||||
// Skip this function.
|
||||
i += coverage.FirstCtrOffset + int(nCtrs) - 1
|
||||
continue
|
||||
}
|
||||
|
||||
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--
|
||||
}
|
||||
|
||||
tcounters = rdCounters(counters, tcounters)
|
||||
if err := f(pkgId, funcId, tcounters); 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"] = strconv.Itoa(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 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 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)
|
||||
}
|
||||
}
|
||||
550
src/internal/coverage/cfile/emitdata_test.go
Normal file
550
src/internal/coverage/cfile/emitdata_test.go
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"internal/coverage"
|
||||
"internal/goexperiment"
|
||||
"internal/platform"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Set to true for debugging (linux only).
|
||||
const fixedTestDir = false
|
||||
|
||||
func TestCoverageApis(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("skipping test: too long for short mode")
|
||||
}
|
||||
if !goexperiment.CoverageRedesign {
|
||||
t.Skipf("skipping new coverage tests (experiment not enabled)")
|
||||
}
|
||||
testenv.MustHaveGoBuild(t)
|
||||
dir := t.TempDir()
|
||||
if fixedTestDir {
|
||||
dir = "/tmp/qqqzzz"
|
||||
os.RemoveAll(dir)
|
||||
mkdir(t, dir)
|
||||
}
|
||||
|
||||
// Build harness. We need two copies of the harness, one built
|
||||
// with -covermode=atomic and one built non-atomic.
|
||||
bdir1 := mkdir(t, filepath.Join(dir, "build1"))
|
||||
hargs1 := []string{"-covermode=atomic", "-coverpkg=all"}
|
||||
atomicHarnessPath := buildHarness(t, bdir1, hargs1)
|
||||
nonAtomicMode := testing.CoverMode()
|
||||
if testing.CoverMode() == "atomic" {
|
||||
nonAtomicMode = "set"
|
||||
}
|
||||
bdir2 := mkdir(t, filepath.Join(dir, "build2"))
|
||||
hargs2 := []string{"-coverpkg=all", "-covermode=" + nonAtomicMode}
|
||||
nonAtomicHarnessPath := buildHarness(t, bdir2, hargs2)
|
||||
|
||||
t.Logf("atomic harness path is %s", atomicHarnessPath)
|
||||
t.Logf("non-atomic harness path is %s", nonAtomicHarnessPath)
|
||||
|
||||
// Sub-tests for each API we want to inspect, plus
|
||||
// extras for error testing.
|
||||
t.Run("emitToDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToDir(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitToWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToWriter(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitToNonexistentDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToNonexistentDir(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitToNilWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToNilWriter(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitToFailingWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToFailingWriter(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitWithCounterClear", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitWithCounterClear(t, atomicHarnessPath, dir)
|
||||
})
|
||||
t.Run("emitToDirNonAtomic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToDirNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
|
||||
})
|
||||
t.Run("emitToWriterNonAtomic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitToWriterNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
|
||||
})
|
||||
t.Run("emitWithCounterClearNonAtomic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEmitWithCounterClearNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
|
||||
})
|
||||
}
|
||||
|
||||
// upmergeCoverData helps improve coverage data for this package
|
||||
// itself. If this test itself is being invoked with "-cover", then
|
||||
// what we'd like is for package coverage data (that is, coverage for
|
||||
// routines in "runtime/coverage") to be incorporated into the test
|
||||
// run from the "harness.exe" runs we've just done. We can accomplish
|
||||
// this by doing a merge from the harness gocoverdir's to the test
|
||||
// gocoverdir.
|
||||
func upmergeCoverData(t *testing.T, gocoverdir string, mode string) {
|
||||
if testing.CoverMode() != mode {
|
||||
return
|
||||
}
|
||||
testGoCoverDir := os.Getenv("GOCOVERDIR")
|
||||
if testGoCoverDir == "" {
|
||||
return
|
||||
}
|
||||
args := []string{"tool", "covdata", "merge", "-pkg=runtime/coverage",
|
||||
"-o", testGoCoverDir, "-i", gocoverdir}
|
||||
t.Logf("up-merge of covdata from %s to %s", gocoverdir, testGoCoverDir)
|
||||
t.Logf("executing: go %+v", args)
|
||||
cmd := exec.Command(testenv.GoToolPath(t), args...)
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("covdata merge failed (%v): %s", err, b)
|
||||
}
|
||||
}
|
||||
|
||||
// buildHarness builds the helper program "harness.exe".
|
||||
func buildHarness(t *testing.T, dir string, opts []string) string {
|
||||
harnessPath := filepath.Join(dir, "harness.exe")
|
||||
harnessSrc := filepath.Join("testdata", "harness.go")
|
||||
args := []string{"build", "-o", harnessPath}
|
||||
args = append(args, opts...)
|
||||
args = append(args, harnessSrc)
|
||||
//t.Logf("harness build: go %+v\n", args)
|
||||
cmd := exec.Command(testenv.GoToolPath(t), args...)
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build failed (%v): %s", err, b)
|
||||
}
|
||||
return harnessPath
|
||||
}
|
||||
|
||||
func mkdir(t *testing.T, d string) string {
|
||||
t.Helper()
|
||||
if err := os.Mkdir(d, 0777); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// updateGoCoverDir updates the specified environment 'env' to set
|
||||
// GOCOVERDIR to 'gcd' (if setGoCoverDir is TRUE) or removes
|
||||
// GOCOVERDIR from the environment (if setGoCoverDir is false).
|
||||
func updateGoCoverDir(env []string, gcd string, setGoCoverDir bool) []string {
|
||||
rv := []string{}
|
||||
found := false
|
||||
for _, v := range env {
|
||||
if strings.HasPrefix(v, "GOCOVERDIR=") {
|
||||
if !setGoCoverDir {
|
||||
continue
|
||||
}
|
||||
v = "GOCOVERDIR=" + gcd
|
||||
found = true
|
||||
}
|
||||
rv = append(rv, v)
|
||||
}
|
||||
if !found && setGoCoverDir {
|
||||
rv = append(rv, "GOCOVERDIR="+gcd)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
func runHarness(t *testing.T, harnessPath string, tp string, setGoCoverDir bool, rdir, edir string) (string, error) {
|
||||
t.Logf("running: %s -tp %s -o %s with rdir=%s and GOCOVERDIR=%v", harnessPath, tp, edir, rdir, setGoCoverDir)
|
||||
cmd := exec.Command(harnessPath, "-tp", tp, "-o", edir)
|
||||
cmd.Dir = rdir
|
||||
cmd.Env = updateGoCoverDir(os.Environ(), rdir, setGoCoverDir)
|
||||
b, err := cmd.CombinedOutput()
|
||||
//t.Logf("harness run output: %s\n", string(b))
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func testForSpecificFunctions(t *testing.T, dir string, want []string, avoid []string) string {
|
||||
args := []string{"tool", "covdata", "debugdump",
|
||||
"-live", "-pkg=command-line-arguments", "-i=" + dir}
|
||||
t.Logf("running: go %v\n", args)
|
||||
cmd := exec.Command(testenv.GoToolPath(t), args...)
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("'go tool covdata failed (%v): %s", err, b)
|
||||
}
|
||||
output := string(b)
|
||||
rval := ""
|
||||
for _, f := range want {
|
||||
wf := "Func: " + f + "\n"
|
||||
if strings.Contains(output, wf) {
|
||||
continue
|
||||
}
|
||||
rval += fmt.Sprintf("error: output should contain %q but does not\n", wf)
|
||||
}
|
||||
for _, f := range avoid {
|
||||
wf := "Func: " + f + "\n"
|
||||
if strings.Contains(output, wf) {
|
||||
rval += fmt.Sprintf("error: output should not contain %q but does\n", wf)
|
||||
}
|
||||
}
|
||||
if rval != "" {
|
||||
t.Logf("=-= begin output:\n" + output + "\n=-= end output\n")
|
||||
}
|
||||
return rval
|
||||
}
|
||||
|
||||
func withAndWithoutRunner(f func(setit bool, tag string)) {
|
||||
// Run 'f' with and without GOCOVERDIR set.
|
||||
for i := 0; i < 2; i++ {
|
||||
tag := "x"
|
||||
setGoCoverDir := true
|
||||
if i == 0 {
|
||||
setGoCoverDir = false
|
||||
tag = "y"
|
||||
}
|
||||
f(setGoCoverDir, tag)
|
||||
}
|
||||
}
|
||||
|
||||
func mktestdirs(t *testing.T, tag, tp, dir string) (string, string) {
|
||||
t.Helper()
|
||||
rdir := mkdir(t, filepath.Join(dir, tp+"-rdir-"+tag))
|
||||
edir := mkdir(t, filepath.Join(dir, tp+"-edir-"+tag))
|
||||
return rdir, edir
|
||||
}
|
||||
|
||||
func testEmitToDir(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitToDir"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp,
|
||||
setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp emitDir': %v", err)
|
||||
}
|
||||
|
||||
// Just check to make sure meta-data file and counter data file were
|
||||
// written. Another alternative would be to run "go tool covdata"
|
||||
// or equivalent, but for now, this is what we've got.
|
||||
dents, err := os.ReadDir(edir)
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadDir(%s) failed: %v", edir, err)
|
||||
}
|
||||
mfc := 0
|
||||
cdc := 0
|
||||
for _, e := range dents {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Name(), coverage.MetaFilePref) {
|
||||
mfc++
|
||||
} else if strings.HasPrefix(e.Name(), coverage.CounterFilePref) {
|
||||
cdc++
|
||||
}
|
||||
}
|
||||
wantmf := 1
|
||||
wantcf := 1
|
||||
if mfc != wantmf {
|
||||
t.Errorf("EmitToDir: want %d meta-data files, got %d\n", wantmf, mfc)
|
||||
}
|
||||
if cdc != wantcf {
|
||||
t.Errorf("EmitToDir: want %d counter-data files, got %d\n", wantcf, cdc)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToWriter(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitToWriter"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
want := []string{"main", tp}
|
||||
avoid := []string{"final"}
|
||||
if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
|
||||
t.Errorf("coverage data from %q output match failed: %s", tp, msg)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToNonexistentDir(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitToNonexistentDir"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToUnwritableDir(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
|
||||
tp := "emitToUnwritableDir"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
|
||||
// Make edir unwritable.
|
||||
if err := os.Chmod(edir, 0555); err != nil {
|
||||
t.Fatalf("chmod failed: %v", err)
|
||||
}
|
||||
defer os.Chmod(edir, 0777)
|
||||
|
||||
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToNilWriter(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitToNilWriter"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToFailingWriter(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitToFailingWriter"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitWithCounterClear(t *testing.T, harnessPath string, dir string) {
|
||||
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
|
||||
tp := "emitWithCounterClear"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp,
|
||||
setGoCoverDir, rdir, edir)
|
||||
if err != nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': %v", tp, err)
|
||||
}
|
||||
want := []string{tp, "postClear"}
|
||||
avoid := []string{"preClear", "main", "final"}
|
||||
if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
|
||||
t.Logf("%s", output)
|
||||
t.Errorf("coverage data from %q output match failed: %s", tp, msg)
|
||||
}
|
||||
upmergeCoverData(t, edir, "atomic")
|
||||
upmergeCoverData(t, rdir, "atomic")
|
||||
})
|
||||
}
|
||||
|
||||
func testEmitToDirNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
|
||||
tp := "emitToDir"
|
||||
tag := "nonatomdir"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp,
|
||||
true, rdir, edir)
|
||||
|
||||
// We expect an error here.
|
||||
if err == nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(string(output))
|
||||
want := "WriteCountersDir invoked for program built"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
|
||||
tp, got, want)
|
||||
}
|
||||
upmergeCoverData(t, edir, naMode)
|
||||
upmergeCoverData(t, rdir, naMode)
|
||||
}
|
||||
|
||||
func testEmitToWriterNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
|
||||
tp := "emitToWriter"
|
||||
tag := "nonatomw"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp,
|
||||
true, rdir, edir)
|
||||
|
||||
// We expect an error here.
|
||||
if err == nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(string(output))
|
||||
want := "WriteCounters invoked for program built"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
|
||||
tp, got, want)
|
||||
}
|
||||
|
||||
upmergeCoverData(t, edir, naMode)
|
||||
upmergeCoverData(t, rdir, naMode)
|
||||
}
|
||||
|
||||
func testEmitWithCounterClearNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
|
||||
tp := "emitWithCounterClear"
|
||||
tag := "cclear"
|
||||
rdir, edir := mktestdirs(t, tag, tp, dir)
|
||||
output, err := runHarness(t, harnessPath, tp,
|
||||
true, rdir, edir)
|
||||
|
||||
// We expect an error here.
|
||||
if err == nil {
|
||||
t.Logf("%s", output)
|
||||
t.Fatalf("running 'harness -tp %s' nonatomic: did not get expected error", tp)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(string(output))
|
||||
want := "ClearCounters invoked for program built"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
|
||||
tp, got, want)
|
||||
}
|
||||
|
||||
upmergeCoverData(t, edir, naMode)
|
||||
upmergeCoverData(t, rdir, naMode)
|
||||
}
|
||||
|
||||
func TestApisOnNocoverBinary(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("skipping test: too long for short mode")
|
||||
}
|
||||
testenv.MustHaveGoBuild(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
// Build harness with no -cover.
|
||||
bdir := mkdir(t, filepath.Join(dir, "nocover"))
|
||||
edir := mkdir(t, filepath.Join(dir, "emitDirNo"))
|
||||
harnessPath := buildHarness(t, bdir, nil)
|
||||
output, err := runHarness(t, harnessPath, "emitToDir", false, edir, edir)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error on TestApisOnNocoverBinary harness run")
|
||||
}
|
||||
const want = "not built with -cover"
|
||||
if !strings.Contains(output, want) {
|
||||
t.Errorf("error output does not contain %q: %s", want, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue56006EmitDataRaceCoverRunningGoroutine(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("skipping test: too long for short mode")
|
||||
}
|
||||
if !goexperiment.CoverageRedesign {
|
||||
t.Skipf("skipping new coverage tests (experiment not enabled)")
|
||||
}
|
||||
|
||||
// This test requires "go test -race -cover", meaning that we need
|
||||
// go build, go run, and "-race" support.
|
||||
testenv.MustHaveGoRun(t)
|
||||
if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) ||
|
||||
!testenv.HasCGO() {
|
||||
t.Skip("skipped due to lack of race detector support / CGO")
|
||||
}
|
||||
|
||||
// This will run a program with -cover and -race where we have a
|
||||
// goroutine still running (and updating counters) at the point where
|
||||
// the test runtime is trying to write out counter data.
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "test", "-cover", "-race")
|
||||
cmd.Dir = filepath.Join("testdata", "issue56006")
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go test -cover -race failed: %v\n%s", err, b)
|
||||
}
|
||||
|
||||
// Don't want to see any data races in output.
|
||||
avoid := []string{"DATA RACE"}
|
||||
for _, no := range avoid {
|
||||
if strings.Contains(string(b), no) {
|
||||
t.Logf("%s\n", string(b))
|
||||
t.Fatalf("found %s in test output, not permitted", no)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue59563TruncatedCoverPkgAll(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("skipping test: too long for short mode")
|
||||
}
|
||||
testenv.MustHaveGoRun(t)
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
ppath := filepath.Join(tmpdir, "foo.cov")
|
||||
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "test", "-coverpkg=all", "-coverprofile="+ppath)
|
||||
cmd.Dir = filepath.Join("testdata", "issue59563")
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go test -cover failed: %v\n%s", err, b)
|
||||
}
|
||||
|
||||
cmd = exec.Command(testenv.GoToolPath(t), "tool", "cover", "-func="+ppath)
|
||||
b, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go tool cover -func failed: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(b), "\n")
|
||||
nfound := 0
|
||||
bad := false
|
||||
for _, line := range lines {
|
||||
f := strings.Fields(line)
|
||||
if len(f) == 0 {
|
||||
continue
|
||||
}
|
||||
// We're only interested in the specific function "large" for
|
||||
// the testcase being built. See the #59563 for details on why
|
||||
// size matters.
|
||||
if !(strings.HasPrefix(f[0], "internal/coverage/cfile/testdata/issue59563/repro.go") && strings.Contains(line, "large")) {
|
||||
continue
|
||||
}
|
||||
nfound++
|
||||
want := "100.0%"
|
||||
if f[len(f)-1] != want {
|
||||
t.Errorf("wanted %s got: %q\n", want, line)
|
||||
bad = true
|
||||
}
|
||||
}
|
||||
if nfound != 1 {
|
||||
t.Errorf("wanted 1 found, got %d\n", nfound)
|
||||
bad = true
|
||||
}
|
||||
if bad {
|
||||
t.Logf("func output:\n%s\n", string(b))
|
||||
}
|
||||
}
|
||||
42
src/internal/coverage/cfile/hooks.go
Normal file
42
src/internal/coverage/cfile/hooks.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// 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
|
||||
|
||||
import _ "unsafe"
|
||||
|
||||
// InitHook is invoked from the main package "init" routine in
|
||||
// programs built with "-cover". This function is intended to be
|
||||
// called only by the compiler (via runtime/coverage.initHook).
|
||||
//
|
||||
// If 'istest' is false, it indicates we're building a regular program
|
||||
// ("go build -cover ..."), in which case we immediately try to write
|
||||
// out the meta-data file, and register emitCounterData as an exit
|
||||
// hook.
|
||||
//
|
||||
// If 'istest' is true (indicating that the program in question is a
|
||||
// Go test binary), then we tentatively queue up both emitMetaData and
|
||||
// emitCounterData as exit hooks. In the normal case (e.g. regular "go
|
||||
// test -cover" run) the testmain.go boilerplate will run at the end
|
||||
// of the test, write out the coverage percentage, and then invoke
|
||||
// MarkProfileEmitted to indicate that no more work needs to be
|
||||
// done. If however that call is never made, this is a sign that the
|
||||
// test binary is being used as a replacement binary for the tool
|
||||
// being tested, hence we do want to run exit hooks when the program
|
||||
// terminates.
|
||||
func InitHook(istest bool) {
|
||||
// Note: hooks are run in reverse registration order, so
|
||||
// register the counter data hook before the meta-data hook
|
||||
// (in the case where two hooks are needed).
|
||||
runOnNonZeroExit := true
|
||||
runtime_addExitHook(emitCounterData, runOnNonZeroExit)
|
||||
if istest {
|
||||
runtime_addExitHook(emitMetaData, runOnNonZeroExit)
|
||||
} else {
|
||||
emitMetaData()
|
||||
}
|
||||
}
|
||||
|
||||
//go:linkname runtime_addExitHook runtime.addExitHook
|
||||
func runtime_addExitHook(f func(), runOnNonZeroExit bool)
|
||||
259
src/internal/coverage/cfile/testdata/harness.go
vendored
Normal file
259
src/internal/coverage/cfile/testdata/harness.go
vendored
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"internal/coverage/slicewriter"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/coverage"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var verbflag = flag.Int("v", 0, "Verbose trace output level")
|
||||
var testpointflag = flag.String("tp", "", "Testpoint to run")
|
||||
var outdirflag = flag.String("o", "", "Output dir into which to emit")
|
||||
|
||||
func emitToWriter() {
|
||||
log.SetPrefix("emitToWriter: ")
|
||||
var slwm slicewriter.WriteSeeker
|
||||
if err := coverage.WriteMeta(&slwm); err != nil {
|
||||
log.Fatalf("error: WriteMeta returns %v", err)
|
||||
}
|
||||
mf := filepath.Join(*outdirflag, "covmeta.0abcdef")
|
||||
if err := os.WriteFile(mf, slwm.BytesWritten(), 0666); err != nil {
|
||||
log.Fatalf("error: writing %s: %v", mf, err)
|
||||
}
|
||||
var slwc slicewriter.WriteSeeker
|
||||
if err := coverage.WriteCounters(&slwc); err != nil {
|
||||
log.Fatalf("error: WriteCounters returns %v", err)
|
||||
}
|
||||
cf := filepath.Join(*outdirflag, "covcounters.0abcdef.99.77")
|
||||
if err := os.WriteFile(cf, slwc.BytesWritten(), 0666); err != nil {
|
||||
log.Fatalf("error: writing %s: %v", cf, err)
|
||||
}
|
||||
}
|
||||
|
||||
func emitToDir() {
|
||||
log.SetPrefix("emitToDir: ")
|
||||
if err := coverage.WriteMetaDir(*outdirflag); err != nil {
|
||||
log.Fatalf("error: WriteMetaDir returns %v", err)
|
||||
}
|
||||
if err := coverage.WriteCountersDir(*outdirflag); err != nil {
|
||||
log.Fatalf("error: WriteCountersDir returns %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func emitToNonexistentDir() {
|
||||
log.SetPrefix("emitToNonexistentDir: ")
|
||||
|
||||
want := []string{
|
||||
"no such file or directory", // linux-ish
|
||||
"system cannot find the file specified", // windows
|
||||
"does not exist", // plan9
|
||||
}
|
||||
|
||||
checkWant := func(which string, got string) {
|
||||
found := false
|
||||
for _, w := range want {
|
||||
if strings.Contains(got, w) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Fatalf("%s emit to bad dir: got error:\n %v\nwanted error with one of:\n %+v", which, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Mangle the output directory to produce something nonexistent.
|
||||
mangled := *outdirflag + "_MANGLED"
|
||||
if err := coverage.WriteMetaDir(mangled); err == nil {
|
||||
log.Fatal("expected error from WriteMetaDir to nonexistent dir")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
checkWant("meta data", got)
|
||||
}
|
||||
|
||||
// Now try to emit counter data file to a bad dir.
|
||||
if err := coverage.WriteCountersDir(mangled); err == nil {
|
||||
log.Fatal("expected error emitting counter data to bad dir")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
checkWant("counter data", got)
|
||||
}
|
||||
}
|
||||
|
||||
func emitToUnwritableDir() {
|
||||
log.SetPrefix("emitToUnwritableDir: ")
|
||||
|
||||
want := "permission denied"
|
||||
|
||||
if err := coverage.WriteMetaDir(*outdirflag); err == nil {
|
||||
log.Fatal("expected error from WriteMetaDir to unwritable dir")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if !strings.Contains(got, want) {
|
||||
log.Fatalf("meta-data emit to unwritable dir: wanted error containing %q got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Similarly with writing counter data.
|
||||
if err := coverage.WriteCountersDir(*outdirflag); err == nil {
|
||||
log.Fatal("expected error emitting counter data to unwritable dir")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if !strings.Contains(got, want) {
|
||||
log.Fatalf("emitting counter data to unwritable dir: wanted error containing %q got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func emitToNilWriter() {
|
||||
log.SetPrefix("emitToWriter: ")
|
||||
want := "nil writer"
|
||||
var bad io.WriteSeeker
|
||||
if err := coverage.WriteMeta(bad); err == nil {
|
||||
log.Fatal("expected error passing nil writer for meta emit")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if !strings.Contains(got, want) {
|
||||
log.Fatalf("emitting meta-data passing nil writer: wanted error containing %q got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
if err := coverage.WriteCounters(bad); err == nil {
|
||||
log.Fatal("expected error passing nil writer for counter emit")
|
||||
} else {
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if !strings.Contains(got, want) {
|
||||
log.Fatalf("emitting counter data passing nil writer: wanted error containing %q got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type failingWriter struct {
|
||||
writeCount int
|
||||
writeLimit int
|
||||
slws slicewriter.WriteSeeker
|
||||
}
|
||||
|
||||
func (f *failingWriter) Write(p []byte) (n int, err error) {
|
||||
c := f.writeCount
|
||||
f.writeCount++
|
||||
if f.writeLimit < 0 || c < f.writeLimit {
|
||||
return f.slws.Write(p)
|
||||
}
|
||||
return 0, fmt.Errorf("manufactured write error")
|
||||
}
|
||||
|
||||
func (f *failingWriter) Seek(offset int64, whence int) (int64, error) {
|
||||
return f.slws.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f *failingWriter) reset(lim int) {
|
||||
f.writeCount = 0
|
||||
f.writeLimit = lim
|
||||
f.slws = slicewriter.WriteSeeker{}
|
||||
}
|
||||
|
||||
func writeStressTest(tag string, testf func(testf *failingWriter) error) {
|
||||
// Invoke the function initially without the write limit
|
||||
// set, to capture the number of writes performed.
|
||||
fw := &failingWriter{writeLimit: -1}
|
||||
testf(fw)
|
||||
|
||||
// Now that we know how many writes are going to happen, run the
|
||||
// function repeatedly, each time with a Write operation set to
|
||||
// fail at a new spot. The goal here is to make sure that:
|
||||
// A) an error is reported, and B) nothing crashes.
|
||||
tot := fw.writeCount
|
||||
for i := 0; i < tot; i++ {
|
||||
fw.reset(i)
|
||||
err := testf(fw)
|
||||
if err == nil {
|
||||
log.Fatalf("no error from write %d tag %s", i, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func postClear() int {
|
||||
return 42
|
||||
}
|
||||
|
||||
func preClear() int {
|
||||
return 42
|
||||
}
|
||||
|
||||
// This test is designed to ensure that write errors are properly
|
||||
// handled by the code that writes out coverage data. It repeatedly
|
||||
// invokes the 'emit to writer' apis using a specially crafted writer
|
||||
// that captures the total number of expected writes, then replays the
|
||||
// execution N times with a manufactured write error at the
|
||||
// appropriate spot.
|
||||
func emitToFailingWriter() {
|
||||
log.SetPrefix("emitToFailingWriter: ")
|
||||
|
||||
writeStressTest("emit-meta", func(f *failingWriter) error {
|
||||
return coverage.WriteMeta(f)
|
||||
})
|
||||
writeStressTest("emit-counter", func(f *failingWriter) error {
|
||||
return coverage.WriteCounters(f)
|
||||
})
|
||||
}
|
||||
|
||||
func emitWithCounterClear() {
|
||||
log.SetPrefix("emitWitCounterClear: ")
|
||||
preClear()
|
||||
if err := coverage.ClearCounters(); err != nil {
|
||||
log.Fatalf("clear failed: %v", err)
|
||||
}
|
||||
postClear()
|
||||
if err := coverage.WriteMetaDir(*outdirflag); err != nil {
|
||||
log.Fatalf("error: WriteMetaDir returns %v", err)
|
||||
}
|
||||
if err := coverage.WriteCountersDir(*outdirflag); err != nil {
|
||||
log.Fatalf("error: WriteCountersDir returns %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func final() int {
|
||||
println("I run last.")
|
||||
return 43
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
if *testpointflag == "" {
|
||||
log.Fatalf("error: no testpoint (use -tp flag)")
|
||||
}
|
||||
if *outdirflag == "" {
|
||||
log.Fatalf("error: no output dir specified (use -o flag)")
|
||||
}
|
||||
switch *testpointflag {
|
||||
case "emitToDir":
|
||||
emitToDir()
|
||||
case "emitToWriter":
|
||||
emitToWriter()
|
||||
case "emitToNonexistentDir":
|
||||
emitToNonexistentDir()
|
||||
case "emitToUnwritableDir":
|
||||
emitToUnwritableDir()
|
||||
case "emitToNilWriter":
|
||||
emitToNilWriter()
|
||||
case "emitToFailingWriter":
|
||||
emitToFailingWriter()
|
||||
case "emitWithCounterClear":
|
||||
emitWithCounterClear()
|
||||
default:
|
||||
log.Fatalf("error: unknown testpoint %q", *testpointflag)
|
||||
}
|
||||
final()
|
||||
}
|
||||
26
src/internal/coverage/cfile/testdata/issue56006/repro.go
vendored
Normal file
26
src/internal/coverage/cfile/testdata/issue56006/repro.go
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
//go:noinline
|
||||
func blah(x int) int {
|
||||
if x != 0 {
|
||||
return x + 42
|
||||
}
|
||||
return x - 42
|
||||
}
|
||||
|
||||
func main() {
|
||||
go infloop()
|
||||
println(blah(1) + blah(0))
|
||||
}
|
||||
|
||||
var G int
|
||||
|
||||
func infloop() {
|
||||
for {
|
||||
G += blah(1)
|
||||
G += blah(0)
|
||||
if G > 10000 {
|
||||
G = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/internal/coverage/cfile/testdata/issue56006/repro_test.go
vendored
Normal file
8
src/internal/coverage/cfile/testdata/issue56006/repro_test.go
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSomething(t *testing.T) {
|
||||
go infloop()
|
||||
println(blah(1) + blah(0))
|
||||
}
|
||||
823
src/internal/coverage/cfile/testdata/issue59563/repro.go
vendored
Normal file
823
src/internal/coverage/cfile/testdata/issue59563/repro.go
vendored
Normal file
|
|
@ -0,0 +1,823 @@
|
|||
// Copyright 2023 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 repro
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func small() {
|
||||
go func() {
|
||||
fmt.Println(http.ListenAndServe("localhost:7070", nil))
|
||||
}()
|
||||
}
|
||||
|
||||
func large(x int) int {
|
||||
if x == 0 {
|
||||
x += 0
|
||||
} else if x == 1 {
|
||||
x += 1
|
||||
} else if x == 2 {
|
||||
x += 2
|
||||
} else if x == 3 {
|
||||
x += 3
|
||||
} else if x == 4 {
|
||||
x += 4
|
||||
} else if x == 5 {
|
||||
x += 5
|
||||
} else if x == 6 {
|
||||
x += 6
|
||||
} else if x == 7 {
|
||||
x += 7
|
||||
} else if x == 8 {
|
||||
x += 8
|
||||
} else if x == 9 {
|
||||
x += 9
|
||||
} else if x == 10 {
|
||||
x += 10
|
||||
} else if x == 11 {
|
||||
x += 11
|
||||
} else if x == 12 {
|
||||
x += 12
|
||||
} else if x == 13 {
|
||||
x += 13
|
||||
} else if x == 14 {
|
||||
x += 14
|
||||
} else if x == 15 {
|
||||
x += 15
|
||||
} else if x == 16 {
|
||||
x += 16
|
||||
} else if x == 17 {
|
||||
x += 17
|
||||
} else if x == 18 {
|
||||
x += 18
|
||||
} else if x == 19 {
|
||||
x += 19
|
||||
} else if x == 20 {
|
||||
x += 20
|
||||
} else if x == 21 {
|
||||
x += 21
|
||||
} else if x == 22 {
|
||||
x += 22
|
||||
} else if x == 23 {
|
||||
x += 23
|
||||
} else if x == 24 {
|
||||
x += 24
|
||||
} else if x == 25 {
|
||||
x += 25
|
||||
} else if x == 26 {
|
||||
x += 26
|
||||
} else if x == 27 {
|
||||
x += 27
|
||||
} else if x == 28 {
|
||||
x += 28
|
||||
} else if x == 29 {
|
||||
x += 29
|
||||
} else if x == 30 {
|
||||
x += 30
|
||||
} else if x == 31 {
|
||||
x += 31
|
||||
} else if x == 32 {
|
||||
x += 32
|
||||
} else if x == 33 {
|
||||
x += 33
|
||||
} else if x == 34 {
|
||||
x += 34
|
||||
} else if x == 35 {
|
||||
x += 35
|
||||
} else if x == 36 {
|
||||
x += 36
|
||||
} else if x == 37 {
|
||||
x += 37
|
||||
} else if x == 38 {
|
||||
x += 38
|
||||
} else if x == 39 {
|
||||
x += 39
|
||||
} else if x == 40 {
|
||||
x += 40
|
||||
} else if x == 41 {
|
||||
x += 41
|
||||
} else if x == 42 {
|
||||
x += 42
|
||||
} else if x == 43 {
|
||||
x += 43
|
||||
} else if x == 44 {
|
||||
x += 44
|
||||
} else if x == 45 {
|
||||
x += 45
|
||||
} else if x == 46 {
|
||||
x += 46
|
||||
} else if x == 47 {
|
||||
x += 47
|
||||
} else if x == 48 {
|
||||
x += 48
|
||||
} else if x == 49 {
|
||||
x += 49
|
||||
} else if x == 50 {
|
||||
x += 50
|
||||
} else if x == 51 {
|
||||
x += 51
|
||||
} else if x == 52 {
|
||||
x += 52
|
||||
} else if x == 53 {
|
||||
x += 53
|
||||
} else if x == 54 {
|
||||
x += 54
|
||||
} else if x == 55 {
|
||||
x += 55
|
||||
} else if x == 56 {
|
||||
x += 56
|
||||
} else if x == 57 {
|
||||
x += 57
|
||||
} else if x == 58 {
|
||||
x += 58
|
||||
} else if x == 59 {
|
||||
x += 59
|
||||
} else if x == 60 {
|
||||
x += 60
|
||||
} else if x == 61 {
|
||||
x += 61
|
||||
} else if x == 62 {
|
||||
x += 62
|
||||
} else if x == 63 {
|
||||
x += 63
|
||||
} else if x == 64 {
|
||||
x += 64
|
||||
} else if x == 65 {
|
||||
x += 65
|
||||
} else if x == 66 {
|
||||
x += 66
|
||||
} else if x == 67 {
|
||||
x += 67
|
||||
} else if x == 68 {
|
||||
x += 68
|
||||
} else if x == 69 {
|
||||
x += 69
|
||||
} else if x == 70 {
|
||||
x += 70
|
||||
} else if x == 71 {
|
||||
x += 71
|
||||
} else if x == 72 {
|
||||
x += 72
|
||||
} else if x == 73 {
|
||||
x += 73
|
||||
} else if x == 74 {
|
||||
x += 74
|
||||
} else if x == 75 {
|
||||
x += 75
|
||||
} else if x == 76 {
|
||||
x += 76
|
||||
} else if x == 77 {
|
||||
x += 77
|
||||
} else if x == 78 {
|
||||
x += 78
|
||||
} else if x == 79 {
|
||||
x += 79
|
||||
} else if x == 80 {
|
||||
x += 80
|
||||
} else if x == 81 {
|
||||
x += 81
|
||||
} else if x == 82 {
|
||||
x += 82
|
||||
} else if x == 83 {
|
||||
x += 83
|
||||
} else if x == 84 {
|
||||
x += 84
|
||||
} else if x == 85 {
|
||||
x += 85
|
||||
} else if x == 86 {
|
||||
x += 86
|
||||
} else if x == 87 {
|
||||
x += 87
|
||||
} else if x == 88 {
|
||||
x += 88
|
||||
} else if x == 89 {
|
||||
x += 89
|
||||
} else if x == 90 {
|
||||
x += 90
|
||||
} else if x == 91 {
|
||||
x += 91
|
||||
} else if x == 92 {
|
||||
x += 92
|
||||
} else if x == 93 {
|
||||
x += 93
|
||||
} else if x == 94 {
|
||||
x += 94
|
||||
} else if x == 95 {
|
||||
x += 95
|
||||
} else if x == 96 {
|
||||
x += 96
|
||||
} else if x == 97 {
|
||||
x += 97
|
||||
} else if x == 98 {
|
||||
x += 98
|
||||
} else if x == 99 {
|
||||
x += 99
|
||||
} else if x == 100 {
|
||||
x += 100
|
||||
} else if x == 101 {
|
||||
x += 101
|
||||
} else if x == 102 {
|
||||
x += 102
|
||||
} else if x == 103 {
|
||||
x += 103
|
||||
} else if x == 104 {
|
||||
x += 104
|
||||
} else if x == 105 {
|
||||
x += 105
|
||||
} else if x == 106 {
|
||||
x += 106
|
||||
} else if x == 107 {
|
||||
x += 107
|
||||
} else if x == 108 {
|
||||
x += 108
|
||||
} else if x == 109 {
|
||||
x += 109
|
||||
} else if x == 110 {
|
||||
x += 110
|
||||
} else if x == 111 {
|
||||
x += 111
|
||||
} else if x == 112 {
|
||||
x += 112
|
||||
} else if x == 113 {
|
||||
x += 113
|
||||
} else if x == 114 {
|
||||
x += 114
|
||||
} else if x == 115 {
|
||||
x += 115
|
||||
} else if x == 116 {
|
||||
x += 116
|
||||
} else if x == 117 {
|
||||
x += 117
|
||||
} else if x == 118 {
|
||||
x += 118
|
||||
} else if x == 119 {
|
||||
x += 119
|
||||
} else if x == 120 {
|
||||
x += 120
|
||||
} else if x == 121 {
|
||||
x += 121
|
||||
} else if x == 122 {
|
||||
x += 122
|
||||
} else if x == 123 {
|
||||
x += 123
|
||||
} else if x == 124 {
|
||||
x += 124
|
||||
} else if x == 125 {
|
||||
x += 125
|
||||
} else if x == 126 {
|
||||
x += 126
|
||||
} else if x == 127 {
|
||||
x += 127
|
||||
} else if x == 128 {
|
||||
x += 128
|
||||
} else if x == 129 {
|
||||
x += 129
|
||||
} else if x == 130 {
|
||||
x += 130
|
||||
} else if x == 131 {
|
||||
x += 131
|
||||
} else if x == 132 {
|
||||
x += 132
|
||||
} else if x == 133 {
|
||||
x += 133
|
||||
} else if x == 134 {
|
||||
x += 134
|
||||
} else if x == 135 {
|
||||
x += 135
|
||||
} else if x == 136 {
|
||||
x += 136
|
||||
} else if x == 137 {
|
||||
x += 137
|
||||
} else if x == 138 {
|
||||
x += 138
|
||||
} else if x == 139 {
|
||||
x += 139
|
||||
} else if x == 140 {
|
||||
x += 140
|
||||
} else if x == 141 {
|
||||
x += 141
|
||||
} else if x == 142 {
|
||||
x += 142
|
||||
} else if x == 143 {
|
||||
x += 143
|
||||
} else if x == 144 {
|
||||
x += 144
|
||||
} else if x == 145 {
|
||||
x += 145
|
||||
} else if x == 146 {
|
||||
x += 146
|
||||
} else if x == 147 {
|
||||
x += 147
|
||||
} else if x == 148 {
|
||||
x += 148
|
||||
} else if x == 149 {
|
||||
x += 149
|
||||
} else if x == 150 {
|
||||
x += 150
|
||||
} else if x == 151 {
|
||||
x += 151
|
||||
} else if x == 152 {
|
||||
x += 152
|
||||
} else if x == 153 {
|
||||
x += 153
|
||||
} else if x == 154 {
|
||||
x += 154
|
||||
} else if x == 155 {
|
||||
x += 155
|
||||
} else if x == 156 {
|
||||
x += 156
|
||||
} else if x == 157 {
|
||||
x += 157
|
||||
} else if x == 158 {
|
||||
x += 158
|
||||
} else if x == 159 {
|
||||
x += 159
|
||||
} else if x == 160 {
|
||||
x += 160
|
||||
} else if x == 161 {
|
||||
x += 161
|
||||
} else if x == 162 {
|
||||
x += 162
|
||||
} else if x == 163 {
|
||||
x += 163
|
||||
} else if x == 164 {
|
||||
x += 164
|
||||
} else if x == 165 {
|
||||
x += 165
|
||||
} else if x == 166 {
|
||||
x += 166
|
||||
} else if x == 167 {
|
||||
x += 167
|
||||
} else if x == 168 {
|
||||
x += 168
|
||||
} else if x == 169 {
|
||||
x += 169
|
||||
} else if x == 170 {
|
||||
x += 170
|
||||
} else if x == 171 {
|
||||
x += 171
|
||||
} else if x == 172 {
|
||||
x += 172
|
||||
} else if x == 173 {
|
||||
x += 173
|
||||
} else if x == 174 {
|
||||
x += 174
|
||||
} else if x == 175 {
|
||||
x += 175
|
||||
} else if x == 176 {
|
||||
x += 176
|
||||
} else if x == 177 {
|
||||
x += 177
|
||||
} else if x == 178 {
|
||||
x += 178
|
||||
} else if x == 179 {
|
||||
x += 179
|
||||
} else if x == 180 {
|
||||
x += 180
|
||||
} else if x == 181 {
|
||||
x += 181
|
||||
} else if x == 182 {
|
||||
x += 182
|
||||
} else if x == 183 {
|
||||
x += 183
|
||||
} else if x == 184 {
|
||||
x += 184
|
||||
} else if x == 185 {
|
||||
x += 185
|
||||
} else if x == 186 {
|
||||
x += 186
|
||||
} else if x == 187 {
|
||||
x += 187
|
||||
} else if x == 188 {
|
||||
x += 188
|
||||
} else if x == 189 {
|
||||
x += 189
|
||||
} else if x == 190 {
|
||||
x += 190
|
||||
} else if x == 191 {
|
||||
x += 191
|
||||
} else if x == 192 {
|
||||
x += 192
|
||||
} else if x == 193 {
|
||||
x += 193
|
||||
} else if x == 194 {
|
||||
x += 194
|
||||
} else if x == 195 {
|
||||
x += 195
|
||||
} else if x == 196 {
|
||||
x += 196
|
||||
} else if x == 197 {
|
||||
x += 197
|
||||
} else if x == 198 {
|
||||
x += 198
|
||||
} else if x == 199 {
|
||||
x += 199
|
||||
} else if x == 200 {
|
||||
x += 200
|
||||
} else if x == 201 {
|
||||
x += 201
|
||||
} else if x == 202 {
|
||||
x += 202
|
||||
} else if x == 203 {
|
||||
x += 203
|
||||
} else if x == 204 {
|
||||
x += 204
|
||||
} else if x == 205 {
|
||||
x += 205
|
||||
} else if x == 206 {
|
||||
x += 206
|
||||
} else if x == 207 {
|
||||
x += 207
|
||||
} else if x == 208 {
|
||||
x += 208
|
||||
} else if x == 209 {
|
||||
x += 209
|
||||
} else if x == 210 {
|
||||
x += 210
|
||||
} else if x == 211 {
|
||||
x += 211
|
||||
} else if x == 212 {
|
||||
x += 212
|
||||
} else if x == 213 {
|
||||
x += 213
|
||||
} else if x == 214 {
|
||||
x += 214
|
||||
} else if x == 215 {
|
||||
x += 215
|
||||
} else if x == 216 {
|
||||
x += 216
|
||||
} else if x == 217 {
|
||||
x += 217
|
||||
} else if x == 218 {
|
||||
x += 218
|
||||
} else if x == 219 {
|
||||
x += 219
|
||||
} else if x == 220 {
|
||||
x += 220
|
||||
} else if x == 221 {
|
||||
x += 221
|
||||
} else if x == 222 {
|
||||
x += 222
|
||||
} else if x == 223 {
|
||||
x += 223
|
||||
} else if x == 224 {
|
||||
x += 224
|
||||
} else if x == 225 {
|
||||
x += 225
|
||||
} else if x == 226 {
|
||||
x += 226
|
||||
} else if x == 227 {
|
||||
x += 227
|
||||
} else if x == 228 {
|
||||
x += 228
|
||||
} else if x == 229 {
|
||||
x += 229
|
||||
} else if x == 230 {
|
||||
x += 230
|
||||
} else if x == 231 {
|
||||
x += 231
|
||||
} else if x == 232 {
|
||||
x += 232
|
||||
} else if x == 233 {
|
||||
x += 233
|
||||
} else if x == 234 {
|
||||
x += 234
|
||||
} else if x == 235 {
|
||||
x += 235
|
||||
} else if x == 236 {
|
||||
x += 236
|
||||
} else if x == 237 {
|
||||
x += 237
|
||||
} else if x == 238 {
|
||||
x += 238
|
||||
} else if x == 239 {
|
||||
x += 239
|
||||
} else if x == 240 {
|
||||
x += 240
|
||||
} else if x == 241 {
|
||||
x += 241
|
||||
} else if x == 242 {
|
||||
x += 242
|
||||
} else if x == 243 {
|
||||
x += 243
|
||||
} else if x == 244 {
|
||||
x += 244
|
||||
} else if x == 245 {
|
||||
x += 245
|
||||
} else if x == 246 {
|
||||
x += 246
|
||||
} else if x == 247 {
|
||||
x += 247
|
||||
} else if x == 248 {
|
||||
x += 248
|
||||
} else if x == 249 {
|
||||
x += 249
|
||||
} else if x == 250 {
|
||||
x += 250
|
||||
} else if x == 251 {
|
||||
x += 251
|
||||
} else if x == 252 {
|
||||
x += 252
|
||||
} else if x == 253 {
|
||||
x += 253
|
||||
} else if x == 254 {
|
||||
x += 254
|
||||
} else if x == 255 {
|
||||
x += 255
|
||||
} else if x == 256 {
|
||||
x += 256
|
||||
} else if x == 257 {
|
||||
x += 257
|
||||
} else if x == 258 {
|
||||
x += 258
|
||||
} else if x == 259 {
|
||||
x += 259
|
||||
} else if x == 260 {
|
||||
x += 260
|
||||
} else if x == 261 {
|
||||
x += 261
|
||||
} else if x == 262 {
|
||||
x += 262
|
||||
} else if x == 263 {
|
||||
x += 263
|
||||
} else if x == 264 {
|
||||
x += 264
|
||||
} else if x == 265 {
|
||||
x += 265
|
||||
} else if x == 266 {
|
||||
x += 266
|
||||
} else if x == 267 {
|
||||
x += 267
|
||||
} else if x == 268 {
|
||||
x += 268
|
||||
} else if x == 269 {
|
||||
x += 269
|
||||
} else if x == 270 {
|
||||
x += 270
|
||||
} else if x == 271 {
|
||||
x += 271
|
||||
} else if x == 272 {
|
||||
x += 272
|
||||
} else if x == 273 {
|
||||
x += 273
|
||||
} else if x == 274 {
|
||||
x += 274
|
||||
} else if x == 275 {
|
||||
x += 275
|
||||
} else if x == 276 {
|
||||
x += 276
|
||||
} else if x == 277 {
|
||||
x += 277
|
||||
} else if x == 278 {
|
||||
x += 278
|
||||
} else if x == 279 {
|
||||
x += 279
|
||||
} else if x == 280 {
|
||||
x += 280
|
||||
} else if x == 281 {
|
||||
x += 281
|
||||
} else if x == 282 {
|
||||
x += 282
|
||||
} else if x == 283 {
|
||||
x += 283
|
||||
} else if x == 284 {
|
||||
x += 284
|
||||
} else if x == 285 {
|
||||
x += 285
|
||||
} else if x == 286 {
|
||||
x += 286
|
||||
} else if x == 287 {
|
||||
x += 287
|
||||
} else if x == 288 {
|
||||
x += 288
|
||||
} else if x == 289 {
|
||||
x += 289
|
||||
} else if x == 290 {
|
||||
x += 290
|
||||
} else if x == 291 {
|
||||
x += 291
|
||||
} else if x == 292 {
|
||||
x += 292
|
||||
} else if x == 293 {
|
||||
x += 293
|
||||
} else if x == 294 {
|
||||
x += 294
|
||||
} else if x == 295 {
|
||||
x += 295
|
||||
} else if x == 296 {
|
||||
x += 296
|
||||
} else if x == 297 {
|
||||
x += 297
|
||||
} else if x == 298 {
|
||||
x += 298
|
||||
} else if x == 299 {
|
||||
x += 299
|
||||
} else if x == 300 {
|
||||
x += 300
|
||||
} else if x == 301 {
|
||||
x += 301
|
||||
} else if x == 302 {
|
||||
x += 302
|
||||
} else if x == 303 {
|
||||
x += 303
|
||||
} else if x == 304 {
|
||||
x += 304
|
||||
} else if x == 305 {
|
||||
x += 305
|
||||
} else if x == 306 {
|
||||
x += 306
|
||||
} else if x == 307 {
|
||||
x += 307
|
||||
} else if x == 308 {
|
||||
x += 308
|
||||
} else if x == 309 {
|
||||
x += 309
|
||||
} else if x == 310 {
|
||||
x += 310
|
||||
} else if x == 311 {
|
||||
x += 311
|
||||
} else if x == 312 {
|
||||
x += 312
|
||||
} else if x == 313 {
|
||||
x += 313
|
||||
} else if x == 314 {
|
||||
x += 314
|
||||
} else if x == 315 {
|
||||
x += 315
|
||||
} else if x == 316 {
|
||||
x += 316
|
||||
} else if x == 317 {
|
||||
x += 317
|
||||
} else if x == 318 {
|
||||
x += 318
|
||||
} else if x == 319 {
|
||||
x += 319
|
||||
} else if x == 320 {
|
||||
x += 320
|
||||
} else if x == 321 {
|
||||
x += 321
|
||||
} else if x == 322 {
|
||||
x += 322
|
||||
} else if x == 323 {
|
||||
x += 323
|
||||
} else if x == 324 {
|
||||
x += 324
|
||||
} else if x == 325 {
|
||||
x += 325
|
||||
} else if x == 326 {
|
||||
x += 326
|
||||
} else if x == 327 {
|
||||
x += 327
|
||||
} else if x == 328 {
|
||||
x += 328
|
||||
} else if x == 329 {
|
||||
x += 329
|
||||
} else if x == 330 {
|
||||
x += 330
|
||||
} else if x == 331 {
|
||||
x += 331
|
||||
} else if x == 332 {
|
||||
x += 332
|
||||
} else if x == 333 {
|
||||
x += 333
|
||||
} else if x == 334 {
|
||||
x += 334
|
||||
} else if x == 335 {
|
||||
x += 335
|
||||
} else if x == 336 {
|
||||
x += 336
|
||||
} else if x == 337 {
|
||||
x += 337
|
||||
} else if x == 338 {
|
||||
x += 338
|
||||
} else if x == 339 {
|
||||
x += 339
|
||||
} else if x == 340 {
|
||||
x += 340
|
||||
} else if x == 341 {
|
||||
x += 341
|
||||
} else if x == 342 {
|
||||
x += 342
|
||||
} else if x == 343 {
|
||||
x += 343
|
||||
} else if x == 344 {
|
||||
x += 344
|
||||
} else if x == 345 {
|
||||
x += 345
|
||||
} else if x == 346 {
|
||||
x += 346
|
||||
} else if x == 347 {
|
||||
x += 347
|
||||
} else if x == 348 {
|
||||
x += 348
|
||||
} else if x == 349 {
|
||||
x += 349
|
||||
} else if x == 350 {
|
||||
x += 350
|
||||
} else if x == 351 {
|
||||
x += 351
|
||||
} else if x == 352 {
|
||||
x += 352
|
||||
} else if x == 353 {
|
||||
x += 353
|
||||
} else if x == 354 {
|
||||
x += 354
|
||||
} else if x == 355 {
|
||||
x += 355
|
||||
} else if x == 356 {
|
||||
x += 356
|
||||
} else if x == 357 {
|
||||
x += 357
|
||||
} else if x == 358 {
|
||||
x += 358
|
||||
} else if x == 359 {
|
||||
x += 359
|
||||
} else if x == 360 {
|
||||
x += 360
|
||||
} else if x == 361 {
|
||||
x += 361
|
||||
} else if x == 362 {
|
||||
x += 362
|
||||
} else if x == 363 {
|
||||
x += 363
|
||||
} else if x == 364 {
|
||||
x += 364
|
||||
} else if x == 365 {
|
||||
x += 365
|
||||
} else if x == 366 {
|
||||
x += 366
|
||||
} else if x == 367 {
|
||||
x += 367
|
||||
} else if x == 368 {
|
||||
x += 368
|
||||
} else if x == 369 {
|
||||
x += 369
|
||||
} else if x == 370 {
|
||||
x += 370
|
||||
} else if x == 371 {
|
||||
x += 371
|
||||
} else if x == 372 {
|
||||
x += 372
|
||||
} else if x == 373 {
|
||||
x += 373
|
||||
} else if x == 374 {
|
||||
x += 374
|
||||
} else if x == 375 {
|
||||
x += 375
|
||||
} else if x == 376 {
|
||||
x += 376
|
||||
} else if x == 377 {
|
||||
x += 377
|
||||
} else if x == 378 {
|
||||
x += 378
|
||||
} else if x == 379 {
|
||||
x += 379
|
||||
} else if x == 380 {
|
||||
x += 380
|
||||
} else if x == 381 {
|
||||
x += 381
|
||||
} else if x == 382 {
|
||||
x += 382
|
||||
} else if x == 383 {
|
||||
x += 383
|
||||
} else if x == 384 {
|
||||
x += 384
|
||||
} else if x == 385 {
|
||||
x += 385
|
||||
} else if x == 386 {
|
||||
x += 386
|
||||
} else if x == 387 {
|
||||
x += 387
|
||||
} else if x == 388 {
|
||||
x += 388
|
||||
} else if x == 389 {
|
||||
x += 389
|
||||
} else if x == 390 {
|
||||
x += 390
|
||||
} else if x == 391 {
|
||||
x += 391
|
||||
} else if x == 392 {
|
||||
x += 392
|
||||
} else if x == 393 {
|
||||
x += 393
|
||||
} else if x == 394 {
|
||||
x += 394
|
||||
} else if x == 395 {
|
||||
x += 395
|
||||
} else if x == 396 {
|
||||
x += 396
|
||||
} else if x == 397 {
|
||||
x += 397
|
||||
} else if x == 398 {
|
||||
x += 398
|
||||
} else if x == 399 {
|
||||
x += 399
|
||||
} else if x == 400 {
|
||||
x += 400
|
||||
}
|
||||
return x * x
|
||||
}
|
||||
14
src/internal/coverage/cfile/testdata/issue59563/repro_test.go
vendored
Normal file
14
src/internal/coverage/cfile/testdata/issue59563/repro_test.go
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2023 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 repro
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSomething(t *testing.T) {
|
||||
small()
|
||||
for i := 0; i < 1001; i++ {
|
||||
large(i)
|
||||
}
|
||||
}
|
||||
317
src/internal/coverage/cfile/testsupport.go
Normal file
317
src/internal/coverage/cfile/testsupport.go
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"internal/coverage"
|
||||
"internal/coverage/calloc"
|
||||
"internal/coverage/cformat"
|
||||
"internal/coverage/cmerge"
|
||||
"internal/coverage/decodecounter"
|
||||
"internal/coverage/decodemeta"
|
||||
"internal/coverage/pods"
|
||||
"internal/runtime/atomic"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ProcessCoverTestDir is called from
|
||||
// testmain code when "go test -cover" is in effect. It is not
|
||||
// intended to be used other than internally by the Go command's
|
||||
// generated code.
|
||||
func ProcessCoverTestDir(dir string, cfile string, cm string, cpkg string, w io.Writer) error {
|
||||
cmode := coverage.ParseCounterMode(cm)
|
||||
if cmode == coverage.CtrModeInvalid {
|
||||
return fmt.Errorf("invalid counter mode %q", cm)
|
||||
}
|
||||
|
||||
// Emit meta-data and counter data.
|
||||
ml := getCovMetaList()
|
||||
if len(ml) == 0 {
|
||||
// This corresponds to the case where we have a package that
|
||||
// contains test code but no functions (which is fine). In this
|
||||
// case there is no need to emit anything.
|
||||
} else {
|
||||
if err := emitMetaDataToDirectory(dir, ml); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := emitCounterDataToDirectory(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Collect pods from test run. For the majority of cases we would
|
||||
// expect to see a single pod here, but allow for multiple pods in
|
||||
// case the test harness is doing extra work to collect data files
|
||||
// from builds that it kicks off as part of the testing.
|
||||
podlist, err := pods.CollectPods([]string{dir}, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading from %s: %v", dir, err)
|
||||
}
|
||||
|
||||
// Open text output file if appropriate.
|
||||
var tf *os.File
|
||||
var tfClosed bool
|
||||
if cfile != "" {
|
||||
var err error
|
||||
tf, err = os.Create(cfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal error: opening coverage data output file %q: %v", cfile, err)
|
||||
}
|
||||
defer func() {
|
||||
if !tfClosed {
|
||||
tfClosed = true
|
||||
tf.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Read/process the pods.
|
||||
ts := &tstate{
|
||||
cm: &cmerge.Merger{},
|
||||
cf: cformat.NewFormatter(cmode),
|
||||
cmode: cmode,
|
||||
}
|
||||
// Generate the expected hash string based on the final meta-data
|
||||
// hash for this test, then look only for pods that refer to that
|
||||
// hash (just in case there are multiple instrumented executables
|
||||
// in play). See issue #57924 for more on this.
|
||||
hashstring := fmt.Sprintf("%x", finalHash)
|
||||
importpaths := make(map[string]struct{})
|
||||
for _, p := range podlist {
|
||||
if !strings.Contains(p.MetaFile, hashstring) {
|
||||
continue
|
||||
}
|
||||
if err := ts.processPod(p, importpaths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
metafilespath := filepath.Join(dir, coverage.MetaFilesFileName)
|
||||
if _, err := os.Stat(metafilespath); err == nil {
|
||||
if err := ts.readAuxMetaFiles(metafilespath, importpaths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Emit percent.
|
||||
if err := ts.cf.EmitPercent(w, cpkg, true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Emit text output.
|
||||
if tf != nil {
|
||||
if err := ts.cf.EmitTextual(tf); err != nil {
|
||||
return err
|
||||
}
|
||||
tfClosed = true
|
||||
if err := tf.Close(); err != nil {
|
||||
return fmt.Errorf("closing %s: %v", cfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tstate struct {
|
||||
calloc.BatchCounterAlloc
|
||||
cm *cmerge.Merger
|
||||
cf *cformat.Formatter
|
||||
cmode coverage.CounterMode
|
||||
}
|
||||
|
||||
// processPod reads coverage counter data for a specific pod.
|
||||
func (ts *tstate) processPod(p pods.Pod, importpaths map[string]struct{}) error {
|
||||
// Open meta-data file
|
||||
f, err := os.Open(p.MetaFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open meta-data file %s: %v", p.MetaFile, err)
|
||||
}
|
||||
defer func() {
|
||||
f.Close()
|
||||
}()
|
||||
var mfr *decodemeta.CoverageMetaFileReader
|
||||
mfr, err = decodemeta.NewCoverageMetaFileReader(f, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading meta-data file %s: %v", p.MetaFile, err)
|
||||
}
|
||||
newmode := mfr.CounterMode()
|
||||
if newmode != ts.cmode {
|
||||
return fmt.Errorf("internal error: counter mode clash: %q from test harness, %q from data file %s", ts.cmode.String(), newmode.String(), p.MetaFile)
|
||||
}
|
||||
newgran := mfr.CounterGranularity()
|
||||
if err := ts.cm.SetModeAndGranularity(p.MetaFile, cmode, newgran); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A map to store counter data, indexed by pkgid/fnid tuple.
|
||||
pmm := make(map[pkfunc][]uint32)
|
||||
|
||||
// Helper to read a single counter data file.
|
||||
readcdf := func(cdf string) error {
|
||||
cf, err := os.Open(cdf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening counter data file %s: %s", cdf, err)
|
||||
}
|
||||
defer cf.Close()
|
||||
var cdr *decodecounter.CounterDataReader
|
||||
cdr, err = decodecounter.NewCounterDataReader(cdf, cf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading counter data file %s: %s", cdf, err)
|
||||
}
|
||||
var data decodecounter.FuncPayload
|
||||
for {
|
||||
ok, err := cdr.NextFunc(&data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading counter data file %s: %v", cdf, err)
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// NB: sanity check on pkg and func IDs?
|
||||
key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx}
|
||||
if prev, found := pmm[key]; found {
|
||||
// Note: no overflow reporting here.
|
||||
if err, _ := ts.cm.MergeCounters(data.Counters, prev); err != nil {
|
||||
return fmt.Errorf("processing counter data file %s: %v", cdf, err)
|
||||
}
|
||||
}
|
||||
c := ts.AllocateCounters(len(data.Counters))
|
||||
copy(c, data.Counters)
|
||||
pmm[key] = c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read counter data files.
|
||||
for _, cdf := range p.CounterDataFiles {
|
||||
if err := readcdf(cdf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Visit meta-data file.
|
||||
np := uint32(mfr.NumPackages())
|
||||
payload := []byte{}
|
||||
for pkIdx := uint32(0); pkIdx < np; pkIdx++ {
|
||||
var pd *decodemeta.CoverageMetaDataDecoder
|
||||
pd, payload, err = mfr.GetPackageDecoder(pkIdx, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading pkg %d from meta-file %s: %s", pkIdx, p.MetaFile, err)
|
||||
}
|
||||
ts.cf.SetPackage(pd.PackagePath())
|
||||
importpaths[pd.PackagePath()] = struct{}{}
|
||||
var fd coverage.FuncDesc
|
||||
nf := pd.NumFuncs()
|
||||
for fnIdx := uint32(0); fnIdx < nf; fnIdx++ {
|
||||
if err := pd.ReadFunc(fnIdx, &fd); err != nil {
|
||||
return fmt.Errorf("reading meta-data file %s: %v",
|
||||
p.MetaFile, err)
|
||||
}
|
||||
key := pkfunc{pk: pkIdx, fcn: fnIdx}
|
||||
counters, haveCounters := pmm[key]
|
||||
for i := 0; i < len(fd.Units); i++ {
|
||||
u := fd.Units[i]
|
||||
// Skip units with non-zero parent (no way to represent
|
||||
// these in the existing format).
|
||||
if u.Parent != 0 {
|
||||
continue
|
||||
}
|
||||
count := uint32(0)
|
||||
if haveCounters {
|
||||
count = counters[i]
|
||||
}
|
||||
ts.cf.AddUnit(fd.Srcfile, fd.Funcname, fd.Lit, u, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type pkfunc struct {
|
||||
pk, fcn uint32
|
||||
}
|
||||
|
||||
func (ts *tstate) readAuxMetaFiles(metafiles string, importpaths map[string]struct{}) error {
|
||||
// Unmarshal the information on available aux metafiles into
|
||||
// a MetaFileCollection struct.
|
||||
var mfc coverage.MetaFileCollection
|
||||
data, err := os.ReadFile(metafiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading auxmetafiles file %q: %v", metafiles, err)
|
||||
}
|
||||
if err := json.Unmarshal(data, &mfc); err != nil {
|
||||
return fmt.Errorf("error reading auxmetafiles file %q: %v", metafiles, err)
|
||||
}
|
||||
|
||||
// Walk through each available aux meta-file. If we've already
|
||||
// seen the package path in question during the walk of the
|
||||
// "regular" meta-data file, then we can skip the package,
|
||||
// otherwise construct a dummy pod with the single meta-data file
|
||||
// (no counters) and invoke processPod on it.
|
||||
for i := range mfc.ImportPaths {
|
||||
p := mfc.ImportPaths[i]
|
||||
if _, ok := importpaths[p]; ok {
|
||||
continue
|
||||
}
|
||||
var pod pods.Pod
|
||||
pod.MetaFile = mfc.MetaFileFragments[i]
|
||||
if err := ts.processPod(pod, importpaths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot returns a snapshot of coverage percentage at a moment of
|
||||
// time within a running test, so as to support the testing.Coverage()
|
||||
// function. This version doesn't examine coverage meta-data, so the
|
||||
// result it returns will be less accurate (more "slop") due to the
|
||||
// fact that we don't look at the meta data to see how many statements
|
||||
// are associated with each counter.
|
||||
func Snapshot() float64 {
|
||||
cl := getCovCounterList()
|
||||
if len(cl) == 0 {
|
||||
// no work to do here.
|
||||
return 0.0
|
||||
}
|
||||
|
||||
tot := uint64(0)
|
||||
totExec := uint64(0)
|
||||
for _, c := range cl {
|
||||
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), c.Len)
|
||||
tot += uint64(len(sd))
|
||||
for i := 0; i < len(sd); i++ {
|
||||
// Skip ahead until the next non-zero value.
|
||||
if sd[i].Load() == 0 {
|
||||
continue
|
||||
}
|
||||
// We found a function that was executed.
|
||||
nCtrs := sd[i+coverage.NumCtrsOffset].Load()
|
||||
cst := i + coverage.FirstCtrOffset
|
||||
|
||||
if cst+int(nCtrs) > len(sd) {
|
||||
break
|
||||
}
|
||||
counters := sd[cst : cst+int(nCtrs)]
|
||||
for i := range counters {
|
||||
if counters[i].Load() != 0 {
|
||||
totExec++
|
||||
}
|
||||
}
|
||||
i += coverage.FirstCtrOffset + int(nCtrs) - 1
|
||||
}
|
||||
}
|
||||
if tot == 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float64(totExec) / float64(tot)
|
||||
}
|
||||
207
src/internal/coverage/cfile/ts_test.go
Normal file
207
src/internal/coverage/cfile/ts_test.go
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"internal/coverage"
|
||||
"internal/goexperiment"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname testing_testGoCoverDir testing.testGoCoverDir
|
||||
func testing_testGoCoverDir() string
|
||||
|
||||
func testGoCoverDir(t *testing.T) string {
|
||||
tgcd := testing_testGoCoverDir()
|
||||
if tgcd != "" {
|
||||
return tgcd
|
||||
}
|
||||
return t.TempDir()
|
||||
}
|
||||
|
||||
// TestTestSupport does a basic verification of the functionality in
|
||||
// ProcessCoverTestDir (doing this here as opposed to
|
||||
// relying on other test paths will provide a better signal when
|
||||
// running "go test -cover" for this package).
|
||||
func TestTestSupport(t *testing.T) {
|
||||
if !goexperiment.CoverageRedesign {
|
||||
return
|
||||
}
|
||||
if testing.CoverMode() == "" {
|
||||
return
|
||||
}
|
||||
tgcd := testGoCoverDir(t)
|
||||
t.Logf("testing.testGoCoverDir() returns %s mode=%s\n",
|
||||
tgcd, testing.CoverMode())
|
||||
|
||||
textfile := filepath.Join(t.TempDir(), "file.txt")
|
||||
var sb strings.Builder
|
||||
err := ProcessCoverTestDir(tgcd, textfile,
|
||||
testing.CoverMode(), "", &sb)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
|
||||
// Check for existence of text file.
|
||||
if inf, err := os.Open(textfile); err != nil {
|
||||
t.Fatalf("problems opening text file %s: %v", textfile, err)
|
||||
} else {
|
||||
inf.Close()
|
||||
}
|
||||
|
||||
// Check for percent output with expected tokens.
|
||||
strout := sb.String()
|
||||
want := "of statements"
|
||||
if !strings.Contains(strout, want) {
|
||||
t.Logf("output from run: %s\n", strout)
|
||||
t.Fatalf("percent output missing token: %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
var funcInvoked bool
|
||||
|
||||
//go:noinline
|
||||
func thisFunctionOnlyCalledFromSnapshotTest(n int) int {
|
||||
if funcInvoked {
|
||||
panic("bad")
|
||||
}
|
||||
funcInvoked = true
|
||||
|
||||
// Contents here not especially important, just so long as we
|
||||
// have some statements.
|
||||
t := 0
|
||||
for i := 0; i < n; i++ {
|
||||
for j := 0; j < i; j++ {
|
||||
t += i ^ j
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Tests runtime/coverage.snapshot() directly. Note that if
|
||||
// coverage is not enabled, the hook is designed to just return
|
||||
// zero.
|
||||
func TestCoverageSnapshot(t *testing.T) {
|
||||
C1 := Snapshot()
|
||||
thisFunctionOnlyCalledFromSnapshotTest(15)
|
||||
C2 := Snapshot()
|
||||
cond := "C1 > C2"
|
||||
val := C1 > C2
|
||||
if testing.CoverMode() != "" {
|
||||
cond = "C1 >= C2"
|
||||
val = C1 >= C2
|
||||
}
|
||||
t.Logf("%f %f\n", C1, C2)
|
||||
if val {
|
||||
t.Errorf("erroneous snapshots, %s = true C1=%f C2=%f",
|
||||
cond, C1, C2)
|
||||
}
|
||||
}
|
||||
|
||||
const hellogo = `
|
||||
package main
|
||||
|
||||
func main() {
|
||||
println("hello")
|
||||
}
|
||||
`
|
||||
|
||||
// Returns a pair F,T where F is a meta-data file generated from
|
||||
// "hello.go" above, and T is a token to look for that should be
|
||||
// present in the coverage report from F.
|
||||
func genAuxMeta(t *testing.T, dstdir string) (string, string) {
|
||||
// Do a GOCOVERDIR=<tmp> go run hello.go
|
||||
src := filepath.Join(dstdir, "hello.go")
|
||||
if err := os.WriteFile(src, []byte(hellogo), 0777); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
args := []string{"run", "-covermode=" + testing.CoverMode(), src}
|
||||
cmd := exec.Command(testenv.GoToolPath(t), args...)
|
||||
cmd.Env = updateGoCoverDir(os.Environ(), dstdir, true)
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go run failed (%v): %s", err, b)
|
||||
}
|
||||
|
||||
// Pick out the generated meta-data file.
|
||||
files, err := os.ReadDir(dstdir)
|
||||
if err != nil {
|
||||
t.Fatalf("reading %s: %v", dstdir, err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), "covmeta") {
|
||||
return filepath.Join(dstdir, f.Name()), "hello.go:"
|
||||
}
|
||||
}
|
||||
t.Fatalf("could not locate generated meta-data file")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func TestAuxMetaDataFiles(t *testing.T) {
|
||||
if !goexperiment.CoverageRedesign {
|
||||
return
|
||||
}
|
||||
if testing.CoverMode() == "" {
|
||||
return
|
||||
}
|
||||
testenv.MustHaveGoRun(t)
|
||||
tgcd := testGoCoverDir(t)
|
||||
t.Logf("testing.testGoCoverDir() returns %s mode=%s\n",
|
||||
tgcd, testing.CoverMode())
|
||||
|
||||
td := t.TempDir()
|
||||
|
||||
// Manufacture a new, separate meta-data file not related to this
|
||||
// test. Contents are not important, just so long as the
|
||||
// packages/paths are different.
|
||||
othermetadir := filepath.Join(td, "othermeta")
|
||||
if err := os.Mkdir(othermetadir, 0777); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
mfile, token := genAuxMeta(t, othermetadir)
|
||||
|
||||
// Write a metafiles file.
|
||||
metafiles := filepath.Join(tgcd, coverage.MetaFilesFileName)
|
||||
mfc := coverage.MetaFileCollection{
|
||||
ImportPaths: []string{"command-line-arguments"},
|
||||
MetaFileFragments: []string{mfile},
|
||||
}
|
||||
jdata, err := json.Marshal(mfc)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal MetaFileCollection: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(metafiles, jdata, 0666); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
|
||||
// Kick off guts of test.
|
||||
var sb strings.Builder
|
||||
textfile := filepath.Join(td, "file2.txt")
|
||||
err = ProcessCoverTestDir(tgcd, textfile,
|
||||
testing.CoverMode(), "", &sb)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
if err = os.Remove(metafiles); err != nil {
|
||||
t.Fatalf("removing metafiles file: %v", err)
|
||||
}
|
||||
|
||||
// Look for the expected things in the coverage profile.
|
||||
contents, err := os.ReadFile(textfile)
|
||||
strc := string(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("problems reading text file %s: %v", textfile, err)
|
||||
}
|
||||
if !strings.Contains(strc, token) {
|
||||
t.Logf("content: %s\n", string(contents))
|
||||
t.Fatalf("cov profile does not contain aux meta content %q", token)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue