aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/coverage
diff options
context:
space:
mode:
authorpjw <pjw@google.com>2021-03-19 09:19:46 -0400
committerPeter Weinberger <pjw@google.com>2021-03-19 18:34:16 +0000
commit9bdb41970fd77def8c31a4e97ae5608900e9883c (patch)
treec34ffdacaae12cd3bbad79d4fa31cb35c6a27420 /gopls/internal/coverage
parentd2e11a2bf35309c8a1bc0c8376946a710f5209d0 (diff)
downloadgolang-x-tools-9bdb41970fd77def8c31a4e97ae5608900e9883c.tar.gz
gopls/internal: add coverage command to compute test coverage
Running gopls/internal/coverage/coverage.go in the tools directory produces a coverage file (/tmp/cover.out by default) and a report showing how well the gopls tests cover the packages in internal/lsp. Change-Id: I1a7a22321f807ae54194833ee6a8e2a80bd9dca0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/303290 Run-TryBot: Peter Weinberger <pjw@google.com> Trust: Peter Weinberger <pjw@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Robert Findley <rfindley@google.com>
Diffstat (limited to 'gopls/internal/coverage')
-rw-r--r--gopls/internal/coverage/coverage.go261
1 files changed, 261 insertions, 0 deletions
diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go
new file mode 100644
index 000000000..e5d17f370
--- /dev/null
+++ b/gopls/internal/coverage/coverage.go
@@ -0,0 +1,261 @@
+// 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", m.Action, m.Test, m.Elapsed)
+ case "run":
+ if *verbose > 2 {
+ fmt.Printf("%s %s %.3f", m.Action, m.Test, m.Elapsed)
+ }
+ case "output":
+ if *verbose > 3 {
+ fmt.Printf("%s %s %q %.3f", m.Action, m.Test, m.Output, m.Elapsed)
+ }
+ default:
+ log.Fatalf("unknown action %s", 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 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)
+ }
+ // and 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("got %q, wanted %q", dir, string(buf))
+ }
+}
+
+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
+}