diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
commit | f6ad4dcef54c5ce997a4bad5a6d86de229015700 (patch) | |
tree | 7cfa4e31ace5c2bd95c72b154d15af494b2bcbef /src/internal/coverage | |
parent | Initial commit. (diff) | |
download | golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.tar.xz golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.zip |
Adding upstream version 1.22.1.upstream/1.22.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
24 files changed, 4158 insertions, 0 deletions
diff --git a/src/internal/coverage/calloc/batchcounteralloc.go b/src/internal/coverage/calloc/batchcounteralloc.go new file mode 100644 index 0000000..2b6495d --- /dev/null +++ b/src/internal/coverage/calloc/batchcounteralloc.go @@ -0,0 +1,29 @@ +// 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 calloc + +// This package contains a simple "batch" allocator for allocating +// coverage counters (slices of uint32 basically), for working with +// coverage data files. Collections of counter arrays tend to all be +// live/dead over the same time period, so a good fit for batch +// allocation. + +type BatchCounterAlloc struct { + pool []uint32 +} + +func (ca *BatchCounterAlloc) AllocateCounters(n int) []uint32 { + const chunk = 8192 + if n > cap(ca.pool) { + siz := chunk + if n > chunk { + siz = n + } + ca.pool = make([]uint32, siz) + } + rv := ca.pool[:n] + ca.pool = ca.pool[n:] + return rv +} diff --git a/src/internal/coverage/cformat/fmt_test.go b/src/internal/coverage/cformat/fmt_test.go new file mode 100644 index 0000000..f5ed01b --- /dev/null +++ b/src/internal/coverage/cformat/fmt_test.go @@ -0,0 +1,155 @@ +// 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 cformat_test + +import ( + "internal/coverage" + "internal/coverage/cformat" + "slices" + "strings" + "testing" +) + +func TestBasics(t *testing.T) { + fm := cformat.NewFormatter(coverage.CtrModeAtomic) + + mku := func(stl, enl, nx uint32) coverage.CoverableUnit { + return coverage.CoverableUnit{ + StLine: stl, + EnLine: enl, + NxStmts: nx, + } + } + fn1units := []coverage.CoverableUnit{ + mku(10, 11, 2), + mku(15, 11, 1), + } + fn2units := []coverage.CoverableUnit{ + mku(20, 25, 3), + mku(30, 31, 2), + mku(33, 40, 7), + } + fn3units := []coverage.CoverableUnit{ + mku(99, 100, 1), + } + fm.SetPackage("my/pack1") + for k, u := range fn1units { + fm.AddUnit("p.go", "f1", false, u, uint32(k)) + } + for k, u := range fn2units { + fm.AddUnit("q.go", "f2", false, u, 0) + fm.AddUnit("q.go", "f2", false, u, uint32(k)) + } + fm.SetPackage("my/pack2") + for _, u := range fn3units { + fm.AddUnit("lit.go", "f3", true, u, 0) + } + + var b1, b2, b3, b4 strings.Builder + if err := fm.EmitTextual(&b1); err != nil { + t.Fatalf("EmitTextual returned %v", err) + } + wantText := strings.TrimSpace(` +mode: atomic +p.go:10.0,11.0 2 0 +p.go:15.0,11.0 1 1 +q.go:20.0,25.0 3 0 +q.go:30.0,31.0 2 1 +q.go:33.0,40.0 7 2 +lit.go:99.0,100.0 1 0`) + gotText := strings.TrimSpace(b1.String()) + if wantText != gotText { + t.Errorf("emit text: got:\n%s\nwant:\n%s\n", gotText, wantText) + } + + // Percent output with no aggregation. + noCoverPkg := "" + if err := fm.EmitPercent(&b2, noCoverPkg, false, false); err != nil { + t.Fatalf("EmitPercent returned %v", err) + } + wantPercent := strings.Fields(` + my/pack1 coverage: 66.7% of statements + my/pack2 coverage: 0.0% of statements +`) + gotPercent := strings.Fields(b2.String()) + if !slices.Equal(wantPercent, gotPercent) { + t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n", + gotPercent, wantPercent) + } + + // Percent mode with aggregation. + withCoverPkg := " in ./..." + if err := fm.EmitPercent(&b3, withCoverPkg, false, true); err != nil { + t.Fatalf("EmitPercent returned %v", err) + } + wantPercent = strings.Fields(` + coverage: 62.5% of statements in ./... +`) + gotPercent = strings.Fields(b3.String()) + if !slices.Equal(wantPercent, gotPercent) { + t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n", + gotPercent, wantPercent) + } + + if err := fm.EmitFuncs(&b4); err != nil { + t.Fatalf("EmitFuncs returned %v", err) + } + wantFuncs := strings.TrimSpace(` +p.go:10: f1 33.3% +q.go:20: f2 75.0% +total (statements) 62.5%`) + gotFuncs := strings.TrimSpace(b4.String()) + if wantFuncs != gotFuncs { + t.Errorf("emit funcs: got:\n%s\nwant:\n%s\n", gotFuncs, wantFuncs) + } + if false { + t.Logf("text is %s\n", b1.String()) + t.Logf("perc is %s\n", b2.String()) + t.Logf("perc2 is %s\n", b3.String()) + t.Logf("funcs is %s\n", b4.String()) + } +} + +func TestEmptyPackages(t *testing.T) { + + fm := cformat.NewFormatter(coverage.CtrModeAtomic) + fm.SetPackage("my/pack1") + fm.SetPackage("my/pack2") + + // No aggregation. + { + var b strings.Builder + noCoverPkg := "" + if err := fm.EmitPercent(&b, noCoverPkg, true, false); err != nil { + t.Fatalf("EmitPercent returned %v", err) + } + wantPercent := strings.Fields(` + my/pack1 coverage: [no statements] + my/pack2 coverage: [no statements] +`) + gotPercent := strings.Fields(b.String()) + if !slices.Equal(wantPercent, gotPercent) { + t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n", + gotPercent, wantPercent) + } + } + + // With aggregation. + { + var b strings.Builder + noCoverPkg := "" + if err := fm.EmitPercent(&b, noCoverPkg, true, true); err != nil { + t.Fatalf("EmitPercent returned %v", err) + } + wantPercent := strings.Fields(` + coverage: [no statements] +`) + gotPercent := strings.Fields(b.String()) + if !slices.Equal(wantPercent, gotPercent) { + t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n", + gotPercent, wantPercent) + } + } +} diff --git a/src/internal/coverage/cformat/format.go b/src/internal/coverage/cformat/format.go new file mode 100644 index 0000000..7e7a277 --- /dev/null +++ b/src/internal/coverage/cformat/format.go @@ -0,0 +1,352 @@ +// 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 cformat + +// This package provides apis for producing human-readable summaries +// of coverage data (e.g. a coverage percentage for a given package or +// set of packages) and for writing data in the legacy test format +// emitted by "go test -coverprofile=<outfile>". +// +// The model for using these apis is to create a Formatter object, +// then make a series of calls to SetPackage and AddUnit passing in +// data read from coverage meta-data and counter-data files. E.g. +// +// myformatter := cformat.NewFormatter() +// ... +// for each package P in meta-data file: { +// myformatter.SetPackage(P) +// for each function F in P: { +// for each coverable unit U in F: { +// myformatter.AddUnit(U) +// } +// } +// } +// myformatter.EmitPercent(os.Stdout, "", true, true) +// myformatter.EmitTextual(somefile) +// +// These apis are linked into tests that are built with "-cover", and +// called at the end of test execution to produce text output or +// emit coverage percentages. + +import ( + "fmt" + "internal/coverage" + "internal/coverage/cmerge" + "io" + "sort" + "text/tabwriter" +) + +type Formatter struct { + // Maps import path to package state. + pm map[string]*pstate + // Records current package being visited. + pkg string + // Pointer to current package state. + p *pstate + // Counter mode. + cm coverage.CounterMode +} + +// pstate records package-level coverage data state: +// - a table of functions (file/fname/literal) +// - a map recording the index/ID of each func encountered so far +// - a table storing execution count for the coverable units in each func +type pstate struct { + // slice of unique functions + funcs []fnfile + // maps function to index in slice above (index acts as function ID) + funcTable map[fnfile]uint32 + + // A table storing coverage counts for each coverable unit. + unitTable map[extcu]uint32 +} + +// extcu encapsulates a coverable unit within some function. +type extcu struct { + fnfid uint32 // index into p.funcs slice + coverage.CoverableUnit +} + +// fnfile is a function-name/file-name tuple. +type fnfile struct { + file string + fname string + lit bool +} + +func NewFormatter(cm coverage.CounterMode) *Formatter { + return &Formatter{ + pm: make(map[string]*pstate), + cm: cm, + } +} + +// SetPackage tells the formatter that we're about to visit the +// coverage data for the package with the specified import path. +// Note that it's OK to call SetPackage more than once with the +// same import path; counter data values will be accumulated. +func (fm *Formatter) SetPackage(importpath string) { + if importpath == fm.pkg { + return + } + fm.pkg = importpath + ps, ok := fm.pm[importpath] + if !ok { + ps = new(pstate) + fm.pm[importpath] = ps + ps.unitTable = make(map[extcu]uint32) + ps.funcTable = make(map[fnfile]uint32) + } + fm.p = ps +} + +// AddUnit passes info on a single coverable unit (file, funcname, +// literal flag, range of lines, and counter value) to the formatter. +// Counter values will be accumulated where appropriate. +func (fm *Formatter) AddUnit(file string, fname string, isfnlit bool, unit coverage.CoverableUnit, count uint32) { + if fm.p == nil { + panic("AddUnit invoked before SetPackage") + } + fkey := fnfile{file: file, fname: fname, lit: isfnlit} + idx, ok := fm.p.funcTable[fkey] + if !ok { + idx = uint32(len(fm.p.funcs)) + fm.p.funcs = append(fm.p.funcs, fkey) + fm.p.funcTable[fkey] = idx + } + ukey := extcu{fnfid: idx, CoverableUnit: unit} + pcount := fm.p.unitTable[ukey] + var result uint32 + if fm.cm == coverage.CtrModeSet { + if count != 0 || pcount != 0 { + result = 1 + } + } else { + // Use saturating arithmetic. + result, _ = cmerge.SaturatingAdd(pcount, count) + } + fm.p.unitTable[ukey] = result +} + +// sortUnits sorts a slice of extcu objects in a package according to +// source position information (e.g. file and line). Note that we don't +// include function name as part of the sorting criteria, the thinking +// being that is better to provide things in the original source order. +func (p *pstate) sortUnits(units []extcu) { + sort.Slice(units, func(i, j int) bool { + ui := units[i] + uj := units[j] + ifile := p.funcs[ui.fnfid].file + jfile := p.funcs[uj.fnfid].file + if ifile != jfile { + return ifile < jfile + } + // NB: not taking function literal flag into account here (no + // need, since other fields are guaranteed to be distinct). + if units[i].StLine != units[j].StLine { + return units[i].StLine < units[j].StLine + } + if units[i].EnLine != units[j].EnLine { + return units[i].EnLine < units[j].EnLine + } + if units[i].StCol != units[j].StCol { + return units[i].StCol < units[j].StCol + } + if units[i].EnCol != units[j].EnCol { + return units[i].EnCol < units[j].EnCol + } + return units[i].NxStmts < units[j].NxStmts + }) +} + +// EmitTextual writes the accumulated coverage data in the legacy +// cmd/cover text format to the writer 'w'. We sort the data items by +// importpath, source file, and line number before emitting (this sorting +// is not explicitly mandated by the format, but seems like a good idea +// for repeatable/deterministic dumps). +func (fm *Formatter) EmitTextual(w io.Writer) error { + if fm.cm == coverage.CtrModeInvalid { + panic("internal error, counter mode unset") + } + if _, err := fmt.Fprintf(w, "mode: %s\n", fm.cm.String()); err != nil { + return err + } + pkgs := make([]string, 0, len(fm.pm)) + for importpath := range fm.pm { + pkgs = append(pkgs, importpath) + } + sort.Strings(pkgs) + for _, importpath := range pkgs { + p := fm.pm[importpath] + units := make([]extcu, 0, len(p.unitTable)) + for u := range p.unitTable { + units = append(units, u) + } + p.sortUnits(units) + for _, u := range units { + count := p.unitTable[u] + file := p.funcs[u.fnfid].file + if _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", + file, u.StLine, u.StCol, + u.EnLine, u.EnCol, u.NxStmts, count); err != nil { + return err + } + } + } + return nil +} + +// EmitPercent writes out a "percentage covered" string to the writer 'w'. +func (fm *Formatter) EmitPercent(w io.Writer, covpkgs string, noteEmpty bool, aggregate bool) error { + pkgs := make([]string, 0, len(fm.pm)) + for importpath := range fm.pm { + pkgs = append(pkgs, importpath) + } + + rep := func(cov, tot uint64) error { + if tot != 0 { + if _, err := fmt.Fprintf(w, "coverage: %.1f%% of statements%s\n", + 100.0*float64(cov)/float64(tot), covpkgs); err != nil { + return err + } + } else if noteEmpty { + if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil { + return err + } + } + return nil + } + + sort.Strings(pkgs) + var totalStmts, coveredStmts uint64 + for _, importpath := range pkgs { + p := fm.pm[importpath] + if !aggregate { + totalStmts, coveredStmts = 0, 0 + } + for unit, count := range p.unitTable { + nx := uint64(unit.NxStmts) + totalStmts += nx + if count != 0 { + coveredStmts += nx + } + } + if !aggregate { + if _, err := fmt.Fprintf(w, "\t%s\t\t", importpath); err != nil { + return err + } + if err := rep(coveredStmts, totalStmts); err != nil { + return err + } + } + } + if aggregate { + if err := rep(coveredStmts, totalStmts); err != nil { + return err + } + } + + return nil +} + +// EmitFuncs writes out a function-level summary to the writer 'w'. A +// note on handling function literals: although we collect coverage +// data for unnamed literals, it probably does not make sense to +// include them in the function summary since there isn't any good way +// to name them (this is also consistent with the legacy cmd/cover +// implementation). We do want to include their counts in the overall +// summary however. +func (fm *Formatter) EmitFuncs(w io.Writer) error { + if fm.cm == coverage.CtrModeInvalid { + panic("internal error, counter mode unset") + } + perc := func(covered, total uint64) float64 { + if total == 0 { + total = 1 + } + return 100.0 * float64(covered) / float64(total) + } + tabber := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) + defer tabber.Flush() + allStmts := uint64(0) + covStmts := uint64(0) + + pkgs := make([]string, 0, len(fm.pm)) + for importpath := range fm.pm { + pkgs = append(pkgs, importpath) + } + sort.Strings(pkgs) + + // Emit functions for each package, sorted by import path. + for _, importpath := range pkgs { + p := fm.pm[importpath] + if len(p.unitTable) == 0 { + continue + } + units := make([]extcu, 0, len(p.unitTable)) + for u := range p.unitTable { + units = append(units, u) + } + + // Within a package, sort the units, then walk through the + // sorted array. Each time we hit a new function, emit the + // summary entry for the previous function, then make one last + // emit call at the end of the loop. + p.sortUnits(units) + fname := "" + ffile := "" + flit := false + var fline uint32 + var cstmts, tstmts uint64 + captureFuncStart := func(u extcu) { + fname = p.funcs[u.fnfid].fname + ffile = p.funcs[u.fnfid].file + flit = p.funcs[u.fnfid].lit + fline = u.StLine + } + emitFunc := func(u extcu) error { + // Don't emit entries for function literals (see discussion + // in function header comment above). + if !flit { + if _, err := fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", + ffile, fline, fname, perc(cstmts, tstmts)); err != nil { + return err + } + } + captureFuncStart(u) + allStmts += tstmts + covStmts += cstmts + tstmts = 0 + cstmts = 0 + return nil + } + for k, u := range units { + if k == 0 { + captureFuncStart(u) + } else { + if fname != p.funcs[u.fnfid].fname { + // New function; emit entry for previous one. + if err := emitFunc(u); err != nil { + return err + } + } + } + tstmts += uint64(u.NxStmts) + count := p.unitTable[u] + if count != 0 { + cstmts += uint64(u.NxStmts) + } + } + if err := emitFunc(extcu{}); err != nil { + return err + } + } + if _, err := fmt.Fprintf(tabber, "%s\t%s\t%.1f%%\n", + "total", "(statements)", perc(covStmts, allStmts)); err != nil { + return err + } + return nil +} diff --git a/src/internal/coverage/cmerge/merge.go b/src/internal/coverage/cmerge/merge.go new file mode 100644 index 0000000..1339803 --- /dev/null +++ b/src/internal/coverage/cmerge/merge.go @@ -0,0 +1,127 @@ +// 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 cmerge + +// package cmerge provides a few small utility APIs for helping +// with merging of counter data for a given function. + +import ( + "fmt" + "internal/coverage" + "math" +) + +type ModeMergePolicy uint8 + +const ( + ModeMergeStrict ModeMergePolicy = iota + ModeMergeRelaxed +) + +// Merger provides state and methods to help manage the process of +// merging together coverage counter data for a given function, for +// tools that need to implicitly merge counter as they read multiple +// coverage counter data files. +type Merger struct { + cmode coverage.CounterMode + cgran coverage.CounterGranularity + policy ModeMergePolicy + overflow bool +} + +func (cm *Merger) SetModeMergePolicy(policy ModeMergePolicy) { + cm.policy = policy +} + +// MergeCounters takes the counter values in 'src' and merges them +// into 'dst' according to the correct counter mode. +func (m *Merger) MergeCounters(dst, src []uint32) (error, bool) { + if len(src) != len(dst) { + return fmt.Errorf("merging counters: len(dst)=%d len(src)=%d", len(dst), len(src)), false + } + if m.cmode == coverage.CtrModeSet { + for i := 0; i < len(src); i++ { + if src[i] != 0 { + dst[i] = 1 + } + } + } else { + for i := 0; i < len(src); i++ { + dst[i] = m.SaturatingAdd(dst[i], src[i]) + } + } + ovf := m.overflow + m.overflow = false + return nil, ovf +} + +// Saturating add does a saturating addition of 'dst' and 'src', +// returning added value or math.MaxUint32 if there is an overflow. +// Overflows are recorded in case the client needs to track them. +func (m *Merger) SaturatingAdd(dst, src uint32) uint32 { + result, overflow := SaturatingAdd(dst, src) + if overflow { + m.overflow = true + } + return result +} + +// Saturating add does a saturating addition of 'dst' and 'src', +// returning added value or math.MaxUint32 plus an overflow flag. +func SaturatingAdd(dst, src uint32) (uint32, bool) { + d, s := uint64(dst), uint64(src) + sum := d + s + overflow := false + if uint64(uint32(sum)) != sum { + overflow = true + sum = math.MaxUint32 + } + return uint32(sum), overflow +} + +// SetModeAndGranularity records the counter mode and granularity for +// the current merge. In the specific case of merging across coverage +// data files from different binaries, where we're combining data from +// more than one meta-data file, we need to check for and resolve +// mode/granularity clashes. +func (cm *Merger) SetModeAndGranularity(mdf string, cmode coverage.CounterMode, cgran coverage.CounterGranularity) error { + if cm.cmode == coverage.CtrModeInvalid { + // Set merger mode based on what we're seeing here. + cm.cmode = cmode + cm.cgran = cgran + } else { + // Granularity clashes are always errors. + if cm.cgran != cgran { + return fmt.Errorf("counter granularity clash while reading meta-data file %s: previous file had %s, new file has %s", mdf, cm.cgran.String(), cgran.String()) + } + // Mode clashes are treated as errors if we're using the + // default strict policy. + if cm.cmode != cmode { + if cm.policy == ModeMergeStrict { + return fmt.Errorf("counter mode clash while reading meta-data file %s: previous file had %s, new file has %s", mdf, cm.cmode.String(), cmode.String()) + } + // In the case of a relaxed mode merge policy, upgrade + // mode if needed. + if cm.cmode < cmode { + cm.cmode = cmode + } + } + } + return nil +} + +func (cm *Merger) ResetModeAndGranularity() { + cm.cmode = coverage.CtrModeInvalid + cm.cgran = coverage.CtrGranularityInvalid + cm.overflow = false +} + +func (cm *Merger) Mode() coverage.CounterMode { + return cm.cmode +} + +func (cm *Merger) Granularity() coverage.CounterGranularity { + return cm.cgran +} diff --git a/src/internal/coverage/cmerge/merge_test.go b/src/internal/coverage/cmerge/merge_test.go new file mode 100644 index 0000000..0e6112a --- /dev/null +++ b/src/internal/coverage/cmerge/merge_test.go @@ -0,0 +1,118 @@ +// 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 cmerge_test + +import ( + "fmt" + "internal/coverage" + "internal/coverage/cmerge" + "testing" +) + +func TestClash(t *testing.T) { + m := &cmerge.Merger{} + err := m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerBlock) + if err != nil { + t.Fatalf("unexpected clash: %v", err) + } + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerBlock) + if err != nil { + t.Fatalf("unexpected clash: %v", err) + } + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeCount, coverage.CtrGranularityPerBlock) + if err == nil { + t.Fatalf("expected mode clash, not found") + } + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerFunc) + if err == nil { + t.Fatalf("expected granularity clash, not found") + } + m.SetModeMergePolicy(cmerge.ModeMergeRelaxed) + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeCount, coverage.CtrGranularityPerBlock) + if err != nil { + t.Fatalf("unexpected clash: %v", err) + } + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerBlock) + if err != nil { + t.Fatalf("unexpected clash: %v", err) + } + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeAtomic, coverage.CtrGranularityPerBlock) + if err != nil { + t.Fatalf("unexpected clash: %v", err) + } + m.ResetModeAndGranularity() + err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeCount, coverage.CtrGranularityPerFunc) + if err != nil { + t.Fatalf("unexpected clash after reset: %v", err) + } +} + +func TestBasic(t *testing.T) { + scenarios := []struct { + cmode coverage.CounterMode + cgran coverage.CounterGranularity + src, dst, res []uint32 + iters int + merr bool + overflow bool + }{ + { + cmode: coverage.CtrModeSet, + cgran: coverage.CtrGranularityPerBlock, + src: []uint32{1, 0, 1}, + dst: []uint32{1, 1, 0}, + res: []uint32{1, 1, 1}, + iters: 2, + overflow: false, + }, + { + cmode: coverage.CtrModeCount, + cgran: coverage.CtrGranularityPerBlock, + src: []uint32{1, 0, 3}, + dst: []uint32{5, 7, 0}, + res: []uint32{6, 7, 3}, + iters: 1, + overflow: false, + }, + { + cmode: coverage.CtrModeCount, + cgran: coverage.CtrGranularityPerBlock, + src: []uint32{4294967200, 0, 3}, + dst: []uint32{4294967001, 7, 0}, + res: []uint32{4294967295, 7, 3}, + iters: 1, + overflow: true, + }, + } + + for k, scenario := range scenarios { + var err error + var ovf bool + m := &cmerge.Merger{} + mdf := fmt.Sprintf("file%d", k) + err = m.SetModeAndGranularity(mdf, scenario.cmode, scenario.cgran) + if err != nil { + t.Fatalf("case %d SetModeAndGranularity failed: %v", k, err) + } + for i := 0; i < scenario.iters; i++ { + err, ovf = m.MergeCounters(scenario.dst, scenario.src) + if ovf != scenario.overflow { + t.Fatalf("case %d overflow mismatch: got %v want %v", k, ovf, scenario.overflow) + } + if !scenario.merr && err != nil { + t.Fatalf("case %d unexpected err %v", k, err) + } + if scenario.merr && err == nil { + t.Fatalf("case %d expected err, not received", k) + } + for i := range scenario.dst { + if scenario.dst[i] != scenario.res[i] { + t.Fatalf("case %d: bad merge at %d got %d want %d", + k, i, scenario.dst[i], scenario.res[i]) + } + } + } + } +} diff --git a/src/internal/coverage/decodecounter/decodecounterfile.go b/src/internal/coverage/decodecounter/decodecounterfile.go new file mode 100644 index 0000000..83934fe --- /dev/null +++ b/src/internal/coverage/decodecounter/decodecounterfile.go @@ -0,0 +1,373 @@ +// Copyright 2021 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 decodecounter + +import ( + "encoding/binary" + "fmt" + "internal/coverage" + "internal/coverage/slicereader" + "internal/coverage/stringtab" + "io" + "os" + "strconv" + "unsafe" +) + +// This file contains helpers for reading counter data files created +// during the executions of a coverage-instrumented binary. + +type CounterDataReader struct { + stab *stringtab.Reader + args map[string]string + osargs []string + goarch string // GOARCH setting from run that produced counter data + goos string // GOOS setting from run that produced counter data + mr io.ReadSeeker + hdr coverage.CounterFileHeader + ftr coverage.CounterFileFooter + shdr coverage.CounterSegmentHeader + u32b []byte + u8b []byte + fcnCount uint32 + segCount uint32 + debug bool +} + +func NewCounterDataReader(fn string, rs io.ReadSeeker) (*CounterDataReader, error) { + cdr := &CounterDataReader{ + mr: rs, + u32b: make([]byte, 4), + u8b: make([]byte, 1), + } + // Read header + if err := binary.Read(rs, binary.LittleEndian, &cdr.hdr); err != nil { + return nil, err + } + if cdr.debug { + fmt.Fprintf(os.Stderr, "=-= counter file header: %+v\n", cdr.hdr) + } + if !checkMagic(cdr.hdr.Magic) { + return nil, fmt.Errorf("invalid magic string: not a counter data file") + } + if cdr.hdr.Version > coverage.CounterFileVersion { + return nil, fmt.Errorf("version data incompatibility: reader is %d data is %d", coverage.CounterFileVersion, cdr.hdr.Version) + } + + // Read footer. + if err := cdr.readFooter(); err != nil { + return nil, err + } + // Seek back to just past the file header. + hsz := int64(unsafe.Sizeof(cdr.hdr)) + if _, err := cdr.mr.Seek(hsz, io.SeekStart); err != nil { + return nil, err + } + // Read preamble for first segment. + if err := cdr.readSegmentPreamble(); err != nil { + return nil, err + } + return cdr, nil +} + +func checkMagic(v [4]byte) bool { + g := coverage.CovCounterMagic + return v[0] == g[0] && v[1] == g[1] && v[2] == g[2] && v[3] == g[3] +} + +func (cdr *CounterDataReader) readFooter() error { + ftrSize := int64(unsafe.Sizeof(cdr.ftr)) + if _, err := cdr.mr.Seek(-ftrSize, io.SeekEnd); err != nil { + return err + } + if err := binary.Read(cdr.mr, binary.LittleEndian, &cdr.ftr); err != nil { + return err + } + if !checkMagic(cdr.ftr.Magic) { + return fmt.Errorf("invalid magic string (not a counter data file)") + } + if cdr.ftr.NumSegments == 0 { + return fmt.Errorf("invalid counter data file (no segments)") + } + return nil +} + +// readSegmentPreamble reads and consumes the segment header, segment string +// table, and segment args table. +func (cdr *CounterDataReader) readSegmentPreamble() error { + // Read segment header. + if err := binary.Read(cdr.mr, binary.LittleEndian, &cdr.shdr); err != nil { + return err + } + if cdr.debug { + fmt.Fprintf(os.Stderr, "=-= read counter segment header: %+v", cdr.shdr) + fmt.Fprintf(os.Stderr, " FcnEntries=0x%x StrTabLen=0x%x ArgsLen=0x%x\n", + cdr.shdr.FcnEntries, cdr.shdr.StrTabLen, cdr.shdr.ArgsLen) + } + + // Read string table and args. + if err := cdr.readStringTable(); err != nil { + return err + } + if err := cdr.readArgs(); err != nil { + return err + } + // Seek past any padding to bring us up to a 4-byte boundary. + if of, err := cdr.mr.Seek(0, io.SeekCurrent); err != nil { + return err + } else { + rem := of % 4 + if rem != 0 { + pad := 4 - rem + if _, err := cdr.mr.Seek(pad, io.SeekCurrent); err != nil { + return err + } + } + } + return nil +} + +func (cdr *CounterDataReader) readStringTable() error { + b := make([]byte, cdr.shdr.StrTabLen) + nr, err := cdr.mr.Read(b) + if err != nil { + return err + } + if nr != int(cdr.shdr.StrTabLen) { + return fmt.Errorf("error: short read on string table") + } + slr := slicereader.NewReader(b, false /* not readonly */) + cdr.stab = stringtab.NewReader(slr) + cdr.stab.Read() + return nil +} + +func (cdr *CounterDataReader) readArgs() error { + b := make([]byte, cdr.shdr.ArgsLen) + nr, err := cdr.mr.Read(b) + if err != nil { + return err + } + if nr != int(cdr.shdr.ArgsLen) { + return fmt.Errorf("error: short read on args table") + } + slr := slicereader.NewReader(b, false /* not readonly */) + sget := func() (string, error) { + kidx := slr.ReadULEB128() + if int(kidx) >= cdr.stab.Entries() { + return "", fmt.Errorf("malformed string table ref") + } + return cdr.stab.Get(uint32(kidx)), nil + } + nents := slr.ReadULEB128() + cdr.args = make(map[string]string, int(nents)) + for i := uint64(0); i < nents; i++ { + k, errk := sget() + if errk != nil { + return errk + } + v, errv := sget() + if errv != nil { + return errv + } + if _, ok := cdr.args[k]; ok { + return fmt.Errorf("malformed args table") + } + cdr.args[k] = v + } + if argcs, ok := cdr.args["argc"]; ok { + argc, err := strconv.Atoi(argcs) + if err != nil { + return fmt.Errorf("malformed argc in counter data file args section") + } + cdr.osargs = make([]string, 0, argc) + for i := 0; i < argc; i++ { + arg := cdr.args[fmt.Sprintf("argv%d", i)] + cdr.osargs = append(cdr.osargs, arg) + } + } + if goos, ok := cdr.args["GOOS"]; ok { + cdr.goos = goos + } + if goarch, ok := cdr.args["GOARCH"]; ok { + cdr.goarch = goarch + } + return nil +} + +// OsArgs returns the program arguments (saved from os.Args during +// the run of the instrumented binary) read from the counter +// data file. Not all coverage data files will have os.Args values; +// for example, if a data file is produced by merging coverage +// data from two distinct runs, no os args will be available (an +// empty list is returned). +func (cdr *CounterDataReader) OsArgs() []string { + return cdr.osargs +} + +// Goos returns the GOOS setting in effect for the "-cover" binary +// that produced this counter data file. The GOOS value may be +// empty in the case where the counter data file was produced +// from a merge in which more than one GOOS value was present. +func (cdr *CounterDataReader) Goos() string { + return cdr.goos +} + +// Goarch returns the GOARCH setting in effect for the "-cover" binary +// that produced this counter data file. The GOARCH value may be +// empty in the case where the counter data file was produced +// from a merge in which more than one GOARCH value was present. +func (cdr *CounterDataReader) Goarch() string { + return cdr.goarch +} + +// FuncPayload encapsulates the counter data payload for a single +// function as read from a counter data file. +type FuncPayload struct { + PkgIdx uint32 + FuncIdx uint32 + Counters []uint32 +} + +// NumSegments returns the number of execution segments in the file. +func (cdr *CounterDataReader) NumSegments() uint32 { + return cdr.ftr.NumSegments +} + +// BeginNextSegment sets up the reader to read the next segment, +// returning TRUE if we do have another segment to read, or FALSE +// if we're done with all the segments (also an error if +// something went wrong). +func (cdr *CounterDataReader) BeginNextSegment() (bool, error) { + if cdr.segCount >= cdr.ftr.NumSegments { + return false, nil + } + cdr.segCount++ + cdr.fcnCount = 0 + // Seek past footer from last segment. + ftrSize := int64(unsafe.Sizeof(cdr.ftr)) + if _, err := cdr.mr.Seek(ftrSize, io.SeekCurrent); err != nil { + return false, err + } + // Read preamble for this segment. + if err := cdr.readSegmentPreamble(); err != nil { + return false, err + } + return true, nil +} + +// NumFunctionsInSegment returns the number of live functions +// in the currently selected segment. +func (cdr *CounterDataReader) NumFunctionsInSegment() uint32 { + return uint32(cdr.shdr.FcnEntries) +} + +const supportDeadFunctionsInCounterData = false + +// NextFunc reads data for the next function in this current segment +// into "p", returning TRUE if the read was successful or FALSE +// if we've read all the functions already (also an error if +// something went wrong with the read or we hit a premature +// EOF). +func (cdr *CounterDataReader) NextFunc(p *FuncPayload) (bool, error) { + if cdr.fcnCount >= uint32(cdr.shdr.FcnEntries) { + return false, nil + } + cdr.fcnCount++ + var rdu32 func() (uint32, error) + if cdr.hdr.CFlavor == coverage.CtrULeb128 { + rdu32 = func() (uint32, error) { + var shift uint + var value uint64 + for { + _, err := cdr.mr.Read(cdr.u8b) + if err != nil { + return 0, err + } + b := cdr.u8b[0] + value |= (uint64(b&0x7F) << shift) + if b&0x80 == 0 { + break + } + shift += 7 + } + return uint32(value), nil + } + } else if cdr.hdr.CFlavor == coverage.CtrRaw { + if cdr.hdr.BigEndian { + rdu32 = func() (uint32, error) { + n, err := cdr.mr.Read(cdr.u32b) + if err != nil { + return 0, err + } + if n != 4 { + return 0, io.EOF + } + return binary.BigEndian.Uint32(cdr.u32b), nil + } + } else { + rdu32 = func() (uint32, error) { + n, err := cdr.mr.Read(cdr.u32b) + if err != nil { + return 0, err + } + if n != 4 { + return 0, io.EOF + } + return binary.LittleEndian.Uint32(cdr.u32b), nil + } + } + } else { + panic("internal error: unknown counter flavor") + } + + // Alternative/experimental path: one way we could handling writing + // out counter data would be to just memcpy the counter segment + // out to a file, meaning that a region in the counter memory + // corresponding to a dead (never-executed) function would just be + // zeroes. The code path below handles this case. + var nc uint32 + var err error + if supportDeadFunctionsInCounterData { + for { + nc, err = rdu32() + if err == io.EOF { + return false, io.EOF + } else if err != nil { + break + } + if nc != 0 { + break + } + } + } else { + nc, err = rdu32() + } + if err != nil { + return false, err + } + + // Read package and func indices. + p.PkgIdx, err = rdu32() + if err != nil { + return false, err + } + p.FuncIdx, err = rdu32() + if err != nil { + return false, err + } + if cap(p.Counters) < 1024 { + p.Counters = make([]uint32, 0, 1024) + } + p.Counters = p.Counters[:0] + for i := uint32(0); i < nc; i++ { + v, err := rdu32() + if err != nil { + return false, err + } + p.Counters = append(p.Counters, v) + } + return true, nil +} diff --git a/src/internal/coverage/decodemeta/decode.go b/src/internal/coverage/decodemeta/decode.go new file mode 100644 index 0000000..fa047c7 --- /dev/null +++ b/src/internal/coverage/decodemeta/decode.go @@ -0,0 +1,136 @@ +// Copyright 2021 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 decodemeta + +// This package contains APIs and helpers for decoding a single package's +// meta data "blob" emitted by the compiler when coverage instrumentation +// is turned on. + +import ( + "encoding/binary" + "fmt" + "internal/coverage" + "internal/coverage/slicereader" + "internal/coverage/stringtab" + "io" + "os" +) + +// See comments in the encodecovmeta package for details on the format. + +type CoverageMetaDataDecoder struct { + r *slicereader.Reader + hdr coverage.MetaSymbolHeader + strtab *stringtab.Reader + tmp []byte + debug bool +} + +func NewCoverageMetaDataDecoder(b []byte, readonly bool) (*CoverageMetaDataDecoder, error) { + slr := slicereader.NewReader(b, readonly) + x := &CoverageMetaDataDecoder{ + r: slr, + tmp: make([]byte, 0, 256), + } + if err := x.readHeader(); err != nil { + return nil, err + } + if err := x.readStringTable(); err != nil { + return nil, err + } + return x, nil +} + +func (d *CoverageMetaDataDecoder) readHeader() error { + if err := binary.Read(d.r, binary.LittleEndian, &d.hdr); err != nil { + return err + } + if d.debug { + fmt.Fprintf(os.Stderr, "=-= after readHeader: %+v\n", d.hdr) + } + return nil +} + +func (d *CoverageMetaDataDecoder) readStringTable() error { + // Seek to the correct location to read the string table. + stringTableLocation := int64(coverage.CovMetaHeaderSize + 4*d.hdr.NumFuncs) + if _, err := d.r.Seek(stringTableLocation, io.SeekStart); err != nil { + return err + } + + // Read the table itself. + d.strtab = stringtab.NewReader(d.r) + d.strtab.Read() + return nil +} + +func (d *CoverageMetaDataDecoder) PackagePath() string { + return d.strtab.Get(d.hdr.PkgPath) +} + +func (d *CoverageMetaDataDecoder) PackageName() string { + return d.strtab.Get(d.hdr.PkgName) +} + +func (d *CoverageMetaDataDecoder) ModulePath() string { + return d.strtab.Get(d.hdr.ModulePath) +} + +func (d *CoverageMetaDataDecoder) NumFuncs() uint32 { + return d.hdr.NumFuncs +} + +// ReadFunc reads the coverage meta-data for the function with index +// 'findex', filling it into the FuncDesc pointed to by 'f'. +func (d *CoverageMetaDataDecoder) ReadFunc(fidx uint32, f *coverage.FuncDesc) error { + if fidx >= d.hdr.NumFuncs { + return fmt.Errorf("illegal function index") + } + + // Seek to the correct location to read the function offset and read it. + funcOffsetLocation := int64(coverage.CovMetaHeaderSize + 4*fidx) + if _, err := d.r.Seek(funcOffsetLocation, io.SeekStart); err != nil { + return err + } + foff := d.r.ReadUint32() + + // Check assumptions + if foff < uint32(funcOffsetLocation) || foff > d.hdr.Length { + return fmt.Errorf("malformed func offset %d", foff) + } + + // Seek to the correct location to read the function. + floc := int64(foff) + if _, err := d.r.Seek(floc, io.SeekStart); err != nil { + return err + } + + // Preamble containing number of units, file, and function. + numUnits := uint32(d.r.ReadULEB128()) + fnameidx := uint32(d.r.ReadULEB128()) + fileidx := uint32(d.r.ReadULEB128()) + + f.Srcfile = d.strtab.Get(fileidx) + f.Funcname = d.strtab.Get(fnameidx) + + // Now the units + f.Units = f.Units[:0] + if cap(f.Units) < int(numUnits) { + f.Units = make([]coverage.CoverableUnit, 0, numUnits) + } + for k := uint32(0); k < numUnits; k++ { + f.Units = append(f.Units, + coverage.CoverableUnit{ + StLine: uint32(d.r.ReadULEB128()), + StCol: uint32(d.r.ReadULEB128()), + EnLine: uint32(d.r.ReadULEB128()), + EnCol: uint32(d.r.ReadULEB128()), + NxStmts: uint32(d.r.ReadULEB128()), + }) + } + lit := d.r.ReadULEB128() + f.Lit = lit != 0 + return nil +} diff --git a/src/internal/coverage/decodemeta/decodefile.go b/src/internal/coverage/decodemeta/decodefile.go new file mode 100644 index 0000000..96e0765 --- /dev/null +++ b/src/internal/coverage/decodemeta/decodefile.go @@ -0,0 +1,223 @@ +// Copyright 2021 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 decodemeta + +// This package contains APIs and helpers for reading and decoding +// meta-data output files emitted by the runtime when a +// coverage-instrumented binary executes. A meta-data file contains +// top-level info (counter mode, number of packages) and then a +// separate self-contained meta-data section for each Go package. + +import ( + "bufio" + "crypto/md5" + "encoding/binary" + "fmt" + "internal/coverage" + "internal/coverage/slicereader" + "internal/coverage/stringtab" + "io" + "os" +) + +// CoverageMetaFileReader provides state and methods for reading +// a meta-data file from a code coverage run. +type CoverageMetaFileReader struct { + f *os.File + hdr coverage.MetaFileHeader + tmp []byte + pkgOffsets []uint64 + pkgLengths []uint64 + strtab *stringtab.Reader + fileRdr *bufio.Reader + fileView []byte + debug bool +} + +// NewCoverageMetaFileReader returns a new helper object for reading +// the coverage meta-data output file 'f'. The param 'fileView' is a +// read-only slice containing the contents of 'f' obtained by mmap'ing +// the file read-only; 'fileView' may be nil, in which case the helper +// will read the contents of the file using regular file Read +// operations. +func NewCoverageMetaFileReader(f *os.File, fileView []byte) (*CoverageMetaFileReader, error) { + r := &CoverageMetaFileReader{ + f: f, + fileView: fileView, + tmp: make([]byte, 256), + } + + if err := r.readFileHeader(); err != nil { + return nil, err + } + return r, nil +} + +func (r *CoverageMetaFileReader) readFileHeader() error { + var err error + + r.fileRdr = bufio.NewReader(r.f) + + // Read file header. + if err := binary.Read(r.fileRdr, binary.LittleEndian, &r.hdr); err != nil { + return err + } + + // Verify magic string + m := r.hdr.Magic + g := coverage.CovMetaMagic + if m[0] != g[0] || m[1] != g[1] || m[2] != g[2] || m[3] != g[3] { + return fmt.Errorf("invalid meta-data file magic string") + } + + // Vet the version. If this is a meta-data file from the future, + // we won't be able to read it. + if r.hdr.Version > coverage.MetaFileVersion { + return fmt.Errorf("meta-data file withn unknown version %d (expected %d)", r.hdr.Version, coverage.MetaFileVersion) + } + + // Read package offsets for good measure + r.pkgOffsets = make([]uint64, r.hdr.Entries) + for i := uint64(0); i < r.hdr.Entries; i++ { + if r.pkgOffsets[i], err = r.rdUint64(); err != nil { + return err + } + if r.pkgOffsets[i] > r.hdr.TotalLength { + return fmt.Errorf("insane pkg offset %d: %d > totlen %d", + i, r.pkgOffsets[i], r.hdr.TotalLength) + } + } + r.pkgLengths = make([]uint64, r.hdr.Entries) + for i := uint64(0); i < r.hdr.Entries; i++ { + if r.pkgLengths[i], err = r.rdUint64(); err != nil { + return err + } + if r.pkgLengths[i] > r.hdr.TotalLength { + return fmt.Errorf("insane pkg length %d: %d > totlen %d", + i, r.pkgLengths[i], r.hdr.TotalLength) + } + } + + // Read string table. + b := make([]byte, r.hdr.StrTabLength) + nr, err := r.fileRdr.Read(b) + if err != nil { + return err + } + if nr != int(r.hdr.StrTabLength) { + return fmt.Errorf("error: short read on string table") + } + slr := slicereader.NewReader(b, false /* not readonly */) + r.strtab = stringtab.NewReader(slr) + r.strtab.Read() + + if r.debug { + fmt.Fprintf(os.Stderr, "=-= read-in header is: %+v\n", *r) + } + + return nil +} + +func (r *CoverageMetaFileReader) rdUint64() (uint64, error) { + r.tmp = r.tmp[:0] + r.tmp = append(r.tmp, make([]byte, 8)...) + n, err := r.fileRdr.Read(r.tmp) + if err != nil { + return 0, err + } + if n != 8 { + return 0, fmt.Errorf("premature end of file on read") + } + v := binary.LittleEndian.Uint64(r.tmp) + return v, nil +} + +// NumPackages returns the number of packages for which this file +// contains meta-data. +func (r *CoverageMetaFileReader) NumPackages() uint64 { + return r.hdr.Entries +} + +// CounterMode returns the counter mode (set, count, atomic) used +// when building for coverage for the program that produce this +// meta-data file. +func (r *CoverageMetaFileReader) CounterMode() coverage.CounterMode { + return r.hdr.CMode +} + +// CounterGranularity returns the counter granularity (single counter per +// function, or counter per block) selected when building for coverage +// for the program that produce this meta-data file. +func (r *CoverageMetaFileReader) CounterGranularity() coverage.CounterGranularity { + return r.hdr.CGranularity +} + +// FileHash returns the hash computed for all of the package meta-data +// blobs. Coverage counter data files refer to this hash, and the +// hash will be encoded into the meta-data file name. +func (r *CoverageMetaFileReader) FileHash() [16]byte { + return r.hdr.MetaFileHash +} + +// GetPackageDecoder requests a decoder object for the package within +// the meta-data file whose index is 'pkIdx'. If the +// CoverageMetaFileReader was set up with a read-only file view, a +// pointer into that file view will be returned, otherwise the buffer +// 'payloadbuf' will be written to (or if it is not of sufficient +// size, a new buffer will be allocated). Return value is the decoder, +// a byte slice with the encoded meta-data, and an error. +func (r *CoverageMetaFileReader) GetPackageDecoder(pkIdx uint32, payloadbuf []byte) (*CoverageMetaDataDecoder, []byte, error) { + pp, err := r.GetPackagePayload(pkIdx, payloadbuf) + if r.debug { + fmt.Fprintf(os.Stderr, "=-= pkidx=%d payload length is %d hash=%s\n", + pkIdx, len(pp), fmt.Sprintf("%x", md5.Sum(pp))) + } + if err != nil { + return nil, nil, err + } + mdd, err := NewCoverageMetaDataDecoder(pp, r.fileView != nil) + if err != nil { + return nil, nil, err + } + return mdd, pp, nil +} + +// GetPackagePayload returns the raw (encoded) meta-data payload for the +// package with index 'pkIdx'. As with GetPackageDecoder, if the +// CoverageMetaFileReader was set up with a read-only file view, a +// pointer into that file view will be returned, otherwise the buffer +// 'payloadbuf' will be written to (or if it is not of sufficient +// size, a new buffer will be allocated). Return value is the decoder, +// a byte slice with the encoded meta-data, and an error. +func (r *CoverageMetaFileReader) GetPackagePayload(pkIdx uint32, payloadbuf []byte) ([]byte, error) { + + // Determine correct offset/length. + if uint64(pkIdx) >= r.hdr.Entries { + return nil, fmt.Errorf("GetPackagePayload: illegal pkg index %d", pkIdx) + } + off := r.pkgOffsets[pkIdx] + len := r.pkgLengths[pkIdx] + + if r.debug { + fmt.Fprintf(os.Stderr, "=-= for pk %d, off=%d len=%d\n", pkIdx, off, len) + } + + if r.fileView != nil { + return r.fileView[off : off+len], nil + } + + payload := payloadbuf[:0] + if cap(payload) < int(len) { + payload = make([]byte, 0, len) + } + payload = append(payload, make([]byte, len)...) + if _, err := r.f.Seek(int64(off), io.SeekStart); err != nil { + return nil, err + } + if _, err := io.ReadFull(r.f, payload); err != nil { + return nil, err + } + return payload, nil +} diff --git a/src/internal/coverage/defs.go b/src/internal/coverage/defs.go new file mode 100644 index 0000000..340ac95 --- /dev/null +++ b/src/internal/coverage/defs.go @@ -0,0 +1,388 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package coverage + +// Types and constants related to the output files written +// by code coverage tooling. When a coverage-instrumented binary +// is run, it emits two output files: a meta-data output file, and +// a counter data output file. + +//..................................................................... +// +// Meta-data definitions: +// +// The meta-data file is composed of a file header, a series of +// meta-data blobs/sections (one per instrumented package), and an offsets +// area storing the offsets of each section. Format of the meta-data +// file looks like: +// +// --header---------- +// | magic: [4]byte magic string +// | version +// | total length of meta-data file in bytes +// | numPkgs: number of package entries in file +// | hash: [16]byte hash of entire meta-data payload +// | offset to string table section +// | length of string table +// | number of entries in string table +// | counter mode +// | counter granularity +// --package offsets table------ +// <offset to pkg 0> +// <offset to pkg 1> +// ... +// --package lengths table------ +// <length of pkg 0> +// <length of pkg 1> +// ... +// --string table------ +// <uleb128 len> 8 +// <data> "somestring" +// ... +// --package payloads------ +// <meta-symbol for pkg 0> +// <meta-symbol for pkg 1> +// ... +// +// Each package payload is a stand-alone blob emitted by the compiler, +// and does not depend on anything else in the meta-data file. In +// particular, each blob has it's own string table. Note that the +// file-level string table is expected to be very short (most strings +// will be in the meta-data blobs themselves). + +// CovMetaMagic holds the magic string for a meta-data file. +var CovMetaMagic = [4]byte{'\x00', '\x63', '\x76', '\x6d'} + +// MetaFilePref is a prefix used when emitting meta-data files; these +// files are of the form "covmeta.<hash>", where hash is a hash +// computed from the hashes of all the package meta-data symbols in +// the program. +const MetaFilePref = "covmeta" + +// MetaFileVersion contains the current (most recent) meta-data file version. +const MetaFileVersion = 1 + +// MetaFileHeader stores file header information for a meta-data file. +type MetaFileHeader struct { + Magic [4]byte + Version uint32 + TotalLength uint64 + Entries uint64 + MetaFileHash [16]byte + StrTabOffset uint32 + StrTabLength uint32 + CMode CounterMode + CGranularity CounterGranularity + _ [6]byte // padding +} + +// MetaSymbolHeader stores header information for a single +// meta-data blob, e.g. the coverage meta-data payload +// computed for a given Go package. +type MetaSymbolHeader struct { + Length uint32 // size of meta-symbol payload in bytes + PkgName uint32 // string table index + PkgPath uint32 // string table index + ModulePath uint32 // string table index + MetaHash [16]byte + _ byte // currently unused + _ [3]byte // padding + NumFiles uint32 + NumFuncs uint32 +} + +const CovMetaHeaderSize = 16 + 4 + 4 + 4 + 4 + 4 + 4 + 4 // keep in sync with above + +// As an example, consider the following Go package: +// +// 01: package p +// 02: +// 03: var v, w, z int +// 04: +// 05: func small(x, y int) int { +// 06: v++ +// 07: // comment +// 08: if y == 0 { +// 09: return x +// 10: } +// 11: return (x << 1) ^ (9 / y) +// 12: } +// 13: +// 14: func Medium(q, r int) int { +// 15: s1 := small(q, r) +// 16: z += s1 +// 17: s2 := small(r, q) +// 18: w -= s2 +// 19: return w + z +// 20: } +// +// The meta-data blob for the single package above might look like the +// following: +// +// -- MetaSymbolHeader header---------- +// | size: size of this blob in bytes +// | packagepath: <path to p> +// | modulepath: <modpath for p> +// | nfiles: 1 +// | nfunctions: 2 +// --func offsets table------ +// <offset to func 0> +// <offset to func 1> +// --string table (contains all files and functions)------ +// | <uleb128 len> 4 +// | <data> "p.go" +// | <uleb128 len> 5 +// | <data> "small" +// | <uleb128 len> 6 +// | <data> "Medium" +// --func 0------ +// | <uleb128> num units: 3 +// | <uleb128> func name: S1 (index into string table) +// | <uleb128> file: S0 (index into string table) +// | <unit 0>: S0 L6 L8 2 +// | <unit 1>: S0 L9 L9 1 +// | <unit 2>: S0 L11 L11 1 +// --func 1------ +// | <uleb128> num units: 1 +// | <uleb128> func name: S2 (index into string table) +// | <uleb128> file: S0 (index into string table) +// | <unit 0>: S0 L15 L19 5 +// ---end----------- + +// The following types and constants used by the meta-data encoder/decoder. + +// FuncDesc encapsulates the meta-data definitions for a single Go function. +// This version assumes that we're looking at a function before inlining; +// if we want to capture a post-inlining view of the world, the +// representations of source positions would need to be a good deal more +// complicated. +type FuncDesc struct { + Funcname string + Srcfile string + Units []CoverableUnit + Lit bool // true if this is a function literal +} + +// CoverableUnit describes the source characteristics of a single +// program unit for which we want to gather coverage info. Coverable +// units are either "simple" or "intraline"; a "simple" coverable unit +// corresponds to a basic block (region of straight-line code with no +// jumps or control transfers). An "intraline" unit corresponds to a +// logical clause nested within some other simple unit. A simple unit +// will have a zero Parent value; for an intraline unit NxStmts will +// be zero and Parent will be set to 1 plus the index of the +// containing simple statement. Example: +// +// L7: q := 1 +// L8: x := (y == 101 || launch() == false) +// L9: r := x * 2 +// +// For the code above we would have three simple units (one for each +// line), then an intraline unit describing the "launch() == false" +// clause in line 8, with Parent pointing to the index of the line 8 +// unit in the units array. +// +// Note: in the initial version of the coverage revamp, only simple +// units will be in use. +type CoverableUnit struct { + StLine, StCol uint32 + EnLine, EnCol uint32 + NxStmts uint32 + Parent uint32 +} + +// CounterMode tracks the "flavor" of the coverage counters being +// used in a given coverage-instrumented program. +type CounterMode uint8 + +const ( + CtrModeInvalid CounterMode = iota + CtrModeSet // "set" mode + CtrModeCount // "count" mode + CtrModeAtomic // "atomic" mode + CtrModeRegOnly // registration-only pseudo-mode + CtrModeTestMain // testmain pseudo-mode +) + +func (cm CounterMode) String() string { + switch cm { + case CtrModeSet: + return "set" + case CtrModeCount: + return "count" + case CtrModeAtomic: + return "atomic" + case CtrModeRegOnly: + return "regonly" + case CtrModeTestMain: + return "testmain" + } + return "<invalid>" +} + +func ParseCounterMode(mode string) CounterMode { + var cm CounterMode + switch mode { + case "set": + cm = CtrModeSet + case "count": + cm = CtrModeCount + case "atomic": + cm = CtrModeAtomic + case "regonly": + cm = CtrModeRegOnly + case "testmain": + cm = CtrModeTestMain + default: + cm = CtrModeInvalid + } + return cm +} + +// CounterGranularity tracks the granularity of the coverage counters being +// used in a given coverage-instrumented program. +type CounterGranularity uint8 + +const ( + CtrGranularityInvalid CounterGranularity = iota + CtrGranularityPerBlock + CtrGranularityPerFunc +) + +func (cm CounterGranularity) String() string { + switch cm { + case CtrGranularityPerBlock: + return "perblock" + case CtrGranularityPerFunc: + return "perfunc" + } + return "<invalid>" +} + +// Name of file within the "go test -cover" temp coverdir directory +// containing a list of meta-data files for packages being tested +// in a "go test -coverpkg=... ..." run. This constant is shared +// by the Go command and by the coverage runtime. +const MetaFilesFileName = "metafiles.txt" + +// MetaFilePaths contains information generated by the Go command and +// the read in by coverage test support functions within an executing +// "go test -cover" binary. +type MetaFileCollection struct { + ImportPaths []string + MetaFileFragments []string +} + +//..................................................................... +// +// Counter data definitions: +// + +// A counter data file is composed of a file header followed by one or +// more "segments" (each segment representing a given run or partial +// run of a give binary) followed by a footer. + +// CovCounterMagic holds the magic string for a coverage counter-data file. +var CovCounterMagic = [4]byte{'\x00', '\x63', '\x77', '\x6d'} + +// CounterFileVersion stores the most recent counter data file version. +const CounterFileVersion = 1 + +// CounterFileHeader stores files header information for a counter-data file. +type CounterFileHeader struct { + Magic [4]byte + Version uint32 + MetaHash [16]byte + CFlavor CounterFlavor + BigEndian bool + _ [6]byte // padding +} + +// CounterSegmentHeader encapsulates information about a specific +// segment in a counter data file, which at the moment contains +// counters data from a single execution of a coverage-instrumented +// program. Following the segment header will be the string table and +// args table, and then (possibly) padding bytes to bring the byte +// size of the preamble up to a multiple of 4. Immediately following +// that will be the counter payloads. +// +// The "args" section of a segment is used to store annotations +// describing where the counter data came from; this section is +// basically a series of key-value pairs (can be thought of as an +// encoded 'map[string]string'). At the moment we only write os.Args() +// data to this section, using pairs of the form "argc=<integer>", +// "argv0=<os.Args[0]>", "argv1=<os.Args[1]>", and so on. In the +// future the args table may also include things like GOOS/GOARCH +// values, and/or tags indicating which tests were run to generate the +// counter data. +type CounterSegmentHeader struct { + FcnEntries uint64 + StrTabLen uint32 + ArgsLen uint32 +} + +// CounterFileFooter appears at the tail end of a counter data file, +// and stores the number of segments it contains. +type CounterFileFooter struct { + Magic [4]byte + _ [4]byte // padding + NumSegments uint32 + _ [4]byte // padding +} + +// CounterFilePref is the file prefix used when emitting coverage data +// output files. CounterFileTemplate describes the format of the file +// name: prefix followed by meta-file hash followed by process ID +// followed by emit UnixNanoTime. +const CounterFilePref = "covcounters" +const CounterFileTempl = "%s.%x.%d.%d" +const CounterFileRegexp = `^%s\.(\S+)\.(\d+)\.(\d+)+$` + +// CounterFlavor describes how function and counters are +// stored/represented in the counter section of the file. +type CounterFlavor uint8 + +const ( + // "Raw" representation: all values (pkg ID, func ID, num counters, + // and counters themselves) are stored as uint32's. + CtrRaw CounterFlavor = iota + 1 + + // "ULeb" representation: all values (pkg ID, func ID, num counters, + // and counters themselves) are stored with ULEB128 encoding. + CtrULeb128 +) + +func Round4(x int) int { + return (x + 3) &^ 3 +} + +//..................................................................... +// +// Runtime counter data definitions. +// + +// At runtime within a coverage-instrumented program, the "counters" +// object we associated with instrumented function can be thought of +// as a struct of the following form: +// +// struct { +// numCtrs uint32 +// pkgid uint32 +// funcid uint32 +// counterArray [numBlocks]uint32 +// } +// +// where "numCtrs" is the number of blocks / coverable units within the +// function, "pkgid" is the unique index assigned to this package by +// the runtime, "funcid" is the index of this function within its containing +// package, and "counterArray" stores the actual counters. +// +// The counter variable itself is created not as a struct but as a flat +// array of uint32's; we then use the offsets below to index into it. + +const NumCtrsOffset = 0 +const PkgIdOffset = 1 +const FuncIdOffset = 2 +const FirstCtrOffset = 3 diff --git a/src/internal/coverage/encodecounter/encode.go b/src/internal/coverage/encodecounter/encode.go new file mode 100644 index 0000000..5958673 --- /dev/null +++ b/src/internal/coverage/encodecounter/encode.go @@ -0,0 +1,297 @@ +// Copyright 2021 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 encodecounter + +import ( + "bufio" + "encoding/binary" + "fmt" + "internal/coverage" + "internal/coverage/slicewriter" + "internal/coverage/stringtab" + "internal/coverage/uleb128" + "io" + "os" + "sort" +) + +// This package contains APIs and helpers for encoding initial portions +// of the counter data files emitted at runtime when coverage instrumentation +// is enabled. Counter data files may contain multiple segments; the file +// header and first segment are written via the "Write" method below, and +// additional segments can then be added using "AddSegment". + +type CoverageDataWriter struct { + stab *stringtab.Writer + w *bufio.Writer + csh coverage.CounterSegmentHeader + tmp []byte + cflavor coverage.CounterFlavor + segs uint32 + debug bool +} + +func NewCoverageDataWriter(w io.Writer, flav coverage.CounterFlavor) *CoverageDataWriter { + r := &CoverageDataWriter{ + stab: &stringtab.Writer{}, + w: bufio.NewWriter(w), + + tmp: make([]byte, 64), + cflavor: flav, + } + r.stab.InitWriter() + r.stab.Lookup("") + return r +} + +// CounterVisitor describes a helper object used during counter file +// writing; when writing counter data files, clients pass a +// CounterVisitor to the write/emit routines, then the expectation is +// that the VisitFuncs method will then invoke the callback "f" with +// data for each function to emit to the file. +type CounterVisitor interface { + VisitFuncs(f CounterVisitorFn) error +} + +// CounterVisitorFn describes a callback function invoked when writing +// coverage counter data. +type CounterVisitorFn func(pkid uint32, funcid uint32, counters []uint32) error + +// Write writes the contents of the count-data file to the writer +// previously supplied to NewCoverageDataWriter. Returns an error +// if something went wrong somewhere with the write. +func (cfw *CoverageDataWriter) Write(metaFileHash [16]byte, args map[string]string, visitor CounterVisitor) error { + if err := cfw.writeHeader(metaFileHash); err != nil { + return err + } + return cfw.AppendSegment(args, visitor) +} + +func padToFourByteBoundary(ws *slicewriter.WriteSeeker) error { + sz := len(ws.BytesWritten()) + zeros := []byte{0, 0, 0, 0} + rem := uint32(sz) % 4 + if rem != 0 { + pad := zeros[:(4 - rem)] + if nw, err := ws.Write(pad); err != nil { + return err + } else if nw != len(pad) { + return fmt.Errorf("error: short write") + } + } + return nil +} + +func (cfw *CoverageDataWriter) patchSegmentHeader(ws *slicewriter.WriteSeeker) error { + // record position + off, err := ws.Seek(0, io.SeekCurrent) + if err != nil { + return fmt.Errorf("error seeking in patchSegmentHeader: %v", err) + } + // seek back to start so that we can update the segment header + if _, err := ws.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("error seeking in patchSegmentHeader: %v", err) + } + if cfw.debug { + fmt.Fprintf(os.Stderr, "=-= writing counter segment header: %+v", cfw.csh) + } + if err := binary.Write(ws, binary.LittleEndian, cfw.csh); err != nil { + return err + } + // ... and finally return to the original offset. + if _, err := ws.Seek(off, io.SeekStart); err != nil { + return fmt.Errorf("error seeking in patchSegmentHeader: %v", err) + } + return nil +} + +func (cfw *CoverageDataWriter) writeSegmentPreamble(args map[string]string, ws *slicewriter.WriteSeeker) error { + if err := binary.Write(ws, binary.LittleEndian, cfw.csh); err != nil { + return err + } + hdrsz := uint32(len(ws.BytesWritten())) + + // Write string table and args to a byte slice (since we need + // to capture offsets at various points), then emit the slice + // once we are done. + cfw.stab.Freeze() + if err := cfw.stab.Write(ws); err != nil { + return err + } + cfw.csh.StrTabLen = uint32(len(ws.BytesWritten())) - hdrsz + + akeys := make([]string, 0, len(args)) + for k := range args { + akeys = append(akeys, k) + } + sort.Strings(akeys) + + wrULEB128 := func(v uint) error { + cfw.tmp = cfw.tmp[:0] + cfw.tmp = uleb128.AppendUleb128(cfw.tmp, v) + if _, err := ws.Write(cfw.tmp); err != nil { + return err + } + return nil + } + + // Count of arg pairs. + if err := wrULEB128(uint(len(args))); err != nil { + return err + } + // Arg pairs themselves. + for _, k := range akeys { + ki := uint(cfw.stab.Lookup(k)) + if err := wrULEB128(ki); err != nil { + return err + } + v := args[k] + vi := uint(cfw.stab.Lookup(v)) + if err := wrULEB128(vi); err != nil { + return err + } + } + if err := padToFourByteBoundary(ws); err != nil { + return err + } + cfw.csh.ArgsLen = uint32(len(ws.BytesWritten())) - (cfw.csh.StrTabLen + hdrsz) + + return nil +} + +// AppendSegment appends a new segment to a counter data, with a new +// args section followed by a payload of counter data clauses. +func (cfw *CoverageDataWriter) AppendSegment(args map[string]string, visitor CounterVisitor) error { + cfw.stab = &stringtab.Writer{} + cfw.stab.InitWriter() + cfw.stab.Lookup("") + + var err error + for k, v := range args { + cfw.stab.Lookup(k) + cfw.stab.Lookup(v) + } + + ws := &slicewriter.WriteSeeker{} + if err = cfw.writeSegmentPreamble(args, ws); err != nil { + return err + } + if err = cfw.writeCounters(visitor, ws); err != nil { + return err + } + if err = cfw.patchSegmentHeader(ws); err != nil { + return err + } + if err := cfw.writeBytes(ws.BytesWritten()); err != nil { + return err + } + if err = cfw.writeFooter(); err != nil { + return err + } + if err := cfw.w.Flush(); err != nil { + return fmt.Errorf("write error: %v", err) + } + cfw.stab = nil + return nil +} + +func (cfw *CoverageDataWriter) writeHeader(metaFileHash [16]byte) error { + // Emit file header. + ch := coverage.CounterFileHeader{ + Magic: coverage.CovCounterMagic, + Version: coverage.CounterFileVersion, + MetaHash: metaFileHash, + CFlavor: cfw.cflavor, + BigEndian: false, + } + if err := binary.Write(cfw.w, binary.LittleEndian, ch); err != nil { + return err + } + return nil +} + +func (cfw *CoverageDataWriter) writeBytes(b []byte) error { + if len(b) == 0 { + return nil + } + nw, err := cfw.w.Write(b) + if err != nil { + return fmt.Errorf("error writing counter data: %v", err) + } + if len(b) != nw { + return fmt.Errorf("error writing counter data: short write") + } + return nil +} + +func (cfw *CoverageDataWriter) writeCounters(visitor CounterVisitor, ws *slicewriter.WriteSeeker) error { + // Notes: + // - this version writes everything little-endian, which means + // a call is needed to encode every value (expensive) + // - we may want to move to a model in which we just blast out + // all counters, or possibly mmap the file and do the write + // implicitly. + ctrb := make([]byte, 4) + wrval := func(val uint32) error { + var buf []byte + var towr int + if cfw.cflavor == coverage.CtrRaw { + binary.LittleEndian.PutUint32(ctrb, val) + buf = ctrb + towr = 4 + } else if cfw.cflavor == coverage.CtrULeb128 { + cfw.tmp = cfw.tmp[:0] + cfw.tmp = uleb128.AppendUleb128(cfw.tmp, uint(val)) + buf = cfw.tmp + towr = len(buf) + } else { + panic("internal error: bad counter flavor") + } + if sz, err := ws.Write(buf); err != nil { + return err + } else if sz != towr { + return fmt.Errorf("writing counters: short write") + } + return nil + } + + // Write out entries for each live function. + emitter := func(pkid uint32, funcid uint32, counters []uint32) error { + cfw.csh.FcnEntries++ + if err := wrval(uint32(len(counters))); err != nil { + return err + } + + if err := wrval(pkid); err != nil { + return err + } + + if err := wrval(funcid); err != nil { + return err + } + for _, val := range counters { + if err := wrval(val); err != nil { + return err + } + } + return nil + } + if err := visitor.VisitFuncs(emitter); err != nil { + return err + } + return nil +} + +func (cfw *CoverageDataWriter) writeFooter() error { + cfw.segs++ + cf := coverage.CounterFileFooter{ + Magic: coverage.CovCounterMagic, + NumSegments: cfw.segs, + } + if err := binary.Write(cfw.w, binary.LittleEndian, cf); err != nil { + return err + } + return nil +} diff --git a/src/internal/coverage/encodemeta/encode.go b/src/internal/coverage/encodemeta/encode.go new file mode 100644 index 0000000..d211c7c --- /dev/null +++ b/src/internal/coverage/encodemeta/encode.go @@ -0,0 +1,215 @@ +// Copyright 2021 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 encodemeta + +// This package contains APIs and helpers for encoding the meta-data +// "blob" for a single Go package, created when coverage +// instrumentation is turned on. + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "fmt" + "hash" + "internal/coverage" + "internal/coverage/stringtab" + "internal/coverage/uleb128" + "io" + "os" +) + +type CoverageMetaDataBuilder struct { + stab stringtab.Writer + funcs []funcDesc + tmp []byte // temp work slice + h hash.Hash + pkgpath uint32 + pkgname uint32 + modpath uint32 + debug bool + werr error +} + +func NewCoverageMetaDataBuilder(pkgpath string, pkgname string, modulepath string) (*CoverageMetaDataBuilder, error) { + if pkgpath == "" { + return nil, fmt.Errorf("invalid empty package path") + } + x := &CoverageMetaDataBuilder{ + tmp: make([]byte, 0, 256), + h: md5.New(), + } + x.stab.InitWriter() + x.stab.Lookup("") + x.pkgpath = x.stab.Lookup(pkgpath) + x.pkgname = x.stab.Lookup(pkgname) + x.modpath = x.stab.Lookup(modulepath) + io.WriteString(x.h, pkgpath) + io.WriteString(x.h, pkgname) + io.WriteString(x.h, modulepath) + return x, nil +} + +func h32(x uint32, h hash.Hash, tmp []byte) { + tmp = tmp[:0] + tmp = append(tmp, []byte{0, 0, 0, 0}...) + binary.LittleEndian.PutUint32(tmp, x) + h.Write(tmp) +} + +type funcDesc struct { + encoded []byte +} + +// AddFunc registers a new function with the meta data builder. +func (b *CoverageMetaDataBuilder) AddFunc(f coverage.FuncDesc) uint { + hashFuncDesc(b.h, &f, b.tmp) + fd := funcDesc{} + b.tmp = b.tmp[:0] + b.tmp = uleb128.AppendUleb128(b.tmp, uint(len(f.Units))) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(b.stab.Lookup(f.Funcname))) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(b.stab.Lookup(f.Srcfile))) + for _, u := range f.Units { + b.tmp = uleb128.AppendUleb128(b.tmp, uint(u.StLine)) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(u.StCol)) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(u.EnLine)) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(u.EnCol)) + b.tmp = uleb128.AppendUleb128(b.tmp, uint(u.NxStmts)) + } + lit := uint(0) + if f.Lit { + lit = 1 + } + b.tmp = uleb128.AppendUleb128(b.tmp, lit) + fd.encoded = bytes.Clone(b.tmp) + rv := uint(len(b.funcs)) + b.funcs = append(b.funcs, fd) + return rv +} + +func (b *CoverageMetaDataBuilder) emitFuncOffsets(w io.WriteSeeker, off int64) int64 { + nFuncs := len(b.funcs) + var foff int64 = coverage.CovMetaHeaderSize + int64(b.stab.Size()) + int64(nFuncs)*4 + for idx := 0; idx < nFuncs; idx++ { + b.wrUint32(w, uint32(foff)) + foff += int64(len(b.funcs[idx].encoded)) + } + return off + (int64(len(b.funcs)) * 4) +} + +func (b *CoverageMetaDataBuilder) emitFunc(w io.WriteSeeker, off int64, f funcDesc) (int64, error) { + ew := len(f.encoded) + if nw, err := w.Write(f.encoded); err != nil { + return 0, err + } else if ew != nw { + return 0, fmt.Errorf("short write emitting coverage meta-data") + } + return off + int64(ew), nil +} + +func (b *CoverageMetaDataBuilder) reportWriteError(err error) { + if b.werr != nil { + b.werr = err + } +} + +func (b *CoverageMetaDataBuilder) wrUint32(w io.WriteSeeker, v uint32) { + b.tmp = b.tmp[:0] + b.tmp = append(b.tmp, []byte{0, 0, 0, 0}...) + binary.LittleEndian.PutUint32(b.tmp, v) + if nw, err := w.Write(b.tmp); err != nil { + b.reportWriteError(err) + } else if nw != 4 { + b.reportWriteError(fmt.Errorf("short write")) + } +} + +// Emit writes the meta-data accumulated so far in this builder to 'w'. +// Returns a hash of the meta-data payload and an error. +func (b *CoverageMetaDataBuilder) Emit(w io.WriteSeeker) ([16]byte, error) { + // Emit header. Length will initially be zero, we'll + // back-patch it later. + var digest [16]byte + copy(digest[:], b.h.Sum(nil)) + mh := coverage.MetaSymbolHeader{ + // hash and length initially zero, will be back-patched + PkgPath: uint32(b.pkgpath), + PkgName: uint32(b.pkgname), + ModulePath: uint32(b.modpath), + NumFiles: uint32(b.stab.Nentries()), + NumFuncs: uint32(len(b.funcs)), + MetaHash: digest, + } + if b.debug { + fmt.Fprintf(os.Stderr, "=-= writing header: %+v\n", mh) + } + if err := binary.Write(w, binary.LittleEndian, mh); err != nil { + return digest, fmt.Errorf("error writing meta-file header: %v", err) + } + off := int64(coverage.CovMetaHeaderSize) + + // Write function offsets section + off = b.emitFuncOffsets(w, off) + + // Check for any errors up to this point. + if b.werr != nil { + return digest, b.werr + } + + // Write string table. + if err := b.stab.Write(w); err != nil { + return digest, err + } + off += int64(b.stab.Size()) + + // Write functions + for _, f := range b.funcs { + var err error + off, err = b.emitFunc(w, off, f) + if err != nil { + return digest, err + } + } + + // Back-patch the length. + totalLength := uint32(off) + if _, err := w.Seek(0, io.SeekStart); err != nil { + return digest, err + } + b.wrUint32(w, totalLength) + if b.werr != nil { + return digest, b.werr + } + return digest, nil +} + +// HashFuncDesc computes an md5 sum of a coverage.FuncDesc and returns +// a digest for it. +func HashFuncDesc(f *coverage.FuncDesc) [16]byte { + h := md5.New() + tmp := make([]byte, 0, 32) + hashFuncDesc(h, f, tmp) + var r [16]byte + copy(r[:], h.Sum(nil)) + return r +} + +// hashFuncDesc incorporates a given function 'f' into the hash 'h'. +func hashFuncDesc(h hash.Hash, f *coverage.FuncDesc, tmp []byte) { + io.WriteString(h, f.Funcname) + io.WriteString(h, f.Srcfile) + for _, u := range f.Units { + h32(u.StLine, h, tmp) + h32(u.StCol, h, tmp) + h32(u.EnLine, h, tmp) + h32(u.EnCol, h, tmp) + h32(u.NxStmts, h, tmp) + } + lit := uint32(0) + if f.Lit { + lit = 1 + } + h32(lit, h, tmp) +} diff --git a/src/internal/coverage/encodemeta/encodefile.go b/src/internal/coverage/encodemeta/encodefile.go new file mode 100644 index 0000000..38ae46e --- /dev/null +++ b/src/internal/coverage/encodemeta/encodefile.go @@ -0,0 +1,132 @@ +// Copyright 2021 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 encodemeta + +import ( + "bufio" + "crypto/md5" + "encoding/binary" + "fmt" + "internal/coverage" + "internal/coverage/stringtab" + "io" + "os" + "unsafe" +) + +// This package contains APIs and helpers for writing out a meta-data +// file (composed of a file header, offsets/lengths, and then a series of +// meta-data blobs emitted by the compiler, one per Go package). + +type CoverageMetaFileWriter struct { + stab stringtab.Writer + mfname string + w *bufio.Writer + tmp []byte + debug bool +} + +func NewCoverageMetaFileWriter(mfname string, w io.Writer) *CoverageMetaFileWriter { + r := &CoverageMetaFileWriter{ + mfname: mfname, + w: bufio.NewWriter(w), + tmp: make([]byte, 64), + } + r.stab.InitWriter() + r.stab.Lookup("") + return r +} + +func (m *CoverageMetaFileWriter) Write(finalHash [16]byte, blobs [][]byte, mode coverage.CounterMode, granularity coverage.CounterGranularity) error { + mhsz := uint64(unsafe.Sizeof(coverage.MetaFileHeader{})) + stSize := m.stab.Size() + stOffset := mhsz + uint64(16*len(blobs)) + preambleLength := stOffset + uint64(stSize) + + if m.debug { + fmt.Fprintf(os.Stderr, "=+= sizeof(MetaFileHeader)=%d\n", mhsz) + fmt.Fprintf(os.Stderr, "=+= preambleLength=%d stSize=%d\n", preambleLength, stSize) + } + + // Compute total size + tlen := preambleLength + for i := 0; i < len(blobs); i++ { + tlen += uint64(len(blobs[i])) + } + + // Emit header + mh := coverage.MetaFileHeader{ + Magic: coverage.CovMetaMagic, + Version: coverage.MetaFileVersion, + TotalLength: tlen, + Entries: uint64(len(blobs)), + MetaFileHash: finalHash, + StrTabOffset: uint32(stOffset), + StrTabLength: stSize, + CMode: mode, + CGranularity: granularity, + } + var err error + if err = binary.Write(m.w, binary.LittleEndian, mh); err != nil { + return fmt.Errorf("error writing %s: %v", m.mfname, err) + } + + if m.debug { + fmt.Fprintf(os.Stderr, "=+= len(blobs) is %d\n", mh.Entries) + } + + // Emit package offsets section followed by package lengths section. + off := preambleLength + off2 := mhsz + buf := make([]byte, 8) + for _, blob := range blobs { + binary.LittleEndian.PutUint64(buf, off) + if _, err = m.w.Write(buf); err != nil { + return fmt.Errorf("error writing %s: %v", m.mfname, err) + } + if m.debug { + fmt.Fprintf(os.Stderr, "=+= pkg offset %d 0x%x\n", off, off) + } + off += uint64(len(blob)) + off2 += 8 + } + for _, blob := range blobs { + bl := uint64(len(blob)) + binary.LittleEndian.PutUint64(buf, bl) + if _, err = m.w.Write(buf); err != nil { + return fmt.Errorf("error writing %s: %v", m.mfname, err) + } + if m.debug { + fmt.Fprintf(os.Stderr, "=+= pkg len %d 0x%x\n", bl, bl) + } + off2 += 8 + } + + // Emit string table + if err = m.stab.Write(m.w); err != nil { + return err + } + + // Now emit blobs themselves. + for k, blob := range blobs { + if m.debug { + fmt.Fprintf(os.Stderr, "=+= writing blob %d len %d at off=%d hash %s\n", k, len(blob), off2, fmt.Sprintf("%x", md5.Sum(blob))) + } + if _, err = m.w.Write(blob); err != nil { + return fmt.Errorf("error writing %s: %v", m.mfname, err) + } + if m.debug { + fmt.Fprintf(os.Stderr, "=+= wrote package payload of %d bytes\n", + len(blob)) + } + off2 += uint64(len(blob)) + } + + // Flush writer, and we're done. + if err = m.w.Flush(); err != nil { + return fmt.Errorf("error writing %s: %v", m.mfname, err) + } + return nil +} diff --git a/src/internal/coverage/pkid.go b/src/internal/coverage/pkid.go new file mode 100644 index 0000000..372a9cb --- /dev/null +++ b/src/internal/coverage/pkid.go @@ -0,0 +1,81 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package coverage + +// Building the runtime package with coverage instrumentation enabled +// is tricky. For all other packages, you can be guaranteed that +// the package init function is run before any functions are executed, +// but this invariant is not maintained for packages such as "runtime", +// "internal/cpu", etc. To handle this, hard-code the package ID for +// the set of packages whose functions may be running before the +// init function of the package is complete. +// +// Hardcoding is unfortunate because it means that the tool that does +// coverage instrumentation has to keep a list of runtime packages, +// meaning that if someone makes changes to the pkg "runtime" +// dependencies, unexpected behavior will result for coverage builds. +// The coverage runtime will detect and report the unexpected +// behavior; look for an error of this form: +// +// internal error in coverage meta-data tracking: +// list of hard-coded runtime package IDs needs revising. +// registered list: +// slot: 0 path='internal/cpu' hard-coded id: 1 +// slot: 1 path='internal/goarch' hard-coded id: 2 +// slot: 2 path='runtime/internal/atomic' hard-coded id: 3 +// slot: 3 path='internal/goos' +// slot: 4 path='runtime/internal/sys' hard-coded id: 5 +// slot: 5 path='internal/abi' hard-coded id: 4 +// slot: 6 path='runtime/internal/math' hard-coded id: 6 +// slot: 7 path='internal/bytealg' hard-coded id: 7 +// slot: 8 path='internal/goexperiment' +// slot: 9 path='runtime/internal/syscall' hard-coded id: 8 +// slot: 10 path='runtime' hard-coded id: 9 +// fatal error: runtime.addCovMeta +// +// For the error above, the hard-coded list is missing "internal/goos" +// and "internal/goexperiment" ; the developer in question will need +// to copy the list above into "rtPkgs" below. +// +// Note: this strategy assumes that the list of dependencies of +// package runtime is fixed, and doesn't vary depending on OS/arch. If +// this were to be the case, we would need a table of some sort below +// as opposed to a fixed list. + +var rtPkgs = [...]string{ + "internal/cpu", + "internal/goarch", + "runtime/internal/atomic", + "internal/goos", + "internal/chacha8rand", + "runtime/internal/sys", + "internal/abi", + "runtime/internal/math", + "internal/bytealg", + "internal/goexperiment", + "runtime/internal/syscall", + "runtime", +} + +// Scoping note: the constants and apis in this file are internal +// only, not expected to ever be exposed outside of the runtime (unlike +// other coverage file formats and APIs, which will likely be shared +// at some point). + +// NotHardCoded is a package pseudo-ID indicating that a given package +// is not part of the runtime and doesn't require a hard-coded ID. +const NotHardCoded = -1 + +// HardCodedPkgID returns the hard-coded ID for the specified package +// path, or -1 if we don't use a hard-coded ID. Hard-coded IDs start +// at -2 and decrease as we go down the list. +func HardCodedPkgID(pkgpath string) int { + for k, p := range rtPkgs { + if p == pkgpath { + return (0 - k) - 2 + } + } + return NotHardCoded +} diff --git a/src/internal/coverage/pods/pods.go b/src/internal/coverage/pods/pods.go new file mode 100644 index 0000000..e08f82e --- /dev/null +++ b/src/internal/coverage/pods/pods.go @@ -0,0 +1,197 @@ +// 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 pods + +import ( + "fmt" + "internal/coverage" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" +) + +// Pod encapsulates a set of files emitted during the executions of a +// coverage-instrumented binary. Each pod contains a single meta-data +// file, and then 0 or more counter data files that refer to that +// meta-data file. Pods are intended to simplify processing of +// coverage output files in the case where we have several coverage +// output directories containing output files derived from more +// than one instrumented executable. In the case where the files that +// make up a pod are spread out across multiple directories, each +// element of the "Origins" field below will be populated with the +// index of the originating directory for the corresponding counter +// data file (within the slice of input dirs handed to CollectPods). +// The ProcessIDs field will be populated with the process ID of each +// data file in the CounterDataFiles slice. +type Pod struct { + MetaFile string + CounterDataFiles []string + Origins []int + ProcessIDs []int +} + +// CollectPods visits the files contained within the directories in +// the list 'dirs', collects any coverage-related files, partitions +// them into pods, and returns a list of the pods to the caller, along +// with an error if something went wrong during directory/file +// reading. +// +// CollectPods skips over any file that is not related to coverage +// (e.g. avoids looking at things that are not meta-data files or +// counter-data files). CollectPods also skips over 'orphaned' counter +// data files (e.g. counter data files for which we can't find the +// corresponding meta-data file). If "warn" is true, CollectPods will +// issue warnings to stderr when it encounters non-fatal problems (for +// orphans or a directory with no meta-data files). +func CollectPods(dirs []string, warn bool) ([]Pod, error) { + files := []string{} + dirIndices := []int{} + for k, dir := range dirs { + dents, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, e := range dents { + if e.IsDir() { + continue + } + files = append(files, filepath.Join(dir, e.Name())) + dirIndices = append(dirIndices, k) + } + } + return collectPodsImpl(files, dirIndices, warn), nil +} + +// CollectPodsFromFiles functions the same as "CollectPods" but +// operates on an explicit list of files instead of a directory. +func CollectPodsFromFiles(files []string, warn bool) []Pod { + return collectPodsImpl(files, nil, warn) +} + +type fileWithAnnotations struct { + file string + origin int + pid int +} + +type protoPod struct { + mf string + elements []fileWithAnnotations +} + +// collectPodsImpl examines the specified list of files and picks out +// subsets that correspond to coverage pods. The first stage in this +// process is collecting a set { M1, M2, ... MN } where each M_k is a +// distinct coverage meta-data file. We then create a single pod for +// each meta-data file M_k, then find all of the counter data files +// that refer to that meta-data file (recall that the counter data +// file name incorporates the meta-data hash), and add the counter +// data file to the appropriate pod. +// +// This process is complicated by the fact that we need to keep track +// of directory indices for counter data files. Here is an example to +// motivate: +// +// directory 1: +// +// M1 covmeta.9bbf1777f47b3fcacb05c38b035512d6 +// C1 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677673.1662138360208416486 +// C2 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677637.1662138359974441782 +// +// directory 2: +// +// M2 covmeta.9bbf1777f47b3fcacb05c38b035512d6 +// C3 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677445.1662138360208416480 +// C4 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677677.1662138359974441781 +// M3 covmeta.a723844208cea2ae80c63482c78b2245 +// C5 covcounters.a723844208cea2ae80c63482c78b2245.3677445.1662138360208416480 +// C6 covcounters.a723844208cea2ae80c63482c78b2245.1877677.1662138359974441781 +// +// In these two directories we have three meta-data files, but only +// two are distinct, meaning that we'll wind up with two pods. The +// first pod (with meta-file M1) will have four counter data files +// (C1, C2, C3, C4) and the second pod will have two counter data files +// (C5, C6). +func collectPodsImpl(files []string, dirIndices []int, warn bool) []Pod { + metaRE := regexp.MustCompile(fmt.Sprintf(`^%s\.(\S+)$`, coverage.MetaFilePref)) + mm := make(map[string]protoPod) + for _, f := range files { + base := filepath.Base(f) + if m := metaRE.FindStringSubmatch(base); m != nil { + tag := m[1] + // We need to allow for the possibility of duplicate + // meta-data files. If we hit this case, use the + // first encountered as the canonical version. + if _, ok := mm[tag]; !ok { + mm[tag] = protoPod{mf: f} + } + // FIXME: should probably check file length and hash here for + // the duplicate. + } + } + counterRE := regexp.MustCompile(fmt.Sprintf(coverage.CounterFileRegexp, coverage.CounterFilePref)) + for k, f := range files { + base := filepath.Base(f) + if m := counterRE.FindStringSubmatch(base); m != nil { + tag := m[1] // meta hash + pid, err := strconv.Atoi(m[2]) + if err != nil { + continue + } + if v, ok := mm[tag]; ok { + idx := -1 + if dirIndices != nil { + idx = dirIndices[k] + } + fo := fileWithAnnotations{file: f, origin: idx, pid: pid} + v.elements = append(v.elements, fo) + mm[tag] = v + } else { + if warn { + warning("skipping orphaned counter file: %s", f) + } + } + } + } + if len(mm) == 0 { + if warn { + warning("no coverage data files found") + } + return nil + } + pods := make([]Pod, 0, len(mm)) + for _, p := range mm { + sort.Slice(p.elements, func(i, j int) bool { + if p.elements[i].origin != p.elements[j].origin { + return p.elements[i].origin < p.elements[j].origin + } + return p.elements[i].file < p.elements[j].file + }) + pod := Pod{ + MetaFile: p.mf, + CounterDataFiles: make([]string, 0, len(p.elements)), + Origins: make([]int, 0, len(p.elements)), + ProcessIDs: make([]int, 0, len(p.elements)), + } + for _, e := range p.elements { + pod.CounterDataFiles = append(pod.CounterDataFiles, e.file) + pod.Origins = append(pod.Origins, e.origin) + pod.ProcessIDs = append(pod.ProcessIDs, e.pid) + } + pods = append(pods, pod) + } + sort.Slice(pods, func(i, j int) bool { + return pods[i].MetaFile < pods[j].MetaFile + }) + return pods +} + +func warning(s string, a ...interface{}) { + fmt.Fprintf(os.Stderr, "warning: ") + fmt.Fprintf(os.Stderr, s, a...) + fmt.Fprintf(os.Stderr, "\n") +} diff --git a/src/internal/coverage/pods/pods_test.go b/src/internal/coverage/pods/pods_test.go new file mode 100644 index 0000000..69c16e0 --- /dev/null +++ b/src/internal/coverage/pods/pods_test.go @@ -0,0 +1,142 @@ +// 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 pods_test + +import ( + "crypto/md5" + "fmt" + "internal/coverage" + "internal/coverage/pods" + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestPodCollection(t *testing.T) { + //testenv.MustHaveGoBuild(t) + + mkdir := func(d string, perm os.FileMode) string { + dp := filepath.Join(t.TempDir(), d) + if err := os.Mkdir(dp, perm); err != nil { + t.Fatal(err) + } + return dp + } + + mkfile := func(d string, fn string) string { + fp := filepath.Join(d, fn) + if err := os.WriteFile(fp, []byte("foo"), 0666); err != nil { + t.Fatal(err) + } + return fp + } + + mkmeta := func(dir string, tag string) string { + hash := md5.Sum([]byte(tag)) + fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, hash) + return mkfile(dir, fn) + } + + mkcounter := func(dir string, tag string, nt int, pid int) string { + hash := md5.Sum([]byte(tag)) + fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, hash, pid, nt) + return mkfile(dir, fn) + } + + trim := func(path string) string { + b := filepath.Base(path) + d := filepath.Dir(path) + db := filepath.Base(d) + return db + "/" + b + } + + podToString := func(p pods.Pod) string { + rv := trim(p.MetaFile) + " [\n" + for k, df := range p.CounterDataFiles { + rv += trim(df) + if p.Origins != nil { + rv += fmt.Sprintf(" o:%d", p.Origins[k]) + } + rv += "\n" + } + return rv + "]" + } + + // Create a couple of directories. + o1 := mkdir("o1", 0777) + o2 := mkdir("o2", 0777) + + // Add some random files (not coverage related) + mkfile(o1, "blah.txt") + mkfile(o1, "something.exe") + + // Add a meta-data file with two counter files to first dir. + mkmeta(o1, "m1") + mkcounter(o1, "m1", 1, 42) + mkcounter(o1, "m1", 2, 41) + mkcounter(o1, "m1", 2, 40) + + // Add a counter file with no associated meta file. + mkcounter(o1, "orphan", 9, 39) + + // Add a meta-data file with three counter files to second dir. + mkmeta(o2, "m2") + mkcounter(o2, "m2", 1, 38) + mkcounter(o2, "m2", 2, 37) + mkcounter(o2, "m2", 3, 36) + + // Add a duplicate of the first meta-file and a corresponding + // counter file to the second dir. This is intended to capture + // the scenario where we have two different runs of the same + // coverage-instrumented binary, but with the output files + // sent to separate directories. + mkmeta(o2, "m1") + mkcounter(o2, "m1", 11, 35) + + // Collect pods. + podlist, err := pods.CollectPods([]string{o1, o2}, true) + if err != nil { + t.Fatal(err) + } + + // Verify pods + if len(podlist) != 2 { + t.Fatalf("expected 2 pods got %d pods", len(podlist)) + } + + for k, p := range podlist { + t.Logf("%d: mf=%s\n", k, p.MetaFile) + } + + expected := []string{ + `o1/covmeta.ae7be26cdaa742ca148068d5ac90eaca [ +o1/covcounters.ae7be26cdaa742ca148068d5ac90eaca.40.2 o:0 +o1/covcounters.ae7be26cdaa742ca148068d5ac90eaca.41.2 o:0 +o1/covcounters.ae7be26cdaa742ca148068d5ac90eaca.42.1 o:0 +o2/covcounters.ae7be26cdaa742ca148068d5ac90eaca.35.11 o:1 +]`, + `o2/covmeta.aaf2f89992379705dac844c0a2a1d45f [ +o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.36.3 o:1 +o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.37.2 o:1 +o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.38.1 o:1 +]`, + } + for k, exp := range expected { + got := podToString(podlist[k]) + if exp != got { + t.Errorf("pod %d: expected:\n%s\ngot:\n%s", k, exp, got) + } + } + + // Check handling of bad/unreadable dir. + if runtime.GOOS == "linux" { + dbad := "/dev/null" + _, err = pods.CollectPods([]string{dbad}, true) + if err == nil { + t.Errorf("executed error due to unreadable dir") + } + } +} diff --git a/src/internal/coverage/rtcov/rtcov.go b/src/internal/coverage/rtcov/rtcov.go new file mode 100644 index 0000000..bbb93ac --- /dev/null +++ b/src/internal/coverage/rtcov/rtcov.go @@ -0,0 +1,34 @@ +// 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 rtcov + +// This package contains types whose structure is shared between +// the runtime package and the "runtime/coverage" package. + +// CovMetaBlob is a container for holding the meta-data symbol (an +// RODATA variable) for an instrumented Go package. Here "p" points to +// the symbol itself, "len" is the length of the sym in bytes, and +// "hash" is an md5sum for the sym computed by the compiler. When +// the init function for a coverage-instrumented package executes, it +// will make a call into the runtime which will create a covMetaBlob +// object for the package and chain it onto a global list. +type CovMetaBlob struct { + P *byte + Len uint32 + Hash [16]byte + PkgPath string + PkgID int + CounterMode uint8 // coverage.CounterMode + CounterGranularity uint8 // coverage.CounterGranularity +} + +// CovCounterBlob is a container for encapsulating a counter section +// (BSS variable) for an instrumented Go module. Here "counters" +// points to the counter payload and "len" is the number of uint32 +// entries in the section. +type CovCounterBlob struct { + Counters *uint32 + Len uint64 +} diff --git a/src/internal/coverage/slicereader/slicereader.go b/src/internal/coverage/slicereader/slicereader.go new file mode 100644 index 0000000..d9f2a7e --- /dev/null +++ b/src/internal/coverage/slicereader/slicereader.go @@ -0,0 +1,123 @@ +// Copyright 2021 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 slicereader + +import ( + "encoding/binary" + "fmt" + "io" + "unsafe" +) + +// This file contains the helper "SliceReader", a utility for +// reading values from a byte slice that may or may not be backed +// by a read-only mmap'd region. + +type Reader struct { + b []byte + readonly bool + off int64 +} + +func NewReader(b []byte, readonly bool) *Reader { + r := Reader{ + b: b, + readonly: readonly, + } + return &r +} + +func (r *Reader) Read(b []byte) (int, error) { + amt := len(b) + toread := r.b[r.off:] + if len(toread) < amt { + amt = len(toread) + } + copy(b, toread) + r.off += int64(amt) + return amt, nil +} + +func (r *Reader) Seek(offset int64, whence int) (ret int64, err error) { + switch whence { + case io.SeekStart: + if offset < 0 || offset > int64(len(r.b)) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", offset, len(r.b)) + } + r.off = offset + return offset, nil + case io.SeekCurrent: + newoff := r.off + offset + if newoff < 0 || newoff > int64(len(r.b)) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", newoff, len(r.b)) + } + r.off = newoff + return r.off, nil + case io.SeekEnd: + newoff := int64(len(r.b)) + offset + if newoff < 0 || newoff > int64(len(r.b)) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", newoff, len(r.b)) + } + r.off = newoff + return r.off, nil + } + // other modes are not supported + return 0, fmt.Errorf("unsupported seek mode %d", whence) +} + +func (r *Reader) Offset() int64 { + return r.off +} + +func (r *Reader) ReadUint8() uint8 { + rv := uint8(r.b[int(r.off)]) + r.off += 1 + return rv +} + +func (r *Reader) ReadUint32() uint32 { + end := int(r.off) + 4 + rv := binary.LittleEndian.Uint32(r.b[int(r.off):end:end]) + r.off += 4 + return rv +} + +func (r *Reader) ReadUint64() uint64 { + end := int(r.off) + 8 + rv := binary.LittleEndian.Uint64(r.b[int(r.off):end:end]) + r.off += 8 + return rv +} + +func (r *Reader) ReadULEB128() (value uint64) { + var shift uint + + for { + b := r.b[r.off] + r.off++ + value |= (uint64(b&0x7F) << shift) + if b&0x80 == 0 { + break + } + shift += 7 + } + return +} + +func (r *Reader) ReadString(len int64) string { + b := r.b[r.off : r.off+len] + r.off += len + if r.readonly { + return toString(b) // backed by RO memory, ok to make unsafe string + } + return string(b) +} + +func toString(b []byte) string { + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) +} diff --git a/src/internal/coverage/slicereader/slr_test.go b/src/internal/coverage/slicereader/slr_test.go new file mode 100644 index 0000000..461436d --- /dev/null +++ b/src/internal/coverage/slicereader/slr_test.go @@ -0,0 +1,95 @@ +// Copyright 2021 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 slicereader + +import ( + "encoding/binary" + "io" + "testing" +) + +func TestSliceReader(t *testing.T) { + b := []byte{} + + bt := make([]byte, 4) + e32 := uint32(1030507) + binary.LittleEndian.PutUint32(bt, e32) + b = append(b, bt...) + + bt = make([]byte, 8) + e64 := uint64(907050301) + binary.LittleEndian.PutUint64(bt, e64) + b = append(b, bt...) + + b = appendUleb128(b, uint(e32)) + b = appendUleb128(b, uint(e64)) + b = appendUleb128(b, 6) + s1 := "foobar" + s1b := []byte(s1) + b = append(b, s1b...) + b = appendUleb128(b, 9) + s2 := "bazbasher" + s2b := []byte(s2) + b = append(b, s2b...) + + readStr := func(slr *Reader) string { + len := slr.ReadULEB128() + return slr.ReadString(int64(len)) + } + + for i := 0; i < 2; i++ { + slr := NewReader(b, i == 0) + g32 := slr.ReadUint32() + if g32 != e32 { + t.Fatalf("slr.ReadUint32() got %d want %d", g32, e32) + } + g64 := slr.ReadUint64() + if g64 != e64 { + t.Fatalf("slr.ReadUint64() got %d want %d", g64, e64) + } + g32 = uint32(slr.ReadULEB128()) + if g32 != e32 { + t.Fatalf("slr.ReadULEB128() got %d want %d", g32, e32) + } + g64 = slr.ReadULEB128() + if g64 != e64 { + t.Fatalf("slr.ReadULEB128() got %d want %d", g64, e64) + } + gs1 := readStr(slr) + if gs1 != s1 { + t.Fatalf("readStr got %s want %s", gs1, s1) + } + gs2 := readStr(slr) + if gs2 != s2 { + t.Fatalf("readStr got %s want %s", gs2, s2) + } + if _, err := slr.Seek(4, io.SeekStart); err != nil { + t.Fatal(err) + } + off := slr.Offset() + if off != 4 { + t.Fatalf("Offset() returned %d wanted 4", off) + } + g64 = slr.ReadUint64() + if g64 != e64 { + t.Fatalf("post-seek slr.ReadUint64() got %d want %d", g64, e64) + } + } +} + +func appendUleb128(b []byte, v uint) []byte { + for { + c := uint8(v & 0x7f) + v >>= 7 + if v != 0 { + c |= 0x80 + } + b = append(b, c) + if c&0x80 == 0 { + break + } + } + return b +} diff --git a/src/internal/coverage/slicewriter/slicewriter.go b/src/internal/coverage/slicewriter/slicewriter.go new file mode 100644 index 0000000..460e9dc --- /dev/null +++ b/src/internal/coverage/slicewriter/slicewriter.go @@ -0,0 +1,80 @@ +// 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 slicewriter + +import ( + "fmt" + "io" +) + +// WriteSeeker is a helper object that implements the io.WriteSeeker +// interface. Clients can create a WriteSeeker, make a series of Write +// calls to add data to it (and possibly Seek calls to update +// previously written portions), then finally invoke BytesWritten() to +// get a pointer to the constructed byte slice. +type WriteSeeker struct { + payload []byte + off int64 +} + +func (sws *WriteSeeker) Write(p []byte) (n int, err error) { + amt := len(p) + towrite := sws.payload[sws.off:] + if len(towrite) < amt { + sws.payload = append(sws.payload, make([]byte, amt-len(towrite))...) + towrite = sws.payload[sws.off:] + } + copy(towrite, p) + sws.off += int64(amt) + return amt, nil +} + +// Seek repositions the read/write position of the WriteSeeker within +// its internally maintained slice. Note that it is not possible to +// expand the size of the slice using SEEK_SET; trying to seek outside +// the slice will result in an error. +func (sws *WriteSeeker) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + if sws.off != offset && (offset < 0 || offset > int64(len(sws.payload))) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", offset, len(sws.payload)) + } + sws.off = offset + return offset, nil + case io.SeekCurrent: + newoff := sws.off + offset + if newoff != sws.off && (newoff < 0 || newoff > int64(len(sws.payload))) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", newoff, len(sws.payload)) + } + sws.off += offset + return sws.off, nil + case io.SeekEnd: + newoff := int64(len(sws.payload)) + offset + if newoff != sws.off && (newoff < 0 || newoff > int64(len(sws.payload))) { + return 0, fmt.Errorf("invalid seek: new offset %d (out of range [0 %d]", newoff, len(sws.payload)) + } + sws.off = newoff + return sws.off, nil + } + // other modes not supported + return 0, fmt.Errorf("unsupported seek mode %d", whence) +} + +// BytesWritten returns the underlying byte slice for the WriteSeeker, +// containing the data written to it via Write/Seek calls. +func (sws *WriteSeeker) BytesWritten() []byte { + return sws.payload +} + +func (sws *WriteSeeker) Read(p []byte) (n int, err error) { + amt := len(p) + toread := sws.payload[sws.off:] + if len(toread) < amt { + amt = len(toread) + } + copy(p, toread) + sws.off += int64(amt) + return amt, nil +} diff --git a/src/internal/coverage/slicewriter/slw_test.go b/src/internal/coverage/slicewriter/slw_test.go new file mode 100644 index 0000000..9e26767 --- /dev/null +++ b/src/internal/coverage/slicewriter/slw_test.go @@ -0,0 +1,134 @@ +// 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 slicewriter + +import ( + "io" + "testing" +) + +func TestSliceWriter(t *testing.T) { + + sleq := func(t *testing.T, got []byte, want []byte) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("bad length got %d want %d", len(got), len(want)) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("bad read at %d got %d want %d", i, got[i], want[i]) + } + } + } + + wf := func(t *testing.T, ws *WriteSeeker, p []byte) { + t.Helper() + nw, werr := ws.Write(p) + if werr != nil { + t.Fatalf("unexpected write error: %v", werr) + } + if nw != len(p) { + t.Fatalf("wrong amount written want %d got %d", len(p), nw) + } + } + + rf := func(t *testing.T, ws *WriteSeeker, p []byte) { + t.Helper() + b := make([]byte, len(p)) + nr, rerr := ws.Read(b) + if rerr != nil { + t.Fatalf("unexpected read error: %v", rerr) + } + if nr != len(p) { + t.Fatalf("wrong amount read want %d got %d", len(p), nr) + } + sleq(t, b, p) + } + + sk := func(t *testing.T, ws *WriteSeeker, offset int64, whence int) int64 { + t.Helper() + off, err := ws.Seek(offset, whence) + if err != nil { + t.Fatalf("unexpected seek error: %v", err) + } + return off + } + + wp1 := []byte{1, 2} + ws := &WriteSeeker{} + + // write some stuff + wf(t, ws, wp1) + // check that BytesWritten returns what we wrote. + sleq(t, ws.BytesWritten(), wp1) + // offset is at end of slice, so reading should return zero bytes. + rf(t, ws, []byte{}) + + // write some more stuff + wp2 := []byte{7, 8, 9} + wf(t, ws, wp2) + // check that BytesWritten returns what we expect. + wpex := []byte{1, 2, 7, 8, 9} + sleq(t, ws.BytesWritten(), wpex) + rf(t, ws, []byte{}) + + // seeks and reads. + sk(t, ws, 1, io.SeekStart) + rf(t, ws, []byte{2, 7}) + sk(t, ws, -2, io.SeekCurrent) + rf(t, ws, []byte{2, 7}) + sk(t, ws, -4, io.SeekEnd) + rf(t, ws, []byte{2, 7}) + off := sk(t, ws, 0, io.SeekEnd) + sk(t, ws, off, io.SeekStart) + + // seek back and overwrite + sk(t, ws, 1, io.SeekStart) + wf(t, ws, []byte{9, 11}) + wpex = []byte{1, 9, 11, 8, 9} + sleq(t, ws.BytesWritten(), wpex) + + // seeks on empty writer. + ws2 := &WriteSeeker{} + sk(t, ws2, 0, io.SeekStart) + sk(t, ws2, 0, io.SeekCurrent) + sk(t, ws2, 0, io.SeekEnd) + + // check for seek errors. + _, err := ws.Seek(-1, io.SeekStart) + if err == nil { + t.Fatalf("expected error on invalid -1 seek") + } + _, err = ws.Seek(int64(len(ws.BytesWritten())+1), io.SeekStart) + if err == nil { + t.Fatalf("expected error on invalid %d seek", len(ws.BytesWritten())) + } + + ws.Seek(0, io.SeekStart) + _, err = ws.Seek(-1, io.SeekCurrent) + if err == nil { + t.Fatalf("expected error on invalid -1 seek") + } + _, err = ws.Seek(int64(len(ws.BytesWritten())+1), io.SeekCurrent) + if err == nil { + t.Fatalf("expected error on invalid %d seek", len(ws.BytesWritten())) + } + + _, err = ws.Seek(1, io.SeekEnd) + if err == nil { + t.Fatalf("expected error on invalid 1 seek") + } + bsamt := int64(-1*len(ws.BytesWritten()) - 1) + _, err = ws.Seek(bsamt, io.SeekEnd) + if err == nil { + t.Fatalf("expected error on invalid %d seek", bsamt) + } + + // bad seek mode + _, err = ws.Seek(-1, io.SeekStart+9) + if err == nil { + t.Fatalf("expected error on invalid seek mode") + } +} diff --git a/src/internal/coverage/stringtab/stringtab.go b/src/internal/coverage/stringtab/stringtab.go new file mode 100644 index 0000000..156c8ad --- /dev/null +++ b/src/internal/coverage/stringtab/stringtab.go @@ -0,0 +1,139 @@ +// 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 stringtab + +import ( + "fmt" + "internal/coverage/slicereader" + "internal/coverage/uleb128" + "io" +) + +// This package implements string table writer and reader utilities, +// for use in emitting and reading/decoding coverage meta-data and +// counter-data files. + +// Writer implements a string table writing utility. +type Writer struct { + stab map[string]uint32 + strs []string + tmp []byte + frozen bool +} + +// InitWriter initializes a stringtab.Writer. +func (stw *Writer) InitWriter() { + stw.stab = make(map[string]uint32) + stw.tmp = make([]byte, 64) +} + +// Nentries returns the number of strings interned so far. +func (stw *Writer) Nentries() uint32 { + return uint32(len(stw.strs)) +} + +// Lookup looks up string 's' in the writer's table, adding +// a new entry if need be, and returning an index into the table. +func (stw *Writer) Lookup(s string) uint32 { + if idx, ok := stw.stab[s]; ok { + return idx + } + if stw.frozen { + panic("internal error: string table previously frozen") + } + idx := uint32(len(stw.strs)) + stw.stab[s] = idx + stw.strs = append(stw.strs, s) + return idx +} + +// Size computes the memory in bytes needed for the serialized +// version of a stringtab.Writer. +func (stw *Writer) Size() uint32 { + rval := uint32(0) + stw.tmp = stw.tmp[:0] + stw.tmp = uleb128.AppendUleb128(stw.tmp, uint(len(stw.strs))) + rval += uint32(len(stw.tmp)) + for _, s := range stw.strs { + stw.tmp = stw.tmp[:0] + slen := uint(len(s)) + stw.tmp = uleb128.AppendUleb128(stw.tmp, slen) + rval += uint32(len(stw.tmp)) + uint32(slen) + } + return rval +} + +// Write writes the string table in serialized form to the specified +// io.Writer. +func (stw *Writer) Write(w io.Writer) error { + wr128 := func(v uint) error { + stw.tmp = stw.tmp[:0] + stw.tmp = uleb128.AppendUleb128(stw.tmp, v) + if nw, err := w.Write(stw.tmp); err != nil { + return fmt.Errorf("writing string table: %v", err) + } else if nw != len(stw.tmp) { + return fmt.Errorf("short write emitting stringtab uleb") + } + return nil + } + if err := wr128(uint(len(stw.strs))); err != nil { + return err + } + for _, s := range stw.strs { + if err := wr128(uint(len(s))); err != nil { + return err + } + if nw, err := w.Write([]byte(s)); err != nil { + return fmt.Errorf("writing string table: %v", err) + } else if nw != len([]byte(s)) { + return fmt.Errorf("short write emitting stringtab") + } + } + return nil +} + +// Freeze sends a signal to the writer that no more additions are +// allowed, only lookups of existing strings (if a lookup triggers +// addition, a panic will result). Useful as a mechanism for +// "finalizing" a string table prior to writing it out. +func (stw *Writer) Freeze() { + stw.frozen = true +} + +// Reader is a helper for reading a string table previously +// serialized by a Writer.Write call. +type Reader struct { + r *slicereader.Reader + strs []string +} + +// NewReader creates a stringtab.Reader to read the contents +// of a string table from 'r'. +func NewReader(r *slicereader.Reader) *Reader { + str := &Reader{ + r: r, + } + return str +} + +// Read reads/decodes a string table using the reader provided. +func (str *Reader) Read() { + numEntries := int(str.r.ReadULEB128()) + str.strs = make([]string, 0, numEntries) + for idx := 0; idx < numEntries; idx++ { + slen := str.r.ReadULEB128() + str.strs = append(str.strs, str.r.ReadString(int64(slen))) + } +} + +// Entries returns the number of decoded entries in a string table. +func (str *Reader) Entries() int { + return len(str.strs) +} + +// Get returns string 'idx' within the string table. +func (str *Reader) Get(idx uint32) string { + return str.strs[idx] +} diff --git a/src/internal/coverage/test/counter_test.go b/src/internal/coverage/test/counter_test.go new file mode 100644 index 0000000..e29baed --- /dev/null +++ b/src/internal/coverage/test/counter_test.go @@ -0,0 +1,237 @@ +// Copyright 2021 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 test + +import ( + "fmt" + "internal/coverage" + "internal/coverage/decodecounter" + "internal/coverage/encodecounter" + "io" + "os" + "path/filepath" + "testing" +) + +type ctrVis struct { + funcs []decodecounter.FuncPayload +} + +func (v *ctrVis) VisitFuncs(f encodecounter.CounterVisitorFn) error { + for _, fn := range v.funcs { + if err := f(fn.PkgIdx, fn.FuncIdx, fn.Counters); err != nil { + return err + } + } + return nil +} + +func mkfunc(p uint32, f uint32, c []uint32) decodecounter.FuncPayload { + return decodecounter.FuncPayload{ + PkgIdx: p, + FuncIdx: f, + Counters: c, + } +} + +func TestCounterDataWriterReader(t *testing.T) { + flavors := []coverage.CounterFlavor{ + coverage.CtrRaw, + coverage.CtrULeb128, + } + + isDead := func(fp decodecounter.FuncPayload) bool { + for _, v := range fp.Counters { + if v != 0 { + return false + } + } + return true + } + + funcs := []decodecounter.FuncPayload{ + mkfunc(0, 0, []uint32{13, 14, 15}), + mkfunc(0, 1, []uint32{16, 17}), + mkfunc(1, 0, []uint32{18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 976543, 7}), + } + writeVisitor := &ctrVis{funcs: funcs} + + for kf, flav := range flavors { + + t.Logf("testing flavor %d\n", flav) + + // Open a counter data file in preparation for emitting data. + d := t.TempDir() + cfpath := filepath.Join(d, fmt.Sprintf("covcounters.hash.0.%d", kf)) + of, err := os.OpenFile(cfpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + t.Fatalf("opening covcounters: %v", err) + } + + // Perform the encode and write. + cdfw := encodecounter.NewCoverageDataWriter(of, flav) + if cdfw == nil { + t.Fatalf("NewCoverageDataWriter failed") + } + finalHash := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0} + args := map[string]string{"argc": "3", "argv0": "arg0", "argv1": "arg1", "argv2": "arg_________2"} + if err := cdfw.Write(finalHash, args, writeVisitor); err != nil { + t.Fatalf("counter file Write failed: %v", err) + } + if err := of.Close(); err != nil { + t.Fatalf("closing covcounters: %v", err) + } + cdfw = nil + + // Decode the same file. + var cdr *decodecounter.CounterDataReader + inf, err := os.Open(cfpath) + defer func() { + if err := inf.Close(); err != nil { + t.Fatalf("close failed with: %v", err) + } + }() + + if err != nil { + t.Fatalf("reopening covcounters file: %v", err) + } + if cdr, err = decodecounter.NewCounterDataReader(cfpath, inf); err != nil { + t.Fatalf("opening covcounters for read: %v", err) + } + decodedArgs := cdr.OsArgs() + aWant := "[arg0 arg1 arg_________2]" + aGot := fmt.Sprintf("%+v", decodedArgs) + if aWant != aGot { + t.Errorf("reading decoded args, got %s want %s", aGot, aWant) + } + for i := range funcs { + if isDead(funcs[i]) { + continue + } + var fp decodecounter.FuncPayload + if ok, err := cdr.NextFunc(&fp); err != nil { + t.Fatalf("reading func %d: %v", i, err) + } else if !ok { + t.Fatalf("reading func %d: bad return", i) + } + got := fmt.Sprintf("%+v", fp) + want := fmt.Sprintf("%+v", funcs[i]) + if got != want { + t.Errorf("cdr.NextFunc iter %d\ngot %+v\nwant %+v", i, got, want) + } + } + var dummy decodecounter.FuncPayload + if ok, err := cdr.NextFunc(&dummy); err != nil { + t.Fatalf("reading func after loop: %v", err) + } else if ok { + t.Fatalf("reading func after loop: expected EOF") + } + } +} + +func TestCounterDataAppendSegment(t *testing.T) { + d := t.TempDir() + cfpath := filepath.Join(d, "covcounters.hash2.0") + of, err := os.OpenFile(cfpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + t.Fatalf("opening covcounters: %v", err) + } + + const numSegments = 2 + + // Write a counter with with multiple segments. + args := map[string]string{"argc": "1", "argv0": "prog.exe"} + allfuncs := [][]decodecounter.FuncPayload{} + ctrs := []uint32{} + q := uint32(0) + var cdfw *encodecounter.CoverageDataWriter + for idx := 0; idx < numSegments; idx++ { + args[fmt.Sprintf("seg%d", idx)] = "x" + q += 7 + ctrs = append(ctrs, q) + funcs := []decodecounter.FuncPayload{} + for k := 0; k < idx+1; k++ { + c := make([]uint32, len(ctrs)) + copy(c, ctrs) + funcs = append(funcs, mkfunc(uint32(idx), uint32(k), c)) + } + allfuncs = append(allfuncs, funcs) + + writeVisitor := &ctrVis{funcs: funcs} + + if idx == 0 { + // Perform the encode and write. + cdfw = encodecounter.NewCoverageDataWriter(of, coverage.CtrRaw) + if cdfw == nil { + t.Fatalf("NewCoverageDataWriter failed") + } + finalHash := [16]byte{1, 2} + if err := cdfw.Write(finalHash, args, writeVisitor); err != nil { + t.Fatalf("counter file Write failed: %v", err) + } + } else { + if err := cdfw.AppendSegment(args, writeVisitor); err != nil { + t.Fatalf("counter file AppendSegment failed: %v", err) + } + } + } + if err := of.Close(); err != nil { + t.Fatalf("closing covcounters: %v", err) + } + + // Read the result file. + var cdr *decodecounter.CounterDataReader + inf, err := os.Open(cfpath) + defer func() { + if err := inf.Close(); err != nil { + t.Fatalf("close failed with: %v", err) + } + }() + + if err != nil { + t.Fatalf("reopening covcounters file: %v", err) + } + if cdr, err = decodecounter.NewCounterDataReader(cfpath, inf); err != nil { + t.Fatalf("opening covcounters for read: %v", err) + } + ns := cdr.NumSegments() + if ns != numSegments { + t.Fatalf("got %d segments want %d", ns, numSegments) + } + if len(allfuncs) != numSegments { + t.Fatalf("expected %d got %d", numSegments, len(allfuncs)) + } + + for sidx := 0; sidx < int(ns); sidx++ { + if off, err := inf.Seek(0, io.SeekCurrent); err != nil { + t.Fatalf("Seek failed: %v", err) + } else { + t.Logf("sidx=%d off=%d\n", sidx, off) + } + + if sidx != 0 { + if ok, err := cdr.BeginNextSegment(); err != nil { + t.Fatalf("BeginNextSegment failed: %v", err) + } else if !ok { + t.Fatalf("BeginNextSegment return %v on iter %d", + ok, sidx) + } + } + funcs := allfuncs[sidx] + for i := range funcs { + var fp decodecounter.FuncPayload + if ok, err := cdr.NextFunc(&fp); err != nil { + t.Fatalf("reading func %d: %v", i, err) + } else if !ok { + t.Fatalf("reading func %d: bad return", i) + } + got := fmt.Sprintf("%+v", fp) + want := fmt.Sprintf("%+v", funcs[i]) + if got != want { + t.Errorf("cdr.NextFunc iter %d\ngot %+v\nwant %+v", i, got, want) + } + } + } +} diff --git a/src/internal/coverage/test/roundtrip_test.go b/src/internal/coverage/test/roundtrip_test.go new file mode 100644 index 0000000..614f56e --- /dev/null +++ b/src/internal/coverage/test/roundtrip_test.go @@ -0,0 +1,331 @@ +// Copyright 2021 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 test + +import ( + "fmt" + "internal/coverage" + "internal/coverage/decodemeta" + "internal/coverage/encodemeta" + "internal/coverage/slicewriter" + "io" + "os" + "path/filepath" + "testing" +) + +func cmpFuncDesc(want, got coverage.FuncDesc) string { + swant := fmt.Sprintf("%+v", want) + sgot := fmt.Sprintf("%+v", got) + if swant == sgot { + return "" + } + return fmt.Sprintf("wanted %q got %q", swant, sgot) +} + +func TestMetaDataEmptyPackage(t *testing.T) { + // Make sure that encoding/decoding works properly with packages + // that don't actually have any functions. + p := "empty/package" + pn := "package" + mp := "m" + b, err := encodemeta.NewCoverageMetaDataBuilder(p, pn, mp) + if err != nil { + t.Fatalf("making builder: %v", err) + } + drws := &slicewriter.WriteSeeker{} + b.Emit(drws) + drws.Seek(0, io.SeekStart) + dec, err := decodemeta.NewCoverageMetaDataDecoder(drws.BytesWritten(), false) + if err != nil { + t.Fatalf("making decoder: %v", err) + } + nf := dec.NumFuncs() + if nf != 0 { + t.Errorf("dec.NumFuncs(): got %d want %d", nf, 0) + } + pp := dec.PackagePath() + if pp != p { + t.Errorf("dec.PackagePath(): got %s want %s", pp, p) + } + ppn := dec.PackageName() + if ppn != pn { + t.Errorf("dec.PackageName(): got %s want %s", ppn, pn) + } + pmp := dec.ModulePath() + if pmp != mp { + t.Errorf("dec.ModulePath(): got %s want %s", pmp, mp) + } +} + +func TestMetaDataEncoderDecoder(t *testing.T) { + // Test encode path. + pp := "foo/bar/pkg" + pn := "pkg" + mp := "barmod" + b, err := encodemeta.NewCoverageMetaDataBuilder(pp, pn, mp) + if err != nil { + t.Fatalf("making builder: %v", err) + } + f1 := coverage.FuncDesc{ + Funcname: "func", + Srcfile: "foo.go", + Units: []coverage.CoverableUnit{ + coverage.CoverableUnit{StLine: 1, StCol: 2, EnLine: 3, EnCol: 4, NxStmts: 5}, + coverage.CoverableUnit{StLine: 6, StCol: 7, EnLine: 8, EnCol: 9, NxStmts: 10}, + }, + } + idx := b.AddFunc(f1) + if idx != 0 { + t.Errorf("b.AddFunc(f1) got %d want %d", idx, 0) + } + + f2 := coverage.FuncDesc{ + Funcname: "xfunc", + Srcfile: "bar.go", + Units: []coverage.CoverableUnit{ + coverage.CoverableUnit{StLine: 1, StCol: 2, EnLine: 3, EnCol: 4, NxStmts: 5}, + coverage.CoverableUnit{StLine: 6, StCol: 7, EnLine: 8, EnCol: 9, NxStmts: 10}, + coverage.CoverableUnit{StLine: 11, StCol: 12, EnLine: 13, EnCol: 14, NxStmts: 15}, + }, + } + idx = b.AddFunc(f2) + if idx != 1 { + t.Errorf("b.AddFunc(f2) got %d want %d", idx, 0) + } + + // Emit into a writer. + drws := &slicewriter.WriteSeeker{} + b.Emit(drws) + + // Test decode path. + drws.Seek(0, io.SeekStart) + dec, err := decodemeta.NewCoverageMetaDataDecoder(drws.BytesWritten(), false) + if err != nil { + t.Fatalf("NewCoverageMetaDataDecoder error: %v", err) + } + nf := dec.NumFuncs() + if nf != 2 { + t.Errorf("dec.NumFuncs(): got %d want %d", nf, 2) + } + + gotpp := dec.PackagePath() + if gotpp != pp { + t.Errorf("packagepath: got %s want %s", gotpp, pp) + } + gotpn := dec.PackageName() + if gotpn != pn { + t.Errorf("packagename: got %s want %s", gotpn, pn) + } + + cases := []coverage.FuncDesc{f1, f2} + for i := uint32(0); i < uint32(len(cases)); i++ { + var fn coverage.FuncDesc + if err := dec.ReadFunc(i, &fn); err != nil { + t.Fatalf("err reading function %d: %v", i, err) + } + res := cmpFuncDesc(cases[i], fn) + if res != "" { + t.Errorf("ReadFunc(%d): %s", i, res) + } + } +} + +func createFuncs(i int) []coverage.FuncDesc { + res := []coverage.FuncDesc{} + lc := uint32(1) + for fi := 0; fi < i+1; fi++ { + units := []coverage.CoverableUnit{} + for ui := 0; ui < (fi+1)*(i+1); ui++ { + units = append(units, + coverage.CoverableUnit{StLine: lc, StCol: lc + 1, + EnLine: lc + 2, EnCol: lc + 3, NxStmts: lc + 4, + }) + lc += 5 + } + f := coverage.FuncDesc{ + Funcname: fmt.Sprintf("func_%d_%d", i, fi), + Srcfile: fmt.Sprintf("foo_%d.go", i), + Units: units, + } + res = append(res, f) + } + return res +} + +func createBlob(t *testing.T, i int) []byte { + nomodule := "" + b, err := encodemeta.NewCoverageMetaDataBuilder("foo/pkg", "pkg", nomodule) + if err != nil { + t.Fatalf("making builder: %v", err) + } + + funcs := createFuncs(i) + for _, f := range funcs { + b.AddFunc(f) + } + drws := &slicewriter.WriteSeeker{} + b.Emit(drws) + return drws.BytesWritten() +} + +func createMetaDataBlobs(t *testing.T, nb int) [][]byte { + res := [][]byte{} + for i := 0; i < nb; i++ { + res = append(res, createBlob(t, i)) + } + return res +} + +func TestMetaDataWriterReader(t *testing.T) { + d := t.TempDir() + + // Emit a meta-file... + mfpath := filepath.Join(d, "covmeta.hash.0") + of, err := os.OpenFile(mfpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + t.Fatalf("opening covmeta: %v", err) + } + //t.Logf("meta-file path is %s", mfpath) + blobs := createMetaDataBlobs(t, 7) + gran := coverage.CtrGranularityPerBlock + mfw := encodemeta.NewCoverageMetaFileWriter(mfpath, of) + finalHash := [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + err = mfw.Write(finalHash, blobs, coverage.CtrModeAtomic, gran) + if err != nil { + t.Fatalf("writing meta-file: %v", err) + } + if err = of.Close(); err != nil { + t.Fatalf("closing meta-file: %v", err) + } + + // ... then read it back in, first time without setting fileView, + // second time setting it. + for k := 0; k < 2; k++ { + var fileView []byte + + inf, err := os.Open(mfpath) + if err != nil { + t.Fatalf("open() on meta-file: %v", err) + } + + if k != 0 { + // Use fileview to exercise different paths in reader. + fi, err := os.Stat(mfpath) + if err != nil { + t.Fatalf("stat() on meta-file: %v", err) + } + fileView = make([]byte, fi.Size()) + if _, err := inf.Read(fileView); err != nil { + t.Fatalf("read() on meta-file: %v", err) + } + if _, err := inf.Seek(int64(0), io.SeekStart); err != nil { + t.Fatalf("seek() on meta-file: %v", err) + } + } + + mfr, err := decodemeta.NewCoverageMetaFileReader(inf, fileView) + if err != nil { + t.Fatalf("k=%d NewCoverageMetaFileReader failed with: %v", k, err) + } + np := mfr.NumPackages() + if np != 7 { + t.Fatalf("k=%d wanted 7 packages got %d", k, np) + } + md := mfr.CounterMode() + wmd := coverage.CtrModeAtomic + if md != wmd { + t.Fatalf("k=%d wanted mode %d got %d", k, wmd, md) + } + gran := mfr.CounterGranularity() + wgran := coverage.CtrGranularityPerBlock + if gran != wgran { + t.Fatalf("k=%d wanted gran %d got %d", k, wgran, gran) + } + + payload := []byte{} + for pi := 0; pi < int(np); pi++ { + var pd *decodemeta.CoverageMetaDataDecoder + var err error + pd, payload, err = mfr.GetPackageDecoder(uint32(pi), payload) + if err != nil { + t.Fatalf("GetPackageDecoder(%d) failed with: %v", pi, err) + } + efuncs := createFuncs(pi) + nf := pd.NumFuncs() + if len(efuncs) != int(nf) { + t.Fatalf("decoding pk %d wanted %d funcs got %d", + pi, len(efuncs), nf) + } + var f coverage.FuncDesc + for fi := 0; fi < int(nf); fi++ { + if err := pd.ReadFunc(uint32(fi), &f); err != nil { + t.Fatalf("ReadFunc(%d) pk %d got error %v", + fi, pi, err) + } + res := cmpFuncDesc(efuncs[fi], f) + if res != "" { + t.Errorf("ReadFunc(%d) pk %d: %s", fi, pi, res) + } + } + } + inf.Close() + } +} + +func TestMetaDataDecodeLitFlagIssue57942(t *testing.T) { + + // Encode a package with a few functions. The funcs alternate + // between regular functions and function literals. + pp := "foo/bar/pkg" + pn := "pkg" + mp := "barmod" + b, err := encodemeta.NewCoverageMetaDataBuilder(pp, pn, mp) + if err != nil { + t.Fatalf("making builder: %v", err) + } + const NF = 6 + const NCU = 1 + ln := uint32(10) + wantfds := []coverage.FuncDesc{} + for fi := uint32(0); fi < NF; fi++ { + fis := fmt.Sprintf("%d", fi) + fd := coverage.FuncDesc{ + Funcname: "func" + fis, + Srcfile: "foo" + fis + ".go", + Units: []coverage.CoverableUnit{ + coverage.CoverableUnit{StLine: ln + 1, StCol: 2, EnLine: ln + 3, EnCol: 4, NxStmts: fi + 2}, + }, + Lit: (fi % 2) == 0, + } + wantfds = append(wantfds, fd) + b.AddFunc(fd) + } + + // Emit into a writer. + drws := &slicewriter.WriteSeeker{} + b.Emit(drws) + + // Decode the result. + drws.Seek(0, io.SeekStart) + dec, err := decodemeta.NewCoverageMetaDataDecoder(drws.BytesWritten(), false) + if err != nil { + t.Fatalf("making decoder: %v", err) + } + nf := dec.NumFuncs() + if nf != NF { + t.Fatalf("decoder number of functions: got %d want %d", nf, NF) + } + var fn coverage.FuncDesc + for i := uint32(0); i < uint32(NF); i++ { + if err := dec.ReadFunc(i, &fn); err != nil { + t.Fatalf("err reading function %d: %v", i, err) + } + res := cmpFuncDesc(wantfds[i], fn) + if res != "" { + t.Errorf("ReadFunc(%d): %s", i, res) + } + } +} diff --git a/src/internal/coverage/uleb128/uleb128.go b/src/internal/coverage/uleb128/uleb128.go new file mode 100644 index 0000000..e5cd92a --- /dev/null +++ b/src/internal/coverage/uleb128/uleb128.go @@ -0,0 +1,20 @@ +// Copyright 2021 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 uleb128 + +func AppendUleb128(b []byte, v uint) []byte { + for { + c := uint8(v & 0x7f) + v >>= 7 + if v != 0 { + c |= 0x80 + } + b = append(b, c) + if c&0x80 == 0 { + break + } + } + return b +} |