// 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: // // /covdata0 -- prog1.go compiled -cover // /covdata1 -- prog1.go compiled -cover // /covdata2 -- prog1.go compiled -covermode=atomic // /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) } } } }