github.com/joeky888/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 }