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 `))