vet_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. // Copyright 2013 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package main_test
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "internal/testenv"
  10. "log"
  11. "os"
  12. "os/exec"
  13. "path"
  14. "path/filepath"
  15. "regexp"
  16. "runtime"
  17. "strconv"
  18. "strings"
  19. "sync"
  20. "testing"
  21. )
  22. const dataDir = "testdata"
  23. var binary string
  24. // We implement TestMain so remove the test binary when all is done.
  25. func TestMain(m *testing.M) {
  26. os.Exit(testMain(m))
  27. }
  28. func testMain(m *testing.M) int {
  29. dir, err := os.MkdirTemp("", "vet_test")
  30. if err != nil {
  31. fmt.Fprintln(os.Stderr, err)
  32. return 1
  33. }
  34. defer os.RemoveAll(dir)
  35. binary = filepath.Join(dir, "testvet.exe")
  36. return m.Run()
  37. }
  38. var (
  39. buildMu sync.Mutex // guards following
  40. built = false // We have built the binary.
  41. failed = false // We have failed to build the binary, don't try again.
  42. )
  43. func Build(t *testing.T) {
  44. buildMu.Lock()
  45. defer buildMu.Unlock()
  46. if built {
  47. return
  48. }
  49. if failed {
  50. t.Skip("cannot run on this environment")
  51. }
  52. testenv.MustHaveGoBuild(t)
  53. cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", binary)
  54. output, err := cmd.CombinedOutput()
  55. if err != nil {
  56. failed = true
  57. fmt.Fprintf(os.Stderr, "%s\n", output)
  58. t.Fatal(err)
  59. }
  60. built = true
  61. }
  62. func vetCmd(t *testing.T, arg, pkg string) *exec.Cmd {
  63. cmd := exec.Command(testenv.GoToolPath(t), "vet", "-vettool="+binary, arg, path.Join("cmd/vet/testdata", pkg))
  64. cmd.Env = os.Environ()
  65. return cmd
  66. }
  67. func TestVet(t *testing.T) {
  68. t.Parallel()
  69. Build(t)
  70. for _, pkg := range []string{
  71. "asm",
  72. "assign",
  73. "atomic",
  74. "bool",
  75. "buildtag",
  76. "cgo",
  77. "composite",
  78. "copylock",
  79. "deadcode",
  80. "httpresponse",
  81. "lostcancel",
  82. "method",
  83. "nilfunc",
  84. "print",
  85. "rangeloop",
  86. "shift",
  87. "structtag",
  88. "testingpkg",
  89. // "testtag" has its own test
  90. "unmarshal",
  91. "unsafeptr",
  92. "unused",
  93. } {
  94. pkg := pkg
  95. t.Run(pkg, func(t *testing.T) {
  96. t.Parallel()
  97. // Skip cgo test on platforms without cgo.
  98. if pkg == "cgo" && !cgoEnabled(t) {
  99. return
  100. }
  101. cmd := vetCmd(t, "-printfuncs=Warn,Warnf", pkg)
  102. // The asm test assumes amd64.
  103. if pkg == "asm" {
  104. if runtime.Compiler == "gccgo" {
  105. t.Skip("asm test assumes gc")
  106. }
  107. cmd.Env = append(cmd.Env, "GOOS=linux", "GOARCH=amd64")
  108. }
  109. dir := filepath.Join("testdata", pkg)
  110. gos, err := filepath.Glob(filepath.Join(dir, "*.go"))
  111. if err != nil {
  112. t.Fatal(err)
  113. }
  114. asms, err := filepath.Glob(filepath.Join(dir, "*.s"))
  115. if err != nil {
  116. t.Fatal(err)
  117. }
  118. var files []string
  119. files = append(files, gos...)
  120. files = append(files, asms...)
  121. errchk(cmd, files, t)
  122. })
  123. }
  124. }
  125. func cgoEnabled(t *testing.T) bool {
  126. // Don't trust build.Default.CgoEnabled as it is false for
  127. // cross-builds unless CGO_ENABLED is explicitly specified.
  128. // That's fine for the builders, but causes commands like
  129. // 'GOARCH=386 go test .' to fail.
  130. // Instead, we ask the go command.
  131. cmd := exec.Command(testenv.GoToolPath(t), "list", "-f", "{{context.CgoEnabled}}")
  132. out, _ := cmd.CombinedOutput()
  133. return string(out) == "true\n"
  134. }
  135. func errchk(c *exec.Cmd, files []string, t *testing.T) {
  136. output, err := c.CombinedOutput()
  137. if _, ok := err.(*exec.ExitError); !ok {
  138. t.Logf("vet output:\n%s", output)
  139. t.Fatal(err)
  140. }
  141. fullshort := make([]string, 0, len(files)*2)
  142. for _, f := range files {
  143. fullshort = append(fullshort, f, filepath.Base(f))
  144. }
  145. err = errorCheck(string(output), false, fullshort...)
  146. if err != nil {
  147. t.Errorf("error check failed: %s", err)
  148. }
  149. }
  150. // TestTags verifies that the -tags argument controls which files to check.
  151. func TestTags(t *testing.T) {
  152. t.Parallel()
  153. Build(t)
  154. for tag, wantFile := range map[string]int{
  155. "testtag": 1, // file1
  156. "x testtag y": 1,
  157. "othertag": 2,
  158. } {
  159. tag, wantFile := tag, wantFile
  160. t.Run(tag, func(t *testing.T) {
  161. t.Parallel()
  162. t.Logf("-tags=%s", tag)
  163. cmd := vetCmd(t, "-tags="+tag, "tagtest")
  164. output, err := cmd.CombinedOutput()
  165. want := fmt.Sprintf("file%d.go", wantFile)
  166. dontwant := fmt.Sprintf("file%d.go", 3-wantFile)
  167. // file1 has testtag and file2 has !testtag.
  168. if !bytes.Contains(output, []byte(filepath.Join("tagtest", want))) {
  169. t.Errorf("%s: %s was excluded, should be included", tag, want)
  170. }
  171. if bytes.Contains(output, []byte(filepath.Join("tagtest", dontwant))) {
  172. t.Errorf("%s: %s was included, should be excluded", tag, dontwant)
  173. }
  174. if t.Failed() {
  175. t.Logf("err=%s, output=<<%s>>", err, output)
  176. }
  177. })
  178. }
  179. }
  180. // All declarations below were adapted from test/run.go.
  181. // errorCheck matches errors in outStr against comments in source files.
  182. // For each line of the source files which should generate an error,
  183. // there should be a comment of the form // ERROR "regexp".
  184. // If outStr has an error for a line which has no such comment,
  185. // this function will report an error.
  186. // Likewise if outStr does not have an error for a line which has a comment,
  187. // or if the error message does not match the <regexp>.
  188. // The <regexp> syntax is Perl but it's best to stick to egrep.
  189. //
  190. // Sources files are supplied as fullshort slice.
  191. // It consists of pairs: full path to source file and its base name.
  192. func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
  193. var errs []error
  194. out := splitOutput(outStr, wantAuto)
  195. // Cut directory name.
  196. for i := range out {
  197. for j := 0; j < len(fullshort); j += 2 {
  198. full, short := fullshort[j], fullshort[j+1]
  199. out[i] = strings.ReplaceAll(out[i], full, short)
  200. }
  201. }
  202. var want []wantedError
  203. for j := 0; j < len(fullshort); j += 2 {
  204. full, short := fullshort[j], fullshort[j+1]
  205. want = append(want, wantedErrors(full, short)...)
  206. }
  207. for _, we := range want {
  208. var errmsgs []string
  209. if we.auto {
  210. errmsgs, out = partitionStrings("<autogenerated>", out)
  211. } else {
  212. errmsgs, out = partitionStrings(we.prefix, out)
  213. }
  214. if len(errmsgs) == 0 {
  215. errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
  216. continue
  217. }
  218. matched := false
  219. n := len(out)
  220. for _, errmsg := range errmsgs {
  221. // Assume errmsg says "file:line: foo".
  222. // Cut leading "file:line: " to avoid accidental matching of file name instead of message.
  223. text := errmsg
  224. if _, suffix, ok := strings.Cut(text, " "); ok {
  225. text = suffix
  226. }
  227. if we.re.MatchString(text) {
  228. matched = true
  229. } else {
  230. out = append(out, errmsg)
  231. }
  232. }
  233. if !matched {
  234. 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")))
  235. continue
  236. }
  237. }
  238. if len(out) > 0 {
  239. errs = append(errs, fmt.Errorf("Unmatched Errors:"))
  240. for _, errLine := range out {
  241. errs = append(errs, fmt.Errorf("%s", errLine))
  242. }
  243. }
  244. if len(errs) == 0 {
  245. return nil
  246. }
  247. if len(errs) == 1 {
  248. return errs[0]
  249. }
  250. var buf bytes.Buffer
  251. fmt.Fprintf(&buf, "\n")
  252. for _, err := range errs {
  253. fmt.Fprintf(&buf, "%s\n", err.Error())
  254. }
  255. return errors.New(buf.String())
  256. }
  257. func splitOutput(out string, wantAuto bool) []string {
  258. // gc error messages continue onto additional lines with leading tabs.
  259. // Split the output at the beginning of each line that doesn't begin with a tab.
  260. // <autogenerated> lines are impossible to match so those are filtered out.
  261. var res []string
  262. for _, line := range strings.Split(out, "\n") {
  263. line = strings.TrimSuffix(line, "\r") // normalize Windows output
  264. if strings.HasPrefix(line, "\t") {
  265. res[len(res)-1] += "\n" + line
  266. } else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
  267. continue
  268. } else if strings.TrimSpace(line) != "" {
  269. res = append(res, line)
  270. }
  271. }
  272. return res
  273. }
  274. // matchPrefix reports whether s starts with file name prefix followed by a :,
  275. // and possibly preceded by a directory name.
  276. func matchPrefix(s, prefix string) bool {
  277. i := strings.Index(s, ":")
  278. if i < 0 {
  279. return false
  280. }
  281. j := strings.LastIndex(s[:i], "/")
  282. s = s[j+1:]
  283. if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
  284. return false
  285. }
  286. if s[len(prefix)] == ':' {
  287. return true
  288. }
  289. return false
  290. }
  291. func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
  292. for _, s := range strs {
  293. if matchPrefix(s, prefix) {
  294. matched = append(matched, s)
  295. } else {
  296. unmatched = append(unmatched, s)
  297. }
  298. }
  299. return
  300. }
  301. type wantedError struct {
  302. reStr string
  303. re *regexp.Regexp
  304. lineNum int
  305. auto bool // match <autogenerated> line
  306. file string
  307. prefix string
  308. }
  309. var (
  310. errRx = regexp.MustCompile(`// (?:GC_)?ERROR(NEXT)? (.*)`)
  311. errAutoRx = regexp.MustCompile(`// (?:GC_)?ERRORAUTO(NEXT)? (.*)`)
  312. errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
  313. lineRx = regexp.MustCompile(`LINE(([+-])([0-9]+))?`)
  314. )
  315. // wantedErrors parses expected errors from comments in a file.
  316. func wantedErrors(file, short string) (errs []wantedError) {
  317. cache := make(map[string]*regexp.Regexp)
  318. src, err := os.ReadFile(file)
  319. if err != nil {
  320. log.Fatal(err)
  321. }
  322. for i, line := range strings.Split(string(src), "\n") {
  323. lineNum := i + 1
  324. if strings.Contains(line, "////") {
  325. // double comment disables ERROR
  326. continue
  327. }
  328. var auto bool
  329. m := errAutoRx.FindStringSubmatch(line)
  330. if m != nil {
  331. auto = true
  332. } else {
  333. m = errRx.FindStringSubmatch(line)
  334. }
  335. if m == nil {
  336. continue
  337. }
  338. if m[1] == "NEXT" {
  339. lineNum++
  340. }
  341. all := m[2]
  342. mm := errQuotesRx.FindAllStringSubmatch(all, -1)
  343. if mm == nil {
  344. log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line)
  345. }
  346. for _, m := range mm {
  347. replacedOnce := false
  348. rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
  349. if replacedOnce {
  350. return m
  351. }
  352. replacedOnce = true
  353. n := lineNum
  354. if strings.HasPrefix(m, "LINE+") {
  355. delta, _ := strconv.Atoi(m[5:])
  356. n += delta
  357. } else if strings.HasPrefix(m, "LINE-") {
  358. delta, _ := strconv.Atoi(m[5:])
  359. n -= delta
  360. }
  361. return fmt.Sprintf("%s:%d", short, n)
  362. })
  363. re := cache[rx]
  364. if re == nil {
  365. var err error
  366. re, err = regexp.Compile(rx)
  367. if err != nil {
  368. log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
  369. }
  370. cache[rx] = re
  371. }
  372. prefix := fmt.Sprintf("%s:%d", short, lineNum)
  373. errs = append(errs, wantedError{
  374. reStr: rx,
  375. re: re,
  376. prefix: prefix,
  377. auto: auto,
  378. lineNum: lineNum,
  379. file: short,
  380. })
  381. }
  382. }
  383. return
  384. }