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