• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5package main_test
6
7import (
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
27const (
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.
35func 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.
45var testTempDir string
46
47// If set, this will preserve all the tmpdir files from the test run.
48var 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.
53func 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
98var tdmu sync.Mutex
99var tdcount int
100
101func 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".
115func 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
139func 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.
221func 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
305type 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
311func 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.
340func 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.
362func 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.
431func 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 = `
452package htmlunformatted
453
454var g int
455
456func F() {
457//line x.go:1
458	{ { F(); goto lab } }
459lab:
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.
484const lineDupContents = `
485package linedup
486
487var G int
488
489func 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.
510const lineDupTestContents = `
511package linedup
512
513import "testing"
514
515func TestLineDup(t *testing.T) {
516	LineDup(100)
517}
518`
519
520// Test -func with duplicate //line directives with different numbers
521// of statements.
522func 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
559func 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
571func 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.
583func 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 = `
605package noeol
606import "testing"
607func 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
620func 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
629func 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