Source file src/internal/coverage/cfile/emitdata_test.go

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

View as plain text