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 }