| // 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) |
| } |
| } |