Skip to content

Commit 4845bda

Browse files
committed
Refactor test functions finding into a separate package and add tests.
This should de-clutter tool.go a bit and help with maintainability in future.
1 parent 405ff02 commit 4845bda

File tree

8 files changed

+558
-204
lines changed

8 files changed

+558
-204
lines changed

build/build.go

+2
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ func (p *PackageData) InternalBuildContext() *build.Context {
364364
func (p *PackageData) TestPackage() *PackageData {
365365
return &PackageData{
366366
Package: &build.Package{
367+
Name: p.Name,
367368
ImportPath: p.ImportPath,
368369
Dir: p.Dir,
369370
GoFiles: append(p.GoFiles, p.TestGoFiles...),
@@ -379,6 +380,7 @@ func (p *PackageData) TestPackage() *PackageData {
379380
func (p *PackageData) XTestPackage() *PackageData {
380381
return &PackageData{
381382
Package: &build.Package{
383+
Name: p.Name + "_test",
382384
ImportPath: p.ImportPath + "_test",
383385
Dir: p.Dir,
384386
GoFiles: p.XTestGoFiles,

internal/srctesting/srctesting.go

+16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
package srctesting
44

55
import (
6+
"bytes"
67
"go/ast"
8+
"go/format"
79
"go/parser"
810
"go/token"
911
"go/types"
@@ -63,3 +65,17 @@ func ParseFuncDecl(t *testing.T, src string) *ast.FuncDecl {
6365
}
6466
return fdecl
6567
}
68+
69+
// Format AST node into a string.
70+
//
71+
// The node type must be *ast.File, *printer.CommentedNode, []ast.Decl,
72+
// []ast.Stmt, or assignment-compatible to ast.Expr, ast.Decl, ast.Spec, or
73+
// ast.Stmt.
74+
func Format(t *testing.T, fset *token.FileSet, node any) string {
75+
t.Helper()
76+
buf := &bytes.Buffer{}
77+
if err := format.Node(buf, fset, node); err != nil {
78+
t.Fatalf("Failed to format AST node %T: %s", node, err)
79+
}
80+
return buf.String()
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package testpkg_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestYyy(t *testing.T) {}
9+
10+
func BenchmarkYyy(b *testing.B) {}
11+
12+
func FuzzYyy(f *testing.F) { f.Skip() }
13+
14+
func ExampleYyy() {
15+
fmt.Println("hello") // Output: hello
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testpkg
2+
3+
import "testing"
4+
5+
func TestXxx(t *testing.T) {}
6+
7+
func BenchmarkXxx(b *testing.B) {}
8+
9+
func FuzzXxx(f *testing.F) { f.Skip() }
10+
11+
func ExampleXxx() {}
12+
13+
func TestMain(m *testing.M) { m.Run() }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package testpkg
2+
3+
// Xxx is an sample function.
4+
func Xxx() {}

internal/testmain/testmain.go

+298
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
Examples []ExampleFunc
73+
TestMain *TestFunc
74+
}
75+
76+
// Scan package for tests functions.
77+
func (tm *TestMain) Scan(fset *token.FileSet) error {
78+
if err := tm.scanPkg(fset, tm.Package.TestGoFiles, LocInPackage); err != nil {
79+
return err
80+
}
81+
if err := tm.scanPkg(fset, tm.Package.XTestGoFiles, LocExternal); err != nil {
82+
return err
83+
}
84+
return nil
85+
}
86+
87+
func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error {
88+
for _, name := range files {
89+
srcPath := path.Join(tm.Package.Dir, name)
90+
f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath)
91+
if err != nil {
92+
return fmt.Errorf("failed to open source file %q: %w", srcPath, err)
93+
}
94+
defer f.Close()
95+
parsed, err := parser.ParseFile(fset, srcPath, f, parser.ParseComments)
96+
if err != nil {
97+
return fmt.Errorf("failed to parse %q: %w", srcPath, err)
98+
}
99+
100+
if err := tm.scanFile(parsed, loc); err != nil {
101+
return err
102+
}
103+
}
104+
return nil
105+
}
106+
107+
func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error {
108+
for _, d := range f.Decls {
109+
n, ok := d.(*ast.FuncDecl)
110+
if !ok {
111+
continue
112+
}
113+
if n.Recv != nil {
114+
continue
115+
}
116+
name := n.Name.String()
117+
switch {
118+
case isTestMain(n):
119+
if tm.TestMain != nil {
120+
return errors.New("multiple definitions of TestMain")
121+
}
122+
tm.TestMain = &TestFunc{
123+
Location: loc,
124+
Name: name,
125+
}
126+
case isTest(name, "Test"):
127+
tm.Tests = append(tm.Tests, TestFunc{
128+
Location: loc,
129+
Name: name,
130+
})
131+
case isTest(name, "Benchmark"):
132+
tm.Benchmarks = append(tm.Benchmarks, TestFunc{
133+
Location: loc,
134+
Name: name,
135+
})
136+
}
137+
}
138+
139+
ex := doc.Examples(f)
140+
sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order })
141+
for _, e := range ex {
142+
tm.Examples = append(tm.Examples, ExampleFunc{
143+
Location: loc,
144+
Name: "Example" + e.Name,
145+
Output: e.Output,
146+
Unordered: e.Unordered,
147+
EmptyOutput: e.EmptyOutput,
148+
})
149+
}
150+
151+
return nil
152+
}
153+
154+
// Synthesize main package for the tests.
155+
func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) {
156+
buf := &bytes.Buffer{}
157+
if err := testmainTmpl.Execute(buf, tm); err != nil {
158+
return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err)
159+
}
160+
src, err := parser.ParseFile(fset, "_testmain.go", buf, 0)
161+
if err != nil {
162+
return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err)
163+
}
164+
pkg := &build.PackageData{
165+
Package: &gobuild.Package{
166+
ImportPath: tm.Package.ImportPath + ".testmain",
167+
Name: "main",
168+
GoFiles: []string{"_testmain.go"},
169+
},
170+
}
171+
return pkg, src, nil
172+
}
173+
174+
func (tm *TestMain) hasTests(loc FuncLocation, executableOnly bool) bool {
175+
if tm.TestMain != nil && tm.TestMain.Location == loc {
176+
return true
177+
}
178+
// Tests, Benchmarks and Fuzz targets are always executable.
179+
all := []TestFunc{}
180+
all = append(all, tm.Tests...)
181+
all = append(all, tm.Benchmarks...)
182+
183+
for _, t := range all {
184+
if t.Location == loc {
185+
return true
186+
}
187+
}
188+
189+
for _, e := range tm.Examples {
190+
if e.Location == loc && (e.Executable() || !executableOnly) {
191+
return true
192+
}
193+
}
194+
return false
195+
}
196+
197+
// ImportTest returns true if in-package test package needs to be imported.
198+
func (tm *TestMain) ImportTest() bool { return tm.hasTests(LocInPackage, false) }
199+
200+
// ImportXTest returns true if external test package needs to be imported.
201+
func (tm *TestMain) ImportXTest() bool { return tm.hasTests(LocExternal, false) }
202+
203+
// ExecutesTest returns true if in-package test package has executable tests.
204+
func (tm *TestMain) ExecutesTest() bool { return tm.hasTests(LocInPackage, true) }
205+
206+
// ExecutesXTest returns true if external package test package has executable tests.
207+
func (tm *TestMain) ExecutesXTest() bool { return tm.hasTests(LocExternal, true) }
208+
209+
// isTestMain tells whether fn is a TestMain(m *testing.M) function.
210+
func isTestMain(fn *ast.FuncDecl) bool {
211+
if fn.Name.String() != "TestMain" ||
212+
fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
213+
fn.Type.Params == nil ||
214+
len(fn.Type.Params.List) != 1 ||
215+
len(fn.Type.Params.List[0].Names) > 1 {
216+
return false
217+
}
218+
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
219+
if !ok {
220+
return false
221+
}
222+
// We can't easily check that the type is *testing.M
223+
// because we don't know how testing has been imported,
224+
// but at least check that it's *M or *something.M.
225+
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" {
226+
return true
227+
}
228+
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" {
229+
return true
230+
}
231+
return false
232+
}
233+
234+
// isTest tells whether name looks like a test (or benchmark, according to prefix).
235+
// It is a Test (say) if there is a character after Test that is not a lower-case letter.
236+
// We don't want TesticularCancer.
237+
func isTest(name, prefix string) bool {
238+
if !strings.HasPrefix(name, prefix) {
239+
return false
240+
}
241+
if len(name) == len(prefix) { // "Test" is ok
242+
return true
243+
}
244+
rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
245+
return !unicode.IsLower(rune)
246+
}
247+
248+
var testmainTmpl = template.Must(template.New("main").Parse(`
249+
package main
250+
251+
import (
252+
{{if not .TestMain}}
253+
"os"
254+
{{end}}
255+
"testing"
256+
"testing/internal/testdeps"
257+
258+
{{if .ImportTest}}
259+
{{if .ExecutesTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
260+
{{end -}}
261+
{{- if .ImportXTest -}}
262+
{{if .ExecutesXTest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
263+
{{end}}
264+
)
265+
266+
var tests = []testing.InternalTest{
267+
{{- range .Tests}}
268+
{"{{.Name}}", {{.Location}}.{{.Name}}},
269+
{{- end}}
270+
}
271+
272+
var benchmarks = []testing.InternalBenchmark{
273+
{{- range .Benchmarks}}
274+
{"{{.Name}}", {{.Location}}.{{.Name}}},
275+
{{- end}}
276+
}
277+
278+
// TODO(nevkontakte): Extract fuzz targets from the source.
279+
var fuzzTargets = []testing.InternalFuzzTarget{}
280+
281+
var examples = []testing.InternalExample{
282+
{{- range .Examples }}
283+
{{- if .Executable }}
284+
{"{{.Name}}", {{.Location}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
285+
{{- end }}
286+
{{- end }}
287+
}
288+
289+
func main() {
290+
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
291+
{{with .TestMain}}
292+
{{.Location}}.{{.Name}}(m)
293+
{{else}}
294+
os.Exit(m.Run())
295+
{{end -}}
296+
}
297+
298+
`))

0 commit comments

Comments
 (0)