1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cfile
6
7import (
8	"fmt"
9	"internal/coverage"
10	"internal/goexperiment"
11	"internal/platform"
12	"internal/testenv"
13	"os"
14	"os/exec"
15	"path/filepath"
16	"runtime"
17	"strings"
18	"testing"
19)
20
21// Set to true for debugging (linux only).
22const fixedTestDir = false
23
24func TestCoverageApis(t *testing.T) {
25	if testing.Short() {
26		t.Skipf("skipping test: too long for short mode")
27	}
28	if !goexperiment.CoverageRedesign {
29		t.Skipf("skipping new coverage tests (experiment not enabled)")
30	}
31	testenv.MustHaveGoBuild(t)
32	dir := t.TempDir()
33	if fixedTestDir {
34		dir = "/tmp/qqqzzz"
35		os.RemoveAll(dir)
36		mkdir(t, dir)
37	}
38
39	// Build harness. We need two copies of the harness, one built
40	// with -covermode=atomic and one built non-atomic.
41	bdir1 := mkdir(t, filepath.Join(dir, "build1"))
42	hargs1 := []string{"-covermode=atomic", "-coverpkg=all"}
43	atomicHarnessPath := buildHarness(t, bdir1, hargs1)
44	nonAtomicMode := testing.CoverMode()
45	if testing.CoverMode() == "atomic" {
46		nonAtomicMode = "set"
47	}
48	bdir2 := mkdir(t, filepath.Join(dir, "build2"))
49	hargs2 := []string{"-coverpkg=all", "-covermode=" + nonAtomicMode}
50	nonAtomicHarnessPath := buildHarness(t, bdir2, hargs2)
51
52	t.Logf("atomic harness path is %s", atomicHarnessPath)
53	t.Logf("non-atomic harness path is %s", nonAtomicHarnessPath)
54
55	// Sub-tests for each API we want to inspect, plus
56	// extras for error testing.
57	t.Run("emitToDir", func(t *testing.T) {
58		t.Parallel()
59		testEmitToDir(t, atomicHarnessPath, dir)
60	})
61	t.Run("emitToWriter", func(t *testing.T) {
62		t.Parallel()
63		testEmitToWriter(t, atomicHarnessPath, dir)
64	})
65	t.Run("emitToNonexistentDir", func(t *testing.T) {
66		t.Parallel()
67		testEmitToNonexistentDir(t, atomicHarnessPath, dir)
68	})
69	t.Run("emitToNilWriter", func(t *testing.T) {
70		t.Parallel()
71		testEmitToNilWriter(t, atomicHarnessPath, dir)
72	})
73	t.Run("emitToFailingWriter", func(t *testing.T) {
74		t.Parallel()
75		testEmitToFailingWriter(t, atomicHarnessPath, dir)
76	})
77	t.Run("emitWithCounterClear", func(t *testing.T) {
78		t.Parallel()
79		testEmitWithCounterClear(t, atomicHarnessPath, dir)
80	})
81	t.Run("emitToDirNonAtomic", func(t *testing.T) {
82		t.Parallel()
83		testEmitToDirNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
84	})
85	t.Run("emitToWriterNonAtomic", func(t *testing.T) {
86		t.Parallel()
87		testEmitToWriterNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
88	})
89	t.Run("emitWithCounterClearNonAtomic", func(t *testing.T) {
90		t.Parallel()
91		testEmitWithCounterClearNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
92	})
93}
94
95// upmergeCoverData helps improve coverage data for this package
96// itself. If this test itself is being invoked with "-cover", then
97// what we'd like is for package coverage data (that is, coverage for
98// routines in "runtime/coverage") to be incorporated into the test
99// run from the "harness.exe" runs we've just done. We can accomplish
100// this by doing a merge from the harness gocoverdir's to the test
101// gocoverdir.
102func upmergeCoverData(t *testing.T, gocoverdir string, mode string) {
103	if testing.CoverMode() != mode {
104		return
105	}
106	testGoCoverDir := os.Getenv("GOCOVERDIR")
107	if testGoCoverDir == "" {
108		return
109	}
110	args := []string{"tool", "covdata", "merge", "-pkg=runtime/coverage",
111		"-o", testGoCoverDir, "-i", gocoverdir}
112	t.Logf("up-merge of covdata from %s to %s", gocoverdir, testGoCoverDir)
113	t.Logf("executing: go %+v", args)
114	cmd := exec.Command(testenv.GoToolPath(t), args...)
115	if b, err := cmd.CombinedOutput(); err != nil {
116		t.Fatalf("covdata merge failed (%v): %s", err, b)
117	}
118}
119
120// buildHarness builds the helper program "harness.exe".
121func buildHarness(t *testing.T, dir string, opts []string) string {
122	harnessPath := filepath.Join(dir, "harness.exe")
123	harnessSrc := filepath.Join("testdata", "harness.go")
124	args := []string{"build", "-o", harnessPath}
125	args = append(args, opts...)
126	args = append(args, harnessSrc)
127	//t.Logf("harness build: go %+v\n", args)
128	cmd := exec.Command(testenv.GoToolPath(t), args...)
129	if b, err := cmd.CombinedOutput(); err != nil {
130		t.Fatalf("build failed (%v): %s", err, b)
131	}
132	return harnessPath
133}
134
135func mkdir(t *testing.T, d string) string {
136	t.Helper()
137	if err := os.Mkdir(d, 0777); err != nil {
138		t.Fatalf("mkdir failed: %v", err)
139	}
140	return d
141}
142
143// updateGoCoverDir updates the specified environment 'env' to set
144// GOCOVERDIR to 'gcd' (if setGoCoverDir is TRUE) or removes
145// GOCOVERDIR from the environment (if setGoCoverDir is false).
146func updateGoCoverDir(env []string, gcd string, setGoCoverDir bool) []string {
147	rv := []string{}
148	found := false
149	for _, v := range env {
150		if strings.HasPrefix(v, "GOCOVERDIR=") {
151			if !setGoCoverDir {
152				continue
153			}
154			v = "GOCOVERDIR=" + gcd
155			found = true
156		}
157		rv = append(rv, v)
158	}
159	if !found && setGoCoverDir {
160		rv = append(rv, "GOCOVERDIR="+gcd)
161	}
162	return rv
163}
164
165func runHarness(t *testing.T, harnessPath string, tp string, setGoCoverDir bool, rdir, edir string) (string, error) {
166	t.Logf("running: %s -tp %s -o %s with rdir=%s and GOCOVERDIR=%v", harnessPath, tp, edir, rdir, setGoCoverDir)
167	cmd := exec.Command(harnessPath, "-tp", tp, "-o", edir)
168	cmd.Dir = rdir
169	cmd.Env = updateGoCoverDir(os.Environ(), rdir, setGoCoverDir)
170	b, err := cmd.CombinedOutput()
171	//t.Logf("harness run output: %s\n", string(b))
172	return string(b), err
173}
174
175func testForSpecificFunctions(t *testing.T, dir string, want []string, avoid []string) string {
176	args := []string{"tool", "covdata", "debugdump",
177		"-live", "-pkg=command-line-arguments", "-i=" + dir}
178	t.Logf("running: go %v\n", args)
179	cmd := exec.Command(testenv.GoToolPath(t), args...)
180	b, err := cmd.CombinedOutput()
181	if err != nil {
182		t.Fatalf("'go tool covdata failed (%v): %s", err, b)
183	}
184	output := string(b)
185	rval := ""
186	for _, f := range want {
187		wf := "Func: " + f + "\n"
188		if strings.Contains(output, wf) {
189			continue
190		}
191		rval += fmt.Sprintf("error: output should contain %q but does not\n", wf)
192	}
193	for _, f := range avoid {
194		wf := "Func: " + f + "\n"
195		if strings.Contains(output, wf) {
196			rval += fmt.Sprintf("error: output should not contain %q but does\n", wf)
197		}
198	}
199	if rval != "" {
200		t.Logf("=-= begin output:\n%s\n=-= end output\n", output)
201	}
202	return rval
203}
204
205func withAndWithoutRunner(f func(setit bool, tag string)) {
206	// Run 'f' with and without GOCOVERDIR set.
207	for i := 0; i < 2; i++ {
208		tag := "x"
209		setGoCoverDir := true
210		if i == 0 {
211			setGoCoverDir = false
212			tag = "y"
213		}
214		f(setGoCoverDir, tag)
215	}
216}
217
218func mktestdirs(t *testing.T, tag, tp, dir string) (string, string) {
219	t.Helper()
220	rdir := mkdir(t, filepath.Join(dir, tp+"-rdir-"+tag))
221	edir := mkdir(t, filepath.Join(dir, tp+"-edir-"+tag))
222	return rdir, edir
223}
224
225func testEmitToDir(t *testing.T, harnessPath string, dir string) {
226	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
227		tp := "emitToDir"
228		rdir, edir := mktestdirs(t, tag, tp, dir)
229		output, err := runHarness(t, harnessPath, tp,
230			setGoCoverDir, rdir, edir)
231		if err != nil {
232			t.Logf("%s", output)
233			t.Fatalf("running 'harness -tp emitDir': %v", err)
234		}
235
236		// Just check to make sure meta-data file and counter data file were
237		// written. Another alternative would be to run "go tool covdata"
238		// or equivalent, but for now, this is what we've got.
239		dents, err := os.ReadDir(edir)
240		if err != nil {
241			t.Fatalf("os.ReadDir(%s) failed: %v", edir, err)
242		}
243		mfc := 0
244		cdc := 0
245		for _, e := range dents {
246			if e.IsDir() {
247				continue
248			}
249			if strings.HasPrefix(e.Name(), coverage.MetaFilePref) {
250				mfc++
251			} else if strings.HasPrefix(e.Name(), coverage.CounterFilePref) {
252				cdc++
253			}
254		}
255		wantmf := 1
256		wantcf := 1
257		if mfc != wantmf {
258			t.Errorf("EmitToDir: want %d meta-data files, got %d\n", wantmf, mfc)
259		}
260		if cdc != wantcf {
261			t.Errorf("EmitToDir: want %d counter-data files, got %d\n", wantcf, cdc)
262		}
263		upmergeCoverData(t, edir, "atomic")
264		upmergeCoverData(t, rdir, "atomic")
265	})
266}
267
268func testEmitToWriter(t *testing.T, harnessPath string, dir string) {
269	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
270		tp := "emitToWriter"
271		rdir, edir := mktestdirs(t, tag, tp, dir)
272		output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
273		if err != nil {
274			t.Logf("%s", output)
275			t.Fatalf("running 'harness -tp %s': %v", tp, err)
276		}
277		want := []string{"main", tp}
278		avoid := []string{"final"}
279		if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
280			t.Errorf("coverage data from %q output match failed: %s", tp, msg)
281		}
282		upmergeCoverData(t, edir, "atomic")
283		upmergeCoverData(t, rdir, "atomic")
284	})
285}
286
287func testEmitToNonexistentDir(t *testing.T, harnessPath string, dir string) {
288	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
289		tp := "emitToNonexistentDir"
290		rdir, edir := mktestdirs(t, tag, tp, dir)
291		output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
292		if err != nil {
293			t.Logf("%s", output)
294			t.Fatalf("running 'harness -tp %s': %v", tp, err)
295		}
296		upmergeCoverData(t, edir, "atomic")
297		upmergeCoverData(t, rdir, "atomic")
298	})
299}
300
301func testEmitToUnwritableDir(t *testing.T, harnessPath string, dir string) {
302	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
303
304		tp := "emitToUnwritableDir"
305		rdir, edir := mktestdirs(t, tag, tp, dir)
306
307		// Make edir unwritable.
308		if err := os.Chmod(edir, 0555); err != nil {
309			t.Fatalf("chmod failed: %v", err)
310		}
311		defer os.Chmod(edir, 0777)
312
313		output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
314		if err != nil {
315			t.Logf("%s", output)
316			t.Fatalf("running 'harness -tp %s': %v", tp, err)
317		}
318		upmergeCoverData(t, edir, "atomic")
319		upmergeCoverData(t, rdir, "atomic")
320	})
321}
322
323func testEmitToNilWriter(t *testing.T, harnessPath string, dir string) {
324	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
325		tp := "emitToNilWriter"
326		rdir, edir := mktestdirs(t, tag, tp, dir)
327		output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
328		if err != nil {
329			t.Logf("%s", output)
330			t.Fatalf("running 'harness -tp %s': %v", tp, err)
331		}
332		upmergeCoverData(t, edir, "atomic")
333		upmergeCoverData(t, rdir, "atomic")
334	})
335}
336
337func testEmitToFailingWriter(t *testing.T, harnessPath string, dir string) {
338	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
339		tp := "emitToFailingWriter"
340		rdir, edir := mktestdirs(t, tag, tp, dir)
341		output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
342		if err != nil {
343			t.Logf("%s", output)
344			t.Fatalf("running 'harness -tp %s': %v", tp, err)
345		}
346		upmergeCoverData(t, edir, "atomic")
347		upmergeCoverData(t, rdir, "atomic")
348	})
349}
350
351func testEmitWithCounterClear(t *testing.T, harnessPath string, dir string) {
352	withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
353		tp := "emitWithCounterClear"
354		rdir, edir := mktestdirs(t, tag, tp, dir)
355		output, err := runHarness(t, harnessPath, tp,
356			setGoCoverDir, rdir, edir)
357		if err != nil {
358			t.Logf("%s", output)
359			t.Fatalf("running 'harness -tp %s': %v", tp, err)
360		}
361		want := []string{tp, "postClear"}
362		avoid := []string{"preClear", "main", "final"}
363		if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
364			t.Logf("%s", output)
365			t.Errorf("coverage data from %q output match failed: %s", tp, msg)
366		}
367		upmergeCoverData(t, edir, "atomic")
368		upmergeCoverData(t, rdir, "atomic")
369	})
370}
371
372func testEmitToDirNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
373	tp := "emitToDir"
374	tag := "nonatomdir"
375	rdir, edir := mktestdirs(t, tag, tp, dir)
376	output, err := runHarness(t, harnessPath, tp,
377		true, rdir, edir)
378
379	// We expect an error here.
380	if err == nil {
381		t.Logf("%s", output)
382		t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
383	}
384
385	got := strings.TrimSpace(string(output))
386	want := "WriteCountersDir invoked for program built"
387	if !strings.Contains(got, want) {
388		t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
389			tp, got, want)
390	}
391	upmergeCoverData(t, edir, naMode)
392	upmergeCoverData(t, rdir, naMode)
393}
394
395func testEmitToWriterNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
396	tp := "emitToWriter"
397	tag := "nonatomw"
398	rdir, edir := mktestdirs(t, tag, tp, dir)
399	output, err := runHarness(t, harnessPath, tp,
400		true, rdir, edir)
401
402	// We expect an error here.
403	if err == nil {
404		t.Logf("%s", output)
405		t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
406	}
407
408	got := strings.TrimSpace(string(output))
409	want := "WriteCounters invoked for program built"
410	if !strings.Contains(got, want) {
411		t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
412			tp, got, want)
413	}
414
415	upmergeCoverData(t, edir, naMode)
416	upmergeCoverData(t, rdir, naMode)
417}
418
419func testEmitWithCounterClearNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
420	tp := "emitWithCounterClear"
421	tag := "cclear"
422	rdir, edir := mktestdirs(t, tag, tp, dir)
423	output, err := runHarness(t, harnessPath, tp,
424		true, rdir, edir)
425
426	// We expect an error here.
427	if err == nil {
428		t.Logf("%s", output)
429		t.Fatalf("running 'harness -tp %s' nonatomic: did not get expected error", tp)
430	}
431
432	got := strings.TrimSpace(string(output))
433	want := "ClearCounters invoked for program built"
434	if !strings.Contains(got, want) {
435		t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
436			tp, got, want)
437	}
438
439	upmergeCoverData(t, edir, naMode)
440	upmergeCoverData(t, rdir, naMode)
441}
442
443func TestApisOnNocoverBinary(t *testing.T) {
444	if testing.Short() {
445		t.Skipf("skipping test: too long for short mode")
446	}
447	testenv.MustHaveGoBuild(t)
448	dir := t.TempDir()
449
450	// Build harness with no -cover.
451	bdir := mkdir(t, filepath.Join(dir, "nocover"))
452	edir := mkdir(t, filepath.Join(dir, "emitDirNo"))
453	harnessPath := buildHarness(t, bdir, nil)
454	output, err := runHarness(t, harnessPath, "emitToDir", false, edir, edir)
455	if err == nil {
456		t.Fatalf("expected error on TestApisOnNocoverBinary harness run")
457	}
458	const want = "not built with -cover"
459	if !strings.Contains(output, want) {
460		t.Errorf("error output does not contain %q: %s", want, output)
461	}
462}
463
464func TestIssue56006EmitDataRaceCoverRunningGoroutine(t *testing.T) {
465	if testing.Short() {
466		t.Skipf("skipping test: too long for short mode")
467	}
468	if !goexperiment.CoverageRedesign {
469		t.Skipf("skipping new coverage tests (experiment not enabled)")
470	}
471
472	// This test requires "go test -race -cover", meaning that we need
473	// go build, go run, and "-race" support.
474	testenv.MustHaveGoRun(t)
475	if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) ||
476		!testenv.HasCGO() {
477		t.Skip("skipped due to lack of race detector support / CGO")
478	}
479
480	// This will run a program with -cover and -race where we have a
481	// goroutine still running (and updating counters) at the point where
482	// the test runtime is trying to write out counter data.
483	cmd := exec.Command(testenv.GoToolPath(t), "test", "-cover", "-race")
484	cmd.Dir = filepath.Join("testdata", "issue56006")
485	b, err := cmd.CombinedOutput()
486	if err != nil {
487		t.Fatalf("go test -cover -race failed: %v\n%s", err, b)
488	}
489
490	// Don't want to see any data races in output.
491	avoid := []string{"DATA RACE"}
492	for _, no := range avoid {
493		if strings.Contains(string(b), no) {
494			t.Logf("%s\n", string(b))
495			t.Fatalf("found %s in test output, not permitted", no)
496		}
497	}
498}
499
500func TestIssue59563TruncatedCoverPkgAll(t *testing.T) {
501	if testing.Short() {
502		t.Skipf("skipping test: too long for short mode")
503	}
504	testenv.MustHaveGoRun(t)
505
506	tmpdir := t.TempDir()
507	ppath := filepath.Join(tmpdir, "foo.cov")
508
509	cmd := exec.Command(testenv.GoToolPath(t), "test", "-coverpkg=all", "-coverprofile="+ppath)
510	cmd.Dir = filepath.Join("testdata", "issue59563")
511	b, err := cmd.CombinedOutput()
512	if err != nil {
513		t.Fatalf("go test -cover failed: %v\n%s", err, b)
514	}
515
516	cmd = exec.Command(testenv.GoToolPath(t), "tool", "cover", "-func="+ppath)
517	b, err = cmd.CombinedOutput()
518	if err != nil {
519		t.Fatalf("go tool cover -func failed: %v", err)
520	}
521
522	lines := strings.Split(string(b), "\n")
523	nfound := 0
524	bad := false
525	for _, line := range lines {
526		f := strings.Fields(line)
527		if len(f) == 0 {
528			continue
529		}
530		// We're only interested in the specific function "large" for
531		// the testcase being built. See the #59563 for details on why
532		// size matters.
533		if !(strings.HasPrefix(f[0], "internal/coverage/cfile/testdata/issue59563/repro.go") && strings.Contains(line, "large")) {
534			continue
535		}
536		nfound++
537		want := "100.0%"
538		if f[len(f)-1] != want {
539			t.Errorf("wanted %s got: %q\n", want, line)
540			bad = true
541		}
542	}
543	if nfound != 1 {
544		t.Errorf("wanted 1 found, got %d\n", nfound)
545		bad = true
546	}
547	if bad {
548		t.Logf("func output:\n%s\n", string(b))
549	}
550}
551