...

Source file src/cmd/cover/cover_test.go

Documentation: cmd/cover

     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  	exe, err := os.Executable()
    37  	if err != nil {
    38  		t.Helper()
    39  		t.Fatal(err)
    40  	}
    41  	return exe
    42  }
    43  
    44  // testTempDir is a temporary directory created in TestMain.
    45  var testTempDir string
    46  
    47  // If set, this will preserve all the tmpdir files from the test run.
    48  var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
    49  
    50  // TestMain used here so that we can leverage the test executable
    51  // itself as a cmd/cover executable; compare to similar usage in
    52  // the cmd/go tests.
    53  func TestMain(m *testing.M) {
    54  	if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
    55  		// When CMDCOVER_TOOLEXEC is set, the test binary is also
    56  		// running as a -toolexec wrapper.
    57  		tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
    58  		if tool == "cover" {
    59  			// Inject this test binary as cmd/cover in place of the
    60  			// installed tool, so that the go command's invocations of
    61  			// cover produce coverage for the configuration in which
    62  			// the test was built.
    63  			os.Args = os.Args[1:]
    64  			cmdcover.Main()
    65  		} else {
    66  			cmd := exec.Command(os.Args[1], os.Args[2:]...)
    67  			cmd.Stdout = os.Stdout
    68  			cmd.Stderr = os.Stderr
    69  			if err := cmd.Run(); err != nil {
    70  				os.Exit(1)
    71  			}
    72  		}
    73  		os.Exit(0)
    74  	}
    75  	if os.Getenv("CMDCOVER_TEST_RUN_MAIN") != "" {
    76  		// When CMDCOVER_TEST_RUN_MAIN is set, we're reusing the test
    77  		// binary as cmd/cover. In this case we run the main func exported
    78  		// via export_test.go, and exit; CMDCOVER_TEST_RUN_MAIN is set below
    79  		// for actual test invocations.
    80  		cmdcover.Main()
    81  		os.Exit(0)
    82  	}
    83  	flag.Parse()
    84  	topTmpdir, err := os.MkdirTemp("", "cmd-cover-test-")
    85  	if err != nil {
    86  		log.Fatal(err)
    87  	}
    88  	testTempDir = topTmpdir
    89  	if !*debug {
    90  		defer os.RemoveAll(topTmpdir)
    91  	} else {
    92  		fmt.Fprintf(os.Stderr, "debug: preserving tmpdir %s\n", topTmpdir)
    93  	}
    94  	os.Setenv("CMDCOVER_TEST_RUN_MAIN", "normal")
    95  	os.Exit(m.Run())
    96  }
    97  
    98  var tdmu sync.Mutex
    99  var tdcount int
   100  
   101  func tempDir(t *testing.T) string {
   102  	tdmu.Lock()
   103  	dir := filepath.Join(testTempDir, fmt.Sprintf("%03d", tdcount))
   104  	tdcount++
   105  	if err := os.Mkdir(dir, 0777); err != nil {
   106  		t.Fatal(err)
   107  	}
   108  	defer tdmu.Unlock()
   109  	return dir
   110  }
   111  
   112  // TestCoverWithToolExec runs a set of subtests that all make use of a
   113  // "-toolexec" wrapper program to invoke the cover test executable
   114  // itself via "go test -cover".
   115  func TestCoverWithToolExec(t *testing.T) {
   116  	testenv.MustHaveExec(t)
   117  
   118  	toolexecArg := "-toolexec=" + testcover(t)
   119  
   120  	t.Run("CoverHTML", func(t *testing.T) {
   121  		testCoverHTML(t, toolexecArg)
   122  	})
   123  	t.Run("HtmlUnformatted", func(t *testing.T) {
   124  		testHtmlUnformatted(t, toolexecArg)
   125  	})
   126  	t.Run("FuncWithDuplicateLines", func(t *testing.T) {
   127  		testFuncWithDuplicateLines(t, toolexecArg)
   128  	})
   129  	t.Run("MissingTrailingNewlineIssue58370", func(t *testing.T) {
   130  		testMissingTrailingNewlineIssue58370(t, toolexecArg)
   131  	})
   132  }
   133  
   134  // Execute this command sequence:
   135  //
   136  //	replace the word LINE with the line number < testdata/test.go > testdata/test_line.go
   137  //	testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go
   138  //	go run ./testdata/main.go ./testdata/test.go
   139  func TestCover(t *testing.T) {
   140  	testenv.MustHaveGoRun(t)
   141  	t.Parallel()
   142  	dir := tempDir(t)
   143  
   144  	// Read in the test file (testTest) and write it, with LINEs specified, to coverInput.
   145  	testTest := filepath.Join(testdata, "test.go")
   146  	file, err := os.ReadFile(testTest)
   147  	if err != nil {
   148  		t.Fatal(err)
   149  	}
   150  	lines := bytes.Split(file, []byte("\n"))
   151  	for i, line := range lines {
   152  		lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1)))
   153  	}
   154  
   155  	// Add a function that is not gofmt'ed. This used to cause a crash.
   156  	// We don't put it in test.go because then we would have to gofmt it.
   157  	// Issue 23927.
   158  	lines = append(lines, []byte("func unFormatted() {"),
   159  		[]byte("\tif true {"),
   160  		[]byte("\t}else{"),
   161  		[]byte("\t}"),
   162  		[]byte("}"))
   163  	lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}"))
   164  
   165  	coverInput := filepath.Join(dir, "test_line.go")
   166  	if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil {
   167  		t.Fatal(err)
   168  	}
   169  
   170  	// testcover -mode=count -var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest -o ./testdata/test_cover.go testdata/test_line.go
   171  	coverOutput := filepath.Join(dir, "test_cover.go")
   172  	cmd := testenv.Command(t, testcover(t), "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput)
   173  	run(cmd, t)
   174  
   175  	cmd = testenv.Command(t, testcover(t), "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput)
   176  	err = cmd.Run()
   177  	if err == nil {
   178  		t.Error("Expected cover to fail with an error")
   179  	}
   180  
   181  	// Copy testmain to tmpdir, so that it is in the same directory
   182  	// as coverOutput.
   183  	testMain := filepath.Join(testdata, "main.go")
   184  	b, err := os.ReadFile(testMain)
   185  	if err != nil {
   186  		t.Fatal(err)
   187  	}
   188  	tmpTestMain := filepath.Join(dir, "main.go")
   189  	if err := os.WriteFile(tmpTestMain, b, 0444); err != nil {
   190  		t.Fatal(err)
   191  	}
   192  
   193  	// go run ./testdata/main.go ./testdata/test.go
   194  	cmd = testenv.Command(t, testenv.GoToolPath(t), "run", tmpTestMain, coverOutput)
   195  	run(cmd, t)
   196  
   197  	file, err = os.ReadFile(coverOutput)
   198  	if err != nil {
   199  		t.Fatal(err)
   200  	}
   201  	// compiler directive must appear right next to function declaration.
   202  	if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
   203  		t.Error("misplaced compiler directive")
   204  	}
   205  	// "go:linkname" compiler directive should be present.
   206  	if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
   207  		t.Error("'go:linkname' compiler directive not found")
   208  	}
   209  
   210  	// Other comments should be preserved too.
   211  	c := ".*// This comment didn't appear in generated go code.*"
   212  	if got, err := regexp.MatchString(c, string(file)); err != nil || !got {
   213  		t.Errorf("non compiler directive comment %q not found", c)
   214  	}
   215  }
   216  
   217  // TestDirectives checks that compiler directives are preserved and positioned
   218  // correctly. Directives that occur before top-level declarations should remain
   219  // above those declarations, even if they are not part of the block of
   220  // documentation comments.
   221  func TestDirectives(t *testing.T) {
   222  	testenv.MustHaveExec(t)
   223  	t.Parallel()
   224  
   225  	// Read the source file and find all the directives. We'll keep
   226  	// track of whether each one has been seen in the output.
   227  	testDirectives := filepath.Join(testdata, "directives.go")
   228  	source, err := os.ReadFile(testDirectives)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	sourceDirectives := findDirectives(source)
   233  
   234  	// testcover -mode=atomic ./testdata/directives.go
   235  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", testDirectives)
   236  	cmd.Stderr = os.Stderr
   237  	output, err := cmd.Output()
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	// Check that all directives are present in the output.
   243  	outputDirectives := findDirectives(output)
   244  	foundDirective := make(map[string]bool)
   245  	for _, p := range sourceDirectives {
   246  		foundDirective[p.name] = false
   247  	}
   248  	for _, p := range outputDirectives {
   249  		if found, ok := foundDirective[p.name]; !ok {
   250  			t.Errorf("unexpected directive in output: %s", p.text)
   251  		} else if found {
   252  			t.Errorf("directive found multiple times in output: %s", p.text)
   253  		}
   254  		foundDirective[p.name] = true
   255  	}
   256  	for name, found := range foundDirective {
   257  		if !found {
   258  			t.Errorf("missing directive: %s", name)
   259  		}
   260  	}
   261  
   262  	// Check that directives that start with the name of top-level declarations
   263  	// come before the beginning of the named declaration and after the end
   264  	// of the previous declaration.
   265  	fset := token.NewFileSet()
   266  	astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
   267  	if err != nil {
   268  		t.Fatal(err)
   269  	}
   270  
   271  	prevEnd := 0
   272  	for _, decl := range astFile.Decls {
   273  		var name string
   274  		switch d := decl.(type) {
   275  		case *ast.FuncDecl:
   276  			name = d.Name.Name
   277  		case *ast.GenDecl:
   278  			if len(d.Specs) == 0 {
   279  				// An empty group declaration. We still want to check that
   280  				// directives can be associated with it, so we make up a name
   281  				// to match directives in the test data.
   282  				name = "_empty"
   283  			} else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
   284  				name = spec.Name.Name
   285  			}
   286  		}
   287  		pos := fset.Position(decl.Pos()).Offset
   288  		end := fset.Position(decl.End()).Offset
   289  		if name == "" {
   290  			prevEnd = end
   291  			continue
   292  		}
   293  		for _, p := range outputDirectives {
   294  			if !strings.HasPrefix(p.name, name) {
   295  				continue
   296  			}
   297  			if p.offset < prevEnd || pos < p.offset {
   298  				t.Errorf("directive %s does not appear before definition %s", p.text, name)
   299  			}
   300  		}
   301  		prevEnd = end
   302  	}
   303  }
   304  
   305  type directiveInfo struct {
   306  	text   string // full text of the comment, not including newline
   307  	name   string // text after //go:
   308  	offset int    // byte offset of first slash in comment
   309  }
   310  
   311  func findDirectives(source []byte) []directiveInfo {
   312  	var directives []directiveInfo
   313  	directivePrefix := []byte("\n//go:")
   314  	offset := 0
   315  	for {
   316  		i := bytes.Index(source[offset:], directivePrefix)
   317  		if i < 0 {
   318  			break
   319  		}
   320  		i++ // skip newline
   321  		p := source[offset+i:]
   322  		j := bytes.IndexByte(p, '\n')
   323  		if j < 0 {
   324  			// reached EOF
   325  			j = len(p)
   326  		}
   327  		directive := directiveInfo{
   328  			text:   string(p[:j]),
   329  			name:   string(p[len(directivePrefix)-1 : j]),
   330  			offset: offset + i,
   331  		}
   332  		directives = append(directives, directive)
   333  		offset += i + j
   334  	}
   335  	return directives
   336  }
   337  
   338  // Makes sure that `cover -func=profile.cov` reports accurate coverage.
   339  // Issue #20515.
   340  func TestCoverFunc(t *testing.T) {
   341  	testenv.MustHaveExec(t)
   342  
   343  	// testcover -func ./testdata/profile.cov
   344  	coverProfile := filepath.Join(testdata, "profile.cov")
   345  	cmd := testenv.Command(t, testcover(t), "-func", coverProfile)
   346  	out, err := cmd.Output()
   347  	if err != nil {
   348  		if ee, ok := err.(*exec.ExitError); ok {
   349  			t.Logf("%s", ee.Stderr)
   350  		}
   351  		t.Fatal(err)
   352  	}
   353  
   354  	if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got {
   355  		t.Logf("%s", out)
   356  		t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err)
   357  	}
   358  }
   359  
   360  // Check that cover produces correct HTML.
   361  // Issue #25767.
   362  func testCoverHTML(t *testing.T, toolexecArg string) {
   363  	testenv.MustHaveGoRun(t)
   364  	dir := tempDir(t)
   365  
   366  	t.Parallel()
   367  
   368  	// go test -coverprofile testdata/html/html.cov cmd/cover/testdata/html
   369  	htmlProfile := filepath.Join(dir, "html.cov")
   370  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html")
   371  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   372  	run(cmd, t)
   373  	// testcover -html testdata/html/html.cov -o testdata/html/html.html
   374  	htmlHTML := filepath.Join(dir, "html.html")
   375  	cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
   376  	run(cmd, t)
   377  
   378  	// Extract the parts of the HTML with comment markers,
   379  	// and compare against a golden file.
   380  	entireHTML, err := os.ReadFile(htmlHTML)
   381  	if err != nil {
   382  		t.Fatal(err)
   383  	}
   384  	var out strings.Builder
   385  	scan := bufio.NewScanner(bytes.NewReader(entireHTML))
   386  	in := false
   387  	for scan.Scan() {
   388  		line := scan.Text()
   389  		if strings.Contains(line, "// START") {
   390  			in = true
   391  		}
   392  		if in {
   393  			fmt.Fprintln(&out, line)
   394  		}
   395  		if strings.Contains(line, "// END") {
   396  			in = false
   397  		}
   398  	}
   399  	if scan.Err() != nil {
   400  		t.Error(scan.Err())
   401  	}
   402  	htmlGolden := filepath.Join(testdata, "html", "html.golden")
   403  	golden, err := os.ReadFile(htmlGolden)
   404  	if err != nil {
   405  		t.Fatalf("reading golden file: %v", err)
   406  	}
   407  	// Ignore white space differences.
   408  	// Break into lines, then compare by breaking into words.
   409  	goldenLines := strings.Split(string(golden), "\n")
   410  	outLines := strings.Split(out.String(), "\n")
   411  	// Compare at the line level, stopping at first different line so
   412  	// we don't generate tons of output if there's an inserted or deleted line.
   413  	for i, goldenLine := range goldenLines {
   414  		if i >= len(outLines) {
   415  			t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine)
   416  		}
   417  		// Convert all white space to simple spaces, for easy comparison.
   418  		goldenLine = strings.Join(strings.Fields(goldenLine), " ")
   419  		outLine := strings.Join(strings.Fields(outLines[i]), " ")
   420  		if outLine != goldenLine {
   421  			t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine)
   422  		}
   423  	}
   424  	if len(goldenLines) != len(outLines) {
   425  		t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)])
   426  	}
   427  }
   428  
   429  // Test HTML processing with a source file not run through gofmt.
   430  // Issue #27350.
   431  func testHtmlUnformatted(t *testing.T, toolexecArg string) {
   432  	testenv.MustHaveGoRun(t)
   433  	dir := tempDir(t)
   434  
   435  	t.Parallel()
   436  
   437  	htmlUDir := filepath.Join(dir, "htmlunformatted")
   438  	htmlU := filepath.Join(htmlUDir, "htmlunformatted.go")
   439  	htmlUTest := filepath.Join(htmlUDir, "htmlunformatted_test.go")
   440  	htmlUProfile := filepath.Join(htmlUDir, "htmlunformatted.cov")
   441  	htmlUHTML := filepath.Join(htmlUDir, "htmlunformatted.html")
   442  
   443  	if err := os.Mkdir(htmlUDir, 0777); err != nil {
   444  		t.Fatal(err)
   445  	}
   446  
   447  	if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil {
   448  		t.Fatal(err)
   449  	}
   450  
   451  	const htmlUContents = `
   452  package htmlunformatted
   453  
   454  var g int
   455  
   456  func F() {
   457  //line x.go:1
   458  	{ { F(); goto lab } }
   459  lab:
   460  }`
   461  
   462  	const htmlUTestContents = `package htmlunformatted`
   463  
   464  	if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil {
   465  		t.Fatal(err)
   466  	}
   467  	if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil {
   468  		t.Fatal(err)
   469  	}
   470  
   471  	// go test -covermode=count -coverprofile TMPDIR/htmlunformatted.cov
   472  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-test.v", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile)
   473  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   474  	cmd.Dir = htmlUDir
   475  	run(cmd, t)
   476  
   477  	// testcover -html TMPDIR/htmlunformatted.cov -o unformatted.html
   478  	cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
   479  	cmd.Dir = htmlUDir
   480  	run(cmd, t)
   481  }
   482  
   483  // lineDupContents becomes linedup.go in testFuncWithDuplicateLines.
   484  const lineDupContents = `
   485  package linedup
   486  
   487  var G int
   488  
   489  func LineDup(c int) {
   490  	for i := 0; i < c; i++ {
   491  //line ld.go:100
   492  		if i % 2 == 0 {
   493  			G++
   494  		}
   495  		if i % 3 == 0 {
   496  			G++; G++
   497  		}
   498  //line ld.go:100
   499  		if i % 4 == 0 {
   500  			G++; G++; G++
   501  		}
   502  		if i % 5 == 0 {
   503  			G++; G++; G++; G++
   504  		}
   505  	}
   506  }
   507  `
   508  
   509  // lineDupTestContents becomes linedup_test.go in testFuncWithDuplicateLines.
   510  const lineDupTestContents = `
   511  package linedup
   512  
   513  import "testing"
   514  
   515  func TestLineDup(t *testing.T) {
   516  	LineDup(100)
   517  }
   518  `
   519  
   520  // Test -func with duplicate //line directives with different numbers
   521  // of statements.
   522  func testFuncWithDuplicateLines(t *testing.T, toolexecArg string) {
   523  	testenv.MustHaveGoRun(t)
   524  	dir := tempDir(t)
   525  
   526  	t.Parallel()
   527  
   528  	lineDupDir := filepath.Join(dir, "linedup")
   529  	lineDupGo := filepath.Join(lineDupDir, "linedup.go")
   530  	lineDupTestGo := filepath.Join(lineDupDir, "linedup_test.go")
   531  	lineDupProfile := filepath.Join(lineDupDir, "linedup.out")
   532  
   533  	if err := os.Mkdir(lineDupDir, 0777); err != nil {
   534  		t.Fatal(err)
   535  	}
   536  
   537  	if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil {
   541  		t.Fatal(err)
   542  	}
   543  	if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil {
   544  		t.Fatal(err)
   545  	}
   546  
   547  	// go test -cover -covermode count -coverprofile TMPDIR/linedup.out
   548  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile)
   549  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   550  	cmd.Dir = lineDupDir
   551  	run(cmd, t)
   552  
   553  	// testcover -func=TMPDIR/linedup.out
   554  	cmd = testenv.Command(t, testcover(t), "-func", lineDupProfile)
   555  	cmd.Dir = lineDupDir
   556  	run(cmd, t)
   557  }
   558  
   559  func run(c *exec.Cmd, t *testing.T) {
   560  	t.Helper()
   561  	t.Log("running", c.Args)
   562  	out, err := c.CombinedOutput()
   563  	if len(out) > 0 {
   564  		t.Logf("%s", out)
   565  	}
   566  	if err != nil {
   567  		t.Fatal(err)
   568  	}
   569  }
   570  
   571  func runExpectingError(c *exec.Cmd, t *testing.T) string {
   572  	t.Helper()
   573  	t.Log("running", c.Args)
   574  	out, err := c.CombinedOutput()
   575  	if err == nil {
   576  		return fmt.Sprintf("unexpected pass for %+v", c.Args)
   577  	}
   578  	return string(out)
   579  }
   580  
   581  // Test instrumentation of package that ends before an expected
   582  // trailing newline following package clause. Issue #58370.
   583  func testMissingTrailingNewlineIssue58370(t *testing.T, toolexecArg string) {
   584  	testenv.MustHaveGoBuild(t)
   585  	dir := tempDir(t)
   586  
   587  	t.Parallel()
   588  
   589  	noeolDir := filepath.Join(dir, "issue58370")
   590  	noeolGo := filepath.Join(noeolDir, "noeol.go")
   591  	noeolTestGo := filepath.Join(noeolDir, "noeol_test.go")
   592  
   593  	if err := os.Mkdir(noeolDir, 0777); err != nil {
   594  		t.Fatal(err)
   595  	}
   596  
   597  	if err := os.WriteFile(filepath.Join(noeolDir, "go.mod"), []byte("module noeol\n"), 0666); err != nil {
   598  		t.Fatal(err)
   599  	}
   600  	const noeolContents = `package noeol`
   601  	if err := os.WriteFile(noeolGo, []byte(noeolContents), 0444); err != nil {
   602  		t.Fatal(err)
   603  	}
   604  	const noeolTestContents = `
   605  package noeol
   606  import "testing"
   607  func TestCoverage(t *testing.T) { }
   608  `
   609  	if err := os.WriteFile(noeolTestGo, []byte(noeolTestContents), 0444); err != nil {
   610  		t.Fatal(err)
   611  	}
   612  
   613  	// go test -covermode atomic
   614  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", toolexecArg, "-covermode", "atomic")
   615  	cmd.Env = append(cmd.Environ(), "CMDCOVER_TOOLEXEC=true")
   616  	cmd.Dir = noeolDir
   617  	run(cmd, t)
   618  }
   619  
   620  func TestSrcPathWithNewline(t *testing.T) {
   621  	testenv.MustHaveExec(t)
   622  	t.Parallel()
   623  
   624  	// srcPath is intentionally not clean so that the path passed to testcover
   625  	// will not normalize the trailing / to a \ on Windows.
   626  	srcPath := t.TempDir() + string(filepath.Separator) + "\npackage main\nfunc main() { panic(string([]rune{'u', 'h', '-', 'o', 'h'}))\n/*/main.go"
   627  	mainSrc := ` package main
   628  
   629  func main() {
   630  	/* nothing here */
   631  	println("ok")
   632  }
   633  `
   634  	if err := os.MkdirAll(filepath.Dir(srcPath), 0777); err != nil {
   635  		t.Skipf("creating directory with bogus path: %v", err)
   636  	}
   637  	if err := os.WriteFile(srcPath, []byte(mainSrc), 0666); err != nil {
   638  		t.Skipf("writing file with bogus directory: %v", err)
   639  	}
   640  
   641  	cmd := testenv.Command(t, testcover(t), "-mode=atomic", srcPath)
   642  	cmd.Stderr = new(bytes.Buffer)
   643  	out, err := cmd.Output()
   644  	t.Logf("%v:\n%s", cmd, out)
   645  	t.Logf("stderr:\n%s", cmd.Stderr)
   646  	if err == nil {
   647  		t.Errorf("unexpected success; want failure due to newline in file path")
   648  	}
   649  }
   650  

View as plain text