x/playground: allow playground to run fuzz tests
Current implementation does not check for Fuzz functions. In case we
have test functions and no main, this change moves a file as
prog_test.go and runs go test -c.
Change also includes TestMain function, and removeBanner
Fixes golang/go#57029
Fixes golang/go#32237
Fixes golang/go#45431
Change-Id: I249df00a98db484a04a1380c73e2ff268a2e3379
Reviewed-on: https://go-review.googlesource.com/c/playground/+/456355
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jenny Rakoczy <jenny@golang.org>
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
diff --git a/sandbox.go b/sandbox.go
index 4854cc5..70eaa95 100644
--- a/sandbox.go
+++ b/sandbox.go
@@ -28,7 +28,6 @@
"strconv"
"strings"
"sync"
- "text/template"
"time"
"unicode"
"unicode/utf8"
@@ -49,7 +48,8 @@
// progName is the implicit program name written to the temp
// dir and used in compiler and vet errors.
- progName = "prog.go"
+ progName = "prog.go"
+ progTestName = "prog_test.go"
)
const (
@@ -171,7 +171,7 @@
return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil))
}
-// isTestFunc tells whether fn has the type of a testing function.
+// isTestFunc tells whether fn has the type of a testing, or fuzz function, or a TestMain func.
func isTestFunc(fn *ast.FuncDecl) bool {
if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params.List == nil ||
@@ -183,19 +183,19 @@
if !ok {
return false
}
- // We can't easily check that the type is *testing.T
+ // We can't easily check that the type is *testing.T or *testing.F
// because we don't know how testing has been imported,
- // but at least check that it's *T or *something.T.
- if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "T" {
+ // but at least check that it's *T (or *F) or *something.T (or *something.F).
+ if name, ok := ptr.X.(*ast.Ident); ok && (name.Name == "T" || name.Name == "F" || name.Name == "M") {
return true
}
- if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "T" {
+ if sel, ok := ptr.X.(*ast.SelectorExpr); ok && (sel.Sel.Name == "T" || sel.Sel.Name == "F" || sel.Sel.Name == "M") {
return true
}
return false
}
-// isTest tells whether name looks like a test (or benchmark, according to prefix).
+// isTest tells whether name looks like a test (or benchmark, or fuzz, 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 mistaken Testimony or erroneous Benchmarking.
func isTest(name, prefix string) bool {
@@ -213,32 +213,22 @@
// If the main function is present or there are no tests or examples, it returns nil.
// getTestProg emulates the "go test" command as closely as possible.
// Benchmarks are not supported because of sandboxing.
-func getTestProg(src []byte) []byte {
+func isTestProg(src []byte) bool {
fset := token.NewFileSet()
// Early bail for most cases.
f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly)
if err != nil || f.Name.Name != "main" {
- return nil
- }
-
- // importPos stores the position to inject the "testing" import declaration, if needed.
- importPos := fset.Position(f.Name.End()).Offset
-
- var testingImported bool
- for _, s := range f.Imports {
- if s.Path.Value == `"testing"` && s.Name == nil {
- testingImported = true
- break
- }
+ return false
}
// Parse everything and extract test names.
f, err = parser.ParseFile(fset, progName, src, parser.ParseComments)
if err != nil {
- return nil
+ return false
}
- var tests []string
+ var hasTest bool
+ var hasFuzz bool
for _, d := range f.Decls {
n, ok := d.(*ast.FuncDecl)
if !ok {
@@ -249,79 +239,24 @@
case name == "main":
// main declared as a method will not obstruct creation of our main function.
if n.Recv == nil {
- return nil
+ return false
}
+ case name == "TestMain" && isTestFunc(n):
+ hasTest = true
case isTest(name, "Test") && isTestFunc(n):
- tests = append(tests, name)
+ hasTest = true
+ case isTest(name, "Fuzz") && isTestFunc(n):
+ hasFuzz = true
}
}
- // Tests imply imported "testing" package in the code.
- // If there is no import, bail to let the compiler produce an error.
- if !testingImported && len(tests) > 0 {
- return nil
+ if hasTest || hasFuzz {
+ return true
}
- // We emulate "go test". An example with no "Output" comment is compiled,
- // but not executed. An example with no text after "Output:" is compiled,
- // executed, and expected to produce no output.
- var ex []*doc.Example
- // exNoOutput indicates whether an example with no output is found.
- // We need to compile the program containing such an example even if there are no
- // other tests or examples.
- exNoOutput := false
- for _, e := range doc.Examples(f) {
- if e.Output != "" || e.EmptyOutput {
- ex = append(ex, e)
- }
- if e.Output == "" && !e.EmptyOutput {
- exNoOutput = true
- }
- }
-
- if len(tests) == 0 && len(ex) == 0 && !exNoOutput {
- return nil
- }
-
- if !testingImported && (len(ex) > 0 || exNoOutput) {
- // In case of the program with examples and no "testing" package imported,
- // add import after "package main" without modifying line numbers.
- importDecl := []byte(`;import "testing";`)
- src = bytes.Join([][]byte{src[:importPos], importDecl, src[importPos:]}, nil)
- }
-
- data := struct {
- Tests []string
- Examples []*doc.Example
- }{
- tests,
- ex,
- }
- code := new(bytes.Buffer)
- if err := testTmpl.Execute(code, data); err != nil {
- panic(err)
- }
- src = append(src, code.Bytes()...)
- return src
+ return len(doc.Examples(f)) > 0
}
-var testTmpl = template.Must(template.New("main").Parse(`
-func main() {
- matchAll := func(t string, pat string) (bool, error) { return true, nil }
- tests := []testing.InternalTest{
-{{range .Tests}}
- {"{{.}}", {{.}}},
-{{end}}
- }
- examples := []testing.InternalExample{
-{{range .Examples}}
- {"Example{{.Name}}", Example{{.Name}}, {{printf "%q" .Output}}, {{.Unordered}}},
-{{end}}
- }
- testing.Main(matchAll, tests, nil, examples)
-}
-`))
-
var failedTestPattern = "--- FAIL"
// compileAndRun tries to build and run a user program.
@@ -341,7 +276,7 @@
return nil, err
}
if br.errorMessage != "" {
- return &response{Errors: br.errorMessage}, nil
+ return &response{Errors: removeBanner(br.errorMessage)}, nil
}
execRes, err := sandboxRun(ctx, br.exePath, br.testParam)
@@ -425,11 +360,10 @@
defer br.cleanup()
var buildPkgArg = "."
if files.Num() == 1 && len(files.Data(progName)) > 0 {
- buildPkgArg = progName
src := files.Data(progName)
- if code := getTestProg(src); code != nil {
+ if isTestProg(src) {
br.testParam = "-test.v"
- files.AddFile(progName, code)
+ files.MvFile(progName, progTestName)
}
}
@@ -474,7 +408,15 @@
return nil, fmt.Errorf("error copying GOCACHE: %v", err)
}
- cmd := exec.Command("/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime")
+ var goArgs []string
+ if br.testParam != "" {
+ goArgs = append(goArgs, "test", "-c")
+ } else {
+ goArgs = append(goArgs, "build")
+ }
+ goArgs = append(goArgs, "-o", br.exePath, "-tags=faketime")
+
+ cmd := exec.Command("/usr/local/go-faketime/bin/go", goArgs...)
cmd.Dir = tmpDir
cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"}
cmd.Env = append(cmd.Env, "GOCACHE="+goCache)
@@ -668,6 +610,16 @@
}
}
+// removeBanner remove package name banner
+func removeBanner(output string) string {
+ if strings.HasPrefix(output, "#") {
+ if nl := strings.Index(output, "\n"); nl != -1 {
+ output = output[nl+1:]
+ }
+ }
+ return output
+}
+
const healthProg = `
package main
diff --git a/sandbox_test.go b/sandbox_test.go
index f090a48..de390bf 100644
--- a/sandbox_test.go
+++ b/sandbox_test.go
@@ -59,6 +59,12 @@
{"Benchmark", Benchmark1IsABenchmark, true},
{"Benchmark", BenchmarkisNotABenchmark, false},
+
+ {"Fuzz", Fuzz, true},
+ {"Fuzz", Fuzz1IsAFuzz, true},
+ {"Fuzz", FuzzÑIsAFuzz, true},
+
+ {"Fuzz", FuzzisNotAFuzz, false},
} {
name := nameOf(t, tc.f)
t.Run(name, func(t *testing.T) {
@@ -108,7 +114,7 @@
panic("This is not a valid test function.")
}
-// Test11IsATest is a valid test function.
+// Test1IsATest is a valid test function.
func Test1IsATest(t *testing.T) {
}
@@ -181,3 +187,25 @@
func BenchmarkisNotABenchmark(b *testing.B) {
panic("This is not a valid benchmark function.")
}
+
+// FuzzisNotAFuzz is not a fuzz test function, despite appearances.
+//
+// Please ignore any lint or vet warnings for this function.
+func FuzzisNotAFuzz(f *testing.F) {
+ panic("This is not a valid fuzzing function.")
+}
+
+// Fuzz1IsAFuzz is a valid fuzz function.
+func Fuzz1IsAFuzz(f *testing.F) {
+ f.Skip()
+}
+
+// Fuzz is a fuzz with a minimal name.
+func Fuzz(f *testing.F) {
+ f.Skip()
+}
+
+// FuzzÑIsAFuzz is a fuzz with an interesting Unicode name.
+func FuzzÑIsAFuzz(f *testing.F) {
+ f.Skip()
+}
diff --git a/tests.go b/tests.go
index 3c07789..46731a6 100644
--- a/tests.go
+++ b/tests.go
@@ -274,7 +274,7 @@
func ExampleNotExecuted() {
// Output: it should not run
}
-`, want: "", errors: "./prog.go:4:20: undefined: testing\n"},
+`, want: "", errors: "./prog_test.go:4:20: undefined: testing\n"},
{
name: "test_with_import_ignored",
@@ -469,7 +469,7 @@
{
name: "compile_modules_with_vet",
withVet: true,
- wantVetErrors: "go: finding module for package github.com/bradfitz/iter\ngo: found github.com/bradfitz/iter in github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8\n# play\n./prog.go:6:2: fmt.Printf format %v reads arg #1, but call has 0 args\n",
+ wantVetErrors: "./prog.go:6:2: fmt.Printf format %v reads arg #1, but call has 0 args\n",
prog: `
package main
import ("fmt"; "github.com/bradfitz/iter")
@@ -581,4 +581,56 @@
fmt.Println(net.ParseIP("1.2.3.4"))
}
`, want: "1.2.3.4\n"},
+ {
+ name: "fuzz_executed",
+ prog: `
+package main
+
+import "testing"
+
+func FuzzSanity(f *testing.F) {
+ f.Add("a")
+ f.Fuzz(func(t *testing.T, v string) {
+ })
+}
+`, want: `=== RUN FuzzSanity
+=== RUN FuzzSanity/seed#0
+--- PASS: FuzzSanity (0.00s)
+ --- PASS: FuzzSanity/seed#0 (0.00s)
+PASS`},
+ {
+ name: "test_main",
+ prog: `
+package main
+
+import (
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(m.Run())
+}`, want: `testing: warning: no tests to run
+PASS`,
+ },
+ {
+ name: "multiple_files_no_banner",
+ prog: `
+package main
+
+func main() {
+ print()
+}
+
+-- foo.go --
+package main
+
+import "fmt"
+
+func print() {
+ =fmt.Println("Hello, playground")
+}
+`, errors: `./foo.go:6:2: syntax error: unexpected =, expecting }
+`,
+ },
}
diff --git a/txtar.go b/txtar.go
index 591dcbf..11f6ab4 100644
--- a/txtar.go
+++ b/txtar.go
@@ -48,6 +48,30 @@
}
}
+func (fs *fileSet) Update(filename string, src []byte) {
+ if fs.Contains(filename) {
+ fs.m[filename] = src
+ }
+}
+
+func (fs *fileSet) MvFile(source, target string) {
+ if fs.m == nil {
+ return
+ }
+ data, ok := fs.m[source]
+ if !ok {
+ return
+ }
+ fs.m[target] = data
+ delete(fs.m, source)
+ for i := range fs.files {
+ if fs.files[i] == source {
+ fs.files[i] = target
+ break
+ }
+ }
+}
+
// Format returns fs formatted as a txtar archive.
func (fs *fileSet) Format() []byte {
a := new(txtar.Archive)
diff --git a/vet.go b/vet.go
index ece62a7..d58c0f7 100644
--- a/vet.go
+++ b/vet.go
@@ -83,12 +83,6 @@
// Rewrite compiler errors to refer to progName
// instead of '/tmp/sandbox1234/main.go'.
errs := strings.Replace(string(out), dir, "", -1)
-
- // Remove vet's package name banner.
- if strings.HasPrefix(errs, "#") {
- if nl := strings.Index(errs, "\n"); nl != -1 {
- errs = errs[nl+1:]
- }
- }
+ errs = removeBanner(errs)
return errs, nil
}