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