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

View as plain text