Source file
src/cmd/cover/cover_test.go
1
2
3
4
5 package main_test
6
7 import (
8 "bufio"
9 "bytes"
10 "flag"
11 "fmt"
12 "go/ast"
13 "go/parser"
14 "go/token"
15 "internal/testenv"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "regexp"
20 "strings"
21 "sync"
22 "testing"
23 )
24
25 const (
26
27 testdata = "testdata"
28 )
29
30 var (
31
32 testMain = filepath.Join(testdata, "main.go")
33 testTest = filepath.Join(testdata, "test.go")
34 coverProfile = filepath.Join(testdata, "profile.cov")
35 toolexecSource = filepath.Join(testdata, "toolexec.go")
36
37
38
39 htmlGolden = filepath.Join(testdata, "html", "html.golden")
40
41
42 tmpTestMain string
43 coverInput string
44 coverOutput string
45 htmlProfile string
46 htmlHTML string
47 htmlUDir string
48 htmlU string
49 htmlUTest string
50 htmlUProfile string
51 htmlUHTML string
52 lineDupDir string
53 lineDupGo string
54 lineDupTestGo string
55 lineDupProfile string
56 )
57
58 var (
59
60 testTempDir string
61
62
63 testcover string
64
65
66 toolexec string
67
68
69 testcoverErr error
70
71
72 testcoverOnce sync.Once
73
74
75 toolexecArg string
76 )
77
78 var debug = flag.Bool("debug", false, "keep rewritten files for debugging")
79
80
81
82 func TestMain(m *testing.M) {
83 dir, err := os.MkdirTemp("", "go-testcover")
84 if err != nil {
85 fmt.Fprintln(os.Stderr, err)
86 os.Exit(1)
87 }
88 os.Setenv("GOPATH", filepath.Join(dir, "_gopath"))
89
90 testTempDir = dir
91
92 tmpTestMain = filepath.Join(dir, "main.go")
93 coverInput = filepath.Join(dir, "test_line.go")
94 coverOutput = filepath.Join(dir, "test_cover.go")
95 htmlProfile = filepath.Join(dir, "html.cov")
96 htmlHTML = filepath.Join(dir, "html.html")
97 htmlUDir = filepath.Join(dir, "htmlunformatted")
98 htmlU = filepath.Join(htmlUDir, "htmlunformatted.go")
99 htmlUTest = filepath.Join(htmlUDir, "htmlunformatted_test.go")
100 htmlUProfile = filepath.Join(htmlUDir, "htmlunformatted.cov")
101 htmlUHTML = filepath.Join(htmlUDir, "htmlunformatted.html")
102 lineDupDir = filepath.Join(dir, "linedup")
103 lineDupGo = filepath.Join(lineDupDir, "linedup.go")
104 lineDupTestGo = filepath.Join(lineDupDir, "linedup_test.go")
105 lineDupProfile = filepath.Join(lineDupDir, "linedup.out")
106
107 status := m.Run()
108
109 if !*debug {
110 os.RemoveAll(dir)
111 }
112
113 os.Exit(status)
114 }
115
116
117
118 func buildCover(t *testing.T) {
119 t.Helper()
120 testenv.MustHaveGoBuild(t)
121 testcoverOnce.Do(func() {
122 var wg sync.WaitGroup
123 wg.Add(2)
124
125 var err1, err2 error
126 go func() {
127 defer wg.Done()
128 testcover = filepath.Join(testTempDir, "cover.exe")
129 t.Logf("running [go build -o %s]", testcover)
130 out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", testcover).CombinedOutput()
131 if len(out) > 0 {
132 t.Logf("%s", out)
133 }
134 err1 = err
135 }()
136
137 go func() {
138 defer wg.Done()
139 toolexec = filepath.Join(testTempDir, "toolexec.exe")
140 t.Logf("running [go -build -o %s %s]", toolexec, toolexecSource)
141 out, err := exec.Command(testenv.GoToolPath(t), "build", "-o", toolexec, toolexecSource).CombinedOutput()
142 if len(out) > 0 {
143 t.Logf("%s", out)
144 }
145 err2 = err
146 }()
147
148 wg.Wait()
149
150 testcoverErr = err1
151 if err2 != nil && err1 == nil {
152 testcoverErr = err2
153 }
154
155 toolexecArg = "-toolexec=" + toolexec + " " + testcover
156 })
157 if testcoverErr != nil {
158 t.Fatal("failed to build testcover or toolexec program:", testcoverErr)
159 }
160 }
161
162
163
164
165
166
167
168
169 func TestCover(t *testing.T) {
170 t.Parallel()
171 testenv.MustHaveGoRun(t)
172 buildCover(t)
173
174
175 file, err := os.ReadFile(testTest)
176 if err != nil {
177 t.Fatal(err)
178 }
179 lines := bytes.Split(file, []byte("\n"))
180 for i, line := range lines {
181 lines[i] = bytes.ReplaceAll(line, []byte("LINE"), []byte(fmt.Sprint(i+1)))
182 }
183
184
185
186
187 lines = append(lines, []byte("func unFormatted() {"),
188 []byte("\tif true {"),
189 []byte("\t}else{"),
190 []byte("\t}"),
191 []byte("}"))
192 lines = append(lines, []byte("func unFormatted2(b bool) {if b{}else{}}"))
193
194 if err := os.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666); err != nil {
195 t.Fatal(err)
196 }
197
198
199 cmd := exec.Command(testcover, "-mode=count", "-var=thisNameMustBeVeryLongToCauseOverflowOfCounterIncrementStatementOntoNextLineForTest", "-o", coverOutput, coverInput)
200 run(cmd, t)
201
202 cmd = exec.Command(testcover, "-mode=set", "-var=Not_an-identifier", "-o", coverOutput, coverInput)
203 err = cmd.Run()
204 if err == nil {
205 t.Error("Expected cover to fail with an error")
206 }
207
208
209
210 b, err := os.ReadFile(testMain)
211 if err != nil {
212 t.Fatal(err)
213 }
214 if err := os.WriteFile(tmpTestMain, b, 0444); err != nil {
215 t.Fatal(err)
216 }
217
218
219 cmd = exec.Command(testenv.GoToolPath(t), "run", tmpTestMain, coverOutput)
220 run(cmd, t)
221
222 file, err = os.ReadFile(coverOutput)
223 if err != nil {
224 t.Fatal(err)
225 }
226
227 if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
228 t.Error("misplaced compiler directive")
229 }
230
231 if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
232 t.Error("'go:linkname' compiler directive not found")
233 }
234
235
236 c := ".*// This comment didn't appear in generated go code.*"
237 if got, err := regexp.MatchString(c, string(file)); err != nil || !got {
238 t.Errorf("non compiler directive comment %q not found", c)
239 }
240 }
241
242
243
244
245
246 func TestDirectives(t *testing.T) {
247 t.Parallel()
248 buildCover(t)
249
250
251
252 testDirectives := filepath.Join(testdata, "directives.go")
253 source, err := os.ReadFile(testDirectives)
254 if err != nil {
255 t.Fatal(err)
256 }
257 sourceDirectives := findDirectives(source)
258
259
260 cmd := exec.Command(testcover, "-mode=atomic", testDirectives)
261 cmd.Stderr = os.Stderr
262 output, err := cmd.Output()
263 if err != nil {
264 t.Fatal(err)
265 }
266
267
268 outputDirectives := findDirectives(output)
269 foundDirective := make(map[string]bool)
270 for _, p := range sourceDirectives {
271 foundDirective[p.name] = false
272 }
273 for _, p := range outputDirectives {
274 if found, ok := foundDirective[p.name]; !ok {
275 t.Errorf("unexpected directive in output: %s", p.text)
276 } else if found {
277 t.Errorf("directive found multiple times in output: %s", p.text)
278 }
279 foundDirective[p.name] = true
280 }
281 for name, found := range foundDirective {
282 if !found {
283 t.Errorf("missing directive: %s", name)
284 }
285 }
286
287
288
289
290 fset := token.NewFileSet()
291 astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
292 if err != nil {
293 t.Fatal(err)
294 }
295
296 prevEnd := 0
297 for _, decl := range astFile.Decls {
298 var name string
299 switch d := decl.(type) {
300 case *ast.FuncDecl:
301 name = d.Name.Name
302 case *ast.GenDecl:
303 if len(d.Specs) == 0 {
304
305
306
307 name = "_empty"
308 } else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
309 name = spec.Name.Name
310 }
311 }
312 pos := fset.Position(decl.Pos()).Offset
313 end := fset.Position(decl.End()).Offset
314 if name == "" {
315 prevEnd = end
316 continue
317 }
318 for _, p := range outputDirectives {
319 if !strings.HasPrefix(p.name, name) {
320 continue
321 }
322 if p.offset < prevEnd || pos < p.offset {
323 t.Errorf("directive %s does not appear before definition %s", p.text, name)
324 }
325 }
326 prevEnd = end
327 }
328 }
329
330 type directiveInfo struct {
331 text string
332 name string
333 offset int
334 }
335
336 func findDirectives(source []byte) []directiveInfo {
337 var directives []directiveInfo
338 directivePrefix := []byte("\n//go:")
339 offset := 0
340 for {
341 i := bytes.Index(source[offset:], directivePrefix)
342 if i < 0 {
343 break
344 }
345 i++
346 p := source[offset+i:]
347 j := bytes.IndexByte(p, '\n')
348 if j < 0 {
349
350 j = len(p)
351 }
352 directive := directiveInfo{
353 text: string(p[:j]),
354 name: string(p[len(directivePrefix)-1 : j]),
355 offset: offset + i,
356 }
357 directives = append(directives, directive)
358 offset += i + j
359 }
360 return directives
361 }
362
363
364
365 func TestCoverFunc(t *testing.T) {
366 t.Parallel()
367 buildCover(t)
368
369 cmd := exec.Command(testcover, "-func", coverProfile)
370 out, err := cmd.Output()
371 if err != nil {
372 if ee, ok := err.(*exec.ExitError); ok {
373 t.Logf("%s", ee.Stderr)
374 }
375 t.Fatal(err)
376 }
377
378 if got, err := regexp.Match(".*total:.*100.0.*", out); err != nil || !got {
379 t.Logf("%s", out)
380 t.Errorf("invalid coverage counts. got=(%v, %v); want=(true; nil)", got, err)
381 }
382 }
383
384
385
386 func TestCoverHTML(t *testing.T) {
387 t.Parallel()
388 testenv.MustHaveGoRun(t)
389 buildCover(t)
390
391
392 cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-coverprofile", htmlProfile, "cmd/cover/testdata/html")
393 run(cmd, t)
394
395 cmd = exec.Command(testcover, "-html", htmlProfile, "-o", htmlHTML)
396 run(cmd, t)
397
398
399
400 entireHTML, err := os.ReadFile(htmlHTML)
401 if err != nil {
402 t.Fatal(err)
403 }
404 var out bytes.Buffer
405 scan := bufio.NewScanner(bytes.NewReader(entireHTML))
406 in := false
407 for scan.Scan() {
408 line := scan.Text()
409 if strings.Contains(line, "// START") {
410 in = true
411 }
412 if in {
413 fmt.Fprintln(&out, line)
414 }
415 if strings.Contains(line, "// END") {
416 in = false
417 }
418 }
419 if scan.Err() != nil {
420 t.Error(scan.Err())
421 }
422 golden, err := os.ReadFile(htmlGolden)
423 if err != nil {
424 t.Fatalf("reading golden file: %v", err)
425 }
426
427
428 goldenLines := strings.Split(string(golden), "\n")
429 outLines := strings.Split(out.String(), "\n")
430
431
432 for i, goldenLine := range goldenLines {
433 if i >= len(outLines) {
434 t.Fatalf("output shorter than golden; stops before line %d: %s\n", i+1, goldenLine)
435 }
436
437 goldenLine = strings.Join(strings.Fields(goldenLine), " ")
438 outLine := strings.Join(strings.Fields(outLines[i]), " ")
439 if outLine != goldenLine {
440 t.Fatalf("line %d differs: got:\n\t%s\nwant:\n\t%s", i+1, outLine, goldenLine)
441 }
442 }
443 if len(goldenLines) != len(outLines) {
444 t.Fatalf("output longer than golden; first extra output line %d: %q\n", len(goldenLines)+1, outLines[len(goldenLines)])
445 }
446 }
447
448
449
450 func TestHtmlUnformatted(t *testing.T) {
451 t.Parallel()
452 testenv.MustHaveGoRun(t)
453 buildCover(t)
454
455 if err := os.Mkdir(htmlUDir, 0777); err != nil {
456 t.Fatal(err)
457 }
458
459 if err := os.WriteFile(filepath.Join(htmlUDir, "go.mod"), []byte("module htmlunformatted\n"), 0666); err != nil {
460 t.Fatal(err)
461 }
462
463 const htmlUContents = `
464 package htmlunformatted
465
466 var g int
467
468 func F() {
469 //line x.go:1
470 { { F(); goto lab } }
471 lab:
472 }`
473
474 const htmlUTestContents = `package htmlunformatted`
475
476 if err := os.WriteFile(htmlU, []byte(htmlUContents), 0444); err != nil {
477 t.Fatal(err)
478 }
479 if err := os.WriteFile(htmlUTest, []byte(htmlUTestContents), 0444); err != nil {
480 t.Fatal(err)
481 }
482
483
484 cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-covermode=count", "-coverprofile", htmlUProfile)
485 cmd.Dir = htmlUDir
486 run(cmd, t)
487
488
489 cmd = exec.Command(testcover, "-html", htmlUProfile, "-o", htmlUHTML)
490 cmd.Dir = htmlUDir
491 run(cmd, t)
492 }
493
494
495 const lineDupContents = `
496 package linedup
497
498 var G int
499
500 func LineDup(c int) {
501 for i := 0; i < c; i++ {
502 //line ld.go:100
503 if i % 2 == 0 {
504 G++
505 }
506 if i % 3 == 0 {
507 G++; G++
508 }
509 //line ld.go:100
510 if i % 4 == 0 {
511 G++; G++; G++
512 }
513 if i % 5 == 0 {
514 G++; G++; G++; G++
515 }
516 }
517 }
518 `
519
520
521 const lineDupTestContents = `
522 package linedup
523
524 import "testing"
525
526 func TestLineDup(t *testing.T) {
527 LineDup(100)
528 }
529 `
530
531
532
533 func TestFuncWithDuplicateLines(t *testing.T) {
534 t.Parallel()
535 testenv.MustHaveGoRun(t)
536 buildCover(t)
537
538 if err := os.Mkdir(lineDupDir, 0777); err != nil {
539 t.Fatal(err)
540 }
541
542 if err := os.WriteFile(filepath.Join(lineDupDir, "go.mod"), []byte("module linedup\n"), 0666); err != nil {
543 t.Fatal(err)
544 }
545 if err := os.WriteFile(lineDupGo, []byte(lineDupContents), 0444); err != nil {
546 t.Fatal(err)
547 }
548 if err := os.WriteFile(lineDupTestGo, []byte(lineDupTestContents), 0444); err != nil {
549 t.Fatal(err)
550 }
551
552
553 cmd := exec.Command(testenv.GoToolPath(t), "test", toolexecArg, "-cover", "-covermode", "count", "-coverprofile", lineDupProfile)
554 cmd.Dir = lineDupDir
555 run(cmd, t)
556
557
558 cmd = exec.Command(testcover, "-func", lineDupProfile)
559 cmd.Dir = lineDupDir
560 run(cmd, t)
561 }
562
563 func run(c *exec.Cmd, t *testing.T) {
564 t.Helper()
565 t.Log("running", c.Args)
566 out, err := c.CombinedOutput()
567 if len(out) > 0 {
568 t.Logf("%s", out)
569 }
570 if err != nil {
571 t.Fatal(err)
572 }
573 }
574
View as plain text