cmd/govulncheck: test the command directly

Add a test that builds govulncheck and compares its output to a golden.

This uses the github.com/google/go-cmdtest package, which makes it easy
to do that.

Move the buildtest package out of vulncheck/internal so cmd/govulncheck
can use it, and fix a bug when GoBuild is passed ".".

Change-Id: I2e216f1980cfe0548e7231531bd68b2996142917
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/399115
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 1a9a07a..b24c34e 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -65,7 +65,7 @@
 or ./...) or with a single path to a Go binary. In the latter case module and symbol
 information will be extracted from the binary in order to detect vulnerable symbols.
 
-The environment variable GOVULNDB can be set to a comma-separate list of vulnerability
+The environment variable GOVULNDB can be set to a comma-separated list of vulnerability
 database URLs, with http://, https://, or file:// protocols. Entries from multiple
 databases are merged.
 `
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index ba70211..f839d1d 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -8,13 +8,30 @@
 package main
 
 import (
+	"flag"
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmdtest"
+	"golang.org/x/vuln/internal/buildtest"
 	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
+var update = flag.Bool("update", false, "update test files with results")
+
+func TestCommand(t *testing.T) {
+	binary, cleanup := buildtest.GoBuild(t, ".")
+	defer cleanup()
+
+	ts, err := cmdtest.Read("testdata")
+	if err != nil {
+		t.Fatal(err)
+	}
+	ts.Commands["govulncheck"] = cmdtest.Program(binary)
+	ts.Run(t, *update)
+}
+
 func TestLatestFixed(t *testing.T) {
 	for _, test := range []struct {
 		name string
diff --git a/cmd/govulncheck/testdata/usage.ct b/cmd/govulncheck/testdata/usage.ct
new file mode 100644
index 0000000..b22e7b8
--- /dev/null
+++ b/cmd/govulncheck/testdata/usage.ct
@@ -0,0 +1,55 @@
+$ govulncheck -h
+govulncheck: identify known vulnerabilities by call graph traversal.
+
+Usage:
+
+	govulncheck [flags] {package pattern...}
+
+	govulncheck [flags] {binary path}
+
+Flags:
+
+	-json	Print vulnerability findings in JSON format.
+
+	-html	Generate HTML with the vulnerability findings.
+
+	-tags	Comma-separated list of build tags.
+
+	-tests	Boolean flag indicating if test files should be analyzed too.
+
+govulncheck can be used with either one or more package patterns (i.e. golang.org/x/crypto/...
+or ./...) or with a single path to a Go binary. In the latter case module and symbol
+information will be extracted from the binary in order to detect vulnerable symbols.
+
+The environment variable GOVULNDB can be set to a comma-separated list of vulnerability
+database URLs, with http://, https://, or file:// protocols. Entries from multiple
+databases are merged.
+
+
+# Same output as -h, but failure.
+$ govulncheck --> FAIL
+govulncheck: identify known vulnerabilities by call graph traversal.
+
+Usage:
+
+	govulncheck [flags] {package pattern...}
+
+	govulncheck [flags] {binary path}
+
+Flags:
+
+	-json	Print vulnerability findings in JSON format.
+
+	-html	Generate HTML with the vulnerability findings.
+
+	-tags	Comma-separated list of build tags.
+
+	-tests	Boolean flag indicating if test files should be analyzed too.
+
+govulncheck can be used with either one or more package patterns (i.e. golang.org/x/crypto/...
+or ./...) or with a single path to a Go binary. In the latter case module and symbol
+information will be extracted from the binary in order to detect vulnerable symbols.
+
+The environment variable GOVULNDB can be set to a comma-separated list of vulnerability
+database URLs, with http://, https://, or file:// protocols. Entries from multiple
+databases are merged.
diff --git a/go.mod b/go.mod
index d03cb52..e9610e4 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@
 
 require (
 	github.com/client9/misspell v0.3.4
+	github.com/google/go-cmdtest v0.3.0
 	github.com/google/go-cmp v0.5.6
 	golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
@@ -14,6 +15,7 @@
 
 require (
 	github.com/BurntSushi/toml v0.3.1 // indirect
+	github.com/google/renameio v0.1.0 // indirect
 	golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 )
diff --git a/go.sum b/go.sum
index d6c958d..301a878 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,13 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/google/go-cmdtest v0.3.0 h1:382oNMtKBpvJjOm5c5ONU3pzwh2ZK/eNA4/h2v9PnXM=
+github.com/google/go-cmdtest v0.3.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
diff --git a/vulncheck/internal/buildtest/buildtest.go b/internal/buildtest/buildtest.go
similarity index 88%
rename from vulncheck/internal/buildtest/buildtest.go
rename to internal/buildtest/buildtest.go
index e57b784..c8b1417 100644
--- a/vulncheck/internal/buildtest/buildtest.go
+++ b/internal/buildtest/buildtest.go
@@ -18,6 +18,7 @@
 
 var unsupportedGoosGoarch = map[string]bool{
 	"darwin/386": true,
+	"darwin/arm": true,
 }
 
 // GoBuild runs "go build" on dir using the additional environment variables in
@@ -50,7 +51,11 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	binaryPath = filepath.Join(tmpDir, filepath.Base(dir))
+	abs, err := filepath.Abs(dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	binaryPath = filepath.Join(tmpDir, filepath.Base(abs))
 	var exeSuffix string
 	if runtime.GOOS == "windows" {
 		exeSuffix = ".exe"
@@ -60,7 +65,7 @@
 	if _, err := os.Stat(goCommandPath); err != nil {
 		t.Fatal(err)
 	}
-	cmd := exec.Command(goCommandPath, "build", "-o", binaryPath)
+	cmd := exec.Command(goCommandPath, "build", "-o", binaryPath+exeSuffix)
 	cmd.Dir = dir
 	cmd.Env = env
 	cmd.Stdout = os.Stdout
@@ -68,7 +73,7 @@
 	if err := cmd.Run(); err != nil {
 		t.Fatal(err)
 	}
-	return binaryPath, func() { os.RemoveAll(tmpDir) }
+	return binaryPath + exeSuffix, func() { os.RemoveAll(tmpDir) }
 }
 
 // lookEnv looks for name in env, a list of "VAR=VALUE" strings. It returns
diff --git a/vulncheck/internal/binscan/scan_test.go b/vulncheck/internal/binscan/scan_test.go
index 9c9cb83..f4811e7 100644
--- a/vulncheck/internal/binscan/scan_test.go
+++ b/vulncheck/internal/binscan/scan_test.go
@@ -12,7 +12,7 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"golang.org/x/vuln/vulncheck/internal/buildtest"
+	"golang.org/x/vuln/internal/buildtest"
 )
 
 func TestExtractPackagesAndSymbols(t *testing.T) {
diff --git a/vulncheck/internal/gosym/pclntab_test.go b/vulncheck/internal/gosym/pclntab_test.go
index 7a412dd..95589ee 100644
--- a/vulncheck/internal/gosym/pclntab_test.go
+++ b/vulncheck/internal/gosym/pclntab_test.go
@@ -16,7 +16,7 @@
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
-	"golang.org/x/vuln/vulncheck/internal/buildtest"
+	"golang.org/x/vuln/internal/buildtest"
 )
 
 func dotest(t *testing.T) (binaryName string, cleanup func()) {