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