github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/builder.go (about)

     1  package godog
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/build"
     7  	"go/parser"
     8  	"go/token"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  	"text/template"
    16  	"time"
    17  	"unicode"
    18  )
    19  
    20  var tooldir = findToolDir()
    21  var compiler = filepath.Join(tooldir, "compile")
    22  var linker = filepath.Join(tooldir, "link")
    23  var gopaths = filepath.SplitList(build.Default.GOPATH)
    24  var goarch = build.Default.GOARCH
    25  var goos = build.Default.GOOS
    26  
    27  var godogImportPath = "github.com/DATA-DOG/godog"
    28  var runnerTemplate = template.Must(template.New("testmain").Parse(`package main
    29  
    30  import (
    31  	"github.com/DATA-DOG/godog"
    32  	{{if .Contexts}}_test "{{.ImportPath}}"{{end}}
    33  	"os"
    34  )
    35  
    36  func main() {
    37  	status := godog.Run("{{ .Name }}", func (suite *godog.Suite) {
    38  		os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}")
    39  		{{range .Contexts}}
    40  			_test.{{ . }}(suite)
    41  		{{end}}
    42  	})
    43  	os.Exit(status)
    44  }`))
    45  
    46  // Build creates a test package like go test command at given target path.
    47  // If there are no go files in tested directory, then
    48  // it simply builds a godog executable to scan features.
    49  //
    50  // If there are go test files, it first builds a test
    51  // package with standard go test command.
    52  //
    53  // Finally it generates godog suite executable which
    54  // registers exported godog contexts from the test files
    55  // of tested package.
    56  //
    57  // Returns the path to generated executable
    58  func Build(bin string) error {
    59  	abs, err := filepath.Abs(".")
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	// we allow package to be nil, if godog is run only when
    65  	// there is a feature file in empty directory
    66  	pkg := importPackage(abs)
    67  	src, anyContexts, err := buildTestMain(pkg)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	workdir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano())
    73  	testdir := workdir
    74  
    75  	// if none of test files exist, or there are no contexts found
    76  	// we will skip test package compilation, since it is useless
    77  	if anyContexts {
    78  		// first of all compile test package dependencies
    79  		// that will save was many compilations for dependencies
    80  		// go does it better
    81  		out, err := exec.Command("go", "test", "-i").CombinedOutput()
    82  		if err != nil {
    83  			return fmt.Errorf("failed to compile package: %s, reason: %v, output: %s", pkg.Name, err, string(out))
    84  		}
    85  
    86  		// let go do the dirty work and compile test
    87  		// package with it's dependencies. Older go
    88  		// versions does not accept existing file output
    89  		// so we create a temporary executable which will
    90  		// removed.
    91  		temp := fmt.Sprintf(filepath.Join("%s", "temp-%d.test"), os.TempDir(), time.Now().UnixNano())
    92  
    93  		// builds and compile the tested package.
    94  		// generated test executable will be removed
    95  		// since we do not need it for godog suite.
    96  		// we also print back the temp WORK directory
    97  		// go has built. We will reuse it for our suite workdir.
    98  		out, err = exec.Command("go", "test", "-c", "-work", "-o", temp).CombinedOutput()
    99  		if err != nil {
   100  			return fmt.Errorf("failed to compile tested package: %s, reason: %v, output: %s", pkg.Name, err, string(out))
   101  		}
   102  		defer os.Remove(temp)
   103  
   104  		// extract go-build temporary directory as our workdir
   105  		workdir = strings.TrimSpace(string(out))
   106  		if !strings.HasPrefix(workdir, "WORK=") {
   107  			return fmt.Errorf("expected WORK dir path, but got: %s", workdir)
   108  		}
   109  		workdir = strings.Replace(workdir, "WORK=", "", 1)
   110  		testdir = filepath.Join(workdir, pkg.ImportPath, "_test")
   111  	} else {
   112  		// still need to create temporary workdir
   113  		if err = os.MkdirAll(testdir, 0755); err != nil {
   114  			return err
   115  		}
   116  	}
   117  	defer os.RemoveAll(workdir)
   118  
   119  	// replace _testmain.go file with our own
   120  	testmain := filepath.Join(testdir, "_testmain.go")
   121  	err = ioutil.WriteFile(testmain, src, 0644)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// godog library may not be imported in tested package
   127  	// but we need it for our testmain package.
   128  	// So we look it up in available source paths
   129  	// including vendor directory, supported since 1.5.
   130  	try := maybeVendorPaths(abs)
   131  	for _, d := range build.Default.SrcDirs() {
   132  		try = append(try, filepath.Join(d, godogImportPath))
   133  	}
   134  	godogPkg, err := locatePackage(try)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	// make sure godog package archive is installed, gherkin
   140  	// will be installed as dependency of godog
   141  	cmd := exec.Command("go", "install", godogPkg.ImportPath)
   142  	cmd.Env = os.Environ()
   143  	out, err := cmd.CombinedOutput()
   144  	if err != nil {
   145  		return fmt.Errorf("failed to install godog package: %s, reason: %v", string(out), err)
   146  	}
   147  
   148  	// collect all possible package dirs, will be
   149  	// used for includes and linker
   150  	pkgDirs := []string{workdir, testdir}
   151  	for _, gopath := range gopaths {
   152  		pkgDirs = append(pkgDirs, filepath.Join(gopath, "pkg", goos+"_"+goarch))
   153  	}
   154  	pkgDirs = uniqStringList(pkgDirs)
   155  
   156  	// compile godog testmain package archive
   157  	// we do not depend on CGO so a lot of checks are not necessary
   158  	testMainPkgOut := filepath.Join(testdir, "main.a")
   159  	args := []string{
   160  		"-o", testMainPkgOut,
   161  		// "-trimpath", workdir,
   162  		"-p", "main",
   163  		"-complete",
   164  	}
   165  	// if godog library is in vendor directory
   166  	// link it with import map
   167  	if i := strings.LastIndex(godogPkg.ImportPath, "vendor/"); i != -1 {
   168  		args = append(args, "-importmap", godogImportPath+"="+godogPkg.ImportPath)
   169  	}
   170  	for _, inc := range pkgDirs {
   171  		args = append(args, "-I", inc)
   172  	}
   173  	args = append(args, "-pack", testmain)
   174  	cmd = exec.Command(compiler, args...)
   175  	cmd.Env = os.Environ()
   176  	out, err = cmd.CombinedOutput()
   177  	if err != nil {
   178  		return fmt.Errorf("failed to compile testmain package: %v - output: %s", err, string(out))
   179  	}
   180  
   181  	// link test suite executable
   182  	args = []string{
   183  		"-o", bin,
   184  		"-buildmode=exe",
   185  	}
   186  	for _, link := range pkgDirs {
   187  		args = append(args, "-L", link)
   188  	}
   189  	args = append(args, testMainPkgOut)
   190  	cmd = exec.Command(linker, args...)
   191  	cmd.Env = os.Environ()
   192  
   193  	out, err = cmd.CombinedOutput()
   194  	if err != nil {
   195  		msg := `failed to link test executable:
   196  	reason: %s
   197  	command: %s`
   198  		return fmt.Errorf(msg, string(out), linker+" '"+strings.Join(args, "' '")+"'")
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func locatePackage(try []string) (*build.Package, error) {
   205  	for _, p := range try {
   206  		abs, err := filepath.Abs(p)
   207  		if err != nil {
   208  			continue
   209  		}
   210  		pkg, err := build.ImportDir(abs, 0)
   211  		if err != nil {
   212  			continue
   213  		}
   214  		return pkg, nil
   215  	}
   216  	return nil, fmt.Errorf("failed to find godog package in any of:\n%s", strings.Join(try, "\n"))
   217  }
   218  
   219  func importPackage(dir string) *build.Package {
   220  	pkg, _ := build.ImportDir(dir, 0)
   221  
   222  	// normalize import path for local import packages
   223  	// taken from go source code
   224  	// see: https://github.com/golang/go/blob/go1.7rc5/src/cmd/go/pkg.go#L279
   225  	if pkg != nil && pkg.ImportPath == "." {
   226  		pkg.ImportPath = path.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
   227  	}
   228  
   229  	return pkg
   230  }
   231  
   232  // from go src
   233  func makeImportValid(r rune) rune {
   234  	// Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
   235  	const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
   236  	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
   237  		return '_'
   238  	}
   239  	return r
   240  }
   241  
   242  func uniqStringList(strs []string) (unique []string) {
   243  	uniq := make(map[string]void, len(strs))
   244  	for _, s := range strs {
   245  		if _, ok := uniq[s]; !ok {
   246  			uniq[s] = void{}
   247  			unique = append(unique, s)
   248  		}
   249  	}
   250  	return
   251  }
   252  
   253  // buildTestMain if given package is valid
   254  // it scans test files for contexts
   255  // and produces a testmain source code.
   256  func buildTestMain(pkg *build.Package) ([]byte, bool, error) {
   257  	var contexts []string
   258  	var importPath string
   259  	name := "main"
   260  	if nil != pkg {
   261  		ctxs, err := processPackageTestFiles(
   262  			pkg.TestGoFiles,
   263  			pkg.XTestGoFiles,
   264  		)
   265  		if err != nil {
   266  			return nil, false, err
   267  		}
   268  		contexts = ctxs
   269  		importPath = pkg.ImportPath
   270  		name = pkg.Name
   271  	}
   272  
   273  	data := struct {
   274  		Name       string
   275  		Contexts   []string
   276  		ImportPath string
   277  	}{name, contexts, importPath}
   278  
   279  	var buf bytes.Buffer
   280  	if err := runnerTemplate.Execute(&buf, data); err != nil {
   281  		return nil, len(contexts) > 0, err
   282  	}
   283  	return buf.Bytes(), len(contexts) > 0, nil
   284  }
   285  
   286  // maybeVendorPaths determines possible vendor paths
   287  // which goes levels down from given directory
   288  // until it reaches GOPATH source dir
   289  func maybeVendorPaths(dir string) (paths []string) {
   290  	for _, gopath := range gopaths {
   291  		gopath = filepath.Join(gopath, "src")
   292  		for strings.HasPrefix(dir, gopath) && dir != gopath {
   293  			paths = append(paths, filepath.Join(dir, "vendor", godogImportPath))
   294  			dir = filepath.Dir(dir)
   295  		}
   296  	}
   297  	return
   298  }
   299  
   300  // processPackageTestFiles runs through ast of each test
   301  // file pack and looks for godog suite contexts to register
   302  // on run
   303  func processPackageTestFiles(packs ...[]string) ([]string, error) {
   304  	var ctxs []string
   305  	fset := token.NewFileSet()
   306  	for _, pack := range packs {
   307  		for _, testFile := range pack {
   308  			node, err := parser.ParseFile(fset, testFile, nil, 0)
   309  			if err != nil {
   310  				return ctxs, err
   311  			}
   312  
   313  			ctxs = append(ctxs, astContexts(node)...)
   314  		}
   315  	}
   316  	var failed []string
   317  	for _, ctx := range ctxs {
   318  		runes := []rune(ctx)
   319  		if unicode.IsLower(runes[0]) {
   320  			expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)
   321  			failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected)))
   322  		}
   323  	}
   324  	if len(failed) > 0 {
   325  		return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t"))
   326  	}
   327  	return ctxs, nil
   328  }
   329  
   330  func findToolDir() string {
   331  	if out, err := exec.Command("go", "env", "GOTOOLDIR").Output(); err != nil {
   332  		return filepath.Clean(strings.TrimSpace(string(out)))
   333  	}
   334  	return filepath.Clean(build.ToolDir)
   335  }