Files
moxa/interp/interp_consistent_test.go
mpl 896bfeb5a1 interp: new EvalPath API
The recent changes that added some redeclaration checks implicitly added more
strictness related to namespaces and scopes which, among other things, broke
some uses that "accidentally" used to work.

For example, given

const script1 = `
	import "fmt"

	// more code
`
const script2 = `
	import "fmt"

	// some other code
`
If one Evals script1, then script2, with the same interpreter, without
specifying any scope, as the two fragments would be considered part of the same
(.go file) scope by default, a redeclaration error would be triggered because
import "fmt" is seen twice.

A work-around would have been to specify (a different) i.Name before each Eval
call, so that each script is considered as coming from a different .go file, and
hence are respectively in different scopes with respect to imports.

That lead us to realize we had to make specifying things such as file-scope, and
"incremental mode" (aka REPL), more obvious in the context of an Eval call.

In addition, we want to lay down the foundations for Yaegi being able to behave
more like the go tool wrt to various inputs, i.e. it should be able to take a
package directory, or an import path, as input, instead of just a .go file.

Hence the introduction of a new kind of Eval method (whose signature is not fixed yet):

func (interp *Interpreter) EvalPath(path string) (res reflect.Value, err error)

It partially solves the problem described above because:

1. the path given to EvalPath can be used as the file-scope hint mentioned
above, for now (even though the related implementation details might change).
2. Eval always runs in incremental mode, whereas EvalPath always runs in
non-incremental mode, hence clarifying the situation in that respect.

And to avoid confusion, the Name field of Interpreter is now non-exported,
since it is somewhat redundant with the path argument of EvalPath.

Note that #731 is not fully fixed (and might never be), as a requirement of the
proposed solution is to move the input bits of code into respective files
(instead of leaving them as strings).

Finally, some related bugfixes, documention changes, and some refactoring have
been included. Notably, there is no "empty scope" anymore, i.e. name defaults
to "_.go" when it is not specified.

Updates #731
Fixes #778
Fixes #798
Fixes #789 

Co-authored-by: Marc Vertes <mvertes@free.fr>
2020-08-20 13:14:15 +02:00

275 lines
8.9 KiB
Go

package interp_test
import (
"go/build"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/containous/yaegi/stdlib/unsafe"
)
func TestInterpConsistencyBuild(t *testing.T) {
if testing.Short() {
t.Skip("short mode")
}
dir := filepath.Join("..", "_test", "tmp")
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.Mkdir(dir, 0700); err != nil {
t.Fatal(err)
}
}
baseDir := filepath.Join("..", "_test")
files, err := ioutil.ReadDir(baseDir)
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if filepath.Ext(file.Name()) != ".go" ||
file.Name() == "assign11.go" || // expect error
file.Name() == "assign12.go" || // expect error
file.Name() == "assign15.go" || // expect error
file.Name() == "bad0.go" || // expect error
file.Name() == "const9.go" || // expect error
file.Name() == "export1.go" || // non-main package
file.Name() == "export0.go" || // non-main package
file.Name() == "for7.go" || // expect error
file.Name() == "fun21.go" || // expect error
file.Name() == "fun22.go" || // expect error
file.Name() == "if2.go" || // expect error
file.Name() == "import6.go" || // expect error
file.Name() == "init1.go" || // expect error
file.Name() == "io0.go" || // use random number
file.Name() == "op1.go" || // expect error
file.Name() == "op7.go" || // expect error
file.Name() == "op9.go" || // expect error
file.Name() == "bltn0.go" || // expect error
file.Name() == "method16.go" || // private struct field
file.Name() == "switch8.go" || // expect error
file.Name() == "switch9.go" || // expect error
file.Name() == "switch13.go" || // expect error
file.Name() == "switch19.go" || // expect error
file.Name() == "time0.go" || // display time (similar to random number)
file.Name() == "factor.go" || // bench
file.Name() == "fib.go" || // bench
file.Name() == "type5.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types
file.Name() == "type6.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types
file.Name() == "redeclaration0.go" || // expect error
file.Name() == "redeclaration1.go" || // expect error
file.Name() == "redeclaration2.go" || // expect error
file.Name() == "redeclaration3.go" || // expect error
file.Name() == "redeclaration4.go" || // expect error
file.Name() == "redeclaration5.go" || // expect error
file.Name() == "redeclaration-global0.go" || // expect error
file.Name() == "redeclaration-global1.go" || // expect error
file.Name() == "redeclaration-global2.go" || // expect error
file.Name() == "redeclaration-global3.go" || // expect error
file.Name() == "redeclaration-global4.go" || // expect error
file.Name() == "redeclaration-global5.go" || // expect error
file.Name() == "redeclaration-global6.go" || // expect error
file.Name() == "redeclaration-global7.go" || // expect error
file.Name() == "pkgname0.go" || // has deps
file.Name() == "pkgname1.go" || // expect error
file.Name() == "pkgname2.go" || // has deps
file.Name() == "ipp_as_key.go" || // has deps
file.Name() == "restricted0.go" || // expect error
file.Name() == "restricted1.go" || // expect error
file.Name() == "restricted2.go" || // expect error
file.Name() == "restricted3.go" || // expect error
file.Name() == "server6.go" || // syntax parsing
file.Name() == "server5.go" || // syntax parsing
file.Name() == "server4.go" || // syntax parsing
file.Name() == "server3.go" || // syntax parsing
file.Name() == "server2.go" || // syntax parsing
file.Name() == "server1a.go" || // syntax parsing
file.Name() == "server1.go" || // syntax parsing
file.Name() == "server0.go" || // syntax parsing
file.Name() == "server.go" || // syntax parsing
file.Name() == "range9.go" { // expect error
continue
}
file := file
t.Run(file.Name(), func(t *testing.T) {
filePath := filepath.Join(baseDir, file.Name())
// catch stdout
backupStdout := os.Stdout
defer func() {
os.Stdout = backupStdout
}()
r, w, _ := os.Pipe()
os.Stdout = w
i := interp.New(interp.Options{GoPath: build.Default.GOPATH})
i.Use(stdlib.Symbols)
i.Use(interp.Symbols)
i.Use(unsafe.Symbols)
_, err = i.EvalPath(filePath)
if err != nil {
t.Fatal(err)
}
// read stdout
if err = w.Close(); err != nil {
t.Fatal(err)
}
outInterp, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
// restore Stdout
os.Stdout = backupStdout
bin := filepath.Join(dir, strings.TrimSuffix(file.Name(), ".go"))
cmdBuild := exec.Command("go", "build", "-tags=dummy", "-o", bin, filePath)
outBuild, err := cmdBuild.CombinedOutput()
if err != nil {
t.Log(string(outBuild))
t.Fatal(err)
}
cmd := exec.Command(bin)
outRun, err := cmd.CombinedOutput()
if err != nil {
t.Log(string(outRun))
t.Fatal(err)
}
if string(outInterp) != string(outRun) {
t.Errorf("\nGot: %q,\n want: %q", string(outInterp), string(outRun))
}
})
}
}
func TestInterpErrorConsistency(t *testing.T) {
testCases := []struct {
fileName string
expectedInterp string
expectedExec string
}{
{
fileName: "assign11.go",
expectedInterp: "6:2: assignment mismatch: 3 variables but fmt.Println returns 2 values",
expectedExec: "6:10: assignment mismatch: 3 variables but fmt.Println returns 2 values",
},
{
fileName: "assign12.go",
expectedInterp: "6:2: assignment mismatch: 3 variables but fmt.Println returns 2 values",
expectedExec: "6:10: assignment mismatch: 3 variables but fmt.Println returns 2 values",
},
{
fileName: "bad0.go",
expectedInterp: "1:1: expected 'package', found println",
expectedExec: "1:1: expected 'package', found println",
},
{
fileName: "const9.go",
expectedInterp: "5:2: constant definition loop",
expectedExec: "5:2: constant definition loop",
},
{
fileName: "if2.go",
expectedInterp: "7:5: non-bool used as if condition",
expectedExec: "7:2: non-bool i % 1000000 (type int) used as if condition",
},
{
fileName: "for7.go",
expectedInterp: "4:14: non-bool used as for condition",
expectedExec: "4:2: non-bool i (type int) used as for condition",
},
{
fileName: "fun21.go",
expectedInterp: "4:2: not enough arguments to return",
expectedExec: "4:2: not enough arguments to return",
},
{
fileName: "fun22.go",
expectedInterp: "6:2: not enough arguments in call to time.Date",
expectedExec: "6:11: not enough arguments in call to time.Date",
},
{
fileName: "op1.go",
expectedInterp: "5:2: invalid operation: mismatched types int and float64",
expectedExec: "5:4: constant 1.3 truncated to integer",
},
{
fileName: "bltn0.go",
expectedInterp: "4:7: use of builtin println not in function call",
},
{
fileName: "import6.go",
expectedInterp: "import cycle not allowed",
expectedExec: "import cycle not allowed",
},
{
fileName: "switch8.go",
expectedInterp: "5:2: fallthrough statement out of place",
expectedExec: "5:2: fallthrough statement out of place",
},
{
fileName: "switch9.go",
expectedInterp: "9:3: cannot fallthrough in type switch",
expectedExec: "9:3: cannot fallthrough in type switch",
},
{
fileName: "switch13.go",
expectedInterp: "9:2: i is not a type",
expectedExec: "9:2: i (type interface {}) is not a type",
},
{
fileName: "switch19.go",
expectedInterp: "37:2: duplicate case Bir in type switch",
expectedExec: "37:2: duplicate case Bir in type switch",
},
}
for _, test := range testCases {
t.Run(test.fileName, func(t *testing.T) {
if len(test.expectedInterp) == 0 && len(test.expectedExec) == 0 {
t.Fatal("at least expectedInterp must be define")
}
filePath := filepath.Join("..", "_test", test.fileName)
i := interp.New(interp.Options{GoPath: build.Default.GOPATH})
i.Use(stdlib.Symbols)
_, errEval := i.EvalPath(filePath)
if errEval == nil {
t.Fatal("An error is expected but got none.")
}
if !strings.Contains(errEval.Error(), test.expectedInterp) {
t.Errorf("got %q, want: %q", errEval.Error(), test.expectedInterp)
}
cmd := exec.Command("go", "run", filePath)
outRun, errExec := cmd.CombinedOutput()
if errExec == nil {
t.Log(string(outRun))
t.Fatal("An error is expected but got none.")
}
if len(test.expectedExec) == 0 && !strings.Contains(string(outRun), test.expectedInterp) {
t.Errorf("got %q, want: %q", string(outRun), test.expectedInterp)
} else if !strings.Contains(string(outRun), test.expectedExec) {
t.Errorf("got %q, want: %q", string(outRun), test.expectedExec)
}
})
}
}