github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/fake/sandbox.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package fake
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/april1989/origin-go-tools/internal/gocommand"
    16  	"github.com/april1989/origin-go-tools/internal/testenv"
    17  	"github.com/april1989/origin-go-tools/txtar"
    18  	errors "golang.org/x/xerrors"
    19  )
    20  
    21  // Sandbox holds a collection of temporary resources to use for working with Go
    22  // code in tests.
    23  type Sandbox struct {
    24  	gopath  string
    25  	basedir string
    26  	goproxy string
    27  	Workdir *Workdir
    28  }
    29  
    30  // SandboxConfig controls the behavior of a test sandbox. The zero value
    31  // defines a reasonable default.
    32  type SandboxConfig struct {
    33  	// RootDir sets the base directory to use when creating temporary
    34  	// directories. If not specified, defaults to a new temporary directory.
    35  	RootDir string
    36  	// Files holds a txtar-encoded archive of files to populate the initial state
    37  	// of the working directory.
    38  	Files string
    39  	// InGoPath specifies that the working directory should be within the
    40  	// temporary GOPATH.
    41  	InGoPath bool
    42  	// Workdir configures the working directory of the Sandbox, for running in a
    43  	// pre-existing directory. If unset, a new working directory will be created
    44  	// under RootDir.
    45  	//
    46  	// This option is incompatible with InGoPath or Files.
    47  	Workdir string
    48  
    49  	// ProxyFiles holds a txtar-encoded archive of files to populate a file-based
    50  	// Go proxy.
    51  	ProxyFiles string
    52  	// GOPROXY is the explicit GOPROXY value that should be used for the sandbox.
    53  	//
    54  	// This option is incompatible with ProxyFiles.
    55  	GOPROXY string
    56  }
    57  
    58  // NewSandbox creates a collection of named temporary resources, with a
    59  // working directory populated by the txtar-encoded content in srctxt, and a
    60  // file-based module proxy populated with the txtar-encoded content in
    61  // proxytxt.
    62  //
    63  // If rootDir is non-empty, it will be used as the root of temporary
    64  // directories created for the sandbox. Otherwise, a new temporary directory
    65  // will be used as root.
    66  func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
    67  	if config == nil {
    68  		config = new(SandboxConfig)
    69  	}
    70  
    71  	if config.Workdir != "" && (config.Files != "" || config.InGoPath) {
    72  		return nil, fmt.Errorf("invalid SandboxConfig: Workdir cannot be used in conjunction with Files or InGoPath. Got %+v", config)
    73  	}
    74  
    75  	if config.GOPROXY != "" && config.ProxyFiles != "" {
    76  		return nil, fmt.Errorf("invalid SandboxConfig: GOPROXY cannot be set in conjunction with ProxyFiles. Got %+v", config)
    77  	}
    78  
    79  	sb := &Sandbox{}
    80  	defer func() {
    81  		// Clean up if we fail at any point in this constructor.
    82  		if err != nil {
    83  			sb.Close()
    84  		}
    85  	}()
    86  
    87  	baseDir, err := ioutil.TempDir(config.RootDir, "gopls-sandbox-")
    88  	if err != nil {
    89  		return nil, fmt.Errorf("creating temporary workdir: %v", err)
    90  	}
    91  	sb.basedir = baseDir
    92  	sb.gopath = filepath.Join(sb.basedir, "gopath")
    93  	if err := os.Mkdir(sb.gopath, 0755); err != nil {
    94  		return nil, err
    95  	}
    96  	if config.GOPROXY != "" {
    97  		sb.goproxy = config.GOPROXY
    98  	} else {
    99  		proxydir := filepath.Join(sb.basedir, "proxy")
   100  		if err := os.Mkdir(proxydir, 0755); err != nil {
   101  			return nil, err
   102  		}
   103  		sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  	}
   108  	if config.Workdir != "" {
   109  		sb.Workdir = NewWorkdir(config.Workdir)
   110  	} else {
   111  		workdir := config.Workdir
   112  		// If we don't have a pre-existing work dir, we want to create either
   113  		// $GOPATH/src or <RootDir/work>.
   114  		if config.InGoPath {
   115  			// Set the working directory as $GOPATH/src.
   116  			workdir = filepath.Join(sb.gopath, "src")
   117  		} else if workdir == "" {
   118  			workdir = filepath.Join(sb.basedir, "work")
   119  		}
   120  		if err := os.Mkdir(workdir, 0755); err != nil {
   121  			return nil, err
   122  		}
   123  		sb.Workdir = NewWorkdir(workdir)
   124  		if err := sb.Workdir.WriteInitialFiles(config.Files); err != nil {
   125  			return nil, err
   126  		}
   127  	}
   128  
   129  	return sb, nil
   130  }
   131  
   132  func unpackTxt(txt string) map[string][]byte {
   133  	dataMap := make(map[string][]byte)
   134  	archive := txtar.Parse([]byte(txt))
   135  	for _, f := range archive.Files {
   136  		dataMap[f.Name] = f.Data
   137  	}
   138  	return dataMap
   139  }
   140  
   141  // splitModuleVersionPath extracts module information from files stored in the
   142  // directory structure modulePath@version/suffix.
   143  // For example:
   144  //  splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package")
   145  func splitModuleVersionPath(path string) (modulePath, version, suffix string) {
   146  	parts := strings.Split(path, "/")
   147  	var modulePathParts []string
   148  	for i, p := range parts {
   149  		if strings.Contains(p, "@") {
   150  			mv := strings.SplitN(p, "@", 2)
   151  			modulePathParts = append(modulePathParts, mv[0])
   152  			return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/")
   153  		}
   154  		modulePathParts = append(modulePathParts, p)
   155  	}
   156  	// Default behavior: this is just a module path.
   157  	return path, "", ""
   158  }
   159  
   160  // GOPATH returns the value of the Sandbox GOPATH.
   161  func (sb *Sandbox) GOPATH() string {
   162  	return sb.gopath
   163  }
   164  
   165  // GoEnv returns the default environment variables that can be used for
   166  // invoking Go commands in the sandbox.
   167  func (sb *Sandbox) GoEnv() map[string]string {
   168  	vars := map[string]string{
   169  		"GOPATH":           sb.GOPATH(),
   170  		"GOPROXY":          sb.goproxy,
   171  		"GO111MODULE":      "",
   172  		"GOSUMDB":          "off",
   173  		"GOPACKAGESDRIVER": "off",
   174  	}
   175  	if testenv.Go1Point() >= 5 {
   176  		vars["GOMODCACHE"] = ""
   177  	}
   178  	return vars
   179  }
   180  
   181  // RunGoCommand executes a go command in the sandbox.
   182  func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string) error {
   183  	var vars []string
   184  	for k, v := range sb.GoEnv() {
   185  		vars = append(vars, fmt.Sprintf("%s=%s", k, v))
   186  	}
   187  	inv := gocommand.Invocation{
   188  		Verb: verb,
   189  		Args: args,
   190  		Env:  vars,
   191  	}
   192  	// Use the provided directory for the working directory, if available.
   193  	// sb.Workdir may be nil if we exited the constructor with errors (we call
   194  	// Close to clean up any partial state from the constructor, which calls
   195  	// RunGoCommand).
   196  	if dir != "" {
   197  		inv.WorkingDir = sb.Workdir.filePath(dir)
   198  	} else if sb.Workdir != nil {
   199  		inv.WorkingDir = sb.Workdir.workdir
   200  	}
   201  	gocmdRunner := &gocommand.Runner{}
   202  	_, _, _, err := gocmdRunner.RunRaw(ctx, inv)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	// Since running a go command may result in changes to workspace files,
   207  	// check if we need to send any any "watched" file events.
   208  	if sb.Workdir != nil {
   209  		if err := sb.Workdir.CheckForFileChanges(ctx); err != nil {
   210  			return errors.Errorf("checking for file changes: %w", err)
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  // Close removes all state associated with the sandbox.
   217  func (sb *Sandbox) Close() error {
   218  	var goCleanErr error
   219  	if sb.gopath != "" {
   220  		goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"})
   221  	}
   222  	err := os.RemoveAll(sb.basedir)
   223  	if err != nil || goCleanErr != nil {
   224  		return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err)
   225  	}
   226  	return nil
   227  }