| // 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 i := strings.Index(text, " "); i >= 0 { | |
| text = text[i+1:] | |
| } | |
| 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 | |
| } |