mirror of https://github.com/golang/go.git
356 lines
8.6 KiB
Go
356 lines
8.6 KiB
Go
// 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"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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(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, gotChanged, gotReload := w.invalidate(ctx, changes)
|
|
if gotChanged != test.wantChanged {
|
|
t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged)
|
|
}
|
|
if gotReload != test.wantReload {
|
|
t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload)
|
|
}
|
|
checkState(ctx, t, fs, rel, got, test.finalState)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|