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 return testenv.Executable(t)
37 }
38
39
40 var testTempDir string
41
42
43 var debug = flag.Bool("debug", false, "keep tmpdir files for debugging")
44
45
46
47
48 func TestMain(m *testing.M) {
49 if os.Getenv("CMDCOVER_TOOLEXEC") != "" {
50
51
52 tool := strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe")
53 if tool == "cover" {
54
55
56
57
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
72
73
74
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
108
109
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
128
129
130
131
132 func TestCover(t *testing.T) {
133 testenv.MustHaveGoRun(t)
134 t.Parallel()
135 dir := tempDir(t)
136
137
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
149
150
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
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
175
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
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
195 if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
196 t.Error("misplaced compiler directive")
197 }
198
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
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
211
212
213
214 func TestDirectives(t *testing.T) {
215 testenv.MustHaveExec(t)
216 t.Parallel()
217
218
219
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
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
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
256
257
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
273
274
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
300 name string
301 offset int
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++
314 p := source[offset+i:]
315 j := bytes.IndexByte(p, '\n')
316 if j < 0 {
317
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
332
333 func TestCoverFunc(t *testing.T) {
334
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
352
353 func testCoverHTML(t *testing.T, toolexecArg string) {
354 testenv.MustHaveGoRun(t)
355 dir := tempDir(t)
356
357 t.Parallel()
358
359
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
365 htmlHTML := filepath.Join(dir, "html.html")
366 cmd = testenv.Command(t, testcover(t), "-html", htmlProfile, "-o", htmlHTML)
367 run(cmd, t)
368
369
370
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
399
400 goldenLines := strings.Split(string(golden), "\n")
401 outLines := strings.Split(out.String(), "\n")
402
403
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
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
421
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
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
469 cmd = testenv.Command(t, testcover(t), "-html", htmlUProfile, "-o", htmlUHTML)
470 cmd.Dir = htmlUDir
471 run(cmd, t)
472 }
473
474
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
501 const lineDupTestContents = `
502 package linedup
503
504 import "testing"
505
506 func TestLineDup(t *testing.T) {
507 LineDup(100)
508 }
509 `
510
511
512
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
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
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
573
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
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
616
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