]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/compile/internal/types2/check_test.go
cmd/compile/internal/types2: make TestManual work for directories
[gostls13.git] / src / cmd / compile / internal / types2 / check_test.go
1 // Copyright 2011 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
5 // This file implements a typechecker test harness. The packages specified
6 // in tests are typechecked. Error messages reported by the typechecker are
7 // compared against the error messages expected in the test files.
8 //
9 // Expected errors are indicated in the test files by putting a comment
10 // of the form /* ERROR "rx" */ immediately following an offending token.
11 // The harness will verify that an error matching the regular expression
12 // rx is reported at that source position. Consecutive comments may be
13 // used to indicate multiple errors for the same token position.
14 //
15 // For instance, the following test file indicates that a "not declared"
16 // error should be reported for the undeclared variable x:
17 //
18 //      package p
19 //      func f() {
20 //              _ = x /* ERROR "not declared" */ + 1
21 //      }
22
23 // TODO(gri) Also collect strict mode errors of the form /* STRICT ... */
24 //           and test against strict mode.
25
26 package types2_test
27
28 import (
29         "cmd/compile/internal/syntax"
30         "flag"
31         "internal/testenv"
32         "os"
33         "path/filepath"
34         "regexp"
35         "sort"
36         "strings"
37         "testing"
38
39         . "cmd/compile/internal/types2"
40 )
41
42 var (
43         haltOnError  = flag.Bool("halt", false, "halt on error")
44         verifyErrors = flag.Bool("verify", false, "verify errors (rather than list them) in TestManual")
45         goVersion    = flag.String("lang", "", "Go language version (e.g. \"go1.12\")")
46 )
47
48 func parseFiles(t *testing.T, filenames []string, mode syntax.Mode) ([]*syntax.File, []error) {
49         var files []*syntax.File
50         var errlist []error
51         errh := func(err error) { errlist = append(errlist, err) }
52         for _, filename := range filenames {
53                 file, err := syntax.ParseFile(filename, errh, nil, mode)
54                 if file == nil {
55                         t.Fatalf("%s: %s", filename, err)
56                 }
57                 files = append(files, file)
58         }
59         return files, errlist
60 }
61
62 func unpackError(err error) syntax.Error {
63         switch err := err.(type) {
64         case syntax.Error:
65                 return err
66         case Error:
67                 return syntax.Error{Pos: err.Pos, Msg: err.Msg}
68         default:
69                 return syntax.Error{Msg: err.Error()}
70         }
71 }
72
73 // delta returns the absolute difference between x and y.
74 func delta(x, y uint) uint {
75         switch {
76         case x < y:
77                 return y - x
78         case x > y:
79                 return x - y
80         default:
81                 return 0
82         }
83 }
84
85 // goVersionRx matches a Go version string using '_', e.g. "go1_12".
86 var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*_(0|[1-9][0-9]*)$`)
87
88 // asGoVersion returns a regular Go language version string
89 // if s is a Go version string using '_' rather than '.' to
90 // separate the major and minor version numbers (e.g. "go1_12").
91 // Otherwise it returns the empty string.
92 func asGoVersion(s string) string {
93         if goVersionRx.MatchString(s) {
94                 return strings.Replace(s, "_", ".", 1)
95         }
96         return ""
97 }
98
99 func testFiles(t *testing.T, filenames []string, colDelta uint, manual bool) {
100         if len(filenames) == 0 {
101                 t.Fatal("no source files")
102         }
103
104         var mode syntax.Mode
105         if strings.HasSuffix(filenames[0], ".go2") {
106                 mode |= syntax.AllowGenerics
107         }
108         // parse files and collect parser errors
109         files, errlist := parseFiles(t, filenames, mode)
110
111         pkgName := "<no package>"
112         if len(files) > 0 {
113                 pkgName = files[0].PkgName.Value
114         }
115
116         // if no Go version is given, consider the package name
117         goVersion := *goVersion
118         if goVersion == "" {
119                 goVersion = asGoVersion(pkgName)
120         }
121
122         listErrors := manual && !*verifyErrors
123         if listErrors && len(errlist) > 0 {
124                 t.Errorf("--- %s:", pkgName)
125                 for _, err := range errlist {
126                         t.Error(err)
127                 }
128         }
129
130         // typecheck and collect typechecker errors
131         var conf Config
132         conf.GoVersion = goVersion
133         // special case for importC.src
134         if len(filenames) == 1 && strings.HasSuffix(filenames[0], "importC.src") {
135                 conf.FakeImportC = true
136         }
137         conf.Trace = manual && testing.Verbose()
138         conf.Importer = defaultImporter()
139         conf.Error = func(err error) {
140                 if *haltOnError {
141                         defer panic(err)
142                 }
143                 if listErrors {
144                         t.Error(err)
145                         return
146                 }
147                 errlist = append(errlist, err)
148         }
149         conf.Check(pkgName, files, nil)
150
151         if listErrors {
152                 return
153         }
154
155         // sort errlist in source order
156         sort.Slice(errlist, func(i, j int) bool {
157                 pi := unpackError(errlist[i]).Pos
158                 pj := unpackError(errlist[j]).Pos
159                 return pi.Cmp(pj) < 0
160         })
161
162         // collect expected errors
163         errmap := make(map[string]map[uint][]syntax.Error)
164         for _, filename := range filenames {
165                 f, err := os.Open(filename)
166                 if err != nil {
167                         t.Error(err)
168                         continue
169                 }
170                 if m := syntax.ErrorMap(f); len(m) > 0 {
171                         errmap[filename] = m
172                 }
173                 f.Close()
174         }
175
176         // match against found errors
177         for _, err := range errlist {
178                 got := unpackError(err)
179
180                 // find list of errors for the respective error line
181                 filename := got.Pos.Base().Filename()
182                 filemap := errmap[filename]
183                 line := got.Pos.Line()
184                 var list []syntax.Error
185                 if filemap != nil {
186                         list = filemap[line]
187                 }
188                 // list may be nil
189
190                 // one of errors in list should match the current error
191                 index := -1 // list index of matching message, if any
192                 for i, want := range list {
193                         rx, err := regexp.Compile(want.Msg)
194                         if err != nil {
195                                 t.Errorf("%s:%d:%d: %v", filename, line, want.Pos.Col(), err)
196                                 continue
197                         }
198                         if rx.MatchString(got.Msg) {
199                                 index = i
200                                 break
201                         }
202                 }
203                 if index < 0 {
204                         t.Errorf("%s: no error expected: %q", got.Pos, got.Msg)
205                         continue
206                 }
207
208                 // column position must be within expected colDelta
209                 want := list[index]
210                 if delta(got.Pos.Col(), want.Pos.Col()) > colDelta {
211                         t.Errorf("%s: got col = %d; want %d", got.Pos, got.Pos.Col(), want.Pos.Col())
212                 }
213
214                 // eliminate from list
215                 if n := len(list) - 1; n > 0 {
216                         // not the last entry - slide entries down (don't reorder)
217                         copy(list[index:], list[index+1:])
218                         filemap[line] = list[:n]
219                 } else {
220                         // last entry - remove list from filemap
221                         delete(filemap, line)
222                 }
223
224                 // if filemap is empty, eliminate from errmap
225                 if len(filemap) == 0 {
226                         delete(errmap, filename)
227                 }
228         }
229
230         // there should be no expected errors left
231         if len(errmap) > 0 {
232                 t.Errorf("--- %s: unreported errors:", pkgName)
233                 for filename, filemap := range errmap {
234                         for line, list := range filemap {
235                                 for _, err := range list {
236                                         t.Errorf("%s:%d:%d: %s", filename, line, err.Pos.Col(), err.Msg)
237                                 }
238                         }
239                 }
240         }
241 }
242
243 // TestManual is for manual testing of a package - either provided
244 // as a list of filenames belonging to the package, or a directory
245 // name containing the package files - after the test arguments
246 // (and a separating "--"). For instance, to test the package made
247 // of the files foo.go and bar.go, use:
248 //
249 //      go test -run Manual -- foo.go bar.go
250 //
251 // If no source arguments are provided, the file testdata/manual.go2
252 // is used instead.
253 // Provide the -verify flag to verify errors against ERROR comments
254 // in the input files rather than having a list of errors reported.
255 // The accepted Go language version can be controlled with the -lang
256 // flag.
257 func TestManual(t *testing.T) {
258         testenv.MustHaveGoBuild(t)
259
260         filenames := flag.Args()
261         if len(filenames) == 0 {
262                 filenames = []string{filepath.FromSlash("testdata/manual.go2")}
263         }
264
265         info, err := os.Stat(filenames[0])
266         if err != nil {
267                 t.Fatalf("TestManual: %v", err)
268         }
269
270         DefPredeclaredTestFuncs()
271         if info.IsDir() {
272                 if len(filenames) > 1 {
273                         t.Fatal("TestManual: must have only one directory argument")
274                 }
275                 testDir(t, filenames[0], 0, true)
276         } else {
277                 testFiles(t, filenames, 0, true)
278         }
279 }
280
281 // TODO(gri) go/types has extra TestLongConstants and TestIndexRepresentability tests
282
283 func TestCheck(t *testing.T)     { DefPredeclaredTestFuncs(); testDirFiles(t, "testdata/check", 75, false) } // TODO(gri) narrow column tolerance
284 func TestExamples(t *testing.T)  { testDirFiles(t, "testdata/examples", 0, false) }
285 func TestFixedbugs(t *testing.T) { testDirFiles(t, "testdata/fixedbugs", 0, false) }
286
287 func testDirFiles(t *testing.T, dir string, colDelta uint, manual bool) {
288         testenv.MustHaveGoBuild(t)
289         dir = filepath.FromSlash(dir)
290
291         fis, err := os.ReadDir(dir)
292         if err != nil {
293                 t.Error(err)
294                 return
295         }
296
297         for _, fi := range fis {
298                 path := filepath.Join(dir, fi.Name())
299
300                 // If fi is a directory, its files make up a single package.
301                 if fi.IsDir() {
302                         testDir(t, path, colDelta, manual)
303                 } else {
304                         t.Run(filepath.Base(path), func(t *testing.T) {
305                                 testFiles(t, []string{path}, colDelta, manual)
306                         })
307                 }
308         }
309 }
310
311 func testDir(t *testing.T, dir string, colDelta uint, manual bool) {
312         fis, err := os.ReadDir(dir)
313         if err != nil {
314                 t.Error(err)
315                 return
316         }
317
318         var filenames []string
319         for _, fi := range fis {
320                 filenames = append(filenames, filepath.Join(dir, fi.Name()))
321         }
322
323         t.Run(filepath.Base(dir), func(t *testing.T) {
324                 testFiles(t, filenames, colDelta, manual)
325         })
326 }