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  	cmdcover "cmd/cover"
    11  	"cmp"
    12  	"flag"
    13  	"fmt"
    14  	"go/ast"
    15  	"go/parser"
    16  	"go/token"
    17  	"internal/testenv"
    18  	"log"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"regexp"
    23  	"slices"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  )
    29  
    30  const (
    31  	// Data directory, also the package directory for the test.
    32  	testdata = "testdata"
    33  )
    34  
    35  // testcover returns the path to the cmd/cover binary that we are going to
    36  // test. At one point this was created via "go build"; we now reuse the unit
    37  // test executable itself.
    38  func testcover(t testing.TB) string {
    39  	return testenv.Executable(t)
    40  }
    41  
    42  // testTempDir is a temporary directory created in TestMain.
    43  var testTempDir string
    44  
    45  // If set, this will preserve all the tmpdir files from the test run.
    46  var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
    47  
    48  // TestMain used here so that we can leverage the test executable
    49  // itself as a cmd/cover executable; compare to similar usage in
    50  // the cmd/go tests.
    51  func TestMain(m *testing.M) {
    52  	if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
    53  		// When CMDCOVER_TOOLEXEC is set, the test binary is also
    54  		// running as a -toolexec wrapper.
    55  		tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
    56  		if tool == "cover" {
    57  			// Inject this test binary as cmd/cover in place of the
    58  			// installed tool, so that the go command's invocations of
    59  			// cover produce coverage for the configuration in which
    60  			// the test was built.
    61  			os.Args = os.Args[1:]
    62  			cmdcover.Main()
    63  		} else {
    64  			cmd := exec.Command(os.Args[1], os.Args[2:]...)
    65  			cmd.Stdout = os.Stdout
    66  			cmd.Stderr = os.Stderr
    67  			if err := cmd.Run(); err != nil {
    68  				os.Exit(1)
    69  			}
    70  		}
    71  		os.Exit(0)
    72  	}
    73  	if os.Getenv("CMDCOVER_TEST_RUN_MAIN") != "" {
    74  		// When CMDCOVER_TEST_RUN_MAIN is set, we're reusing the test
    75  		// binary as cmd/cover. In this case we run the main func exported
    76  		// via export_test.go, and exit; CMDCOVER_TEST_RUN_MAIN is set below
    77  		// for actual test invocations.
    78  		cmdcover.Main()
    79  		os.Exit(0)
    80  	}
    81  	flag.Parse()
    82  	topTmpdir, err := os.MkdirTemp("", "cmd-cover-test-")
    83  	if err != nil {
    84  		log.Fatal(err)
    85  	}
    86  	testTempDir = topTmpdir
    87  	if !*debug {
    88  		defer os.RemoveAll(topTmpdir)
    89  	} else {
    90  		fmt.Fprintf(os.Stderr, "debug: preserving tmpdir %s\n", topTmpdir)
    91  	}
    92  	os.Setenv("CMDCOVER_TEST_RUN_MAIN", "normal")
    93  	m.Run()
    94  }
    95  
    96  var tdmu sync.Mutex
    97  var tdcount int
    98  
    99  func tempDir(t *testing.T) string {
   100  	tdmu.Lock()
   101  	dir := filepath.Join(testTempDir, fmt.Sprintf("%03d", tdcount))
   102  	tdcount++
   103  	if err := os.Mkdir(dir, 0777); err != nil {
   104  		t.Fatal(err)
   105  	}
   106  	defer tdmu.Unlock()
   107  	return dir
   108  }
   109  
   110  // TestCoverWithToolExec runs a set of subtests that all make use of a
   111  // "-toolexec" wrapper program to invoke the cover test executable
   112  // itself via "go test -cover".
   113  func TestCoverWithToolExec(t *testing.T) {
   114  	toolexecArg := "-toolexec=" + testcover(t)
   115  
   116  	t.Run("CoverHTML", func(t *testing.T) {
   117  		testCoverHTML(t, toolexecArg)
   118  	})
   119  	t.Run("HtmlUnformatted", func(t *testing.T) {
   120  		testHtmlUnformatted(t, toolexecArg)
   121  	})
   122  	t.Run("FuncWithDuplicateLines", func(t *testing.T) {
   123  		testFuncWithDuplicateLines(t, toolexecArg)
   124  	})
   125  	t.Run("MissingTrailingNewlineIssue58370", func(t *testing.T) {
   126  		testMissingTrailingNewlineIssue58370(t, toolexecArg)
   127  	})
   128  }
   129  
   130  // Execute this command sequence:
   131  //
   132  //	replace the word LINE with the line number < testdata/test.go > testdata/test_line.go
   133  //	testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go
   134  //	go run ./testdata/main.go ./testdata/test.go
   135  func TestCover(t *testing.T) {
   136  	testenv.MustHaveGoRun(t)
   137  	t.Parallel()
   138  	dir := tempDir(t)
   139  
   140  	// Read in the test file (testTest) and write it, with LINEs specified, to coverInput.
   141  	testTest := filepath.Join(testdata, "test.go")
   142  	file, err := os.ReadFile(testTest)
   143  	if err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	lines := bytes.Split(file, []byte("\n"))
   147  	for i, line := range lines {
   148  		lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1)))
   149  	}
   150  
   151  	// Add a function that is not gofmt'ed. This used to cause a crash.
   152  	// We don't put it in test.go because then we would have to gofmt it.
   153  	// Issue 23927.
   154  	lines = append(lines, []byte("func unFormatted() {"),
   155  		[]byte("\tif true {"),
   156  		[]byte("\t}else{"),
   157  		[]byte("\t}"),
   158  		[]byte("}"))
   159  	lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}"))
   160  
   161  	coverInput := filepath.Join(dir, "test_line.go")
   162  	if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil {
   163  		t.Fatal(err)
   164  	}
   165  
   166  	// testcover -mode=count -var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest -o ./testdata/test_cover.go testdata/test_line.go
   167  	coverOutput := filepath.Join(dir, "test_cover.go")
   168  	cmd := testenv.Command(t, testcover(t), "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput)
   169  	run(cmd, t)
   170  
   171  	cmd = testenv.Command(t, testcover(t), "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput)
   172  	err = cmd.Run()
   173  	if err == nil {
   174  		t.Error("Expected cover to fail with an error")
   175  	}
   176  
   177  	// Copy testmain to tmpdir, so that it is in the same directory
   178  	// as coverOutput.
   179  	testMain := filepath.Join(testdata, "main.go")
   180  	b, err := os.ReadFile(testMain)
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	tmpTestMain := filepath.Join(dir, "main.go")
   185  	if err := os.WriteFile(tmpTestMain, b, 0444); err != nil {
   186  		t.Fatal(err)
   187  	}
   188  
   189  	// go run ./testdata/main.go ./testdata/test.go
   190  	cmd = testenv.Command(t, testenv.GoToolPath(t), "run", tmpTestMain, coverOutput)
   191  	run(cmd, t)
   192  
   193  	file, err = os.ReadFile(coverOutput)
   194  	if err != nil {
   195  		t.Fatal(err)
   196  	}
   197  	// compiler directive must appear right next to function declaration.
   198  	if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
   199  		t.Error("misplaced compiler directive")
   200  	}
   201  	// "go:linkname" compiler directive should be present.
   202  	if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
   203  		t.Error("'go:linkname' compiler directive not found")
   204  	}
   205  
   206  	// Other comments should be preserved too.
   207  	c := ".*// This comment didn't appear in generated go code.*"
   208  	if got, err := regexp.MatchString(c, string(file)); err != nil || !got {
   209  		t.Errorf("non compiler directive comment %q not found", c)
   210  	}
   211  }
   212  
   213  // TestDirectives checks that compiler directives are preserved and positioned
   214  // correctly. Directives that occur before top-level declarations should remain
   215  // above those declarations, even if they are not part of the block of
   216  // documentation comments.
   217  func TestDirectives(t *testing.T) {
   218  	testenv.MustHaveExec(t)
   219  	t.Parallel()
   220  
   221  	// Read the source file and find all the directives. We'll keep
   222  	// track of whether each one has been seen in the output.
   223  	testDirectives := filepath.Join(testdata, "directives.go")
   224  	source, err := os.ReadFile(testDirectives)
   225  	if err != nil {
   226  		t.Fatal(err)
   227  	}
   228  	sourceDirectives := findDirectives(source)
   229  
   230  	// testcover -mode=atomic ./testdata/directives.go
   231  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", testDirectives)
   232  	cmd.Stderr = os.Stderr
   233  	output, err := cmd.Output()
   234  	if err != nil {
   235  		t.Fatal(err)
   236  	}
   237  
   238  	// Check that all directives are present in the output.
   239  	outputDirectives := findDirectives(output)
   240  	foundDirective := make(map[string]bool)
   241  	for _, p := range sourceDirectives {
   242  		foundDirective[p.name] = false
   243  	}
   244  	for _, p := range outputDirectives {
   245  		if found, ok := foundDirective[p.name]; !ok {
   246  			t.Errorf("unexpected directive in output: %s", p.text)
   247  		} else if found {
   248  			t.Errorf("directive found multiple times in output: %s", p.text)
   249  		}
   250  		foundDirective[p.name] = true
   251  	}
   252  	for name, found := range foundDirective {
   253  		if !found {
   254  			t.Errorf("missing directive: %s", name)
   255  		}
   256  	}
   257  
   258  	// Check that directives that start with the name of top-level declarations
   259  	// come before the beginning of the named declaration and after the end
   260  	// of the previous declaration.
   261  	fset := token.NewFileSet()
   262  	astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
   263  	if err != nil {
   264  		t.Fatal(err)
   265  	}
   266  
   267  	prevEnd := 0
   268  	for _, decl := range astFile.Decls {
   269  		var name string
   270  		switch d := decl.(type) {
   271  		case *ast.FuncDecl:
   272  			name = d.Name.Name
   273  		case *ast.GenDecl:
   274  			if len(d.Specs) == 0 {
   275  				// An empty group declaration. We still want to check that
   276  				// directives can be associated with it, so we make up a name
   277  				// to match directives in the test data.
   278  				name = "_empty"
   279  			} else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
   280  				name = spec.Name.Name
   281  			}
   282  		}
   283  		pos := fset.Position(decl.Pos()).Offset
   284  		end := fset.Position(decl.End()).Offset
   285  		if name == "" {
   286  			prevEnd = end
   287  			continue
   288  		}
   289  		for _, p := range outputDirectives {
   290  			if !strings.HasPrefix(p.name, name) {
   291  				continue
   292  			}
   293  			if p.offset < prevEnd || pos < p.offset {
   294  				t.Errorf("directive %s does not appear before definition %s", p.text, name)
   295  			}
   296  		}
   297  		prevEnd = end
   298  	}
   299  }
   300  
   301  type directiveInfo struct {
   302  	text   string // full text of the comment, not including newline
   303  	name   string // text after //go:
   304  	offset int    // byte offset of first slash in comment
   305  }
   306  
   307  func findDirectives(source []byte) []directiveInfo {
   308  	var directives []directiveInfo
   309  	directivePrefix := []byte("\n//go:")
   310  	offset := 0
   311  	for {
   312  		i := bytes.Index(source[offset:], directivePrefix)
   313  		if i < 0 {
   314  			break
   315  		}
   316  		i++ // skip newline
   317  		p := source[offset+i:]
   318  		j := bytes.IndexByte(p, '\n')
   319  		if j < 0 {
   320  			// reached EOF
   321  			j = len(p)
   322  		}
   323  		directive := directiveInfo{
   324  			text:   string(p[:j]),
   325  			name:   string(p[len(directivePrefix)-1 : j]),
   326  			offset: offset + i,
   327  		}
   328  		directives = append(directives, directive)
   329  		offset += i + j
   330  	}
   331  	return directives
   332  }
   333  
   334  // Makes sure that `cover -func=profile.cov` reports accurate coverage.
   335  // Issue #20515.
   336  func TestCoverFunc(t *testing.T) {
   337  	// testcover -func ./testdata/profile.cov
   338  	coverProfile := filepath.Join(testdata, "profile.cov")
   339  	cmd := testenv.Command(t, testcover(t), "-func", coverProfile)
   340  	out, err := cmd.Output()
   341  	if err != nil {
   342  		if ee, ok := err.(*exec.ExitError); ok {
   343  			t.Logf("%s", ee.Stderr)
   344  		}
   345  		t.Fatal(err)
   346  	}
   347  
   348  	if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got {
   349  		t.Logf("%s", out)
   350  		t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err)
   351  	}
   352  }
   353  
   354  // Check that cover produces correct HTML.
   355  // Issue #25767.
   356  func testCoverHTML(t *testing.T, toolexecArg string) {
   357  	testenv.MustHaveGoRun(t)
   358  	dir := tempDir(t)
   359  
   360  	t.Parallel()
   361  
   362  	// go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html
   363  	htmlProfile := filepath.Join(dir, "html.cov")
   364  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html")
   365  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   366  	run(cmd, t)
   367  	// testcover -html testdata/html/html.cov -o testdata/html/html.html
   368  	htmlHTML := filepath.Join(dir, "html.html")
   369  	cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
   370  	run(cmd, t)
   371  
   372  	// Extract the parts of the HTML with comment markers,
   373  	// and compare against a golden file.
   374  	entireHTML, err := os.ReadFile(htmlHTML)
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	var out strings.Builder
   379  	scan := bufio.NewScanner(bytes.NewReader(entireHTML))
   380  	in := false
   381  	for scan.Scan() {
   382  		line := scan.Text()
   383  		if strings.Contains(line, "// START") {
   384  			in = true
   385  		}
   386  		if in {
   387  			fmt.Fprintln(&out, line)
   388  		}
   389  		if strings.Contains(line, "// END") {
   390  			in = false
   391  		}
   392  	}
   393  	if scan.Err() != nil {
   394  		t.Error(scan.Err())
   395  	}
   396  	htmlGolden := filepath.Join(testdata, "html", "html.golden")
   397  	golden, err := os.ReadFile(htmlGolden)
   398  	if err != nil {
   399  		t.Fatalf("reading golden file: %v", err)
   400  	}
   401  	// Ignore white space differences.
   402  	// Break into lines, then compare by breaking into words.
   403  	goldenLines := strings.Split(string(golden), "\n")
   404  	outLines := strings.Split(out.String(), "\n")
   405  	// Compare at the line level, stopping at first different line so
   406  	// we don't generate tons of output if there's an inserted or deleted line.
   407  	for i, goldenLine := range goldenLines {
   408  		if i >= len(outLines) {
   409  			t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine)
   410  		}
   411  		// Convert all white space to simple spaces, for easy comparison.
   412  		goldenLine = strings.Join(strings.Fields(goldenLine), " ")
   413  		outLine := strings.Join(strings.Fields(outLines[i]), " ")
   414  		if outLine != goldenLine {
   415  			t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine)
   416  		}
   417  	}
   418  	if len(goldenLines) != len(outLines) {
   419  		t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)])
   420  	}
   421  }
   422  
   423  // Test HTML processing with a source file not run through gofmt.
   424  // Issue #27350.
   425  func testHtmlUnformatted(t *testing.T, toolexecArg string) {
   426  	testenv.MustHaveGoRun(t)
   427  	dir := tempDir(t)
   428  
   429  	t.Parallel()
   430  
   431  	htmlUDir := filepath.Join(dir, "htmlunformatted")
   432  	htmlU := filepath.Join(htmlUDir, "htmlunformatted.go")
   433  	htmlUTest := filepath.Join(htmlUDir, "htmlunformatted_test.go")
   434  	htmlUProfile := filepath.Join(htmlUDir, "htmlunformatted.cov")
   435  	htmlUHTML := filepath.Join(htmlUDir, "htmlunformatted.html")
   436  
   437  	if err := os.Mkdir(htmlUDir, 0777); err != nil {
   438  		t.Fatal(err)
   439  	}
   440  
   441  	if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil {
   442  		t.Fatal(err)
   443  	}
   444  
   445  	const htmlUContents = `
   446  package htmlunformatted
   447  
   448  var g int
   449  
   450  func F() {
   451  //line x.go:1
   452  	{ { F(); goto lab } }
   453  lab:
   454  }`
   455  
   456  	const htmlUTestContents = `package htmlunformatted`
   457  
   458  	if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil {
   459  		t.Fatal(err)
   460  	}
   461  	if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil {
   462  		t.Fatal(err)
   463  	}
   464  
   465  	// go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov
   466  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-test.v", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile)
   467  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   468  	cmd.Dir = htmlUDir
   469  	run(cmd, t)
   470  
   471  	// testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html
   472  	cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
   473  	cmd.Dir = htmlUDir
   474  	run(cmd, t)
   475  }
   476  
   477  // lineDupContents becomes linedup.go in testFuncWithDuplicateLines.
   478  const lineDupContents = `
   479  package linedup
   480  
   481  var G int
   482  
   483  func LineDup(c int) {
   484  	for i := 0; i < c; i++ {
   485  //line ld.go:100
   486  		if i % 2 == 0 {
   487  			G++
   488  		}
   489  		if i % 3 == 0 {
   490  			G++; G++
   491  		}
   492  //line ld.go:100
   493  		if i % 4 == 0 {
   494  			G++; G++; G++
   495  		}
   496  		if i % 5 == 0 {
   497  			G++; G++; G++; G++
   498  		}
   499  	}
   500  }
   501  `
   502  
   503  // lineDupTestContents becomes linedup_test.go in testFuncWithDuplicateLines.
   504  const lineDupTestContents = `
   505  package linedup
   506  
   507  import "testing"
   508  
   509  func TestLineDup(t *testing.T) {
   510  	LineDup(100)
   511  }
   512  `
   513  
   514  // Test -func with duplicate //line directives with different numbers
   515  // of statements.
   516  func testFuncWithDuplicateLines(t *testing.T, toolexecArg string) {
   517  	testenv.MustHaveGoRun(t)
   518  	dir := tempDir(t)
   519  
   520  	t.Parallel()
   521  
   522  	lineDupDir := filepath.Join(dir, "linedup")
   523  	lineDupGo := filepath.Join(lineDupDir, "linedup.go")
   524  	lineDupTestGo := filepath.Join(lineDupDir, "linedup_test.go")
   525  	lineDupProfile := filepath.Join(lineDupDir, "linedup.out")
   526  
   527  	if err := os.Mkdir(lineDupDir, 0777); err != nil {
   528  		t.Fatal(err)
   529  	}
   530  
   531  	if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil {
   532  		t.Fatal(err)
   533  	}
   534  	if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil {
   535  		t.Fatal(err)
   536  	}
   537  	if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil {
   538  		t.Fatal(err)
   539  	}
   540  
   541  	// go test -cover -covermode count -coverprofile TMPDIR/linedup.out
   542  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile)
   543  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   544  	cmd.Dir = lineDupDir
   545  	run(cmd, t)
   546  
   547  	// testcover -func=TMPDIR/linedup.out
   548  	cmd = testenv.Command(t, testcover(t), "-func", lineDupProfile)
   549  	cmd.Dir = lineDupDir
   550  	run(cmd, t)
   551  }
   552  
   553  func run(c *exec.Cmd, t *testing.T) {
   554  	t.Helper()
   555  	t.Log("running", c.Args)
   556  	out, err := c.CombinedOutput()
   557  	if len(out) > 0 {
   558  		t.Logf("%s", out)
   559  	}
   560  	if err != nil {
   561  		t.Fatal(err)
   562  	}
   563  }
   564  
   565  func runExpectingError(c *exec.Cmd, t *testing.T) string {
   566  	t.Helper()
   567  	t.Log("running", c.Args)
   568  	out, err := c.CombinedOutput()
   569  	if err == nil {
   570  		return fmt.Sprintf("unexpected pass for %+v", c.Args)
   571  	}
   572  	return string(out)
   573  }
   574  
   575  // Test instrumentation of package that ends before an expected
   576  // trailing newline following package clause. Issue #58370.
   577  func testMissingTrailingNewlineIssue58370(t *testing.T, toolexecArg string) {
   578  	testenv.MustHaveGoBuild(t)
   579  	dir := tempDir(t)
   580  
   581  	t.Parallel()
   582  
   583  	noeolDir := filepath.Join(dir, "issue58370")
   584  	noeolGo := filepath.Join(noeolDir, "noeol.go")
   585  	noeolTestGo := filepath.Join(noeolDir, "noeol_test.go")
   586  
   587  	if err := os.Mkdir(noeolDir, 0777); err != nil {
   588  		t.Fatal(err)
   589  	}
   590  
   591  	if err := os.WriteFile(filepath.Join(noeolDir, "go.mod"), []byte("module noeol\n"), 0666); err != nil {
   592  		t.Fatal(err)
   593  	}
   594  	const noeolContents = `package noeol`
   595  	if err := os.WriteFile(noeolGo, []byte(noeolContents), 0444); err != nil {
   596  		t.Fatal(err)
   597  	}
   598  	const noeolTestContents = `
   599  package noeol
   600  import "testing"
   601  func TestCoverage(t *testing.T) { }
   602  `
   603  	if err := os.WriteFile(noeolTestGo, []byte(noeolTestContents), 0444); err != nil {
   604  		t.Fatal(err)
   605  	}
   606  
   607  	// go test -covermode atomic
   608  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-covermode", "atomic")
   609  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   610  	cmd.Dir = noeolDir
   611  	run(cmd, t)
   612  }
   613  
   614  func TestSrcPathWithNewline(t *testing.T) {
   615  	testenv.MustHaveExec(t)
   616  	t.Parallel()
   617  
   618  	// srcPath is intentionally not clean so that the path passed to testcover
   619  	// will not normalize the trailing / to a \ on Windows.
   620  	srcPath := t.TempDir() + string(filepath.Separator) + "\npackage main\nfunc main() { panic(string([]rune{'u', 'h', '-', 'o', 'h'}))\n/*/main.go"
   621  	mainSrc := ` package main
   622  
   623  func main() {
   624  	/* nothing here */
   625  	println("ok")
   626  }
   627  `
   628  	if err := os.MkdirAll(filepath.Dir(srcPath), 0777); err != nil {
   629  		t.Skipf("creating directory with bogus path: %v", err)
   630  	}
   631  	if err := os.WriteFile(srcPath, []byte(mainSrc), 0666); err != nil {
   632  		t.Skipf("writing file with bogus directory: %v", err)
   633  	}
   634  
   635  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", srcPath)
   636  	cmd.Stderr = new(bytes.Buffer)
   637  	out, err := cmd.Output()
   638  	t.Logf("%v:\n%s", cmd, out)
   639  	t.Logf("stderr:\n%s", cmd.Stderr)
   640  	if err == nil {
   641  		t.Errorf("unexpected success; want failure due to newline in file path")
   642  	}
   643  }
   644  
   645  func TestAlignment(t *testing.T) {
   646  	// Test that cover data structures are aligned appropriately. See issue 58936.
   647  	testenv.MustHaveGoRun(t)
   648  	t.Parallel()
   649  
   650  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-cover", filepath.Join(testdata, "align.go"), filepath.Join(testdata, "align_test.go"))
   651  	run(cmd, t)
   652  }
   653  
   654  // lineRange represents a coverage range as a pair of line numbers (1-indexed, inclusive).
   655  type lineRange struct {
   656  	start, end int
   657  }
   658  
   659  // parseBrackets strips bracket markers (U+00AB «, U+00BB ») from src,
   660  // returning the cleaned Go source and a list of expected coverage ranges
   661  // as line number pairs (1-indexed, inclusive).
   662  func parseBrackets(src []byte) ([]byte, []lineRange) {
   663  	const (
   664  		open  = "\u00ab" // «
   665  		close = "\u00bb" // »
   666  	)
   667  	var (
   668  		cleaned []byte
   669  		ranges  []lineRange
   670  		stack   []int // stack of open marker byte positions in cleaned
   671  	)
   672  	i := 0
   673  	for i < len(src) {
   674  		if bytes.HasPrefix(src[i:], []byte(open)) {
   675  			stack = append(stack, len(cleaned))
   676  			i += len(open)
   677  		} else if bytes.HasPrefix(src[i:], []byte(close)) {
   678  			if len(stack) == 0 {
   679  				panic("unmatched close bracket at offset " + strconv.Itoa(i))
   680  			}
   681  			startPos := stack[len(stack)-1]
   682  			stack = stack[:len(stack)-1]
   683  			endPos := len(cleaned)
   684  			// Convert byte positions to line numbers.
   685  			startLine := 1 + bytes.Count(cleaned[:startPos], []byte{'\n'})
   686  			endLine := 1 + bytes.Count(cleaned[:endPos], []byte{'\n'})
   687  			// If endPos is at the start of a line (after \n), the range
   688  			// actually ended on the previous line.
   689  			if endPos > 0 && cleaned[endPos-1] == '\n' {
   690  				endLine--
   691  			}
   692  			if endLine < startLine {
   693  				endLine = startLine
   694  			}
   695  			ranges = append(ranges, lineRange{startLine, endLine})
   696  			i += len(close)
   697  		} else {
   698  			cleaned = append(cleaned, src[i])
   699  			i++
   700  		}
   701  	}
   702  	if len(stack) != 0 {
   703  		panic("unmatched open bracket(s)")
   704  	}
   705  	return cleaned, ranges
   706  }
   707  
   708  // coverRanges runs the cover tool on src and returns the coverage ranges
   709  // as line number pairs (1-indexed, inclusive).
   710  func coverRanges(t *testing.T, src []byte) []lineRange {
   711  	t.Helper()
   712  	tmpdir := t.TempDir()
   713  	srcPath := filepath.Join(tmpdir, "test.go")
   714  	if err := os.WriteFile(srcPath, src, 0666); err != nil {
   715  		t.Fatalf("writing test file: %v", err)
   716  	}
   717  
   718  	cmd := testenv.Command(t, testcover(t), "-mode=set", srcPath)
   719  	out, err := cmd.Output()
   720  	if err != nil {
   721  		t.Fatalf("cover failed: %v\nOutput: %s", err, out)
   722  	}
   723  
   724  	outStr := string(out)
   725  	// Skip the //line directive that cover adds at the top.
   726  	if _, after, ok := strings.Cut(outStr, "\n"); ok {
   727  		outStr = after
   728  	}
   729  
   730  	// Parse the coverage struct's Pos array to extract ranges.
   731  	// Format: startLine, endLine, (endCol<<16)|startCol, // [index]
   732  	re := regexp.MustCompile(`(\d+), (\d+), (0x[0-9a-f]+), // \[\d+\]`)
   733  	matches := re.FindAllStringSubmatch(outStr, -1)
   734  
   735  	var result []lineRange
   736  	for _, m := range matches {
   737  		startLine, _ := strconv.Atoi(m[1])
   738  		endLine, _ := strconv.Atoi(m[2])
   739  		var packed int
   740  		fmt.Sscanf(m[3], "0x%x", &packed)
   741  		endCol := (packed >> 16) & 0xFFFF
   742  		startCol := packed & 0xFFFF
   743  		// Skip zero-width ranges (comment-only or empty blocks).
   744  		if startLine == endLine && startCol == endCol {
   745  			continue
   746  		}
   747  		// The range [start, end) is half-open. When endCol==1, the
   748  		// range reaches the beginning of endLine but includes no
   749  		// content on it, so the last covered line is endLine-1.
   750  		if endCol == 1 && endLine > startLine {
   751  			endLine--
   752  		}
   753  		result = append(result, lineRange{startLine, endLine})
   754  	}
   755  	return result
   756  }
   757  
   758  // compareRanges is a test helper that compares got and want line ranges.
   759  // Both slices are sorted before comparison since the cover tool's output
   760  // order (AST walk order) may differ from source order (marker order).
   761  func compareRanges(t *testing.T, src []byte, got, want []lineRange) {
   762  	t.Helper()
   763  	lines := strings.Split(string(src), "\n")
   764  	snippet := func(r lineRange) string {
   765  		start, end := r.start-1, r.end
   766  		if start < 0 {
   767  			start = 0
   768  		}
   769  		if end > len(lines) {
   770  			end = len(lines)
   771  		}
   772  		s := strings.Join(lines[start:end], "\n")
   773  		if len(s) > 120 {
   774  			s = s[:117] + "..."
   775  		}
   776  		return s
   777  	}
   778  
   779  	sortRanges := func(rs []lineRange) []lineRange {
   780  		s := append([]lineRange(nil), rs...)
   781  		slices.SortFunc(s, func(a, b lineRange) int {
   782  			if c := cmp.Compare(a.start, b.start); c != 0 {
   783  				return c
   784  			}
   785  			return cmp.Compare(a.end, b.end)
   786  		})
   787  		return s
   788  	}
   789  
   790  	gotSorted := sortRanges(got)
   791  	wantSorted := sortRanges(want)
   792  
   793  	if len(gotSorted) != len(wantSorted) {
   794  		t.Errorf("got %d ranges, want %d", len(gotSorted), len(wantSorted))
   795  		for i, r := range gotSorted {
   796  			t.Logf("  got[%d]: lines %d-%d %q", i, r.start, r.end, snippet(r))
   797  		}
   798  		for i, r := range wantSorted {
   799  			t.Logf("  want[%d]: lines %d-%d %q", i, r.start, r.end, snippet(r))
   800  		}
   801  		return
   802  	}
   803  	for i := range wantSorted {
   804  		if gotSorted[i] != wantSorted[i] {
   805  			t.Errorf("range %d: got lines %d-%d %q, want lines %d-%d %q",
   806  				i, gotSorted[i].start, gotSorted[i].end, snippet(gotSorted[i]),
   807  				wantSorted[i].start, wantSorted[i].end, snippet(wantSorted[i]))
   808  		}
   809  	}
   810  }
   811  
   812  // TestCommentedOutCodeExclusion tests that comment-only and blank lines
   813  // are excluded from coverage ranges using a bracket-marker testdata file.
   814  func TestCommentedOutCodeExclusion(t *testing.T) {
   815  	testenv.MustHaveGoBuild(t)
   816  
   817  	markedSrc, err := os.ReadFile(filepath.Join(testdata, "ranges", "ranges.go"))
   818  	if err != nil {
   819  		t.Fatal(err)
   820  	}
   821  	src, want := parseBrackets(markedSrc)
   822  	got := coverRanges(t, src)
   823  	compareRanges(t, src, got, want)
   824  }
   825  
   826  // TestLineDirective verifies that //line directives don't affect coverage
   827  // line number calculations (we use physical lines, not remapped ones).
   828  func TestLineDirective(t *testing.T) {
   829  	testenv.MustHaveGoBuild(t)
   830  
   831  	// Source with //line directive that would remap lines if not handled correctly.
   832  	testSrc := `package main
   833  
   834  import "fmt"
   835  
   836  func main() {
   837  	//line other.go:100
   838  «	x := 1
   839  »	// comment that should be excluded
   840  «	y := 2
   841  »	//line other.go:200
   842  «	fmt.Println(x, y)
   843  »}`
   844  
   845  	src, want := parseBrackets([]byte(testSrc))
   846  	got := coverRanges(t, src)
   847  	compareRanges(t, src, got, want)
   848  }
   849  
   850  // TestCommentExclusionBasic is a simple test verifying that a comment splits
   851  // a single block into two separate coverage ranges.
   852  func TestCommentExclusionBasic(t *testing.T) {
   853  	testenv.MustHaveGoBuild(t)
   854  
   855  	testSrc := `package main
   856  
   857  func main() {
   858  «	x := 1
   859  »	// this comment should split the block
   860  «	y := 2
   861  »}`
   862  
   863  	src, want := parseBrackets([]byte(testSrc))
   864  	got := coverRanges(t, src)
   865  	compareRanges(t, src, got, want)
   866  }
   867  

View as plain text