[tools] gopls/internal: add coverage command to compute test coverage

53 views
Skip to first unread message

Peter Weinberger (Gerrit)

unread,
Mar 19, 2021, 2:34:19 PM3/19/21
to goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Go Bot, kokoro, Robert Findley, golang-co...@googlegroups.com

Peter Weinberger submitted this change.

View Change

Approvals: Robert Findley: Looks good to me, approved Peter Weinberger: Trusted; Run TryBots Go Bot: TryBots succeeded kokoro: gopls CI succeeded
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 <p...@google.com>
Trust: Peter Weinberger <p...@google.com>
gopls-CI: kokoro <noreply...@google.com>
TryBot-Result: Go Bot <go...@golang.org>
Reviewed-by: Robert Findley <rfin...@google.com>
---
A gopls/internal/coverage/coverage.go
1 file changed, 261 insertions(+), 0 deletions(-)

diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go
new file mode 100644
index 0000000..e5d17f3
--- /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
+}

4 is the latest approved patch-set. The change was submitted with unreviewed changes in the following files: The name of the file: gopls/internal/coverage/coverage.go Insertions: 26, Deletions: 21. ``` @@ -15:16, +15:16 @@ - // -t tests only tests packages in the given comma-separated list of directories. + // -t tests only tests packages in the given comma-separated list of directories in gopls. @@ +18:20 @@ + // + // Despite gopls' use of goroutines, the counts are almost deterministic. @@ -28:29 @@ - "path" @@ -61:64, +62:63 @@ - // (skipping testdata, typescript) - packages := listDirs("internal/lsp") - parg := realNames(packages) + parg := "golang.org/x/tools/internal/lsp/..." @@ -89:90, +88:89 @@ - if err := os.WriteFile("/tmp/cover.out", []byte(strings.Join(accum, "\n")), 0644); err != nil { + if err := os.WriteFile(*outFile, []byte(strings.Join(accum, "\n")), 0644); err != nil { @@ -92:93, +91:92 @@ - report("/tmp/cover.out") + report(*outFile) @@ -105:106, +104:105 @@ - args := []string{"test", "-short", "-coverpkg", parg, "-coverprofile", "/tmp/cover.out", + args := []string{"test", "-short", "-coverpkg", parg, "-coverprofile", *outFile, @@ -131:132, +130:131 @@ - buf, err := os.ReadFile("/tmp/cover.out") + buf, err := os.ReadFile(*outFile) @@ -221:230 @@ - // change the internal/lsp directory names to their package names - func realNames(p []string) string { - names := []string{} - for _, x := range p { - names = append(names, "golang.org/x/tools/"+x) - } - return strings.Join(names, ",") - } - @@ -236:239, +226:244 @@ - base := path.Base(dir) - if base != "tools" { - log.Fatalf("must execute in tools dir, not in %s", dir) + // 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)) ```

To view, visit change 303290. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: I1a7a22321f807ae54194833ee6a8e2a80bd9dca0
Gerrit-Change-Number: 303290
Gerrit-PatchSet: 6
Gerrit-Owner: Peter Weinberger <p...@google.com>
Gerrit-Reviewer: Go Bot <go...@golang.org>
Gerrit-Reviewer: Peter Weinberger <p...@google.com>
Gerrit-Reviewer: Robert Findley <rfin...@google.com>
Gerrit-Reviewer: kokoro <noreply...@google.com>
Gerrit-MessageType: merged
Reply all
Reply to author
Forward
0 new messages