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