cmd/govulncheck: add a -dir flag for testing

When the "testmode" tag is active, add a flag that lets govulncheck
read source from any directory.

Update command tests to use the feature: remove the cdmodule
command and replace it with an environment variable.

This change will enable running the command tests
in parallel.

Change-Id: I6c166ee907520cebb10887835d58e9672b84d459
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/432355
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 8bf044d..bd97ff9 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -13,6 +13,7 @@
 	"go/build"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"sort"
 	"strings"
 
@@ -28,6 +29,7 @@
 	jsonFlag    = flag.Bool("json", false, "output JSON")
 	verboseFlag = flag.Bool("v", false, "print a full call stack for each vulnerability")
 	testFlag    = flag.Bool("test", false, "analyze test files. Only valid for source code.")
+	dirFlag     string
 )
 
 func init() {
@@ -97,15 +99,16 @@
 		}
 	} else {
 		cfg := &packages.Config{
+			Dir:        filepath.FromSlash(dirFlag),
 			Tests:      *testFlag,
 			BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(build.Default.BuildTags, ","))},
 		}
 		pkgs, err = govulncheck.LoadPackages(cfg, patterns...)
 		if err != nil {
 			// Try to provide a meaningful and actionable error message.
-			if !fileExists("go.mod") {
+			if !fileExists(filepath.Join(dirFlag, "go.mod")) {
 				die(noGoModErrorMessage)
-			} else if !fileExists("go.sum") {
+			} else if !fileExists(filepath.Join(dirFlag, "go.sum")) {
 				die(noGoSumErrorMessage)
 			}
 			die("govulncheck: %v", err)
diff --git a/cmd/govulncheck/main_command_118_test.go b/cmd/govulncheck/main_command_118_test.go
index 5fe6330..2437f8b 100644
--- a/cmd/govulncheck/main_command_118_test.go
+++ b/cmd/govulncheck/main_command_118_test.go
@@ -36,20 +36,15 @@
 		t.Fatal(err)
 	}
 	ts.DisableLogging = false
-	// Define a command that lets us cd into a module directory.
-	// The modules for these tests live under testdata/modules.
-	ts.Commands["cdmodule"] = func(args []string, inputFile string) ([]byte, error) {
-		if len(args) != 1 {
-			return nil, errors.New("need exactly 1 argument")
-		}
-		return nil, os.Chdir(filepath.Join(testDir, "testdata", "modules", args[0]))
-	}
 	// Define a command that runs govulncheck with our local DB. We can't use
 	// cmdtest.Program for this because it doesn't let us set the environment,
 	// and that is the only way to tell govulncheck about an alternative vuln
 	// database.
-	binary, cleanup := buildtest.GoBuild(t, ".") // build govulncheck
-	defer cleanup()
+	binary, cleanup := buildtest.GoBuild(t, ".", "testmode") // build govulncheck
+	// Use Cleanup instead of defer, because when subtests are parallel, defer
+	// runs too early.
+	t.Cleanup(cleanup)
+
 	ts.Commands["govulncheck"] = func(args []string, inputFile string) ([]byte, error) {
 		cmd := exec.Command(binary, args...)
 		if inputFile != "" {
@@ -78,13 +73,14 @@
 		"nogosum": true,
 	}
 
+	os.Setenv("moddir", filepath.Join(testDir, "testdata", "modules"))
 	for _, md := range moduleDirs {
 		if skipBuild[filepath.Base(md)] {
 			continue
 		}
 
-		binary, cleanup := buildtest.GoBuild(t, md)
-		defer cleanup()
+		binary, cleanup := buildtest.GoBuild(t, md, "")
+		t.Cleanup(cleanup)
 		// Set an environment variable to the path to the binary, so tests
 		// can refer to it.
 		varName := filepath.Base(md) + "_binary"
diff --git a/cmd/govulncheck/main_testmode.go b/cmd/govulncheck/main_testmode.go
new file mode 100644
index 0000000..0ac803b
--- /dev/null
+++ b/cmd/govulncheck/main_testmode.go
@@ -0,0 +1,13 @@
+// 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.
+
+//go:build testmode
+
+package main
+
+import "flag"
+
+func init() {
+	flag.StringVar(&dirFlag, "dir", "", "directory to use for loading source files")
+}
diff --git a/cmd/govulncheck/testdata/default.ct b/cmd/govulncheck/testdata/default.ct
index 327e906..e86d477 100644
--- a/cmd/govulncheck/testdata/default.ct
+++ b/cmd/govulncheck/testdata/default.ct
@@ -1,15 +1,13 @@
 # Test of default mode.
 
 # No vulnerabilities, no output.
-$ cdmodule novuln
-$ govulncheck .
+$ govulncheck -dir ${moddir}/novuln .
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
 No vulnerabilities found.
 
-$ cdmodule vuln
-$ govulncheck . --> FAIL 3
+$ govulncheck -dir ${moddir}/vuln . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/import-no-call.ct b/cmd/govulncheck/testdata/import-no-call.ct
index 0fbe8c2..69d414b 100644
--- a/cmd/govulncheck/testdata/import-no-call.ct
+++ b/cmd/govulncheck/testdata/import-no-call.ct
@@ -1,8 +1,7 @@
 # Test of default mode.
 
 # All vulnerabilities imported, but never called.
-$ cdmodule vuln3
-$ govulncheck .
+$ govulncheck -dir ${moddir}/vuln3 .
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/json.ct b/cmd/govulncheck/testdata/json.ct
index 9e75f84..e7cc2b3 100644
--- a/cmd/govulncheck/testdata/json.ct
+++ b/cmd/govulncheck/testdata/json.ct
@@ -2,8 +2,7 @@
 # TODO(zpavlinovic): add test for stdlib that works
 # on all underlying Go build systems.
 
-$ cdmodule novuln
-$ govulncheck -json .
+$ govulncheck -dir ${moddir}/novuln -json .
 {
 	"Calls": {
 		"Functions": {},
@@ -40,8 +39,7 @@
 	]
 }
 
-$ cdmodule vuln
-$ govulncheck -json .
+$ govulncheck -dir ${moddir}/vuln -json .
 {
 	"Calls": {
 		"Functions": {
diff --git a/cmd/govulncheck/testdata/manystacks-verbose.ct b/cmd/govulncheck/testdata/manystacks-verbose.ct
index ae6980a..6de910c 100644
--- a/cmd/govulncheck/testdata/manystacks-verbose.ct
+++ b/cmd/govulncheck/testdata/manystacks-verbose.ct
@@ -1,5 +1,4 @@
-$ cdmodule manystacks
-$ govulncheck -v . --> FAIL 3
+$ govulncheck -dir ${moddir}/manystacks -v . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/manystacks.ct b/cmd/govulncheck/testdata/manystacks.ct
index 00e0354..cfadefa 100644
--- a/cmd/govulncheck/testdata/manystacks.ct
+++ b/cmd/govulncheck/testdata/manystacks.ct
@@ -1,5 +1,4 @@
-$ cdmodule manystacks
-$ govulncheck . --> FAIL 3
+$ govulncheck -dir ${moddir}/manystacks . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/multi-module.ct b/cmd/govulncheck/testdata/multi-module.ct
index c191c53..98d9f88 100644
--- a/cmd/govulncheck/testdata/multi-module.ct
+++ b/cmd/govulncheck/testdata/multi-module.ct
@@ -1,7 +1,6 @@
 # Test fix correctness for vulns affecting multiple modules
 
-$ cdmodule multimodvuln
-$ govulncheck .
+$ govulncheck -dir ${moddir}/multimodvuln .
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/nogomod.ct b/cmd/govulncheck/testdata/nogomod.ct
index a6b5ed3..4390707 100644
--- a/cmd/govulncheck/testdata/nogomod.ct
+++ b/cmd/govulncheck/testdata/nogomod.ct
@@ -1,7 +1,6 @@
 # Test of missing go.mod error message.
 
-$ cdmodule nogomod
-$ govulncheck . --> FAIL 1
+$ govulncheck -dir ${moddir}/nogomod . --> FAIL 1
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/nogosum.ct b/cmd/govulncheck/testdata/nogosum.ct
index f68ab95..eaeb61e 100644
--- a/cmd/govulncheck/testdata/nogosum.ct
+++ b/cmd/govulncheck/testdata/nogosum.ct
@@ -1,7 +1,6 @@
 # Test of missing go.sum error message.
 
-$ cdmodule nogosum
-$ govulncheck . --> FAIL 1
+$ govulncheck -dir ${moddir}/nogosum . --> FAIL 1
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/stdlib.ct b/cmd/govulncheck/testdata/stdlib.ct
index 67f4b3c..090a713 100644
--- a/cmd/govulncheck/testdata/stdlib.ct
+++ b/cmd/govulncheck/testdata/stdlib.ct
@@ -1,7 +1,6 @@
 # Test of stdlib vuln detection.
 
-$ cdmodule stdvuln
-$ govulncheck . --> FAIL 3
+$ govulncheck -dir ${moddir}/stdvuln . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/two-symbols.ct b/cmd/govulncheck/testdata/two-symbols.ct
index ecefbb7..8e0e087 100644
--- a/cmd/govulncheck/testdata/two-symbols.ct
+++ b/cmd/govulncheck/testdata/two-symbols.ct
@@ -1,5 +1,4 @@
-$ cdmodule vuln2
-$ govulncheck . --> FAIL 3
+$ govulncheck -dir ${moddir}/vuln2 . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/cmd/govulncheck/testdata/usage.ct b/cmd/govulncheck/testdata/usage.ct
index 4d2d761..72afbcc 100644
--- a/cmd/govulncheck/testdata/usage.ct
+++ b/cmd/govulncheck/testdata/usage.ct
@@ -3,6 +3,8 @@
 	govulncheck [flags] package...
 	govulncheck [flags] binary
 
+  -dir string
+    	directory to use for loading source files
   -json
     	output JSON
   -tags list
@@ -20,6 +22,8 @@
 	govulncheck [flags] package...
 	govulncheck [flags] binary
 
+  -dir string
+    	directory to use for loading source files
   -json
     	output JSON
   -tags list
diff --git a/cmd/govulncheck/testdata/verbose.ct b/cmd/govulncheck/testdata/verbose.ct
index df96302..dcab77f 100644
--- a/cmd/govulncheck/testdata/verbose.ct
+++ b/cmd/govulncheck/testdata/verbose.ct
@@ -1,15 +1,13 @@
 # Test of verbose mode.
 
 # No vulnerabilities, no output.
-$ cdmodule novuln
-$ govulncheck -v .
+$ govulncheck -dir ${moddir}/novuln -v .
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
 No vulnerabilities found.
 
-$ cdmodule vuln
-$ govulncheck -v . --> FAIL 3
+$ govulncheck -dir ${moddir}/vuln -v . --> FAIL 3
 govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
 
 Scanning for dependencies with known vulnerabilities...
diff --git a/internal/buildtest/buildtest.go b/internal/buildtest/buildtest.go
index c8b1417..559ca08 100644
--- a/internal/buildtest/buildtest.go
+++ b/internal/buildtest/buildtest.go
@@ -25,7 +25,7 @@
 // envVarVals, which should be an alternating list of variables and values.
 // It returns the path to the resulting binary, and a function
 // to call when finished with the binary.
-func GoBuild(t *testing.T, dir string, envVarVals ...string) (binaryPath string, cleanup func()) {
+func GoBuild(t *testing.T, dir, tags string, envVarVals ...string) (binaryPath string, cleanup func()) {
 	switch runtime.GOOS {
 	case "android", "js", "ios":
 		t.Skipf("skipping on OS without 'go build' %s", runtime.GOOS)
@@ -65,7 +65,11 @@
 	if _, err := os.Stat(goCommandPath); err != nil {
 		t.Fatal(err)
 	}
-	cmd := exec.Command(goCommandPath, "build", "-o", binaryPath+exeSuffix)
+	args := []string{"build", "-o", binaryPath + exeSuffix}
+	if tags != "" {
+		args = append(args, "-tags", tags)
+	}
+	cmd := exec.Command(goCommandPath, args...)
 	cmd.Dir = dir
 	cmd.Env = env
 	cmd.Stdout = os.Stdout
diff --git a/vulncheck/internal/binscan/scan_test.go b/vulncheck/internal/binscan/scan_test.go
index 673598a..4fbabe9 100644
--- a/vulncheck/internal/binscan/scan_test.go
+++ b/vulncheck/internal/binscan/scan_test.go
@@ -20,7 +20,7 @@
 	for _, gg := range []string{"linux/amd64", "darwin/amd64", "windows/amd64"} {
 		t.Run(gg, func(t *testing.T) {
 			goos, goarch, _ := strings.Cut(gg, "/")
-			binary, done := buildtest.GoBuild(t, "testdata", "GOOS", goos, "GOARCH", goarch)
+			binary, done := buildtest.GoBuild(t, "testdata", "", "GOOS", goos, "GOARCH", goarch)
 			defer done()
 
 			f, err := os.Open(binary)
diff --git a/vulncheck/internal/gosym/pclntab_test.go b/vulncheck/internal/gosym/pclntab_test.go
index b1e433f..c116d71 100644
--- a/vulncheck/internal/gosym/pclntab_test.go
+++ b/vulncheck/internal/gosym/pclntab_test.go
@@ -31,7 +31,7 @@
 		t.Skipf("skipping in short mode on non-Linux system %s", runtime.GOARCH)
 	}
 
-	return buildtest.GoBuild(t, "testdata", "GOOS", "linux")
+	return buildtest.GoBuild(t, "testdata", "", "GOOS", "linux")
 }
 
 // skipIfNotELF skips the test if we are not running on an ELF system.