blob: 7bb3640bdbd9d5086d2d246a38dfad8281eacf20 [file] [log] [blame]
// 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
}