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. 4 5package cache 6 7import ( 8 "context" 9 "os" 10 "strings" 11 "testing" 12 13 "golang.org/x/tools/internal/lsp/fake" 14 "golang.org/x/tools/internal/lsp/source" 15 "golang.org/x/tools/internal/span" 16) 17 18// osFileSource is a fileSource that just reads from the operating system. 19type osFileSource struct { 20 overlays map[span.URI]fakeOverlay 21} 22 23type fakeOverlay struct { 24 source.VersionedFileHandle 25 uri span.URI 26 content string 27 err error 28 saved bool 29} 30 31func (o fakeOverlay) Saved() bool { return o.saved } 32 33func (o fakeOverlay) Read() ([]byte, error) { 34 if o.err != nil { 35 return nil, o.err 36 } 37 return []byte(o.content), nil 38} 39 40func (o fakeOverlay) URI() span.URI { 41 return o.uri 42} 43 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 46// persisted to disk. 47func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) { 48 if content == "" { 49 delete(s.overlays, uri) 50 if saved { 51 if err := os.Remove(uri.Filename()); err != nil { 52 return nil, err 53 } 54 } 55 fh, err := s.GetFile(ctx, uri) 56 if err != nil { 57 return nil, err 58 } 59 data, err := fh.Read() 60 return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil 61 } 62 if s.overlays == nil { 63 s.overlays = map[span.URI]fakeOverlay{} 64 } 65 s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved} 66 return &fileChange{ 67 exists: content != "", 68 content: []byte(content), 69 fileHandle: s.overlays[uri], 70 }, nil 71} 72 73func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { 74 if overlay, ok := s.overlays[uri]; ok { 75 return overlay, nil 76 } 77 fi, statErr := os.Stat(uri.Filename()) 78 if statErr != nil { 79 return &fileHandle{ 80 err: statErr, 81 uri: uri, 82 }, nil 83 } 84 fh, err := readFile(ctx, uri, fi) 85 if err != nil { 86 return nil, err 87 } 88 return fh, nil 89} 90 91type wsState struct { 92 source workspaceSource 93 modules []string 94 dirs []string 95 sum string 96} 97 98type wsChange struct { 99 content string 100 saved bool 101} 102 103func TestWorkspaceModule(t *testing.T) { 104 tests := []struct { 105 desc string 106 initial string // txtar-encoded 107 legacyMode bool 108 initialState wsState 109 updates map[string]wsChange 110 wantChanged bool 111 wantReload bool 112 finalState wsState 113 }{ 114 { 115 desc: "legacy mode", 116 initial: ` 117-- go.mod -- 118module mod.com 119-- go.sum -- 120golang.org/x/mod v0.3.0 h1:deadbeef 121-- a/go.mod -- 122module moda.com`, 123 legacyMode: true, 124 initialState: wsState{ 125 modules: []string{"./go.mod"}, 126 source: legacyWorkspace, 127 dirs: []string{"."}, 128 sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", 129 }, 130 }, 131 { 132 desc: "nested module", 133 initial: ` 134-- go.mod -- 135module mod.com 136-- a/go.mod -- 137module moda.com`, 138 initialState: wsState{ 139 modules: []string{"./go.mod", "a/go.mod"}, 140 source: fileSystemWorkspace, 141 dirs: []string{".", "a"}, 142 }, 143 }, 144 { 145 desc: "removing module", 146 initial: ` 147-- a/go.mod -- 148module moda.com 149-- a/go.sum -- 150golang.org/x/mod v0.3.0 h1:deadbeef 151-- b/go.mod -- 152module modb.com 153-- b/go.sum -- 154golang.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", 160 }, 161 updates: map[string]wsChange{ 162 "gopls.mod": {`module gopls-workspace 163 164require moda.com v0.0.0-goplsworkspace 165replace moda.com => $SANDBOX_WORKDIR/a`, true}, 166 }, 167 wantChanged: true, 168 wantReload: true, 169 finalState: wsState{ 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", 174 }, 175 }, 176 { 177 desc: "adding module", 178 initial: ` 179-- gopls.mod -- 180require moda.com v0.0.0-goplsworkspace 181replace moda.com => $SANDBOX_WORKDIR/a 182-- a/go.mod -- 183module moda.com 184-- b/go.mod -- 185module modb.com`, 186 initialState: wsState{ 187 modules: []string{"a/go.mod"}, 188 source: goplsModWorkspace, 189 dirs: []string{".", "a"}, 190 }, 191 updates: map[string]wsChange{ 192 "gopls.mod": {`module gopls-workspace 193 194require moda.com v0.0.0-goplsworkspace 195require modb.com v0.0.0-goplsworkspace 196 197replace moda.com => $SANDBOX_WORKDIR/a 198replace modb.com => $SANDBOX_WORKDIR/b`, true}, 199 }, 200 wantChanged: true, 201 wantReload: true, 202 finalState: wsState{ 203 modules: []string{"a/go.mod", "b/go.mod"}, 204 source: goplsModWorkspace, 205 dirs: []string{".", "a", "b"}, 206 }, 207 }, 208 { 209 desc: "deleting gopls.mod", 210 initial: ` 211-- gopls.mod -- 212module gopls-workspace 213 214require moda.com v0.0.0-goplsworkspace 215replace moda.com => $SANDBOX_WORKDIR/a 216-- a/go.mod -- 217module moda.com 218-- b/go.mod -- 219module modb.com`, 220 initialState: wsState{ 221 modules: []string{"a/go.mod"}, 222 source: goplsModWorkspace, 223 dirs: []string{".", "a"}, 224 }, 225 updates: map[string]wsChange{ 226 "gopls.mod": {"", true}, 227 }, 228 wantChanged: true, 229 wantReload: true, 230 finalState: wsState{ 231 modules: []string{"a/go.mod", "b/go.mod"}, 232 source: fileSystemWorkspace, 233 dirs: []string{".", "a", "b"}, 234 }, 235 }, 236 { 237 desc: "broken module parsing", 238 initial: ` 239-- a/go.mod -- 240module moda.com 241 242require gopls.test v0.0.0-goplsworkspace 243replace gopls.test => ../../gopls.test // (this path shouldn't matter) 244-- b/go.mod -- 245module modb.com`, 246 initialState: wsState{ 247 modules: []string{"a/go.mod", "b/go.mod"}, 248 source: fileSystemWorkspace, 249 dirs: []string{".", "a", "b", "../gopls.test"}, 250 }, 251 updates: map[string]wsChange{ 252 "a/go.mod": {`modul moda.com 253 254require gopls.test v0.0.0-goplsworkspace 255replace gopls.test => ../../gopls.test2`, false}, 256 }, 257 wantChanged: true, 258 wantReload: false, 259 finalState: wsState{ 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"}, 265 }, 266 }, 267 } 268 269 for _, test := range tests { 270 t.Run(test.desc, func(t *testing.T) { 271 ctx := context.Background() 272 dir, err := fake.Tempdir(fake.UnpackTxt(test.initial)) 273 if err != nil { 274 t.Fatal(err) 275 } 276 defer os.RemoveAll(dir) 277 root := span.URIFromPath(dir) 278 279 fs := &osFileSource{} 280 excludeNothing := func(string) bool { return false } 281 w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode) 282 if err != nil { 283 t.Fatal(err) 284 } 285 rel := fake.RelativeTo(dir) 286 checkState(ctx, t, fs, rel, w, test.initialState) 287 288 // Apply updates. 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) 295 if err != nil { 296 t.Fatal(err) 297 } 298 } 299 got, gotChanged, gotReload := w.invalidate(ctx, changes, fs) 300 if gotChanged != test.wantChanged { 301 t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) 302 } 303 if gotReload != test.wantReload { 304 t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload) 305 } 306 checkState(ctx, t, fs, rel, got, test.finalState) 307 } 308 }) 309 } 310} 311 312func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) { 313 t.Helper() 314 if got.moduleSource != want.source { 315 t.Errorf("module source = %v, want %v", got.moduleSource, want.source) 316 } 317 modules := make(map[span.URI]struct{}) 318 for k := range got.getActiveModFiles() { 319 modules[k] = struct{}{} 320 } 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) 326 } 327 delete(modules, uri) 328 } 329 for remaining := range modules { 330 t.Errorf("unexpected module %q", remaining) 331 } 332 gotDirs := got.dirs(ctx, fs) 333 gotM := make(map[span.URI]bool) 334 for _, dir := range gotDirs { 335 gotM[dir] = true 336 } 337 for _, dir := range want.dirs { 338 path := rel.AbsPath(dir) 339 uri := span.URIFromPath(path) 340 if !gotM[uri] { 341 t.Errorf("missing dir %q", uri) 342 } 343 delete(gotM, uri) 344 } 345 for remaining := range gotM { 346 t.Errorf("unexpected dir %q", remaining) 347 } 348 gotSumBytes, err := got.sumFile(ctx, fs) 349 if err != nil { 350 t.Fatal(err) 351 } 352 if gotSum := string(gotSumBytes); gotSum != want.sum { 353 t.Errorf("got final sum %q, want %q", gotSum, want.sum) 354 } 355} 356