Source file src/cmd/cover/cover_test.go

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"flag"
    11  	"fmt"
    12  	"go/ast"
    13  	"go/parser"
    14  	"go/token"
    15  	"internal/testenv"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"regexp"
    20  	"strings"
    21  	"sync"
    22  	"testing"
    23  )
    24  
    25  const (
    26  	// Data directory, also the package directory for the test.
    27  	testdata = "testdata"
    28  )
    29  
    30  var (
    31  	// Input files.
    32  	testMain       = filepath.Join(testdata, "main.go")
    33  	testTest       = filepath.Join(testdata, "test.go")
    34  	coverProfile   = filepath.Join(testdata, "profile.cov")
    35  	toolexecSource = filepath.Join(testdata, "toolexec.go")
    36  
    37  	// The HTML test files are in a separate directory
    38  	// so they are a complete package.
    39  	htmlGolden = filepath.Join(testdata, "html", "html.golden")
    40  
    41  	// Temporary files.
    42  	tmpTestMain    string
    43  	coverInput     string
    44  	coverOutput    string
    45  	htmlProfile    string
    46  	htmlHTML       string
    47  	htmlUDir       string
    48  	htmlU          string
    49  	htmlUTest      string
    50  	htmlUProfile   string
    51  	htmlUHTML      string
    52  	lineDupDir     string
    53  	lineDupGo      string
    54  	lineDupTestGo  string
    55  	lineDupProfile string
    56  )
    57  
    58  var (
    59  	// testTempDir is a temporary directory created in TestMain.
    60  	testTempDir string
    61  
    62  	// testcover is a newly built version of the cover program.
    63  	testcover string
    64  
    65  	// toolexec is a program to use as the go tool's -toolexec argument.
    66  	toolexec string
    67  
    68  	// testcoverErr records an error building testcover or toolexec.
    69  	testcoverErr error
    70  
    71  	// testcoverOnce is used to build testcover once.
    72  	testcoverOnce sync.Once
    73  
    74  	// toolexecArg is the argument to pass to the go tool.
    75  	toolexecArg string
    76  )
    77  
    78  var debug = flag.Bool("debug", false, "keep rewritten files for debugging")
    79  
    80  // We use TestMain to set up a temporary directory and remove it when
    81  // the tests are done.
    82  func TestMain(m *testing.M) {
    83  	dir, err := os.MkdirTemp("", "go-testcover")
    84  	if err != nil {
    85  		fmt.Fprintln(os.Stderr, err)
    86  		os.Exit(1)
    87  	}
    88  	os.Setenv("GOPATH", filepath.Join(dir, "_gopath"))
    89  
    90  	testTempDir = dir
    91  
    92  	tmpTestMain = filepath.Join(dir, "main.go")
    93  	coverInput = filepath.Join(dir, "test_line.go")
    94  	coverOutput = filepath.Join(dir, "test_cover.go")
    95  	htmlProfile = filepath.Join(dir, "html.cov")
    96  	htmlHTML = filepath.Join(dir, "html.html")
    97  	htmlUDir = filepath.Join(dir, "htmlunformatted")
    98  	htmlU = filepath.Join(htmlUDir, "htmlunformatted.go")
    99  	htmlUTest = filepath.Join(htmlUDir, "htmlunformatted_test.go")
   100  	htmlUProfile = filepath.Join(htmlUDir, "htmlunformatted.cov")
   101  	htmlUHTML = filepath.Join(htmlUDir, "htmlunformatted.html")
   102  	lineDupDir = filepath.Join(dir, "linedup")
   103  	lineDupGo = filepath.Join(lineDupDir, "linedup.go")
   104  	lineDupTestGo = filepath.Join(lineDupDir, "linedup_test.go")
   105  	lineDupProfile = filepath.Join(lineDupDir, "linedup.out")
   106  
   107  	status := m.Run()
   108  
   109  	if !*debug {
   110  		os.RemoveAll(dir)
   111  	}
   112  
   113  	os.Exit(status)
   114  }
   115  
   116  // buildCover builds a version of the cover program for testing.
   117  // This ensures that "go test cmd/cover" tests the current cmd/cover.
   118  func buildCover(t *testing.T) {
   119  	t.Helper()
   120  	testenv.MustHaveGoBuild(t)
   121  	testcoverOnce.Do(func() {
   122  		var wg sync.WaitGroup
   123  		wg.Add(2)
   124  
   125  		var err1, err2 error
   126  		go func() {
   127  			defer wg.Done()
   128  			testcover = filepath.Join(testTempDir, "cover.exe")
   129  			t.Logf("running [go build -o %s]", testcover)
   130  			out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", testcover).CombinedOutput()
   131  			if len(out) > 0 {
   132  				t.Logf("%s", out)
   133  			}
   134  			err1 = err
   135  		}()
   136  
   137  		go func() {
   138  			defer wg.Done()
   139  			toolexec = filepath.Join(testTempDir, "toolexec.exe")
   140  			t.Logf("running [go -build -o %s %s]", toolexec, toolexecSource)
   141  			out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", toolexec, toolexecSource).CombinedOutput()
   142  			if len(out) > 0 {
   143  				t.Logf("%s", out)
   144  			}
   145  			err2 = err
   146  		}()
   147  
   148  		wg.Wait()
   149  
   150  		testcoverErr = err1
   151  		if err2 != nil && err1 == nil {
   152  			testcoverErr = err2
   153  		}
   154  
   155  		toolexecArg = "-toolexec=" + toolexec + " " + testcover
   156  	})
   157  	if testcoverErr != nil {
   158  		t.Fatal("failed to build testcover or toolexec program:", testcoverErr)
   159  	}
   160  }
   161  
   162  // Run this shell script, but do it in Go so it can be run by "go test".
   163  //
   164  //	replace the word LINE with the line number < testdata/test.go > testdata/test_line.go
   165  // 	go build -o testcover
   166  // 	testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go
   167  //	go run ./testdata/main.go ./testdata/test.go
   168  //
   169  func TestCover(t *testing.T) {
   170  	t.Parallel()
   171  	testenv.MustHaveGoRun(t)
   172  	buildCover(t)
   173  
   174  	// Read in the test file (testTest) and write it, with LINEs specified, to coverInput.
   175  	file, err := os.ReadFile(testTest)
   176  	if err != nil {
   177  		t.Fatal(err)
   178  	}
   179  	lines := bytes.Split(file, []byte("\n"))
   180  	for i, line := range lines {
   181  		lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1)))
   182  	}
   183  
   184  	// Add a function that is not gofmt'ed. This used to cause a crash.
   185  	// We don't put it in test.go because then we would have to gofmt it.
   186  	// Issue 23927.
   187  	lines = append(lines, []byte("func unFormatted() {"),
   188  		[]byte("\tif true {"),
   189  		[]byte("\t}else{"),
   190  		[]byte("\t}"),
   191  		[]byte("}"))
   192  	lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}"))
   193  
   194  	if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil {
   195  		t.Fatal(err)
   196  	}
   197  
   198  	// testcover -mode=count -var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest -o ./testdata/test_cover.go testdata/test_line.go
   199  	cmd := exec.Command(testcover, "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput)
   200  	run(cmd, t)
   201  
   202  	cmd = exec.Command(testcover, "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput)
   203  	err = cmd.Run()
   204  	if err == nil {
   205  		t.Error("Expected cover to fail with an error")
   206  	}
   207  
   208  	// Copy testmain to testTempDir, so that it is in the same directory
   209  	// as coverOutput.
   210  	b, err := os.ReadFile(testMain)
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	if err := os.WriteFile(tmpTestMain, b, 0444); err != nil {
   215  		t.Fatal(err)
   216  	}
   217  
   218  	// go run ./testdata/main.go ./testdata/test.go
   219  	cmd = exec.Command(testenv.GoToolPath(t), "run", tmpTestMain, coverOutput)
   220  	run(cmd, t)
   221  
   222  	file, err = os.ReadFile(coverOutput)
   223  	if err != nil {
   224  		t.Fatal(err)
   225  	}
   226  	// compiler directive must appear right next to function declaration.
   227  	if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
   228  		t.Error("misplaced compiler directive")
   229  	}
   230  	// "go:linkname" compiler directive should be present.
   231  	if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
   232  		t.Error("'go:linkname' compiler directive not found")
   233  	}
   234  
   235  	// Other comments should be preserved too.
   236  	c := ".*// This comment didn't appear in generated go code.*"
   237  	if got, err := regexp.MatchString(c, string(file)); err != nil || !got {
   238  		t.Errorf("non compiler directive comment %q not found", c)
   239  	}
   240  }
   241  
   242  // TestDirectives checks that compiler directives are preserved and positioned
   243  // correctly. Directives that occur before top-level declarations should remain
   244  // above those declarations, even if they are not part of the block of
   245  // documentation comments.
   246  func TestDirectives(t *testing.T) {
   247  	t.Parallel()
   248  	buildCover(t)
   249  
   250  	// Read the source file and find all the directives. We'll keep
   251  	// track of whether each one has been seen in the output.
   252  	testDirectives := filepath.Join(testdata, "directives.go")
   253  	source, err := os.ReadFile(testDirectives)
   254  	if err != nil {
   255  		t.Fatal(err)
   256  	}
   257  	sourceDirectives := findDirectives(source)
   258  
   259  	// testcover -mode=atomic ./testdata/directives.go
   260  	cmd := exec.Command(testcover, "-mode=atomic", testDirectives)
   261  	cmd.Stderr = os.Stderr
   262  	output, err := cmd.Output()
   263  	if err != nil {
   264  		t.Fatal(err)
   265  	}
   266  
   267  	// Check that all directives are present in the output.
   268  	outputDirectives := findDirectives(output)
   269  	foundDirective := make(map[string]bool)
   270  	for _, p := range sourceDirectives {
   271  		foundDirective[p.name] = false
   272  	}
   273  	for _, p := range outputDirectives {
   274  		if found, ok := foundDirective[p.name]; !ok {
   275  			t.Errorf("unexpected directive in output: %s", p.text)
   276  		} else if found {
   277  			t.Errorf("directive found multiple times in output: %s", p.text)
   278  		}
   279  		foundDirective[p.name] = true
   280  	}
   281  	for name, found := range foundDirective {
   282  		if !found {
   283  			t.Errorf("missing directive: %s", name)
   284  		}
   285  	}
   286  
   287  	// Check that directives that start with the name of top-level declarations
   288  	// come before the beginning of the named declaration and after the end
   289  	// of the previous declaration.
   290  	fset := token.NewFileSet()
   291  	astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  
   296  	prevEnd := 0
   297  	for _, decl := range astFile.Decls {
   298  		var name string
   299  		switch d := decl.(type) {
   300  		case *ast.FuncDecl:
   301  			name = d.Name.Name
   302  		case *ast.GenDecl:
   303  			if len(d.Specs) == 0 {
   304  				// An empty group declaration. We still want to check that
   305  				// directives can be associated with it, so we make up a name
   306  				// to match directives in the test data.
   307  				name = "_empty"
   308  			} else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
   309  				name = spec.Name.Name
   310  			}
   311  		}
   312  		pos := fset.Position(decl.Pos()).Offset
   313  		end := fset.Position(decl.End()).Offset
   314  		if name == "" {
   315  			prevEnd = end
   316  			continue
   317  		}
   318  		for _, p := range outputDirectives {
   319  			if !strings.HasPrefix(p.name, name) {
   320  				continue
   321  			}
   322  			if p.offset < prevEnd || pos < p.offset {
   323  				t.Errorf("directive %s does not appear before definition %s", p.text, name)
   324  			}
   325  		}
   326  		prevEnd = end
   327  	}
   328  }
   329  
   330  type directiveInfo struct {
   331  	text   string // full text of the comment, not including newline
   332  	name   string // text after //go:
   333  	offset int    // byte offset of first slash in comment
   334  }
   335  
   336  func findDirectives(source []byte) []directiveInfo {
   337  	var directives []directiveInfo
   338  	directivePrefix := []byte("\n//go:")
   339  	offset := 0
   340  	for {
   341  		i := bytes.Index(source[offset:], directivePrefix)
   342  		if i < 0 {
   343  			break
   344  		}
   345  		i++ // skip newline
   346  		p := source[offset+i:]
   347  		j := bytes.IndexByte(p, '\n')
   348  		if j < 0 {
   349  			// reached EOF
   350  			j = len(p)
   351  		}
   352  		directive := directiveInfo{
   353  			text:   string(p[:j]),
   354  			name:   string(p[len(directivePrefix)-1 : j]),
   355  			offset: offset + i,
   356  		}
   357  		directives = append(directives, directive)
   358  		offset += i + j
   359  	}
   360  	return directives
   361  }
   362  
   363  // Makes sure that `cover -func=profile.cov` reports accurate coverage.
   364  // Issue #20515.
   365  func TestCoverFunc(t *testing.T) {
   366  	t.Parallel()
   367  	buildCover(t)
   368  	// testcover -func ./testdata/profile.cov
   369  	cmd := exec.Command(testcover, "-func", coverProfile)
   370  	out, err := cmd.Output()
   371  	if err != nil {
   372  		if ee, ok := err.(*exec.ExitError); ok {
   373  			t.Logf("%s", ee.Stderr)
   374  		}
   375  		t.Fatal(err)
   376  	}
   377  
   378  	if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got {
   379  		t.Logf("%s", out)
   380  		t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err)
   381  	}
   382  }
   383  
   384  // Check that cover produces correct HTML.
   385  // Issue #25767.
   386  func TestCoverHTML(t *testing.T) {
   387  	t.Parallel()
   388  	testenv.MustHaveGoRun(t)
   389  	buildCover(t)
   390  
   391  	// go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html
   392  	cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html")
   393  	run(cmd, t)
   394  	// testcover -html testdata/html/html.cov -o testdata/html/html.html
   395  	cmd = exec.Command(testcover, "-html", htmlProfile, "-o", htmlHTML)
   396  	run(cmd, t)
   397  
   398  	// Extract the parts of the HTML with comment markers,
   399  	// and compare against a golden file.
   400  	entireHTML, err := os.ReadFile(htmlHTML)
   401  	if err != nil {
   402  		t.Fatal(err)
   403  	}
   404  	var out bytes.Buffer
   405  	scan := bufio.NewScanner(bytes.NewReader(entireHTML))
   406  	in := false
   407  	for scan.Scan() {
   408  		line := scan.Text()
   409  		if strings.Contains(line, "// START") {
   410  			in = true
   411  		}
   412  		if in {
   413  			fmt.Fprintln(&out, line)
   414  		}
   415  		if strings.Contains(line, "// END") {
   416  			in = false
   417  		}
   418  	}
   419  	if scan.Err() != nil {
   420  		t.Error(scan.Err())
   421  	}
   422  	golden, err := os.ReadFile(htmlGolden)
   423  	if err != nil {
   424  		t.Fatalf("reading golden file: %v", err)
   425  	}
   426  	// Ignore white space differences.
   427  	// Break into lines, then compare by breaking into words.
   428  	goldenLines := strings.Split(string(golden), "\n")
   429  	outLines := strings.Split(out.String(), "\n")
   430  	// Compare at the line level, stopping at first different line so
   431  	// we don't generate tons of output if there's an inserted or deleted line.
   432  	for i, goldenLine := range goldenLines {
   433  		if i >= len(outLines) {
   434  			t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine)
   435  		}
   436  		// Convert all white space to simple spaces, for easy comparison.
   437  		goldenLine = strings.Join(strings.Fields(goldenLine), " ")
   438  		outLine := strings.Join(strings.Fields(outLines[i]), " ")
   439  		if outLine != goldenLine {
   440  			t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine)
   441  		}
   442  	}
   443  	if len(goldenLines) != len(outLines) {
   444  		t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)])
   445  	}
   446  }
   447  
   448  // Test HTML processing with a source file not run through gofmt.
   449  // Issue #27350.
   450  func TestHtmlUnformatted(t *testing.T) {
   451  	t.Parallel()
   452  	testenv.MustHaveGoRun(t)
   453  	buildCover(t)
   454  
   455  	if err := os.Mkdir(htmlUDir, 0777); err != nil {
   456  		t.Fatal(err)
   457  	}
   458  
   459  	if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil {
   460  		t.Fatal(err)
   461  	}
   462  
   463  	const htmlUContents = `
   464  package htmlunformatted
   465  
   466  var g int
   467  
   468  func F() {
   469  //line x.go:1
   470  	{ { F(); goto lab } }
   471  lab:
   472  }`
   473  
   474  	const htmlUTestContents = `package htmlunformatted`
   475  
   476  	if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil {
   477  		t.Fatal(err)
   478  	}
   479  	if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil {
   480  		t.Fatal(err)
   481  	}
   482  
   483  	// go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov
   484  	cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile)
   485  	cmd.Dir = htmlUDir
   486  	run(cmd, t)
   487  
   488  	// testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html
   489  	cmd = exec.Command(testcover, "-html", htmlUProfile, "-o", htmlUHTML)
   490  	cmd.Dir = htmlUDir
   491  	run(cmd, t)
   492  }
   493  
   494  // lineDupContents becomes linedup.go in TestFuncWithDuplicateLines.
   495  const lineDupContents = `
   496  package linedup
   497  
   498  var G int
   499  
   500  func LineDup(c int) {
   501  	for i := 0; i < c; i++ {
   502  //line ld.go:100
   503  		if i % 2 == 0 {
   504  			G++
   505  		}
   506  		if i % 3 == 0 {
   507  			G++; G++
   508  		}
   509  //line ld.go:100
   510  		if i % 4 == 0 {
   511  			G++; G++; G++
   512  		}
   513  		if i % 5 == 0 {
   514  			G++; G++; G++; G++
   515  		}
   516  	}
   517  }
   518  `
   519  
   520  // lineDupTestContents becomes linedup_test.go in TestFuncWithDuplicateLines.
   521  const lineDupTestContents = `
   522  package linedup
   523  
   524  import "testing"
   525  
   526  func TestLineDup(t *testing.T) {
   527  	LineDup(100)
   528  }
   529  `
   530  
   531  // Test -func with duplicate //line directives with different numbers
   532  // of statements.
   533  func TestFuncWithDuplicateLines(t *testing.T) {
   534  	t.Parallel()
   535  	testenv.MustHaveGoRun(t)
   536  	buildCover(t)
   537  
   538  	if err := os.Mkdir(lineDupDir, 0777); err != nil {
   539  		t.Fatal(err)
   540  	}
   541  
   542  	if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil {
   543  		t.Fatal(err)
   544  	}
   545  	if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil {
   546  		t.Fatal(err)
   547  	}
   548  	if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil {
   549  		t.Fatal(err)
   550  	}
   551  
   552  	// go test -cover -covermode count -coverprofile TMPDIR/linedup.out
   553  	cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile)
   554  	cmd.Dir = lineDupDir
   555  	run(cmd, t)
   556  
   557  	// testcover -func=TMPDIR/linedup.out
   558  	cmd = exec.Command(testcover, "-func", lineDupProfile)
   559  	cmd.Dir = lineDupDir
   560  	run(cmd, t)
   561  }
   562  
   563  func run(c *exec.Cmd, t *testing.T) {
   564  	t.Helper()
   565  	t.Log("running", c.Args)
   566  	out, err := c.CombinedOutput()
   567  	if len(out) > 0 {
   568  		t.Logf("%s", out)
   569  	}
   570  	if err != nil {
   571  		t.Fatal(err)
   572  	}
   573  }
   574  

View as plain text