github.com/ajguerrer/rules_go@v0.20.3/go/tools/bazel_testing/bazel_testing.go (about) 1 // Copyright 2019 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package bazel_testing provides an integration testing framework for 16 // testing rules_go with Bazel. 17 // 18 // Tests may be written by declaring a go_bazel_test target instead of 19 // a go_test (go_bazel_test is defined in def.bzl here), then calling 20 // TestMain. Tests are run in a synthetic test workspace. Tests may run 21 // bazel commands with RunBazel. 22 package bazel_testing 23 24 import ( 25 "bytes" 26 "flag" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "os" 31 "os/exec" 32 "os/signal" 33 "path" 34 "path/filepath" 35 "regexp" 36 "runtime" 37 "sort" 38 "strings" 39 "testing" 40 "text/template" 41 42 "github.com/bazelbuild/rules_go/go/tools/bazel" 43 "github.com/bazelbuild/rules_go/go/tools/internal/txtar" 44 ) 45 46 // Args is a list of arguments to TestMain. It's defined as a struct so 47 // that new optional arguments may be added without breaking compatibility. 48 type Args struct { 49 // Main is a text archive containing files in the main workspace. 50 // The text archive format is parsed by 51 // //go/tools/internal/txtar:go_default_library, which is copied from 52 // cmd/go/internal/txtar. If this archive does not contain a WORKSPACE file, 53 // a default file will be synthesized. 54 Main string 55 56 // Nogo is the nogo target to pass to go_register_toolchains. By default, 57 // nogo is not used. 58 Nogo string 59 60 // WorkspaceSuffix is a string that should be appended to the end 61 // of the default generated WORKSPACE file. 62 WorkspaceSuffix string 63 64 // SetUp is a function that is executed inside the context of the testing 65 // workspace. It is executed once and only once before the beginning of 66 // all tests. If SetUp returns a non-nil error, execution is halted and 67 // tests cases are not executed. 68 SetUp func() error 69 } 70 71 // debug may be set to make the test print the test workspace path and stop 72 // instead of running tests. 73 const debug = false 74 75 // TestMain should be called by tests using this framework from a function named 76 // "TestMain". For example: 77 // 78 // func TestMain(m *testing.M) { 79 // os.Exit(bazel_testing.TestMain(m, bazel_testing.Args{...})) 80 // } 81 // 82 // TestMain constructs a set of workspaces and changes the working directory to 83 // the main workspace. 84 func TestMain(m *testing.M, args Args) { 85 // Defer os.Exit with the correct code. This ensures other deferred cleanup 86 // functions are run first. 87 code := 1 88 defer func() { 89 if r := recover(); r != nil { 90 fmt.Fprintf(os.Stderr, "panic: %v\n", r) 91 code = 1 92 } 93 os.Exit(code) 94 }() 95 96 var files []string 97 beginFiles, endFiles := -1, -1 98 for i, arg := range os.Args { 99 if arg == "-begin_files" { 100 beginFiles = i 101 } else if arg == "-end_files" { 102 endFiles = i 103 break 104 } else if arg == "--" { 105 break 106 } 107 } 108 if beginFiles >= 0 && endFiles < 0 || 109 beginFiles < 0 && endFiles >= 0 || 110 beginFiles >= 0 && beginFiles >= endFiles { 111 fmt.Fprintf(os.Stderr, "error: -begin_files, -end_files not set together or in order\n") 112 return 113 } 114 if beginFiles >= 0 { 115 files = os.Args[beginFiles+1 : endFiles-1] 116 os.Args = append(os.Args[:beginFiles:beginFiles], os.Args[endFiles+1:]...) 117 } 118 119 flag.Parse() 120 121 workspaceDir, cleanup, err := setupWorkspace(args, files) 122 defer func() { 123 if err := cleanup(); err != nil { 124 fmt.Fprintf(os.Stderr, "cleanup error: %v\n", err) 125 // Don't fail the test on a cleanup error. 126 // Some operating systems (windows, maybe also darwin) can't reliably 127 // delete executable files after they're run. 128 } 129 }() 130 if err != nil { 131 fmt.Fprintf(os.Stderr, "error: %v\n", err) 132 return 133 } 134 135 if debug { 136 fmt.Fprintf(os.Stderr, "test setup in %s\n", workspaceDir) 137 interrupted := make(chan os.Signal) 138 signal.Notify(interrupted, os.Interrupt) 139 <-interrupted 140 return 141 } 142 143 if err := os.Chdir(workspaceDir); err != nil { 144 fmt.Fprintf(os.Stderr, "%v\n", err) 145 return 146 } 147 defer exec.Command("bazel", "shutdown").Run() 148 149 if args.SetUp != nil { 150 if err := args.SetUp(); err != nil { 151 fmt.Fprintf(os.Stderr, "test provided SetUp method returned error: %v\n", err) 152 return 153 } 154 } 155 156 code = m.Run() 157 } 158 159 // BazelCmd prepares a bazel command for execution. It chooses the correct 160 // bazel binary based on the environment and sanitizes the environment to 161 // hide that this code is executing inside a bazel test. 162 func BazelCmd(args ...string) *exec.Cmd { 163 cmd := exec.Command("bazel", args...) 164 for _, e := range os.Environ() { 165 // Filter environment variables set by the bazel test wrapper script. 166 // These confuse recursive invocations of Bazel. 167 if strings.HasPrefix(e, "TEST_") || strings.HasPrefix(e, "RUNFILES_") { 168 continue 169 } 170 cmd.Env = append(cmd.Env, e) 171 } 172 return cmd 173 } 174 175 // RunBazel invokes a bazel command with a list of arguments. 176 // 177 // If the command starts but exits with a non-zero status, a *StderrExitError 178 // will be returned which wraps the original *exec.ExitError. 179 func RunBazel(args ...string) error { 180 cmd := BazelCmd(args...) 181 182 buf := &bytes.Buffer{} 183 cmd.Stderr = buf 184 err := cmd.Run() 185 if eErr, ok := err.(*exec.ExitError); ok { 186 eErr.Stderr = buf.Bytes() 187 err = &StderrExitError{Err: eErr} 188 } 189 return err 190 } 191 192 // BazelOutput invokes a bazel command with a list of arguments and returns 193 // the content of stdout. 194 // 195 // If the command starts but exits with a non-zero status, a *StderrExitError 196 // will be returned which wraps the original *exec.ExitError. 197 func BazelOutput(args ...string) ([]byte, error) { 198 cmd := BazelCmd(args...) 199 stdout := &bytes.Buffer{} 200 stderr := &bytes.Buffer{} 201 cmd.Stdout = stdout 202 cmd.Stderr = stderr 203 err := cmd.Run() 204 if eErr, ok := err.(*exec.ExitError); ok { 205 eErr.Stderr = stderr.Bytes() 206 err = &StderrExitError{Err: eErr} 207 } 208 return stdout.Bytes(), err 209 } 210 211 // StderrExitError wraps *exec.ExitError and prints the complete stderr output 212 // from a command. 213 type StderrExitError struct { 214 Err *exec.ExitError 215 } 216 217 func (e *StderrExitError) Error() string { 218 sb := &strings.Builder{} 219 sb.Write(e.Err.Stderr) 220 sb.WriteString(e.Err.Error()) 221 return sb.String() 222 } 223 224 func setupWorkspace(args Args, files []string) (dir string, cleanup func() error, err error) { 225 var cleanups []func() error 226 cleanup = func() error { 227 var firstErr error 228 for i := len(cleanups) - 1; i >= 0; i-- { 229 if err := cleanups[i](); err != nil && firstErr == nil { 230 firstErr = err 231 } 232 } 233 return firstErr 234 } 235 defer func() { 236 if err != nil { 237 cleanup() 238 cleanup = func() error { return nil } 239 } 240 }() 241 242 // Find a suitable cache directory. We want something persistent where we 243 // can store a bazel output base across test runs, even for multiple tests. 244 var cacheDir, outBaseDir string 245 if tmpDir := os.Getenv("TEST_TMPDIR"); tmpDir != "" { 246 // TEST_TMPDIR is set by Bazel's test wrapper. Bazel itself uses this to 247 // detect that it's run by a test. When invoked like this, Bazel sets 248 // its output base directory to a temporary directory. This wastes a lot 249 // of time (a simple test takes 45s instead of 3s). We use TEST_TMPDIR 250 // to find a persistent location in the execroot. We won't pass TEST_TMPDIR 251 // to bazel in RunBazel. 252 tmpDir = filepath.Clean(tmpDir) 253 if i := strings.Index(tmpDir, string(os.PathSeparator)+"execroot"+string(os.PathSeparator)); i >= 0 { 254 outBaseDir = tmpDir[:i] 255 cacheDir = filepath.Join(outBaseDir, "bazel_testing") 256 } else { 257 cacheDir = filepath.Join(tmpDir, "bazel_testing") 258 } 259 } else { 260 // The test is not invoked by Bazel, so just use the user's cache. 261 cacheDir, err = os.UserCacheDir() 262 if err != nil { 263 return "", cleanup, err 264 } 265 cacheDir = filepath.Join(cacheDir, "bazel_testing") 266 } 267 268 // TODO(jayconrod): any other directories needed for caches? 269 execDir := filepath.Join(cacheDir, "bazel_go_test") 270 if err := os.RemoveAll(execDir); err != nil { 271 return "", cleanup, err 272 } 273 cleanups = append(cleanups, func() error { return os.RemoveAll(execDir) }) 274 275 // Extract test files for the main workspace. 276 mainDir := filepath.Join(execDir, "main") 277 if err := os.MkdirAll(mainDir, 0777); err != nil { 278 return "", cleanup, err 279 } 280 if err := extractTxtar(mainDir, args.Main); err != nil { 281 return "", cleanup, fmt.Errorf("building main workspace: %v", err) 282 } 283 284 // If some of the path arguments are missing an explicit workspace, 285 // read the workspace name from WORKSPACE. We need this to map arguments 286 // to runfiles in specific workspaces. 287 haveDefaultWorkspace := false 288 var defaultWorkspaceName string 289 for _, argPath := range files { 290 workspace, _, err := parseLocationArg(argPath) 291 if err == nil && workspace == "" { 292 haveDefaultWorkspace = true 293 cleanPath := path.Clean(argPath) 294 if cleanPath == "WORKSPACE" { 295 defaultWorkspaceName, err = loadWorkspaceName(cleanPath) 296 if err != nil { 297 return "", cleanup, fmt.Errorf("could not load default workspace name: %v", err) 298 } 299 break 300 } 301 } 302 } 303 if haveDefaultWorkspace && defaultWorkspaceName == "" { 304 return "", cleanup, fmt.Errorf("found files from default workspace, but not WORKSPACE") 305 } 306 307 // Index runfiles by workspace and short path. We need this to determine 308 // destination paths when we copy or link files. 309 runfiles, err := bazel.ListRunfiles() 310 if err != nil { 311 return "", cleanup, err 312 } 313 314 type runfileKey struct{ workspace, short string } 315 runfileMap := make(map[runfileKey]string) 316 for _, rf := range runfiles { 317 runfileMap[runfileKey{rf.Workspace, rf.ShortPath}] = rf.Path 318 } 319 320 // Copy or link file arguments from runfiles into fake workspace dirctories. 321 // Keep track of the workspace names we see, since we'll generate a WORKSPACE 322 // with local_repository rules later. 323 workspaceNames := make(map[string]bool) 324 for _, argPath := range files { 325 workspace, shortPath, err := parseLocationArg(argPath) 326 if err != nil { 327 return "", cleanup, err 328 } 329 if workspace == "" { 330 workspace = defaultWorkspaceName 331 } 332 workspaceNames[workspace] = true 333 334 srcPath, ok := runfileMap[runfileKey{workspace, shortPath}] 335 if !ok { 336 return "", cleanup, fmt.Errorf("unknown runfile: %s", argPath) 337 } 338 dstPath := filepath.Join(execDir, workspace, shortPath) 339 if err := copyOrLink(dstPath, srcPath); err != nil { 340 return "", cleanup, err 341 } 342 } 343 344 // If there's no WORKSPACE file, create one. 345 workspacePath := filepath.Join(mainDir, "WORKSPACE") 346 if _, err := os.Stat(workspacePath); os.IsNotExist(err) { 347 w, err := os.Create(workspacePath) 348 if err != nil { 349 return "", cleanup, err 350 } 351 defer func() { 352 if cerr := w.Close(); err == nil && cerr != nil { 353 err = cerr 354 } 355 }() 356 info := workspaceTemplateInfo{ 357 Suffix: args.WorkspaceSuffix, 358 Nogo: args.Nogo, 359 } 360 for name := range workspaceNames { 361 info.WorkspaceNames = append(info.WorkspaceNames, name) 362 } 363 sort.Strings(info.WorkspaceNames) 364 if outBaseDir != "" { 365 goSDKPath := filepath.Join(outBaseDir, "external", "go_sdk") 366 rel, err := filepath.Rel(mainDir, goSDKPath) 367 if err != nil { 368 return "", cleanup, fmt.Errorf("could not find relative path from %q to %q for go_sdk", mainDir, goSDKPath) 369 } 370 rel = filepath.ToSlash(rel) 371 info.GoSDKPath = rel 372 } 373 if err := defaultWorkspaceTpl.Execute(w, info); err != nil { 374 return "", cleanup, err 375 } 376 } 377 378 return mainDir, cleanup, nil 379 } 380 381 func extractTxtar(dir, txt string) error { 382 ar := txtar.Parse([]byte(txt)) 383 for _, f := range ar.Files { 384 if parentDir := filepath.Dir(f.Name); parentDir != "." { 385 if err := os.MkdirAll(filepath.Join(dir, parentDir), 0777); err != nil { 386 return err 387 } 388 } 389 if err := ioutil.WriteFile(filepath.Join(dir, f.Name), f.Data, 0666); err != nil { 390 return err 391 } 392 } 393 return nil 394 } 395 396 func parseLocationArg(arg string) (workspace, shortPath string, err error) { 397 cleanPath := path.Clean(arg) 398 if !strings.HasPrefix(cleanPath, "external/") { 399 return "", cleanPath, nil 400 } 401 i := strings.IndexByte(arg[len("external/"):], '/') 402 if i < 0 { 403 return "", "", fmt.Errorf("unexpected file (missing / after external/): %s", arg) 404 } 405 i += len("external/") 406 workspace = cleanPath[len("external/"):i] 407 shortPath = cleanPath[i+1:] 408 return workspace, shortPath, nil 409 } 410 411 func loadWorkspaceName(workspacePath string) (string, error) { 412 workspaceData, err := ioutil.ReadFile(workspacePath) 413 if err != nil { 414 return "", err 415 } 416 nameRe := regexp.MustCompile(`(?m)^workspace\(\s*name\s*=\s*("[^"]*"|'[^']*')\s*,?\s*\)$`) 417 match := nameRe.FindSubmatchIndex(workspaceData) 418 if match == nil { 419 return "", fmt.Errorf("%s: workspace name not set", workspacePath) 420 } 421 name := string(workspaceData[match[2]+1 : match[3]-1]) 422 if name == "" { 423 return "", fmt.Errorf("%s: workspace name is empty", workspacePath) 424 } 425 return name, nil 426 } 427 428 type workspaceTemplateInfo struct { 429 WorkspaceNames []string 430 GoSDKPath string 431 Nogo string 432 Suffix string 433 } 434 435 var defaultWorkspaceTpl = template.Must(template.New("").Parse(` 436 {{range .WorkspaceNames}} 437 local_repository( 438 name = "{{.}}", 439 path = "../{{.}}", 440 ) 441 {{end}} 442 443 {{if not .GoSDKPath}} 444 load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains") 445 446 go_rules_dependencies() 447 448 go_register_toolchains(go_version = "host") 449 {{else}} 450 local_repository( 451 name = "local_go_sdk", 452 path = "{{.GoSDKPath}}", 453 ) 454 455 load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains", "go_wrap_sdk") 456 457 go_rules_dependencies() 458 459 go_wrap_sdk( 460 name = "go_sdk", 461 root_file = "@local_go_sdk//:ROOT", 462 ) 463 464 go_register_toolchains({{if .Nogo}}nogo = "{{.Nogo}}"{{end}}) 465 {{end}} 466 {{.Suffix}} 467 `)) 468 469 func copyOrLink(dstPath, srcPath string) error { 470 if err := os.MkdirAll(filepath.Dir(dstPath), 0777); err != nil { 471 return err 472 } 473 474 copy := func(dstPath, srcPath string) (err error) { 475 src, err := os.Open(srcPath) 476 if err != nil { 477 return err 478 } 479 defer src.Close() 480 481 dst, err := os.Create(dstPath) 482 if err != nil { 483 return err 484 } 485 defer func() { 486 if cerr := dst.Close(); err == nil && cerr != nil { 487 err = cerr 488 } 489 }() 490 491 _, err = io.Copy(dst, src) 492 return err 493 } 494 495 if runtime.GOOS == "windows" { 496 return copy(dstPath, srcPath) 497 } 498 absSrcPath, err := filepath.Abs(srcPath) 499 if err != nil { 500 return err 501 } 502 return os.Symlink(absSrcPath, dstPath) 503 }