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  	"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  	// Data directory, also the package directory for the test.
    29  	testdata = "testdata"
    30  )
    31  
    32  // testcover returns the path to the cmd/cover binary that we are going to
    33  // test. At one point this was created via "go build"; we now reuse the unit
    34  // test executable itself.
    35  func testcover(t testing.TB) string {
    36  	return testenv.Executable(t)
    37  }
    38  
    39  // testTempDir is a temporary directory created in TestMain.
    40  var testTempDir string
    41  
    42  // If set, this will preserve all the tmpdir files from the test run.
    43  var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
    44  
    45  // TestMain used here so that we can leverage the test executable
    46  // itself as a cmd/cover executable; compare to similar usage in
    47  // the cmd/go tests.
    48  func TestMain(m *testing.M) {
    49  	if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
    50  		// When CMDCOVER_TOOLEXEC is set, the test binary is also
    51  		// running as a -toolexec wrapper.
    52  		tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
    53  		if tool == "cover" {
    54  			// Inject this test binary as cmd/cover in place of the
    55  			// installed tool, so that the go command's invocations of
    56  			// cover produce coverage for the configuration in which
    57  			// the test was built.
    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  		// When CMDCOVER_TEST_RUN_MAIN is set, we're reusing the test
    72  		// binary as cmd/cover. In this case we run the main func exported
    73  		// via export_test.go, and exit; CMDCOVER_TEST_RUN_MAIN is set below
    74  		// for actual test invocations.
    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  // TestCoverWithToolExec runs a set of subtests that all make use of a
   108  // "-toolexec" wrapper program to invoke the cover test executable
   109  // itself via "go test -cover".
   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  // Execute this command sequence:
   128  //
   129  //	replace the word LINE with the line number < testdata/test.go > testdata/test_line.go
   130  //	testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go
   131  //	go run ./testdata/main.go ./testdata/test.go
   132  func TestCover(t *testing.T) {
   133  	testenv.MustHaveGoRun(t)
   134  	t.Parallel()
   135  	dir := tempDir(t)
   136  
   137  	// Read in the test file (testTest) and write it, with LINEs specified, to coverInput.
   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  	// Add a function that is not gofmt'ed. This used to cause a crash.
   149  	// We don't put it in test.go because then we would have to gofmt it.
   150  	// Issue 23927.
   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  	// testcover -mode=count -var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest -o ./testdata/test_cover.go testdata/test_line.go
   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  	// Copy testmain to tmpdir, so that it is in the same directory
   175  	// as coverOutput.
   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  	// go run ./testdata/main.go ./testdata/test.go
   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  	// compiler directive must appear right next to function declaration.
   195  	if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
   196  		t.Error("misplaced compiler directive")
   197  	}
   198  	// "go:linkname" compiler directive should be present.
   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  	// Other comments should be preserved too.
   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  // TestDirectives checks that compiler directives are preserved and positioned
   211  // correctly. Directives that occur before top-level declarations should remain
   212  // above those declarations, even if they are not part of the block of
   213  // documentation comments.
   214  func TestDirectives(t *testing.T) {
   215  	testenv.MustHaveExec(t)
   216  	t.Parallel()
   217  
   218  	// Read the source file and find all the directives. We'll keep
   219  	// track of whether each one has been seen in the output.
   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  	// testcover -mode=atomic ./testdata/directives.go
   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  	// Check that all directives are present in the output.
   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  	// Check that directives that start with the name of top-level declarations
   256  	// come before the beginning of the named declaration and after the end
   257  	// of the previous declaration.
   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  				// An empty group declaration. We still want to check that
   273  				// directives can be associated with it, so we make up a name
   274  				// to match directives in the test data.
   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 // full text of the comment, not including newline
   300  	name   string // text after //go:
   301  	offset int    // byte offset of first slash in comment
   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++ // skip newline
   314  		p := source[offset+i:]
   315  		j := bytes.IndexByte(p, '\n')
   316  		if j < 0 {
   317  			// reached EOF
   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  // Makes sure that `cover -func=profile.cov` reports accurate coverage.
   332  // Issue #20515.
   333  func TestCoverFunc(t *testing.T) {
   334  	// testcover -func ./testdata/profile.cov
   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  // Check that cover produces correct HTML.
   352  // Issue #25767.
   353  func testCoverHTML(t *testing.T, toolexecArg string) {
   354  	testenv.MustHaveGoRun(t)
   355  	dir := tempDir(t)
   356  
   357  	t.Parallel()
   358  
   359  	// go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html
   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  	// testcover -html testdata/html/html.cov -o testdata/html/html.html
   365  	htmlHTML := filepath.Join(dir, "html.html")
   366  	cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
   367  	run(cmd, t)
   368  
   369  	// Extract the parts of the HTML with comment markers,
   370  	// and compare against a golden file.
   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  	// Ignore white space differences.
   399  	// Break into lines, then compare by breaking into words.
   400  	goldenLines := strings.Split(string(golden), "\n")
   401  	outLines := strings.Split(out.String(), "\n")
   402  	// Compare at the line level, stopping at first different line so
   403  	// we don't generate tons of output if there's an inserted or deleted line.
   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  		// Convert all white space to simple spaces, for easy comparison.
   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  // Test HTML processing with a source file not run through gofmt.
   421  // Issue #27350.
   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  	// go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov
   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  	// testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html
   469  	cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
   470  	cmd.Dir = htmlUDir
   471  	run(cmd, t)
   472  }
   473  
   474  // lineDupContents becomes linedup.go in testFuncWithDuplicateLines.
   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  // lineDupTestContents becomes linedup_test.go in testFuncWithDuplicateLines.
   501  const lineDupTestContents = `
   502  package linedup
   503  
   504  import "testing"
   505  
   506  func TestLineDup(t *testing.T) {
   507  	LineDup(100)
   508  }
   509  `
   510  
   511  // Test -func with duplicate //line directives with different numbers
   512  // of statements.
   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  	// go test -cover -covermode count -coverprofile TMPDIR/linedup.out
   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  	// testcover -func=TMPDIR/linedup.out
   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  // Test instrumentation of package that ends before an expected
   573  // trailing newline following package clause. Issue #58370.
   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  	// go test -covermode atomic
   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  	// srcPath is intentionally not clean so that the path passed to testcover
   616  	// will not normalize the trailing / to a \ on Windows.
   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