Skip to content

Commit 74b5d36

Browse files
h9jianggopherbot
authored andcommitted
gopls/internal/filewatcher: add stress test for file watcher
The stress test spins up multiple go routines and each go routine perform one file system operations out of 6, i.e. create, delete, rename file or dir. If any failed, the test should reported as fatal failure. The stress test will verify: - All expected events is received within 30 seconds timeout. - Filewatcher Close() return without error. - No error reported from call back func. For golang/go#74292 Change-Id: Ifec5b6ebc736f13317648257d2f9a60f4cbf40c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/686596 Auto-Submit: Hongxiang Jiang <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Commit-Queue: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 57c8fd3 commit 74b5d36

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

gopls/internal/filewatcher/filewatcher.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,14 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
248248
// because of the issue fsnotify/fsnotify#502.
249249
func (w *Watcher) watchDir(path string) error {
250250
// Dir with broken symbolic link can not be watched.
251-
// TODO(hxjiang): is it possible the files/dirs are
252-
// created before the watch is successfully registered.
251+
// TODO(hxjiang): Address a race condition where file or directory creations
252+
// under current directory might be missed between the current directory
253+
// creation and the establishment of the file watch.
254+
//
255+
// To fix this, we should:
256+
// 1. Retrospectively check for and trigger creation events for any new
257+
// files/directories.
258+
// 2. Recursively add watches for any newly created subdirectories.
253259
return w.watcher.Add(path)
254260
}
255261

gopls/internal/filewatcher/filewatcher_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
package filewatcher_test
66

77
import (
8+
"cmp"
9+
"fmt"
810
"os"
911
"path/filepath"
1012
"runtime"
1113
"slices"
1214
"testing"
1315
"time"
1416

17+
"golang.org/x/sync/errgroup"
1518
"golang.org/x/tools/gopls/internal/filewatcher"
1619
"golang.org/x/tools/gopls/internal/protocol"
20+
"golang.org/x/tools/gopls/internal/util/moremaps"
1721
"golang.org/x/tools/txtar"
1822
)
1923

@@ -277,3 +281,121 @@ package foo
277281
})
278282
}
279283
}
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

Comments
 (0)