github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/types2/check_test.go (about) 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 errors expected in the test files. 8 // 9 // Expected errors are indicated in the test files by putting comments 10 // of the form /* ERROR pattern */ or /* ERRORx pattern */ (or a similar 11 // //-style line comment) immediately following the tokens where errors 12 // are reported. There must be exactly one blank before and after the 13 // ERROR/ERRORx indicator, and the pattern must be a properly quoted Go 14 // string. 15 // 16 // The harness will verify that each ERROR pattern is a substring of the 17 // error reported at that source position, and that each ERRORx pattern 18 // is a regular expression matching the respective error. 19 // Consecutive comments may be used to indicate multiple errors reported 20 // at the same position. 21 // 22 // For instance, the following test source indicates that an "undeclared" 23 // error should be reported for the undeclared variable x: 24 // 25 // package p 26 // func f() { 27 // _ = x /* ERROR "undeclared" */ + 1 28 // } 29 30 package types2_test 31 32 import ( 33 "bytes" 34 "flag" 35 "fmt" 36 "os" 37 "path/filepath" 38 "reflect" 39 "regexp" 40 "runtime" 41 "strconv" 42 "strings" 43 "testing" 44 45 "github.com/go-asm/go/buildcfg" 46 "github.com/go-asm/go/cmd/compile/syntax" 47 "github.com/go-asm/go/testenv" 48 49 . "github.com/go-asm/go/cmd/compile/types2" 50 ) 51 52 var ( 53 haltOnError = flag.Bool("halt", false, "halt on error") 54 verifyErrors = flag.Bool("verify", false, "verify errors (rather than list them) in TestManual") 55 ) 56 57 func parseFiles(t *testing.T, filenames []string, srcs [][]byte, mode syntax.Mode) ([]*syntax.File, []error) { 58 var files []*syntax.File 59 var errlist []error 60 errh := func(err error) { errlist = append(errlist, err) } 61 for i, filename := range filenames { 62 base := syntax.NewFileBase(filename) 63 r := bytes.NewReader(srcs[i]) 64 file, err := syntax.Parse(base, r, errh, nil, mode) 65 if file == nil { 66 t.Fatalf("%s: %s", filename, err) 67 } 68 files = append(files, file) 69 } 70 return files, errlist 71 } 72 73 func unpackError(err error) (syntax.Pos, string) { 74 switch err := err.(type) { 75 case syntax.Error: 76 return err.Pos, err.Msg 77 case Error: 78 return err.Pos, err.Msg 79 default: 80 return nopos, err.Error() 81 } 82 } 83 84 // absDiff returns the absolute difference between x and y. 85 func absDiff(x, y uint) uint { 86 if x < y { 87 return y - x 88 } 89 return x - y 90 } 91 92 // parseFlags parses flags from the first line of the given source if the line 93 // starts with "//" (line comment) followed by "-" (possibly with spaces 94 // between). Otherwise the line is ignored. 95 func parseFlags(src []byte, flags *flag.FlagSet) error { 96 // we must have a line comment that starts with a "-" 97 const prefix = "//" 98 if !bytes.HasPrefix(src, []byte(prefix)) { 99 return nil // first line is not a line comment 100 } 101 src = src[len(prefix):] 102 if i := bytes.Index(src, []byte("-")); i < 0 || len(bytes.TrimSpace(src[:i])) != 0 { 103 return nil // comment doesn't start with a "-" 104 } 105 end := bytes.Index(src, []byte("\n")) 106 const maxLen = 256 107 if end < 0 || end > maxLen { 108 return fmt.Errorf("flags comment line too long") 109 } 110 111 return flags.Parse(strings.Fields(string(src[:end]))) 112 } 113 114 // testFiles type-checks the package consisting of the given files, and 115 // compares the resulting errors with the ERROR annotations in the source. 116 // Except for manual tests, each package is type-checked twice, once without 117 // use of Alias types, and once with Alias types. 118 // 119 // The srcs slice contains the file content for the files named in the 120 // filenames slice. The colDelta parameter specifies the tolerance for position 121 // mismatch when comparing errors. The manual parameter specifies whether this 122 // is a 'manual' test. 123 // 124 // If provided, opts may be used to mutate the Config before type-checking. 125 func testFiles(t *testing.T, filenames []string, srcs [][]byte, colDelta uint, manual bool, opts ...func(*Config)) { 126 // Alias types are disabled by default 127 testFilesImpl(t, filenames, srcs, colDelta, manual, opts...) 128 if !manual { 129 t.Setenv("GODEBUG", "gotypesalias=1") 130 testFilesImpl(t, filenames, srcs, colDelta, manual, opts...) 131 } 132 } 133 134 func testFilesImpl(t *testing.T, filenames []string, srcs [][]byte, colDelta uint, manual bool, opts ...func(*Config)) { 135 if len(filenames) == 0 { 136 t.Fatal("no source files") 137 } 138 139 // parse files 140 files, errlist := parseFiles(t, filenames, srcs, 0) 141 pkgName := "<no package>" 142 if len(files) > 0 { 143 pkgName = files[0].PkgName.Value 144 } 145 listErrors := manual && !*verifyErrors 146 if listErrors && len(errlist) > 0 { 147 t.Errorf("--- %s:", pkgName) 148 for _, err := range errlist { 149 t.Error(err) 150 } 151 } 152 153 // set up typechecker 154 var conf Config 155 conf.Trace = manual && testing.Verbose() 156 conf.Importer = defaultImporter() 157 conf.Error = func(err error) { 158 if *haltOnError { 159 defer panic(err) 160 } 161 if listErrors { 162 t.Error(err) 163 return 164 } 165 errlist = append(errlist, err) 166 } 167 168 // apply custom configuration 169 for _, opt := range opts { 170 opt(&conf) 171 } 172 173 // apply flag setting (overrides custom configuration) 174 var goexperiment, gotypesalias string 175 flags := flag.NewFlagSet("", flag.PanicOnError) 176 flags.StringVar(&conf.GoVersion, "lang", "", "") 177 flags.StringVar(&goexperiment, "goexperiment", "", "") 178 flags.BoolVar(&conf.FakeImportC, "fakeImportC", false, "") 179 flags.StringVar(&gotypesalias, "gotypesalias", "", "") 180 if err := parseFlags(srcs[0], flags); err != nil { 181 t.Fatal(err) 182 } 183 184 exp, err := buildcfg.ParseGOEXPERIMENT(runtime.GOOS, runtime.GOARCH, goexperiment) 185 if err != nil { 186 t.Fatal(err) 187 } 188 old := buildcfg.Experiment 189 defer func() { 190 buildcfg.Experiment = old 191 }() 192 buildcfg.Experiment = *exp 193 194 // By default, gotypesalias is not set. 195 if gotypesalias != "" { 196 t.Setenv("GODEBUG", "gotypesalias="+gotypesalias) 197 } 198 199 // Provide Config.Info with all maps so that info recording is tested. 200 info := Info{ 201 Types: make(map[syntax.Expr]TypeAndValue), 202 Instances: make(map[*syntax.Name]Instance), 203 Defs: make(map[*syntax.Name]Object), 204 Uses: make(map[*syntax.Name]Object), 205 Implicits: make(map[syntax.Node]Object), 206 Selections: make(map[*syntax.SelectorExpr]*Selection), 207 Scopes: make(map[syntax.Node]*Scope), 208 FileVersions: make(map[*syntax.PosBase]string), 209 } 210 211 // typecheck 212 conf.Check(pkgName, files, &info) 213 if listErrors { 214 return 215 } 216 217 // collect expected errors 218 errmap := make(map[string]map[uint][]syntax.Error) 219 for i, filename := range filenames { 220 if m := syntax.CommentMap(bytes.NewReader(srcs[i]), regexp.MustCompile("^ ERRORx? ")); len(m) > 0 { 221 errmap[filename] = m 222 } 223 } 224 225 // match against found errors 226 var indices []int // list indices of matching errors, reused for each error 227 for _, err := range errlist { 228 gotPos, gotMsg := unpackError(err) 229 230 // find list of errors for the respective error line 231 filename := gotPos.Base().Filename() 232 filemap := errmap[filename] 233 line := gotPos.Line() 234 var errList []syntax.Error 235 if filemap != nil { 236 errList = filemap[line] 237 } 238 239 // At least one of the errors in errList should match the current error. 240 indices = indices[:0] 241 for i, want := range errList { 242 pattern, substr := strings.CutPrefix(want.Msg, " ERROR ") 243 if !substr { 244 var found bool 245 pattern, found = strings.CutPrefix(want.Msg, " ERRORx ") 246 if !found { 247 panic("unreachable") 248 } 249 } 250 pattern, err := strconv.Unquote(strings.TrimSpace(pattern)) 251 if err != nil { 252 t.Errorf("%s:%d:%d: %v", filename, line, want.Pos.Col(), err) 253 continue 254 } 255 if substr { 256 if !strings.Contains(gotMsg, pattern) { 257 continue 258 } 259 } else { 260 rx, err := regexp.Compile(pattern) 261 if err != nil { 262 t.Errorf("%s:%d:%d: %v", filename, line, want.Pos.Col(), err) 263 continue 264 } 265 if !rx.MatchString(gotMsg) { 266 continue 267 } 268 } 269 indices = append(indices, i) 270 } 271 if len(indices) == 0 { 272 t.Errorf("%s: no error expected: %q", gotPos, gotMsg) 273 continue 274 } 275 // len(indices) > 0 276 277 // If there are multiple matching errors, select the one with the closest column position. 278 index := -1 // index of matching error 279 var delta uint 280 for _, i := range indices { 281 if d := absDiff(gotPos.Col(), errList[i].Pos.Col()); index < 0 || d < delta { 282 index, delta = i, d 283 } 284 } 285 286 // The closest column position must be within expected colDelta. 287 if delta > colDelta { 288 t.Errorf("%s: got col = %d; want %d", gotPos, gotPos.Col(), errList[index].Pos.Col()) 289 } 290 291 // eliminate from errList 292 if n := len(errList) - 1; n > 0 { 293 // not the last entry - slide entries down (don't reorder) 294 copy(errList[index:], errList[index+1:]) 295 filemap[line] = errList[:n] 296 } else { 297 // last entry - remove errList from filemap 298 delete(filemap, line) 299 } 300 301 // if filemap is empty, eliminate from errmap 302 if len(filemap) == 0 { 303 delete(errmap, filename) 304 } 305 } 306 307 // there should be no expected errors left 308 if len(errmap) > 0 { 309 t.Errorf("--- %s: unreported errors:", pkgName) 310 for filename, filemap := range errmap { 311 for line, errList := range filemap { 312 for _, err := range errList { 313 t.Errorf("%s:%d:%d: %s", filename, line, err.Pos.Col(), err.Msg) 314 } 315 } 316 } 317 } 318 } 319 320 // boolFieldAddr(conf, name) returns the address of the boolean field conf.<name>. 321 // For accessing unexported fields. 322 func boolFieldAddr(conf *Config, name string) *bool { 323 v := reflect.Indirect(reflect.ValueOf(conf)) 324 return (*bool)(v.FieldByName(name).Addr().UnsafePointer()) 325 } 326 327 // TestManual is for manual testing of a package - either provided 328 // as a list of filenames belonging to the package, or a directory 329 // name containing the package files - after the test arguments 330 // (and a separating "--"). For instance, to test the package made 331 // of the files foo.go and bar.go, use: 332 // 333 // go test -run Manual -- foo.go bar.go 334 // 335 // If no source arguments are provided, the file testdata/manual.go 336 // is used instead. 337 // Provide the -verify flag to verify errors against ERROR comments 338 // in the input files rather than having a list of errors reported. 339 // The accepted Go language version can be controlled with the -lang 340 // flag. 341 func TestManual(t *testing.T) { 342 testenv.MustHaveGoBuild(t) 343 344 filenames := flag.Args() 345 if len(filenames) == 0 { 346 filenames = []string{filepath.FromSlash("testdata/manual.go")} 347 } 348 349 info, err := os.Stat(filenames[0]) 350 if err != nil { 351 t.Fatalf("TestManual: %v", err) 352 } 353 354 DefPredeclaredTestFuncs() 355 if info.IsDir() { 356 if len(filenames) > 1 { 357 t.Fatal("TestManual: must have only one directory argument") 358 } 359 testDir(t, filenames[0], 0, true) 360 } else { 361 testPkg(t, filenames, 0, true) 362 } 363 } 364 365 func TestLongConstants(t *testing.T) { 366 format := `package longconst; const _ = %s /* ERROR "constant overflow" */; const _ = %s // ERROR "excessively long constant"` 367 src := fmt.Sprintf(format, strings.Repeat("1", 9999), strings.Repeat("1", 10001)) 368 testFiles(t, []string{"longconst.go"}, [][]byte{[]byte(src)}, 0, false) 369 } 370 371 func withSizes(sizes Sizes) func(*Config) { 372 return func(cfg *Config) { 373 cfg.Sizes = sizes 374 } 375 } 376 377 // TestIndexRepresentability tests that constant index operands must 378 // be representable as int even if they already have a type that can 379 // represent larger values. 380 func TestIndexRepresentability(t *testing.T) { 381 const src = `package index; var s []byte; var _ = s[int64 /* ERRORx "int64\\(1\\) << 40 \\(.*\\) overflows int" */ (1) << 40]` 382 testFiles(t, []string{"index.go"}, [][]byte{[]byte(src)}, 0, false, withSizes(&StdSizes{4, 4})) 383 } 384 385 func TestIssue47243_TypedRHS(t *testing.T) { 386 // The RHS of the shift expression below overflows uint on 32bit platforms, 387 // but this is OK as it is explicitly typed. 388 const src = `package issue47243; var a uint64; var _ = a << uint64(4294967296)` // uint64(1<<32) 389 testFiles(t, []string{"p.go"}, [][]byte{[]byte(src)}, 0, false, withSizes(&StdSizes{4, 4})) 390 } 391 392 func TestCheck(t *testing.T) { 393 old := buildcfg.Experiment.RangeFunc 394 defer func() { 395 buildcfg.Experiment.RangeFunc = old 396 }() 397 buildcfg.Experiment.RangeFunc = true 398 399 DefPredeclaredTestFuncs() 400 testDirFiles(t, "../../../../github.com/go-asm/go/types/testdata/check", 50, false) // TODO(gri) narrow column tolerance 401 } 402 func TestSpec(t *testing.T) { 403 testDirFiles(t, "../../../../github.com/go-asm/go/types/testdata/spec", 0, false) 404 } 405 func TestExamples(t *testing.T) { 406 testDirFiles(t, "../../../../github.com/go-asm/go/types/testdata/examples", 125, false) 407 } // TODO(gri) narrow column tolerance 408 func TestFixedbugs(t *testing.T) { 409 testDirFiles(t, "../../../../github.com/go-asm/go/types/testdata/fixedbugs", 100, false) 410 } // TODO(gri) narrow column tolerance 411 func TestLocal(t *testing.T) { testDirFiles(t, "testdata/local", 0, false) } 412 413 func testDirFiles(t *testing.T, dir string, colDelta uint, manual bool) { 414 testenv.MustHaveGoBuild(t) 415 dir = filepath.FromSlash(dir) 416 417 fis, err := os.ReadDir(dir) 418 if err != nil { 419 t.Error(err) 420 return 421 } 422 423 for _, fi := range fis { 424 path := filepath.Join(dir, fi.Name()) 425 426 // If fi is a directory, its files make up a single package. 427 if fi.IsDir() { 428 testDir(t, path, colDelta, manual) 429 } else { 430 t.Run(filepath.Base(path), func(t *testing.T) { 431 testPkg(t, []string{path}, colDelta, manual) 432 }) 433 } 434 } 435 } 436 437 func testDir(t *testing.T, dir string, colDelta uint, manual bool) { 438 fis, err := os.ReadDir(dir) 439 if err != nil { 440 t.Error(err) 441 return 442 } 443 444 var filenames []string 445 for _, fi := range fis { 446 filenames = append(filenames, filepath.Join(dir, fi.Name())) 447 } 448 449 t.Run(filepath.Base(dir), func(t *testing.T) { 450 testPkg(t, filenames, colDelta, manual) 451 }) 452 } 453 454 func testPkg(t *testing.T, filenames []string, colDelta uint, manual bool) { 455 srcs := make([][]byte, len(filenames)) 456 for i, filename := range filenames { 457 src, err := os.ReadFile(filename) 458 if err != nil { 459 t.Fatalf("could not read %s: %v", filename, err) 460 } 461 srcs[i] = src 462 } 463 testFiles(t, filenames, srcs, colDelta, manual) 464 }