1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
48
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
61
62 type Args struct {
63
64
65
66
67
68 Main string
69
70
71
72 Nogo string
73
74
75 NogoIncludes []string
76
77
78 NogoExcludes []string
79
80
81
82 WorkspaceSuffix string
83
84
85
86
87 ModuleFileSuffix string
88
89
90
91
92
93 SetUp func() error
94 }
95
96
97
98 const debug = false
99
100
101
102
103
104 var outputUserRoot string
105
106
107
108
109
110
111
112
113
114
115 func TestMain(m *testing.M, args Args) {
116
117
118 code := 1
119 defer func() {
120 if r := recover(); r != nil {
121 fmt.Fprintf(os.Stderr, "panic: %v\n", r)
122 code = 1
123 }
124 os.Exit(code)
125 }()
126
127 files, err := bazel.SpliceDelimitedOSArgs("-begin_files", "-end_files")
128 if err != nil {
129 fmt.Fprint(os.Stderr, err)
130 return
131 }
132
133 flag.Parse()
134
135 workspaceDir, cleanup, err := setupWorkspace(args, files)
136 defer func() {
137 if err := cleanup(); err != nil {
138 fmt.Fprintf(os.Stderr, "cleanup error: %v\n", err)
139
140
141
142 }
143 }()
144 if err != nil {
145 fmt.Fprintf(os.Stderr, "error: %v\n", err)
146 return
147 }
148
149 if debug {
150 fmt.Fprintf(os.Stderr, "test setup in %s\n", workspaceDir)
151 interrupted := make(chan os.Signal)
152 signal.Notify(interrupted, os.Interrupt)
153 <-interrupted
154 return
155 }
156
157 if err := os.Chdir(workspaceDir); err != nil {
158 fmt.Fprintf(os.Stderr, "%v\n", err)
159 return
160 }
161 defer exec.Command("bazel", "shutdown").Run()
162
163 if args.SetUp != nil {
164 if err := args.SetUp(); err != nil {
165 fmt.Fprintf(os.Stderr, "test provided SetUp method returned error: %v\n", err)
166 return
167 }
168 }
169
170 code = m.Run()
171 }
172
173
174
175
176 func BazelCmd(args ...string) *exec.Cmd {
177 cmd := exec.Command("bazel")
178 if outputUserRoot != "" {
179 cmd.Args = append(cmd.Args,
180 "--output_user_root="+outputUserRoot,
181 "--nosystem_rc",
182 "--nohome_rc",
183 )
184 }
185 cmd.Args = append(cmd.Args, args...)
186 for _, e := range os.Environ() {
187
188
189 if strings.HasPrefix(e, "TEST_") || strings.HasPrefix(e, "RUNFILES_") {
190 continue
191 }
192 cmd.Env = append(cmd.Env, e)
193 }
194 return cmd
195 }
196
197
198
199
200
201 func RunBazel(args ...string) error {
202 cmd := BazelCmd(args...)
203
204 buf := &bytes.Buffer{}
205 cmd.Stderr = buf
206 err := cmd.Run()
207 if eErr, ok := err.(*exec.ExitError); ok {
208 eErr.Stderr = buf.Bytes()
209 err = &StderrExitError{Err: eErr}
210 }
211 return err
212 }
213
214
215
216
217
218
219 func BazelOutput(args ...string) ([]byte, error) {
220 stdout, _, err := BazelOutputWithInput(nil, args...)
221 return stdout, err
222 }
223
224
225
226
227
228
229 func BazelOutputWithInput(stdin io.Reader, args ...string) ([]byte, []byte, error) {
230 cmd := BazelCmd(args...)
231 stdout := &bytes.Buffer{}
232 stderr := &bytes.Buffer{}
233 cmd.Stdout = stdout
234 cmd.Stderr = stderr
235 if stdin != nil {
236 cmd.Stdin = stdin
237 }
238 err := cmd.Run()
239 if eErr, ok := err.(*exec.ExitError); ok {
240 eErr.Stderr = stderr.Bytes()
241 err = &StderrExitError{Err: eErr}
242 }
243 return stdout.Bytes(), stderr.Bytes(), err
244 }
245
246
247
248 type StderrExitError struct {
249 Err *exec.ExitError
250 }
251
252 func (e *StderrExitError) Error() string {
253 sb := &strings.Builder{}
254 sb.Write(e.Err.Stderr)
255 sb.WriteString(e.Err.Error())
256 return sb.String()
257 }
258
259 func (e *StderrExitError) Unwrap() error {
260 return e.Err
261 }
262
263 func setupWorkspace(args Args, files []string) (dir string, cleanup func() error, err error) {
264 var cleanups []func() error
265 cleanup = func() error {
266 var firstErr error
267 for i := len(cleanups) - 1; i >= 0; i-- {
268 if err := cleanups[i](); err != nil && firstErr == nil {
269 firstErr = err
270 }
271 }
272 return firstErr
273 }
274 defer func() {
275 if err != nil {
276 cleanup()
277 cleanup = func() error { return nil }
278 }
279 }()
280
281
282
283 var cacheDir, outBaseDir string
284 if tmpDir := os.Getenv("TEST_TMPDIR"); tmpDir != "" {
285
286
287
288
289
290
291 tmpDir = filepath.Clean(tmpDir)
292 if i := strings.Index(tmpDir, string(os.PathSeparator)+"execroot"+string(os.PathSeparator)); i >= 0 {
293 outBaseDir = tmpDir[:i]
294 outputUserRoot = filepath.Dir(outBaseDir)
295 cacheDir = filepath.Join(outBaseDir, "bazel_testing")
296 } else {
297 cacheDir = filepath.Join(tmpDir, "bazel_testing")
298 }
299 } else {
300
301 cacheDir, err = os.UserCacheDir()
302 if err != nil {
303 return "", cleanup, err
304 }
305 cacheDir = filepath.Join(cacheDir, "bazel_testing")
306 }
307
308
309 execDir := filepath.Join(cacheDir, "bazel_go_test")
310 if err := os.RemoveAll(execDir); err != nil {
311 return "", cleanup, err
312 }
313 cleanups = append(cleanups, func() error { return os.RemoveAll(execDir) })
314
315
316 mainDir := filepath.Join(execDir, "main")
317 if err := os.MkdirAll(mainDir, 0777); err != nil {
318 return "", cleanup, err
319 }
320
321
322
323 if flags := os.Getenv("GO_BAZEL_TEST_BAZELFLAGS"); flags != "" {
324 bazelrcPath := filepath.Join(mainDir, ".bazelrc")
325 content := "build " + flags
326 if err := ioutil.WriteFile(bazelrcPath, []byte(content), 0666); err != nil {
327 return "", cleanup, err
328 }
329 }
330
331
332 if err := extractTxtar(mainDir, args.Main); err != nil {
333 return "", cleanup, fmt.Errorf("building main workspace: %v", err)
334 }
335
336
337
338
339 haveDefaultWorkspace := false
340 var defaultWorkspaceName string
341 for _, argPath := range files {
342 workspace, _, err := parseLocationArg(argPath)
343 if err == nil && workspace == "" {
344 haveDefaultWorkspace = true
345 cleanPath := path.Clean(argPath)
346 if cleanPath == "WORKSPACE" {
347 defaultWorkspaceName, err = loadWorkspaceName(cleanPath)
348 if err != nil {
349 return "", cleanup, fmt.Errorf("could not load default workspace name: %v", err)
350 }
351 break
352 }
353 }
354 }
355 if haveDefaultWorkspace && defaultWorkspaceName == "" {
356 return "", cleanup, fmt.Errorf("found files from default workspace, but not WORKSPACE")
357 }
358
359
360
361 runfiles, err := bazel.ListRunfiles()
362 if err != nil {
363 return "", cleanup, err
364 }
365
366 type runfileKey struct{ workspace, short string }
367 runfileMap := make(map[runfileKey]string)
368 for _, rf := range runfiles {
369 runfileMap[runfileKey{rf.Workspace, rf.ShortPath}] = rf.Path
370 }
371
372
373
374
375 workspaceNames := make(map[string]bool)
376 for _, argPath := range files {
377 workspace, shortPath, err := parseLocationArg(argPath)
378 if err != nil {
379 return "", cleanup, err
380 }
381 if workspace == "" {
382 workspace = defaultWorkspaceName
383 }
384 workspaceNames[workspace] = true
385
386 srcPath, ok := runfileMap[runfileKey{workspace, shortPath}]
387 if !ok {
388 return "", cleanup, fmt.Errorf("unknown runfile: %s", argPath)
389 }
390 dstPath := filepath.Join(execDir, workspace, shortPath)
391 if err := copyOrLink(dstPath, srcPath); err != nil {
392 return "", cleanup, err
393 }
394 }
395
396
397 workspacePath := filepath.Join(mainDir, "WORKSPACE")
398 if _, err = os.Stat(workspacePath); os.IsNotExist(err) {
399 var w *os.File
400 w, err = os.Create(workspacePath)
401 if err != nil {
402 return "", cleanup, err
403 }
404 defer func() {
405 if cerr := w.Close(); err == nil && cerr != nil {
406 err = cerr
407 }
408 }()
409 info := workspaceTemplateInfo{
410 Suffix: args.WorkspaceSuffix,
411 Nogo: args.Nogo,
412 NogoIncludes: args.NogoIncludes,
413 NogoExcludes: args.NogoExcludes,
414 }
415 for name := range workspaceNames {
416 info.WorkspaceNames = append(info.WorkspaceNames, name)
417 }
418 sort.Strings(info.WorkspaceNames)
419 if outBaseDir != "" {
420 goSDKPath := filepath.Join(outBaseDir, "external", "go_sdk")
421 rel, err := filepath.Rel(mainDir, goSDKPath)
422 if err != nil {
423 return "", cleanup, fmt.Errorf("could not find relative path from %q to %q for go_sdk", mainDir, goSDKPath)
424 }
425 rel = filepath.ToSlash(rel)
426 info.GoSDKPath = rel
427 }
428 if err := defaultWorkspaceTpl.Execute(w, info); err != nil {
429 return "", cleanup, err
430 }
431 }
432
433
434 if args.ModuleFileSuffix != "" {
435 moduleBazelPath := filepath.Join(mainDir, "MODULE.bazel")
436 if _, err = os.Stat(moduleBazelPath); err == nil {
437 return "", cleanup, fmt.Errorf("ModuleFileSuffix set but MODULE.bazel exists")
438 }
439 var w *os.File
440 w, err = os.Create(moduleBazelPath)
441 if err != nil {
442 return "", cleanup, err
443 }
444 defer func() {
445 if cerr := w.Close(); err == nil && cerr != nil {
446 err = cerr
447 }
448 }()
449 rulesGoAbsPath := filepath.Join(execDir, "io_bazel_rules_go")
450 rulesGoPath, err := filepath.Rel(mainDir, rulesGoAbsPath)
451 if err != nil {
452 return "", cleanup, fmt.Errorf("could not find relative path from %q to %q for io_bazel_rules_go", mainDir, rulesGoAbsPath)
453 }
454 rulesGoPath = filepath.ToSlash(rulesGoPath)
455 info := moduleFileTemplateInfo{
456 RulesGoPath: rulesGoPath,
457 Suffix: args.ModuleFileSuffix,
458 }
459 if err := defaultModuleBazelTpl.Execute(w, info); err != nil {
460 return "", cleanup, err
461 }
462
463
464 bazelrcPath := filepath.Join(mainDir, ".bazelrc")
465 if _, err = os.Stat(bazelrcPath); os.IsNotExist(err) {
466 err = os.WriteFile(bazelrcPath, []byte("common --enable_bzlmod"), 0666)
467 if err != nil {
468 return "", cleanup, err
469 }
470 }
471 }
472
473 return mainDir, cleanup, nil
474 }
475
476 func extractTxtar(dir, txt string) error {
477 ar := txtar.Parse([]byte(txt))
478 for _, f := range ar.Files {
479 if parentDir := filepath.Dir(f.Name); parentDir != "." {
480 if err := os.MkdirAll(filepath.Join(dir, parentDir), 0777); err != nil {
481 return err
482 }
483 }
484 if err := ioutil.WriteFile(filepath.Join(dir, f.Name), f.Data, 0666); err != nil {
485 return err
486 }
487 }
488 return nil
489 }
490
491 func parseLocationArg(arg string) (workspace, shortPath string, err error) {
492 cleanPath := path.Clean(arg)
493
494 if !strings.HasPrefix(cleanPath, "../") && !strings.HasPrefix(cleanPath, "external/") {
495 return "", cleanPath, nil
496 }
497 var trimmedPath string
498 if strings.HasPrefix(cleanPath, "../") {
499 trimmedPath = cleanPath[len("../"):]
500 } else {
501 trimmedPath = cleanPath[len("external/"):]
502 }
503 i := strings.IndexByte(trimmedPath, '/')
504 if i < 0 {
505 return "", "", fmt.Errorf("unexpected file (missing / after ../): %s", arg)
506 }
507 workspace = trimmedPath[:i]
508 shortPath = trimmedPath[i+1:]
509 return workspace, shortPath, nil
510 }
511
512 func loadWorkspaceName(workspacePath string) (string, error) {
513 runfilePath, err := bazel.Runfile(workspacePath)
514 if err == nil {
515 workspacePath = runfilePath
516 }
517 workspaceData, err := ioutil.ReadFile(workspacePath)
518 if err != nil {
519 return "", err
520 }
521 nameRe := regexp.MustCompile(`(?m)^workspace\(\s*name\s*=\s*("[^"]*"|'[^']*')\s*,?\s*\)\s*$`)
522 match := nameRe.FindSubmatchIndex(workspaceData)
523 if match == nil {
524 return "", fmt.Errorf("%s: workspace name not set", workspacePath)
525 }
526 name := string(workspaceData[match[2]+1 : match[3]-1])
527 if name == "" {
528 return "", fmt.Errorf("%s: workspace name is empty", workspacePath)
529 }
530 return name, nil
531 }
532
533 type workspaceTemplateInfo struct {
534 WorkspaceNames []string
535 GoSDKPath string
536 Nogo string
537 NogoIncludes []string
538 NogoExcludes []string
539 Suffix string
540 }
541
542 var defaultWorkspaceTpl = template.Must(template.New("").Parse(`
543 {{range .WorkspaceNames}}
544 local_repository(
545 name = "{{.}}",
546 path = "../{{.}}",
547 )
548 {{end}}
549
550 {{if not .GoSDKPath}}
551 load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")
552
553 go_rules_dependencies()
554
555 go_register_toolchains(go_version = "host")
556 {{else}}
557 local_repository(
558 name = "local_go_sdk",
559 path = "{{.GoSDKPath}}",
560 )
561
562 load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains", "go_wrap_sdk", "go_register_nogo")
563
564 go_rules_dependencies()
565
566 go_wrap_sdk(
567 name = "go_sdk",
568 root_file = "@local_go_sdk//:ROOT",
569 )
570
571 go_register_toolchains()
572
573 {{if .Nogo}}
574 go_register_nogo(
575 nogo = "{{.Nogo}}",
576 {{ if .NogoIncludes }}
577 includes = [
578 {{range .NogoIncludes }}
579 "{{ . }}",
580 {{ end }}
581 ],
582 {{ else }}
583 includes = ["all"],
584 {{ end}}
585 {{ if .NogoExcludes }}
586 excludes = [
587 {{range .NogoExcludes }}
588 "{{ . }}",
589 {{ end }}
590 ],
591 {{ else }}
592 excludes = None,
593 {{ end}}
594 )
595 {{end}}
596 {{end}}
597 {{.Suffix}}
598 `))
599
600 type moduleFileTemplateInfo struct {
601 RulesGoPath string
602 Suffix string
603 }
604
605 var defaultModuleBazelTpl = template.Must(template.New("").Parse(`
606 bazel_dep(name = "rules_go", version = "", repo_name = "io_bazel_rules_go")
607 local_path_override(
608 module_name = "rules_go",
609 path = "{{.RulesGoPath}}",
610 )
611 {{.Suffix}}
612 `))
613
614 func copyOrLink(dstPath, srcPath string) error {
615 if err := os.MkdirAll(filepath.Dir(dstPath), 0777); err != nil {
616 return err
617 }
618
619 copy := func(dstPath, srcPath string) (err error) {
620 src, err := os.Open(srcPath)
621 if err != nil {
622 return err
623 }
624 defer src.Close()
625
626 dst, err := os.Create(dstPath)
627 if err != nil {
628 return err
629 }
630 defer func() {
631 if cerr := dst.Close(); err == nil && cerr != nil {
632 err = cerr
633 }
634 }()
635
636 _, err = io.Copy(dst, src)
637 return err
638 }
639
640 if runtime.GOOS == "windows" {
641 return copy(dstPath, srcPath)
642 }
643 absSrcPath, err := filepath.Abs(srcPath)
644 if err != nil {
645 return err
646 }
647 return os.Symlink(absSrcPath, dstPath)
648 }
649
View as plain text