github.com/data-DOG/godog@v0.7.9/builder_go110.go (about)

     1  // +build go1.10
     2  
     3  package godog
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"fmt"
     9  	"go/build"
    10  	"go/parser"
    11  	"go/token"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path"
    16  	"path/filepath"
    17  	"strings"
    18  	"text/template"
    19  	"time"
    20  	"unicode"
    21  )
    22  
    23  var (
    24  	tooldir         = findToolDir()
    25  	compiler        = filepath.Join(tooldir, "compile")
    26  	linker          = filepath.Join(tooldir, "link")
    27  	gopaths         = filepath.SplitList(build.Default.GOPATH)
    28  	godogImportPath = "github.com/DATA-DOG/godog"
    29  
    30  	// godep
    31  	runnerTemplate = template.Must(template.New("testmain").Parse(`package main
    32  
    33  import (
    34  	"github.com/DATA-DOG/godog"
    35  	{{if .Contexts}}_test "{{.ImportPath}}"{{end}}
    36  	{{if .XContexts}}_xtest "{{.ImportPath}}_test"{{end}}
    37  	{{if .XContexts}}"testing/internal/testdeps"{{end}}
    38  	"os"
    39  )
    40  
    41  {{if .XContexts}}
    42  func init() {
    43  	testdeps.ImportPath = "{{.ImportPath}}"
    44  }
    45  {{end}}
    46  
    47  func main() {
    48  	status := godog.Run("{{ .Name }}", func (suite *godog.Suite) {
    49  		os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}")
    50  		{{range .Contexts}}
    51  			_test.{{ . }}(suite)
    52  		{{end}}
    53  		{{range .XContexts}}
    54  			_xtest.{{ . }}(suite)
    55  		{{end}}
    56  	})
    57  	os.Exit(status)
    58  }`))
    59  )
    60  
    61  type module struct {
    62  	Path, Dir string
    63  }
    64  
    65  func (mod *module) match(name string) *build.Package {
    66  	if strings.Index(name, mod.Path) == -1 {
    67  		return nil
    68  	}
    69  
    70  	suffix := strings.Replace(name, mod.Path, "", 1)
    71  	add := strings.Replace(suffix, "/", string(filepath.Separator), -1)
    72  	pkg, err := build.ImportDir(mod.Dir+add, 0)
    73  	if err != nil {
    74  		return nil
    75  	}
    76  
    77  	return pkg
    78  }
    79  
    80  // Build creates a test package like go test command at given target path.
    81  // If there are no go files in tested directory, then
    82  // it simply builds a godog executable to scan features.
    83  //
    84  // If there are go test files, it first builds a test
    85  // package with standard go test command.
    86  //
    87  // Finally it generates godog suite executable which
    88  // registers exported godog contexts from the test files
    89  // of tested package.
    90  //
    91  // Returns the path to generated executable
    92  func Build(bin string) error {
    93  	abs, err := filepath.Abs(".")
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	// we allow package to be nil, if godog is run only when
    99  	// there is a feature file in empty directory
   100  	pkg := importPackage(abs)
   101  	src, anyContexts, err := buildTestMain(pkg)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	workdir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano())
   107  	testdir := workdir
   108  
   109  	// if none of test files exist, or there are no contexts found
   110  	// we will skip test package compilation, since it is useless
   111  	if anyContexts {
   112  		// build and compile the tested package.
   113  		// generated test executable will be removed
   114  		// since we do not need it for godog suite.
   115  		// we also print back the temp WORK directory
   116  		// go has built. We will reuse it for our suite workdir.
   117  		temp := fmt.Sprintf(filepath.Join("%s", "temp-%d.test"), os.TempDir(), time.Now().UnixNano())
   118  		out, err := exec.Command("go", "test", "-c", "-work", "-o", temp).CombinedOutput()
   119  		if err != nil {
   120  			return fmt.Errorf("failed to compile tested package: %s, reason: %v, output: %s", pkg.Name, err, string(out))
   121  		}
   122  		defer os.Remove(temp)
   123  
   124  		// extract go-build temporary directory as our workdir
   125  		lines := strings.Split(strings.TrimSpace(string(out)), "\n")
   126  		// it may have some compilation warnings, in the output, but these are not
   127  		// considered to be errors, since command exit status is 0
   128  		for _, ln := range lines {
   129  			if !strings.HasPrefix(ln, "WORK=") {
   130  				continue
   131  			}
   132  			workdir = strings.Replace(ln, "WORK=", "", 1)
   133  			break
   134  		}
   135  
   136  		// may not locate it in output
   137  		if workdir == testdir {
   138  			return fmt.Errorf("expected WORK dir path to be present in output: %s", string(out))
   139  		}
   140  
   141  		// check whether workdir exists
   142  		stats, err := os.Stat(workdir)
   143  		if os.IsNotExist(err) {
   144  			return fmt.Errorf("expected WORK dir: %s to be available", workdir)
   145  		}
   146  
   147  		if !stats.IsDir() {
   148  			return fmt.Errorf("expected WORK dir: %s to be directory", workdir)
   149  		}
   150  		testdir = filepath.Join(workdir, "b001")
   151  	} else {
   152  		// still need to create temporary workdir
   153  		if err = os.MkdirAll(testdir, 0755); err != nil {
   154  			return err
   155  		}
   156  	}
   157  	defer os.RemoveAll(workdir)
   158  
   159  	// replace _testmain.go file with our own
   160  	testmain := filepath.Join(testdir, "_testmain.go")
   161  	err = ioutil.WriteFile(testmain, src, 0644)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	mods := readModules()
   167  	// if it was not located as module
   168  	// we look it up in available source paths
   169  	// including vendor directory, supported since 1.5.
   170  	godogPkg, err := locatePackage(godogImportPath, mods)
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	if !isModule(godogImportPath, mods) {
   176  		// must make sure that package is installed
   177  		// modules are installed on download
   178  		cmd := exec.Command("go", "install", "-i", godogPkg.ImportPath)
   179  		cmd.Env = os.Environ()
   180  		if out, err := cmd.CombinedOutput(); err != nil {
   181  			return fmt.Errorf("failed to install godog package: %s, reason: %v", string(out), err)
   182  		}
   183  	}
   184  
   185  	// compile godog testmain package archive
   186  	// we do not depend on CGO so a lot of checks are not necessary
   187  	testMainPkgOut := filepath.Join(testdir, "main.a")
   188  	args := []string{
   189  		"-o", testMainPkgOut,
   190  		"-p", "main",
   191  		"-complete",
   192  	}
   193  
   194  	cfg := filepath.Join(testdir, "importcfg.link")
   195  	args = append(args, "-importcfg", cfg)
   196  	if _, err := os.Stat(cfg); err != nil {
   197  		// there were no go sources in the directory
   198  		// so we need to build all dependency tree ourselves
   199  		in, err := os.Create(cfg)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		fmt.Fprintln(in, "# import config")
   204  
   205  		deps := make(map[string]string)
   206  		if err := dependencies(godogPkg, mods, deps, false); err != nil {
   207  			in.Close()
   208  			return err
   209  		}
   210  
   211  		for pkgName, pkgObj := range deps {
   212  			if i := strings.LastIndex(pkgName, "vendor/"); i != -1 {
   213  				name := pkgName[i+7:]
   214  				fmt.Fprintf(in, "importmap %s=%s\n", name, pkgName)
   215  			}
   216  			fmt.Fprintf(in, "packagefile %s=%s\n", pkgName, pkgObj)
   217  		}
   218  		in.Close()
   219  	} else {
   220  		// need to make sure that vendor dependencies are mapped
   221  		in, err := os.OpenFile(cfg, os.O_APPEND|os.O_WRONLY, 0600)
   222  		if err != nil {
   223  			return err
   224  		}
   225  		deps := make(map[string]string)
   226  		if err := dependencies(pkg, mods, deps, true); err != nil {
   227  			in.Close()
   228  			return err
   229  		}
   230  		if err := dependencies(godogPkg, mods, deps, false); err != nil {
   231  			in.Close()
   232  			return err
   233  		}
   234  		for pkgName := range deps {
   235  			if i := strings.LastIndex(pkgName, "vendor/"); i != -1 {
   236  				name := pkgName[i+7:]
   237  				fmt.Fprintf(in, "importmap %s=%s\n", name, pkgName)
   238  			}
   239  		}
   240  		in.Close()
   241  	}
   242  
   243  	args = append(args, "-pack", testmain)
   244  	cmd := exec.Command(compiler, args...)
   245  	cmd.Env = os.Environ()
   246  	out, err := cmd.CombinedOutput()
   247  	if err != nil {
   248  		return fmt.Errorf("failed to compile testmain package: %v - output: %s", err, string(out))
   249  	}
   250  
   251  	// link test suite executable
   252  	args = []string{
   253  		"-o", bin,
   254  		"-importcfg", cfg,
   255  		"-buildmode=exe",
   256  	}
   257  	args = append(args, testMainPkgOut)
   258  	cmd = exec.Command(linker, args...)
   259  	cmd.Env = os.Environ()
   260  
   261  	// in case if build is without contexts, need to remove import maps
   262  	data, err := ioutil.ReadFile(cfg)
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	lines := strings.Split(string(data), "\n")
   268  	var fixed []string
   269  	for _, line := range lines {
   270  		if strings.Index(line, "importmap") == 0 {
   271  			continue
   272  		}
   273  		fixed = append(fixed, line)
   274  	}
   275  	if err := ioutil.WriteFile(cfg, []byte(strings.Join(fixed, "\n")), 0600); err != nil {
   276  		return err
   277  	}
   278  
   279  	out, err = cmd.CombinedOutput()
   280  	if err != nil {
   281  		msg := `failed to link test executable:
   282  	reason: %s
   283  	command: %s`
   284  		return fmt.Errorf(msg, string(out), linker+" '"+strings.Join(args, "' '")+"'")
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  func locatePackage(name string, mods []*module) (*build.Package, error) {
   291  	// search vendor paths first since that takes priority
   292  	dir, err := filepath.Abs(".")
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  
   297  	// first of all check modules
   298  	if mods != nil {
   299  		for _, mod := range mods {
   300  			if pkg := mod.match(name); pkg != nil {
   301  				return pkg, nil
   302  			}
   303  		}
   304  	}
   305  
   306  	for _, gopath := range gopaths {
   307  		gopath = filepath.Join(gopath, "src")
   308  		for strings.HasPrefix(dir, gopath) && dir != gopath {
   309  			pkg, err := build.ImportDir(filepath.Join(dir, "vendor", name), 0)
   310  			if err != nil {
   311  				dir = filepath.Dir(dir)
   312  				continue
   313  			}
   314  			return pkg, nil
   315  		}
   316  	}
   317  
   318  	// search source paths otherwise
   319  	for _, p := range build.Default.SrcDirs() {
   320  		abs, err := filepath.Abs(filepath.Join(p, name))
   321  		if err != nil {
   322  			continue
   323  		}
   324  		pkg, err := build.ImportDir(abs, 0)
   325  		if err != nil {
   326  			continue
   327  		}
   328  		return pkg, nil
   329  	}
   330  
   331  	return nil, fmt.Errorf("failed to find %s package in any of:\n%s", name, strings.Join(build.Default.SrcDirs(), "\n"))
   332  }
   333  
   334  func importPackage(dir string) *build.Package {
   335  	pkg, _ := build.ImportDir(dir, 0)
   336  
   337  	// normalize import path for local import packages
   338  	// taken from go source code
   339  	// see: https://github.com/golang/go/blob/go1.7rc5/src/cmd/go/pkg.go#L279
   340  	if pkg != nil && pkg.ImportPath == "." {
   341  		pkg.ImportPath = path.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
   342  	}
   343  
   344  	return pkg
   345  }
   346  
   347  // from go src
   348  func makeImportValid(r rune) rune {
   349  	// Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
   350  	const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
   351  	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
   352  		return '_'
   353  	}
   354  	return r
   355  }
   356  
   357  // buildTestMain if given package is valid
   358  // it scans test files for contexts
   359  // and produces a testmain source code.
   360  func buildTestMain(pkg *build.Package) ([]byte, bool, error) {
   361  	var (
   362  		contexts         []string
   363  		xcontexts        []string
   364  		err              error
   365  		name, importPath string
   366  	)
   367  	if nil != pkg {
   368  		contexts, err = processPackageTestFiles(pkg.TestGoFiles)
   369  		if err != nil {
   370  			return nil, false, err
   371  		}
   372  		xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles)
   373  		if err != nil {
   374  			return nil, false, err
   375  		}
   376  		importPath = parseImport(pkg.ImportPath, pkg.Root)
   377  		name = pkg.Name
   378  	} else {
   379  		name = "main"
   380  	}
   381  	data := struct {
   382  		Name       string
   383  		Contexts   []string
   384  		XContexts  []string
   385  		ImportPath string
   386  	}{
   387  		Name:       name,
   388  		Contexts:   contexts,
   389  		XContexts:  xcontexts,
   390  		ImportPath: importPath,
   391  	}
   392  
   393  	hasContext := len(contexts) > 0 || len(xcontexts) > 0
   394  	var buf bytes.Buffer
   395  	if err = runnerTemplate.Execute(&buf, data); err != nil {
   396  		return nil, hasContext, err
   397  	}
   398  	return buf.Bytes(), hasContext, nil
   399  }
   400  
   401  // parseImport parses the import path to deal with go module.
   402  func parseImport(rawPath, rootPath string) string {
   403  	// with go > 1.11 and go module enabled out of the GOPATH,
   404  	// the import path begins with an underscore and the GOPATH is unknown on build.
   405  	if rootPath != "" {
   406  		// go < 1.11 or it's a module inside the GOPATH
   407  		return rawPath
   408  	}
   409  	// for module support, query the module import path
   410  	cmd := exec.Command("go", "list", "-m", "-json")
   411  	out, err := cmd.StdoutPipe()
   412  	if err != nil {
   413  		// Unable to read stdout
   414  		return rawPath
   415  	}
   416  	if cmd.Start() != nil {
   417  		// Does not using modules
   418  		return rawPath
   419  	}
   420  	var mod struct {
   421  		Dir  string `json:"Dir"`
   422  		Path string `json:"Path"`
   423  	}
   424  	if json.NewDecoder(out).Decode(&mod) != nil {
   425  		// Unexpected result
   426  		return rawPath
   427  	}
   428  	if cmd.Wait() != nil {
   429  		return rawPath
   430  	}
   431  	// Concatenates the module path with the current sub-folders if needed
   432  	return mod.Path + filepath.ToSlash(strings.TrimPrefix(strings.TrimPrefix(rawPath, "_"), mod.Dir))
   433  }
   434  
   435  // processPackageTestFiles runs through ast of each test
   436  // file pack and looks for godog suite contexts to register
   437  // on run
   438  func processPackageTestFiles(packs ...[]string) ([]string, error) {
   439  	var ctxs []string
   440  	fset := token.NewFileSet()
   441  	for _, pack := range packs {
   442  		for _, testFile := range pack {
   443  			node, err := parser.ParseFile(fset, testFile, nil, 0)
   444  			if err != nil {
   445  				return ctxs, err
   446  			}
   447  
   448  			ctxs = append(ctxs, astContexts(node)...)
   449  		}
   450  	}
   451  	var failed []string
   452  	for _, ctx := range ctxs {
   453  		runes := []rune(ctx)
   454  		if unicode.IsLower(runes[0]) {
   455  			expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)
   456  			failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected)))
   457  		}
   458  	}
   459  	if len(failed) > 0 {
   460  		return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t"))
   461  	}
   462  	return ctxs, nil
   463  }
   464  
   465  func findToolDir() string {
   466  	if out, err := exec.Command("go", "env", "GOTOOLDIR").Output(); err != nil {
   467  		return filepath.Clean(strings.TrimSpace(string(out)))
   468  	}
   469  	return filepath.Clean(build.ToolDir)
   470  }
   471  
   472  func dependencies(pkg *build.Package, mods []*module, visited map[string]string, vendor bool) error {
   473  	visited[pkg.ImportPath] = pkg.PkgObj
   474  	imports := pkg.Imports
   475  	if vendor {
   476  		imports = append(imports, pkg.TestImports...)
   477  	}
   478  	for _, name := range imports {
   479  		if i := strings.LastIndex(name, "vendor/"); vendor && i == -1 {
   480  			continue // only interested in vendor packages
   481  		}
   482  		if _, ok := visited[name]; ok {
   483  			continue
   484  		}
   485  
   486  		next, err := locatePackage(name, mods)
   487  		if err != nil {
   488  			return err
   489  		}
   490  
   491  		visited[name] = pkg.PkgObj
   492  		if err := dependencies(next, mods, visited, vendor); err != nil {
   493  			return err
   494  		}
   495  	}
   496  	return nil
   497  }
   498  
   499  func readModules() []*module {
   500  	// for module support, query the module import path
   501  	out, err := exec.Command("go", "mod", "download", "-json").Output()
   502  	if err != nil {
   503  		// Unable to read stdout
   504  		return nil
   505  	}
   506  
   507  	var mods []*module
   508  	reader := json.NewDecoder(bytes.NewReader(out))
   509  	for {
   510  		var mod *module
   511  		if err := reader.Decode(&mod); err != nil {
   512  			break // might be also EOF
   513  		}
   514  		mods = append(mods, mod)
   515  	}
   516  	return mods
   517  }
   518  
   519  func isModule(name string, mods []*module) bool {
   520  	if mods == nil {
   521  		return false
   522  	}
   523  
   524  	for _, mod := range mods {
   525  		if pkg := mod.match(name); pkg != nil {
   526  			return true
   527  		}
   528  	}
   529  
   530  	return false
   531  }