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  }