| // Copyright 2021 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build go.1.16 |
| // +build go.1.16 |
| |
| // Running this program in the tools directory will produce a coverage file /tmp/cover.out |
| // and a coverage report for all the packages under internal/lsp, accumulated by all the tests |
| // under gopls. |
| // |
| // -o controls where the coverage file is written, defaulting to /tmp/cover.out |
| // -i coverage-file will generate the report from an existing coverage file |
| // -v controls verbosity (0: only report coverage, 1: report as each directory is finished, |
| // 2: report on each test, 3: more details, 4: too much) |
| // -t tests only tests packages in the given comma-separated list of directories in gopls. |
| // The names should start with ., as in ./internal/regtest/bench |
| // -run tests. If set, -run tests is passed on to the go test command. |
| // |
| // Despite gopls' use of goroutines, the counts are almost deterministic. |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "sort" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/cover" |
| ) |
| |
| var ( |
| proFile = flag.String("i", "", "existing profile file") |
| outFile = flag.String("o", "/tmp/cover.out", "where to write the coverage file") |
| verbose = flag.Int("v", 0, "how much detail to print as tests are running") |
| tests = flag.String("t", "", "list of tests to run") |
| run = flag.String("run", "", "value of -run to pass to go test") |
| ) |
| |
| func main() { |
| log.SetFlags(log.Lshortfile) |
| flag.Parse() |
| |
| if *proFile != "" { |
| report(*proFile) |
| return |
| } |
| |
| checkCwd() |
| // find the packages under gopls containing tests |
| tests := listDirs("gopls") |
| tests = onlyTests(tests) |
| tests = realTestName(tests) |
| |
| // report coverage for packages under internal/lsp |
| parg := "golang.org/x/tools/internal/lsp/..." |
| |
| accum := []string{} |
| seen := make(map[string]bool) |
| now := time.Now() |
| for _, toRun := range tests { |
| if excluded(toRun) { |
| continue |
| } |
| x := runTest(toRun, parg) |
| if *verbose > 0 { |
| fmt.Printf("finished %s %.1fs\n", toRun, time.Since(now).Seconds()) |
| } |
| lines := bytes.Split(x, []byte{'\n'}) |
| for _, l := range lines { |
| if len(l) == 0 { |
| continue |
| } |
| if !seen[string(l)] { |
| // not accumulating counts, so only works for mode:set |
| seen[string(l)] = true |
| accum = append(accum, string(l)) |
| } |
| } |
| } |
| sort.Strings(accum[1:]) |
| if err := os.WriteFile(*outFile, []byte(strings.Join(accum, "\n")), 0644); err != nil { |
| log.Print(err) |
| } |
| report(*outFile) |
| } |
| |
| type result struct { |
| Time time.Time |
| Test string |
| Action string |
| Package string |
| Output string |
| Elapsed float64 |
| } |
| |
| func runTest(tName, parg string) []byte { |
| args := []string{"test", "-short", "-coverpkg", parg, "-coverprofile", *outFile, |
| "-json"} |
| if *run != "" { |
| args = append(args, fmt.Sprintf("-run=%s", *run)) |
| } |
| args = append(args, tName) |
| cmd := exec.Command("go", args...) |
| cmd.Dir = "./gopls" |
| ans, err := cmd.Output() |
| if *verbose > 1 { |
| got := strings.Split(string(ans), "\n") |
| for _, g := range got { |
| if g == "" { |
| continue |
| } |
| var m result |
| if err := json.Unmarshal([]byte(g), &m); err != nil { |
| log.Printf("%T/%v", err, err) // shouldn't happen |
| continue |
| } |
| maybePrint(m) |
| } |
| } |
| if err != nil { |
| log.Printf("%s: %q, cmd=%s", tName, ans, cmd.String()) |
| } |
| buf, err := os.ReadFile(*outFile) |
| if err != nil { |
| log.Fatal(err) |
| } |
| return buf |
| } |
| |
| func report(fn string) { |
| profs, err := cover.ParseProfiles(fn) |
| if err != nil { |
| log.Fatal(err) |
| } |
| for _, p := range profs { |
| statements, counts := 0, 0 |
| for _, x := range p.Blocks { |
| statements += x.NumStmt |
| if x.Count != 0 { |
| counts += x.NumStmt // sic: if any were executed, all were |
| } |
| } |
| pc := 100 * float64(counts) / float64(statements) |
| fmt.Printf("%3.0f%% %3d/%3d %s\n", pc, counts, statements, p.FileName) |
| } |
| } |
| |
| var todo []string // tests to run |
| |
| func excluded(tname string) bool { |
| if *tests == "" { // run all tests |
| return false |
| } |
| if todo == nil { |
| todo = strings.Split(*tests, ",") |
| } |
| for _, nm := range todo { |
| if tname == nm { // run this test |
| return false |
| } |
| } |
| // not in list, skip it |
| return true |
| } |
| |
| // should m.Package be printed sometime? |
| func maybePrint(m result) { |
| switch m.Action { |
| case "pass", "fail", "skip": |
| fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) |
| case "run": |
| if *verbose > 2 { |
| fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) |
| } |
| case "output": |
| if *verbose > 3 { |
| fmt.Printf("%s %s %q %.3f\n", m.Action, m.Test, m.Output, m.Elapsed) |
| } |
| default: |
| log.Fatalf("unknown action %s\n", m.Action) |
| } |
| } |
| |
| // return only the directories that contain tests |
| func onlyTests(s []string) []string { |
| ans := []string{} |
| outer: |
| for _, d := range s { |
| files, err := os.ReadDir(d) |
| if err != nil { |
| log.Fatalf("%s: %v", d, err) |
| } |
| for _, de := range files { |
| if strings.Contains(de.Name(), "_test.go") { |
| ans = append(ans, d) |
| continue outer |
| } |
| } |
| } |
| return ans |
| } |
| |
| // replace the prefix gopls/ with ./ as the tests are run in the gopls directory |
| func realTestName(p []string) []string { |
| ans := []string{} |
| for _, x := range p { |
| x = x[len("gopls/"):] |
| ans = append(ans, "./"+x) |
| } |
| return ans |
| } |
| |
| // make sure we start in a tools directory |
| func checkCwd() { |
| dir, err := os.Getwd() |
| if err != nil { |
| log.Fatal(err) |
| } |
| // we expect to be a the root of golang.org/x/tools |
| cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "golang.org/x/tools") |
| buf, err := cmd.Output() |
| buf = bytes.Trim(buf, "\n \t") // remove \n at end |
| if err != nil { |
| log.Fatal(err) |
| } |
| if string(buf) != dir { |
| log.Fatalf("wrong directory: in %q, should be in %q", dir, string(buf)) |
| } |
| // and we expect gopls and internal/lsp as subdirectories |
| _, err = os.Stat("gopls") |
| if err != nil { |
| log.Fatalf("expected a gopls directory, %v", err) |
| } |
| _, err = os.Stat("internal/lsp") |
| if err != nil { |
| log.Fatalf("expected to see internal/lsp, %v", err) |
| } |
| } |
| |
| func listDirs(dir string) []string { |
| ans := []string{} |
| f := func(path string, dirEntry os.DirEntry, err error) error { |
| if strings.HasSuffix(path, "/testdata") || strings.HasSuffix(path, "/typescript") { |
| return filepath.SkipDir |
| } |
| if dirEntry.IsDir() { |
| ans = append(ans, path) |
| } |
| return nil |
| } |
| filepath.WalkDir(dir, f) |
| return ans |
| } |