| // 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.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{}}")) | |
| 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) | |
| } | |
| } |