From 1c379d8d445883b8bfe4c981413d85a4c413e24f Mon Sep 17 00:00:00 2001 From: gedi Date: Thu, 14 Mar 2019 15:39:06 +0200 Subject: [PATCH] resolve issues with godog package import when using cmd --- builder_go110.go | 373 ++++++++++++++--------------------------------- 1 file changed, 110 insertions(+), 263 deletions(-) diff --git a/builder_go110.go b/builder_go110.go index 304e7b6..81e45bb 100644 --- a/builder_go110.go +++ b/builder_go110.go @@ -56,27 +56,16 @@ func main() { }) os.Exit(status) }`)) + + // temp file for import + tempFileTemplate = template.Must(template.New("temp").Parse(`package {{.Name}} + +import "github.com/DATA-DOG/godog" + +var _ = godog.Version +`)) ) -type module struct { - Path, Dir string -} - -func (mod *module) match(name string) *build.Package { - if strings.Index(name, mod.Path) == -1 { - return nil - } - - suffix := strings.Replace(name, mod.Path, "", 1) - add := strings.Replace(suffix, "/", string(filepath.Separator), -1) - pkg, err := build.ImportDir(mod.Dir+add, 0) - if err != nil { - return nil - } - - return pkg -} - // Build creates a test package like go test command at given target path. // If there are no go files in tested directory, then // it simply builds a godog executable to scan features. @@ -98,62 +87,69 @@ func Build(bin string) error { // we allow package to be nil, if godog is run only when // there is a feature file in empty directory pkg := importPackage(abs) - src, anyContexts, err := buildTestMain(pkg) + src, err := buildTestMain(pkg) if err != nil { return err } - workdir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano()) - testdir := workdir + // may need to produce temp file for godog dependency + srcTemp, err := buildTempFile(pkg) + if err != nil { + return err + } - // if none of test files exist, or there are no contexts found - // we will skip test package compilation, since it is useless - if anyContexts { - // build and compile the tested package. - // generated test executable will be removed - // since we do not need it for godog suite. - // we also print back the temp WORK directory - // go has built. We will reuse it for our suite workdir. - temp := fmt.Sprintf(filepath.Join("%s", "temp-%d.test"), os.TempDir(), time.Now().UnixNano()) - out, err := exec.Command("go", "test", "-c", "-work", "-o", temp).CombinedOutput() + if srcTemp != nil { + // @TODO: in case of modules we cannot build it our selves, we need to have this hacky option + pathTemp := filepath.Join(abs, "godog_dependency_file_test.go") + err = ioutil.WriteFile(pathTemp, srcTemp, 0644) if err != nil { - return fmt.Errorf("failed to compile tested package: %s, reason: %v, output: %s", pkg.Name, err, string(out)) - } - defer os.Remove(temp) - - // extract go-build temporary directory as our workdir - lines := strings.Split(strings.TrimSpace(string(out)), "\n") - // it may have some compilation warnings, in the output, but these are not - // considered to be errors, since command exit status is 0 - for _, ln := range lines { - if !strings.HasPrefix(ln, "WORK=") { - continue - } - workdir = strings.Replace(ln, "WORK=", "", 1) - break - } - - // may not locate it in output - if workdir == testdir { - return fmt.Errorf("expected WORK dir path to be present in output: %s", string(out)) - } - - // check whether workdir exists - stats, err := os.Stat(workdir) - if os.IsNotExist(err) { - return fmt.Errorf("expected WORK dir: %s to be available", workdir) - } - - if !stats.IsDir() { - return fmt.Errorf("expected WORK dir: %s to be directory", workdir) - } - testdir = filepath.Join(workdir, "b001") - } else { - // still need to create temporary workdir - if err = os.MkdirAll(testdir, 0755); err != nil { return err } + defer os.Remove(pathTemp) } + + workdir := "" + testdir := workdir + + // build and compile the tested package. + // generated test executable will be removed + // since we do not need it for godog suite. + // we also print back the temp WORK directory + // go has built. We will reuse it for our suite workdir. + temp := fmt.Sprintf(filepath.Join("%s", "temp-%d.test"), os.TempDir(), time.Now().UnixNano()) + testOutput, err := exec.Command("go", "test", "-c", "-work", "-o", temp).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to compile tested package: %s, reason: %v, output: %s", abs, err, string(testOutput)) + } + defer os.Remove(temp) + + // extract go-build temporary directory as our workdir + linesOut := strings.Split(strings.TrimSpace(string(testOutput)), "\n") + // it may have some compilation warnings, in the output, but these are not + // considered to be errors, since command exit status is 0 + for _, ln := range linesOut { + if !strings.HasPrefix(ln, "WORK=") { + continue + } + workdir = strings.Replace(ln, "WORK=", "", 1) + break + } + + // may not locate it in output + if workdir == testdir { + return fmt.Errorf("expected WORK dir path to be present in output: %s", string(testOutput)) + } + + // check whether workdir exists + stats, err := os.Stat(workdir) + if os.IsNotExist(err) { + return fmt.Errorf("expected WORK dir: %s to be available", workdir) + } + + if !stats.IsDir() { + return fmt.Errorf("expected WORK dir: %s to be directory", workdir) + } + testdir = filepath.Join(workdir, "b001") defer os.RemoveAll(workdir) // replace _testmain.go file with our own @@ -163,83 +159,17 @@ func Build(bin string) error { return err } - mods := readModules() - // if it was not located as module - // we look it up in available source paths - // including vendor directory, supported since 1.5. - godogPkg, err := locatePackage(godogImportPath, mods) - if err != nil { - return err - } - - if !isModule(godogImportPath, mods) { - // must make sure that package is installed - // modules are installed on download - cmd := exec.Command("go", "install", "-i", godogPkg.ImportPath) - cmd.Env = os.Environ() - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to install godog package: %s, reason: %v", string(out), err) - } - } - // compile godog testmain package archive // we do not depend on CGO so a lot of checks are not necessary + cfg := filepath.Join(testdir, "importcfg.link") testMainPkgOut := filepath.Join(testdir, "main.a") args := []string{ "-o", testMainPkgOut, + "-importcfg", cfg, "-p", "main", "-complete", } - cfg := filepath.Join(testdir, "importcfg.link") - args = append(args, "-importcfg", cfg) - if _, err := os.Stat(cfg); err != nil { - // there were no go sources in the directory - // so we need to build all dependency tree ourselves - in, err := os.Create(cfg) - if err != nil { - return err - } - fmt.Fprintln(in, "# import config") - - deps := make(map[string]string) - if err := dependencies(godogPkg, mods, deps, false); err != nil { - in.Close() - return err - } - - for pkgName, pkgObj := range deps { - if i := strings.LastIndex(pkgName, "vendor/"); i != -1 { - name := pkgName[i+7:] - fmt.Fprintf(in, "importmap %s=%s\n", name, pkgName) - } - fmt.Fprintf(in, "packagefile %s=%s\n", pkgName, pkgObj) - } - in.Close() - } else { - // need to make sure that vendor dependencies are mapped - in, err := os.OpenFile(cfg, os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return err - } - deps := make(map[string]string) - if err := dependencies(pkg, mods, deps, true); err != nil { - in.Close() - return err - } - if err := dependencies(godogPkg, mods, deps, false); err != nil { - in.Close() - return err - } - for pkgName := range deps { - if i := strings.LastIndex(pkgName, "vendor/"); i != -1 { - name := pkgName[i+7:] - fmt.Fprintf(in, "importmap %s=%s\n", name, pkgName) - } - } - in.Close() - } - args = append(args, "-pack", testmain) cmd := exec.Command(compiler, args...) cmd.Env = os.Environ() @@ -258,24 +188,6 @@ func Build(bin string) error { cmd = exec.Command(linker, args...) cmd.Env = os.Environ() - // in case if build is without contexts, need to remove import maps - data, err := ioutil.ReadFile(cfg) - if err != nil { - return err - } - - lines := strings.Split(string(data), "\n") - var fixed []string - for _, line := range lines { - if strings.Index(line, "importmap") == 0 { - continue - } - fixed = append(fixed, line) - } - if err := ioutil.WriteFile(cfg, []byte(strings.Join(fixed, "\n")), 0600); err != nil { - return err - } - out, err = cmd.CombinedOutput() if err != nil { msg := `failed to link test executable: @@ -287,50 +199,6 @@ func Build(bin string) error { return nil } -func locatePackage(name string, mods []*module) (*build.Package, error) { - // search vendor paths first since that takes priority - dir, err := filepath.Abs(".") - if err != nil { - return nil, err - } - - // first of all check modules - if mods != nil { - for _, mod := range mods { - if pkg := mod.match(name); pkg != nil { - return pkg, nil - } - } - } - - for _, gopath := range gopaths { - gopath = filepath.Join(gopath, "src") - for strings.HasPrefix(dir, gopath) && dir != gopath { - pkg, err := build.ImportDir(filepath.Join(dir, "vendor", name), 0) - if err != nil { - dir = filepath.Dir(dir) - continue - } - return pkg, nil - } - } - - // search source paths otherwise - for _, p := range build.Default.SrcDirs() { - abs, err := filepath.Abs(filepath.Join(p, name)) - if err != nil { - continue - } - pkg, err := build.ImportDir(abs, 0) - if err != nil { - continue - } - return pkg, nil - } - - return nil, fmt.Errorf("failed to find %s package in any of:\n%s", name, strings.Join(build.Default.SrcDirs(), "\n")) -} - func importPackage(dir string) *build.Package { pkg, _ := build.ImportDir(dir, 0) @@ -354,10 +222,51 @@ func makeImportValid(r rune) rune { return r } +// build temporary file content if godog +// package is not present in currently tested package +func buildTempFile(pkg *build.Package) ([]byte, error) { + shouldBuild := true + var name string + if pkg != nil { + name = pkg.Name + all := pkg.Imports + all = append(all, pkg.TestImports...) + all = append(all, pkg.XTestImports...) + for _, imp := range all { + if imp == godogImportPath { + shouldBuild = false + break + } + } + + // maybe we are testing the godog package on it's own + if name == "godog" { + if parseImport(pkg.ImportPath, pkg.Root) == godogImportPath { + shouldBuild = false + } + } + } + + if name == "" { + name = "main" + } + + if !shouldBuild { + return nil, nil + } + + data := struct{ Name string }{name} + var buf bytes.Buffer + if err := tempFileTemplate.Execute(&buf, data); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + // buildTestMain if given package is valid // it scans test files for contexts // and produces a testmain source code. -func buildTestMain(pkg *build.Package) ([]byte, bool, error) { +func buildTestMain(pkg *build.Package) ([]byte, error) { var ( contexts []string xcontexts []string @@ -367,11 +276,11 @@ func buildTestMain(pkg *build.Package) ([]byte, bool, error) { if nil != pkg { contexts, err = processPackageTestFiles(pkg.TestGoFiles) if err != nil { - return nil, false, err + return nil, err } xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles) if err != nil { - return nil, false, err + return nil, err } importPath = parseImport(pkg.ImportPath, pkg.Root) name = pkg.Name @@ -390,12 +299,11 @@ func buildTestMain(pkg *build.Package) ([]byte, bool, error) { ImportPath: importPath, } - hasContext := len(contexts) > 0 || len(xcontexts) > 0 var buf bytes.Buffer if err = runnerTemplate.Execute(&buf, data); err != nil { - return nil, hasContext, err + return nil, err } - return buf.Bytes(), hasContext, nil + return buf.Bytes(), nil } // parseImport parses the import path to deal with go module. @@ -468,64 +376,3 @@ func findToolDir() string { } return filepath.Clean(build.ToolDir) } - -func dependencies(pkg *build.Package, mods []*module, visited map[string]string, vendor bool) error { - visited[pkg.ImportPath] = pkg.PkgObj - imports := pkg.Imports - if vendor { - imports = append(imports, pkg.TestImports...) - } - for _, name := range imports { - if i := strings.LastIndex(name, "vendor/"); vendor && i == -1 { - continue // only interested in vendor packages - } - if _, ok := visited[name]; ok { - continue - } - - next, err := locatePackage(name, mods) - if err != nil { - return err - } - - visited[name] = pkg.PkgObj - if err := dependencies(next, mods, visited, vendor); err != nil { - return err - } - } - return nil -} - -func readModules() []*module { - // for module support, query the module import path - out, err := exec.Command("go", "mod", "download", "-json").Output() - if err != nil { - // Unable to read stdout - return nil - } - - var mods []*module - reader := json.NewDecoder(bytes.NewReader(out)) - for { - var mod *module - if err := reader.Decode(&mod); err != nil { - break // might be also EOF - } - mods = append(mods, mod) - } - return mods -} - -func isModule(name string, mods []*module) bool { - if mods == nil { - return false - } - - for _, mod := range mods { - if pkg := mod.match(name); pkg != nil { - return true - } - } - - return false -}