Skip to content

Commit

Permalink
Merge pull request gopherjs#1132 from nevkontakte/go1.18
Browse files Browse the repository at this point in the history
Detect and execute fuzz targets as tests
  • Loading branch information
nevkontakte authored Aug 1, 2022
2 parents 405ff02 + 2db03cc commit 6ca1089
Show file tree
Hide file tree
Showing 10 changed files with 621 additions and 229 deletions.
2 changes: 2 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ func (p *PackageData) InternalBuildContext() *build.Context {
func (p *PackageData) TestPackage() *PackageData {
return &PackageData{
Package: &build.Package{
Name: p.Name,
ImportPath: p.ImportPath,
Dir: p.Dir,
GoFiles: append(p.GoFiles, p.TestGoFiles...),
Expand All @@ -379,6 +380,7 @@ func (p *PackageData) TestPackage() *PackageData {
func (p *PackageData) XTestPackage() *PackageData {
return &PackageData{
Package: &build.Package{
Name: p.Name + "_test",
ImportPath: p.ImportPath + "_test",
Dir: p.Dir,
GoFiles: p.XTestGoFiles,
Expand Down
58 changes: 33 additions & 25 deletions compiler/natives/fs_vfsdata.go

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions compiler/natives/src/net/netip/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build js

package netip_test

import "testing"

func checkStringParseRoundTrip(t *testing.T, x interface{}, parse interface{}) {
// TODO(nevkontakte): This function requires generics to function.
// Re-enable after https://github.com/gopherjs/gopherjs/issues/1013 is resolved.
}
16 changes: 16 additions & 0 deletions internal/srctesting/srctesting.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
package srctesting

import (
"bytes"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
Expand Down Expand Up @@ -63,3 +65,17 @@ func ParseFuncDecl(t *testing.T, src string) *ast.FuncDecl {
}
return fdecl
}

// Format AST node into a string.
//
// The node type must be *ast.File, *printer.CommentedNode, []ast.Decl,
// []ast.Stmt, or assignment-compatible to ast.Expr, ast.Decl, ast.Spec, or
// ast.Stmt.
func Format(t *testing.T, fset *token.FileSet, node any) string {
t.Helper()
buf := &bytes.Buffer{}
if err := format.Node(buf, fset, node); err != nil {
t.Fatalf("Failed to format AST node %T: %s", node, err)
}
return buf.String()
}
16 changes: 16 additions & 0 deletions internal/testmain/testdata/testpkg/external_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package testpkg_test

import (
"fmt"
"testing"
)

func TestYyy(t *testing.T) {}

func BenchmarkYyy(b *testing.B) {}

func FuzzYyy(f *testing.F) { f.Skip() }

func ExampleYyy() {
fmt.Println("hello") // Output: hello
}
13 changes: 13 additions & 0 deletions internal/testmain/testdata/testpkg/inpackage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package testpkg

import "testing"

func TestXxx(t *testing.T) {}

func BenchmarkXxx(b *testing.B) {}

func FuzzXxx(f *testing.F) { f.Skip() }

func ExampleXxx() {}

func TestMain(m *testing.M) { m.Run() }
4 changes: 4 additions & 0 deletions internal/testmain/testdata/testpkg/testpkg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package testpkg

// Xxx is an sample function.
func Xxx() {}
307 changes: 307 additions & 0 deletions internal/testmain/testmain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
package testmain

import (
"bytes"
"errors"
"fmt"
"go/ast"
gobuild "go/build"
"go/doc"
"go/parser"
"go/token"
"path"
"sort"
"strings"
"text/template"
"unicode"
"unicode/utf8"

"github.com/gopherjs/gopherjs/build"
"golang.org/x/tools/go/buildutil"
)

// FuncLocation describes whether a test function is in-package or external
// (i.e. in the xxx_test package).
type FuncLocation uint8

const (
// LocUnknown is the default, invalid value of the PkgType.
LocUnknown FuncLocation = iota
// LocInPackage is an in-package test.
LocInPackage
// LocExternal is an external test (i.e. in the xxx_test package).
LocExternal
)

func (tl FuncLocation) String() string {
switch tl {
case LocInPackage:
return "_test"
case LocExternal:
return "_xtest"
default:
return "<unknown>"
}
}

// TestFunc describes a single test/benchmark/fuzz function in a package.
type TestFunc struct {
Location FuncLocation // Where the function is defined.
Name string // Function name.
}

// ExampleFunc describes an example.
type ExampleFunc struct {
Location FuncLocation // Where the function is defined.
Name string // Function name.
Output string // Expected output.
Unordered bool // Output is allowed to be unordered.
EmptyOutput bool // Whether the output is expected to be empty.
}

// Executable returns true if the example function should be executed with tests.
func (ef ExampleFunc) Executable() bool {
return ef.EmptyOutput || ef.Output != ""
}

// TestMain is a helper type responsible for generation of the test main package.
type TestMain struct {
Package *build.PackageData
Tests []TestFunc
Benchmarks []TestFunc
Fuzz []TestFunc
Examples []ExampleFunc
TestMain *TestFunc
}

// Scan package for tests functions.
func (tm *TestMain) Scan(fset *token.FileSet) error {
if err := tm.scanPkg(fset, tm.Package.TestGoFiles, LocInPackage); err != nil {
return err
}
if err := tm.scanPkg(fset, tm.Package.XTestGoFiles, LocExternal); err != nil {
return err
}
return nil
}

func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error {
for _, name := range files {
srcPath := path.Join(tm.Package.Dir, name)
f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath)
if err != nil {
return fmt.Errorf("failed to open source file %q: %w", srcPath, err)
}
defer f.Close()
parsed, err := parser.ParseFile(fset, srcPath, f, parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse %q: %w", srcPath, err)
}

if err := tm.scanFile(parsed, loc); err != nil {
return err
}
}
return nil
}

func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error {
for _, d := range f.Decls {
n, ok := d.(*ast.FuncDecl)
if !ok {
continue
}
if n.Recv != nil {
continue
}
name := n.Name.String()
switch {
case isTestMain(n):
if tm.TestMain != nil {
return errors.New("multiple definitions of TestMain")
}
tm.TestMain = &TestFunc{
Location: loc,
Name: name,
}
case isTest(name, "Test"):
tm.Tests = append(tm.Tests, TestFunc{
Location: loc,
Name: name,
})
case isTest(name, "Benchmark"):
tm.Benchmarks = append(tm.Benchmarks, TestFunc{
Location: loc,
Name: name,
})
case isTest(name, "Fuzz"):
tm.Fuzz = append(tm.Fuzz, TestFunc{
Location: loc,
Name: name,
})
}
}

ex := doc.Examples(f)
sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order })
for _, e := range ex {
tm.Examples = append(tm.Examples, ExampleFunc{
Location: loc,
Name: "Example" + e.Name,
Output: e.Output,
Unordered: e.Unordered,
EmptyOutput: e.EmptyOutput,
})
}

return nil
}

// Synthesize main package for the tests.
func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) {
buf := &bytes.Buffer{}
if err := testmainTmpl.Execute(buf, tm); err != nil {
return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err)
}
src, err := parser.ParseFile(fset, "_testmain.go", buf, 0)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err)
}
pkg := &build.PackageData{
Package: &gobuild.Package{
ImportPath: tm.Package.ImportPath + ".testmain",
Name: "main",
GoFiles: []string{"_testmain.go"},
},
}
return pkg, src, nil
}

func (tm *TestMain) hasTests(loc FuncLocation, executableOnly bool) bool {
if tm.TestMain != nil && tm.TestMain.Location == loc {
return true
}
// Tests, Benchmarks and Fuzz targets are always executable.
all := []TestFunc{}
all = append(all, tm.Tests...)
all = append(all, tm.Benchmarks...)

for _, t := range all {
if t.Location == loc {
return true
}
}

for _, e := range tm.Examples {
if e.Location == loc && (e.Executable() || !executableOnly) {
return true
}
}
return false
}

// ImportTest returns true if in-package test package needs to be imported.
func (tm *TestMain) ImportTest() bool { return tm.hasTests(LocInPackage, false) }

// ImportXTest returns true if external test package needs to be imported.
func (tm *TestMain) ImportXTest() bool { return tm.hasTests(LocExternal, false) }

// ExecutesTest returns true if in-package test package has executable tests.
func (tm *TestMain) ExecutesTest() bool { return tm.hasTests(LocInPackage, true) }

// ExecutesXTest returns true if external package test package has executable tests.
func (tm *TestMain) ExecutesXTest() bool { return tm.hasTests(LocExternal, true) }

// isTestMain tells whether fn is a TestMain(m *testing.M) function.
func isTestMain(fn *ast.FuncDecl) bool {
if fn.Name.String() != "TestMain" ||
fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params == nil ||
len(fn.Type.Params.List) != 1 ||
len(fn.Type.Params.List[0].Names) > 1 {
return false
}
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
if !ok {
return false
}
// We can't easily check that the type is *testing.M
// because we don't know how testing has been imported,
// but at least check that it's *M or *something.M.
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" {
return true
}
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" {
return true
}
return false
}

// isTest tells whether name looks like a test (or benchmark, according to prefix).
// It is a Test (say) if there is a character after Test that is not a lower-case letter.
// We don't want TesticularCancer.
func isTest(name, prefix string) bool {
if !strings.HasPrefix(name, prefix) {
return false
}
if len(name) == len(prefix) { // "Test" is ok
return true
}
rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(rune)
}

var testmainTmpl = template.Must(template.New("main").Parse(`
package main
import (
{{if not .TestMain}}
"os"
{{end}}
"testing"
"testing/internal/testdeps"
{{if .ImportTest}}
{{if .ExecutesTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
{{end -}}
{{- if .ImportXTest -}}
{{if .ExecutesXTest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
)
var tests = []testing.InternalTest{
{{- range .Tests}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var benchmarks = []testing.InternalBenchmark{
{{- range .Benchmarks}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var fuzzTargets = []testing.InternalFuzzTarget{
{{- range .Fuzz}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var examples = []testing.InternalExample{
{{- range .Examples }}
{{- if .Executable }}
{"{{.Name}}", {{.Location}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
{{- end }}
{{- end }}
}
func main() {
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
{{with .TestMain}}
{{.Location}}.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end -}}
}
`))
Loading

0 comments on commit 6ca1089

Please sign in to comment.