Skip to content

Commit

Permalink
feature: test subcommand to run test and benchmark functions
Browse files Browse the repository at this point in the history
This change allows the interpreter to execute tests and benchmarks
functions provided by packages.

The test subcommand is similar to the "go test" command and
all the relevant flags have been kept.

The ability to evaluate a directory or a package has also been added.

A new method Symbol to access exported symbol values of an interpreted
package has been added. This method is used by the test subcommand.

An EvalTest method has been added to evaluate all Go files, including "*_test.go".

The testing packages from the standard library have been added to stdlib used
symbols.
  • Loading branch information
mvertes committed Sep 14, 2020
1 parent f1f3ca7 commit 151699e
Show file tree
Hide file tree
Showing 21 changed files with 577 additions and 79 deletions.
14 changes: 14 additions & 0 deletions _test/m1/main.go
@@ -0,0 +1,14 @@
package main

import (
"fmt"
"testing"
)

func main() {
fmt.Println("vim-go")
}

func TestWeird(t *testing.T) {
fmt.Println("in TestWeird")
}
17 changes: 17 additions & 0 deletions _test/m1/main_test.go
@@ -0,0 +1,17 @@
package main

import (
"fmt"
"math/rand"
"testing"
)

func TestMain(t *testing.T) {
fmt.Println("in test")
}

func BenchmarkMain(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
4 changes: 2 additions & 2 deletions cmd/goexports/goexports.go
Expand Up @@ -108,9 +108,9 @@ func main() {

var oFile string
if pkgIdent == "syscall" {
oFile = strings.Replace(importPath, "/", "_", -1) + "_" + goos + "_" + goarch + ".go"
oFile = strings.ReplaceAll(importPath, "/", "_") + "_" + goos + "_" + goarch + ".go"
} else {
oFile = strings.Replace(importPath, "/", "_", -1) + ".go"
oFile = strings.ReplaceAll(importPath, "/", "_") + ".go"
}

prefix := runtime.Version()
Expand Down
2 changes: 1 addition & 1 deletion cmd/yaegi/extract.go
Expand Up @@ -58,7 +58,7 @@ func extractCmd(arg []string) error {
continue
}

oFile := strings.Replace(importPath, "/", "_", -1) + ".go"
oFile := strings.ReplaceAll(importPath, "/", "_") + ".go"
f, err := os.Create(oFile)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/yaegi/help.go
Expand Up @@ -36,7 +36,7 @@ func help(arg []string) error {
case Run:
return run([]string{"-h"})
case Test:
return fmt.Errorf("help: test not implemented")
return test([]string{"-h"})
default:
return fmt.Errorf("help: invalid yaegi command: %v", cmd)
}
Expand Down
56 changes: 25 additions & 31 deletions cmd/yaegi/run.go
Expand Up @@ -57,11 +57,13 @@ func run(arg []string) error {

if cmd != "" {
_, err = i.Eval(cmd)
showError(err)
}

if len(args) == 0 {
if interactive || cmd == "" {
_, err = i.REPL()
showError(err)
}
return err
}
Expand All @@ -71,40 +73,27 @@ func run(arg []string) error {
os.Args = arg[1:]
flag.CommandLine = flag.NewFlagSet(path, flag.ExitOnError)

if isPackageName(path) {
err = runPackage(i, path)
if isFile(path) {
err = runFile(i, path)
} else {
if isDir(path) {
err = runDir(i, path)
} else {
err = runFile(i, path)
}
_, err = i.EvalPath(path)
}
showError(err)

if err != nil {
return err
}

if interactive {
_, err = i.REPL()
showError(err)
}
return err
}

func isPackageName(path string) bool {
return !strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "./") && !strings.HasPrefix(path, "../") && !strings.HasSuffix(path, ".go")
}

func isDir(path string) bool {
fi, err := os.Lstat(path)
return err == nil && fi.IsDir()
}

func runPackage(i *interp.Interpreter, path string) error {
return fmt.Errorf("runPackage not implemented")
}

func runDir(i *interp.Interpreter, path string) error {
return fmt.Errorf("runDir not implemented")
func isFile(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular()
}

func runFile(i *interp.Interpreter, path string) error {
Expand All @@ -117,15 +106,20 @@ func runFile(i *interp.Interpreter, path string) error {
// Allow executable go scripts, Have the same behavior as in interactive mode.
s = strings.Replace(s, "#!", "//", 1)
_, err = i.Eval(s)
} else {
// Files not starting with "#!" are supposed to be pure Go, directly Evaled.
_, err := i.EvalPath(path)
if err != nil {
fmt.Println(err)
if p, ok := err.(interp.Panic); ok {
fmt.Println(string(p.Stack))
}
}
return err
}

// Files not starting with "#!" are supposed to be pure Go, directly Evaled.
_, err = i.EvalPath(path)
return err
}

func showError(err error) {
if err == nil {
return
}
fmt.Fprintln(os.Stderr, err)
if p, ok := err.(interp.Panic); ok {
fmt.Fprintln(os.Stderr, string(p.Stack))
}
}
131 changes: 131 additions & 0 deletions cmd/yaegi/test.go
@@ -0,0 +1,131 @@
package main

import (
"flag"
"fmt"
"go/build"
"os"
"regexp"
"strings"
"testing"

"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/containous/yaegi/stdlib/syscall"
"github.com/containous/yaegi/stdlib/unrestricted"
"github.com/containous/yaegi/stdlib/unsafe"
)

func test(arg []string) (err error) {
var (
bench string
benchmem bool
benchtime string
count string
cpu string
failfast bool
run string
short bool
tags string
useUnrestricted bool
useUnsafe bool
useSyscall bool
timeout string
verbose bool
)

tflag := flag.NewFlagSet("test", flag.ContinueOnError)
tflag.StringVar(&bench, "bench", "", "Run only those benchmarks matching a regular expression.")
tflag.BoolVar(&benchmem, "benchmem", false, "Print memory allocation statistics for benchmarks.")
tflag.StringVar(&benchtime, "benchtime", "", "Run enough iterations of each benchmark to take t.")
tflag.StringVar(&count, "count", "", "Run each test and benchmark n times (default 1).")
tflag.StringVar(&cpu, "cpu", "", "Specify a list of GOMAXPROCS values for which the tests or benchmarks should be executed.")
tflag.BoolVar(&failfast, "failfast", false, "Do not start new tests after the first test failure.")
tflag.StringVar(&run, "run", "", "Run only those tests matching a regular expression.")
tflag.BoolVar(&short, "short", false, "Tell long-running tests to shorten their run time.")
tflag.StringVar(&tags, "tags", "", "Set a list of build tags.")
tflag.StringVar(&timeout, "timeout", "", "If a test binary runs longer than duration d, panic.")
tflag.BoolVar(&useUnrestricted, "unrestricted", false, "Include unrestricted symbols.")
tflag.BoolVar(&useUnsafe, "unsafe", false, "Include usafe symbols.")
tflag.BoolVar(&useSyscall, "syscall", false, "Include syscall symbols.")
tflag.BoolVar(&verbose, "v", false, "Verbose output: log all tests as they are run.")
tflag.Usage = func() {
fmt.Println("Usage: yaegi test [options] [path]")
fmt.Println("Options:")
tflag.PrintDefaults()
}
if err = tflag.Parse(arg); err != nil {
return err
}
args := tflag.Args()
path := "."
if len(args) > 0 {
path = args[0]
}

// Overwrite os.Args with correct flags to setup testing.Init.
tf := []string{""}
if bench != "" {
tf = append(tf, "-test.bench", bench)
}
if benchmem {
tf = append(tf, "-test.benchmem")
}
if benchtime != "" {
tf = append(tf, "-test.benchtime", benchtime)
}
if count != "" {
tf = append(tf, "-test.count", count)
}
if cpu != "" {
tf = append(tf, "-test.cpu", cpu)
}
if failfast {
tf = append(tf, "-test.failfast")
}
if run != "" {
tf = append(tf, "-test.run", run)
}
if short {
tf = append(tf, "-test.short")
}
if timeout != "" {
tf = append(tf, "-test.timeout", timeout)
}
if verbose {
tf = append(tf, "-test.v")
}
testing.Init()
os.Args = tf
flag.Parse()

i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")})
i.Use(stdlib.Symbols)
i.Use(interp.Symbols)
if useSyscall {
i.Use(syscall.Symbols)
}
if useUnrestricted {
i.Use(unrestricted.Symbols)
}
if useUnsafe {
i.Use(unsafe.Symbols)
}
if err = i.EvalTest(path); err != nil {
return err
}

benchmarks := []testing.InternalBenchmark{}
tests := []testing.InternalTest{}
for name, sym := range i.Symbols(path) {
switch fun := sym.Interface().(type) {
case func(*testing.B):
benchmarks = append(benchmarks, testing.InternalBenchmark{name, fun})
case func(*testing.T):
tests = append(tests, testing.InternalTest{name, fun})
}
}

testing.Main(regexp.MatchString, tests, benchmarks, nil)
return nil
}
2 changes: 1 addition & 1 deletion cmd/yaegi/yaegi.go
Expand Up @@ -118,7 +118,7 @@ func main() {
case Run:
err = run(os.Args[2:])
case Test:
err = fmt.Errorf("test not implemented")
err = test(os.Args[2:])
default:
// If no command is given, fallback to default "run" command.
// This allows scripts starting with "#!/usr/bin/env yaegi",
Expand Down
8 changes: 6 additions & 2 deletions interp/build.go
Expand Up @@ -5,6 +5,7 @@ import (
"go/build"
"go/parser"
"path"
"path/filepath"
"strconv"
"strings"
)
Expand Down Expand Up @@ -129,12 +130,15 @@ func goMinorVersion(ctx *build.Context) int {
}

// skipFile returns true if file should be skipped.
func skipFile(ctx *build.Context, p string) bool {
func skipFile(ctx *build.Context, p string, skipTest bool) bool {
if !strings.HasSuffix(p, ".go") {
return true
}
p = strings.TrimSuffix(path.Base(p), ".go")
if strings.HasSuffix(p, "_test") {
if pp := filepath.Base(p); strings.HasPrefix(pp, "_") || strings.HasPrefix(pp, ".") {
return true
}
if skipTest && strings.HasSuffix(p, "_test") {
return true
}
i := strings.Index(p, "_")
Expand Down
2 changes: 1 addition & 1 deletion interp/build_test.go
Expand Up @@ -74,7 +74,7 @@ func TestBuildFile(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test.src, func(t *testing.T) {
if r := skipFile(&ctx, test.src); r != test.res {
if r := skipFile(&ctx, test.src, NoTest); r != test.res {
t.Errorf("got %v, want %v", r, test.res)
}
})
Expand Down
2 changes: 1 addition & 1 deletion interp/dot.go
Expand Up @@ -19,7 +19,7 @@ func (n *node) astDot(out io.Writer, name string) {
var label string
switch n.kind {
case basicLit, identExpr:
label = strings.Replace(n.ident, "\"", "\\\"", -1)
label = strings.ReplaceAll(n.ident, "\"", "\\\"")
default:
if n.action != aNop {
label = n.action.String()
Expand Down
2 changes: 1 addition & 1 deletion interp/gta.go
Expand Up @@ -216,7 +216,7 @@ func (interp *Interpreter) gta(root *node, rpath, importPath string) ([]*node, e
err = n.cfgErrorf("%s redeclared in this block", name)
return false
}
} else if pkgName, err = interp.importSrc(rpath, ipath); err == nil {
} else if pkgName, err = interp.importSrc(rpath, ipath, NoTest); err == nil {
sc.types = interp.universe.types
switch name {
case "_": // no import of symbols
Expand Down

0 comments on commit 151699e

Please sign in to comment.