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