217 lines
6.0 KiB
Go
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()
|
|
}
|