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