github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/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/jhump/golang-x-tools/internal/gocommand" 16 "github.com/jhump/golang-x-tools/internal/testenv" 17 "github.com/jhump/golang-x-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 rootdir 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 // 39 // For convenience, the special substring "$SANDBOX_WORKDIR" is replaced with 40 // the sandbox's resolved working directory before writing files. 41 Files map[string][]byte 42 // InGoPath specifies that the working directory should be within the 43 // temporary GOPATH. 44 InGoPath bool 45 // Workdir configures the working directory of the Sandbox. It behaves as 46 // follows: 47 // - if set to an absolute path, use that path as the working directory. 48 // - if set to a relative path, create and use that path relative to the 49 // sandbox. 50 // - if unset, default to a the 'work' subdirectory of the sandbox. 51 // 52 // This option is incompatible with InGoPath or Files. 53 Workdir string 54 // ProxyFiles holds a txtar-encoded archive of files to populate a file-based 55 // Go proxy. 56 ProxyFiles map[string][]byte 57 // GOPROXY is the explicit GOPROXY value that should be used for the sandbox. 58 // 59 // This option is incompatible with ProxyFiles. 60 GOPROXY string 61 } 62 63 // NewSandbox creates a collection of named temporary resources, with a 64 // working directory populated by the txtar-encoded content in srctxt, and a 65 // file-based module proxy populated with the txtar-encoded content in 66 // proxytxt. 67 // 68 // If rootDir is non-empty, it will be used as the root of temporary 69 // directories created for the sandbox. Otherwise, a new temporary directory 70 // will be used as root. 71 func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) { 72 if config == nil { 73 config = new(SandboxConfig) 74 } 75 if err := validateConfig(*config); err != nil { 76 return nil, fmt.Errorf("invalid SandboxConfig: %v", err) 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 rootDir := config.RootDir 88 if rootDir == "" { 89 rootDir, err = ioutil.TempDir(config.RootDir, "gopls-sandbox-") 90 if err != nil { 91 return nil, fmt.Errorf("creating temporary workdir: %v", err) 92 } 93 } 94 sb.rootdir = rootDir 95 sb.gopath = filepath.Join(sb.rootdir, "gopath") 96 if err := os.Mkdir(sb.gopath, 0755); err != nil { 97 return nil, err 98 } 99 if config.GOPROXY != "" { 100 sb.goproxy = config.GOPROXY 101 } else { 102 proxydir := filepath.Join(sb.rootdir, "proxy") 103 if err := os.Mkdir(proxydir, 0755); err != nil { 104 return nil, err 105 } 106 sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles) 107 if err != nil { 108 return nil, err 109 } 110 } 111 // Short-circuit writing the workdir if we're given an absolute path, since 112 // this is used for running in an existing directory. 113 // TODO(findleyr): refactor this to be less of a workaround. 114 if filepath.IsAbs(config.Workdir) { 115 sb.Workdir = NewWorkdir(config.Workdir) 116 return sb, nil 117 } 118 var workdir string 119 if config.Workdir == "" { 120 if config.InGoPath { 121 // Set the working directory as $GOPATH/src. 122 workdir = filepath.Join(sb.gopath, "src") 123 } else if workdir == "" { 124 workdir = filepath.Join(sb.rootdir, "work") 125 } 126 } else { 127 // relative path 128 workdir = filepath.Join(sb.rootdir, config.Workdir) 129 } 130 if err := os.MkdirAll(workdir, 0755); err != nil { 131 return nil, err 132 } 133 sb.Workdir = NewWorkdir(workdir) 134 if err := sb.Workdir.writeInitialFiles(config.Files); err != nil { 135 return nil, err 136 } 137 return sb, nil 138 } 139 140 // Tempdir creates a new temp directory with the given txtar-encoded files. It 141 // is the responsibility of the caller to call os.RemoveAll on the returned 142 // file path when it is no longer needed. 143 func Tempdir(files map[string][]byte) (string, error) { 144 dir, err := ioutil.TempDir("", "gopls-tempdir-") 145 if err != nil { 146 return "", err 147 } 148 for name, data := range files { 149 if err := WriteFileData(name, data, RelativeTo(dir)); err != nil { 150 return "", errors.Errorf("writing to tempdir: %w", err) 151 } 152 } 153 return dir, nil 154 } 155 156 func UnpackTxt(txt string) map[string][]byte { 157 dataMap := make(map[string][]byte) 158 archive := txtar.Parse([]byte(txt)) 159 for _, f := range archive.Files { 160 dataMap[f.Name] = f.Data 161 } 162 return dataMap 163 } 164 165 func validateConfig(config SandboxConfig) error { 166 if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) { 167 return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath") 168 } 169 if config.Workdir != "" && config.InGoPath { 170 return errors.New("Workdir cannot be set in conjunction with InGoPath") 171 } 172 if config.GOPROXY != "" && config.ProxyFiles != nil { 173 return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles") 174 } 175 return nil 176 } 177 178 // splitModuleVersionPath extracts module information from files stored in the 179 // directory structure modulePath@version/suffix. 180 // For example: 181 // splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package") 182 func splitModuleVersionPath(path string) (modulePath, version, suffix string) { 183 parts := strings.Split(path, "/") 184 var modulePathParts []string 185 for i, p := range parts { 186 if strings.Contains(p, "@") { 187 mv := strings.SplitN(p, "@", 2) 188 modulePathParts = append(modulePathParts, mv[0]) 189 return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/") 190 } 191 modulePathParts = append(modulePathParts, p) 192 } 193 // Default behavior: this is just a module path. 194 return path, "", "" 195 } 196 197 func (sb *Sandbox) RootDir() string { 198 return sb.rootdir 199 } 200 201 // GOPATH returns the value of the Sandbox GOPATH. 202 func (sb *Sandbox) GOPATH() string { 203 return sb.gopath 204 } 205 206 // GoEnv returns the default environment variables that can be used for 207 // invoking Go commands in the sandbox. 208 func (sb *Sandbox) GoEnv() map[string]string { 209 vars := map[string]string{ 210 "GOPATH": sb.GOPATH(), 211 "GOPROXY": sb.goproxy, 212 "GO111MODULE": "", 213 "GOSUMDB": "off", 214 "GOPACKAGESDRIVER": "off", 215 } 216 if testenv.Go1Point() >= 5 { 217 vars["GOMODCACHE"] = "" 218 } 219 return vars 220 } 221 222 // RunGoCommand executes a go command in the sandbox. If checkForFileChanges is 223 // true, the sandbox scans the working directory and emits file change events 224 // for any file changes it finds. 225 func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { 226 var vars []string 227 for k, v := range sb.GoEnv() { 228 vars = append(vars, fmt.Sprintf("%s=%s", k, v)) 229 } 230 inv := gocommand.Invocation{ 231 Verb: verb, 232 Args: args, 233 Env: vars, 234 } 235 // Use the provided directory for the working directory, if available. 236 // sb.Workdir may be nil if we exited the constructor with errors (we call 237 // Close to clean up any partial state from the constructor, which calls 238 // RunGoCommand). 239 if dir != "" { 240 inv.WorkingDir = sb.Workdir.AbsPath(dir) 241 } else if sb.Workdir != nil { 242 inv.WorkingDir = string(sb.Workdir.RelativeTo) 243 } 244 gocmdRunner := &gocommand.Runner{} 245 stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv) 246 if err != nil { 247 return errors.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) 248 } 249 // Since running a go command may result in changes to workspace files, 250 // check if we need to send any any "watched" file events. 251 // 252 // TODO(rFindley): this side-effect can impact the usability of the sandbox 253 // for benchmarks. Consider refactoring. 254 if sb.Workdir != nil && checkForFileChanges { 255 if err := sb.Workdir.CheckForFileChanges(ctx); err != nil { 256 return errors.Errorf("checking for file changes: %w", err) 257 } 258 } 259 return nil 260 } 261 262 // Close removes all state associated with the sandbox. 263 func (sb *Sandbox) Close() error { 264 var goCleanErr error 265 if sb.gopath != "" { 266 goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false) 267 } 268 err := os.RemoveAll(sb.rootdir) 269 if err != nil || goCleanErr != nil { 270 return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) 271 } 272 return nil 273 }