xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/bazel_testing/bazel_testing.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
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