github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/cache/workspace_test.go (about) 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 5 package cache 6 7 import ( 8 "context" 9 "errors" 10 "os" 11 "strings" 12 "testing" 13 14 "golang.org/x/mod/modfile" 15 "github.com/powerman/golang-tools/internal/lsp/fake" 16 "github.com/powerman/golang-tools/internal/lsp/source" 17 "github.com/powerman/golang-tools/internal/span" 18 ) 19 20 // osFileSource is a fileSource that just reads from the operating system. 21 type osFileSource struct { 22 overlays map[span.URI]fakeOverlay 23 } 24 25 type fakeOverlay struct { 26 source.VersionedFileHandle 27 uri span.URI 28 content string 29 err error 30 saved bool 31 } 32 33 func (o fakeOverlay) Saved() bool { return o.saved } 34 35 func (o fakeOverlay) Read() ([]byte, error) { 36 if o.err != nil { 37 return nil, o.err 38 } 39 return []byte(o.content), nil 40 } 41 42 func (o fakeOverlay) URI() span.URI { 43 return o.uri 44 } 45 46 // change updates the file source with the given file content. For convenience, 47 // empty content signals a deletion. If saved is true, these changes are 48 // persisted to disk. 49 func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) { 50 if content == "" { 51 delete(s.overlays, uri) 52 if saved { 53 if err := os.Remove(uri.Filename()); err != nil { 54 return nil, err 55 } 56 } 57 fh, err := s.GetFile(ctx, uri) 58 if err != nil { 59 return nil, err 60 } 61 data, err := fh.Read() 62 return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil 63 } 64 if s.overlays == nil { 65 s.overlays = map[span.URI]fakeOverlay{} 66 } 67 s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved} 68 return &fileChange{ 69 exists: content != "", 70 content: []byte(content), 71 fileHandle: s.overlays[uri], 72 }, nil 73 } 74 75 func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { 76 if overlay, ok := s.overlays[uri]; ok { 77 return overlay, nil 78 } 79 fi, statErr := os.Stat(uri.Filename()) 80 if statErr != nil { 81 return &fileHandle{ 82 err: statErr, 83 uri: uri, 84 }, nil 85 } 86 fh, err := readFile(ctx, uri, fi) 87 if err != nil { 88 return nil, err 89 } 90 return fh, nil 91 } 92 93 type wsState struct { 94 source workspaceSource 95 modules []string 96 dirs []string 97 sum string 98 } 99 100 type wsChange struct { 101 content string 102 saved bool 103 } 104 105 func TestWorkspaceModule(t *testing.T) { 106 tests := []struct { 107 desc string 108 initial string // txtar-encoded 109 legacyMode bool 110 initialState wsState 111 updates map[string]wsChange 112 wantChanged bool 113 wantReload bool 114 finalState wsState 115 }{ 116 { 117 desc: "legacy mode", 118 initial: ` 119 -- go.mod -- 120 module mod.com 121 -- go.sum -- 122 golang.org/x/mod v0.3.0 h1:deadbeef 123 -- a/go.mod -- 124 module moda.com`, 125 legacyMode: true, 126 initialState: wsState{ 127 modules: []string{"./go.mod"}, 128 source: legacyWorkspace, 129 dirs: []string{"."}, 130 sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", 131 }, 132 }, 133 { 134 desc: "nested module", 135 initial: ` 136 -- go.mod -- 137 module mod.com 138 -- a/go.mod -- 139 module moda.com`, 140 initialState: wsState{ 141 modules: []string{"./go.mod", "a/go.mod"}, 142 source: fileSystemWorkspace, 143 dirs: []string{".", "a"}, 144 }, 145 }, 146 { 147 desc: "removing module", 148 initial: ` 149 -- a/go.mod -- 150 module moda.com 151 -- a/go.sum -- 152 golang.org/x/mod v0.3.0 h1:deadbeef 153 -- b/go.mod -- 154 module modb.com 155 -- b/go.sum -- 156 golang.org/x/mod v0.3.0 h1:beefdead`, 157 initialState: wsState{ 158 modules: []string{"a/go.mod", "b/go.mod"}, 159 source: fileSystemWorkspace, 160 dirs: []string{".", "a", "b"}, 161 sum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n", 162 }, 163 updates: map[string]wsChange{ 164 "gopls.mod": {`module gopls-workspace 165 166 require moda.com v0.0.0-goplsworkspace 167 replace moda.com => $SANDBOX_WORKDIR/a`, true}, 168 }, 169 wantChanged: true, 170 wantReload: true, 171 finalState: wsState{ 172 modules: []string{"a/go.mod"}, 173 source: goplsModWorkspace, 174 dirs: []string{".", "a"}, 175 sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", 176 }, 177 }, 178 { 179 desc: "adding module", 180 initial: ` 181 -- gopls.mod -- 182 require moda.com v0.0.0-goplsworkspace 183 replace moda.com => $SANDBOX_WORKDIR/a 184 -- a/go.mod -- 185 module moda.com 186 -- b/go.mod -- 187 module modb.com`, 188 initialState: wsState{ 189 modules: []string{"a/go.mod"}, 190 source: goplsModWorkspace, 191 dirs: []string{".", "a"}, 192 }, 193 updates: map[string]wsChange{ 194 "gopls.mod": {`module gopls-workspace 195 196 require moda.com v0.0.0-goplsworkspace 197 require modb.com v0.0.0-goplsworkspace 198 199 replace moda.com => $SANDBOX_WORKDIR/a 200 replace modb.com => $SANDBOX_WORKDIR/b`, true}, 201 }, 202 wantChanged: true, 203 wantReload: true, 204 finalState: wsState{ 205 modules: []string{"a/go.mod", "b/go.mod"}, 206 source: goplsModWorkspace, 207 dirs: []string{".", "a", "b"}, 208 }, 209 }, 210 { 211 desc: "deleting gopls.mod", 212 initial: ` 213 -- gopls.mod -- 214 module gopls-workspace 215 216 require moda.com v0.0.0-goplsworkspace 217 replace moda.com => $SANDBOX_WORKDIR/a 218 -- a/go.mod -- 219 module moda.com 220 -- b/go.mod -- 221 module modb.com`, 222 initialState: wsState{ 223 modules: []string{"a/go.mod"}, 224 source: goplsModWorkspace, 225 dirs: []string{".", "a"}, 226 }, 227 updates: map[string]wsChange{ 228 "gopls.mod": {"", true}, 229 }, 230 wantChanged: true, 231 wantReload: true, 232 finalState: wsState{ 233 modules: []string{"a/go.mod", "b/go.mod"}, 234 source: fileSystemWorkspace, 235 dirs: []string{".", "a", "b"}, 236 }, 237 }, 238 { 239 desc: "broken module parsing", 240 initial: ` 241 -- a/go.mod -- 242 module moda.com 243 244 require gopls.test v0.0.0-goplsworkspace 245 replace gopls.test => ../../gopls.test // (this path shouldn't matter) 246 -- b/go.mod -- 247 module modb.com`, 248 initialState: wsState{ 249 modules: []string{"a/go.mod", "b/go.mod"}, 250 source: fileSystemWorkspace, 251 dirs: []string{".", "a", "b", "../gopls.test"}, 252 }, 253 updates: map[string]wsChange{ 254 "a/go.mod": {`modul moda.com 255 256 require gopls.test v0.0.0-goplsworkspace 257 replace gopls.test => ../../gopls.test2`, false}, 258 }, 259 wantChanged: true, 260 wantReload: false, 261 finalState: wsState{ 262 modules: []string{"a/go.mod", "b/go.mod"}, 263 source: fileSystemWorkspace, 264 // finalDirs should be unchanged: we should preserve dirs in the presence 265 // of a broken modfile. 266 dirs: []string{".", "a", "b", "../gopls.test"}, 267 }, 268 }, 269 } 270 271 for _, test := range tests { 272 t.Run(test.desc, func(t *testing.T) { 273 ctx := context.Background() 274 dir, err := fake.Tempdir(fake.UnpackTxt(test.initial)) 275 if err != nil { 276 t.Fatal(err) 277 } 278 defer os.RemoveAll(dir) 279 root := span.URIFromPath(dir) 280 281 fs := &osFileSource{} 282 excludeNothing := func(string) bool { return false } 283 w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode) 284 if err != nil { 285 t.Fatal(err) 286 } 287 rel := fake.RelativeTo(dir) 288 checkState(ctx, t, fs, rel, w, test.initialState) 289 290 // Apply updates. 291 if test.updates != nil { 292 changes := make(map[span.URI]*fileChange) 293 for k, v := range test.updates { 294 content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel)) 295 uri := span.URIFromPath(rel.AbsPath(k)) 296 changes[uri], err = fs.change(ctx, uri, content, v.saved) 297 if err != nil { 298 t.Fatal(err) 299 } 300 } 301 got, gotChanged, gotReload := w.invalidate(ctx, changes, fs) 302 if gotChanged != test.wantChanged { 303 t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) 304 } 305 if gotReload != test.wantReload { 306 t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload) 307 } 308 checkState(ctx, t, fs, rel, got, test.finalState) 309 } 310 }) 311 } 312 } 313 314 func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) { 315 ctx := context.Background() 316 dir, err := fake.Tempdir(fake.UnpackTxt(files)) 317 if err != nil { 318 return nil, func() {}, err 319 } 320 cleanup := func() { 321 os.RemoveAll(dir) 322 } 323 root := span.URIFromPath(dir) 324 325 fs := &osFileSource{} 326 excludeNothing := func(string) bool { return false } 327 workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false) 328 return workspace, cleanup, err 329 } 330 331 func TestWorkspaceParseError(t *testing.T) { 332 w, cleanup, err := workspaceFromTxtar(t, ` 333 -- go.work -- 334 go 1.18 335 336 usa ./typo 337 -- typo/go.mod -- 338 module foo 339 `) 340 defer cleanup() 341 if err != nil { 342 t.Fatalf("error creating workspace: %v; want no error", err) 343 } 344 w.buildMu.Lock() 345 built, buildErr := w.built, w.buildErr 346 w.buildMu.Unlock() 347 if !built || buildErr == nil { 348 t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr) 349 } 350 var errList modfile.ErrorList 351 if !errors.As(buildErr, &errList) { 352 t.Fatalf("expected error to be an errorlist; got %v", buildErr) 353 } 354 if len(errList) != 1 { 355 t.Fatalf("expected errorList to have one element; got %v elements", len(errList)) 356 } 357 parseErr := errList[0] 358 if parseErr.Pos.Line != 3 { 359 t.Fatalf("expected error to be on line 3; got %v", parseErr.Pos.Line) 360 } 361 } 362 363 func TestWorkspaceMissingModFile(t *testing.T) { 364 w, cleanup, err := workspaceFromTxtar(t, ` 365 -- go.work -- 366 go 1.18 367 368 use ./missing 369 `) 370 defer cleanup() 371 if err != nil { 372 t.Fatalf("error creating workspace: %v; want no error", err) 373 } 374 w.buildMu.Lock() 375 built, buildErr := w.built, w.buildErr 376 w.buildMu.Unlock() 377 if !built || buildErr == nil { 378 t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr) 379 } 380 } 381 382 func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) { 383 t.Helper() 384 if got.moduleSource != want.source { 385 t.Errorf("module source = %v, want %v", got.moduleSource, want.source) 386 } 387 modules := make(map[span.URI]struct{}) 388 for k := range got.getActiveModFiles() { 389 modules[k] = struct{}{} 390 } 391 for _, modPath := range want.modules { 392 path := rel.AbsPath(modPath) 393 uri := span.URIFromPath(path) 394 if _, ok := modules[uri]; !ok { 395 t.Errorf("missing module %q", uri) 396 } 397 delete(modules, uri) 398 } 399 for remaining := range modules { 400 t.Errorf("unexpected module %q", remaining) 401 } 402 gotDirs := got.dirs(ctx, fs) 403 gotM := make(map[span.URI]bool) 404 for _, dir := range gotDirs { 405 gotM[dir] = true 406 } 407 for _, dir := range want.dirs { 408 path := rel.AbsPath(dir) 409 uri := span.URIFromPath(path) 410 if !gotM[uri] { 411 t.Errorf("missing dir %q", uri) 412 } 413 delete(gotM, uri) 414 } 415 for remaining := range gotM { 416 t.Errorf("unexpected dir %q", remaining) 417 } 418 gotSumBytes, err := got.sumFile(ctx, fs) 419 if err != nil { 420 t.Fatal(err) 421 } 422 if gotSum := string(gotSumBytes); gotSum != want.sum { 423 t.Errorf("got final sum %q, want %q", gotSum, want.sum) 424 } 425 }