|
5 | 5 | package filewatcher_test
|
6 | 6 |
|
7 | 7 | import (
|
| 8 | + "cmp" |
| 9 | + "fmt" |
8 | 10 | "os"
|
9 | 11 | "path/filepath"
|
10 | 12 | "runtime"
|
11 | 13 | "slices"
|
12 | 14 | "testing"
|
13 | 15 | "time"
|
14 | 16 |
|
| 17 | + "golang.org/x/sync/errgroup" |
15 | 18 | "golang.org/x/tools/gopls/internal/filewatcher"
|
16 | 19 | "golang.org/x/tools/gopls/internal/protocol"
|
| 20 | + "golang.org/x/tools/gopls/internal/util/moremaps" |
17 | 21 | "golang.org/x/tools/txtar"
|
18 | 22 | )
|
19 | 23 |
|
@@ -277,3 +281,121 @@ package foo
|
277 | 281 | })
|
278 | 282 | }
|
279 | 283 | }
|
| 284 | + |
| 285 | +func TestStress(t *testing.T) { |
| 286 | + switch runtime.GOOS { |
| 287 | + case "darwin", "linux", "windows": |
| 288 | + default: |
| 289 | + t.Skip("unsupported OS") |
| 290 | + } |
| 291 | + |
| 292 | + const ( |
| 293 | + delay = 50 * time.Millisecond |
| 294 | + numGoroutines = 100 |
| 295 | + ) |
| 296 | + |
| 297 | + root := t.TempDir() |
| 298 | + |
| 299 | + mkdir := func(base string) func() error { |
| 300 | + return func() error { |
| 301 | + return os.Mkdir(filepath.Join(root, base), 0755) |
| 302 | + } |
| 303 | + } |
| 304 | + write := func(base string) func() error { |
| 305 | + return func() error { |
| 306 | + return os.WriteFile(filepath.Join(root, base), []byte("package main"), 0644) |
| 307 | + } |
| 308 | + } |
| 309 | + remove := func(base string) func() error { |
| 310 | + return func() error { |
| 311 | + return os.Remove(filepath.Join(root, base)) |
| 312 | + } |
| 313 | + } |
| 314 | + rename := func(old, new string) func() error { |
| 315 | + return func() error { |
| 316 | + return os.Rename(filepath.Join(root, old), filepath.Join(root, new)) |
| 317 | + } |
| 318 | + } |
| 319 | + |
| 320 | + wants := make(map[protocol.FileEvent]bool) |
| 321 | + want := func(base string, t protocol.FileChangeType) { |
| 322 | + wants[protocol.FileEvent{URI: protocol.URIFromPath(filepath.Join(root, base)), Type: t}] = true |
| 323 | + } |
| 324 | + |
| 325 | + for i := range numGoroutines { |
| 326 | + // Create files and dirs that will be deleted or renamed later. |
| 327 | + if err := cmp.Or( |
| 328 | + mkdir(fmt.Sprintf("delete-dir-%d", i))(), |
| 329 | + mkdir(fmt.Sprintf("old-dir-%d", i))(), |
| 330 | + write(fmt.Sprintf("delete-file-%d.go", i))(), |
| 331 | + write(fmt.Sprintf("old-file-%d.go", i))(), |
| 332 | + ); err != nil { |
| 333 | + t.Fatal(err) |
| 334 | + } |
| 335 | + |
| 336 | + // Add expected notification events to the "wants" set. |
| 337 | + want(fmt.Sprintf("file-%d.go", i), protocol.Created) |
| 338 | + want(fmt.Sprintf("delete-file-%d.go", i), protocol.Deleted) |
| 339 | + want(fmt.Sprintf("old-file-%d.go", i), protocol.Deleted) |
| 340 | + want(fmt.Sprintf("new-file-%d.go", i), protocol.Created) |
| 341 | + want(fmt.Sprintf("dir-%d", i), protocol.Created) |
| 342 | + want(fmt.Sprintf("delete-dir-%d", i), protocol.Deleted) |
| 343 | + want(fmt.Sprintf("old-dir-%d", i), protocol.Deleted) |
| 344 | + want(fmt.Sprintf("new-dir-%d", i), protocol.Created) |
| 345 | + } |
| 346 | + |
| 347 | + foundAll := make(chan struct{}) |
| 348 | + w, err := filewatcher.New(delay, nil, func(events []protocol.FileEvent, err error) { |
| 349 | + if err != nil { |
| 350 | + t.Errorf("error from watcher: %v", err) |
| 351 | + return |
| 352 | + } |
| 353 | + for _, e := range events { |
| 354 | + delete(wants, e) |
| 355 | + } |
| 356 | + if len(wants) == 0 { |
| 357 | + close(foundAll) |
| 358 | + } |
| 359 | + }) |
| 360 | + if err != nil { |
| 361 | + t.Fatal(err) |
| 362 | + } |
| 363 | + |
| 364 | + if err := w.WatchDir(root); err != nil { |
| 365 | + t.Fatal(err) |
| 366 | + } |
| 367 | + |
| 368 | + // Spin up multiple goroutines, each performing 6 file system operations |
| 369 | + // i.e. create, delete, rename of file or directory. For deletion and rename, |
| 370 | + // the goroutine deletes / renames files or directories created before the |
| 371 | + // watcher starts. |
| 372 | + var g errgroup.Group |
| 373 | + for id := range numGoroutines { |
| 374 | + ops := []func() error{ |
| 375 | + write(fmt.Sprintf("file-%d.go", id)), |
| 376 | + remove(fmt.Sprintf("delete-file-%d.go", id)), |
| 377 | + rename(fmt.Sprintf("old-file-%d.go", id), fmt.Sprintf("new-file-%d.go", id)), |
| 378 | + mkdir(fmt.Sprintf("dir-%d", id)), |
| 379 | + remove(fmt.Sprintf("delete-dir-%d", id)), |
| 380 | + rename(fmt.Sprintf("old-dir-%d", id), fmt.Sprintf("new-dir-%d", id)), |
| 381 | + } |
| 382 | + for _, f := range ops { |
| 383 | + g.Go(f) |
| 384 | + } |
| 385 | + } |
| 386 | + if err := g.Wait(); err != nil { |
| 387 | + t.Fatal(err) |
| 388 | + } |
| 389 | + |
| 390 | + select { |
| 391 | + case <-foundAll: |
| 392 | + case <-time.After(30 * time.Second): |
| 393 | + if len(wants) > 0 { |
| 394 | + t.Errorf("missing expected events: %#v", moremaps.KeySlice(wants)) |
| 395 | + } |
| 396 | + } |
| 397 | + |
| 398 | + if err := w.Close(); err != nil { |
| 399 | + t.Errorf("failed to close the file watcher: %v", err) |
| 400 | + } |
| 401 | +} |
0 commit comments