blob: f7b049658ee98bafae8dfa49e7af6a8021359186 [file] [log] [blame]
// 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.
// Only run this on Go 1.18 or higher, because govulncheck can't
// run on binaries before 1.18.
//go:build go1.18
// +build go1.18
package main
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"sync"
"testing"
"unsafe"
"github.com/google/go-cmdtest"
"golang.org/x/vuln/internal/test"
"golang.org/x/vuln/internal/web"
"golang.org/x/vuln/scan"
)
var update = flag.Bool("update", false, "update test files with results")
type fixup struct {
pattern string
compiled *regexp.Regexp
replace string
replaceFunc func(b []byte) []byte
}
var fixups = []fixup{
{
// modifies paths to Go files by replacing their directory with "...".
// For example,/a/b/c.go becomes .../c.go .
// This makes it possible to compare govulncheck output across systems, because
// Go filenames include setup-specific paths.
pattern: `[^\s"]*\.go[\s":]`,
replaceFunc: func(b []byte) []byte {
s := string(b)
return []byte(fmt.Sprintf(`.../%s%c`, filepath.Base(s[:len(s)-1]), s[len(s)-1]))
},
}, {
// There was a one-line change in container/heap/heap.go between 1.18
// and 1.19 that makes the stack traces different. Ignore it.
pattern: `heap\.go:(\d+)`,
replace: `N`,
}, {
pattern: `Scanning your code and (\d+) packages across (\d+)`,
replace: `Scanning your code and P packages across M`,
}, {
pattern: `govulncheck@v([^ ]*) `,
replace: `govulncheck@v0.0.0-00000000000-20000101010101 `,
}, {
pattern: `"([^"]*") is a file`,
replace: `govulncheck: myfile is a file`,
}, {
pattern: `"scanner_version": "[^"]*"`,
replace: `"scanner_version": "v0.0.0-00000000000-20000101010101"`,
}, {
pattern: `file:///(.*)/testdata/vulndb`,
replace: `testdata/vulndb`,
}, {
pattern: `package (.*) is not in (GOROOT|std) (.*)`,
replace: `package foo is not in GOROOT (/tmp/foo)`,
}, {
pattern: `modified (.*)\)`,
replace: `modified 01 Jan 21 00:00 UTC)`,
}, {
pattern: `Using (go1.[\.\d]*|devel).* and`,
replace: `Using go1.18 and`,
}, {
pattern: `"go_version": "go[^\s"]*"`,
replace: `"go_version": "go1.18"`,
},
}
func (f *fixup) init() {
f.compiled = regexp.MustCompile(f.pattern)
}
func (f *fixup) apply(data []byte) []byte {
if f.replaceFunc != nil {
return f.compiled.ReplaceAllFunc(data, f.replaceFunc)
}
return f.compiled.ReplaceAll(data, []byte(f.replace))
}
func init() {
for i := range fixups {
fixups[i].init()
}
}
func TestCommand(t *testing.T) {
testDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
vulndbDir, err := filepath.Abs(filepath.Join(testDir, "testdata", "vulndb-v1"))
if err != nil {
t.Fatal(err)
}
govulndbURI, err := web.URLFromFilePath(vulndbDir)
if err != nil {
t.Fatalf("failed to create make vulndb url: %v", err)
}
moduleDirs, err := filepath.Glob("testdata/modules/*")
if err != nil {
t.Fatal(err)
}
os.Setenv("moddir", filepath.Join(testDir, "testdata", "modules"))
for _, md := range moduleDirs {
// Skip nogomod module. It has intended build issues.
if filepath.Base(md) == "nogomod" {
continue
}
// Build test module binary.
binary, cleanup := test.GoBuild(t, md, "", filepath.Base(md) == "strip")
t.Cleanup(cleanup)
// Set an environment variable to the path to the binary, so tests
// can refer to it.
varName := filepath.Base(md) + "_binary"
os.Setenv(varName, binary)
}
runTestSuite(t, filepath.Join(testDir, "testdata"), govulndbURI.String(), *update)
if runtime.GOOS != "darwin" {
// TODO(https://go.dev/issue/59732): investigate why
runTestSuite(t, filepath.Join(testDir, "testdata/strip"), govulndbURI.String(), *update)
}
}
// Limit the number of concurrent scans. Scanning is implemented using
// x/tools/go/ssa, which is known to be memory-hungry
// (see https://go.dev/issue/14113), and by default the testing package
// allows up to GOMAXPROCS parallel tests at a time.
//
// For now we arbitrarily limit to ⌈GOMAXPROCS/4⌉, on the theory that many Go
// developer and CI machines have at least 8 logical cores and we want most
// runs of the test to exercise at least a little concurrency. If that turns
// out to still be too high, we may consider reducing it further.
//
// Since all of the scans run in the same process, we need an especially low
// limit on 32-bit platforms: we may run out of virtual address space well
// before we run out of system RAM.
var (
parallelLimiter chan struct{}
parallelLimiterInit sync.Once
)
// testSuite creates a cmdtest suite from dir. It also defines
// a govulncheck command on the suite that runs govulncheck
// against vulnerability database available at vulndbDir.
func runTestSuite(t *testing.T, dir string, govulndb string, update bool) {
parallelLimiterInit.Do(func() {
limit := (runtime.GOMAXPROCS(0) + 3) / 4
if limit > 2 && unsafe.Sizeof(uintptr(0)) < 8 {
limit = 2
}
parallelLimiter = make(chan struct{}, limit)
})
ts, err := cmdtest.Read(dir)
if err != nil {
t.Fatal(err)
}
ts.DisableLogging = true
ts.Commands["govulncheck"] = func(args []string, inputFile string) ([]byte, error) {
parallelLimiter <- struct{}{}
defer func() { <-parallelLimiter }()
newargs := append([]string{"-db", govulndb}, args...)
buf := &bytes.Buffer{}
cmd := scan.Command(context.Background(), newargs...)
cmd.Stdout = buf
cmd.Stderr = buf
if inputFile != "" {
input, err := os.Open(filepath.Join(dir, inputFile))
if err != nil {
return nil, err
}
defer input.Close()
cmd.Stdin = input
}
// We set GOVERSION to always get the same results regardless of the underlying Go build system.
//TODO: when cmd supports Env:
//cmd.Env = append(os.Environ(), "GOVERSION=go1.18")
os.Setenv("GOVERSION", "go1.18")
if err := cmd.Start(); err != nil {
return nil, err
}
err := cmd.Wait()
switch e := err.(type) {
case nil:
case interface{ ExitCode() int }:
err = &cmdtest.ExitCodeErr{Msg: err.Error(), Code: e.ExitCode()}
if e.ExitCode() == 0 {
err = nil
}
default:
fmt.Fprintln(buf, err)
err = &cmdtest.ExitCodeErr{Msg: err.Error(), Code: 1}
}
out := buf.Bytes()
for _, fix := range fixups {
out = fix.apply(out)
}
return out, err
}
ts.RunParallel(t, update)
}