|  | // 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" | 
|  | "flag" | 
|  | "fmt" | 
|  | "go/ast" | 
|  | "go/parser" | 
|  | "go/token" | 
|  | "internal/testenv" | 
|  | "io/ioutil" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "strings" | 
|  | "sync" | 
|  | "testing" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | // Data directory, also the package directory for the test. | 
|  | testdata = "testdata" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | // Input files. | 
|  | testMain       = filepath.Join(testdata, "main.go") | 
|  | testTest       = filepath.Join(testdata, "test.go") | 
|  | coverProfile   = filepath.Join(testdata, "profile.cov") | 
|  | toolexecSource = filepath.Join(testdata, "toolexec.go") | 
|  |  | 
|  | // The HTML test files are in a separate directory | 
|  | // so they are a complete package. | 
|  | htmlGolden = filepath.Join(testdata, "html", "html.golden") | 
|  |  | 
|  | // Temporary files. | 
|  | tmpTestMain    string | 
|  | coverInput     string | 
|  | coverOutput    string | 
|  | htmlProfile    string | 
|  | htmlHTML       string | 
|  | htmlUDir       string | 
|  | htmlU          string | 
|  | htmlUTest      string | 
|  | htmlUProfile   string | 
|  | htmlUHTML      string | 
|  | lineDupDir     string | 
|  | lineDupGo      string | 
|  | lineDupTestGo  string | 
|  | lineDupProfile string | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | // testTempDir is a temporary directory created in TestMain. | 
|  | testTempDir string | 
|  |  | 
|  | // testcover is a newly built version of the cover program. | 
|  | testcover string | 
|  |  | 
|  | // toolexec is a program to use as the go tool's -toolexec argument. | 
|  | toolexec string | 
|  |  | 
|  | // testcoverErr records an error building testcover or toolexec. | 
|  | testcoverErr error | 
|  |  | 
|  | // testcoverOnce is used to build testcover once. | 
|  | testcoverOnce sync.Once | 
|  |  | 
|  | // toolexecArg is the argument to pass to the go tool. | 
|  | toolexecArg string | 
|  | ) | 
|  |  | 
|  | var debug = flag.Bool("debug", false, "keep rewritten files for debugging") | 
|  |  | 
|  | // We use TestMain to set up a temporary directory and remove it when | 
|  | // the tests are done. | 
|  | func TestMain(m *testing.M) { | 
|  | dir, err := ioutil.TempDir("", "go-testcover") | 
|  | if err != nil { | 
|  | fmt.Fprintln(os.Stderr, err) | 
|  | os.Exit(1) | 
|  | } | 
|  | os.Setenv("GOPATH", filepath.Join(dir, "_gopath")) | 
|  |  | 
|  | testTempDir = dir | 
|  |  | 
|  | tmpTestMain = filepath.Join(dir, "main.go") | 
|  | coverInput = filepath.Join(dir, "test_line.go") | 
|  | coverOutput = filepath.Join(dir, "test_cover.go") | 
|  | htmlProfile = filepath.Join(dir, "html.cov") | 
|  | htmlHTML = filepath.Join(dir, "html.html") | 
|  | 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") | 
|  | lineDupDir = filepath.Join(dir, "linedup") | 
|  | lineDupGo = filepath.Join(lineDupDir, "linedup.go") | 
|  | lineDupTestGo = filepath.Join(lineDupDir, "linedup_test.go") | 
|  | lineDupProfile = filepath.Join(lineDupDir, "linedup.out") | 
|  |  | 
|  | status := m.Run() | 
|  |  | 
|  | if !*debug { | 
|  | os.RemoveAll(dir) | 
|  | } | 
|  |  | 
|  | os.Exit(status) | 
|  | } | 
|  |  | 
|  | // buildCover builds a version of the cover program for testing. | 
|  | // This ensures that "go test cmd/cover" tests the current cmd/cover. | 
|  | func buildCover(t *testing.T) { | 
|  | t.Helper() | 
|  | testenv.MustHaveGoBuild(t) | 
|  | testcoverOnce.Do(func() { | 
|  | var wg sync.WaitGroup | 
|  | wg.Add(2) | 
|  |  | 
|  | var err1, err2 error | 
|  | go func() { | 
|  | defer wg.Done() | 
|  | testcover = filepath.Join(testTempDir, "cover.exe") | 
|  | t.Logf("running [go build -o %s]", testcover) | 
|  | out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", testcover).CombinedOutput() | 
|  | if len(out) > 0 { | 
|  | t.Logf("%s", out) | 
|  | } | 
|  | err1 = err | 
|  | }() | 
|  |  | 
|  | go func() { | 
|  | defer wg.Done() | 
|  | toolexec = filepath.Join(testTempDir, "toolexec.exe") | 
|  | t.Logf("running [go -build -o %s %s]", toolexec, toolexecSource) | 
|  | out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", toolexec, toolexecSource).CombinedOutput() | 
|  | if len(out) > 0 { | 
|  | t.Logf("%s", out) | 
|  | } | 
|  | err2 = err | 
|  | }() | 
|  |  | 
|  | wg.Wait() | 
|  |  | 
|  | testcoverErr = err1 | 
|  | if err2 != nil && err1 == nil { | 
|  | testcoverErr = err2 | 
|  | } | 
|  |  | 
|  | toolexecArg = "-toolexec=" + toolexec + " " + testcover | 
|  | }) | 
|  | if testcoverErr != nil { | 
|  | t.Fatal("failed to build testcover or toolexec program:", testcoverErr) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Run this shell script, but do it in Go so it can be run by "go test". | 
|  | // | 
|  | //	replace the word LINE with the line number < testdata/test.go > testdata/test_line.go | 
|  | // 	go build -o testcover | 
|  | // 	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) { | 
|  | t.Parallel() | 
|  | testenv.MustHaveGoRun(t) | 
|  | buildCover(t) | 
|  |  | 
|  | // Read in the test file (testTest) and write it, with LINEs specified, to coverInput. | 
|  | file, err := ioutil.ReadFile(testTest) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | lines := bytes.Split(file, []byte("\n")) | 
|  | for i, line := range lines { | 
|  | lines[i] = bytes.Replace(line, []byte("LINE"), []byte(fmt.Sprint(i+1)), -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{}}")) | 
|  |  | 
|  | if err := ioutil.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 | 
|  | cmd := exec.Command(testcover, "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput) | 
|  | run(cmd, t) | 
|  |  | 
|  | cmd = exec.Command(testcover, "-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 testTempDir, so that it is in the same directory | 
|  | // as coverOutput. | 
|  | b, err := ioutil.ReadFile(testMain) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := ioutil.WriteFile(tmpTestMain, b, 0444); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | // go run ./testdata/main.go ./testdata/test.go | 
|  | cmd = exec.Command(testenv.GoToolPath(t), "run", tmpTestMain, coverOutput) | 
|  | run(cmd, t) | 
|  |  | 
|  | file, err = ioutil.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) { | 
|  | t.Parallel() | 
|  | buildCover(t) | 
|  |  | 
|  | // 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 := ioutil.ReadFile(testDirectives) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | sourceDirectives := findDirectives(source) | 
|  |  | 
|  | // testcover -mode=atomic ./testdata/directives.go | 
|  | cmd := exec.Command(testcover, "-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) { | 
|  | t.Parallel() | 
|  | buildCover(t) | 
|  | // testcover -func ./testdata/profile.cov | 
|  | cmd := exec.Command(testcover, "-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) { | 
|  | t.Parallel() | 
|  | testenv.MustHaveGoRun(t) | 
|  | buildCover(t) | 
|  |  | 
|  | // go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html | 
|  | cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html") | 
|  | run(cmd, t) | 
|  | // testcover -html testdata/html/html.cov -o testdata/html/html.html | 
|  | cmd = exec.Command(testcover, "-html", htmlProfile, "-o", htmlHTML) | 
|  | run(cmd, t) | 
|  |  | 
|  | // Extract the parts of the HTML with comment markers, | 
|  | // and compare against a golden file. | 
|  | entireHTML, err := ioutil.ReadFile(htmlHTML) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | var out bytes.Buffer | 
|  | 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()) | 
|  | } | 
|  | golden, err := ioutil.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) { | 
|  | t.Parallel() | 
|  | testenv.MustHaveGoRun(t) | 
|  | buildCover(t) | 
|  |  | 
|  | if err := os.Mkdir(htmlUDir, 0777); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | if err := ioutil.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 := ioutil.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := ioutil.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | // go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov | 
|  | cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile) | 
|  | cmd.Dir = htmlUDir | 
|  | run(cmd, t) | 
|  |  | 
|  | // testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html | 
|  | cmd = exec.Command(testcover, "-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) { | 
|  | t.Parallel() | 
|  | testenv.MustHaveGoRun(t) | 
|  | buildCover(t) | 
|  |  | 
|  | if err := os.Mkdir(lineDupDir, 0777); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | if err := ioutil.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := ioutil.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := ioutil.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | // go test -cover -covermode count -coverprofile TMPDIR/linedup.out | 
|  | cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile) | 
|  | cmd.Dir = lineDupDir | 
|  | run(cmd, t) | 
|  |  | 
|  | // testcover -func=TMPDIR/linedup.out | 
|  | cmd = exec.Command(testcover, "-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) | 
|  | } | 
|  | } |