From ccd992355df7192993c666236047820244914598 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:19:13 +0200 Subject: Adding upstream version 1.21.8. Signed-off-by: Daniel Baumann --- src/cmd/cover/cfg_test.go | 188 +++++ src/cmd/cover/cover.go | 1159 ++++++++++++++++++++++++++++ src/cmd/cover/cover_test.go | 649 ++++++++++++++++ src/cmd/cover/doc.go | 32 + src/cmd/cover/export_test.go | 7 + src/cmd/cover/func.go | 248 ++++++ src/cmd/cover/html.go | 306 ++++++++ src/cmd/cover/pkgname_test.go | 31 + src/cmd/cover/testdata/directives.go | 40 + src/cmd/cover/testdata/html/html.go | 30 + src/cmd/cover/testdata/html/html.golden | 18 + src/cmd/cover/testdata/html/html_test.go | 8 + src/cmd/cover/testdata/main.go | 116 +++ src/cmd/cover/testdata/p.go | 27 + src/cmd/cover/testdata/pkgcfg/a/a.go | 28 + src/cmd/cover/testdata/pkgcfg/a/a2.go | 8 + src/cmd/cover/testdata/pkgcfg/a/a_test.go | 14 + src/cmd/cover/testdata/pkgcfg/b/b.go | 10 + src/cmd/cover/testdata/pkgcfg/b/b_test.go | 9 + src/cmd/cover/testdata/pkgcfg/main/main.go | 15 + src/cmd/cover/testdata/profile.cov | 5 + src/cmd/cover/testdata/test.go | 300 +++++++ 22 files changed, 3248 insertions(+) create mode 100644 src/cmd/cover/cfg_test.go create mode 100644 src/cmd/cover/cover.go create mode 100644 src/cmd/cover/cover_test.go create mode 100644 src/cmd/cover/doc.go create mode 100644 src/cmd/cover/export_test.go create mode 100644 src/cmd/cover/func.go create mode 100644 src/cmd/cover/html.go create mode 100644 src/cmd/cover/pkgname_test.go create mode 100644 src/cmd/cover/testdata/directives.go create mode 100644 src/cmd/cover/testdata/html/html.go create mode 100644 src/cmd/cover/testdata/html/html.golden create mode 100644 src/cmd/cover/testdata/html/html_test.go create mode 100644 src/cmd/cover/testdata/main.go create mode 100644 src/cmd/cover/testdata/p.go create mode 100644 src/cmd/cover/testdata/pkgcfg/a/a.go create mode 100644 src/cmd/cover/testdata/pkgcfg/a/a2.go create mode 100644 src/cmd/cover/testdata/pkgcfg/a/a_test.go create mode 100644 src/cmd/cover/testdata/pkgcfg/b/b.go create mode 100644 src/cmd/cover/testdata/pkgcfg/b/b_test.go create mode 100644 src/cmd/cover/testdata/pkgcfg/main/main.go create mode 100644 src/cmd/cover/testdata/profile.cov create mode 100644 src/cmd/cover/testdata/test.go (limited to 'src/cmd/cover') diff --git a/src/cmd/cover/cfg_test.go b/src/cmd/cover/cfg_test.go new file mode 100644 index 0000000..6782ec8 --- /dev/null +++ b/src/cmd/cover/cfg_test.go @@ -0,0 +1,188 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main_test + +import ( + "encoding/json" + "fmt" + "internal/coverage" + "internal/testenv" + "os" + "path/filepath" + "strings" + "testing" +) + +func writeFile(t *testing.T, path string, contents []byte) { + if err := os.WriteFile(path, contents, 0666); err != nil { + t.Fatalf("os.WriteFile(%s) failed: %v", path, err) + } +} + +func writePkgConfig(t *testing.T, outdir, tag, ppath, pname string, gran string) string { + incfg := filepath.Join(outdir, tag+"incfg.txt") + outcfg := filepath.Join(outdir, "outcfg.txt") + p := coverage.CoverPkgConfig{ + PkgPath: ppath, + PkgName: pname, + Granularity: gran, + OutConfig: outcfg, + } + data, err := json.Marshal(p) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + writeFile(t, incfg, data) + return incfg +} + +func writeOutFileList(t *testing.T, infiles []string, outdir, tag string) ([]string, string) { + outfilelist := filepath.Join(outdir, tag+"outfilelist.txt") + var sb strings.Builder + cv := filepath.Join(outdir, "covervars.go") + outfs := []string{cv} + fmt.Fprintf(&sb, "%s\n", cv) + for _, inf := range infiles { + base := filepath.Base(inf) + of := filepath.Join(outdir, tag+".cov."+base) + outfs = append(outfs, of) + fmt.Fprintf(&sb, "%s\n", of) + } + if err := os.WriteFile(outfilelist, []byte(sb.String()), 0666); err != nil { + t.Fatalf("writing %s: %v", outfilelist, err) + } + return outfs, outfilelist +} + +func runPkgCover(t *testing.T, outdir string, tag string, incfg string, mode string, infiles []string, errExpected bool) ([]string, string, string) { + // Write the pkgcfg file. + outcfg := filepath.Join(outdir, "outcfg.txt") + + // Form up the arguments and run the tool. + outfiles, outfilelist := writeOutFileList(t, infiles, outdir, tag) + args := []string{"-pkgcfg", incfg, "-mode=" + mode, "-var=var" + tag, "-outfilelist", outfilelist} + args = append(args, infiles...) + cmd := testenv.Command(t, testcover(t), args...) + if errExpected { + errmsg := runExpectingError(cmd, t) + return nil, "", errmsg + } else { + run(cmd, t) + return outfiles, outcfg, "" + } +} + +// Set to true when debugging unit test (to inspect debris, etc). +// Note that this functionality does not work on windows. +const debugWorkDir = false + +func TestCoverWithCfg(t *testing.T) { + testenv.MustHaveGoRun(t) + + t.Parallel() + + // Subdir in testdata that has our input files of interest. + tpath := filepath.Join("testdata", "pkgcfg") + + // Helper to collect input paths (go files) for a subdir in 'pkgcfg' + pfiles := func(subdir string) []string { + de, err := os.ReadDir(filepath.Join(tpath, subdir)) + if err != nil { + t.Fatalf("reading subdir %s: %v", subdir, err) + } + paths := []string{} + for _, e := range de { + if !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") { + continue + } + paths = append(paths, filepath.Join(tpath, subdir, e.Name())) + } + return paths + } + + dir := tempDir(t) + if debugWorkDir { + dir = "/tmp/qqq" + os.RemoveAll(dir) + os.Mkdir(dir, 0777) + } + instdira := filepath.Join(dir, "insta") + if err := os.Mkdir(instdira, 0777); err != nil { + t.Fatal(err) + } + + scenarios := []struct { + mode, gran string + }{ + { + mode: "count", + gran: "perblock", + }, + { + mode: "set", + gran: "perfunc", + }, + { + mode: "regonly", + gran: "perblock", + }, + } + + var incfg string + for _, scenario := range scenarios { + // Instrument package "a", producing a set of instrumented output + // files and an 'output config' file to pass on to the compiler. + ppath := "cfg/a" + pname := "a" + mode := scenario.mode + gran := scenario.gran + tag := mode + "_" + gran + incfg = writePkgConfig(t, instdira, tag, ppath, pname, gran) + ofs, outcfg, _ := runPkgCover(t, instdira, tag, incfg, mode, + pfiles("a"), false) + t.Logf("outfiles: %+v\n", ofs) + + // Run the compiler on the files to make sure the result is + // buildable. + bargs := []string{"tool", "compile", "-p", "a", "-coveragecfg", outcfg} + bargs = append(bargs, ofs...) + cmd := testenv.Command(t, testenv.GoToolPath(t), bargs...) + cmd.Dir = instdira + run(cmd, t) + } + + // Do some error testing to ensure that various bad options and + // combinations are properly rejected. + + // Expect error if config file inaccessible/unreadable. + mode := "atomic" + errExpected := true + tag := "errors" + _, _, errmsg := runPkgCover(t, instdira, tag, "/not/a/file", mode, + pfiles("a"), errExpected) + want := "error reading pkgconfig file" + if !strings.Contains(errmsg, want) { + t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg) + } + + // Expect err if config file contains unknown stuff. + t.Logf("mangling in config") + writeFile(t, incfg, []byte("blah=foo\n")) + _, _, errmsg = runPkgCover(t, instdira, tag, incfg, mode, + pfiles("a"), errExpected) + want = "error reading pkgconfig file" + if !strings.Contains(errmsg, want) { + t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg) + } + + // Expect error on empty config file. + t.Logf("writing empty config") + writeFile(t, incfg, []byte("\n")) + _, _, errmsg = runPkgCover(t, instdira, tag, incfg, mode, + pfiles("a"), errExpected) + if !strings.Contains(errmsg, want) { + t.Errorf("'bad config file' test: wanted %s got %s", want, errmsg) + } +} diff --git a/src/cmd/cover/cover.go b/src/cmd/cover/cover.go new file mode 100644 index 0000000..a4b837c --- /dev/null +++ b/src/cmd/cover/cover.go @@ -0,0 +1,1159 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "internal/coverage" + "internal/coverage/encodemeta" + "internal/coverage/slicewriter" + "io" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "cmd/internal/edit" + "cmd/internal/objabi" +) + +const usageMessage = "" + + `Usage of 'go tool cover': +Given a coverage profile produced by 'go test': + go test -coverprofile=c.out + +Open a web browser displaying annotated source code: + go tool cover -html=c.out + +Write out an HTML file instead of launching a web browser: + go tool cover -html=c.out -o coverage.html + +Display coverage percentages to stdout for each function: + go tool cover -func=c.out + +Finally, to generate modified source code with coverage annotations +for a package (what go test -cover does): + go tool cover -mode=set -var=CoverageVariableName \ + -pkgcfg= -outfilelist= file1.go ... fileN.go + +where -pkgcfg points to a file containing the package path, +package name, module path, and related info from "go build", +and -outfilelist points to a file containing the filenames +of the instrumented output files (one per input file). +See https://pkg.go.dev/internal/coverage#CoverPkgConfig for +more on the package config. +` + +func usage() { + fmt.Fprint(os.Stderr, usageMessage) + fmt.Fprintln(os.Stderr, "\nFlags:") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "\n Only one of -html, -func, or -mode may be set.") + os.Exit(2) +} + +var ( + mode = flag.String("mode", "", "coverage mode: set, count, atomic") + varVar = flag.String("var", "GoCover", "name of coverage variable to generate") + output = flag.String("o", "", "file for output") + outfilelist = flag.String("outfilelist", "", "file containing list of output files (one per line) if -pkgcfg is in use") + htmlOut = flag.String("html", "", "generate HTML representation of coverage profile") + funcOut = flag.String("func", "", "output coverage profile information for each function") + pkgcfg = flag.String("pkgcfg", "", "enable full-package instrumentation mode using params from specified config file") +) + +var pkgconfig coverage.CoverPkgConfig + +// outputfiles is the list of *.cover.go instrumented outputs to write, +// one per input (set when -pkgcfg is in use) +var outputfiles []string + +// covervarsoutfile is an additional Go source file into which we'll +// write definitions of coverage counter variables + meta data variables +// (set when -pkgcfg is in use). +var covervarsoutfile string + +var profile string // The profile to read; the value of -html or -func + +var counterStmt func(*File, string) string + +const ( + atomicPackagePath = "sync/atomic" + atomicPackageName = "_cover_atomic_" +) + +func main() { + objabi.AddVersionFlag() + flag.Usage = usage + objabi.Flagparse(usage) + + // Usage information when no arguments. + if flag.NFlag() == 0 && flag.NArg() == 0 { + flag.Usage() + } + + err := parseFlags() + if err != nil { + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, `For usage information, run "go tool cover -help"`) + os.Exit(2) + } + + // Generate coverage-annotated source. + if *mode != "" { + annotate(flag.Args()) + return + } + + // Output HTML or function coverage information. + if *htmlOut != "" { + err = htmlOutput(profile, *output) + } else { + err = funcOutput(profile, *output) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "cover: %v\n", err) + os.Exit(2) + } +} + +// parseFlags sets the profile and counterStmt globals and performs validations. +func parseFlags() error { + profile = *htmlOut + if *funcOut != "" { + if profile != "" { + return fmt.Errorf("too many options") + } + profile = *funcOut + } + + // Must either display a profile or rewrite Go source. + if (profile == "") == (*mode == "") { + return fmt.Errorf("too many options") + } + + if *varVar != "" && !token.IsIdentifier(*varVar) { + return fmt.Errorf("-var: %q is not a valid identifier", *varVar) + } + + if *mode != "" { + switch *mode { + case "set": + counterStmt = setCounterStmt + case "count": + counterStmt = incCounterStmt + case "atomic": + counterStmt = atomicCounterStmt + case "regonly", "testmain": + counterStmt = nil + default: + return fmt.Errorf("unknown -mode %v", *mode) + } + + if flag.NArg() == 0 { + return fmt.Errorf("missing source file(s)") + } else { + if *pkgcfg != "" { + if *output != "" { + return fmt.Errorf("please use '-outfilelist' flag instead of '-o'") + } + var err error + if outputfiles, err = readOutFileList(*outfilelist); err != nil { + return err + } + covervarsoutfile = outputfiles[0] + outputfiles = outputfiles[1:] + numInputs := len(flag.Args()) + numOutputs := len(outputfiles) + if numOutputs != numInputs { + return fmt.Errorf("number of output files (%d) not equal to number of input files (%d)", numOutputs, numInputs) + } + if err := readPackageConfig(*pkgcfg); err != nil { + return err + } + return nil + } else { + if *outfilelist != "" { + return fmt.Errorf("'-outfilelist' flag applicable only when -pkgcfg used") + } + } + if flag.NArg() == 1 { + return nil + } + } + } else if flag.NArg() == 0 { + return nil + } + return fmt.Errorf("too many arguments") +} + +func readOutFileList(path string) ([]string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading -outfilelist file %q: %v", path, err) + } + return strings.Split(strings.TrimSpace(string(data)), "\n"), nil +} + +func readPackageConfig(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading pkgconfig file %q: %v", path, err) + } + if err := json.Unmarshal(data, &pkgconfig); err != nil { + return fmt.Errorf("error reading pkgconfig file %q: %v", path, err) + } + if pkgconfig.Granularity != "perblock" && pkgconfig.Granularity != "perfunc" { + return fmt.Errorf(`%s: pkgconfig requires perblock/perfunc value`, path) + } + return nil +} + +// Block represents the information about a basic block to be recorded in the analysis. +// Note: Our definition of basic block is based on control structures; we don't break +// apart && and ||. We could but it doesn't seem important enough to bother. +type Block struct { + startByte token.Pos + endByte token.Pos + numStmt int +} + +// Package holds package-specific state. +type Package struct { + mdb *encodemeta.CoverageMetaDataBuilder + counterLengths []int +} + +// Function holds func-specific state. +type Func struct { + units []coverage.CoverableUnit + counterVar string +} + +// File is a wrapper for the state of a file used in the parser. +// The basic parse tree walker is a method of this type. +type File struct { + fset *token.FileSet + name string // Name of file. + astFile *ast.File + blocks []Block + content []byte + edit *edit.Buffer + mdb *encodemeta.CoverageMetaDataBuilder + fn Func + pkg *Package +} + +// findText finds text in the original source, starting at pos. +// It correctly skips over comments and assumes it need not +// handle quoted strings. +// It returns a byte offset within f.src. +func (f *File) findText(pos token.Pos, text string) int { + b := []byte(text) + start := f.offset(pos) + i := start + s := f.content + for i < len(s) { + if bytes.HasPrefix(s[i:], b) { + return i + } + if i+2 <= len(s) && s[i] == '/' && s[i+1] == '/' { + for i < len(s) && s[i] != '\n' { + i++ + } + continue + } + if i+2 <= len(s) && s[i] == '/' && s[i+1] == '*' { + for i += 2; ; i++ { + if i+2 > len(s) { + return 0 + } + if s[i] == '*' && s[i+1] == '/' { + i += 2 + break + } + } + continue + } + i++ + } + return -1 +} + +// Visit implements the ast.Visitor interface. +func (f *File) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.BlockStmt: + // If it's a switch or select, the body is a list of case clauses; don't tag the block itself. + if len(n.List) > 0 { + switch n.List[0].(type) { + case *ast.CaseClause: // switch + for _, n := range n.List { + clause := n.(*ast.CaseClause) + f.addCounters(clause.Colon+1, clause.Colon+1, clause.End(), clause.Body, false) + } + return f + case *ast.CommClause: // select + for _, n := range n.List { + clause := n.(*ast.CommClause) + f.addCounters(clause.Colon+1, clause.Colon+1, clause.End(), clause.Body, false) + } + return f + } + } + f.addCounters(n.Lbrace, n.Lbrace+1, n.Rbrace+1, n.List, true) // +1 to step past closing brace. + case *ast.IfStmt: + if n.Init != nil { + ast.Walk(f, n.Init) + } + ast.Walk(f, n.Cond) + ast.Walk(f, n.Body) + if n.Else == nil { + return nil + } + // The elses are special, because if we have + // if x { + // } else if y { + // } + // we want to cover the "if y". To do this, we need a place to drop the counter, + // so we add a hidden block: + // if x { + // } else { + // if y { + // } + // } + elseOffset := f.findText(n.Body.End(), "else") + if elseOffset < 0 { + panic("lost else") + } + f.edit.Insert(elseOffset+4, "{") + f.edit.Insert(f.offset(n.Else.End()), "}") + + // We just created a block, now walk it. + // Adjust the position of the new block to start after + // the "else". That will cause it to follow the "{" + // we inserted above. + pos := f.fset.File(n.Body.End()).Pos(elseOffset + 4) + switch stmt := n.Else.(type) { + case *ast.IfStmt: + block := &ast.BlockStmt{ + Lbrace: pos, + List: []ast.Stmt{stmt}, + Rbrace: stmt.End(), + } + n.Else = block + case *ast.BlockStmt: + stmt.Lbrace = pos + default: + panic("unexpected node type in if") + } + ast.Walk(f, n.Else) + return nil + case *ast.SelectStmt: + // Don't annotate an empty select - creates a syntax error. + if n.Body == nil || len(n.Body.List) == 0 { + return nil + } + case *ast.SwitchStmt: + // Don't annotate an empty switch - creates a syntax error. + if n.Body == nil || len(n.Body.List) == 0 { + if n.Init != nil { + ast.Walk(f, n.Init) + } + if n.Tag != nil { + ast.Walk(f, n.Tag) + } + return nil + } + case *ast.TypeSwitchStmt: + // Don't annotate an empty type switch - creates a syntax error. + if n.Body == nil || len(n.Body.List) == 0 { + if n.Init != nil { + ast.Walk(f, n.Init) + } + ast.Walk(f, n.Assign) + return nil + } + case *ast.FuncDecl: + // Don't annotate functions with blank names - they cannot be executed. + // Similarly for bodyless funcs. + if n.Name.Name == "_" || n.Body == nil { + return nil + } + fname := n.Name.Name + // Skip AddUint32 and StoreUint32 if we're instrumenting + // sync/atomic itself in atomic mode (out of an abundance of + // caution), since as part of the instrumentation process we + // add calls to AddUint32/StoreUint32, and we don't want to + // somehow create an infinite loop. + // + // Note that in the current implementation (Go 1.20) both + // routines are assembly stubs that forward calls to the + // runtime/internal/atomic equivalents, hence the infinite + // loop scenario is purely theoretical (maybe if in some + // future implementation one of these functions might be + // written in Go). See #57445 for more details. + if atomicOnAtomic() && (fname == "AddUint32" || fname == "StoreUint32") { + return nil + } + // Determine proper function or method name. + if r := n.Recv; r != nil && len(r.List) == 1 { + t := r.List[0].Type + star := "" + if p, _ := t.(*ast.StarExpr); p != nil { + t = p.X + star = "*" + } + if p, _ := t.(*ast.Ident); p != nil { + fname = star + p.Name + "." + fname + } + } + walkBody := true + if *pkgcfg != "" { + f.preFunc(n, fname) + if pkgconfig.Granularity == "perfunc" { + walkBody = false + } + } + if walkBody { + ast.Walk(f, n.Body) + } + if *pkgcfg != "" { + flit := false + f.postFunc(n, fname, flit, n.Body) + } + return nil + case *ast.FuncLit: + // For function literals enclosed in functions, just glom the + // code for the literal in with the enclosing function (for now). + if f.fn.counterVar != "" { + return f + } + + // Hack: function literals aren't named in the go/ast representation, + // and we don't know what name the compiler will choose. For now, + // just make up a descriptive name. + pos := n.Pos() + p := f.fset.File(pos).Position(pos) + fname := fmt.Sprintf("func.L%d.C%d", p.Line, p.Column) + if *pkgcfg != "" { + f.preFunc(n, fname) + } + if pkgconfig.Granularity != "perfunc" { + ast.Walk(f, n.Body) + } + if *pkgcfg != "" { + flit := true + f.postFunc(n, fname, flit, n.Body) + } + return nil + } + return f +} + +func mkCounterVarName(idx int) string { + return fmt.Sprintf("%s_%d", *varVar, idx) +} + +func mkPackageIdVar() string { + return *varVar + "P" +} + +func mkMetaVar() string { + return *varVar + "M" +} + +func mkPackageIdExpression() string { + ppath := pkgconfig.PkgPath + if hcid := coverage.HardCodedPkgID(ppath); hcid != -1 { + return fmt.Sprintf("uint32(%d)", uint32(hcid)) + } + return mkPackageIdVar() +} + +func (f *File) preFunc(fn ast.Node, fname string) { + f.fn.units = f.fn.units[:0] + + // create a new counter variable for this function. + cv := mkCounterVarName(len(f.pkg.counterLengths)) + f.fn.counterVar = cv +} + +func (f *File) postFunc(fn ast.Node, funcname string, flit bool, body *ast.BlockStmt) { + + // Tack on single counter write if we are in "perfunc" mode. + singleCtr := "" + if pkgconfig.Granularity == "perfunc" { + singleCtr = "; " + f.newCounter(fn.Pos(), fn.Pos(), 1) + } + + // record the length of the counter var required. + nc := len(f.fn.units) + coverage.FirstCtrOffset + f.pkg.counterLengths = append(f.pkg.counterLengths, nc) + + // FIXME: for windows, do we want "\" and not "/"? Need to test here. + // Currently filename is formed as packagepath + "/" + basename. + fnpos := f.fset.Position(fn.Pos()) + ppath := pkgconfig.PkgPath + filename := ppath + "/" + filepath.Base(fnpos.Filename) + + // The convention for cmd/cover is that if the go command that + // kicks off coverage specifies a local import path (e.g. "go test + // -cover ./thispackage"), the tool will capture full pathnames + // for source files instead of relative paths, which tend to work + // more smoothly for "go tool cover -html". See also issue #56433 + // for more details. + if pkgconfig.Local { + filename = f.name + } + + // Hand off function to meta-data builder. + fd := coverage.FuncDesc{ + Funcname: funcname, + Srcfile: filename, + Units: f.fn.units, + Lit: flit, + } + funcId := f.mdb.AddFunc(fd) + + hookWrite := func(cv string, which int, val string) string { + return fmt.Sprintf("%s[%d] = %s", cv, which, val) + } + if *mode == "atomic" { + hookWrite = func(cv string, which int, val string) string { + return fmt.Sprintf("%sStoreUint32(&%s[%d], %s)", + atomicPackagePrefix(), cv, which, val) + } + } + + // Generate the registration hook sequence for the function. This + // sequence looks like + // + // counterVar[0] = + // counterVar[1] = pkgId + // counterVar[2] = fnId + // + cv := f.fn.counterVar + regHook := hookWrite(cv, 0, strconv.Itoa(len(f.fn.units))) + " ; " + + hookWrite(cv, 1, mkPackageIdExpression()) + " ; " + + hookWrite(cv, 2, strconv.Itoa(int(funcId))) + singleCtr + + // Insert the registration sequence into the function. We want this sequence to + // appear before any counter updates, so use a hack to ensure that this edit + // applies before the edit corresponding to the prolog counter update. + + boff := f.offset(body.Pos()) + ipos := f.fset.File(body.Pos()).Pos(boff) + ip := f.offset(ipos) + f.edit.Replace(ip, ip+1, string(f.content[ipos-1])+regHook+" ; ") + + f.fn.counterVar = "" +} + +func annotate(names []string) { + var p *Package + if *pkgcfg != "" { + pp := pkgconfig.PkgPath + pn := pkgconfig.PkgName + mp := pkgconfig.ModulePath + mdb, err := encodemeta.NewCoverageMetaDataBuilder(pp, pn, mp) + if err != nil { + log.Fatalf("creating coverage meta-data builder: %v\n", err) + } + p = &Package{ + mdb: mdb, + } + } + // TODO: process files in parallel here if it matters. + for k, name := range names { + if strings.ContainsAny(name, "\r\n") { + // annotateFile uses '//line' directives, which don't permit newlines. + log.Fatalf("cover: input path contains newline character: %q", name) + } + + fd := os.Stdout + isStdout := true + if *pkgcfg != "" { + var err error + fd, err = os.Create(outputfiles[k]) + if err != nil { + log.Fatalf("cover: %s", err) + } + isStdout = false + } else if *output != "" { + var err error + fd, err = os.Create(*output) + if err != nil { + log.Fatalf("cover: %s", err) + } + isStdout = false + } + p.annotateFile(name, fd) + if !isStdout { + if err := fd.Close(); err != nil { + log.Fatalf("cover: %s", err) + } + } + } + + if *pkgcfg != "" { + fd, err := os.Create(covervarsoutfile) + if err != nil { + log.Fatalf("cover: %s", err) + } + p.emitMetaData(fd) + if err := fd.Close(); err != nil { + log.Fatalf("cover: %s", err) + } + } +} + +func (p *Package) annotateFile(name string, fd io.Writer) { + fset := token.NewFileSet() + content, err := os.ReadFile(name) + if err != nil { + log.Fatalf("cover: %s: %s", name, err) + } + parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments) + if err != nil { + log.Fatalf("cover: %s: %s", name, err) + } + + file := &File{ + fset: fset, + name: name, + content: content, + edit: edit.NewBuffer(content), + astFile: parsedFile, + } + if p != nil { + file.mdb = p.mdb + file.pkg = p + } + + if *mode == "atomic" { + // Add import of sync/atomic immediately after package clause. + // We do this even if there is an existing import, because the + // existing import may be shadowed at any given place we want + // to refer to it, and our name (_cover_atomic_) is less likely to + // be shadowed. The one exception is if we're visiting the + // sync/atomic package itself, in which case we can refer to + // functions directly without an import prefix. See also #57445. + if pkgconfig.PkgPath != "sync/atomic" { + file.edit.Insert(file.offset(file.astFile.Name.End()), + fmt.Sprintf("; import %s %q", atomicPackageName, atomicPackagePath)) + } + } + if pkgconfig.PkgName == "main" { + file.edit.Insert(file.offset(file.astFile.Name.End()), + "; import _ \"runtime/coverage\"") + } + + if counterStmt != nil { + ast.Walk(file, file.astFile) + } + newContent := file.edit.Bytes() + + if strings.ContainsAny(name, "\r\n") { + // This should have been checked by the caller already, but we double check + // here just to be sure we haven't missed a caller somewhere. + panic(fmt.Sprintf("annotateFile: name contains unexpected newline character: %q", name)) + } + fmt.Fprintf(fd, "//line %s:1:1\n", name) + fd.Write(newContent) + + // After printing the source tree, add some declarations for the + // counters etc. We could do this by adding to the tree, but it's + // easier just to print the text. + file.addVariables(fd) + + // Emit a reference to the atomic package to avoid + // import and not used error when there's no code in a file. + if *mode == "atomic" { + fmt.Fprintf(fd, "\nvar _ = %sLoadUint32\n", atomicPackagePrefix()) + } +} + +// setCounterStmt returns the expression: __count[23] = 1. +func setCounterStmt(f *File, counter string) string { + return fmt.Sprintf("%s = 1", counter) +} + +// incCounterStmt returns the expression: __count[23]++. +func incCounterStmt(f *File, counter string) string { + return fmt.Sprintf("%s++", counter) +} + +// atomicCounterStmt returns the expression: atomic.AddUint32(&__count[23], 1) +func atomicCounterStmt(f *File, counter string) string { + return fmt.Sprintf("%sAddUint32(&%s, 1)", atomicPackagePrefix(), counter) +} + +// newCounter creates a new counter expression of the appropriate form. +func (f *File) newCounter(start, end token.Pos, numStmt int) string { + var stmt string + if *pkgcfg != "" { + slot := len(f.fn.units) + coverage.FirstCtrOffset + if f.fn.counterVar == "" { + panic("internal error: counter var unset") + } + stmt = counterStmt(f, fmt.Sprintf("%s[%d]", f.fn.counterVar, slot)) + stpos := f.fset.Position(start) + enpos := f.fset.Position(end) + stpos, enpos = dedup(stpos, enpos) + unit := coverage.CoverableUnit{ + StLine: uint32(stpos.Line), + StCol: uint32(stpos.Column), + EnLine: uint32(enpos.Line), + EnCol: uint32(enpos.Column), + NxStmts: uint32(numStmt), + } + f.fn.units = append(f.fn.units, unit) + } else { + stmt = counterStmt(f, fmt.Sprintf("%s.Count[%d]", *varVar, + len(f.blocks))) + f.blocks = append(f.blocks, Block{start, end, numStmt}) + } + return stmt +} + +// addCounters takes a list of statements and adds counters to the beginning of +// each basic block at the top level of that list. For instance, given +// +// S1 +// if cond { +// S2 +// } +// S3 +// +// counters will be added before S1 and before S3. The block containing S2 +// will be visited in a separate call. +// TODO: Nested simple blocks get unnecessary (but correct) counters +func (f *File) addCounters(pos, insertPos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) { + // Special case: make sure we add a counter to an empty block. Can't do this below + // or we will add a counter to an empty statement list after, say, a return statement. + if len(list) == 0 { + f.edit.Insert(f.offset(insertPos), f.newCounter(insertPos, blockEnd, 0)+";") + return + } + // Make a copy of the list, as we may mutate it and should leave the + // existing list intact. + list = append([]ast.Stmt(nil), list...) + // We have a block (statement list), but it may have several basic blocks due to the + // appearance of statements that affect the flow of control. + for { + // Find first statement that affects flow of control (break, continue, if, etc.). + // It will be the last statement of this basic block. + var last int + end := blockEnd + for last = 0; last < len(list); last++ { + stmt := list[last] + end = f.statementBoundary(stmt) + if f.endsBasicSourceBlock(stmt) { + // If it is a labeled statement, we need to place a counter between + // the label and its statement because it may be the target of a goto + // and thus start a basic block. That is, given + // foo: stmt + // we need to create + // foo: ; stmt + // and mark the label as a block-terminating statement. + // The result will then be + // foo: COUNTER[n]++; stmt + // However, we can't do this if the labeled statement is already + // a control statement, such as a labeled for. + if label, isLabel := stmt.(*ast.LabeledStmt); isLabel && !f.isControl(label.Stmt) { + newLabel := *label + newLabel.Stmt = &ast.EmptyStmt{ + Semicolon: label.Stmt.Pos(), + Implicit: true, + } + end = label.Pos() // Previous block ends before the label. + list[last] = &newLabel + // Open a gap and drop in the old statement, now without a label. + list = append(list, nil) + copy(list[last+1:], list[last:]) + list[last+1] = label.Stmt + } + last++ + extendToClosingBrace = false // Block is broken up now. + break + } + } + if extendToClosingBrace { + end = blockEnd + } + if pos != end { // Can have no source to cover if e.g. blocks abut. + f.edit.Insert(f.offset(insertPos), f.newCounter(pos, end, last)+";") + } + list = list[last:] + if len(list) == 0 { + break + } + pos = list[0].Pos() + insertPos = pos + } +} + +// hasFuncLiteral reports the existence and position of the first func literal +// in the node, if any. If a func literal appears, it usually marks the termination +// of a basic block because the function body is itself a block. +// Therefore we draw a line at the start of the body of the first function literal we find. +// TODO: what if there's more than one? Probably doesn't matter much. +func hasFuncLiteral(n ast.Node) (bool, token.Pos) { + if n == nil { + return false, 0 + } + var literal funcLitFinder + ast.Walk(&literal, n) + return literal.found(), token.Pos(literal) +} + +// statementBoundary finds the location in s that terminates the current basic +// block in the source. +func (f *File) statementBoundary(s ast.Stmt) token.Pos { + // Control flow statements are easy. + switch s := s.(type) { + case *ast.BlockStmt: + // Treat blocks like basic blocks to avoid overlapping counters. + return s.Lbrace + case *ast.IfStmt: + found, pos := hasFuncLiteral(s.Init) + if found { + return pos + } + found, pos = hasFuncLiteral(s.Cond) + if found { + return pos + } + return s.Body.Lbrace + case *ast.ForStmt: + found, pos := hasFuncLiteral(s.Init) + if found { + return pos + } + found, pos = hasFuncLiteral(s.Cond) + if found { + return pos + } + found, pos = hasFuncLiteral(s.Post) + if found { + return pos + } + return s.Body.Lbrace + case *ast.LabeledStmt: + return f.statementBoundary(s.Stmt) + case *ast.RangeStmt: + found, pos := hasFuncLiteral(s.X) + if found { + return pos + } + return s.Body.Lbrace + case *ast.SwitchStmt: + found, pos := hasFuncLiteral(s.Init) + if found { + return pos + } + found, pos = hasFuncLiteral(s.Tag) + if found { + return pos + } + return s.Body.Lbrace + case *ast.SelectStmt: + return s.Body.Lbrace + case *ast.TypeSwitchStmt: + found, pos := hasFuncLiteral(s.Init) + if found { + return pos + } + return s.Body.Lbrace + } + // If not a control flow statement, it is a declaration, expression, call, etc. and it may have a function literal. + // If it does, that's tricky because we want to exclude the body of the function from this block. + // Draw a line at the start of the body of the first function literal we find. + // TODO: what if there's more than one? Probably doesn't matter much. + found, pos := hasFuncLiteral(s) + if found { + return pos + } + return s.End() +} + +// endsBasicSourceBlock reports whether s changes the flow of control: break, if, etc., +// or if it's just problematic, for instance contains a function literal, which will complicate +// accounting due to the block-within-an expression. +func (f *File) endsBasicSourceBlock(s ast.Stmt) bool { + switch s := s.(type) { + case *ast.BlockStmt: + // Treat blocks like basic blocks to avoid overlapping counters. + return true + case *ast.BranchStmt: + return true + case *ast.ForStmt: + return true + case *ast.IfStmt: + return true + case *ast.LabeledStmt: + return true // A goto may branch here, starting a new basic block. + case *ast.RangeStmt: + return true + case *ast.SwitchStmt: + return true + case *ast.SelectStmt: + return true + case *ast.TypeSwitchStmt: + return true + case *ast.ExprStmt: + // Calls to panic change the flow. + // We really should verify that "panic" is the predefined function, + // but without type checking we can't and the likelihood of it being + // an actual problem is vanishingly small. + if call, ok := s.X.(*ast.CallExpr); ok { + if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "panic" && len(call.Args) == 1 { + return true + } + } + } + found, _ := hasFuncLiteral(s) + return found +} + +// isControl reports whether s is a control statement that, if labeled, cannot be +// separated from its label. +func (f *File) isControl(s ast.Stmt) bool { + switch s.(type) { + case *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt, *ast.TypeSwitchStmt: + return true + } + return false +} + +// funcLitFinder implements the ast.Visitor pattern to find the location of any +// function literal in a subtree. +type funcLitFinder token.Pos + +func (f *funcLitFinder) Visit(node ast.Node) (w ast.Visitor) { + if f.found() { + return nil // Prune search. + } + switch n := node.(type) { + case *ast.FuncLit: + *f = funcLitFinder(n.Body.Lbrace) + return nil // Prune search. + } + return f +} + +func (f *funcLitFinder) found() bool { + return token.Pos(*f) != token.NoPos +} + +// Sort interface for []block1; used for self-check in addVariables. + +type block1 struct { + Block + index int +} + +type blockSlice []block1 + +func (b blockSlice) Len() int { return len(b) } +func (b blockSlice) Less(i, j int) bool { return b[i].startByte < b[j].startByte } +func (b blockSlice) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +// offset translates a token position into a 0-indexed byte offset. +func (f *File) offset(pos token.Pos) int { + return f.fset.Position(pos).Offset +} + +// addVariables adds to the end of the file the declarations to set up the counter and position variables. +func (f *File) addVariables(w io.Writer) { + if *pkgcfg != "" { + return + } + // Self-check: Verify that the instrumented basic blocks are disjoint. + t := make([]block1, len(f.blocks)) + for i := range f.blocks { + t[i].Block = f.blocks[i] + t[i].index = i + } + sort.Sort(blockSlice(t)) + for i := 1; i < len(t); i++ { + if t[i-1].endByte > t[i].startByte { + fmt.Fprintf(os.Stderr, "cover: internal error: block %d overlaps block %d\n", t[i-1].index, t[i].index) + // Note: error message is in byte positions, not token positions. + fmt.Fprintf(os.Stderr, "\t%s:#%d,#%d %s:#%d,#%d\n", + f.name, f.offset(t[i-1].startByte), f.offset(t[i-1].endByte), + f.name, f.offset(t[i].startByte), f.offset(t[i].endByte)) + } + } + + // Declare the coverage struct as a package-level variable. + fmt.Fprintf(w, "\nvar %s = struct {\n", *varVar) + fmt.Fprintf(w, "\tCount [%d]uint32\n", len(f.blocks)) + fmt.Fprintf(w, "\tPos [3 * %d]uint32\n", len(f.blocks)) + fmt.Fprintf(w, "\tNumStmt [%d]uint16\n", len(f.blocks)) + fmt.Fprintf(w, "} {\n") + + // Initialize the position array field. + fmt.Fprintf(w, "\tPos: [3 * %d]uint32{\n", len(f.blocks)) + + // A nice long list of positions. Each position is encoded as follows to reduce size: + // - 32-bit starting line number + // - 32-bit ending line number + // - (16 bit ending column number << 16) | (16-bit starting column number). + for i, block := range f.blocks { + start := f.fset.Position(block.startByte) + end := f.fset.Position(block.endByte) + + start, end = dedup(start, end) + + fmt.Fprintf(w, "\t\t%d, %d, %#x, // [%d]\n", start.Line, end.Line, (end.Column&0xFFFF)<<16|(start.Column&0xFFFF), i) + } + + // Close the position array. + fmt.Fprintf(w, "\t},\n") + + // Initialize the position array field. + fmt.Fprintf(w, "\tNumStmt: [%d]uint16{\n", len(f.blocks)) + + // A nice long list of statements-per-block, so we can give a conventional + // valuation of "percent covered". To save space, it's a 16-bit number, so we + // clamp it if it overflows - won't matter in practice. + for i, block := range f.blocks { + n := block.numStmt + if n > 1<<16-1 { + n = 1<<16 - 1 + } + fmt.Fprintf(w, "\t\t%d, // %d\n", n, i) + } + + // Close the statements-per-block array. + fmt.Fprintf(w, "\t},\n") + + // Close the struct initialization. + fmt.Fprintf(w, "}\n") +} + +// It is possible for positions to repeat when there is a line +// directive that does not specify column information and the input +// has not been passed through gofmt. +// See issues #27530 and #30746. +// Tests are TestHtmlUnformatted and TestLineDup. +// We use a map to avoid duplicates. + +// pos2 is a pair of token.Position values, used as a map key type. +type pos2 struct { + p1, p2 token.Position +} + +// seenPos2 tracks whether we have seen a token.Position pair. +var seenPos2 = make(map[pos2]bool) + +// dedup takes a token.Position pair and returns a pair that does not +// duplicate any existing pair. The returned pair will have the Offset +// fields cleared. +func dedup(p1, p2 token.Position) (r1, r2 token.Position) { + key := pos2{ + p1: p1, + p2: p2, + } + + // We want to ignore the Offset fields in the map, + // since cover uses only file/line/column. + key.p1.Offset = 0 + key.p2.Offset = 0 + + for seenPos2[key] { + key.p2.Column++ + } + seenPos2[key] = true + + return key.p1, key.p2 +} + +func (p *Package) emitMetaData(w io.Writer) { + if *pkgcfg == "" { + return + } + + // Something went wrong if regonly/testmain mode is in effect and + // we have instrumented functions. + if counterStmt == nil && len(p.counterLengths) != 0 { + panic("internal error: seen functions with regonly/testmain") + } + + // Emit package name. + fmt.Fprintf(w, "\npackage %s\n\n", pkgconfig.PkgName) + + // Emit package ID var. + fmt.Fprintf(w, "\nvar %sP uint32\n", *varVar) + + // Emit all of the counter variables. + for k := range p.counterLengths { + cvn := mkCounterVarName(k) + fmt.Fprintf(w, "var %s [%d]uint32\n", cvn, p.counterLengths[k]) + } + + // Emit encoded meta-data. + var sws slicewriter.WriteSeeker + digest, err := p.mdb.Emit(&sws) + if err != nil { + log.Fatalf("encoding meta-data: %v", err) + } + p.mdb = nil + fmt.Fprintf(w, "var %s = [...]byte{\n", mkMetaVar()) + payload := sws.BytesWritten() + for k, b := range payload { + fmt.Fprintf(w, " 0x%x,", b) + if k != 0 && k%8 == 0 { + fmt.Fprintf(w, "\n") + } + } + fmt.Fprintf(w, "}\n") + + fixcfg := coverage.CoverFixupConfig{ + Strategy: "normal", + MetaVar: mkMetaVar(), + MetaLen: len(payload), + MetaHash: fmt.Sprintf("%x", digest), + PkgIdVar: mkPackageIdVar(), + CounterPrefix: *varVar, + CounterGranularity: pkgconfig.Granularity, + CounterMode: *mode, + } + fixdata, err := json.Marshal(fixcfg) + if err != nil { + log.Fatalf("marshal fixupcfg: %v", err) + } + if err := os.WriteFile(pkgconfig.OutConfig, fixdata, 0666); err != nil { + log.Fatalf("error writing %s: %v", pkgconfig.OutConfig, err) + } +} + +// atomicOnAtomic returns true if we're instrumenting +// the sync/atomic package AND using atomic mode. +func atomicOnAtomic() bool { + return *mode == "atomic" && pkgconfig.PkgPath == "sync/atomic" +} + +// atomicPackagePrefix returns the import path prefix used to refer to +// our special import of sync/atomic; this is either set to the +// constant atomicPackageName plus a dot or the empty string if we're +// instrumenting the sync/atomic package itself. +func atomicPackagePrefix() string { + if atomicOnAtomic() { + return "" + } + return atomicPackageName + "." +} diff --git a/src/cmd/cover/cover_test.go b/src/cmd/cover/cover_test.go new file mode 100644 index 0000000..7bfe2d0 --- /dev/null +++ b/src/cmd/cover/cover_test.go @@ -0,0 +1,649 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main_test + +import ( + "bufio" + "bytes" + cmdcover "cmd/cover" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "internal/testenv" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" +) + +const ( + // Data directory, also the package directory for the test. + testdata = "testdata" +) + +// testcover returns the path to the cmd/cover binary that we are going to +// test. At one point this was created via "go build"; we now reuse the unit +// test executable itself. +func testcover(t testing.TB) string { + exe, err := os.Executable() + if err != nil { + t.Helper() + t.Fatal(err) + } + return exe +} + +// testTempDir is a temporary directory created in TestMain. +var testTempDir string + +// If set, this will preserve all the tmpdir files from the test run. +var debug = flag.Bool("debug", false, "keep tmpdir files for debugging") + +// TestMain used here so that we can leverage the test executable +// itself as a cmd/cover executable; compare to similar usage in +// the cmd/go tests. +func TestMain(m *testing.M) { + if os.Getenv("CMDCOVER_TOOLEXEC") != "" { + // When CMDCOVER_TOOLEXEC is set, the test binary is also + // running as a -toolexec wrapper. + tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe") + if tool == "cover" { + // Inject this test binary as cmd/cover in place of the + // installed tool, so that the go command's invocations of + // cover produce coverage for the configuration in which + // the test was built. + os.Args = os.Args[1:] + cmdcover.Main() + } else { + cmd := exec.Command(os.Args[1], os.Args[2:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + os.Exit(1) + } + } + os.Exit(0) + } + if os.Getenv("CMDCOVER_TEST_RUN_MAIN") != "" { + // When CMDCOVER_TEST_RUN_MAIN is set, we're reusing the test + // binary as cmd/cover. In this case we run the main func exported + // via export_test.go, and exit; CMDCOVER_TEST_RUN_MAIN is set below + // for actual test invocations. + cmdcover.Main() + os.Exit(0) + } + flag.Parse() + topTmpdir, err := os.MkdirTemp("", "cmd-cover-test-") + if err != nil { + log.Fatal(err) + } + testTempDir = topTmpdir + if !*debug { + defer os.RemoveAll(topTmpdir) + } else { + fmt.Fprintf(os.Stderr, "debug: preserving tmpdir %s\n", topTmpdir) + } + os.Setenv("CMDCOVER_TEST_RUN_MAIN", "normal") + os.Exit(m.Run()) +} + +var tdmu sync.Mutex +var tdcount int + +func tempDir(t *testing.T) string { + tdmu.Lock() + dir := filepath.Join(testTempDir, fmt.Sprintf("%03d", tdcount)) + tdcount++ + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + } + defer tdmu.Unlock() + return dir +} + +// TestCoverWithToolExec runs a set of subtests that all make use of a +// "-toolexec" wrapper program to invoke the cover test executable +// itself via "go test -cover". +func TestCoverWithToolExec(t *testing.T) { + testenv.MustHaveExec(t) + + toolexecArg := "-toolexec=" + testcover(t) + + t.Run("CoverHTML", func(t *testing.T) { + testCoverHTML(t, toolexecArg) + }) + t.Run("HtmlUnformatted", func(t *testing.T) { + testHtmlUnformatted(t, toolexecArg) + }) + t.Run("FuncWithDuplicateLines", func(t *testing.T) { + testFuncWithDuplicateLines(t, toolexecArg) + }) + t.Run("MissingTrailingNewlineIssue58370", func(t *testing.T) { + testMissingTrailingNewlineIssue58370(t, toolexecArg) + }) +} + +// Execute this command sequence: +// +// replace the word LINE with the line number < testdata/test.go > testdata/test_line.go +// testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go +// go run ./testdata/main.go ./testdata/test.go +func TestCover(t *testing.T) { + testenv.MustHaveGoRun(t) + t.Parallel() + dir := tempDir(t) + + // Read in the test file (testTest) and write it, with LINEs specified, to coverInput. + testTest := filepath.Join(testdata, "test.go") + file, err := os.ReadFile(testTest) + if err != nil { + t.Fatal(err) + } + lines := bytes.Split(file, []byte("\n")) + for i, line := range lines { + lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1))) + } + + // Add a function that is not gofmt'ed. This used to cause a crash. + // We don't put it in test.go because then we would have to gofmt it. + // Issue 23927. + lines = append(lines, []byte("func unFormatted() {"), + []byte("\tif true {"), + []byte("\t}else{"), + []byte("\t}"), + []byte("}")) + lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}")) + + coverInput := filepath.Join(dir, "test_line.go") + if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil { + t.Fatal(err) + } + + // testcover -mode=count -var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest -o ./testdata/test_cover.go testdata/test_line.go + coverOutput := filepath.Join(dir, "test_cover.go") + cmd := testenv.Command(t, testcover(t), "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput) + run(cmd, t) + + cmd = testenv.Command(t, testcover(t), "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput) + err = cmd.Run() + if err == nil { + t.Error("Expected cover to fail with an error") + } + + // Copy testmain to tmpdir, so that it is in the same directory + // as coverOutput. + testMain := filepath.Join(testdata, "main.go") + b, err := os.ReadFile(testMain) + if err != nil { + t.Fatal(err) + } + tmpTestMain := filepath.Join(dir, "main.go") + if err := os.WriteFile(tmpTestMain, b, 0444); err != nil { + t.Fatal(err) + } + + // go run ./testdata/main.go ./testdata/test.go + cmd = testenv.Command(t, testenv.GoToolPath(t), "run", tmpTestMain, coverOutput) + run(cmd, t) + + file, err = os.ReadFile(coverOutput) + if err != nil { + t.Fatal(err) + } + // compiler directive must appear right next to function declaration. + if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got { + t.Error("misplaced compiler directive") + } + // "go:linkname" compiler directive should be present. + if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got { + t.Error("'go:linkname' compiler directive not found") + } + + // Other comments should be preserved too. + c := ".*// This comment didn't appear in generated go code.*" + if got, err := regexp.MatchString(c, string(file)); err != nil || !got { + t.Errorf("non compiler directive comment %q not found", c) + } +} + +// TestDirectives checks that compiler directives are preserved and positioned +// correctly. Directives that occur before top-level declarations should remain +// above those declarations, even if they are not part of the block of +// documentation comments. +func TestDirectives(t *testing.T) { + testenv.MustHaveExec(t) + t.Parallel() + + // Read the source file and find all the directives. We'll keep + // track of whether each one has been seen in the output. + testDirectives := filepath.Join(testdata, "directives.go") + source, err := os.ReadFile(testDirectives) + if err != nil { + t.Fatal(err) + } + sourceDirectives := findDirectives(source) + + // testcover -mode=atomic ./testdata/directives.go + cmd := testenv.Command(t, testcover(t), "-mode=atomic", testDirectives) + cmd.Stderr = os.Stderr + output, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + + // Check that all directives are present in the output. + outputDirectives := findDirectives(output) + foundDirective := make(map[string]bool) + for _, p := range sourceDirectives { + foundDirective[p.name] = false + } + for _, p := range outputDirectives { + if found, ok := foundDirective[p.name]; !ok { + t.Errorf("unexpected directive in output: %s", p.text) + } else if found { + t.Errorf("directive found multiple times in output: %s", p.text) + } + foundDirective[p.name] = true + } + for name, found := range foundDirective { + if !found { + t.Errorf("missing directive: %s", name) + } + } + + // Check that directives that start with the name of top-level declarations + // come before the beginning of the named declaration and after the end + // of the previous declaration. + fset := token.NewFileSet() + astFile, err := parser.ParseFile(fset, testDirectives, output, 0) + if err != nil { + t.Fatal(err) + } + + prevEnd := 0 + for _, decl := range astFile.Decls { + var name string + switch d := decl.(type) { + case *ast.FuncDecl: + name = d.Name.Name + case *ast.GenDecl: + if len(d.Specs) == 0 { + // An empty group declaration. We still want to check that + // directives can be associated with it, so we make up a name + // to match directives in the test data. + name = "_empty" + } else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok { + name = spec.Name.Name + } + } + pos := fset.Position(decl.Pos()).Offset + end := fset.Position(decl.End()).Offset + if name == "" { + prevEnd = end + continue + } + for _, p := range outputDirectives { + if !strings.HasPrefix(p.name, name) { + continue + } + if p.offset < prevEnd || pos < p.offset { + t.Errorf("directive %s does not appear before definition %s", p.text, name) + } + } + prevEnd = end + } +} + +type directiveInfo struct { + text string // full text of the comment, not including newline + name string // text after //go: + offset int // byte offset of first slash in comment +} + +func findDirectives(source []byte) []directiveInfo { + var directives []directiveInfo + directivePrefix := []byte("\n//go:") + offset := 0 + for { + i := bytes.Index(source[offset:], directivePrefix) + if i < 0 { + break + } + i++ // skip newline + p := source[offset+i:] + j := bytes.IndexByte(p, '\n') + if j < 0 { + // reached EOF + j = len(p) + } + directive := directiveInfo{ + text: string(p[:j]), + name: string(p[len(directivePrefix)-1 : j]), + offset: offset + i, + } + directives = append(directives, directive) + offset += i + j + } + return directives +} + +// Makes sure that `cover -func=profile.cov` reports accurate coverage. +// Issue #20515. +func TestCoverFunc(t *testing.T) { + testenv.MustHaveExec(t) + + // testcover -func ./testdata/profile.cov + coverProfile := filepath.Join(testdata, "profile.cov") + cmd := testenv.Command(t, testcover(t), "-func", coverProfile) + out, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + t.Logf("%s", ee.Stderr) + } + t.Fatal(err) + } + + if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got { + t.Logf("%s", out) + t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err) + } +} + +// Check that cover produces correct HTML. +// Issue #25767. +func testCoverHTML(t *testing.T, toolexecArg string) { + testenv.MustHaveGoRun(t) + dir := tempDir(t) + + t.Parallel() + + // go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html + htmlProfile := filepath.Join(dir, "html.cov") + cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html") + cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true") + run(cmd, t) + // testcover -html testdata/html/html.cov -o testdata/html/html.html + htmlHTML := filepath.Join(dir, "html.html") + cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML) + run(cmd, t) + + // Extract the parts of the HTML with comment markers, + // and compare against a golden file. + entireHTML, err := os.ReadFile(htmlHTML) + if err != nil { + t.Fatal(err) + } + var out strings.Builder + scan := bufio.NewScanner(bytes.NewReader(entireHTML)) + in := false + for scan.Scan() { + line := scan.Text() + if strings.Contains(line, "// START") { + in = true + } + if in { + fmt.Fprintln(&out, line) + } + if strings.Contains(line, "// END") { + in = false + } + } + if scan.Err() != nil { + t.Error(scan.Err()) + } + htmlGolden := filepath.Join(testdata, "html", "html.golden") + golden, err := os.ReadFile(htmlGolden) + if err != nil { + t.Fatalf("reading golden file: %v", err) + } + // Ignore white space differences. + // Break into lines, then compare by breaking into words. + goldenLines := strings.Split(string(golden), "\n") + outLines := strings.Split(out.String(), "\n") + // Compare at the line level, stopping at first different line so + // we don't generate tons of output if there's an inserted or deleted line. + for i, goldenLine := range goldenLines { + if i >= len(outLines) { + t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine) + } + // Convert all white space to simple spaces, for easy comparison. + goldenLine = strings.Join(strings.Fields(goldenLine), " ") + outLine := strings.Join(strings.Fields(outLines[i]), " ") + if outLine != goldenLine { + t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine) + } + } + if len(goldenLines) != len(outLines) { + t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)]) + } +} + +// Test HTML processing with a source file not run through gofmt. +// Issue #27350. +func testHtmlUnformatted(t *testing.T, toolexecArg string) { + testenv.MustHaveGoRun(t) + dir := tempDir(t) + + t.Parallel() + + htmlUDir := filepath.Join(dir, "htmlunformatted") + htmlU := filepath.Join(htmlUDir, "htmlunformatted.go") + htmlUTest := filepath.Join(htmlUDir, "htmlunformatted_test.go") + htmlUProfile := filepath.Join(htmlUDir, "htmlunformatted.cov") + htmlUHTML := filepath.Join(htmlUDir, "htmlunformatted.html") + + if err := os.Mkdir(htmlUDir, 0777); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil { + t.Fatal(err) + } + + const htmlUContents = ` +package htmlunformatted + +var g int + +func F() { +//line x.go:1 + { { F(); goto lab } } +lab: +}` + + const htmlUTestContents = `package htmlunformatted` + + if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil { + t.Fatal(err) + } + + // go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov + cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-test.v", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile) + cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true") + cmd.Dir = htmlUDir + run(cmd, t) + + // testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html + cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML) + cmd.Dir = htmlUDir + run(cmd, t) +} + +// lineDupContents becomes linedup.go in testFuncWithDuplicateLines. +const lineDupContents = ` +package linedup + +var G int + +func LineDup(c int) { + for i := 0; i < c; i++ { +//line ld.go:100 + if i % 2 == 0 { + G++ + } + if i % 3 == 0 { + G++; G++ + } +//line ld.go:100 + if i % 4 == 0 { + G++; G++; G++ + } + if i % 5 == 0 { + G++; G++; G++; G++ + } + } +} +` + +// lineDupTestContents becomes linedup_test.go in testFuncWithDuplicateLines. +const lineDupTestContents = ` +package linedup + +import "testing" + +func TestLineDup(t *testing.T) { + LineDup(100) +} +` + +// Test -func with duplicate //line directives with different numbers +// of statements. +func testFuncWithDuplicateLines(t *testing.T, toolexecArg string) { + testenv.MustHaveGoRun(t) + dir := tempDir(t) + + t.Parallel() + + lineDupDir := filepath.Join(dir, "linedup") + lineDupGo := filepath.Join(lineDupDir, "linedup.go") + lineDupTestGo := filepath.Join(lineDupDir, "linedup_test.go") + lineDupProfile := filepath.Join(lineDupDir, "linedup.out") + + if err := os.Mkdir(lineDupDir, 0777); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil { + t.Fatal(err) + } + + // go test -cover -covermode count -coverprofile TMPDIR/linedup.out + cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile) + cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true") + cmd.Dir = lineDupDir + run(cmd, t) + + // testcover -func=TMPDIR/linedup.out + cmd = testenv.Command(t, testcover(t), "-func", lineDupProfile) + cmd.Dir = lineDupDir + run(cmd, t) +} + +func run(c *exec.Cmd, t *testing.T) { + t.Helper() + t.Log("running", c.Args) + out, err := c.CombinedOutput() + if len(out) > 0 { + t.Logf("%s", out) + } + if err != nil { + t.Fatal(err) + } +} + +func runExpectingError(c *exec.Cmd, t *testing.T) string { + t.Helper() + t.Log("running", c.Args) + out, err := c.CombinedOutput() + if err == nil { + return fmt.Sprintf("unexpected pass for %+v", c.Args) + } + return string(out) +} + +// Test instrumentation of package that ends before an expected +// trailing newline following package clause. Issue #58370. +func testMissingTrailingNewlineIssue58370(t *testing.T, toolexecArg string) { + testenv.MustHaveGoBuild(t) + dir := tempDir(t) + + t.Parallel() + + noeolDir := filepath.Join(dir, "issue58370") + noeolGo := filepath.Join(noeolDir, "noeol.go") + noeolTestGo := filepath.Join(noeolDir, "noeol_test.go") + + if err := os.Mkdir(noeolDir, 0777); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(noeolDir, "go.mod"), []byte("module noeol\n"), 0666); err != nil { + t.Fatal(err) + } + const noeolContents = `package noeol` + if err := os.WriteFile(noeolGo, []byte(noeolContents), 0444); err != nil { + t.Fatal(err) + } + const noeolTestContents = ` +package noeol +import "testing" +func TestCoverage(t *testing.T) { } +` + if err := os.WriteFile(noeolTestGo, []byte(noeolTestContents), 0444); err != nil { + t.Fatal(err) + } + + // go test -covermode atomic + cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-covermode", "atomic") + cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true") + cmd.Dir = noeolDir + run(cmd, t) +} + +func TestSrcPathWithNewline(t *testing.T) { + testenv.MustHaveExec(t) + t.Parallel() + + // srcPath is intentionally not clean so that the path passed to testcover + // will not normalize the trailing / to a \ on Windows. + srcPath := t.TempDir() + string(filepath.Separator) + "\npackage main\nfunc main() { panic(string([]rune{'u', 'h', '-', 'o', 'h'}))\n/*/main.go" + mainSrc := ` package main + +func main() { + /* nothing here */ + println("ok") +} +` + if err := os.MkdirAll(filepath.Dir(srcPath), 0777); err != nil { + t.Skipf("creating directory with bogus path: %v", err) + } + if err := os.WriteFile(srcPath, []byte(mainSrc), 0666); err != nil { + t.Skipf("writing file with bogus directory: %v", err) + } + + cmd := testenv.Command(t, testcover(t), "-mode=atomic", srcPath) + cmd.Stderr = new(bytes.Buffer) + out, err := cmd.Output() + t.Logf("%v:\n%s", cmd, out) + t.Logf("stderr:\n%s", cmd.Stderr) + if err == nil { + t.Errorf("unexpected success; want failure due to newline in file path") + } +} diff --git a/src/cmd/cover/doc.go b/src/cmd/cover/doc.go new file mode 100644 index 0000000..82580cd --- /dev/null +++ b/src/cmd/cover/doc.go @@ -0,0 +1,32 @@ +// Copyright 2013 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. + +/* +Cover is a program for analyzing the coverage profiles generated by +'go test -coverprofile=cover.out'. + +Cover is also used by 'go test -cover' to rewrite the source code with +annotations to track which parts of each function are executed (this +is referred to "instrumentation"). Cover can operate in "legacy mode" +on a single Go source file at a time, or when invoked by the Go tool +it will process all the source files in a single package at a time +(package-scope instrumentation is enabled via "-pkgcfg" option, + +When generated instrumented code, the cover tool computes approximate +basic block information by studying the source. It is thus more +portable than binary-rewriting coverage tools, but also a little less +capable. For instance, it does not probe inside && and || expressions, +and can be mildly confused by single statements with multiple function +literals. + +When computing coverage of a package that uses cgo, the cover tool +must be applied to the output of cgo preprocessing, not the input, +because cover deletes comments that are significant to cgo. + +For usage information, please see: + + go help testflag + go tool cover -help +*/ +package main diff --git a/src/cmd/cover/export_test.go b/src/cmd/cover/export_test.go new file mode 100644 index 0000000..e4592ee --- /dev/null +++ b/src/cmd/cover/export_test.go @@ -0,0 +1,7 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +func Main() { main() } diff --git a/src/cmd/cover/func.go b/src/cmd/cover/func.go new file mode 100644 index 0000000..dffd3c1 --- /dev/null +++ b/src/cmd/cover/func.go @@ -0,0 +1,248 @@ +// Copyright 2013 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. + +// This file implements the visitor that computes the (line, column)-(line-column) range for each function. + +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "text/tabwriter" + + "golang.org/x/tools/cover" +) + +// funcOutput takes two file names as arguments, a coverage profile to read as input and an output +// file to write ("" means to write to standard output). The function reads the profile and produces +// as output the coverage data broken down by function, like this: +// +// fmt/format.go:30: init 100.0% +// fmt/format.go:57: clearflags 100.0% +// ... +// fmt/scan.go:1046: doScan 100.0% +// fmt/scan.go:1075: advance 96.2% +// fmt/scan.go:1119: doScanf 96.8% +// total: (statements) 91.9% + +func funcOutput(profile, outputFile string) error { + profiles, err := cover.ParseProfiles(profile) + if err != nil { + return err + } + + dirs, err := findPkgs(profiles) + if err != nil { + return err + } + + var out *bufio.Writer + if outputFile == "" { + out = bufio.NewWriter(os.Stdout) + } else { + fd, err := os.Create(outputFile) + if err != nil { + return err + } + defer fd.Close() + out = bufio.NewWriter(fd) + } + defer out.Flush() + + tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) + defer tabber.Flush() + + var total, covered int64 + for _, profile := range profiles { + fn := profile.FileName + file, err := findFile(dirs, fn) + if err != nil { + return err + } + funcs, err := findFuncs(file) + if err != nil { + return err + } + // Now match up functions and profile blocks. + for _, f := range funcs { + c, t := f.coverage(profile) + fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, percent(c, t)) + total += t + covered += c + } + } + fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", percent(covered, total)) + + return nil +} + +// findFuncs parses the file and returns a slice of FuncExtent descriptors. +func findFuncs(name string) ([]*FuncExtent, error) { + fset := token.NewFileSet() + parsedFile, err := parser.ParseFile(fset, name, nil, 0) + if err != nil { + return nil, err + } + visitor := &FuncVisitor{ + fset: fset, + name: name, + astFile: parsedFile, + } + ast.Walk(visitor, visitor.astFile) + return visitor.funcs, nil +} + +// FuncExtent describes a function's extent in the source by file and position. +type FuncExtent struct { + name string + startLine int + startCol int + endLine int + endCol int +} + +// FuncVisitor implements the visitor that builds the function position list for a file. +type FuncVisitor struct { + fset *token.FileSet + name string // Name of file. + astFile *ast.File + funcs []*FuncExtent +} + +// Visit implements the ast.Visitor interface. +func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.FuncDecl: + if n.Body == nil { + // Do not count declarations of assembly functions. + break + } + start := v.fset.Position(n.Pos()) + end := v.fset.Position(n.End()) + fe := &FuncExtent{ + name: n.Name.Name, + startLine: start.Line, + startCol: start.Column, + endLine: end.Line, + endCol: end.Column, + } + v.funcs = append(v.funcs, fe) + } + return v +} + +// coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. +func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { + // We could avoid making this n^2 overall by doing a single scan and annotating the functions, + // but the sizes of the data structures is never very large and the scan is almost instantaneous. + var covered, total int64 + // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. + for _, b := range profile.Blocks { + if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { + // Past the end of the function. + break + } + if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { + // Before the beginning of the function + continue + } + total += int64(b.NumStmt) + if b.Count > 0 { + covered += int64(b.NumStmt) + } + } + return covered, total +} + +// Pkg describes a single package, compatible with the JSON output from 'go list'; see 'go help list'. +type Pkg struct { + ImportPath string + Dir string + Error *struct { + Err string + } +} + +func findPkgs(profiles []*cover.Profile) (map[string]*Pkg, error) { + // Run go list to find the location of every package we care about. + pkgs := make(map[string]*Pkg) + var list []string + for _, profile := range profiles { + if strings.HasPrefix(profile.FileName, ".") || filepath.IsAbs(profile.FileName) { + // Relative or absolute path. + continue + } + pkg := path.Dir(profile.FileName) + if _, ok := pkgs[pkg]; !ok { + pkgs[pkg] = nil + list = append(list, pkg) + } + } + + if len(list) == 0 { + return pkgs, nil + } + + // Note: usually run as "go tool cover" in which case $GOROOT is set, + // in which case runtime.GOROOT() does exactly what we want. + goTool := filepath.Join(runtime.GOROOT(), "bin/go") + cmd := exec.Command(goTool, append([]string{"list", "-e", "-json"}, list...)...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + stdout, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("cannot run go list: %v\n%s", err, stderr.Bytes()) + } + dec := json.NewDecoder(bytes.NewReader(stdout)) + for { + var pkg Pkg + err := dec.Decode(&pkg) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("decoding go list json: %v", err) + } + pkgs[pkg.ImportPath] = &pkg + } + return pkgs, nil +} + +// findFile finds the location of the named file in GOROOT, GOPATH etc. +func findFile(pkgs map[string]*Pkg, file string) (string, error) { + if strings.HasPrefix(file, ".") || filepath.IsAbs(file) { + // Relative or absolute path. + return file, nil + } + pkg := pkgs[path.Dir(file)] + if pkg != nil { + if pkg.Dir != "" { + return filepath.Join(pkg.Dir, path.Base(file)), nil + } + if pkg.Error != nil { + return "", errors.New(pkg.Error.Err) + } + } + return "", fmt.Errorf("did not find package for %s in go list output", file) +} + +func percent(covered, total int64) float64 { + if total == 0 { + total = 1 // Avoid zero denominator. + } + return 100.0 * float64(covered) / float64(total) +} diff --git a/src/cmd/cover/html.go b/src/cmd/cover/html.go new file mode 100644 index 0000000..400a7d8 --- /dev/null +++ b/src/cmd/cover/html.go @@ -0,0 +1,306 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "cmd/internal/browser" + "fmt" + "html/template" + "io" + "math" + "os" + "path/filepath" + "strings" + + "golang.org/x/tools/cover" +) + +// htmlOutput reads the profile data from profile and generates an HTML +// coverage report, writing it to outfile. If outfile is empty, +// it writes the report to a temporary file and opens it in a web browser. +func htmlOutput(profile, outfile string) error { + profiles, err := cover.ParseProfiles(profile) + if err != nil { + return err + } + + var d templateData + + dirs, err := findPkgs(profiles) + if err != nil { + return err + } + + for _, profile := range profiles { + fn := profile.FileName + if profile.Mode == "set" { + d.Set = true + } + file, err := findFile(dirs, fn) + if err != nil { + return err + } + src, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("can't read %q: %v", fn, err) + } + var buf strings.Builder + err = htmlGen(&buf, src, profile.Boundaries(src)) + if err != nil { + return err + } + d.Files = append(d.Files, &templateFile{ + Name: fn, + Body: template.HTML(buf.String()), + Coverage: percentCovered(profile), + }) + } + + var out *os.File + if outfile == "" { + var dir string + dir, err = os.MkdirTemp("", "cover") + if err != nil { + return err + } + out, err = os.Create(filepath.Join(dir, "coverage.html")) + } else { + out, err = os.Create(outfile) + } + if err != nil { + return err + } + err = htmlTemplate.Execute(out, d) + if err2 := out.Close(); err == nil { + err = err2 + } + if err != nil { + return err + } + + if outfile == "" { + if !browser.Open("file://" + out.Name()) { + fmt.Fprintf(os.Stderr, "HTML output written to %s\n", out.Name()) + } + } + + return nil +} + +// percentCovered returns, as a percentage, the fraction of the statements in +// the profile covered by the test run. +// In effect, it reports the coverage of a given source file. +func percentCovered(p *cover.Profile) float64 { + var total, covered int64 + for _, b := range p.Blocks { + total += int64(b.NumStmt) + if b.Count > 0 { + covered += int64(b.NumStmt) + } + } + if total == 0 { + return 0 + } + return float64(covered) / float64(total) * 100 +} + +// htmlGen generates an HTML coverage report with the provided filename, +// source code, and tokens, and writes it to the given Writer. +func htmlGen(w io.Writer, src []byte, boundaries []cover.Boundary) error { + dst := bufio.NewWriter(w) + for i := range src { + for len(boundaries) > 0 && boundaries[0].Offset == i { + b := boundaries[0] + if b.Start { + n := 0 + if b.Count > 0 { + n = int(math.Floor(b.Norm*9)) + 1 + } + fmt.Fprintf(dst, ``, n, b.Count) + } else { + dst.WriteString("") + } + boundaries = boundaries[1:] + } + switch b := src[i]; b { + case '>': + dst.WriteString(">") + case '<': + dst.WriteString("<") + case '&': + dst.WriteString("&") + case '\t': + dst.WriteString(" ") + default: + dst.WriteByte(b) + } + } + return dst.Flush() +} + +// rgb returns an rgb value for the specified coverage value +// between 0 (no coverage) and 10 (max coverage). +func rgb(n int) string { + if n == 0 { + return "rgb(192, 0, 0)" // Red + } + // Gradient from gray to green. + r := 128 - 12*(n-1) + g := 128 + 12*(n-1) + b := 128 + 3*(n-1) + return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b) +} + +// colors generates the CSS rules for coverage colors. +func colors() template.CSS { + var buf strings.Builder + for i := 0; i < 11; i++ { + fmt.Fprintf(&buf, ".cov%v { color: %v }\n", i, rgb(i)) + } + return template.CSS(buf.String()) +} + +var htmlTemplate = template.Must(template.New("html").Funcs(template.FuncMap{ + "colors": colors, +}).Parse(tmplHTML)) + +type templateData struct { + Files []*templateFile + Set bool +} + +// PackageName returns a name for the package being shown. +// It does this by choosing the penultimate element of the path +// name, so foo.bar/baz/foo.go chooses 'baz'. This is cheap +// and easy, avoids parsing the Go file, and gets a better answer +// for package main. It returns the empty string if there is +// a problem. +func (td templateData) PackageName() string { + if len(td.Files) == 0 { + return "" + } + fileName := td.Files[0].Name + elems := strings.Split(fileName, "/") // Package path is always slash-separated. + // Return the penultimate non-empty element. + for i := len(elems) - 2; i >= 0; i-- { + if elems[i] != "" { + return elems[i] + } + } + return "" +} + +type templateFile struct { + Name string + Body template.HTML + Coverage float64 +} + +const tmplHTML = ` + + + + + {{$pkg := .PackageName}}{{if $pkg}}{{$pkg}}: {{end}}Go Coverage Report + + + +
+ +
+ not tracked + {{if .Set}} + not covered + covered + {{else}} + no coverage + low coverage + * + * + * + * + * + * + * + * + high coverage + {{end}} +
+
+
+ {{range $i, $f := .Files}} + + {{end}} +
+ + + +` diff --git a/src/cmd/cover/pkgname_test.go b/src/cmd/cover/pkgname_test.go new file mode 100644 index 0000000..1c731ad --- /dev/null +++ b/src/cmd/cover/pkgname_test.go @@ -0,0 +1,31 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import "testing" + +func TestPackageName(t *testing.T) { + var tests = []struct { + fileName, pkgName string + }{ + {"", ""}, + {"///", ""}, + {"fmt", ""}, // No Go file, improper form. + {"fmt/foo.go", "fmt"}, + {"encoding/binary/foo.go", "binary"}, + {"encoding/binary/////foo.go", "binary"}, + } + var tf templateFile + for _, test := range tests { + tf.Name = test.fileName + td := templateData{ + Files: []*templateFile{&tf}, + } + got := td.PackageName() + if got != test.pkgName { + t.Errorf("%s: got %s want %s", test.fileName, got, test.pkgName) + } + } +} diff --git a/src/cmd/cover/testdata/directives.go b/src/cmd/cover/testdata/directives.go new file mode 100644 index 0000000..dfb7b8e --- /dev/null +++ b/src/cmd/cover/testdata/directives.go @@ -0,0 +1,40 @@ +// Copyright 2017 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. + +// This file is processed by the cover command, then a test verifies that +// all compiler directives are preserved and positioned appropriately. + +//go:a + +//go:b +package main + +//go:c1 + +//go:c2 +//doc +func c() { +} + +//go:d1 + +//doc +//go:d2 +type d int + +//go:e1 + +//doc +//go:e2 +type ( + e int + f int +) + +//go:_empty1 +//doc +//go:_empty2 +type () + +//go:f diff --git a/src/cmd/cover/testdata/html/html.go b/src/cmd/cover/testdata/html/html.go new file mode 100644 index 0000000..2057825 --- /dev/null +++ b/src/cmd/cover/testdata/html/html.go @@ -0,0 +1,30 @@ +package html + +import "fmt" + +// This file is tested by html_test.go. +// The comments below are markers for extracting the annotated source +// from the HTML output. + +// This is a regression test for incorrect sorting of boundaries +// that coincide, specifically for empty select clauses. +// START f +func f() { + ch := make(chan int) + select { + case <-ch: + default: + } +} + +// END f + +// https://golang.org/issue/25767 +// START g +func g() { + if false { + fmt.Printf("Hello") + } +} + +// END g diff --git a/src/cmd/cover/testdata/html/html.golden b/src/cmd/cover/testdata/html/html.golden new file mode 100644 index 0000000..84377d1 --- /dev/null +++ b/src/cmd/cover/testdata/html/html.golden @@ -0,0 +1,18 @@ +// START f +func f() { + ch := make(chan int) + select { + case <-ch: + default: + } +} + +// END f +// START g +func g() { + if false { + fmt.Printf("Hello") + } +} + +// END g diff --git a/src/cmd/cover/testdata/html/html_test.go b/src/cmd/cover/testdata/html/html_test.go new file mode 100644 index 0000000..c15561f --- /dev/null +++ b/src/cmd/cover/testdata/html/html_test.go @@ -0,0 +1,8 @@ +package html + +import "testing" + +func TestAll(t *testing.T) { + f() + g() +} diff --git a/src/cmd/cover/testdata/main.go b/src/cmd/cover/testdata/main.go new file mode 100644 index 0000000..be74b4a --- /dev/null +++ b/src/cmd/cover/testdata/main.go @@ -0,0 +1,116 @@ +// Copyright 2013 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. + +// Test runner for coverage test. This file is not coverage-annotated; test.go is. +// It knows the coverage counter is called +// "thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest". + +package main + +import ( + "fmt" + "os" +) + +func main() { + testAll() + verify() +} + +type block struct { + count uint32 + line uint32 +} + +var counters = make(map[block]bool) + +// shorthand for the long counter variable. +var coverTest = &thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest + +// check records the location and expected value for a counter. +func check(line, count uint32) { + b := block{ + count, + line, + } + counters[b] = true +} + +// checkVal is a version of check that returns its extra argument, +// so it can be used in conditionals. +func checkVal(line, count uint32, val int) int { + b := block{ + count, + line, + } + counters[b] = true + return val +} + +var PASS = true + +// verify checks the expected counts against the actual. It runs after the test has completed. +func verify() { + for b := range counters { + got, index := count(b.line) + if b.count == anything && got != 0 { + got = anything + } + if got != b.count { + fmt.Fprintf(os.Stderr, "test_go:%d expected count %d got %d [counter %d]\n", b.line, b.count, got, index) + PASS = false + } + } + verifyPanic() + if !PASS { + fmt.Fprintf(os.Stderr, "FAIL\n") + os.Exit(2) + } +} + +// verifyPanic is a special check for the known counter that should be +// after the panic call in testPanic. +func verifyPanic() { + if coverTest.Count[panicIndex-1] != 1 { + // Sanity check for test before panic. + fmt.Fprintf(os.Stderr, "bad before panic") + PASS = false + } + if coverTest.Count[panicIndex] != 0 { + fmt.Fprintf(os.Stderr, "bad at panic: %d should be 0\n", coverTest.Count[panicIndex]) + PASS = false + } + if coverTest.Count[panicIndex+1] != 1 { + fmt.Fprintf(os.Stderr, "bad after panic") + PASS = false + } +} + +// count returns the count and index for the counter at the specified line. +func count(line uint32) (uint32, int) { + // Linear search is fine. Choose perfect fit over approximate. + // We can have a closing brace for a range on the same line as a condition for an "else if" + // and we don't want that brace to steal the count for the condition on the "if". + // Therefore we test for a perfect (lo==line && hi==line) match, but if we can't + // find that we take the first imperfect match. + index := -1 + indexLo := uint32(1e9) + for i := range coverTest.Count { + lo, hi := coverTest.Pos[3*i], coverTest.Pos[3*i+1] + if lo == line && line == hi { + return coverTest.Count[i], i + } + // Choose the earliest match (the counters are in unpredictable order). + if lo <= line && line <= hi && indexLo > lo { + index = i + indexLo = lo + } + } + if index == -1 { + fmt.Fprintln(os.Stderr, "cover_test: no counter for line", line) + PASS = false + return 0, 0 + } + return coverTest.Count[index], index +} diff --git a/src/cmd/cover/testdata/p.go b/src/cmd/cover/testdata/p.go new file mode 100644 index 0000000..ce3a8c0 --- /dev/null +++ b/src/cmd/cover/testdata/p.go @@ -0,0 +1,27 @@ +// Copyright 2017 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. + +// A package such that there are 3 functions with zero total and covered lines. +// And one with 1 total and covered lines. Reproduces issue #20515. +package p + +//go:noinline +func A() { + +} + +//go:noinline +func B() { + +} + +//go:noinline +func C() { + +} + +//go:noinline +func D() int64 { + return 42 +} diff --git a/src/cmd/cover/testdata/pkgcfg/a/a.go b/src/cmd/cover/testdata/pkgcfg/a/a.go new file mode 100644 index 0000000..44c380b --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/a/a.go @@ -0,0 +1,28 @@ +package a + +type Atyp int + +func (ap *Atyp) Set(q int) { + *ap = Atyp(q) +} + +func (ap Atyp) Get() int { + inter := func(q Atyp) int { + return int(q) + } + return inter(ap) +} + +var afunc = func(x int) int { + return x + 1 +} +var Avar = afunc(42) + +func A(x int) int { + if x == 0 { + return 22 + } else if x == 1 { + return 33 + } + return 44 +} diff --git a/src/cmd/cover/testdata/pkgcfg/a/a2.go b/src/cmd/cover/testdata/pkgcfg/a/a2.go new file mode 100644 index 0000000..e6b2fc1 --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/a/a2.go @@ -0,0 +1,8 @@ +package a + +func A2() { + { + } + { + } +} diff --git a/src/cmd/cover/testdata/pkgcfg/a/a_test.go b/src/cmd/cover/testdata/pkgcfg/a/a_test.go new file mode 100644 index 0000000..a1608e0 --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/a/a_test.go @@ -0,0 +1,14 @@ +package a_test + +import ( + "cfg/a" + "testing" +) + +func TestA(t *testing.T) { + a.A(0) + var aat a.Atyp + at := &aat + at.Set(42) + println(at.Get()) +} diff --git a/src/cmd/cover/testdata/pkgcfg/b/b.go b/src/cmd/cover/testdata/pkgcfg/b/b.go new file mode 100644 index 0000000..9e330ee --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/b/b.go @@ -0,0 +1,10 @@ +package b + +func B(x int) int { + if x == 0 { + return 22 + } else if x == 1 { + return 33 + } + return 44 +} diff --git a/src/cmd/cover/testdata/pkgcfg/b/b_test.go b/src/cmd/cover/testdata/pkgcfg/b/b_test.go new file mode 100644 index 0000000..7bdb73b --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/b/b_test.go @@ -0,0 +1,9 @@ +package b + +import "testing" + +func TestB(t *testing.T) { + B(0) + B(1) + B(2) +} diff --git a/src/cmd/cover/testdata/pkgcfg/main/main.go b/src/cmd/cover/testdata/pkgcfg/main/main.go new file mode 100644 index 0000000..a908931 --- /dev/null +++ b/src/cmd/cover/testdata/pkgcfg/main/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "cfg/a" + "cfg/b" +) + +func main() { + a.A(2) + a.A(1) + a.A(0) + b.B(1) + b.B(0) + println("done") +} diff --git a/src/cmd/cover/testdata/profile.cov b/src/cmd/cover/testdata/profile.cov new file mode 100644 index 0000000..db08602 --- /dev/null +++ b/src/cmd/cover/testdata/profile.cov @@ -0,0 +1,5 @@ +mode: set +./testdata/p.go:10.10,12.2 0 0 +./testdata/p.go:15.10,17.2 0 0 +./testdata/p.go:20.10,22.2 0 0 +./testdata/p.go:25.16,27.2 1 1 diff --git a/src/cmd/cover/testdata/test.go b/src/cmd/cover/testdata/test.go new file mode 100644 index 0000000..0e1dbc6 --- /dev/null +++ b/src/cmd/cover/testdata/test.go @@ -0,0 +1,300 @@ +// Copyright 2013 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. + +// This program is processed by the cover command, and then testAll is called. +// The test driver in main.go can then compare the coverage statistics with expectation. + +// The word LINE is replaced by the line number in this file. When the file is executed, +// the coverage processing has changed the line numbers, so we can't use runtime.Caller. + +package main + +import _ "unsafe" // for go:linkname + +//go:linkname some_name some_name +var some_name int + +const anything = 1e9 // Just some unlikely value that means "we got here, don't care how often" + +func testAll() { + testSimple() + testBlockRun() + testIf() + testFor() + testRange() + testSwitch() + testTypeSwitch() + testSelect1() + testSelect2() + testPanic() + testEmptySwitches() + testFunctionLiteral() + testGoto() +} + +// The indexes of the counters in testPanic are known to main.go +const panicIndex = 3 + +// This test appears first because the index of its counters is known to main.go +func testPanic() { + defer func() { + recover() + }() + check(LINE, 1) + panic("should not get next line") + check(LINE, 0) // this is GoCover.Count[panicIndex] + // The next counter is in testSimple and it will be non-zero. + // If the panic above does not trigger a counter, the test will fail + // because GoCover.Count[panicIndex] will be the one in testSimple. +} + +func testSimple() { + check(LINE, 1) +} + +func testIf() { + if true { + check(LINE, 1) + } else { + check(LINE, 0) + } + if false { + check(LINE, 0) + } else { + check(LINE, 1) + } + for i := 0; i < 3; i++ { + if checkVal(LINE, 3, i) <= 2 { + check(LINE, 3) + } + if checkVal(LINE, 3, i) <= 1 { + check(LINE, 2) + } + if checkVal(LINE, 3, i) <= 0 { + check(LINE, 1) + } + } + for i := 0; i < 3; i++ { + if checkVal(LINE, 3, i) <= 1 { + check(LINE, 2) + } else { + check(LINE, 1) + } + } + for i := 0; i < 3; i++ { + if checkVal(LINE, 3, i) <= 0 { + check(LINE, 1) + } else if checkVal(LINE, 2, i) <= 1 { + check(LINE, 1) + } else if checkVal(LINE, 1, i) <= 2 { + check(LINE, 1) + } else if checkVal(LINE, 0, i) <= 3 { + check(LINE, 0) + } + } + if func(a, b int) bool { return a < b }(3, 4) { + check(LINE, 1) + } +} + +func testFor() { + for i := 0; i < 10; func() { i++; check(LINE, 10) }() { + check(LINE, 10) + } +} + +func testRange() { + for _, f := range []func(){ + func() { check(LINE, 1) }, + } { + f() + check(LINE, 1) + } +} + +func testBlockRun() { + check(LINE, 1) + { + check(LINE, 1) + } + { + check(LINE, 1) + } + check(LINE, 1) + { + check(LINE, 1) + } + { + check(LINE, 1) + } + check(LINE, 1) +} + +func testSwitch() { + for i := 0; i < 5; func() { i++; check(LINE, 5) }() { + goto label2 + label1: + goto label1 + label2: + switch i { + case 0: + check(LINE, 1) + case 1: + check(LINE, 1) + case 2: + check(LINE, 1) + default: + check(LINE, 2) + } + } +} + +func testTypeSwitch() { + var x = []any{1, 2.0, "hi"} + for _, v := range x { + switch func() { check(LINE, 3) }(); v.(type) { + case int: + check(LINE, 1) + case float64: + check(LINE, 1) + case string: + check(LINE, 1) + case complex128: + check(LINE, 0) + default: + check(LINE, 0) + } + } +} + +func testSelect1() { + c := make(chan int) + go func() { + for i := 0; i < 1000; i++ { + c <- i + } + }() + for { + select { + case <-c: + check(LINE, anything) + case <-c: + check(LINE, anything) + default: + check(LINE, 1) + return + } + } +} + +func testSelect2() { + c1 := make(chan int, 1000) + c2 := make(chan int, 1000) + for i := 0; i < 1000; i++ { + c1 <- i + c2 <- i + } + for { + select { + case <-c1: + check(LINE, 1000) + case <-c2: + check(LINE, 1000) + default: + check(LINE, 1) + return + } + } +} + +// Empty control statements created syntax errors. This function +// is here just to be sure that those are handled correctly now. +func testEmptySwitches() { + check(LINE, 1) + switch 3 { + } + check(LINE, 1) + switch i := (any)(3).(int); i { + } + check(LINE, 1) + c := make(chan int) + go func() { + check(LINE, 1) + c <- 1 + select {} + }() + <-c + check(LINE, 1) +} + +func testFunctionLiteral() { + a := func(f func()) error { + f() + f() + return nil + } + + b := func(f func()) bool { + f() + f() + return true + } + + check(LINE, 1) + a(func() { + check(LINE, 2) + }) + + if err := a(func() { + check(LINE, 2) + }); err != nil { + } + + switch b(func() { + check(LINE, 2) + }) { + } + + x := 2 + switch x { + case func() int { check(LINE, 1); return 1 }(): + check(LINE, 0) + panic("2=1") + case func() int { check(LINE, 1); return 2 }(): + check(LINE, 1) + case func() int { check(LINE, 0); return 3 }(): + check(LINE, 0) + panic("2=3") + } +} + +func testGoto() { + for i := 0; i < 2; i++ { + if i == 0 { + goto Label + } + check(LINE, 1) + Label: + check(LINE, 2) + } + // Now test that we don't inject empty statements + // between a label and a loop. +loop: + for { + check(LINE, 1) + break loop + } +} + +// This comment didn't appear in generated go code. +func haha() { + // Needed for cover to add counter increment here. + _ = 42 +} + +// Some someFunction. +// +//go:nosplit +func someFunction() { +} -- cgit v1.2.3