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