blob: f3c4a1b0508dd94ecb45d5098d3d233c0808c61d [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fake
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/proxydir"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/txtar"
)
// FileEvent wraps the protocol.FileEvent so that it can be associated with a
// workspace-relative path.
type FileEvent struct {
Path string
ProtocolEvent protocol.FileEvent
}
// The Workspace type represents a temporary workspace to use for editing Go
// files in tests.
type Workspace struct {
name string
gopath string
workdir string
proxydir string
watcherMu sync.Mutex
watchers []func(context.Context, []FileEvent)
}
// NewWorkspace creates a named workspace populated by the txtar-encoded
// content given by txt. It creates temporary directories for the workspace
// content and for GOPATH.
func NewWorkspace(name string, srctxt string, proxytxt string) (_ *Workspace, err error) {
w := &Workspace{name: name}
defer func() {
// Clean up if we fail at any point in this constructor.
if err != nil {
w.removeAll()
}
}()
dir, err := ioutil.TempDir("", fmt.Sprintf("goplstest-ws-%s-", name))
if err != nil {
return nil, fmt.Errorf("creating temporary workdir: %v", err)
}
w.workdir = dir
gopath, err := ioutil.TempDir("", fmt.Sprintf("goplstest-gopath-%s-", name))
if err != nil {
return nil, fmt.Errorf("creating temporary gopath: %v", err)
}
w.gopath = gopath
files := unpackTxt(srctxt)
for name, data := range files {
if err := w.writeFileData(name, string(data)); err != nil {
return nil, fmt.Errorf("writing to workdir: %v", err)
}
}
pd, err := ioutil.TempDir("", fmt.Sprintf("goplstest-proxy-%s-", name))
if err != nil {
return nil, fmt.Errorf("creating temporary proxy dir: %v", err)
}
w.proxydir = pd
if err := writeProxyDir(unpackTxt(proxytxt), w.proxydir); err != nil {
return nil, fmt.Errorf("writing proxy dir: %v", err)
}
return w, nil
}
func unpackTxt(txt string) map[string][]byte {
dataMap := make(map[string][]byte)
archive := txtar.Parse([]byte(txt))
for _, f := range archive.Files {
dataMap[f.Name] = f.Data
}
return dataMap
}
func writeProxyDir(files map[string][]byte, dir string) error {
type moduleVersion struct {
modulePath, version string
}
// Transform into the format expected by the proxydir package.
filesByModule := make(map[moduleVersion]map[string][]byte)
for name, data := range files {
modulePath, version, suffix := splitModuleVersionPath(name)
mv := moduleVersion{modulePath, version}
if _, ok := filesByModule[mv]; !ok {
filesByModule[mv] = make(map[string][]byte)
}
filesByModule[mv][suffix] = data
}
for mv, files := range filesByModule {
if err := proxydir.WriteModuleVersion(dir, mv.modulePath, mv.version, files); err != nil {
return fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err)
}
}
return nil
}
// splitModuleVersionPath extracts module information from files stored in the
// directory structure modulePath@version/suffix.
// For example:
// splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package")
func splitModuleVersionPath(path string) (modulePath, version, suffix string) {
parts := strings.Split(path, "/")
var modulePathParts []string
for i, p := range parts {
if strings.Contains(p, "@") {
mv := strings.SplitN(p, "@", 2)
modulePathParts = append(modulePathParts, mv[0])
return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/")
}
modulePathParts = append(modulePathParts, p)
}
// Default behavior: this is just a module path.
return path, "", ""
}
// RootURI returns the root URI for this workspace.
func (w *Workspace) RootURI() protocol.DocumentURI {
return toURI(w.workdir)
}
// GOPATH returns the value that GOPATH should be set to for this workspace.
func (w *Workspace) GOPATH() string {
return w.gopath
}
// GOPROXY returns the value that GOPROXY should be set to for this workspace.
func (w *Workspace) GOPROXY() string {
return proxydir.ToURL(w.proxydir)
}
// AddWatcher registers the given func to be called on any file change.
func (w *Workspace) AddWatcher(watcher func(context.Context, []FileEvent)) {
w.watcherMu.Lock()
w.watchers = append(w.watchers, watcher)
w.watcherMu.Unlock()
}
// filePath returns the absolute filesystem path to a the workspace-relative
// path.
func (w *Workspace) filePath(path string) string {
fp := filepath.FromSlash(path)
if filepath.IsAbs(fp) {
return fp
}
return filepath.Join(w.workdir, filepath.FromSlash(path))
}
// URI returns the URI to a the workspace-relative path.
func (w *Workspace) URI(path string) protocol.DocumentURI {
return toURI(w.filePath(path))
}
// URIToPath converts a uri to a workspace-relative path (or an absolute path,
// if the uri is outside of the workspace).
func (w *Workspace) URIToPath(uri protocol.DocumentURI) string {
root := w.RootURI().SpanURI().Filename()
path := uri.SpanURI().Filename()
if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, "..") {
return filepath.ToSlash(rel)
}
return filepath.ToSlash(path)
}
func toURI(fp string) protocol.DocumentURI {
return protocol.DocumentURI(span.URIFromPath(fp))
}
// ReadFile reads a text file specified by a workspace-relative path.
func (w *Workspace) ReadFile(path string) (string, error) {
b, err := ioutil.ReadFile(w.filePath(path))
if err != nil {
return "", err
}
return string(b), nil
}
// RegexpSearch searches the file corresponding to path for the first position
// matching re.
func (w *Workspace) RegexpSearch(path string, re string) (Pos, error) {
content, err := w.ReadFile(path)
if err != nil {
return Pos{}, err
}
start, _, err := regexpRange(content, re)
return start, err
}
// RemoveFile removes a workspace-relative file path.
func (w *Workspace) RemoveFile(ctx context.Context, path string) error {
fp := w.filePath(path)
if err := os.Remove(fp); err != nil {
return fmt.Errorf("removing %q: %v", path, err)
}
evts := []FileEvent{{
Path: path,
ProtocolEvent: protocol.FileEvent{
URI: w.URI(path),
Type: protocol.Deleted,
},
}}
w.sendEvents(ctx, evts)
return nil
}
// GoEnv returns the environment variables that should be used for invoking Go
// commands in the workspace.
func (w *Workspace) GoEnv() []string {
return []string{
"GOPATH=" + w.GOPATH(),
"GOPROXY=" + w.GOPROXY(),
"GO111MODULE=",
"GOSUMDB=off",
}
}
// RunGoCommand executes a go command in the workspace.
func (w *Workspace) RunGoCommand(ctx context.Context, verb string, args ...string) error {
inv := gocommand.Invocation{
Verb: verb,
Args: args,
WorkingDir: w.workdir,
Env: w.GoEnv(),
}
gocmdRunner := &gocommand.Runner{}
_, stderr, _, err := gocmdRunner.RunRaw(ctx, inv)
if err != nil {
return err
}
// Hardcoded "file watcher": If the command executed was "go mod init",
// send a file creation event for a go.mod in the working directory.
if strings.HasPrefix(stderr.String(), "go: creating new go.mod") {
modpath := filepath.Join(w.workdir, "go.mod")
w.sendEvents(ctx, []FileEvent{{
Path: modpath,
ProtocolEvent: protocol.FileEvent{
URI: toURI(modpath),
Type: protocol.Created,
},
}})
}
return nil
}
func (w *Workspace) sendEvents(ctx context.Context, evts []FileEvent) {
w.watcherMu.Lock()
watchers := make([]func(context.Context, []FileEvent), len(w.watchers))
copy(watchers, w.watchers)
w.watcherMu.Unlock()
for _, w := range watchers {
go w(ctx, evts)
}
}
// WriteFile writes text file content to a workspace-relative path.
func (w *Workspace) WriteFile(ctx context.Context, path, content string) error {
fp := w.filePath(path)
_, err := os.Stat(fp)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("checking if %q exists: %v", path, err)
}
var changeType protocol.FileChangeType
if os.IsNotExist(err) {
changeType = protocol.Created
} else {
changeType = protocol.Changed
}
if err := w.writeFileData(path, content); err != nil {
return err
}
evts := []FileEvent{{
Path: path,
ProtocolEvent: protocol.FileEvent{
URI: w.URI(path),
Type: changeType,
},
}}
w.sendEvents(ctx, evts)
return nil
}
func (w *Workspace) writeFileData(path string, content string) error {
fp := w.filePath(path)
if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil {
return fmt.Errorf("creating nested directory: %v", err)
}
if err := ioutil.WriteFile(fp, []byte(content), 0644); err != nil {
return fmt.Errorf("writing %q: %v", path, err)
}
return nil
}
func (w *Workspace) removeAll() error {
var wsErr, gopathErr error
if w.gopath != "" {
if err := w.RunGoCommand(context.Background(), "clean", "-modcache"); err != nil {
gopathErr = fmt.Errorf("cleaning modcache: %v", err)
} else {
gopathErr = os.RemoveAll(w.gopath)
}
}
if w.workdir != "" {
wsErr = os.RemoveAll(w.workdir)
}
if wsErr != nil || gopathErr != nil {
return fmt.Errorf("error(s) cleaning workspace: removing workdir: %v; removing gopath: %v", wsErr, gopathErr)
}
return nil
}
// Close removes all state associated with the workspace.
func (w *Workspace) Close() error {
return w.removeAll()
}