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