blob: 86c95d15c5d28f808eda67ea84ca121c946a9577 [file] [log] [blame]
// 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"
"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 := os.MkdirTemp("", "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 := 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{}}"))
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
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 := os.ReadFile(testMain)
if err != nil {
t.Fatal(err)
}
if err := os.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 = 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) {
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 := os.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 := os.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 := 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) {
t.Parallel()
testenv.MustHaveGoRun(t)
buildCover(t)
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 := 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 := 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 := 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)
}
}