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