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