blob: 67ff41371770dcb6466666c6fe0f1ffc4d238e9c [file] [log] [blame]
Brad Fitzpatrickce837b32012-02-21 14:28:49 +11001// #ignore
2
3// Copyright 2012 The Go Authors. All rights reserved.
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7// Run runs tests in the test directory.
8//
9// TODO(bradfitz): docs of some sort, once we figure out how we're changing
10// headers of files
11package main
12
13import (
14 "bytes"
15 "errors"
16 "flag"
17 "fmt"
18 "go/build"
19 "io/ioutil"
20 "log"
21 "os"
22 "os/exec"
23 "path/filepath"
24 "regexp"
25 "runtime"
26 "sort"
27 "strconv"
28 "strings"
29)
30
31var (
32 verbose = flag.Bool("v", false, "verbose. if set, parallelism is set to 1.")
33 numParallel = flag.Int("n", 8, "number of parallel tests to run")
34 summary = flag.Bool("summary", false, "show summary of results")
35)
36
37var (
38 // gc and ld are [568][gl].
39 gc, ld string
40
41 // letter is the build.ArchChar
42 letter string
43
44 // dirs are the directories to look for *.go files in.
45 // TODO(bradfitz): just use all directories?
46 dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs", "bugs"}
47
48 // ratec controls the max number of tests running at a time.
49 ratec chan bool
50
51 // toRun is the channel of tests to run.
52 // It is nil until the first test is started.
53 toRun chan *test
54)
55
56// maxTests is an upper bound on the total number of tests.
57// It is used as a channel buffer size to make sure sends don't block.
58const maxTests = 5000
59
60func main() {
61 flag.Parse()
62 if *verbose {
63 *numParallel = 1
64 }
65
66 ratec = make(chan bool, *numParallel)
67 var err error
68 letter, err = build.ArchChar(build.DefaultContext.GOARCH)
69 check(err)
70 gc = letter + "g"
71 ld = letter + "l"
72
73 var tests []*test
74 if flag.NArg() > 0 {
75 for _, arg := range flag.Args() {
76 if arg == "-" || arg == "--" {
77 // Permit running either:
78 // $ go run run.go - env.go
79 // $ go run run.go -- env.go
80 continue
81 }
82 if !strings.HasSuffix(arg, ".go") {
83 log.Fatalf("can't yet deal with non-go file %q", arg)
84 }
85 dir, file := filepath.Split(arg)
86 tests = append(tests, startTest(dir, file))
87 }
88 } else {
89 for _, dir := range dirs {
90 for _, baseGoFile := range goFiles(dir) {
91 tests = append(tests, startTest(dir, baseGoFile))
92 }
93 }
94 }
95
96 failed := false
97 resCount := map[string]int{}
98 for _, test := range tests {
99 <-test.donec
100 _, isSkip := test.err.(skipError)
101 if isSkip {
102 resCount["skip"]++
103 if !*verbose {
104 continue
105 }
106 }
107 errStr := "pass"
108 if test.err != nil {
109 errStr = test.err.Error()
110 if !isSkip {
111 failed = true
112 }
113 }
114 resCount[errStr]++
115 if !*verbose && test.err == nil {
116 continue
117 }
118 fmt.Printf("%-10s %-20s: %s\n", test.action, test.goFileName(), errStr)
119 }
120
121 if *summary {
122 for k, v := range resCount {
123 fmt.Printf("%5d %s\n", v, k)
124 }
125 }
126
127 if failed {
128 os.Exit(1)
129 }
130}
131
132func toolPath(name string) string {
133 p := filepath.Join(os.Getenv("GOROOT"), "bin", "tool", name)
134 if _, err := os.Stat(p); err != nil {
135 log.Fatalf("didn't find binary at %s", p)
136 }
137 return p
138}
139
140func goFiles(dir string) []string {
141 f, err := os.Open(dir)
142 check(err)
143 dirnames, err := f.Readdirnames(-1)
144 check(err)
145 names := []string{}
146 for _, name := range dirnames {
147 if strings.HasSuffix(name, ".go") {
148 names = append(names, name)
149 }
150 }
151 sort.Strings(names)
152 return names
153}
154
155// skipError describes why a test was skipped.
156type skipError string
157
158func (s skipError) Error() string { return string(s) }
159
160func check(err error) {
161 if err != nil {
162 log.Fatal(err)
163 }
164}
165
166// test holds the state of a test.
167type test struct {
168 dir, gofile string
169 donec chan bool // closed when done
170
171 src string
172 action string // "compile", "build", "run", "errorcheck"
173
174 tempDir string
175 err error
176}
177
178// startTest
179func startTest(dir, gofile string) *test {
180 t := &test{
181 dir: dir,
182 gofile: gofile,
183 donec: make(chan bool, 1),
184 }
185 if toRun == nil {
186 toRun = make(chan *test, maxTests)
187 go runTests()
188 }
189 select {
190 case toRun <- t:
191 default:
192 panic("toRun buffer size (maxTests) is too small")
193 }
194 return t
195}
196
197// runTests runs tests in parallel, but respecting the order they
198// were enqueued on the toRun channel.
199func runTests() {
200 for {
201 ratec <- true
202 t := <-toRun
203 go func() {
204 t.run()
205 <-ratec
206 }()
207 }
208}
209
210func (t *test) goFileName() string {
211 return filepath.Join(t.dir, t.gofile)
212}
213
214// run runs a test.
215func (t *test) run() {
216 defer close(t.donec)
217
218 srcBytes, err := ioutil.ReadFile(t.goFileName())
219 if err != nil {
220 t.err = err
221 return
222 }
223 t.src = string(srcBytes)
224 if t.src[0] == '\n' {
225 t.err = skipError("starts with newline")
226 return
227 }
228 pos := strings.Index(t.src, "\n\n")
229 if pos == -1 {
230 t.err = errors.New("double newline not found")
231 return
232 }
233 action := t.src[:pos]
234 if strings.HasPrefix(action, "//") {
235 action = action[2:]
236 }
237 action = strings.TrimSpace(action)
238
239 switch action {
240 case "compile", "build", "run", "errorcheck":
241 t.action = action
242 default:
243 t.err = skipError("skipped; unknown pattern: " + action)
244 t.action = "??"
245 return
246 }
247
248 t.makeTempDir()
249 defer os.RemoveAll(t.tempDir)
250
251 err = ioutil.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0644)
252 check(err)
253
254 cmd := exec.Command("go", "tool", gc, "-e", "-o", "a."+letter, t.gofile)
255 var buf bytes.Buffer
256 cmd.Stdout = &buf
257 cmd.Stderr = &buf
258 cmd.Dir = t.tempDir
259 err = cmd.Run()
260 out := buf.String()
261
262 if action == "errorcheck" {
263 t.err = t.errorCheck(out)
264 return
265 }
266
267 if err != nil {
268 t.err = fmt.Errorf("build = %v (%q)", err, out)
269 return
270 }
271
272 if action == "compile" {
273 return
274 }
275
276 if action == "build" || action == "run" {
277 buf.Reset()
278 cmd = exec.Command("go", "tool", ld, "-o", "a.out", "a."+letter)
279 cmd.Stdout = &buf
280 cmd.Stderr = &buf
281 cmd.Dir = t.tempDir
282 err = cmd.Run()
283 out = buf.String()
284 if err != nil {
285 t.err = fmt.Errorf("link = %v (%q)", err, out)
286 return
287 }
288 if action == "build" {
289 return
290 }
291 }
292
293 if action == "run" {
294 buf.Reset()
295 cmd = exec.Command(filepath.Join(t.tempDir, "a.out"))
296 cmd.Stdout = &buf
297 cmd.Stderr = &buf
298 cmd.Dir = t.tempDir
299 cmd.Env = append(cmd.Env, "GOARCH="+runtime.GOARCH)
300 err = cmd.Run()
301 out = buf.String()
302 if err != nil {
303 t.err = fmt.Errorf("run = %v (%q)", err, out)
304 return
305 }
306
307 if out != t.expectedOutput() {
308 t.err = fmt.Errorf("output differs; got:\n%s", out)
309 }
310 return
311 }
312
313 t.err = fmt.Errorf("unimplemented action %q", action)
314}
315
316func (t *test) String() string {
317 return filepath.Join(t.dir, t.gofile)
318}
319
320func (t *test) makeTempDir() {
321 var err error
322 t.tempDir, err = ioutil.TempDir("", "")
323 check(err)
324}
325
326func (t *test) expectedOutput() string {
327 filename := filepath.Join(t.dir, t.gofile)
328 filename = filename[:len(filename)-len(".go")]
329 filename += ".out"
330 b, _ := ioutil.ReadFile(filename)
331 return string(b)
332}
333
334func (t *test) errorCheck(outStr string) (err error) {
335 defer func() {
336 if *verbose && err != nil {
337 log.Printf("%s gc output:\n%s", t, outStr)
338 }
339 }()
340 var errs []error
341
342 var out []string
343 // 6g error messages continue onto additional lines with leading tabs.
344 // Split the output at the beginning of each line that doesn't begin with a tab.
345 for _, line := range strings.Split(outStr, "\n") {
346 if strings.HasPrefix(line, "\t") {
347 out[len(out)-1] += "\n" + line
348 } else {
349 out = append(out, line)
350 }
351 }
352
353 for _, we := range t.wantedErrors() {
354 var errmsgs []string
355 errmsgs, out = partitionStrings(we.filterRe, out)
356 if len(errmsgs) == 0 {
357 errs = append(errs, fmt.Errorf("errchk: %s:%d: missing expected error: %s", we.file, we.lineNum, we.reStr))
358 continue
359 }
360 matched := false
361 for _, errmsg := range errmsgs {
362 if we.re.MatchString(errmsg) {
363 matched = true
364 } else {
365 out = append(out, errmsg)
366 }
367 }
368 if !matched {
369 errs = append(errs, fmt.Errorf("errchk: %s:%d: error(s) on line didn't match pattern: %s", we.file, we.lineNum, we.reStr))
370 continue
371 }
372 }
373
374 if len(errs) == 0 {
375 return nil
376 }
377 if len(errs) == 1 {
378 return errs[0]
379 }
380 var buf bytes.Buffer
381 buf.WriteString("Multiple errors:\n")
382 for _, err := range errs {
383 fmt.Fprintf(&buf, "%s\n", err.Error())
384 }
385 return errors.New(buf.String())
386
387}
388
389func partitionStrings(rx *regexp.Regexp, strs []string) (matched, unmatched []string) {
390 for _, s := range strs {
391 if rx.MatchString(s) {
392 matched = append(matched, s)
393 } else {
394 unmatched = append(unmatched, s)
395 }
396 }
397 return
398}
399
400type wantedError struct {
401 reStr string
402 re *regexp.Regexp
403 lineNum int
404 file string
405 filterRe *regexp.Regexp // /^file:linenum\b/m
406}
407
408var (
409 errRx = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`)
410 errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
411 lineRx = regexp.MustCompile(`LINE(([+-])([0-9]+))?`)
412)
413
414func (t *test) wantedErrors() (errs []wantedError) {
415 for i, line := range strings.Split(t.src, "\n") {
416 lineNum := i + 1
417 if strings.Contains(line, "////") {
418 // double comment disables ERROR
419 continue
420 }
421 m := errRx.FindStringSubmatch(line)
422 if m == nil {
423 continue
424 }
425 all := m[1]
426 mm := errQuotesRx.FindAllStringSubmatch(all, -1)
427 if mm == nil {
428 log.Fatalf("invalid errchk line in %s: %s", t.goFileName(), line)
429 }
430 for _, m := range mm {
431 rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
432 n := lineNum
433 if strings.HasPrefix(m, "LINE+") {
434 delta, _ := strconv.Atoi(m[5:])
435 n += delta
436 } else if strings.HasPrefix(m, "LINE-") {
437 delta, _ := strconv.Atoi(m[5:])
438 n -= delta
439 }
440 return fmt.Sprintf("%s:%d", t.gofile, n)
441 })
442 filterPattern := fmt.Sprintf(`^(\w+/)?%s:%d[:[]`, t.gofile, lineNum)
443 errs = append(errs, wantedError{
444 reStr: rx,
445 re: regexp.MustCompile(rx),
446 filterRe: regexp.MustCompile(filterPattern),
447 lineNum: lineNum,
448 file: t.gofile,
449 })
450 }
451 }
452
453 return
454}