Source file
src/cmd/cover/cover_test.go
1
2
3
4
5 package main_test
6
7 import (
8 "bufio"
9 "bytes"
10 cmdcover "cmd/cover"
11 "flag"
12 "fmt"
13 "go/ast"
14 "go/parser"
15 "go/token"
16 "internal/testenv"
17 "log"
18 "os"
19 "os/exec"
20 "path/filepath"
21 "regexp"
22 "strings"
23 "sync"
24 "testing"
25 )
26
27 const (
28
29 testdata = "testdata"
30 )
31
32
33
34
35 func testcover(t testing.TB) string {
36 exe, err := os.Executable()
37 if err != nil {
38 t.Helper()
39 t.Fatal(err)
40 }
41 return exe
42 }
43
44
45 var testTempDir string
46
47
48 var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
49
50
51
52
53 func TestMain(m *testing.M) {
54 if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
55
56
57 tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
58 if tool == "cover" {
59
60
61
62
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
77
78
79
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
113
114
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
135
136
137
138
139 func TestCover(t *testing.T) {
140 testenv.MustHaveGoRun(t)
141 t.Parallel()
142 dir := tempDir(t)
143
144
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
156
157
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
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
182
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
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
202 if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
203 t.Error("misplaced compiler directive")
204 }
205
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
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
218
219
220
221 func TestDirectives(t *testing.T) {
222 testenv.MustHaveExec(t)
223 t.Parallel()
224
225
226
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
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
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
263
264
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
280
281
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
307 name string
308 offset int
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++
321 p := source[offset+i:]
322 j := bytes.IndexByte(p, '\n')
323 if j < 0 {
324
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
339
340 func TestCoverFunc(t *testing.T) {
341 testenv.MustHaveExec(t)
342
343
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
361
362 func testCoverHTML(t *testing.T, toolexecArg string) {
363 testenv.MustHaveGoRun(t)
364 dir := tempDir(t)
365
366 t.Parallel()
367
368
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
374 htmlHTML := filepath.Join(dir, "html.html")
375 cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
376 run(cmd, t)
377
378
379
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
408
409 goldenLines := strings.Split(string(golden), "\n")
410 outLines := strings.Split(out.String(), "\n")
411
412
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
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
430
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
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
478 cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
479 cmd.Dir = htmlUDir
480 run(cmd, t)
481 }
482
483
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
510 const lineDupTestContents = `
511 package linedup
512
513 import "testing"
514
515 func TestLineDup(t *testing.T) {
516 LineDup(100)
517 }
518 `
519
520
521
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
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
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
582
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
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
625
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