github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/internal/testmain/testmain.go (about)

     1  package testmain
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"go/ast"
     8  	gobuild "go/build"
     9  	"go/doc"
    10  	"go/parser"
    11  	"go/token"
    12  	"path"
    13  	"sort"
    14  	"strings"
    15  	"text/template"
    16  	"unicode"
    17  	"unicode/utf8"
    18  
    19  	"github.com/gopherjs/gopherjs/build"
    20  	"golang.org/x/tools/go/buildutil"
    21  )
    22  
    23  // FuncLocation describes whether a test function is in-package or external
    24  // (i.e. in the xxx_test package).
    25  type FuncLocation uint8
    26  
    27  const (
    28  	// LocUnknown is the default, invalid value of the PkgType.
    29  	LocUnknown FuncLocation = iota
    30  	// LocInPackage is an in-package test.
    31  	LocInPackage
    32  	// LocExternal is an external test (i.e. in the xxx_test package).
    33  	LocExternal
    34  )
    35  
    36  func (tl FuncLocation) String() string {
    37  	switch tl {
    38  	case LocInPackage:
    39  		return "_test"
    40  	case LocExternal:
    41  		return "_xtest"
    42  	default:
    43  		return "<unknown>"
    44  	}
    45  }
    46  
    47  // TestFunc describes a single test/benchmark/fuzz function in a package.
    48  type TestFunc struct {
    49  	Location FuncLocation // Where the function is defined.
    50  	Name     string       // Function name.
    51  }
    52  
    53  // ExampleFunc describes an example.
    54  type ExampleFunc struct {
    55  	Location    FuncLocation // Where the function is defined.
    56  	Name        string       // Function name.
    57  	Output      string       // Expected output.
    58  	Unordered   bool         // Output is allowed to be unordered.
    59  	EmptyOutput bool         // Whether the output is expected to be empty.
    60  }
    61  
    62  // Executable returns true if the example function should be executed with tests.
    63  func (ef ExampleFunc) Executable() bool {
    64  	return ef.EmptyOutput || ef.Output != ""
    65  }
    66  
    67  // TestMain is a helper type responsible for generation of the test main package.
    68  type TestMain struct {
    69  	Package    *build.PackageData
    70  	Tests      []TestFunc
    71  	Benchmarks []TestFunc
    72  	Fuzz       []TestFunc
    73  	Examples   []ExampleFunc
    74  	TestMain   *TestFunc
    75  }
    76  
    77  // Scan package for tests functions.
    78  func (tm *TestMain) Scan(fset *token.FileSet) error {
    79  	if err := tm.scanPkg(fset, tm.Package.TestGoFiles, LocInPackage); err != nil {
    80  		return err
    81  	}
    82  	if err := tm.scanPkg(fset, tm.Package.XTestGoFiles, LocExternal); err != nil {
    83  		return err
    84  	}
    85  	return nil
    86  }
    87  
    88  func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error {
    89  	for _, name := range files {
    90  		srcPath := path.Join(tm.Package.Dir, name)
    91  		f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath)
    92  		if err != nil {
    93  			return fmt.Errorf("failed to open source file %q: %w", srcPath, err)
    94  		}
    95  		defer f.Close()
    96  		parsed, err := parser.ParseFile(fset, srcPath, f, parser.ParseComments)
    97  		if err != nil {
    98  			return fmt.Errorf("failed to parse %q: %w", srcPath, err)
    99  		}
   100  
   101  		if err := tm.scanFile(parsed, loc); err != nil {
   102  			return err
   103  		}
   104  	}
   105  	return nil
   106  }
   107  
   108  func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error {
   109  	for _, d := range f.Decls {
   110  		n, ok := d.(*ast.FuncDecl)
   111  		if !ok {
   112  			continue
   113  		}
   114  		if n.Recv != nil {
   115  			continue
   116  		}
   117  		name := n.Name.String()
   118  		switch {
   119  		case isTestMain(n):
   120  			if tm.TestMain != nil {
   121  				return errors.New("multiple definitions of TestMain")
   122  			}
   123  			tm.TestMain = &TestFunc{
   124  				Location: loc,
   125  				Name:     name,
   126  			}
   127  		case isTest(name, "Test"):
   128  			tm.Tests = append(tm.Tests, TestFunc{
   129  				Location: loc,
   130  				Name:     name,
   131  			})
   132  		case isTest(name, "Benchmark"):
   133  			tm.Benchmarks = append(tm.Benchmarks, TestFunc{
   134  				Location: loc,
   135  				Name:     name,
   136  			})
   137  		case isTest(name, "Fuzz"):
   138  			tm.Fuzz = append(tm.Fuzz, TestFunc{
   139  				Location: loc,
   140  				Name:     name,
   141  			})
   142  		}
   143  	}
   144  
   145  	ex := doc.Examples(f)
   146  	sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order })
   147  	for _, e := range ex {
   148  		tm.Examples = append(tm.Examples, ExampleFunc{
   149  			Location:    loc,
   150  			Name:        "Example" + e.Name,
   151  			Output:      e.Output,
   152  			Unordered:   e.Unordered,
   153  			EmptyOutput: e.EmptyOutput,
   154  		})
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // Synthesize main package for the tests.
   161  func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) {
   162  	buf := &bytes.Buffer{}
   163  	if err := testmainTmpl.Execute(buf, tm); err != nil {
   164  		return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err)
   165  	}
   166  	src, err := parser.ParseFile(fset, "_testmain.go", buf, 0)
   167  	if err != nil {
   168  		return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err)
   169  	}
   170  	pkg := &build.PackageData{
   171  		Package: &gobuild.Package{
   172  			ImportPath: tm.Package.ImportPath + ".testmain",
   173  			Name:       "main",
   174  			GoFiles:    []string{"_testmain.go"},
   175  		},
   176  	}
   177  	return pkg, src, nil
   178  }
   179  
   180  func (tm *TestMain) hasTests(loc FuncLocation, executableOnly bool) bool {
   181  	if tm.TestMain != nil && tm.TestMain.Location == loc {
   182  		return true
   183  	}
   184  	// Tests, Benchmarks and Fuzz targets are always executable.
   185  	all := []TestFunc{}
   186  	all = append(all, tm.Tests...)
   187  	all = append(all, tm.Benchmarks...)
   188  
   189  	for _, t := range all {
   190  		if t.Location == loc {
   191  			return true
   192  		}
   193  	}
   194  
   195  	for _, e := range tm.Examples {
   196  		if e.Location == loc && (e.Executable() || !executableOnly) {
   197  			return true
   198  		}
   199  	}
   200  	return false
   201  }
   202  
   203  // ImportTest returns true if in-package test package needs to be imported.
   204  func (tm *TestMain) ImportTest() bool { return tm.hasTests(LocInPackage, false) }
   205  
   206  // ImportXTest returns true if external test package needs to be imported.
   207  func (tm *TestMain) ImportXTest() bool { return tm.hasTests(LocExternal, false) }
   208  
   209  // ExecutesTest returns true if in-package test package has executable tests.
   210  func (tm *TestMain) ExecutesTest() bool { return tm.hasTests(LocInPackage, true) }
   211  
   212  // ExecutesXTest returns true if external package test package has executable tests.
   213  func (tm *TestMain) ExecutesXTest() bool { return tm.hasTests(LocExternal, true) }
   214  
   215  // isTestMain tells whether fn is a TestMain(m *testing.M) function.
   216  func isTestMain(fn *ast.FuncDecl) bool {
   217  	if fn.Name.String() != "TestMain" ||
   218  		fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
   219  		fn.Type.Params == nil ||
   220  		len(fn.Type.Params.List) != 1 ||
   221  		len(fn.Type.Params.List[0].Names) > 1 {
   222  		return false
   223  	}
   224  	ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
   225  	if !ok {
   226  		return false
   227  	}
   228  	// We can't easily check that the type is *testing.M
   229  	// because we don't know how testing has been imported,
   230  	// but at least check that it's *M or *something.M.
   231  	if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" {
   232  		return true
   233  	}
   234  	if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" {
   235  		return true
   236  	}
   237  	return false
   238  }
   239  
   240  // isTest tells whether name looks like a test (or benchmark, according to prefix).
   241  // It is a Test (say) if there is a character after Test that is not a lower-case letter.
   242  // We don't want TesticularCancer.
   243  func isTest(name, prefix string) bool {
   244  	if !strings.HasPrefix(name, prefix) {
   245  		return false
   246  	}
   247  	if len(name) == len(prefix) { // "Test" is ok
   248  		return true
   249  	}
   250  	rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
   251  	return !unicode.IsLower(rune)
   252  }
   253  
   254  var testmainTmpl = template.Must(template.New("main").Parse(`
   255  package main
   256  
   257  import (
   258  {{if not .TestMain}}
   259  	"os"
   260  {{end}}
   261  	"testing"
   262  	"testing/internal/testdeps"
   263  
   264  {{if .ImportTest}}
   265  	{{if .ExecutesTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
   266  {{end -}}
   267  {{- if .ImportXTest -}}
   268  	{{if .ExecutesXTest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
   269  {{end}}
   270  )
   271  
   272  var tests = []testing.InternalTest{
   273  {{- range .Tests}}
   274  	{"{{.Name}}", {{.Location}}.{{.Name}}},
   275  {{- end}}
   276  }
   277  
   278  var benchmarks = []testing.InternalBenchmark{
   279  {{- range .Benchmarks}}
   280  	{"{{.Name}}", {{.Location}}.{{.Name}}},
   281  {{- end}}
   282  }
   283  
   284  var fuzzTargets = []testing.InternalFuzzTarget{
   285  {{- range .Fuzz}}
   286  	{"{{.Name}}", {{.Location}}.{{.Name}}},
   287  {{- end}}
   288  }
   289  
   290  var examples = []testing.InternalExample{
   291  {{- range .Examples }}
   292  {{- if .Executable }}
   293  	{"{{.Name}}", {{.Location}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
   294  {{- end }}
   295  {{- end }}
   296  }
   297  
   298  func main() {
   299  	m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
   300  {{with .TestMain}}
   301  	{{.Location}}.{{.Name}}(m)
   302  {{else}}
   303  	os.Exit(m.Run())
   304  {{end -}}
   305  }
   306  
   307  `))