blob: 7cd0ecee563b440c45a1457eab8c7980829b5534 [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"
"os"
"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{}
func (s osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
fi, statErr := os.Stat(uri.Filename())
if statErr != nil {
return &fileHandle{
err: statErr,
uri: uri,
}, nil
}
fh, err := readFile(ctx, uri, fi.ModTime())
if err != nil {
return nil, err
}
return fh, nil
}
func TestWorkspaceModule(t *testing.T) {
tests := []struct {
desc string
initial string // txtar-encoded
legacyMode bool
initialSource workspaceSource
initialModules []string
initialDirs []string
updates map[string]string
finalSource workspaceSource
finalModules []string
finalDirs []string
}{
{
desc: "legacy mode",
initial: `
-- go.mod --
module mod.com
-- a/go.mod --
module moda.com`,
legacyMode: true,
initialModules: []string{"./go.mod"},
initialSource: legacyWorkspace,
initialDirs: []string{"."},
},
{
desc: "nested module",
initial: `
-- go.mod --
module mod.com
-- a/go.mod --
module moda.com`,
initialModules: []string{"./go.mod", "a/go.mod"},
initialSource: fileSystemWorkspace,
initialDirs: []string{".", "a"},
},
{
desc: "removing module",
initial: `
-- a/go.mod --
module moda.com
-- b/go.mod --
module modb.com`,
initialModules: []string{"a/go.mod", "b/go.mod"},
initialSource: fileSystemWorkspace,
initialDirs: []string{".", "a", "b"},
updates: map[string]string{
"gopls.mod": `module gopls-workspace
require moda.com v0.0.0-goplsworkspace
replace moda.com => $SANDBOX_WORKDIR/a`,
},
finalModules: []string{"a/go.mod"},
finalSource: goplsModWorkspace,
finalDirs: []string{".", "a"},
},
{
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`,
initialModules: []string{"a/go.mod"},
initialSource: goplsModWorkspace,
initialDirs: []string{".", "a"},
updates: map[string]string{
"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`,
},
finalModules: []string{"a/go.mod", "b/go.mod"},
finalSource: goplsModWorkspace,
finalDirs: []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`,
initialModules: []string{"a/go.mod"},
initialSource: goplsModWorkspace,
initialDirs: []string{".", "a"},
updates: map[string]string{
"gopls.mod": "",
},
finalModules: []string{"a/go.mod", "b/go.mod"},
finalSource: fileSystemWorkspace,
finalDirs: []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`,
initialModules: []string{"a/go.mod", "b/go.mod"},
initialSource: fileSystemWorkspace,
initialDirs: []string{".", "a", "b", "../gopls.test"},
updates: map[string]string{
"a/go.mod": `modul moda.com
require gopls.test v0.0.0-goplsworkspace
replace gopls.test => ../../gopls.test2`,
},
finalModules: []string{"a/go.mod", "b/go.mod"},
finalSource: fileSystemWorkspace,
// finalDirs should be unchanged: we should preserve dirs in the presence
// of a broken modfile.
finalDirs: []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{}
wm, err := newWorkspace(ctx, root, fs, !test.legacyMode)
if err != nil {
t.Fatal(err)
}
rel := fake.RelativeTo(dir)
checkWorkspaceModule(t, rel, wm, test.initialSource, test.initialModules)
gotDirs := wm.dirs(ctx, fs)
checkWorkspaceDirs(t, rel, gotDirs, test.initialDirs)
if test.updates != nil {
changes := make(map[span.URI]*fileChange)
for k, v := range test.updates {
if v == "" {
// for convenience, use this to signal a deletion. TODO: more doc
err := os.Remove(rel.AbsPath(k))
if err != nil {
t.Fatal(err)
}
} else {
fake.WriteFileData(k, []byte(v), rel)
}
uri := span.URIFromPath(rel.AbsPath(k))
fh, err := fs.GetFile(ctx, uri)
if err != nil {
t.Fatal(err)
}
content, err := fh.Read()
changes[uri] = &fileChange{
content: content,
exists: err == nil,
fileHandle: &closedFile{fh},
}
}
wm, _ := wm.invalidate(ctx, changes)
checkWorkspaceModule(t, rel, wm, test.finalSource, test.finalModules)
gotDirs := wm.dirs(ctx, fs)
checkWorkspaceDirs(t, rel, gotDirs, test.finalDirs)
}
})
}
}
func checkWorkspaceModule(t *testing.T, rel fake.RelativeTo, got *workspace, wantSource workspaceSource, want []string) {
t.Helper()
if got.moduleSource != wantSource {
t.Errorf("module source = %v, want %v", got.moduleSource, wantSource)
}
modules := make(map[span.URI]struct{})
for k := range got.activeModFiles() {
modules[k] = struct{}{}
}
for _, modPath := range want {
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)
}
}
func checkWorkspaceDirs(t *testing.T, rel fake.RelativeTo, got []span.URI, want []string) {
t.Helper()
gotM := make(map[span.URI]bool)
for _, dir := range got {
gotM[dir] = true
}
for _, dir := range want {
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)
}
}