blob: 714ee320aee603f72975e4fd3e9d703f5825d4e8 [file] [log] [blame]
// Copyright 2013 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 (
"bytes"
"errors"
"fmt"
"internal/testenv"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"testing"
)
const dataDir = "testdata"
var binary string
// We implement TestMain so remove the test binary when all is done.
func TestMain(m *testing.M) {
os.Exit(testMain(m))
}
func testMain(m *testing.M) int {
dir, err := os.MkdirTemp("", "vet_test")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
defer os.RemoveAll(dir)
binary = filepath.Join(dir, "testvet.exe")
return m.Run()
}
var (
buildMu sync.Mutex // guards following
built = false // We have built the binary.
failed = false // We have failed to build the binary, don't try again.
)
func Build(t *testing.T) {
buildMu.Lock()
defer buildMu.Unlock()
if built {
return
}
if failed {
t.Skip("cannot run on this environment")
}
testenv.MustHaveGoBuild(t)
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", binary)
output, err := cmd.CombinedOutput()
if err != nil {
failed = true
fmt.Fprintf(os.Stderr, "%s\n", output)
t.Fatal(err)
}
built = true
}
func vetCmd(t *testing.T, arg, pkg string) *exec.Cmd {
cmd := exec.Command(testenv.GoToolPath(t), "vet", "-vettool="+binary, arg, path.Join("cmd/vet/testdata", pkg))
cmd.Env = os.Environ()
return cmd
}
func TestVet(t *testing.T) {
t.Parallel()
Build(t)
for _, pkg := range []string{
"asm",
"assign",
"atomic",
"bool",
"buildtag",
"cgo",
"composite",
"copylock",
"deadcode",
"httpresponse",
"lostcancel",
"method",
"nilfunc",
"print",
"rangeloop",
"shift",
"structtag",
"testingpkg",
// "testtag" has its own test
"unmarshal",
"unsafeptr",
"unused",
} {
pkg := pkg
t.Run(pkg, func(t *testing.T) {
t.Parallel()
// Skip cgo test on platforms without cgo.
if pkg == "cgo" && !cgoEnabled(t) {
return
}
cmd := vetCmd(t, "-printfuncs=Warn,Warnf", pkg)
// The asm test assumes amd64.
if pkg == "asm" {
cmd.Env = append(cmd.Env, "GOOS=linux", "GOARCH=amd64")
}
dir := filepath.Join("testdata", pkg)
gos, err := filepath.Glob(filepath.Join(dir, "*.go"))
if err != nil {
t.Fatal(err)
}
asms, err := filepath.Glob(filepath.Join(dir, "*.s"))
if err != nil {
t.Fatal(err)
}
var files []string
files = append(files, gos...)
files = append(files, asms...)
errchk(cmd, files, t)
})
}
}
func cgoEnabled(t *testing.T) bool {
// Don't trust build.Default.CgoEnabled as it is false for
// cross-builds unless CGO_ENABLED is explicitly specified.
// That's fine for the builders, but causes commands like
// 'GOARCH=386 go test .' to fail.
// Instead, we ask the go command.
cmd := exec.Command(testenv.GoToolPath(t), "list", "-f", "{{context.CgoEnabled}}")
out, _ := cmd.CombinedOutput()
return string(out) == "true\n"
}
func errchk(c *exec.Cmd, files []string, t *testing.T) {
output, err := c.CombinedOutput()
if _, ok := err.(*exec.ExitError); !ok {
t.Logf("vet output:\n%s", output)
t.Fatal(err)
}
fullshort := make([]string, 0, len(files)*2)
for _, f := range files {
fullshort = append(fullshort, f, filepath.Base(f))
}
err = errorCheck(string(output), false, fullshort...)
if err != nil {
t.Errorf("error check failed: %s", err)
}
}
// TestTags verifies that the -tags argument controls which files to check.
func TestTags(t *testing.T) {
t.Parallel()
Build(t)
for tag, wantFile := range map[string]int{
"testtag": 1, // file1
"x testtag y": 1,
"othertag": 2,
} {
tag, wantFile := tag, wantFile
t.Run(tag, func(t *testing.T) {
t.Parallel()
t.Logf("-tags=%s", tag)
cmd := vetCmd(t, "-tags="+tag, "tagtest")
output, err := cmd.CombinedOutput()
want := fmt.Sprintf("file%d.go", wantFile)
dontwant := fmt.Sprintf("file%d.go", 3-wantFile)
// file1 has testtag and file2 has !testtag.
if !bytes.Contains(output, []byte(filepath.Join("tagtest", want))) {
t.Errorf("%s: %s was excluded, should be included", tag, want)
}
if bytes.Contains(output, []byte(filepath.Join("tagtest", dontwant))) {
t.Errorf("%s: %s was included, should be excluded", tag, dontwant)
}
if t.Failed() {
t.Logf("err=%s, output=<<%s>>", err, output)
}
})
}
}
// All declarations below were adapted from test/run.go.
// errorCheck matches errors in outStr against comments in source files.
// For each line of the source files which should generate an error,
// there should be a comment of the form // ERROR "regexp".
// If outStr has an error for a line which has no such comment,
// this function will report an error.
// Likewise if outStr does not have an error for a line which has a comment,
// or if the error message does not match the <regexp>.
// The <regexp> syntax is Perl but it's best to stick to egrep.
//
// Sources files are supplied as fullshort slice.
// It consists of pairs: full path to source file and its base name.
func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
var errs []error
out := splitOutput(outStr, wantAuto)
// Cut directory name.
for i := range out {
for j := 0; j < len(fullshort); j += 2 {
full, short := fullshort[j], fullshort[j+1]
out[i] = strings.ReplaceAll(out[i], full, short)
}
}
var want []wantedError
for j := 0; j < len(fullshort); j += 2 {
full, short := fullshort[j], fullshort[j+1]
want = append(want, wantedErrors(full, short)...)
}
for _, we := range want {
var errmsgs []string
if we.auto {
errmsgs, out = partitionStrings("<autogenerated>", out)
} else {
errmsgs, out = partitionStrings(we.prefix, out)
}
if len(errmsgs) == 0 {
errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
continue
}
matched := false
n := len(out)
for _, errmsg := range errmsgs {
// Assume errmsg says "file:line: foo".
// Cut leading "file:line: " to avoid accidental matching of file name instead of message.
text := errmsg
if _, suffix, ok := strings.Cut(text, " "); ok {
text = suffix
}
if we.re.MatchString(text) {
matched = true
} else {
out = append(out, errmsg)
}
}
if !matched {
errs = append(errs, fmt.Errorf("%s:%d: no match for %#q in:\n\t%s", we.file, we.lineNum, we.reStr, strings.Join(out[n:], "\n\t")))
continue
}
}
if len(out) > 0 {
errs = append(errs, fmt.Errorf("Unmatched Errors:"))
for _, errLine := range out {
errs = append(errs, fmt.Errorf("%s", errLine))
}
}
if len(errs) == 0 {
return nil
}
if len(errs) == 1 {
return errs[0]
}
var buf bytes.Buffer
fmt.Fprintf(&buf, "\n")
for _, err := range errs {
fmt.Fprintf(&buf, "%s\n", err.Error())
}
return errors.New(buf.String())
}
func splitOutput(out string, wantAuto bool) []string {
// gc error messages continue onto additional lines with leading tabs.
// Split the output at the beginning of each line that doesn't begin with a tab.
// <autogenerated> lines are impossible to match so those are filtered out.
var res []string
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSuffix(line, "\r") // normalize Windows output
if strings.HasPrefix(line, "\t") {
res[len(res)-1] += "\n" + line
} else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
continue
} else if strings.TrimSpace(line) != "" {
res = append(res, line)
}
}
return res
}
// matchPrefix reports whether s starts with file name prefix followed by a :,
// and possibly preceded by a directory name.
func matchPrefix(s, prefix string) bool {
i := strings.Index(s, ":")
if i < 0 {
return false
}
j := strings.LastIndex(s[:i], "/")
s = s[j+1:]
if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
return false
}
if s[len(prefix)] == ':' {
return true
}
return false
}
func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
for _, s := range strs {
if matchPrefix(s, prefix) {
matched = append(matched, s)
} else {
unmatched = append(unmatched, s)
}
}
return
}
type wantedError struct {
reStr string
re *regexp.Regexp
lineNum int
auto bool // match <autogenerated> line
file string
prefix string
}
var (
errRx = regexp.MustCompile(`// (?:GC_)?ERROR(NEXT)? (.*)`)
errAutoRx = regexp.MustCompile(`// (?:GC_)?ERRORAUTO(NEXT)? (.*)`)
errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
lineRx = regexp.MustCompile(`LINE(([+-])([0-9]+))?`)
)
// wantedErrors parses expected errors from comments in a file.
func wantedErrors(file, short string) (errs []wantedError) {
cache := make(map[string]*regexp.Regexp)
src, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
for i, line := range strings.Split(string(src), "\n") {
lineNum := i + 1
if strings.Contains(line, "////") {
// double comment disables ERROR
continue
}
var auto bool
m := errAutoRx.FindStringSubmatch(line)
if m != nil {
auto = true
} else {
m = errRx.FindStringSubmatch(line)
}
if m == nil {
continue
}
if m[1] == "NEXT" {
lineNum++
}
all := m[2]
mm := errQuotesRx.FindAllStringSubmatch(all, -1)
if mm == nil {
log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line)
}
for _, m := range mm {
replacedOnce := false
rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
if replacedOnce {
return m
}
replacedOnce = true
n := lineNum
if strings.HasPrefix(m, "LINE+") {
delta, _ := strconv.Atoi(m[5:])
n += delta
} else if strings.HasPrefix(m, "LINE-") {
delta, _ := strconv.Atoi(m[5:])
n -= delta
}
return fmt.Sprintf("%s:%d", short, n)
})
re := cache[rx]
if re == nil {
var err error
re, err = regexp.Compile(rx)
if err != nil {
log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
}
cache[rx] = re
}
prefix := fmt.Sprintf("%s:%d", short, lineNum)
errs = append(errs, wantedError{
reStr: rx,
re: re,
prefix: prefix,
auto: auto,
lineNum: lineNum,
file: short,
})
}
}
return
}