Source file
    src/cmd/cover/cover_test.go
  
  
     1  
     2  
     3  
     4  
     5  package main_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	cmdcover "cmd/cover"
    11  	"flag"
    12  	"fmt"
    13  	"go/ast"
    14  	"go/parser"
    15  	"go/token"
    16  	"internal/testenv"
    17  	"log"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strings"
    23  	"sync"
    24  	"testing"
    25  )
    26  
    27  const (
    28  	
    29  	testdata = "testdata"
    30  )
    31  
    32  
    33  
    34  
    35  func testcover(t testing.TB) string {
    36  	return testenv.Executable(t)
    37  }
    38  
    39  
    40  var testTempDir string
    41  
    42  
    43  var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
    44  
    45  
    46  
    47  
    48  func TestMain(m *testing.M) {
    49  	if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
    50  		
    51  		
    52  		tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
    53  		if tool == "cover" {
    54  			
    55  			
    56  			
    57  			
    58  			os.Args = os.Args[1:]
    59  			cmdcover.Main()
    60  		} else {
    61  			cmd := exec.Command(os.Args[1], os.Args[2:]...)
    62  			cmd.Stdout = os.Stdout
    63  			cmd.Stderr = os.Stderr
    64  			if err := cmd.Run(); err != nil {
    65  				os.Exit(1)
    66  			}
    67  		}
    68  		os.Exit(0)
    69  	}
    70  	if os.Getenv("CMDCOVER_TEST_RUN_MAIN") != "" {
    71  		
    72  		
    73  		
    74  		
    75  		cmdcover.Main()
    76  		os.Exit(0)
    77  	}
    78  	flag.Parse()
    79  	topTmpdir, err := os.MkdirTemp("", "cmd-cover-test-")
    80  	if err != nil {
    81  		log.Fatal(err)
    82  	}
    83  	testTempDir = topTmpdir
    84  	if !*debug {
    85  		defer os.RemoveAll(topTmpdir)
    86  	} else {
    87  		fmt.Fprintf(os.Stderr, "debug: preserving tmpdir %s\n", topTmpdir)
    88  	}
    89  	os.Setenv("CMDCOVER_TEST_RUN_MAIN", "normal")
    90  	os.Exit(m.Run())
    91  }
    92  
    93  var tdmu sync.Mutex
    94  var tdcount int
    95  
    96  func tempDir(t *testing.T) string {
    97  	tdmu.Lock()
    98  	dir := filepath.Join(testTempDir, fmt.Sprintf("%03d", tdcount))
    99  	tdcount++
   100  	if err := os.Mkdir(dir, 0777); err != nil {
   101  		t.Fatal(err)
   102  	}
   103  	defer tdmu.Unlock()
   104  	return dir
   105  }
   106  
   107  
   108  
   109  
   110  func TestCoverWithToolExec(t *testing.T) {
   111  	toolexecArg := "-toolexec=" + testcover(t)
   112  
   113  	t.Run("CoverHTML", func(t *testing.T) {
   114  		testCoverHTML(t, toolexecArg)
   115  	})
   116  	t.Run("HtmlUnformatted", func(t *testing.T) {
   117  		testHtmlUnformatted(t, toolexecArg)
   118  	})
   119  	t.Run("FuncWithDuplicateLines", func(t *testing.T) {
   120  		testFuncWithDuplicateLines(t, toolexecArg)
   121  	})
   122  	t.Run("MissingTrailingNewlineIssue58370", func(t *testing.T) {
   123  		testMissingTrailingNewlineIssue58370(t, toolexecArg)
   124  	})
   125  }
   126  
   127  
   128  
   129  
   130  
   131  
   132  func TestCover(t *testing.T) {
   133  	testenv.MustHaveGoRun(t)
   134  	t.Parallel()
   135  	dir := tempDir(t)
   136  
   137  	
   138  	testTest := filepath.Join(testdata, "test.go")
   139  	file, err := os.ReadFile(testTest)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	lines := bytes.Split(file, []byte("\n"))
   144  	for i, line := range lines {
   145  		lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1)))
   146  	}
   147  
   148  	
   149  	
   150  	
   151  	lines = append(lines, []byte("func unFormatted() {"),
   152  		[]byte("\tif true {"),
   153  		[]byte("\t}else{"),
   154  		[]byte("\t}"),
   155  		[]byte("}"))
   156  	lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}"))
   157  
   158  	coverInput := filepath.Join(dir, "test_line.go")
   159  	if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil {
   160  		t.Fatal(err)
   161  	}
   162  
   163  	
   164  	coverOutput := filepath.Join(dir, "test_cover.go")
   165  	cmd := testenv.Command(t, testcover(t), "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput)
   166  	run(cmd, t)
   167  
   168  	cmd = testenv.Command(t, testcover(t), "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput)
   169  	err = cmd.Run()
   170  	if err == nil {
   171  		t.Error("Expected cover to fail with an error")
   172  	}
   173  
   174  	
   175  	
   176  	testMain := filepath.Join(testdata, "main.go")
   177  	b, err := os.ReadFile(testMain)
   178  	if err != nil {
   179  		t.Fatal(err)
   180  	}
   181  	tmpTestMain := filepath.Join(dir, "main.go")
   182  	if err := os.WriteFile(tmpTestMain, b, 0444); err != nil {
   183  		t.Fatal(err)
   184  	}
   185  
   186  	
   187  	cmd = testenv.Command(t, testenv.GoToolPath(t), "run", tmpTestMain, coverOutput)
   188  	run(cmd, t)
   189  
   190  	file, err = os.ReadFile(coverOutput)
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  	
   195  	if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
   196  		t.Error("misplaced compiler directive")
   197  	}
   198  	
   199  	if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
   200  		t.Error("'go:linkname' compiler directive not found")
   201  	}
   202  
   203  	
   204  	c := ".*// This comment didn't appear in generated go code.*"
   205  	if got, err := regexp.MatchString(c, string(file)); err != nil || !got {
   206  		t.Errorf("non compiler directive comment %q not found", c)
   207  	}
   208  }
   209  
   210  
   211  
   212  
   213  
   214  func TestDirectives(t *testing.T) {
   215  	testenv.MustHaveExec(t)
   216  	t.Parallel()
   217  
   218  	
   219  	
   220  	testDirectives := filepath.Join(testdata, "directives.go")
   221  	source, err := os.ReadFile(testDirectives)
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  	sourceDirectives := findDirectives(source)
   226  
   227  	
   228  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", testDirectives)
   229  	cmd.Stderr = os.Stderr
   230  	output, err := cmd.Output()
   231  	if err != nil {
   232  		t.Fatal(err)
   233  	}
   234  
   235  	
   236  	outputDirectives := findDirectives(output)
   237  	foundDirective := make(map[string]bool)
   238  	for _, p := range sourceDirectives {
   239  		foundDirective[p.name] = false
   240  	}
   241  	for _, p := range outputDirectives {
   242  		if found, ok := foundDirective[p.name]; !ok {
   243  			t.Errorf("unexpected directive in output: %s", p.text)
   244  		} else if found {
   245  			t.Errorf("directive found multiple times in output: %s", p.text)
   246  		}
   247  		foundDirective[p.name] = true
   248  	}
   249  	for name, found := range foundDirective {
   250  		if !found {
   251  			t.Errorf("missing directive: %s", name)
   252  		}
   253  	}
   254  
   255  	
   256  	
   257  	
   258  	fset := token.NewFileSet()
   259  	astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  
   264  	prevEnd := 0
   265  	for _, decl := range astFile.Decls {
   266  		var name string
   267  		switch d := decl.(type) {
   268  		case *ast.FuncDecl:
   269  			name = d.Name.Name
   270  		case *ast.GenDecl:
   271  			if len(d.Specs) == 0 {
   272  				
   273  				
   274  				
   275  				name = "_empty"
   276  			} else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
   277  				name = spec.Name.Name
   278  			}
   279  		}
   280  		pos := fset.Position(decl.Pos()).Offset
   281  		end := fset.Position(decl.End()).Offset
   282  		if name == "" {
   283  			prevEnd = end
   284  			continue
   285  		}
   286  		for _, p := range outputDirectives {
   287  			if !strings.HasPrefix(p.name, name) {
   288  				continue
   289  			}
   290  			if p.offset < prevEnd || pos < p.offset {
   291  				t.Errorf("directive %s does not appear before definition %s", p.text, name)
   292  			}
   293  		}
   294  		prevEnd = end
   295  	}
   296  }
   297  
   298  type directiveInfo struct {
   299  	text   string 
   300  	name   string 
   301  	offset int    
   302  }
   303  
   304  func findDirectives(source []byte) []directiveInfo {
   305  	var directives []directiveInfo
   306  	directivePrefix := []byte("\n//go:")
   307  	offset := 0
   308  	for {
   309  		i := bytes.Index(source[offset:], directivePrefix)
   310  		if i < 0 {
   311  			break
   312  		}
   313  		i++ 
   314  		p := source[offset+i:]
   315  		j := bytes.IndexByte(p, '\n')
   316  		if j < 0 {
   317  			
   318  			j = len(p)
   319  		}
   320  		directive := directiveInfo{
   321  			text:   string(p[:j]),
   322  			name:   string(p[len(directivePrefix)-1 : j]),
   323  			offset: offset + i,
   324  		}
   325  		directives = append(directives, directive)
   326  		offset += i + j
   327  	}
   328  	return directives
   329  }
   330  
   331  
   332  
   333  func TestCoverFunc(t *testing.T) {
   334  	
   335  	coverProfile := filepath.Join(testdata, "profile.cov")
   336  	cmd := testenv.Command(t, testcover(t), "-func", coverProfile)
   337  	out, err := cmd.Output()
   338  	if err != nil {
   339  		if ee, ok := err.(*exec.ExitError); ok {
   340  			t.Logf("%s", ee.Stderr)
   341  		}
   342  		t.Fatal(err)
   343  	}
   344  
   345  	if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got {
   346  		t.Logf("%s", out)
   347  		t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err)
   348  	}
   349  }
   350  
   351  
   352  
   353  func testCoverHTML(t *testing.T, toolexecArg string) {
   354  	testenv.MustHaveGoRun(t)
   355  	dir := tempDir(t)
   356  
   357  	t.Parallel()
   358  
   359  	
   360  	htmlProfile := filepath.Join(dir, "html.cov")
   361  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html")
   362  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   363  	run(cmd, t)
   364  	
   365  	htmlHTML := filepath.Join(dir, "html.html")
   366  	cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
   367  	run(cmd, t)
   368  
   369  	
   370  	
   371  	entireHTML, err := os.ReadFile(htmlHTML)
   372  	if err != nil {
   373  		t.Fatal(err)
   374  	}
   375  	var out strings.Builder
   376  	scan := bufio.NewScanner(bytes.NewReader(entireHTML))
   377  	in := false
   378  	for scan.Scan() {
   379  		line := scan.Text()
   380  		if strings.Contains(line, "// START") {
   381  			in = true
   382  		}
   383  		if in {
   384  			fmt.Fprintln(&out, line)
   385  		}
   386  		if strings.Contains(line, "// END") {
   387  			in = false
   388  		}
   389  	}
   390  	if scan.Err() != nil {
   391  		t.Error(scan.Err())
   392  	}
   393  	htmlGolden := filepath.Join(testdata, "html", "html.golden")
   394  	golden, err := os.ReadFile(htmlGolden)
   395  	if err != nil {
   396  		t.Fatalf("reading golden file: %v", err)
   397  	}
   398  	
   399  	
   400  	goldenLines := strings.Split(string(golden), "\n")
   401  	outLines := strings.Split(out.String(), "\n")
   402  	
   403  	
   404  	for i, goldenLine := range goldenLines {
   405  		if i >= len(outLines) {
   406  			t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine)
   407  		}
   408  		
   409  		goldenLine = strings.Join(strings.Fields(goldenLine), " ")
   410  		outLine := strings.Join(strings.Fields(outLines[i]), " ")
   411  		if outLine != goldenLine {
   412  			t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine)
   413  		}
   414  	}
   415  	if len(goldenLines) != len(outLines) {
   416  		t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)])
   417  	}
   418  }
   419  
   420  
   421  
   422  func testHtmlUnformatted(t *testing.T, toolexecArg string) {
   423  	testenv.MustHaveGoRun(t)
   424  	dir := tempDir(t)
   425  
   426  	t.Parallel()
   427  
   428  	htmlUDir := filepath.Join(dir, "htmlunformatted")
   429  	htmlU := filepath.Join(htmlUDir, "htmlunformatted.go")
   430  	htmlUTest := filepath.Join(htmlUDir, "htmlunformatted_test.go")
   431  	htmlUProfile := filepath.Join(htmlUDir, "htmlunformatted.cov")
   432  	htmlUHTML := filepath.Join(htmlUDir, "htmlunformatted.html")
   433  
   434  	if err := os.Mkdir(htmlUDir, 0777); err != nil {
   435  		t.Fatal(err)
   436  	}
   437  
   438  	if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil {
   439  		t.Fatal(err)
   440  	}
   441  
   442  	const htmlUContents = `
   443  package htmlunformatted
   444  
   445  var g int
   446  
   447  func F() {
   448  //line x.go:1
   449  	{ { F(); goto lab } }
   450  lab:
   451  }`
   452  
   453  	const htmlUTestContents = `package htmlunformatted`
   454  
   455  	if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil {
   456  		t.Fatal(err)
   457  	}
   458  	if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil {
   459  		t.Fatal(err)
   460  	}
   461  
   462  	
   463  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-test.v", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile)
   464  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   465  	cmd.Dir = htmlUDir
   466  	run(cmd, t)
   467  
   468  	
   469  	cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
   470  	cmd.Dir = htmlUDir
   471  	run(cmd, t)
   472  }
   473  
   474  
   475  const lineDupContents = `
   476  package linedup
   477  
   478  var G int
   479  
   480  func LineDup(c int) {
   481  	for i := 0; i < c; i++ {
   482  //line ld.go:100
   483  		if i % 2 == 0 {
   484  			G++
   485  		}
   486  		if i % 3 == 0 {
   487  			G++; G++
   488  		}
   489  //line ld.go:100
   490  		if i % 4 == 0 {
   491  			G++; G++; G++
   492  		}
   493  		if i % 5 == 0 {
   494  			G++; G++; G++; G++
   495  		}
   496  	}
   497  }
   498  `
   499  
   500  
   501  const lineDupTestContents = `
   502  package linedup
   503  
   504  import "testing"
   505  
   506  func TestLineDup(t *testing.T) {
   507  	LineDup(100)
   508  }
   509  `
   510  
   511  
   512  
   513  func testFuncWithDuplicateLines(t *testing.T, toolexecArg string) {
   514  	testenv.MustHaveGoRun(t)
   515  	dir := tempDir(t)
   516  
   517  	t.Parallel()
   518  
   519  	lineDupDir := filepath.Join(dir, "linedup")
   520  	lineDupGo := filepath.Join(lineDupDir, "linedup.go")
   521  	lineDupTestGo := filepath.Join(lineDupDir, "linedup_test.go")
   522  	lineDupProfile := filepath.Join(lineDupDir, "linedup.out")
   523  
   524  	if err := os.Mkdir(lineDupDir, 0777); err != nil {
   525  		t.Fatal(err)
   526  	}
   527  
   528  	if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil {
   529  		t.Fatal(err)
   530  	}
   531  	if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil {
   532  		t.Fatal(err)
   533  	}
   534  	if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil {
   535  		t.Fatal(err)
   536  	}
   537  
   538  	
   539  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile)
   540  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   541  	cmd.Dir = lineDupDir
   542  	run(cmd, t)
   543  
   544  	
   545  	cmd = testenv.Command(t, testcover(t), "-func", lineDupProfile)
   546  	cmd.Dir = lineDupDir
   547  	run(cmd, t)
   548  }
   549  
   550  func run(c *exec.Cmd, t *testing.T) {
   551  	t.Helper()
   552  	t.Log("running", c.Args)
   553  	out, err := c.CombinedOutput()
   554  	if len(out) > 0 {
   555  		t.Logf("%s", out)
   556  	}
   557  	if err != nil {
   558  		t.Fatal(err)
   559  	}
   560  }
   561  
   562  func runExpectingError(c *exec.Cmd, t *testing.T) string {
   563  	t.Helper()
   564  	t.Log("running", c.Args)
   565  	out, err := c.CombinedOutput()
   566  	if err == nil {
   567  		return fmt.Sprintf("unexpected pass for %+v", c.Args)
   568  	}
   569  	return string(out)
   570  }
   571  
   572  
   573  
   574  func testMissingTrailingNewlineIssue58370(t *testing.T, toolexecArg string) {
   575  	testenv.MustHaveGoBuild(t)
   576  	dir := tempDir(t)
   577  
   578  	t.Parallel()
   579  
   580  	noeolDir := filepath.Join(dir, "issue58370")
   581  	noeolGo := filepath.Join(noeolDir, "noeol.go")
   582  	noeolTestGo := filepath.Join(noeolDir, "noeol_test.go")
   583  
   584  	if err := os.Mkdir(noeolDir, 0777); err != nil {
   585  		t.Fatal(err)
   586  	}
   587  
   588  	if err := os.WriteFile(filepath.Join(noeolDir, "go.mod"), []byte("module noeol\n"), 0666); err != nil {
   589  		t.Fatal(err)
   590  	}
   591  	const noeolContents = `package noeol`
   592  	if err := os.WriteFile(noeolGo, []byte(noeolContents), 0444); err != nil {
   593  		t.Fatal(err)
   594  	}
   595  	const noeolTestContents = `
   596  package noeol
   597  import "testing"
   598  func TestCoverage(t *testing.T) { }
   599  `
   600  	if err := os.WriteFile(noeolTestGo, []byte(noeolTestContents), 0444); err != nil {
   601  		t.Fatal(err)
   602  	}
   603  
   604  	
   605  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-covermode", "atomic")
   606  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   607  	cmd.Dir = noeolDir
   608  	run(cmd, t)
   609  }
   610  
   611  func TestSrcPathWithNewline(t *testing.T) {
   612  	testenv.MustHaveExec(t)
   613  	t.Parallel()
   614  
   615  	
   616  	
   617  	srcPath := t.TempDir() + string(filepath.Separator) + "\npackage main\nfunc main() { panic(string([]rune{'u', 'h', '-', 'o', 'h'}))\n/*/main.go"
   618  	mainSrc := ` package main
   619  
   620  func main() {
   621  	/* nothing here */
   622  	println("ok")
   623  }
   624  `
   625  	if err := os.MkdirAll(filepath.Dir(srcPath), 0777); err != nil {
   626  		t.Skipf("creating directory with bogus path: %v", err)
   627  	}
   628  	if err := os.WriteFile(srcPath, []byte(mainSrc), 0666); err != nil {
   629  		t.Skipf("writing file with bogus directory: %v", err)
   630  	}
   631  
   632  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", srcPath)
   633  	cmd.Stderr = new(bytes.Buffer)
   634  	out, err := cmd.Output()
   635  	t.Logf("%v:\n%s", cmd, out)
   636  	t.Logf("stderr:\n%s", cmd.Stderr)
   637  	if err == nil {
   638  		t.Errorf("unexpected success; want failure due to newline in file path")
   639  	}
   640  }
   641  
View as plain text