// Copyright 2022 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.

package main_test

import (
	cmdcovdata "cmd/covdata"
	"flag"
	"fmt"
	"internal/coverage/pods"
	"internal/goexperiment"
	"internal/testenv"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"testing"
)

// testcovdata returns the path to the unit test executable to be used as
// standin for 'go tool covdata'.
func testcovdata(t testing.TB) string {
	exe, err := os.Executable()
	if err != nil {
		t.Helper()
		t.Fatal(err)
	}
	return exe
}

// Top level tempdir for test.
var testTempDir string

// If set, this will preserve all the tmpdir files from the test run.
var preserveTmp = flag.Bool("preservetmp", false, "keep tmpdir files for debugging")

// TestMain used here so that we can leverage the test executable
// itself as a cmd/covdata executable; compare to similar usage in
// the cmd/go tests.
func TestMain(m *testing.M) {
	// When CMDCOVDATA_TEST_RUN_MAIN is set, we're reusing the test
	// binary as cmd/cover. In this case we run the main func exported
	// via export_test.go, and exit; CMDCOVDATA_TEST_RUN_MAIN is set below
	// for actual test invocations.
	if os.Getenv("CMDCOVDATA_TEST_RUN_MAIN") != "" {
		cmdcovdata.Main()
		os.Exit(0)
	}
	flag.Parse()
	topTmpdir, err := os.MkdirTemp("", "cmd-covdata-test-")
	if err != nil {
		log.Fatal(err)
	}
	testTempDir = topTmpdir
	if !*preserveTmp {
		defer os.RemoveAll(topTmpdir)
	} else {
		fmt.Fprintf(os.Stderr, "debug: preserving tmpdir %s\n", topTmpdir)
	}
	os.Setenv("CMDCOVDATA_TEST_RUN_MAIN", "true")
	os.Exit(m.Run())
}

var tdmu sync.Mutex
var tdcount int

func tempDir(t *testing.T) string {
	tdmu.Lock()
	dir := filepath.Join(testTempDir, fmt.Sprintf("%03d", tdcount))
	tdcount++
	if err := os.Mkdir(dir, 0777); err != nil {
		t.Fatal(err)
	}
	defer tdmu.Unlock()
	return dir
}

const debugtrace = false

func gobuild(t *testing.T, indir string, bargs []string) {
	t.Helper()

	if debugtrace {
		if indir != "" {
			t.Logf("in dir %s: ", indir)
		}
		t.Logf("cmd: %s %+v\n", testenv.GoToolPath(t), bargs)
	}
	cmd := testenv.Command(t, testenv.GoToolPath(t), bargs...)
	cmd.Dir = indir
	b, err := cmd.CombinedOutput()
	if len(b) != 0 {
		t.Logf("## build output:\n%s", b)
	}
	if err != nil {
		t.Fatalf("build error: %v", err)
	}
}

func emitFile(t *testing.T, dst, src string) {
	payload, err := os.ReadFile(src)
	if err != nil {
		t.Fatalf("error reading %q: %v", src, err)
	}
	if err := os.WriteFile(dst, payload, 0666); err != nil {
		t.Fatalf("writing %q: %v", dst, err)
	}
}

const mainPkgPath = "prog"

func buildProg(t *testing.T, prog string, dir string, tag string, flags []string) (string, string) {
	// Create subdirs.
	subdir := filepath.Join(dir, prog+"dir"+tag)
	if err := os.Mkdir(subdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", subdir, err)
	}
	depdir := filepath.Join(subdir, "dep")
	if err := os.Mkdir(depdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", depdir, err)
	}

	// Emit program.
	insrc := filepath.Join("testdata", prog+".go")
	src := filepath.Join(subdir, prog+".go")
	emitFile(t, src, insrc)
	indep := filepath.Join("testdata", "dep.go")
	dep := filepath.Join(depdir, "dep.go")
	emitFile(t, dep, indep)

	// Emit go.mod.
	mod := filepath.Join(subdir, "go.mod")
	modsrc := "\nmodule " + mainPkgPath + "\n\ngo 1.19\n"
	if err := os.WriteFile(mod, []byte(modsrc), 0666); err != nil {
		t.Fatal(err)
	}
	exepath := filepath.Join(subdir, prog+".exe")
	bargs := []string{"build", "-cover", "-o", exepath}
	bargs = append(bargs, flags...)
	gobuild(t, subdir, bargs)
	return exepath, subdir
}

type state struct {
	dir      string
	exedir1  string
	exedir2  string
	exedir3  string
	exepath1 string
	exepath2 string
	exepath3 string
	tool     string
	outdirs  [4]string
}

const debugWorkDir = false

func TestCovTool(t *testing.T) {
	testenv.MustHaveGoBuild(t)
	if !goexperiment.CoverageRedesign {
		t.Skipf("stubbed out due to goexperiment.CoverageRedesign=false")
	}
	dir := tempDir(t)
	if testing.Short() {
		t.Skip()
	}
	if debugWorkDir {
		// debugging
		dir = "/tmp/qqq"
		os.RemoveAll(dir)
		os.Mkdir(dir, 0777)
	}

	s := state{
		dir: dir,
	}
	s.exepath1, s.exedir1 = buildProg(t, "prog1", dir, "", nil)
	s.exepath2, s.exedir2 = buildProg(t, "prog2", dir, "", nil)
	flags := []string{"-covermode=atomic"}
	s.exepath3, s.exedir3 = buildProg(t, "prog1", dir, "atomic", flags)

	// Reuse unit test executable as tool to be tested.
	s.tool = testcovdata(t)

	// Create a few coverage output dirs.
	for i := 0; i < 4; i++ {
		d := filepath.Join(dir, fmt.Sprintf("covdata%d", i))
		s.outdirs[i] = d
		if err := os.Mkdir(d, 0777); err != nil {
			t.Fatalf("can't create outdir %s: %v", d, err)
		}
	}

	// Run instrumented program to generate some coverage data output files,
	// as follows:
	//
	//   <tmp>/covdata0   -- prog1.go compiled -cover
	//   <tmp>/covdata1   -- prog1.go compiled -cover
	//   <tmp>/covdata2   -- prog1.go compiled -covermode=atomic
	//   <tmp>/covdata3   -- prog1.go compiled -covermode=atomic
	//
	for m := 0; m < 2; m++ {
		for k := 0; k < 2; k++ {
			args := []string{}
			if k != 0 {
				args = append(args, "foo", "bar")
			}
			for i := 0; i <= k; i++ {
				exepath := s.exepath1
				if m != 0 {
					exepath = s.exepath3
				}
				cmd := testenv.Command(t, exepath, args...)
				cmd.Env = append(cmd.Env, "GOCOVERDIR="+s.outdirs[m*2+k])
				b, err := cmd.CombinedOutput()
				if len(b) != 0 {
					t.Logf("## instrumented run output:\n%s", b)
				}
				if err != nil {
					t.Fatalf("instrumented run error: %v", err)
				}
			}
		}
	}

	// At this point we can fork off a bunch of child tests
	// to check different tool modes.
	t.Run("MergeSimple", func(t *testing.T) {
		t.Parallel()
		testMergeSimple(t, s, s.outdirs[0], s.outdirs[1], "set")
		testMergeSimple(t, s, s.outdirs[2], s.outdirs[3], "atomic")
	})
	t.Run("MergeSelect", func(t *testing.T) {
		t.Parallel()
		testMergeSelect(t, s, s.outdirs[0], s.outdirs[1], "set")
		testMergeSelect(t, s, s.outdirs[2], s.outdirs[3], "atomic")
	})
	t.Run("MergePcombine", func(t *testing.T) {
		t.Parallel()
		testMergeCombinePrograms(t, s)
	})
	t.Run("Dump", func(t *testing.T) {
		t.Parallel()
		testDump(t, s)
	})
	t.Run("Percent", func(t *testing.T) {
		t.Parallel()
		testPercent(t, s)
	})
	t.Run("PkgList", func(t *testing.T) {
		t.Parallel()
		testPkgList(t, s)
	})
	t.Run("Textfmt", func(t *testing.T) {
		t.Parallel()
		testTextfmt(t, s)
	})
	t.Run("Subtract", func(t *testing.T) {
		t.Parallel()
		testSubtract(t, s)
	})
	t.Run("Intersect", func(t *testing.T) {
		t.Parallel()
		testIntersect(t, s, s.outdirs[0], s.outdirs[1], "set")
		testIntersect(t, s, s.outdirs[2], s.outdirs[3], "atomic")
	})
	t.Run("CounterClash", func(t *testing.T) {
		t.Parallel()
		testCounterClash(t, s)
	})
	t.Run("TestEmpty", func(t *testing.T) {
		t.Parallel()
		testEmpty(t, s)
	})
	t.Run("TestCommandLineErrors", func(t *testing.T) {
		t.Parallel()
		testCommandLineErrors(t, s, s.outdirs[0])
	})
}

const showToolInvocations = true

func runToolOp(t *testing.T, s state, op string, args []string) []string {
	// Perform tool run.
	t.Helper()
	args = append([]string{op}, args...)
	if showToolInvocations {
		t.Logf("%s cmd is: %s %+v", op, s.tool, args)
	}
	cmd := testenv.Command(t, s.tool, args...)
	b, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Fprintf(os.Stderr, "## %s output: %s\n", op, string(b))
		t.Fatalf("%q run error: %v", op, err)
	}
	output := strings.TrimSpace(string(b))
	lines := strings.Split(output, "\n")
	if len(lines) == 1 && lines[0] == "" {
		lines = nil
	}
	return lines
}

func testDump(t *testing.T, s state) {
	// Run the dumper on the two dirs we generated.
	dargs := []string{"-pkg=" + mainPkgPath, "-live", "-i=" + s.outdirs[0] + "," + s.outdirs[1]}
	lines := runToolOp(t, s, "debugdump", dargs)

	// Sift through the output to make sure it has some key elements.
	testpoints := []struct {
		tag string
		re  *regexp.Regexp
	}{
		{
			"args",
			regexp.MustCompile(`^data file .+ GOOS=.+ GOARCH=.+ program args: .+$`),
		},
		{
			"main package",
			regexp.MustCompile(`^Package path: ` + mainPkgPath + `\s*$`),
		},
		{
			"main function",
			regexp.MustCompile(`^Func: main\s*$`),
		},
	}

	bad := false
	for _, testpoint := range testpoints {
		found := false
		for _, line := range lines {
			if m := testpoint.re.FindStringSubmatch(line); m != nil {
				found = true
				break
			}
		}
		if !found {
			t.Errorf("dump output regexp match failed for %q", testpoint.tag)
			bad = true
		}
	}
	if bad {
		dumplines(lines)
	}
}

func testPercent(t *testing.T, s state) {
	// Run the dumper on the two dirs we generated.
	dargs := []string{"-pkg=" + mainPkgPath, "-i=" + s.outdirs[0] + "," + s.outdirs[1]}
	lines := runToolOp(t, s, "percent", dargs)

	// Sift through the output to make sure it has the needful.
	testpoints := []struct {
		tag string
		re  *regexp.Regexp
	}{
		{
			"statement coverage percent",
			regexp.MustCompile(`coverage: \d+\.\d% of statements\s*$`),
		},
	}

	bad := false
	for _, testpoint := range testpoints {
		found := false
		for _, line := range lines {
			if m := testpoint.re.FindStringSubmatch(line); m != nil {
				found = true
				break
			}
		}
		if !found {
			t.Errorf("percent output regexp match failed for %s", testpoint.tag)
			bad = true
		}
	}
	if bad {
		dumplines(lines)
	}
}

func testPkgList(t *testing.T, s state) {
	dargs := []string{"-i=" + s.outdirs[0] + "," + s.outdirs[1]}
	lines := runToolOp(t, s, "pkglist", dargs)

	want := []string{mainPkgPath, mainPkgPath + "/dep"}
	bad := false
	if len(lines) != 2 {
		t.Errorf("expect pkglist to return two lines")
		bad = true
	} else {
		for i := 0; i < 2; i++ {
			lines[i] = strings.TrimSpace(lines[i])
			if want[i] != lines[i] {
				t.Errorf("line %d want %s got %s", i, want[i], lines[i])
				bad = true
			}
		}
	}
	if bad {
		dumplines(lines)
	}
}

func testTextfmt(t *testing.T, s state) {
	outf := s.dir + "/" + "t.txt"
	dargs := []string{"-pkg=" + mainPkgPath, "-i=" + s.outdirs[0] + "," + s.outdirs[1],
		"-o", outf}
	lines := runToolOp(t, s, "textfmt", dargs)

	// No output expected.
	if len(lines) != 0 {
		dumplines(lines)
		t.Errorf("unexpected output from go tool covdata textfmt")
	}

	// Open and read the first few bits of the file.
	payload, err := os.ReadFile(outf)
	if err != nil {
		t.Errorf("opening %s: %v\n", outf, err)
	}
	lines = strings.Split(string(payload), "\n")
	want0 := "mode: set"
	if lines[0] != want0 {
		dumplines(lines[0:10])
		t.Errorf("textfmt: want %s got %s", want0, lines[0])
	}
	want1 := mainPkgPath + "/prog1.go:13.14,15.2 1 1"
	if lines[1] != want1 {
		dumplines(lines[0:10])
		t.Errorf("textfmt: want %s got %s", want1, lines[1])
	}
}

func dumplines(lines []string) {
	for i := range lines {
		fmt.Fprintf(os.Stderr, "%s\n", lines[i])
	}
}

type dumpCheck struct {
	tag     string
	re      *regexp.Regexp
	negate  bool
	nonzero bool
	zero    bool
}

// runDumpChecks examines the output of "go tool covdata debugdump"
// for a given output directory, looking for the presence or absence
// of specific markers.
func runDumpChecks(t *testing.T, s state, dir string, flags []string, checks []dumpCheck) {
	dargs := []string{"-i", dir}
	dargs = append(dargs, flags...)
	lines := runToolOp(t, s, "debugdump", dargs)
	if len(lines) == 0 {
		t.Fatalf("dump run produced no output")
	}

	bad := false
	for _, check := range checks {
		found := false
		for _, line := range lines {
			if m := check.re.FindStringSubmatch(line); m != nil {
				found = true
				if check.negate {
					t.Errorf("tag %q: unexpected match", check.tag)
					bad = true

				}
				if check.nonzero || check.zero {
					if len(m) < 2 {
						t.Errorf("tag %s: submatch failed (short m)", check.tag)
						bad = true
						continue
					}
					if m[1] == "" {
						t.Errorf("tag %s: submatch failed", check.tag)
						bad = true
						continue
					}
					i, err := strconv.Atoi(m[1])
					if err != nil {
						t.Errorf("tag %s: match Atoi failed on %s",
							check.tag, m[1])
						continue
					}
					if check.zero && i != 0 {
						t.Errorf("tag %s: match zero failed on %s",
							check.tag, m[1])
					} else if check.nonzero && i == 0 {
						t.Errorf("tag %s: match nonzero failed on %s",
							check.tag, m[1])
					}
				}
				break
			}
		}
		if !found && !check.negate {
			t.Errorf("dump output regexp match failed for %s", check.tag)
			bad = true
		}
	}
	if bad {
		fmt.Printf("output from 'dump' run:\n")
		dumplines(lines)
	}
}

func testMergeSimple(t *testing.T, s state, indir1, indir2, tag string) {
	outdir := filepath.Join(s.dir, "simpleMergeOut"+tag)
	if err := os.Mkdir(outdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", outdir, err)
	}

	// Merge the two dirs into a final result.
	ins := fmt.Sprintf("-i=%s,%s", indir1, indir2)
	out := fmt.Sprintf("-o=%s", outdir)
	margs := []string{ins, out}
	lines := runToolOp(t, s, "merge", margs)
	if len(lines) != 0 {
		t.Errorf("merge run produced %d lines of unexpected output", len(lines))
		dumplines(lines)
	}

	// We expect the merge tool to produce exactly two files: a meta
	// data file and a counter file. If we get more than just this one
	// pair, something went wrong.
	podlist, err := pods.CollectPods([]string{outdir}, true)
	if err != nil {
		t.Fatal(err)
	}
	if len(podlist) != 1 {
		t.Fatalf("expected 1 pod, got %d pods", len(podlist))
	}
	ncdfs := len(podlist[0].CounterDataFiles)
	if ncdfs != 1 {
		t.Fatalf("expected 1 counter data file, got %d", ncdfs)
	}

	// Sift through the output to make sure it has some key elements.
	// In particular, we want to see entries for all three functions
	// ("first", "second", and "third").
	testpoints := []dumpCheck{
		{
			tag: "first function",
			re:  regexp.MustCompile(`^Func: first\s*$`),
		},
		{
			tag: "second function",
			re:  regexp.MustCompile(`^Func: second\s*$`),
		},
		{
			tag: "third function",
			re:  regexp.MustCompile(`^Func: third\s*$`),
		},
		{
			tag:     "third function unit 0",
			re:      regexp.MustCompile(`^0: L23:C23 -- L24:C12 NS=1 = (\d+)$`),
			nonzero: true,
		},
		{
			tag:     "third function unit 1",
			re:      regexp.MustCompile(`^1: L27:C2 -- L28:C10 NS=2 = (\d+)$`),
			nonzero: true,
		},
		{
			tag:     "third function unit 2",
			re:      regexp.MustCompile(`^2: L24:C12 -- L26:C3 NS=1 = (\d+)$`),
			nonzero: true,
		},
	}
	flags := []string{"-live", "-pkg=" + mainPkgPath}
	runDumpChecks(t, s, outdir, flags, testpoints)
}

func testMergeSelect(t *testing.T, s state, indir1, indir2 string, tag string) {
	outdir := filepath.Join(s.dir, "selectMergeOut"+tag)
	if err := os.Mkdir(outdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", outdir, err)
	}

	// Merge two input dirs into a final result, but filter
	// based on package.
	ins := fmt.Sprintf("-i=%s,%s", indir1, indir2)
	out := fmt.Sprintf("-o=%s", outdir)
	margs := []string{"-pkg=" + mainPkgPath + "/dep", ins, out}
	lines := runToolOp(t, s, "merge", margs)
	if len(lines) != 0 {
		t.Errorf("merge run produced %d lines of unexpected output", len(lines))
		dumplines(lines)
	}

	// Dump the files in the merged output dir and examine the result.
	// We expect to see only the functions in package "dep".
	dargs := []string{"-i=" + outdir}
	lines = runToolOp(t, s, "debugdump", dargs)
	if len(lines) == 0 {
		t.Fatalf("dump run produced no output")
	}
	want := map[string]int{
		"Package path: " + mainPkgPath + "/dep": 0,
		"Func: Dep1":                            0,
		"Func: PDep":                            0,
	}
	bad := false
	for _, line := range lines {
		if v, ok := want[line]; ok {
			if v != 0 {
				t.Errorf("duplicate line %s", line)
				bad = true
				break
			}
			want[line] = 1
			continue
		}
		// no other functions or packages expected.
		if strings.HasPrefix(line, "Func:") || strings.HasPrefix(line, "Package path:") {
			t.Errorf("unexpected line: %s", line)
			bad = true
			break
		}
	}
	if bad {
		dumplines(lines)
	}
}

func testMergeCombinePrograms(t *testing.T, s state) {

	// Run the new program, emitting output into a new set
	// of outdirs.
	runout := [2]string{}
	for k := 0; k < 2; k++ {
		runout[k] = filepath.Join(s.dir, fmt.Sprintf("newcovdata%d", k))
		if err := os.Mkdir(runout[k], 0777); err != nil {
			t.Fatalf("can't create outdir %s: %v", runout[k], err)
		}
		args := []string{}
		if k != 0 {
			args = append(args, "foo", "bar")
		}
		cmd := testenv.Command(t, s.exepath2, args...)
		cmd.Env = append(cmd.Env, "GOCOVERDIR="+runout[k])
		b, err := cmd.CombinedOutput()
		if len(b) != 0 {
			t.Logf("## instrumented run output:\n%s", b)
		}
		if err != nil {
			t.Fatalf("instrumented run error: %v", err)
		}
	}

	// Create out dir for -pcombine merge.
	moutdir := filepath.Join(s.dir, "mergeCombineOut")
	if err := os.Mkdir(moutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", moutdir, err)
	}

	// Run a merge over both programs, using the -pcombine
	// flag to do maximal combining.
	ins := fmt.Sprintf("-i=%s,%s,%s,%s", s.outdirs[0], s.outdirs[1],
		runout[0], runout[1])
	out := fmt.Sprintf("-o=%s", moutdir)
	margs := []string{"-pcombine", ins, out}
	lines := runToolOp(t, s, "merge", margs)
	if len(lines) != 0 {
		t.Errorf("merge run produced unexpected output: %v", lines)
	}

	// We expect the merge tool to produce exacty two files: a meta
	// data file and a counter file. If we get more than just this one
	// pair, something went wrong.
	podlist, err := pods.CollectPods([]string{moutdir}, true)
	if err != nil {
		t.Fatal(err)
	}
	if len(podlist) != 1 {
		t.Fatalf("expected 1 pod, got %d pods", len(podlist))
	}
	ncdfs := len(podlist[0].CounterDataFiles)
	if ncdfs != 1 {
		t.Fatalf("expected 1 counter data file, got %d", ncdfs)
	}

	// Sift through the output to make sure it has some key elements.
	testpoints := []dumpCheck{
		{
			tag: "first function",
			re:  regexp.MustCompile(`^Func: first\s*$`),
		},
		{
			tag: "sixth function",
			re:  regexp.MustCompile(`^Func: sixth\s*$`),
		},
	}

	flags := []string{"-live", "-pkg=" + mainPkgPath}
	runDumpChecks(t, s, moutdir, flags, testpoints)
}

func testSubtract(t *testing.T, s state) {
	// Create out dir for subtract merge.
	soutdir := filepath.Join(s.dir, "subtractOut")
	if err := os.Mkdir(soutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", soutdir, err)
	}

	// Subtract the two dirs into a final result.
	ins := fmt.Sprintf("-i=%s,%s", s.outdirs[0], s.outdirs[1])
	out := fmt.Sprintf("-o=%s", soutdir)
	sargs := []string{ins, out}
	lines := runToolOp(t, s, "subtract", sargs)
	if len(lines) != 0 {
		t.Errorf("subtract run produced unexpected output: %+v", lines)
	}

	// Dump the files in the subtract output dir and examine the result.
	dargs := []string{"-pkg=" + mainPkgPath, "-live", "-i=" + soutdir}
	lines = runToolOp(t, s, "debugdump", dargs)
	if len(lines) == 0 {
		t.Errorf("dump run produced no output")
	}

	// Vet the output.
	testpoints := []dumpCheck{
		{
			tag: "first function",
			re:  regexp.MustCompile(`^Func: first\s*$`),
		},
		{
			tag: "dep function",
			re:  regexp.MustCompile(`^Func: Dep1\s*$`),
		},
		{
			tag: "third function",
			re:  regexp.MustCompile(`^Func: third\s*$`),
		},
		{
			tag:  "third function unit 0",
			re:   regexp.MustCompile(`^0: L23:C23 -- L24:C12 NS=1 = (\d+)$`),
			zero: true,
		},
		{
			tag:     "third function unit 1",
			re:      regexp.MustCompile(`^1: L27:C2 -- L28:C10 NS=2 = (\d+)$`),
			nonzero: true,
		},
		{
			tag:  "third function unit 2",
			re:   regexp.MustCompile(`^2: L24:C12 -- L26:C3 NS=1 = (\d+)$`),
			zero: true,
		},
	}
	flags := []string{}
	runDumpChecks(t, s, soutdir, flags, testpoints)
}

func testIntersect(t *testing.T, s state, indir1, indir2, tag string) {
	// Create out dir for intersection.
	ioutdir := filepath.Join(s.dir, "intersectOut"+tag)
	if err := os.Mkdir(ioutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", ioutdir, err)
	}

	// Intersect the two dirs into a final result.
	ins := fmt.Sprintf("-i=%s,%s", indir1, indir2)
	out := fmt.Sprintf("-o=%s", ioutdir)
	sargs := []string{ins, out}
	lines := runToolOp(t, s, "intersect", sargs)
	if len(lines) != 0 {
		t.Errorf("intersect run produced unexpected output: %+v", lines)
	}

	// Dump the files in the subtract output dir and examine the result.
	dargs := []string{"-pkg=" + mainPkgPath, "-live", "-i=" + ioutdir}
	lines = runToolOp(t, s, "debugdump", dargs)
	if len(lines) == 0 {
		t.Errorf("dump run produced no output")
	}

	// Vet the output.
	testpoints := []dumpCheck{
		{
			tag:    "first function",
			re:     regexp.MustCompile(`^Func: first\s*$`),
			negate: true,
		},
		{
			tag: "third function",
			re:  regexp.MustCompile(`^Func: third\s*$`),
		},
	}
	flags := []string{"-live"}
	runDumpChecks(t, s, ioutdir, flags, testpoints)
}

func testCounterClash(t *testing.T, s state) {
	// Create out dir.
	ccoutdir := filepath.Join(s.dir, "ccOut")
	if err := os.Mkdir(ccoutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", ccoutdir, err)
	}

	// Try to merge covdata0 (from prog1.go -countermode=set) with
	// covdata1 (from prog1.go -countermode=atomic"). This should
	// produce a counter mode clash error.
	ins := fmt.Sprintf("-i=%s,%s", s.outdirs[0], s.outdirs[3])
	out := fmt.Sprintf("-o=%s", ccoutdir)
	args := append([]string{}, "merge", ins, out, "-pcombine")
	if debugtrace {
		t.Logf("cc merge command is %s %v\n", s.tool, args)
	}
	cmd := testenv.Command(t, s.tool, args...)
	b, err := cmd.CombinedOutput()
	t.Logf("%% output: %s\n", string(b))
	if err == nil {
		t.Fatalf("clash merge passed unexpectedly")
	}
	got := string(b)
	want := "counter mode clash while reading meta-data"
	if !strings.Contains(got, want) {
		t.Errorf("counter clash merge: wanted %s got %s", want, got)
	}
}

func testEmpty(t *testing.T, s state) {

	// Create a new empty directory.
	empty := filepath.Join(s.dir, "empty")
	if err := os.Mkdir(empty, 0777); err != nil {
		t.Fatalf("can't create dir %s: %v", empty, err)
	}

	// Create out dir.
	eoutdir := filepath.Join(s.dir, "emptyOut")
	if err := os.Mkdir(eoutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", eoutdir, err)
	}

	// Run various operations (merge, dump, textfmt, and so on)
	// using the empty directory. We're not interested in the output
	// here, just making sure that you can do these runs without
	// any error or crash.

	scenarios := []struct {
		tag  string
		args []string
	}{
		{
			tag:  "merge",
			args: []string{"merge", "-o", eoutdir},
		},
		{
			tag:  "textfmt",
			args: []string{"textfmt", "-o", filepath.Join(eoutdir, "foo.txt")},
		},
		{
			tag:  "func",
			args: []string{"func"},
		},
		{
			tag:  "pkglist",
			args: []string{"pkglist"},
		},
		{
			tag:  "debugdump",
			args: []string{"debugdump"},
		},
		{
			tag:  "percent",
			args: []string{"percent"},
		},
	}

	for _, x := range scenarios {
		ins := fmt.Sprintf("-i=%s", empty)
		args := append([]string{}, x.args...)
		args = append(args, ins)
		if false {
			t.Logf("cmd is %s %v\n", s.tool, args)
		}
		cmd := testenv.Command(t, s.tool, args...)
		b, err := cmd.CombinedOutput()
		t.Logf("%% output: %s\n", string(b))
		if err != nil {
			t.Fatalf("command %s %+v failed with %v",
				s.tool, x.args, err)
		}
	}
}

func testCommandLineErrors(t *testing.T, s state, outdir string) {

	// Create out dir.
	eoutdir := filepath.Join(s.dir, "errorsOut")
	if err := os.Mkdir(eoutdir, 0777); err != nil {
		t.Fatalf("can't create outdir %s: %v", eoutdir, err)
	}

	// Run various operations (merge, dump, textfmt, and so on)
	// using the empty directory. We're not interested in the output
	// here, just making sure that you can do these runs without
	// any error or crash.

	scenarios := []struct {
		tag  string
		args []string
		exp  string
	}{
		{
			tag:  "input missing",
			args: []string{"merge", "-o", eoutdir, "-i", "not there"},
			exp:  "error: reading inputs: ",
		},
		{
			tag:  "badv",
			args: []string{"textfmt", "-i", outdir, "-v=abc"},
		},
	}

	for _, x := range scenarios {
		args := append([]string{}, x.args...)
		if false {
			t.Logf("cmd is %s %v\n", s.tool, args)
		}
		cmd := testenv.Command(t, s.tool, args...)
		b, err := cmd.CombinedOutput()
		if err == nil {
			t.Logf("%% output: %s\n", string(b))
			t.Fatalf("command %s %+v unexpectedly succeeded",
				s.tool, x.args)
		} else {
			if !strings.Contains(string(b), x.exp) {
				t.Fatalf("command %s %+v:\ngot:\n%s\nwanted to see: %v\n",
					s.tool, x.args, string(b), x.exp)
			}
		}
	}
}
