Files
kjol/tools/tsgo/internal/execute/tsctests/mock_watch_backend.go
2026-07-09 16:50:43 -04:00

217 lines
6.0 KiB
Go

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()
}