282 строки
5,8 КиБ
Go
282 строки
5,8 КиБ
Go
package godog
|
|
|
|
import (
|
|
"bytes"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
type builder struct {
|
|
files map[string]*ast.File
|
|
fset *token.FileSet
|
|
Contexts []string
|
|
Internal bool
|
|
SuiteName string
|
|
tpl *template.Template
|
|
|
|
imports []*ast.ImportSpec
|
|
}
|
|
|
|
func (b *builder) hasImport(a *ast.ImportSpec) bool {
|
|
for _, b := range b.imports {
|
|
var aname, bname string
|
|
if a.Name != nil {
|
|
aname = a.Name.Name
|
|
}
|
|
if b.Name != nil {
|
|
bname = b.Name.Name
|
|
}
|
|
if bname == aname && a.Path.Value == b.Path.Value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newBuilderSkel() *builder {
|
|
return &builder{
|
|
files: make(map[string]*ast.File),
|
|
fset: token.NewFileSet(),
|
|
tpl: template.Must(template.New("main").Parse(`package main
|
|
import (
|
|
{{ if not .Internal }} "github.com/DATA-DOG/godog"{{ end }}
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
const GodogSuiteName = "{{ .SuiteName }}"
|
|
|
|
func TestMain(m *testing.M) {
|
|
status := {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) {
|
|
{{range .Contexts}}
|
|
{{ . }}(suite)
|
|
{{end}}
|
|
})
|
|
os.Exit(status)
|
|
}`)),
|
|
}
|
|
}
|
|
|
|
func doBuild(buildPath, dir string) error {
|
|
b := newBuilderSkel()
|
|
err := filepath.Walk(buildPath, func(path string, file os.FileInfo, err error) error {
|
|
if file.IsDir() && file.Name() != "." {
|
|
return filepath.SkipDir
|
|
}
|
|
if err == nil && strings.HasSuffix(path, ".go") {
|
|
f, err := parser.ParseFile(b.fset, path, nil, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.register(f, file.Name())
|
|
}
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := b.tpl.Execute(&buf, b); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := parser.ParseFile(b.fset, "", &buf, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.files["godog_test.go"] = f
|
|
|
|
os.Mkdir(dir, 0755)
|
|
for name, node := range b.files {
|
|
f, err := os.Create(filepath.Join(dir, name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := format.Node(f, b.fset, node); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *builder) register(f *ast.File, name string) {
|
|
// mark godog package as internal
|
|
if f.Name.Name == "godog" && !b.Internal {
|
|
b.Internal = true
|
|
}
|
|
b.SuiteName = f.Name.Name
|
|
b.deleteMainFunc(f)
|
|
f.Name.Name = "main"
|
|
b.registerContexts(f)
|
|
b.files[name] = f
|
|
}
|
|
|
|
func (b *builder) removeUnusedImports(f *ast.File) {
|
|
used := b.usedPackages(f)
|
|
isUsed := func(p string) bool {
|
|
for _, ref := range used {
|
|
if p == ref {
|
|
return true
|
|
}
|
|
}
|
|
return p == "_"
|
|
}
|
|
var decls []ast.Decl
|
|
for _, d := range f.Decls {
|
|
gen, ok := d.(*ast.GenDecl)
|
|
if ok && gen.Tok == token.IMPORT {
|
|
var specs []ast.Spec
|
|
for _, spec := range gen.Specs {
|
|
impspec := spec.(*ast.ImportSpec)
|
|
ipath := strings.Trim(impspec.Path.Value, `\"`)
|
|
check := importPathToName(ipath)
|
|
if impspec.Name != nil {
|
|
check = impspec.Name.Name
|
|
}
|
|
|
|
if isUsed(check) {
|
|
specs = append(specs, spec)
|
|
}
|
|
}
|
|
|
|
if len(specs) == 0 {
|
|
continue
|
|
}
|
|
gen.Specs = specs
|
|
}
|
|
decls = append(decls, d)
|
|
}
|
|
f.Decls = decls
|
|
}
|
|
|
|
func (b *builder) deleteImports(f *ast.File) {
|
|
var decls []ast.Decl
|
|
for _, d := range f.Decls {
|
|
gen, ok := d.(*ast.GenDecl)
|
|
if ok && gen.Tok == token.IMPORT {
|
|
for _, spec := range gen.Specs {
|
|
impspec := spec.(*ast.ImportSpec)
|
|
if !b.hasImport(impspec) {
|
|
b.imports = append(b.imports, impspec)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
decls = append(decls, d)
|
|
}
|
|
f.Decls = decls
|
|
}
|
|
|
|
func (b *builder) deleteMainFunc(f *ast.File) {
|
|
var decls []ast.Decl
|
|
var hadMain bool
|
|
for _, d := range f.Decls {
|
|
fun, ok := d.(*ast.FuncDecl)
|
|
if !ok {
|
|
decls = append(decls, d)
|
|
continue
|
|
}
|
|
if fun.Name.Name != "TestMain" {
|
|
decls = append(decls, fun)
|
|
} else {
|
|
hadMain = true
|
|
}
|
|
}
|
|
f.Decls = decls
|
|
|
|
if hadMain {
|
|
b.removeUnusedImports(f)
|
|
}
|
|
}
|
|
|
|
func (b *builder) registerContexts(f *ast.File) {
|
|
for _, d := range f.Decls {
|
|
switch fun := d.(type) {
|
|
case *ast.FuncDecl:
|
|
for _, param := range fun.Type.Params.List {
|
|
switch expr := param.Type.(type) {
|
|
case *ast.StarExpr:
|
|
switch x := expr.X.(type) {
|
|
case *ast.Ident:
|
|
if x.Name == "Suite" {
|
|
b.Contexts = append(b.Contexts, fun.Name.Name)
|
|
}
|
|
case *ast.SelectorExpr:
|
|
switch t := x.X.(type) {
|
|
case *ast.Ident:
|
|
if t.Name == "godog" && x.Sel.Name == "Suite" {
|
|
b.Contexts = append(b.Contexts, fun.Name.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type visitFn func(node ast.Node) ast.Visitor
|
|
|
|
func (fn visitFn) Visit(node ast.Node) ast.Visitor {
|
|
return fn(node)
|
|
}
|
|
|
|
func (b *builder) usedPackages(f *ast.File) []string {
|
|
var refs []string
|
|
var visitor visitFn
|
|
visitor = visitFn(func(node ast.Node) ast.Visitor {
|
|
if node == nil {
|
|
return visitor
|
|
}
|
|
switch v := node.(type) {
|
|
case *ast.SelectorExpr:
|
|
xident, ok := v.X.(*ast.Ident)
|
|
if !ok {
|
|
break
|
|
}
|
|
if xident.Obj != nil {
|
|
// if the parser can resolve it, it's not a package ref
|
|
break
|
|
}
|
|
refs = append(refs, xident.Name)
|
|
}
|
|
return visitor
|
|
})
|
|
ast.Walk(visitor, f)
|
|
return refs
|
|
}
|
|
|
|
// Build creates a runnable Godog executable file
|
|
// from current package source and test source files.
|
|
//
|
|
// The package files are merged with the help of go/ast into
|
|
// a single main package file which has a custom
|
|
// main function to run test suite features.
|
|
//
|
|
// Currently, to manage imports we use "golang.org/x/tools/imports"
|
|
// package, but that may be replaced in order to have
|
|
// no external dependencies
|
|
func Build(dir string) error {
|
|
return doBuild(".", dir)
|
|
}
|
|
|
|
// importPathToName finds out the actual package name, as declared in its .go files.
|
|
// If there's a problem, it falls back to using importPathToNameBasic.
|
|
func importPathToName(importPath string) (packageName string) {
|
|
if buildPkg, err := build.Import(importPath, "", 0); err == nil {
|
|
return buildPkg.Name
|
|
}
|
|
return path.Base(importPath)
|
|
}
|