1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
|
// 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/gopls/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)
}
case "pause", "cont":
if *verbose > 2 {
fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed)
}
default:
fmt.Printf("%#v\n", m)
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 at 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)
}
}
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
}
|