1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
13 "golang.org/x/tools/internal/lsp/fake"
14 "golang.org/x/tools/internal/lsp/source"
15 "golang.org/x/tools/internal/span"
18 // osFileSource is a fileSource that just reads from the operating system.
19 type osFileSource struct {
20 overlays map[span.URI]fakeOverlay
23 type fakeOverlay struct {
24 source.VersionedFileHandle
31 func (o fakeOverlay) Saved() bool { return o.saved }
33 func (o fakeOverlay) Read() ([]byte, error) {
37 return []byte(o.content), nil
40 func (o fakeOverlay) URI() span.URI {
44 // change updates the file source with the given file content. For convenience,
45 // empty content signals a deletion. If saved is true, these changes are
47 func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) {
49 delete(s.overlays, uri)
51 if err := os.Remove(uri.Filename()); err != nil {
55 fh, err := s.GetFile(ctx, uri)
59 data, err := fh.Read()
60 return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil
62 if s.overlays == nil {
63 s.overlays = map[span.URI]fakeOverlay{}
65 s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved}
67 exists: content != "",
68 content: []byte(content),
69 fileHandle: s.overlays[uri],
73 func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
74 if overlay, ok := s.overlays[uri]; ok {
77 fi, statErr := os.Stat(uri.Filename())
84 fh, err := readFile(ctx, uri, fi)
92 source workspaceSource
98 type wsChange struct {
103 func TestWorkspaceModule(t *testing.T) {
106 initial string // txtar-encoded
109 updates map[string]wsChange
120 golang.org/x/mod v0.3.0 h1:deadbeef
124 initialState: wsState{
125 modules: []string{"./go.mod"},
126 source: legacyWorkspace,
128 sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
132 desc: "nested module",
138 initialState: wsState{
139 modules: []string{"./go.mod", "a/go.mod"},
140 source: fileSystemWorkspace,
141 dirs: []string{".", "a"},
145 desc: "removing module",
150 golang.org/x/mod v0.3.0 h1:deadbeef
154 golang.org/x/mod v0.3.0 h1:beefdead`,
155 initialState: wsState{
156 modules: []string{"a/go.mod", "b/go.mod"},
157 source: fileSystemWorkspace,
158 dirs: []string{".", "a", "b"},
159 sum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n",
161 updates: map[string]wsChange{
162 "gopls.mod": {`module gopls-workspace
164 require moda.com v0.0.0-goplsworkspace
165 replace moda.com => $SANDBOX_WORKDIR/a`, true},
170 modules: []string{"a/go.mod"},
171 source: goplsModWorkspace,
172 dirs: []string{".", "a"},
173 sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n",
177 desc: "adding module",
180 require moda.com v0.0.0-goplsworkspace
181 replace moda.com => $SANDBOX_WORKDIR/a
186 initialState: wsState{
187 modules: []string{"a/go.mod"},
188 source: goplsModWorkspace,
189 dirs: []string{".", "a"},
191 updates: map[string]wsChange{
192 "gopls.mod": {`module gopls-workspace
194 require moda.com v0.0.0-goplsworkspace
195 require modb.com v0.0.0-goplsworkspace
197 replace moda.com => $SANDBOX_WORKDIR/a
198 replace modb.com => $SANDBOX_WORKDIR/b`, true},
203 modules: []string{"a/go.mod", "b/go.mod"},
204 source: goplsModWorkspace,
205 dirs: []string{".", "a", "b"},
209 desc: "deleting gopls.mod",
212 module gopls-workspace
214 require moda.com v0.0.0-goplsworkspace
215 replace moda.com => $SANDBOX_WORKDIR/a
220 initialState: wsState{
221 modules: []string{"a/go.mod"},
222 source: goplsModWorkspace,
223 dirs: []string{".", "a"},
225 updates: map[string]wsChange{
226 "gopls.mod": {"", true},
231 modules: []string{"a/go.mod", "b/go.mod"},
232 source: fileSystemWorkspace,
233 dirs: []string{".", "a", "b"},
237 desc: "broken module parsing",
242 require gopls.test v0.0.0-goplsworkspace
243 replace gopls.test => ../../gopls.test // (this path shouldn't matter)
246 initialState: wsState{
247 modules: []string{"a/go.mod", "b/go.mod"},
248 source: fileSystemWorkspace,
249 dirs: []string{".", "a", "b", "../gopls.test"},
251 updates: map[string]wsChange{
252 "a/go.mod": {`modul moda.com
254 require gopls.test v0.0.0-goplsworkspace
255 replace gopls.test => ../../gopls.test2`, false},
260 modules: []string{"a/go.mod", "b/go.mod"},
261 source: fileSystemWorkspace,
262 // finalDirs should be unchanged: we should preserve dirs in the presence
263 // of a broken modfile.
264 dirs: []string{".", "a", "b", "../gopls.test"},
269 for _, test := range tests {
270 t.Run(test.desc, func(t *testing.T) {
271 ctx := context.Background()
272 dir, err := fake.Tempdir(test.initial)
276 defer os.RemoveAll(dir)
277 root := span.URIFromPath(dir)
279 fs := &osFileSource{}
280 excludeNothing := func(string) bool { return false }
281 w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode)
285 rel := fake.RelativeTo(dir)
286 checkState(ctx, t, fs, rel, w, test.initialState)
289 if test.updates != nil {
290 changes := make(map[span.URI]*fileChange)
291 for k, v := range test.updates {
292 content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel))
293 uri := span.URIFromPath(rel.AbsPath(k))
294 changes[uri], err = fs.change(ctx, uri, content, v.saved)
299 got, gotChanged, gotReload := w.invalidate(ctx, changes)
300 if gotChanged != test.wantChanged {
301 t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged)
303 if gotReload != test.wantReload {
304 t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload)
306 checkState(ctx, t, fs, rel, got, test.finalState)
312 func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) {
314 if got.moduleSource != want.source {
315 t.Errorf("module source = %v, want %v", got.moduleSource, want.source)
317 modules := make(map[span.URI]struct{})
318 for k := range got.getActiveModFiles() {
319 modules[k] = struct{}{}
321 for _, modPath := range want.modules {
322 path := rel.AbsPath(modPath)
323 uri := span.URIFromPath(path)
324 if _, ok := modules[uri]; !ok {
325 t.Errorf("missing module %q", uri)
329 for remaining := range modules {
330 t.Errorf("unexpected module %q", remaining)
332 gotDirs := got.dirs(ctx, fs)
333 gotM := make(map[span.URI]bool)
334 for _, dir := range gotDirs {
337 for _, dir := range want.dirs {
338 path := rel.AbsPath(dir)
339 uri := span.URIFromPath(path)
341 t.Errorf("missing dir %q", uri)
345 for remaining := range gotM {
346 t.Errorf("unexpected dir %q", remaining)
348 gotSumBytes, err := got.sumFile(ctx, fs)
352 if gotSum := string(gotSumBytes); gotSum != want.sum {
353 t.Errorf("got final sum %q, want %q", gotSum, want.sum)