Files
moxa/interp/src.go
Marc Vertes 151699ef9f feature: test subcommand to run test and benchmark functions
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.
2020-09-14 11:14:04 +02:00

312 lines
8.1 KiB
Go

package interp
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// importSrc calls gta on the source code for the package identified by
// importPath. rPath is the relative path to the directory containing the source
// code for the package. It can also be "main" as a special value.
func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (string, error) {
var dir string
var err error
if interp.srcPkg[importPath] != nil {
name, ok := interp.pkgNames[importPath]
if !ok {
return "", fmt.Errorf("inconsistent knowledge about %s", importPath)
}
return name, nil
}
// For relative import paths in the form "./xxx" or "../xxx", the initial
// base path is the directory of the interpreter input file, or "." if no file
// was provided.
// In all other cases, absolute import paths are resolved from the GOPATH
// and the nested "vendor" directories.
if isPathRelative(importPath) {
if rPath == mainID {
rPath = "."
}
dir = filepath.Join(filepath.Dir(interp.name), rPath, importPath)
} else {
var root string
if rPath == mainID {
root, err = interp.rootFromSourceLocation()
if err != nil {
return "", err
}
} else {
root = rPath
}
if dir, rPath, err = pkgDir(interp.context.GOPATH, root, importPath); err != nil {
return "", err
}
}
if interp.rdir[importPath] {
return "", fmt.Errorf("import cycle not allowed\n\timports %s", importPath)
}
interp.rdir[importPath] = true
files, err := ioutil.ReadDir(dir)
if err != nil {
return "", err
}
var initNodes []*node
var rootNodes []*node
revisit := make(map[string][]*node)
var root *node
var pkgName string
// Parse source files.
for _, file := range files {
name := file.Name()
if skipFile(&interp.context, name, skipTest) {
continue
}
name = filepath.Join(dir, name)
var buf []byte
if buf, err = ioutil.ReadFile(name); err != nil {
return "", err
}
var pname string
if pname, root, err = interp.ast(string(buf), name, false); err != nil {
return "", err
}
if root == nil {
continue
}
if interp.astDot {
dotCmd := interp.dotCmd
if dotCmd == "" {
dotCmd = defaultDotCmd(name, "yaegi-ast-")
}
root.astDot(dotWriter(dotCmd), name)
}
if pkgName == "" {
pkgName = pname
} else if pkgName != pname && skipTest {
return "", fmt.Errorf("found packages %s and %s in %s", pkgName, pname, dir)
}
rootNodes = append(rootNodes, root)
subRPath := effectivePkg(rPath, importPath)
var list []*node
list, err = interp.gta(root, subRPath, importPath)
if err != nil {
return "", err
}
revisit[subRPath] = append(revisit[subRPath], list...)
}
// Revisit incomplete nodes where GTA could not complete.
for _, nodes := range revisit {
if err = interp.gtaRetry(nodes, importPath); err != nil {
return "", err
}
}
// Generate control flow graphs.
for _, root := range rootNodes {
var nodes []*node
if nodes, err = interp.cfg(root, importPath); err != nil {
return "", err
}
initNodes = append(initNodes, nodes...)
}
// Register source package in the interpreter. The package contains only
// the global symbols in the package scope.
interp.mutex.Lock()
gs := interp.scopes[importPath]
interp.srcPkg[importPath] = gs.sym
interp.pkgNames[importPath] = pkgName
interp.frame.mutex.Lock()
interp.resizeFrame()
interp.frame.mutex.Unlock()
interp.mutex.Unlock()
// Once all package sources have been parsed, execute entry points then init functions.
for _, n := range rootNodes {
if err = genRun(n); err != nil {
return "", err
}
interp.run(n, nil)
}
// Wire and execute global vars in global scope gs.
n, err := genGlobalVars(rootNodes, gs)
if err != nil {
return "", err
}
interp.run(n, nil)
// Add main to list of functions to run, after all inits.
if m := gs.sym[mainID]; pkgName == mainID && m != nil && skipTest {
initNodes = append(initNodes, m.node)
}
for _, n := range initNodes {
interp.run(n, interp.frame)
}
return pkgName, nil
}
// rootFromSourceLocation returns the path to the directory containing the input
// Go file given to the interpreter, relative to $GOPATH/src.
// It is meant to be called in the case when the initial input is a main package.
func (interp *Interpreter) rootFromSourceLocation() (string, error) {
sourceFile := interp.name
if sourceFile == DefaultSourceName {
return "", nil
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
pkgDir := filepath.Join(wd, filepath.Dir(sourceFile))
root := strings.TrimPrefix(pkgDir, filepath.Join(interp.context.GOPATH, "src")+"/")
if root == wd {
return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
}
return root, nil
}
// pkgDir returns the absolute path in filesystem for a package given its import path
// and the root of the subtree dependencies.
func pkgDir(goPath string, root, importPath string) (string, string, error) {
rPath := filepath.Join(root, "vendor")
dir := filepath.Join(goPath, "src", rPath, importPath)
if _, err := os.Stat(dir); err == nil {
return dir, rPath, nil // found!
}
dir = filepath.Join(goPath, "src", effectivePkg(root, importPath))
if _, err := os.Stat(dir); err == nil {
return dir, root, nil // found!
}
if len(root) == 0 {
return "", "", fmt.Errorf("unable to find source related to: %q", importPath)
}
rootPath := filepath.Join(goPath, "src", root)
prevRoot, err := previousRoot(rootPath, root)
if err != nil {
return "", "", err
}
return pkgDir(goPath, prevRoot, importPath)
}
const vendor = "vendor"
// Find the previous source root (vendor > vendor > ... > GOPATH).
func previousRoot(rootPath, root string) (string, error) {
rootPath = filepath.Clean(rootPath)
parent, final := filepath.Split(rootPath)
parent = filepath.Clean(parent)
// TODO(mpl): maybe it works for the special case main, but can't be bothered for now.
if root != mainID && final != vendor {
root = strings.TrimSuffix(root, string(filepath.Separator))
prefix := strings.TrimSuffix(strings.TrimSuffix(rootPath, root), string(filepath.Separator))
// look for the closest vendor in one of our direct ancestors, as it takes priority.
var vendored string
for {
fi, err := os.Lstat(filepath.Join(parent, vendor))
if err == nil && fi.IsDir() {
vendored = strings.TrimPrefix(strings.TrimPrefix(parent, prefix), string(filepath.Separator))
break
}
if !os.IsNotExist(err) {
return "", err
}
// stop when we reach GOPATH/src/blah
parent = filepath.Dir(parent)
if parent == prefix {
break
}
// just an additional failsafe, stop if we reach the filesystem root, or dot (if
// we are dealing with relative paths).
// TODO(mpl): It should probably be a critical error actually,
// as we shouldn't have gone that high up in the tree.
if parent == string(filepath.Separator) || parent == "." {
break
}
}
if vendored != "" {
return vendored, nil
}
}
// TODO(mpl): the algorithm below might be redundant with the one above,
// but keeping it for now. Investigate/simplify/remove later.
splitRoot := strings.Split(root, string(filepath.Separator))
var index int
for i := len(splitRoot) - 1; i >= 0; i-- {
if splitRoot[i] == "vendor" {
index = i
break
}
}
if index == 0 {
return "", nil
}
return filepath.Join(splitRoot[:index]...), nil
}
func effectivePkg(root, path string) string {
splitRoot := strings.Split(root, string(filepath.Separator))
splitPath := strings.Split(path, string(filepath.Separator))
var result []string
rootIndex := 0
prevRootIndex := 0
for i := 0; i < len(splitPath); i++ {
part := splitPath[len(splitPath)-1-i]
index := len(splitRoot) - 1 - rootIndex
if index > 0 && part == splitRoot[index] && i != 0 {
prevRootIndex = rootIndex
rootIndex++
} else if prevRootIndex == rootIndex {
result = append(result, part)
}
}
var frag string
for i := len(result) - 1; i >= 0; i-- {
frag = filepath.Join(frag, result[i])
}
return filepath.Join(root, frag)
}
// isPathRelative returns true if path starts with "./" or "../".
func isPathRelative(s string) bool {
p := "." + string(filepath.Separator)
return strings.HasPrefix(s, p) || strings.HasPrefix(s, "."+p)
}