blob: 871e4bb9b98bd0a084307a7d391ac300b43844e3 [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 cache
import (
"context"
"errors"
"os"
"strings"
"testing"
"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
// osFileSource is a fileSource that just reads from the operating system.
type osFileSource struct {
overlays map[span.URI]fakeOverlay
}
type fakeOverlay struct {
source.VersionedFileHandle
uri span.URI
content string
err error
saved bool
}
func (o fakeOverlay) Saved() bool { return o.saved }
func (o fakeOverlay) Read() ([]byte, error) {
if o.err != nil {
return nil, o.err
}
return []byte(o.content), nil
}
func (o fakeOverlay) URI() span.URI {
return o.uri
}
// change updates the file source with the given file content. For convenience,
// empty content signals a deletion. If saved is true, these changes are
// persisted to disk.
func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) {
if content == "" {
delete(s.overlays, uri)
if saved {
if err := os.Remove(uri.Filename()); err != nil {
return nil, err
}
}
fh, err := s.GetFile(ctx, uri)
if err != nil {
return nil, err
}
data, err := fh.Read()
return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil
}
if s.overlays == nil {
s.overlays = map[span.URI]fakeOverlay{}
}
s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved}
return &fileChange{
exists: content != "",
content: []byte(content),
fileHandle: s.overlays[uri],
}, nil
}
func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
if overlay, ok := s.overlays[uri]; ok {
return overlay, nil
}
fi, statErr := os.Stat(uri.Filename())
if statErr != nil {
return &fileHandle{
err: statErr,
uri: uri,
}, nil
}
fh, err := readFile(ctx, uri, fi)
if err != nil {
return nil, err
}
return fh, nil
}
type wsState struct {
source workspaceSource
modules []string
dirs []string
sum string
}
type wsChange struct {
content string
saved bool
}
func TestWorkspaceModule(t *testing.T) {
tests := []struct {
desc string
initial string // txtar-encoded
legacyMode bool
initialState wsState
updates map[string]wsChange
wantChanged bool
wantReload bool
finalState wsState
}{
{
desc: "legacy mode",
initial: `
-- go.mod --
module mod.com
-- go.sum --
golang.org/x/mod v0.3.0 h1:deadbeef
-- a/go.mod --
module moda.com`,
legacyMode: true,
initialState: wsState{
modules: []string{"./go.mod"},
source: legacyWorkspace,
dirs: []string{"."},
sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
},
},
{
desc: "nested module",
initial: `
-- go.mod --
module mod.com
-- a/go.mod --
module moda.com`,
initialState: wsState{
modules: []string{"./go.mod", "a/go.mod"},
source: fileSystemWorkspace,
dirs: []string{".", "a"},
},
},
{
desc: "removing module",
initial: `
-- a/go.mod --
module moda.com
-- a/go.sum --
golang.org/x/mod v0.3.0 h1:deadbeef
-- b/go.mod --
module modb.com
-- b/go.sum --
golang.org/x/mod v0.3.0 h1:beefdead`,
initialState: wsState{
modules: []string{"a/go.mod", "b/go.mod"},
source: fileSystemWorkspace,
dirs: []string{".", "a", "b"},
sum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n",
},
updates: map[string]wsChange{
"gopls.mod": {`module gopls-workspace
require moda.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a`, true},
},
wantChanged: true,
wantReload: true,
finalState: wsState{
modules: []string{"a/go.mod"},
source: goplsModWorkspace,
dirs: []string{".", "a"},
sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
},
},
{
desc: "adding module",
initial: `
-- gopls.mod --
require moda.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a
-- a/go.mod --
module moda.com
-- b/go.mod --
module modb.com`,
initialState: wsState{
modules: []string{"a/go.mod"},
source: goplsModWorkspace,
dirs: []string{".", "a"},
},
updates: map[string]wsChange{
"gopls.mod": {`module gopls-workspace
require moda.com v0.0.0-goplsworkspace
require modb.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a
replace modb.com => $SANDBOX_WORKDIR/b`, true},
},
wantChanged: true,
wantReload: true,
finalState: wsState{
modules: []string{"a/go.mod", "b/go.mod"},
source: goplsModWorkspace,
dirs: []string{".", "a", "b"},
},
},
{
desc: "deleting gopls.mod",
initial: `
-- gopls.mod --
module gopls-workspace
require moda.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a
-- a/go.mod --
module moda.com
-- b/go.mod --
module modb.com`,
initialState: wsState{
modules: []string{"a/go.mod"},
source: goplsModWorkspace,
dirs: []string{".", "a"},
},
updates: map[string]wsChange{
"gopls.mod": {"", true},
},
wantChanged: true,
wantReload: true,
finalState: wsState{
modules: []string{"a/go.mod", "b/go.mod"},
source: fileSystemWorkspace,
dirs: []string{".", "a", "b"},
},
},
{
desc: "broken module parsing",
initial: `
-- a/go.mod --
module moda.com
require gopls.test v0.0.0-goplsworkspace
replace gopls.test => ../../gopls.test // (this path shouldn't matter)
-- b/go.mod --
module modb.com`,
initialState: wsState{
modules: []string{"a/go.mod", "b/go.mod"},
source: fileSystemWorkspace,
dirs: []string{".", "a", "b", "../gopls.test"},
},
updates: map[string]wsChange{
"a/go.mod": {`modul moda.com
require gopls.test v0.0.0-goplsworkspace
replace gopls.test => ../../gopls.test2`, false},
},
wantChanged: true,
wantReload: false,
finalState: wsState{
modules: []string{"a/go.mod", "b/go.mod"},
source: fileSystemWorkspace,
// finalDirs should be unchanged: we should preserve dirs in the presence
// of a broken modfile.
dirs: []string{".", "a", "b", "../gopls.test"},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ctx := context.Background()
dir, err := fake.Tempdir(fake.UnpackTxt(test.initial))
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
root := span.URIFromPath(dir)
fs := &osFileSource{}
excludeNothing := func(string) bool { return false }
w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode)
if err != nil {
t.Fatal(err)
}
rel := fake.RelativeTo(dir)
checkState(ctx, t, fs, rel, w, test.initialState)
// Apply updates.
if test.updates != nil {
changes := make(map[span.URI]*fileChange)
for k, v := range test.updates {
content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel))
uri := span.URIFromPath(rel.AbsPath(k))
changes[uri], err = fs.change(ctx, uri, content, v.saved)
if err != nil {
t.Fatal(err)
}
}
got, gotReinit := w.Clone(ctx, changes, fs)
gotChanged := got != w
if gotChanged != test.wantChanged {
t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged)
}
if gotReinit != test.wantReload {
t.Errorf("w.invalidate(): got reload %t, want %t", gotReinit, test.wantReload)
}
checkState(ctx, t, fs, rel, got, test.finalState)
}
})
}
}
func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) {
ctx := context.Background()
dir, err := fake.Tempdir(fake.UnpackTxt(files))
if err != nil {
return nil, func() {}, err
}
cleanup := func() {
os.RemoveAll(dir)
}
root := span.URIFromPath(dir)
fs := &osFileSource{}
excludeNothing := func(string) bool { return false }
workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false)
return workspace, cleanup, err
}
func TestWorkspaceParseError(t *testing.T) {
w, cleanup, err := workspaceFromTxtar(t, `
-- go.work --
go 1.18
usa ./typo
-- typo/go.mod --
module foo
`)
defer cleanup()
if err != nil {
t.Fatalf("error creating workspace: %v; want no error", err)
}
w.buildMu.Lock()
built, buildErr := w.built, w.buildErr
w.buildMu.Unlock()
if !built || buildErr == nil {
t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr)
}
var errList modfile.ErrorList
if !errors.As(buildErr, &errList) {
t.Fatalf("expected error to be an errorlist; got %v", buildErr)
}
if len(errList) != 1 {
t.Fatalf("expected errorList to have one element; got %v elements", len(errList))
}
parseErr := errList[0]
if parseErr.Pos.Line != 3 {
t.Fatalf("expected error to be on line 3; got %v", parseErr.Pos.Line)
}
}
func TestWorkspaceMissingModFile(t *testing.T) {
w, cleanup, err := workspaceFromTxtar(t, `
-- go.work --
go 1.18
use ./missing
`)
defer cleanup()
if err != nil {
t.Fatalf("error creating workspace: %v; want no error", err)
}
w.buildMu.Lock()
built, buildErr := w.built, w.buildErr
w.buildMu.Unlock()
if !built || buildErr == nil {
t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr)
}
}
func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) {
t.Helper()
if got.moduleSource != want.source {
t.Errorf("module source = %v, want %v", got.moduleSource, want.source)
}
modules := make(map[span.URI]struct{})
for k := range got.getActiveModFiles() {
modules[k] = struct{}{}
}
for _, modPath := range want.modules {
path := rel.AbsPath(modPath)
uri := span.URIFromPath(path)
if _, ok := modules[uri]; !ok {
t.Errorf("missing module %q", uri)
}
delete(modules, uri)
}
for remaining := range modules {
t.Errorf("unexpected module %q", remaining)
}
gotDirs := got.dirs(ctx, fs)
gotM := make(map[span.URI]bool)
for _, dir := range gotDirs {
gotM[dir] = true
}
for _, dir := range want.dirs {
path := rel.AbsPath(dir)
uri := span.URIFromPath(path)
if !gotM[uri] {
t.Errorf("missing dir %q", uri)
}
delete(gotM, uri)
}
for remaining := range gotM {
t.Errorf("unexpected dir %q", remaining)
}
gotSumBytes, err := got.sumFile(ctx, fs)
if err != nil {
t.Fatal(err)
}
if gotSum := string(gotSumBytes); gotSum != want.sum {
t.Errorf("got final sum %q, want %q", gotSum, want.sum)
}
}