exp/govulncheck: DefaultCache now returns an error

DefaultCache can now return an error.
Disables TestGoEnv on js/wasm which does not support "go env".
Additionally calls GoEnv lazier. GoEnv now returns an error.

Fixes golang/go#57628

Change-Id: Iced82b30de6e53556fde410c25863edd69d8206e
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/460421
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Tim King <taking@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index d62086e..d14ee1a 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -86,8 +86,14 @@
 	if db := os.Getenv(envGOVULNDB); db != "" {
 		dbs = strings.Split(db, ",")
 	}
+
+	cache, err := govulncheck.DefaultCache()
+	if err != nil {
+		return err
+	}
+
 	dbClient, err := client.NewClient(dbs, client.Options{
-		HTTPCache: govulncheck.DefaultCache(),
+		HTTPCache: cache,
 	})
 	if err != nil {
 		return err
diff --git a/internal/goenv.go b/internal/goenv.go
index b3bb86e..05bcade 100644
--- a/internal/goenv.go
+++ b/internal/goenv.go
@@ -12,17 +12,15 @@
 	"os/exec"
 )
 
-// GoEnv returns the value for key in `go env`. In
-// the unlikely case that `go env` fails, prints an
-// error message and returns an empty string.
-func GoEnv(key string) string {
+// GoEnv returns the value for key in `go env`.
+func GoEnv(key string) (string, error) {
 	out, err := exec.Command("go", "env", "-json", key).Output()
 	if err != nil {
-		return ""
+		return "", err
 	}
 	env := make(map[string]string)
 	if err := json.Unmarshal(out, &env); err != nil {
-		return ""
+		return "", err
 	}
-	return env[key]
+	return env[key], nil
 }
diff --git a/internal/goenv_test.go b/internal/goenv_test.go
index 7d02e4f..edb1a1c 100644
--- a/internal/goenv_test.go
+++ b/internal/goenv_test.go
@@ -6,12 +6,38 @@
 
 import (
 	"testing"
+
+	"golang.org/x/vuln/internal/testenv"
 )
 
 func TestGoEnv(t *testing.T) {
+	testenv.NeedsGoEnv(t)
+
 	for _, key := range []string{"GOVERSION", "GOROOT", "GOPATH", "GOMODCACHE"} {
-		if GoEnv(key) == "" {
+		if val, err := GoEnv(key); val == "" {
 			t.Errorf("want something for go env %s; got nothing", key)
+		} else if err != nil {
+			t.Errorf("unexpected error for go env %s: %v", key, err)
 		}
 	}
 }
+
+func TestGoEnvNonVariable(t *testing.T) {
+	testenv.NeedsGoEnv(t)
+
+	key := "NOT_A_GO_ENV_VARIABLE"
+	if val, err := GoEnv(key); val != "" {
+		t.Errorf("expected nothing for go env %s; got %s", key, val)
+	} else if err != nil {
+		t.Errorf("unexpected error for go env %s: %v", key, err)
+	}
+}
+
+func TestGoEnvErr(t *testing.T) {
+	testenv.NeedsGoEnv(t)
+
+	key := "--not-a-flag"
+	if val, err := GoEnv(key); err == nil {
+		t.Errorf("wanted an error from go env %s; got value %q", key, val)
+	}
+}
diff --git a/internal/goenv_testmode.go b/internal/goenv_testmode.go
index c29f602..d187d14 100644
--- a/internal/goenv_testmode.go
+++ b/internal/goenv_testmode.go
@@ -13,21 +13,19 @@
 	"os/exec"
 )
 
-// GoEnv returns the value for key in `go env`. In
-// the unlikely case that `go env` fails, prints an
-// error message and returns an empty string.
+// GoEnv returns the value for key in `go env`.
 //
 // For debugging and testing purposes, the value of
 // undocumented environment variable TEST_GOVERSION
 // is used for go env GOVERSION.
-func GoEnv(key string) string {
+func GoEnv(key string) (string, error) {
 	out, err := exec.Command("go", "env", "-json", key).Output()
 	if err != nil {
-		return ""
+		return "", err
 	}
 	env := make(map[string]string)
 	if err := json.Unmarshal(out, &env); err != nil {
-		return ""
+		return "", err
 	}
 
 	if v := os.Getenv("TEST_GOVERSION"); v != "" {
@@ -35,5 +33,5 @@
 		env["GOVERSION"] = v
 	}
 
-	return env[key]
+	return env[key], nil
 }
diff --git a/internal/govulncheck/cache.go b/internal/govulncheck/cache.go
index 0229c79..24f3a4d 100644
--- a/internal/govulncheck/cache.go
+++ b/internal/govulncheck/cache.go
@@ -45,10 +45,24 @@
 // Assert that *FSCache implements client.Cache.
 var _ client.Cache = (*FSCache)(nil)
 
-var defaultCacheRoot = filepath.Join(internal.GoEnv("GOMODCACHE"), "/cache/download/vulndb")
+var (
+	initDefaultCache sync.Once
+	defaultCache     *FSCache
+	defaultCacheErr  error
+)
 
-func DefaultCache() *FSCache {
-	return &FSCache{rootDir: defaultCacheRoot}
+func DefaultCache() (*FSCache, error) {
+	initDefaultCache.Do(func() {
+		mod, err := internal.GoEnv("GOMODCACHE")
+		if err != nil {
+			defaultCacheErr = err
+			return
+		}
+		defaultCache = &FSCache{
+			rootDir: filepath.Join(mod, "/cache/download/vulndb"),
+		}
+	})
+	return defaultCache, defaultCacheErr
 }
 
 type cachedIndex struct {
diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go
new file mode 100644
index 0000000..8e9021e
--- /dev/null
+++ b/internal/testenv/testenv.go
@@ -0,0 +1,20 @@
+// Copyright 2023 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 testenv
+
+import (
+	"os/exec"
+	"testing"
+)
+
+// NeedsGoEnv skips t if the current system can't get the environment with
+// “go env” in a subprocess.
+func NeedsGoEnv(t testing.TB) {
+	t.Helper()
+
+	if _, err := exec.LookPath("go"); err != nil {
+		t.Skip("skipping test: can't run go env")
+	}
+}
diff --git a/vulncheck/source.go b/vulncheck/source.go
index fcdfb26..9cbed6b 100644
--- a/vulncheck/source.go
+++ b/vulncheck/source.go
@@ -52,7 +52,11 @@
 	if cfg.SourceGoVersion != "" {
 		stdlibModule.Version = semver.GoTagToSemver(cfg.SourceGoVersion)
 	} else {
-		stdlibModule.Version = semver.GoTagToSemver(internal.GoEnv("GOVERSION"))
+		gover, err := internal.GoEnv("GOVERSION")
+		if err != nil {
+			return nil, err
+		}
+		stdlibModule.Version = semver.GoTagToSemver(gover)
 	}
 
 	ctx, cancel := context.WithCancel(ctx)