Files
kjol/tools/tsgo/internal/fswatch/fsevents_darwin_shared_test.go
2026-07-09 16:50:43 -04:00

293 lines
7.7 KiB
Go

//go:build darwin && (amd64 || arm64)
package fswatch
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"testing"
"time"
)
func newTestFSEventsWatcher(impl **fsEventsBackend) Watcher {
return &watcher{
name: "fsevents",
sequence: fsEventsGetCurrentEventID,
factory: func() watcherImpl {
*impl = newFSEventsBackend()
return *impl
},
}
}
func TestFSEventsSharedStreamAcrossWatches(t *testing.T) {
t.Parallel()
var impl *fsEventsBackend
watcherImpl := newTestFSEventsWatcher(&impl)
root := newTmpDir(t)
var subs []Watch
for i := range 5 {
dir := filepath.Join(root, fmt.Sprintf("dir%d", i))
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
sub, err := watcherImpl.WatchDirectory(dir, func([]Event, error) {})
if err != nil {
t.Fatal(err)
}
subs = append(subs, sub)
}
t.Cleanup(func() {
for _, sub := range subs {
_ = sub.Close()
}
})
impl.mu.Lock()
streamCount := len(impl.streams)
watchCount := len(impl.watches)
impl.mu.Unlock()
if streamCount != 1 {
t.Fatalf("expected one shared FSEvents stream, got %d", streamCount)
}
if watchCount != len(subs) {
t.Fatalf("expected %d logical watches, got %d", len(subs), watchCount)
}
}
func TestFSEventsSharedStreamRoutesEvents(t *testing.T) {
t.Parallel()
var impl *fsEventsBackend
watcherImpl := newTestFSEventsWatcher(&impl)
root := newTmpDir(t)
dirA := filepath.Join(root, "a")
dirB := filepath.Join(root, "b")
if err := os.MkdirAll(dirA, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(dirB, 0o755); err != nil {
t.Fatal(err)
}
time.Sleep(preSubscribeSleep(watcherImpl))
recA := newRecorder(t)
recA.watcher = watcherImpl
subA, err := watcherImpl.WatchDirectory(dirA, recA.callback)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = subA.Close() })
recB := newRecorder(t)
recB.watcher = watcherImpl
subB, err := watcherImpl.WatchDirectory(dirB, recB.callback)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = subB.Close() })
time.Sleep(settleSleep(watcherImpl))
fileA := filepath.Join(dirA, "file.ts")
if err := os.WriteFile(fileA, []byte("export {}"), 0o644); err != nil {
t.Fatal(err)
}
expectContains(t, recA, EventUpdate, fileA)
assertNoEventsForPath(t, recB.drainQuiet(500*time.Millisecond), fileA, "sibling watch saw event")
fileB := filepath.Join(dirB, "file.ts")
if err := os.WriteFile(fileB, []byte("export {}"), 0o644); err != nil {
t.Fatal(err)
}
expectContains(t, recB, EventUpdate, fileB)
assertNoEventsForPath(t, recA.drainQuiet(500*time.Millisecond), fileB, "sibling watch saw event")
}
func setupFSEventsConsolidatedParent(t *testing.T) (Watcher, string) {
t.Helper()
var impl *fsEventsBackend
watcherImpl := newTestFSEventsWatcher(&impl)
parent := filepath.Join(newTmpDir(t), "parent")
var subs []Watch
for i := range recursiveConsolidateThreshold {
dir := filepath.Join(parent, fmt.Sprintf("pkg%d", i))
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
sub, err := watcherImpl.WatchDirectory(dir, func([]Event, error) {})
if err != nil {
t.Fatal(err)
}
subs = append(subs, sub)
}
t.Cleanup(func() {
for _, sub := range subs {
_ = sub.Close()
}
})
return watcherImpl, parent
}
func TestFSEventsConsolidatedWatchValidatesLogicalRoot(t *testing.T) {
t.Parallel()
watcherImpl, parent := setupFSEventsConsolidatedParent(t)
if sub, err := watcherImpl.WatchDirectory(filepath.Join(parent, "missing"), func([]Event, error) {}); err == nil {
_ = sub.Close()
t.Fatal("expected error subscribing to missing consolidated child")
}
file := filepath.Join(parent, "file")
if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if sub, err := watcherImpl.WatchDirectory(file, func([]Event, error) {}); err == nil {
_ = sub.Close()
t.Fatal("expected error subscribing to file consolidated child")
}
}
func TestFSEventsConsolidatedWatchTerminatesLogicalRoot(t *testing.T) {
t.Parallel()
watcherImpl, parent := setupFSEventsConsolidatedParent(t)
watched := filepath.Join(parent, "watched")
if err := os.MkdirAll(watched, 0o755); err != nil {
t.Fatal(err)
}
r := newRecorder(t)
r.watcher = watcherImpl
sub, err := watcherImpl.WatchDirectory(watched, r.callback, WithRecursive())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Close() })
time.Sleep(settleSleep(watcherImpl))
if err := os.RemoveAll(watched); err != nil {
t.Fatal(err)
}
expectEventSequence(t, r, []wantEvent{{EventDelete, watched}})
deadline := time.Now().Add(r.deadline())
for time.Now().Before(deadline) {
r.mu.Lock()
n := len(r.errs)
r.mu.Unlock()
if n > 0 {
break
}
time.Sleep(20 * time.Millisecond)
}
r.mu.Lock()
errs := slices.Clone(r.errs)
r.errs = nil
r.mu.Unlock()
if !slices.ContainsFunc(errs, func(err error) bool { return errors.Is(err, ErrWatchTerminated) }) {
t.Fatalf("expected ErrWatchTerminated after watched dir delete, got errs=%v", errs)
}
}
func TestFSEventsSharedStreamFallsBackToChunks(t *testing.T) {
t.Parallel()
const count = fseventsPathsPerStream*2 + 1
watches := make([]fseventsWatchSnapshot, 0, count)
for i := range count {
watches = append(watches, fseventsWatchSnapshot{
w: &dirWatch{physicalDir: fmt.Sprintf("/watch/dir%04d", i)},
state: &fseventsState{},
})
}
var calls []int
var watchCalls []int
streams, err := startFSEventsStreams(watches, func(paths []string, streamWatches []fseventsWatchSnapshot) (*fseventsStream, error) {
calls = append(calls, len(paths))
watchCalls = append(watchCalls, len(streamWatches))
if len(calls) == 1 {
return nil, errStreamStartFailed
}
return &fseventsStream{}, nil
})
if err != nil {
t.Fatal(err)
}
if len(streams) != 3 {
t.Fatalf("expected 3 chunked streams, got %d", len(streams))
}
wantCalls := []int{count, fseventsPathsPerStream, fseventsPathsPerStream, 1}
if !slices.Equal(calls, wantCalls) {
t.Fatalf("startStream calls = %v, want %v", calls, wantCalls)
}
if !slices.Equal(watchCalls, wantCalls) {
t.Fatalf("startStream watch calls = %v, want %v", watchCalls, wantCalls)
}
}
func TestWatchesForFSEventsPaths(t *testing.T) {
t.Parallel()
watchA := &dirWatch{physicalDir: "/watch/a"}
watchB := &dirWatch{physicalDir: "/watch/b"}
watchC := &dirWatch{physicalDir: "/watch/c"}
watches := []fseventsWatchSnapshot{
{w: watchA, state: &fseventsState{}},
{w: watchB, state: &fseventsState{}},
{w: watchC, state: &fseventsState{}},
}
got := watchesForFSEventsPaths(watches, []string{"/watch/a", "/watch/c"})
gotPaths := make([]string, 0, len(got))
for _, watch := range got {
gotPaths = append(gotPaths, watch.w.physicalDir)
}
slices.Sort(gotPaths)
want := []string{"/watch/a", "/watch/c"}
if !slices.Equal(gotPaths, want) {
t.Fatalf("watchesForFSEventsPaths = %v, want %v", gotPaths, want)
}
}
func TestFSEventsOverflowMatchesWatch(t *testing.T) {
t.Parallel()
w := &dirWatch{
dir: "/logical/root",
physicalDir: "/physical/root",
}
cases := []struct {
name string
rawPath string
want bool
}{
{name: "physical root", rawPath: "/physical/root", want: true},
{name: "physical descendant", rawPath: "/physical/root/sub", want: true},
{name: "physical ancestor", rawPath: "/physical", want: true},
{name: "logical root", rawPath: "/logical/root", want: true},
{name: "logical descendant", rawPath: "/logical/root/sub", want: true},
{name: "logical ancestor", rawPath: "/logical", want: true},
{name: "unrelated", rawPath: "/other/root", want: false},
{name: "sibling prefix", rawPath: "/physical/root2", want: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
if got := fseventsOverflowMatches(w, c.rawPath); got != c.want {
t.Fatalf("fseventsOverflowMatches(%q) = %v, want %v", c.rawPath, got, c.want)
}
})
}
}