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