package tsctests import ( "fmt" "io" "path" "sort" "strings" "sync" "github.com/microsoft/typescript-go/internal/execute/watchmanager" "github.com/microsoft/typescript-go/internal/fswatch" "github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil" ) // MockWatchBackend implements watchmanager.WatchBackend for testing. It // records all WatchDirectory calls so tests can verify that // the correct watches are registered. Events can be delivered through // SendEvents, which routes them only through watches whose paths // match, enforcing that tests fail if the wrong watches are set up. type MockWatchBackend struct { mu sync.Mutex Dirs map[string]*MockWatch DirectoryExists func(string) bool // if set, WatchDirectory fails for non-existent dirs } var _ watchmanager.WatchBackend = (*MockWatchBackend)(nil) // NewMockWatchBackend creates a ready-to-use mock backend. func NewMockWatchBackend() *MockWatchBackend { return &MockWatchBackend{ Dirs: make(map[string]*MockWatch), } } // HasWatches reports whether any watches have been registered. func (m *MockWatchBackend) HasWatches() bool { m.mu.Lock() defer m.mu.Unlock() return len(m.Dirs) > 0 } // MockWatch records a single registered watch. type MockWatch struct { Path string Callback fswatch.WatchCallback Recursive bool Ignore func(string) bool Closed bool } func (w *MockWatch) Close() error { w.Closed = true return nil } func (m *MockWatchBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) { closers, err := m.WatchDirectories([]watchmanager.WatchDirectoryRequest{{ Dir: dir, Callback: fn, Recursive: recursive, Ignore: ignore, }}) if err != nil { return nil, err } return closers[0], nil } func (m *MockWatchBackend) WatchDirectories(requests []watchmanager.WatchDirectoryRequest) ([]io.Closer, error) { m.mu.Lock() defer m.mu.Unlock() for _, request := range requests { if m.DirectoryExists != nil && !m.DirectoryExists(request.Dir) { return nil, fmt.Errorf("directory does not exist: %s", request.Dir) } } closers := make([]io.Closer, len(requests)) for i, request := range requests { w := &MockWatch{Path: request.Dir, Callback: request.Callback, Recursive: request.Recursive, Ignore: request.Ignore} m.Dirs[request.Dir] = w closers[i] = w } return closers, nil } // SendEvents routes events through the registered watch callbacks // that match each event's path. Directory watches match if the event // path is a child (or recursive descendant) of the watched directory. // Events that match no watch are silently dropped — this is by design // so that tests fail when the production code doesn't register the // needed watches. func (m *MockWatchBackend) SendEvents(events []fswatch.Event) { // Snapshot callbacks under the lock, then invoke outside the lock // to avoid deadlock if the callback re-enters the mock. m.mu.Lock() type target struct { cb fswatch.WatchCallback events []fswatch.Event } targets := make(map[*MockWatch]*target) for _, e := range events { // Check directory watches. for _, w := range m.Dirs { if w.Closed { continue } if w.Ignore != nil && w.Ignore(e.Path) { continue } if !pathIsUnder(e.Path, w.Path, w.Recursive) { continue } if t, ok := targets[w]; ok { t.events = append(t.events, e) } else { targets[w] = &target{cb: w.Callback, events: []fswatch.Event{e}} } } } m.mu.Unlock() for _, t := range targets { t.cb(t.events, nil) } } // SendChangedPaths converts a list of file changes into fswatch // events with appropriate event kinds and routes them through // registered watches via SendEvents. For new/modified files, it also // emits update events for their parent directories, simulating how // real filesystem watchers report directory events. func (m *MockWatchBackend) SendChangedPaths(changes []fsbaselineutil.FileChange) { events := make([]fswatch.Event, 0, len(changes)*2) seenDirs := make(map[string]struct{}) for _, c := range changes { kind := fswatch.EventUpdate if c.Deleted { kind = fswatch.EventDelete } events = append(events, fswatch.Event{Kind: kind, Path: c.Path}) // Emit update events for parent directories of changed files. // Real filesystem watchers deliver events to non-recursive watches // when a child directory is created, which the mock must replicate. dir := path.Dir(c.Path) for dir != "" && dir != "/" && dir != "." { if _, seen := seenDirs[dir]; seen { break } seenDirs[dir] = struct{}{} events = append(events, fswatch.Event{Kind: fswatch.EventUpdate, Path: dir}) parent := path.Dir(dir) if parent == dir { break } dir = parent } } m.SendEvents(events) } // pathIsUnder reports whether eventPath is inside dir. If recursive is // false, only direct children match. func pathIsUnder(eventPath, dir string, recursive bool) bool { if !strings.HasPrefix(eventPath, dir) { return false } rest := eventPath[len(dir):] if len(rest) == 0 { return false // exact match = the dir itself, not a child } if rest[0] != '/' { return false // e.g. dir="/foo", path="/foobar" } if !recursive { // Direct child only: no further '/' after the separator. return !strings.Contains(rest[1:], "/") } return true } // WatchState returns a deterministic, human-readable summary of all // active watches. This is intended to be included in test baselines // so that watch registration correctness is verified via snapshot diffs. func (m *MockWatchBackend) WatchState() string { m.mu.Lock() defer m.mu.Unlock() var b strings.Builder b.WriteString("Watch Registrations::\n") // Directory watches, sorted by path. var dirs []string for dir, w := range m.Dirs { if !w.Closed { dirs = append(dirs, dir) } } sort.Strings(dirs) b.WriteString("Directory watches::\n") if len(dirs) == 0 { b.WriteString(" (none)\n") } for _, d := range dirs { w := m.Dirs[d] if w.Recursive { fmt.Fprintf(&b, " %s (recursive)\n", d) } else { fmt.Fprintf(&b, " %s\n", d) } } return b.String() }