| // skip |
| |
| // Copyright 2012 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. |
| |
| // Run runs tests in the test directory. |
| // |
| // TODO(bradfitz): docs of some sort, once we figure out how we're changing |
| // headers of files |
| package main |
| |
| import ( |
| "bytes" |
| "errors" |
| "flag" |
| "fmt" |
| "go/build" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| ) |
| |
| var ( |
| verbose = flag.Bool("v", false, "verbose. if set, parallelism is set to 1.") |
| numParallel = flag.Int("n", runtime.NumCPU(), "number of parallel tests to run") |
| summary = flag.Bool("summary", false, "show summary of results") |
| showSkips = flag.Bool("show_skips", false, "show skipped tests") |
| ) |
| |
| var ( |
| // gc and ld are [568][gl]. |
| gc, ld string |
| |
| // letter is the build.ArchChar |
| letter string |
| |
| // dirs are the directories to look for *.go files in. |
| // TODO(bradfitz): just use all directories? |
| dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs", "bugs"} |
| |
| // ratec controls the max number of tests running at a time. |
| ratec chan bool |
| |
| // toRun is the channel of tests to run. |
| // It is nil until the first test is started. |
| toRun chan *test |
| ) |
| |
| // maxTests is an upper bound on the total number of tests. |
| // It is used as a channel buffer size to make sure sends don't block. |
| const maxTests = 5000 |
| |
| func main() { |
| flag.Parse() |
| |
| // Disable parallelism if printing |
| if *verbose { |
| *numParallel = 1 |
| } |
| |
| ratec = make(chan bool, *numParallel) |
| var err error |
| letter, err = build.ArchChar(build.Default.GOARCH) |
| check(err) |
| gc = letter + "g" |
| ld = letter + "l" |
| |
| var tests []*test |
| if flag.NArg() > 0 { |
| for _, arg := range flag.Args() { |
| if arg == "-" || arg == "--" { |
| // Permit running either: |
| // $ go run run.go - env.go |
| // $ go run run.go -- env.go |
| continue |
| } |
| if !strings.HasSuffix(arg, ".go") { |
| log.Fatalf("can't yet deal with non-go file %q", arg) |
| } |
| dir, file := filepath.Split(arg) |
| tests = append(tests, startTest(dir, file)) |
| } |
| } else { |
| for _, dir := range dirs { |
| for _, baseGoFile := range goFiles(dir) { |
| tests = append(tests, startTest(dir, baseGoFile)) |
| } |
| } |
| } |
| |
| failed := false |
| resCount := map[string]int{} |
| for _, test := range tests { |
| <-test.donec |
| _, isSkip := test.err.(skipError) |
| errStr := "pass" |
| if isSkip { |
| errStr = "skip" |
| } |
| if test.err != nil { |
| errStr = test.err.Error() |
| if !isSkip { |
| failed = true |
| } |
| } |
| resCount[errStr]++ |
| if isSkip && !*verbose && !*showSkips { |
| continue |
| } |
| if !*verbose && test.err == nil { |
| continue |
| } |
| fmt.Printf("%-10s %-20s: %s\n", test.action, test.goFileName(), errStr) |
| } |
| |
| if *summary { |
| for k, v := range resCount { |
| fmt.Printf("%5d %s\n", v, k) |
| } |
| } |
| |
| if failed { |
| os.Exit(1) |
| } |
| } |
| |
| func toolPath(name string) string { |
| p := filepath.Join(os.Getenv("GOROOT"), "bin", "tool", name) |
| if _, err := os.Stat(p); err != nil { |
| log.Fatalf("didn't find binary at %s", p) |
| } |
| return p |
| } |
| |
| func goFiles(dir string) []string { |
| f, err := os.Open(dir) |
| check(err) |
| dirnames, err := f.Readdirnames(-1) |
| check(err) |
| names := []string{} |
| for _, name := range dirnames { |
| if !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") { |
| names = append(names, name) |
| } |
| } |
| sort.Strings(names) |
| return names |
| } |
| |
| // skipError describes why a test was skipped. |
| type skipError string |
| |
| func (s skipError) Error() string { return string(s) } |
| |
| func check(err error) { |
| if err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| // test holds the state of a test. |
| type test struct { |
| dir, gofile string |
| donec chan bool // closed when done |
| |
| src string |
| action string // "compile", "build", "run", "errorcheck", "skip" |
| |
| tempDir string |
| err error |
| } |
| |
| // startTest |
| func startTest(dir, gofile string) *test { |
| t := &test{ |
| dir: dir, |
| gofile: gofile, |
| donec: make(chan bool, 1), |
| } |
| if toRun == nil { |
| toRun = make(chan *test, maxTests) |
| go runTests() |
| } |
| select { |
| case toRun <- t: |
| default: |
| panic("toRun buffer size (maxTests) is too small") |
| } |
| return t |
| } |
| |
| // runTests runs tests in parallel, but respecting the order they |
| // were enqueued on the toRun channel. |
| func runTests() { |
| for { |
| ratec <- true |
| t := <-toRun |
| go func() { |
| t.run() |
| <-ratec |
| }() |
| } |
| } |
| |
| var cwd, _ = os.Getwd() |
| |
| func (t *test) goFileName() string { |
| return filepath.Join(t.dir, t.gofile) |
| } |
| |
| // run runs a test. |
| func (t *test) run() { |
| defer close(t.donec) |
| |
| srcBytes, err := ioutil.ReadFile(t.goFileName()) |
| if err != nil { |
| t.err = err |
| return |
| } |
| t.src = string(srcBytes) |
| if t.src[0] == '\n' { |
| t.err = skipError("starts with newline") |
| return |
| } |
| pos := strings.Index(t.src, "\n\n") |
| if pos == -1 { |
| t.err = errors.New("double newline not found") |
| return |
| } |
| action := t.src[:pos] |
| if strings.HasPrefix(action, "//") { |
| action = action[2:] |
| } |
| |
| var args []string |
| f := strings.Fields(action) |
| if len(f) > 0 { |
| action = f[0] |
| args = f[1:] |
| } |
| |
| switch action { |
| case "cmpout": |
| action = "run" // the run case already looks for <dir>/<test>.out files |
| fallthrough |
| case "compile", "build", "run", "errorcheck": |
| t.action = action |
| case "skip": |
| t.action = "skip" |
| return |
| default: |
| t.err = skipError("skipped; unknown pattern: " + action) |
| t.action = "??" |
| return |
| } |
| |
| t.makeTempDir() |
| defer os.RemoveAll(t.tempDir) |
| |
| err = ioutil.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0644) |
| check(err) |
| |
| // A few tests (of things like the environment) require these to be set. |
| os.Setenv("GOOS", runtime.GOOS) |
| os.Setenv("GOARCH", runtime.GOARCH) |
| |
| useTmp := true |
| runcmd := func(args ...string) ([]byte, error) { |
| cmd := exec.Command(args[0], args[1:]...) |
| var buf bytes.Buffer |
| cmd.Stdout = &buf |
| cmd.Stderr = &buf |
| if useTmp { |
| cmd.Dir = t.tempDir |
| } |
| err := cmd.Run() |
| return buf.Bytes(), err |
| } |
| |
| long := filepath.Join(cwd, t.goFileName()) |
| switch action { |
| default: |
| t.err = fmt.Errorf("unimplemented action %q", action) |
| |
| case "errorcheck": |
| out, _ := runcmd("go", "tool", gc, "-e", "-o", "a."+letter, long) |
| t.err = t.errorCheck(string(out), long, t.gofile) |
| return |
| |
| case "compile": |
| out, err := runcmd("go", "tool", gc, "-e", "-o", "a."+letter, long) |
| if err != nil { |
| t.err = fmt.Errorf("%s\n%s", err, out) |
| } |
| |
| case "build": |
| out, err := runcmd("go", "build", "-o", "a.exe", long) |
| if err != nil { |
| t.err = fmt.Errorf("%s\n%s", err, out) |
| } |
| |
| case "run": |
| useTmp = false |
| out, err := runcmd(append([]string{"go", "run", t.goFileName()}, args...)...) |
| if err != nil { |
| t.err = fmt.Errorf("%s\n%s", err, out) |
| } |
| if string(out) != t.expectedOutput() { |
| t.err = fmt.Errorf("incorrect output\n%s", out) |
| } |
| } |
| } |
| |
| func (t *test) String() string { |
| return filepath.Join(t.dir, t.gofile) |
| } |
| |
| func (t *test) makeTempDir() { |
| var err error |
| t.tempDir, err = ioutil.TempDir("", "") |
| check(err) |
| } |
| |
| func (t *test) expectedOutput() string { |
| filename := filepath.Join(t.dir, t.gofile) |
| filename = filename[:len(filename)-len(".go")] |
| filename += ".out" |
| b, _ := ioutil.ReadFile(filename) |
| return string(b) |
| } |
| |
| func (t *test) errorCheck(outStr string, full, short string) (err error) { |
| defer func() { |
| if *verbose && err != nil { |
| log.Printf("%s gc output:\n%s", t, outStr) |
| } |
| }() |
| var errs []error |
| |
| var out []string |
| // 6g 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. |
| for _, line := range strings.Split(outStr, "\n") { |
| if strings.HasPrefix(line, "\t") { |
| out[len(out)-1] += "\n" + line |
| } else { |
| out = append(out, line) |
| } |
| } |
| |
| // Cut directory name. |
| for i := range out { |
| out[i] = strings.Replace(out[i], full, short, -1) |
| } |
| |
| for _, we := range t.wantedErrors() { |
| var errmsgs []string |
| errmsgs, out = partitionStrings(we.filterRe, out) |
| if len(errmsgs) == 0 { |
| errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr)) |
| continue |
| } |
| matched := false |
| for _, errmsg := range errmsgs { |
| if we.re.MatchString(errmsg) { |
| matched = true |
| } else { |
| out = append(out, errmsg) |
| } |
| } |
| if !matched { |
| errs = append(errs, fmt.Errorf("%s:%d: no match for %q in%s", we.file, we.lineNum, we.reStr, strings.Join(out, "\n"))) |
| continue |
| } |
| } |
| |
| 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 partitionStrings(rx *regexp.Regexp, strs []string) (matched, unmatched []string) { |
| for _, s := range strs { |
| if rx.MatchString(s) { |
| matched = append(matched, s) |
| } else { |
| unmatched = append(unmatched, s) |
| } |
| } |
| return |
| } |
| |
| type wantedError struct { |
| reStr string |
| re *regexp.Regexp |
| lineNum int |
| file string |
| filterRe *regexp.Regexp // /^file:linenum\b/m |
| } |
| |
| var ( |
| errRx = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`) |
| errQuotesRx = regexp.MustCompile(`"([^"]*)"`) |
| lineRx = regexp.MustCompile(`LINE(([+-])([0-9]+))?`) |
| ) |
| |
| func (t *test) wantedErrors() (errs []wantedError) { |
| for i, line := range strings.Split(t.src, "\n") { |
| lineNum := i + 1 |
| if strings.Contains(line, "////") { |
| // double comment disables ERROR |
| continue |
| } |
| m := errRx.FindStringSubmatch(line) |
| if m == nil { |
| continue |
| } |
| all := m[1] |
| mm := errQuotesRx.FindAllStringSubmatch(all, -1) |
| if mm == nil { |
| log.Fatalf("invalid errchk line in %s: %s", t.goFileName(), line) |
| } |
| for _, m := range mm { |
| rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string { |
| 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", t.gofile, n) |
| }) |
| filterPattern := fmt.Sprintf(`^(\w+/)?%s:%d[:[]`, t.gofile, lineNum) |
| errs = append(errs, wantedError{ |
| reStr: rx, |
| re: regexp.MustCompile(rx), |
| filterRe: regexp.MustCompile(filterPattern), |
| lineNum: lineNum, |
| file: t.gofile, |
| }) |
| } |
| } |
| |
| return |
| } |