aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/coverage/coverage.go
blob: 9a7d219945e843a49b368f9efb3d44cec9e981c8 (plain)
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
}