From 49701cf8901a7ba93741a057c01e117a77a2da19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnblad?= Date: Thu, 27 Feb 2020 14:49:42 -0300 Subject: [PATCH 1/3] Added support for go1.14 and removed support for go1.10 and go1.11 --- .circleci/config.yml | 20 +-- builder.go | 2 - builder_go110.go | 417 ------------------------------------------- 3 files changed, 10 insertions(+), 429 deletions(-) delete mode 100644 builder_go110.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c16440..7cf2960 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,15 +4,15 @@ orbs: codecov: codecov/codecov@1.0.5 executors: - exec_go_1_11: - docker: - - image: circleci/golang:1.11.13 exec_go_1_12: docker: - image: circleci/golang:1.12.16 exec_go_1_13: docker: - - image: circleci/golang:1.13.7 + - image: circleci/golang:1.13.8 + exec_go_1_14: + docker: + - image: circleci/golang:1.14.0 commands: install: @@ -62,11 +62,6 @@ commands: - coverage jobs: - go1_11: - working_directory: /go/src/github.com/cucumber/godog - executor: exec_go_1_11 - steps: - - all go1_12: working_directory: /go/src/github.com/cucumber/godog executor: exec_go_1_12 @@ -77,11 +72,16 @@ jobs: executor: exec_go_1_13 steps: - all + go1_14: + working_directory: /go/src/github.com/cucumber/godog + executor: exec_go_1_14 + steps: + - all workflows: version: 2 test: jobs: - - go1_11 - go1_12 - go1_13 + - go1_14 diff --git a/builder.go b/builder.go index 7663521..4feef5d 100644 --- a/builder.go +++ b/builder.go @@ -1,5 +1,3 @@ -// +build !go1.10 - package godog import ( diff --git a/builder_go110.go b/builder_go110.go deleted file mode 100644 index cf8f0cb..0000000 --- a/builder_go110.go +++ /dev/null @@ -1,417 +0,0 @@ -// +build go1.10 - -package godog - -import ( - "bytes" - "encoding/json" - "fmt" - "go/build" - "go/parser" - "go/token" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "text/template" - "time" - "unicode" -) - -var ( - tooldir = findToolDir() - compiler = filepath.Join(tooldir, "compile") - linker = filepath.Join(tooldir, "link") - gopaths = filepath.SplitList(build.Default.GOPATH) - godogImportPath = "github.com/cucumber/godog" - - // godep - runnerTemplate = template.Must(template.New("testmain").Parse(`package main - -import ( - "github.com/cucumber/godog" - {{if .Contexts}}_test "{{.ImportPath}}"{{end}} - {{if .XContexts}}_xtest "{{.ImportPath}}_test"{{end}} - {{if .XContexts}}"testing/internal/testdeps"{{end}} - "os" -) - -{{if .XContexts}} -func init() { - testdeps.ImportPath = "{{.ImportPath}}" -} -{{end}} - -func main() { - status := godog.Run("{{ .Name }}", func (suite *godog.Suite) { - os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}") - {{range .Contexts}} - _test.{{ . }}(suite) - {{end}} - {{range .XContexts}} - _xtest.{{ . }}(suite) - {{end}} - }) - os.Exit(status) -}`)) - - // temp file for import - tempFileTemplate = template.Must(template.New("temp").Parse(`package {{.Name}} - -import "github.com/cucumber/godog" - -var _ = godog.Version -`)) -) - -// 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. -// -// If there are go test files, it first builds a test -// package with standard go test command. -// -// Finally it generates godog suite executable which -// registers exported godog contexts from the test files -// of tested package. -// -// Returns the path to generated executable -func Build(bin string) error { - abs, err := filepath.Abs(".") - if err != nil { - return err - } - - // we allow package to be nil, if godog is run only when - // there is a feature file in empty directory - pkg := importPackage(abs) - src, err := buildTestMain(pkg) - if err != nil { - return err - } - - // may need to produce temp file for godog dependency - srcTemp, err := buildTempFile(pkg) - if err != nil { - return err - } - - 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 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 - testmain := filepath.Join(testdir, "_testmain.go") - err = ioutil.WriteFile(testmain, src, 0644) - if err != nil { - return err - } - - // godog package may be vendored and may need importmap - vendored := maybeVendoredGodog() - - // compile godog testmain package archive - // we do not depend on CGO so a lot of checks are not necessary - linkerCfg := filepath.Join(testdir, "importcfg.link") - compilerCfg := linkerCfg - if vendored != nil { - data, err := ioutil.ReadFile(linkerCfg) - if err != nil { - return err - } - - data = append(data, []byte(fmt.Sprintf("importmap %s=%s\n", godogImportPath, vendored.ImportPath))...) - compilerCfg = filepath.Join(testdir, "importcfg") - - err = ioutil.WriteFile(compilerCfg, data, 0644) - if err != nil { - return err - } - } - - testMainPkgOut := filepath.Join(testdir, "main.a") - args := []string{ - "-o", testMainPkgOut, - "-importcfg", compilerCfg, - "-p", "main", - "-complete", - } - - args = append(args, "-pack", testmain) - cmd := exec.Command(compiler, args...) - cmd.Env = os.Environ() - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("failed to compile testmain package: %v - output: %s", err, string(out)) - } - - // link test suite executable - args = []string{ - "-o", bin, - "-importcfg", linkerCfg, - "-buildmode=exe", - } - args = append(args, testMainPkgOut) - cmd = exec.Command(linker, args...) - cmd.Env = os.Environ() - - out, err = cmd.CombinedOutput() - if err != nil { - msg := `failed to link test executable: - reason: %s - command: %s` - return fmt.Errorf(msg, string(out), linker+" '"+strings.Join(args, "' '")+"'") - } - - return nil -} - -func maybeVendoredGodog() *build.Package { - dir, err := filepath.Abs(".") - if err != nil { - return 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", godogImportPath), 0) - if err != nil { - dir = filepath.Dir(dir) - continue - } - return pkg - } - } - return nil -} - -func importPackage(dir string) *build.Package { - pkg, _ := build.ImportDir(dir, 0) - - // normalize import path for local import packages - // taken from go source code - // see: https://github.com/golang/go/blob/go1.7rc5/src/cmd/go/pkg.go#L279 - if pkg != nil && pkg.ImportPath == "." { - pkg.ImportPath = path.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir))) - } - - return pkg -} - -// from go src -func makeImportValid(r rune) rune { - // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport. - const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD" - if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) { - return '_' - } - 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, error) { - var ( - contexts []string - xcontexts []string - err error - name, importPath string - ) - if nil != pkg { - contexts, err = processPackageTestFiles(pkg.TestGoFiles) - if err != nil { - return nil, err - } - xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles) - if err != nil { - return nil, err - } - importPath = parseImport(pkg.ImportPath, pkg.Root) - name = pkg.Name - } else { - name = "main" - } - data := struct { - Name string - Contexts []string - XContexts []string - ImportPath string - }{ - Name: name, - Contexts: contexts, - XContexts: xcontexts, - ImportPath: importPath, - } - - var buf bytes.Buffer - if err = runnerTemplate.Execute(&buf, data); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -// parseImport parses the import path to deal with go module. -func parseImport(rawPath, rootPath string) string { - // with go > 1.11 and go module enabled out of the GOPATH, - // the import path begins with an underscore and the GOPATH is unknown on build. - if rootPath != "" { - // go < 1.11 or it's a module inside the GOPATH - return rawPath - } - // for module support, query the module import path - cmd := exec.Command("go", "list", "-m", "-json") - out, err := cmd.StdoutPipe() - if err != nil { - // Unable to read stdout - return rawPath - } - if cmd.Start() != nil { - // Does not using modules - return rawPath - } - var mod struct { - Dir string `json:"Dir"` - Path string `json:"Path"` - } - if json.NewDecoder(out).Decode(&mod) != nil { - // Unexpected result - return rawPath - } - if cmd.Wait() != nil { - return rawPath - } - // Concatenates the module path with the current sub-folders if needed - return mod.Path + filepath.ToSlash(strings.TrimPrefix(strings.TrimPrefix(rawPath, "_"), mod.Dir)) -} - -// processPackageTestFiles runs through ast of each test -// file pack and looks for godog suite contexts to register -// on run -func processPackageTestFiles(packs ...[]string) ([]string, error) { - var ctxs []string - fset := token.NewFileSet() - for _, pack := range packs { - for _, testFile := range pack { - node, err := parser.ParseFile(fset, testFile, nil, 0) - if err != nil { - return ctxs, err - } - - ctxs = append(ctxs, astContexts(node)...) - } - } - var failed []string - for _, ctx := range ctxs { - runes := []rune(ctx) - if unicode.IsLower(runes[0]) { - expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...) - failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected))) - } - } - if len(failed) > 0 { - return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t")) - } - return ctxs, nil -} - -func findToolDir() string { - if out, err := exec.Command("go", "env", "GOTOOLDIR").Output(); err != nil { - return filepath.Clean(strings.TrimSpace(string(out))) - } - return filepath.Clean(build.ToolDir) -} From 87b6c9a9c8362de3090f61a4fb19e893de22d2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnblad?= Date: Thu, 27 Feb 2020 15:14:44 -0300 Subject: [PATCH 2/3] Updated the circleci conf --- .circleci/config.yml | 3 +- builder_go111_test.go | 186 ------------------------------------------ 2 files changed, 1 insertion(+), 188 deletions(-) delete mode 100644 builder_go111_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 7cf2960..ab316c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,8 +38,7 @@ commands: godog: description: "Run godog" steps: - - run: go install ./cmd/godog - - run: godog -f progress + - run: ./cmd/godog -f progress go_test: description: "Run go test" steps: diff --git a/builder_go111_test.go b/builder_go111_test.go deleted file mode 100644 index 51ccf72..0000000 --- a/builder_go111_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// +build go1.11 - -package godog - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "testing" -) - -func TestGodogBuildWithModuleOutsideGopathAndHavingOnlyFeature(t *testing.T) { - dir := filepath.Join(os.TempDir(), "godogs") - err := buildTestPackage(dir, map[string]string{ - "godogs.feature": builderFeatureFile, - }) - if err != nil { - os.RemoveAll(dir) - t.Fatal(err) - } - defer os.RemoveAll(dir) - - prevDir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - defer os.Chdir(prevDir) - - if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { - t.Log(string(out)) - t.Fatal(err) - } - - if out, err := exec.Command("go", "mod", "edit", "-require", fmt.Sprintf("github.com/cucumber/godog@%s", Version)).CombinedOutput(); err != nil { - t.Log(string(out)) - t.Fatal(err) - } - - var stdout, stderr bytes.Buffer - cmd := buildTestCommand(t, "godogs.feature") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = os.Environ() - - if err := cmd.Run(); err != nil { - t.Log(stdout.String()) - t.Log(stderr.String()) - t.Fatal(err) - } -} - -func TestGodogBuildWithModuleOutsideGopath(t *testing.T) { - dir := filepath.Join(os.TempDir(), "godogs") - err := buildTestPackage(dir, map[string]string{ - "godogs.feature": builderFeatureFile, - "godogs.go": builderMainCodeFile, - "godogs_test.go": builderTestFile, - }) - if err != nil { - os.RemoveAll(dir) - t.Fatal(err) - } - defer os.RemoveAll(dir) - - prevDir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - defer os.Chdir(prevDir) - - if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { - t.Log(string(out)) - t.Fatal(err) - } - - var stdout, stderr bytes.Buffer - cmd := buildTestCommand(t, "godogs.feature") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = os.Environ() - - if err := cmd.Run(); err != nil { - t.Log(stdout.String()) - t.Log(stderr.String()) - t.Fatal(err) - } -} - -func TestGodogBuildWithModuleWithXTestOutsideGopath(t *testing.T) { - dir := filepath.Join(os.TempDir(), "godogs") - err := buildTestPackage(dir, map[string]string{ - "godogs.feature": builderFeatureFile, - "godogs.go": builderMainCodeFile, - "godogs_test.go": builderXTestFile, - }) - if err != nil { - os.RemoveAll(dir) - t.Fatal(err) - } - defer os.RemoveAll(dir) - - prevDir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - defer os.Chdir(prevDir) - - if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { - t.Log(string(out)) - t.Fatal(err) - } - - var stdout, stderr bytes.Buffer - cmd := buildTestCommand(t, "godogs.feature") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = os.Environ() - - if err := cmd.Run(); err != nil { - t.Log(stdout.String()) - t.Log(stderr.String()) - t.Fatal(err) - } -} - -func TestGodogBuildWithModuleInsideGopath(t *testing.T) { - gopath := filepath.Join(os.TempDir(), "_gp") - dir := filepath.Join(gopath, "src", "godogs") - err := buildTestPackage(dir, map[string]string{ - "godogs.feature": builderFeatureFile, - "godogs.go": builderMainCodeFile, - "godogs_test.go": builderTestFile, - }) - if err != nil { - os.RemoveAll(gopath) - t.Fatal(err) - } - defer os.RemoveAll(gopath) - - prevDir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - defer os.Chdir(prevDir) - - c := exec.Command("go", "mod", "init", "godogs") - c.Env = os.Environ() - c.Env = append(c.Env, "GOPATH="+gopath) - c.Env = append(c.Env, "GO111MODULE=on") - if out, err := c.CombinedOutput(); err != nil { - t.Log(string(out)) - t.Fatal(err) - } - - var stdout, stderr bytes.Buffer - cmd := buildTestCommand(t, "godogs.feature") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GOPATH="+gopath) - cmd.Env = append(cmd.Env, "GO111MODULE=on") - - if err := cmd.Run(); err != nil { - t.Log(stdout.String()) - t.Log(stderr.String()) - t.Fatal(err) - } -} From 67fef9fc3a77245c44d832beca0b806f80211014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnblad?= Date: Thu, 27 Feb 2020 15:27:54 -0300 Subject: [PATCH 3/3] Readded some files --- .circleci/config.yml | 3 +- builder.go | 2 + builder_go110.go | 417 ++++++++++++++++++++++++++++++++++++++++++ builder_go111_test.go | 186 +++++++++++++++++++ 4 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 builder_go110.go create mode 100644 builder_go111_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index ab316c6..7cf2960 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,7 +38,8 @@ commands: godog: description: "Run godog" steps: - - run: ./cmd/godog -f progress + - run: go install ./cmd/godog + - run: godog -f progress go_test: description: "Run go test" steps: diff --git a/builder.go b/builder.go index 4feef5d..7663521 100644 --- a/builder.go +++ b/builder.go @@ -1,3 +1,5 @@ +// +build !go1.10 + package godog import ( diff --git a/builder_go110.go b/builder_go110.go new file mode 100644 index 0000000..cf8f0cb --- /dev/null +++ b/builder_go110.go @@ -0,0 +1,417 @@ +// +build go1.10 + +package godog + +import ( + "bytes" + "encoding/json" + "fmt" + "go/build" + "go/parser" + "go/token" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "text/template" + "time" + "unicode" +) + +var ( + tooldir = findToolDir() + compiler = filepath.Join(tooldir, "compile") + linker = filepath.Join(tooldir, "link") + gopaths = filepath.SplitList(build.Default.GOPATH) + godogImportPath = "github.com/cucumber/godog" + + // godep + runnerTemplate = template.Must(template.New("testmain").Parse(`package main + +import ( + "github.com/cucumber/godog" + {{if .Contexts}}_test "{{.ImportPath}}"{{end}} + {{if .XContexts}}_xtest "{{.ImportPath}}_test"{{end}} + {{if .XContexts}}"testing/internal/testdeps"{{end}} + "os" +) + +{{if .XContexts}} +func init() { + testdeps.ImportPath = "{{.ImportPath}}" +} +{{end}} + +func main() { + status := godog.Run("{{ .Name }}", func (suite *godog.Suite) { + os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}") + {{range .Contexts}} + _test.{{ . }}(suite) + {{end}} + {{range .XContexts}} + _xtest.{{ . }}(suite) + {{end}} + }) + os.Exit(status) +}`)) + + // temp file for import + tempFileTemplate = template.Must(template.New("temp").Parse(`package {{.Name}} + +import "github.com/cucumber/godog" + +var _ = godog.Version +`)) +) + +// 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. +// +// If there are go test files, it first builds a test +// package with standard go test command. +// +// Finally it generates godog suite executable which +// registers exported godog contexts from the test files +// of tested package. +// +// Returns the path to generated executable +func Build(bin string) error { + abs, err := filepath.Abs(".") + if err != nil { + return err + } + + // we allow package to be nil, if godog is run only when + // there is a feature file in empty directory + pkg := importPackage(abs) + src, err := buildTestMain(pkg) + if err != nil { + return err + } + + // may need to produce temp file for godog dependency + srcTemp, err := buildTempFile(pkg) + if err != nil { + return err + } + + 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 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 + testmain := filepath.Join(testdir, "_testmain.go") + err = ioutil.WriteFile(testmain, src, 0644) + if err != nil { + return err + } + + // godog package may be vendored and may need importmap + vendored := maybeVendoredGodog() + + // compile godog testmain package archive + // we do not depend on CGO so a lot of checks are not necessary + linkerCfg := filepath.Join(testdir, "importcfg.link") + compilerCfg := linkerCfg + if vendored != nil { + data, err := ioutil.ReadFile(linkerCfg) + if err != nil { + return err + } + + data = append(data, []byte(fmt.Sprintf("importmap %s=%s\n", godogImportPath, vendored.ImportPath))...) + compilerCfg = filepath.Join(testdir, "importcfg") + + err = ioutil.WriteFile(compilerCfg, data, 0644) + if err != nil { + return err + } + } + + testMainPkgOut := filepath.Join(testdir, "main.a") + args := []string{ + "-o", testMainPkgOut, + "-importcfg", compilerCfg, + "-p", "main", + "-complete", + } + + args = append(args, "-pack", testmain) + cmd := exec.Command(compiler, args...) + cmd.Env = os.Environ() + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to compile testmain package: %v - output: %s", err, string(out)) + } + + // link test suite executable + args = []string{ + "-o", bin, + "-importcfg", linkerCfg, + "-buildmode=exe", + } + args = append(args, testMainPkgOut) + cmd = exec.Command(linker, args...) + cmd.Env = os.Environ() + + out, err = cmd.CombinedOutput() + if err != nil { + msg := `failed to link test executable: + reason: %s + command: %s` + return fmt.Errorf(msg, string(out), linker+" '"+strings.Join(args, "' '")+"'") + } + + return nil +} + +func maybeVendoredGodog() *build.Package { + dir, err := filepath.Abs(".") + if err != nil { + return 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", godogImportPath), 0) + if err != nil { + dir = filepath.Dir(dir) + continue + } + return pkg + } + } + return nil +} + +func importPackage(dir string) *build.Package { + pkg, _ := build.ImportDir(dir, 0) + + // normalize import path for local import packages + // taken from go source code + // see: https://github.com/golang/go/blob/go1.7rc5/src/cmd/go/pkg.go#L279 + if pkg != nil && pkg.ImportPath == "." { + pkg.ImportPath = path.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir))) + } + + return pkg +} + +// from go src +func makeImportValid(r rune) rune { + // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport. + const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD" + if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) { + return '_' + } + 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, error) { + var ( + contexts []string + xcontexts []string + err error + name, importPath string + ) + if nil != pkg { + contexts, err = processPackageTestFiles(pkg.TestGoFiles) + if err != nil { + return nil, err + } + xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles) + if err != nil { + return nil, err + } + importPath = parseImport(pkg.ImportPath, pkg.Root) + name = pkg.Name + } else { + name = "main" + } + data := struct { + Name string + Contexts []string + XContexts []string + ImportPath string + }{ + Name: name, + Contexts: contexts, + XContexts: xcontexts, + ImportPath: importPath, + } + + var buf bytes.Buffer + if err = runnerTemplate.Execute(&buf, data); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// parseImport parses the import path to deal with go module. +func parseImport(rawPath, rootPath string) string { + // with go > 1.11 and go module enabled out of the GOPATH, + // the import path begins with an underscore and the GOPATH is unknown on build. + if rootPath != "" { + // go < 1.11 or it's a module inside the GOPATH + return rawPath + } + // for module support, query the module import path + cmd := exec.Command("go", "list", "-m", "-json") + out, err := cmd.StdoutPipe() + if err != nil { + // Unable to read stdout + return rawPath + } + if cmd.Start() != nil { + // Does not using modules + return rawPath + } + var mod struct { + Dir string `json:"Dir"` + Path string `json:"Path"` + } + if json.NewDecoder(out).Decode(&mod) != nil { + // Unexpected result + return rawPath + } + if cmd.Wait() != nil { + return rawPath + } + // Concatenates the module path with the current sub-folders if needed + return mod.Path + filepath.ToSlash(strings.TrimPrefix(strings.TrimPrefix(rawPath, "_"), mod.Dir)) +} + +// processPackageTestFiles runs through ast of each test +// file pack and looks for godog suite contexts to register +// on run +func processPackageTestFiles(packs ...[]string) ([]string, error) { + var ctxs []string + fset := token.NewFileSet() + for _, pack := range packs { + for _, testFile := range pack { + node, err := parser.ParseFile(fset, testFile, nil, 0) + if err != nil { + return ctxs, err + } + + ctxs = append(ctxs, astContexts(node)...) + } + } + var failed []string + for _, ctx := range ctxs { + runes := []rune(ctx) + if unicode.IsLower(runes[0]) { + expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...) + failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected))) + } + } + if len(failed) > 0 { + return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t")) + } + return ctxs, nil +} + +func findToolDir() string { + if out, err := exec.Command("go", "env", "GOTOOLDIR").Output(); err != nil { + return filepath.Clean(strings.TrimSpace(string(out))) + } + return filepath.Clean(build.ToolDir) +} diff --git a/builder_go111_test.go b/builder_go111_test.go new file mode 100644 index 0000000..51ccf72 --- /dev/null +++ b/builder_go111_test.go @@ -0,0 +1,186 @@ +// +build go1.11 + +package godog + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestGodogBuildWithModuleOutsideGopathAndHavingOnlyFeature(t *testing.T) { + dir := filepath.Join(os.TempDir(), "godogs") + err := buildTestPackage(dir, map[string]string{ + "godogs.feature": builderFeatureFile, + }) + if err != nil { + os.RemoveAll(dir) + t.Fatal(err) + } + defer os.RemoveAll(dir) + + prevDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + defer os.Chdir(prevDir) + + if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + if out, err := exec.Command("go", "mod", "edit", "-require", fmt.Sprintf("github.com/cucumber/godog@%s", Version)).CombinedOutput(); err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + cmd := buildTestCommand(t, "godogs.feature") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = os.Environ() + + if err := cmd.Run(); err != nil { + t.Log(stdout.String()) + t.Log(stderr.String()) + t.Fatal(err) + } +} + +func TestGodogBuildWithModuleOutsideGopath(t *testing.T) { + dir := filepath.Join(os.TempDir(), "godogs") + err := buildTestPackage(dir, map[string]string{ + "godogs.feature": builderFeatureFile, + "godogs.go": builderMainCodeFile, + "godogs_test.go": builderTestFile, + }) + if err != nil { + os.RemoveAll(dir) + t.Fatal(err) + } + defer os.RemoveAll(dir) + + prevDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + defer os.Chdir(prevDir) + + if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + cmd := buildTestCommand(t, "godogs.feature") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = os.Environ() + + if err := cmd.Run(); err != nil { + t.Log(stdout.String()) + t.Log(stderr.String()) + t.Fatal(err) + } +} + +func TestGodogBuildWithModuleWithXTestOutsideGopath(t *testing.T) { + dir := filepath.Join(os.TempDir(), "godogs") + err := buildTestPackage(dir, map[string]string{ + "godogs.feature": builderFeatureFile, + "godogs.go": builderMainCodeFile, + "godogs_test.go": builderXTestFile, + }) + if err != nil { + os.RemoveAll(dir) + t.Fatal(err) + } + defer os.RemoveAll(dir) + + prevDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + defer os.Chdir(prevDir) + + if out, err := exec.Command("go", "mod", "init", "godogs").CombinedOutput(); err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + cmd := buildTestCommand(t, "godogs.feature") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = os.Environ() + + if err := cmd.Run(); err != nil { + t.Log(stdout.String()) + t.Log(stderr.String()) + t.Fatal(err) + } +} + +func TestGodogBuildWithModuleInsideGopath(t *testing.T) { + gopath := filepath.Join(os.TempDir(), "_gp") + dir := filepath.Join(gopath, "src", "godogs") + err := buildTestPackage(dir, map[string]string{ + "godogs.feature": builderFeatureFile, + "godogs.go": builderMainCodeFile, + "godogs_test.go": builderTestFile, + }) + if err != nil { + os.RemoveAll(gopath) + t.Fatal(err) + } + defer os.RemoveAll(gopath) + + prevDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + defer os.Chdir(prevDir) + + c := exec.Command("go", "mod", "init", "godogs") + c.Env = os.Environ() + c.Env = append(c.Env, "GOPATH="+gopath) + c.Env = append(c.Env, "GO111MODULE=on") + if out, err := c.CombinedOutput(); err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + cmd := buildTestCommand(t, "godogs.feature") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOPATH="+gopath) + cmd.Env = append(cmd.Env, "GO111MODULE=on") + + if err := cmd.Run(); err != nil { + t.Log(stdout.String()) + t.Log(stderr.String()) + t.Fatal(err) + } +}