Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
de1b363
initial impl
johnfav03 Mar 18, 2026
cf41ca0
Merge remote-tracking branch 'origin/main' into watch-cli-efficiency
johnfav03 Mar 24, 2026
208b2b6
extensive testing and fixes
johnfav03 Mar 24, 2026
7c564ba
removed unnecessary code and updated tests
johnfav03 Mar 24, 2026
303671b
Merge remote-tracking branch 'origin/main' into watch-cli-efficiency
johnfav03 Mar 24, 2026
3d23bd1
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 24, 2026
95cb142
addressed copilot comments
johnfav03 Mar 24, 2026
bcdaabc
forced configmodified true
johnfav03 Mar 24, 2026
fec4058
added coarse diff checking for tsconfig
johnfav03 Mar 25, 2026
5ef0db6
added wildcard includes and seperated program building logic for lsp …
johnfav03 Mar 25, 2026
1fa230f
moved wildcard logic and switched to cachedvfs
johnfav03 Mar 25, 2026
8c55f5a
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 25, 2026
bab9ed0
accepted baseline
johnfav03 Mar 25, 2026
bc5873a
added basic ast caching
johnfav03 Mar 26, 2026
c676f36
fixed panic #3015 and formatting issues
johnfav03 Mar 26, 2026
ddc2738
added further cache eviction
johnfav03 Mar 26, 2026
e803e75
Merge remote-tracking branch 'origin/main' into watch-cli-efficiency
johnfav03 Mar 26, 2026
aa81e0a
added wildcard recursive walking and testing
johnfav03 Mar 27, 2026
0c2d356
minor caching and error reporting fixes
johnfav03 Mar 27, 2026
69d599d
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 27, 2026
3b56aa4
fixed double caching
johnfav03 Mar 27, 2026
6b19e2f
walkdir and wildcard improvements
johnfav03 Mar 27, 2026
e532647
updated logging
johnfav03 Mar 27, 2026
b4cca29
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 27, 2026
7ad6575
accepted baselines
johnfav03 Mar 30, 2026
abe2aae
Merge remote-tracking branch 'origin/main' into watch-cli-efficiency
johnfav03 Mar 30, 2026
da45092
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 30, 2026
90048b7
abstracted trackingfs to seperate file
johnfav03 Mar 30, 2026
3bec264
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Mar 30, 2026
cf5891d
abstracted filewatcher to separate file
johnfav03 Mar 31, 2026
6c953d9
formatting fix
johnfav03 Mar 31, 2026
6cfdfac
restructured for concurrency, embedded compilerhost, minor fixes
johnfav03 Apr 6, 2026
a7d6ad0
Add concurrency fuzz/race tests for watcher
andrewbranch Apr 3, 2026
03a34cc
updated race tests for new structure
johnfav03 Apr 6, 2026
501de90
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Apr 6, 2026
6f793ed
fixed linting for tests
johnfav03 Apr 6, 2026
c320d26
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Apr 8, 2026
efcbeb4
created UpdateWatchState for code cleanliness
johnfav03 Apr 9, 2026
708d1bf
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Apr 9, 2026
8680e2f
fixed directory walking
johnfav03 Apr 10, 2026
b072ec3
fixed rename issue with child hashing
johnfav03 Apr 10, 2026
118ac33
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Apr 10, 2026
5ea61a1
minor fixes
johnfav03 Apr 10, 2026
709230e
changed hashing to xxh3, added file/dir prefix
johnfav03 Apr 10, 2026
a1f6cec
Merge remote-tracking branch 'upstream/main' into watch-cli-efficiency
johnfav03 Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
474 changes: 473 additions & 1 deletion internal/execute/tsctests/tscwatch_test.go

Large diffs are not rendered by default.

262 changes: 262 additions & 0 deletions internal/execute/tsctests/watcher_race_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package tsctests

import (
"fmt"
"sync"
"testing"

"github.com/microsoft/typescript-go/internal/execute"
)

// createTestWatcher sets up a minimal project with a tsconfig and
// returns a Watcher ready for concurrent testing, plus the TestSys
// for file manipulation.
func createTestWatcher(t *testing.T) (*execute.Watcher, *TestSys) {
t.Helper()
input := &tscInput{
files: FileMap{
"/home/src/workspaces/project/a.ts": `const a: number = 1;`,
"/home/src/workspaces/project/b.ts": `import { a } from "./a"; export const b = a;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
}
sys := newTestSys(input, false)
result := execute.CommandLine(sys, []string{"--watch"}, sys)
if result.Watcher == nil {
t.Fatal("expected Watcher to be non-nil in watch mode")
}
w, ok := result.Watcher.(*execute.Watcher)
if !ok {
t.Fatalf("expected *execute.Watcher, got %T", result.Watcher)
}
return w, sys
}

// TestWatcherConcurrentDoCycle calls DoCycle from multiple goroutines
// while modifying source files, exposing data races on Watcher fields
// such as configModified, program, config, and the underlying
// FileWatcher state. Run with -race to detect.
func TestWatcherConcurrentDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)

var wg sync.WaitGroup

for i := range 8 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 10 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*10+j),
)
w.DoCycle()
}
}(i)
}

wg.Wait()
}

// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle from
// multiple goroutines, some modifying files and some not, to test
// concurrent access to all Watcher and FileWatcher state.
func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)

var wg sync.WaitGroup

// DoCycle goroutines
for i := range 4 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 15 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*15+j),
)
w.DoCycle()
}
}(i)
}

// State reader goroutines
for range 8 {
wg.Go(func() {
for range 50 {
w.DoCycle()
w.DoCycle()
w.DoCycle()
w.DoCycle()
}
})
}

wg.Wait()
}

// TestWatcherConcurrentFileChangesAndDoCycle creates, modifies, and
// deletes files from multiple goroutines while DoCycle runs, testing
// races between FS mutations and watch state updates.
func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)

var wg sync.WaitGroup

// File creators
for i := range 4 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 20 {
path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j)
_ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j))
}
}(i)
}

// File deleters
wg.Go(func() {
for j := range 20 {
_ = sys.fsFromFileMap().Remove(
fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j),
)
}
})

// DoCycle callers
for range 4 {
wg.Go(func() {
for range 10 {
w.DoCycle()
}
})
}

wg.Wait()
}

// TestWatcherRapidConfigChanges modifies tsconfig.json rapidly from
// multiple goroutines while DoCycle runs, testing races on
// config-related fields (configModified, configHasErrors,
// configFilePaths, config, extendedConfigCache).
func TestWatcherRapidConfigChanges(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)

var wg sync.WaitGroup

configs := []string{
`{}`,
`{"compilerOptions": {"strict": true}}`,
`{"compilerOptions": {"target": "ES2020"}}`,
`{"compilerOptions": {"noEmit": true}}`,
}

// Config modifiers + DoCycle
for i := range 3 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 10 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/tsconfig.json",
configs[(i+j)%len(configs)],
)
w.DoCycle()
}
}(i)
}

// Concurrent source file modifications
for i := range 2 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 15 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*15+j),
)
w.DoCycle()
}
}(i)
}

// State readers
for range 4 {
wg.Go(func() {
for range 30 {
w.DoCycle()
w.DoCycle()
}
})
}

wg.Wait()
}

// TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many
// goroutines when no files have changed, testing the early-return
// path where WatchState is read and HasChanges is called.
func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) {
t.Parallel()
w, _ := createTestWatcher(t)

var wg sync.WaitGroup

for range 16 {
wg.Go(func() {
for range 50 {
w.DoCycle()
}
})
}

wg.Wait()
}

// TestWatcherAlternatingModifyAndDoCycle alternates between modifying
// a file and calling DoCycle from different goroutines, creating a
// realistic scenario where the file watcher detects changes mid-cycle.
func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)

var wg sync.WaitGroup

// Writer goroutine: continuously modifies files
wg.Go(func() {
for j := range 100 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", j),
)
}
})

// Multiple DoCycle goroutines
for range 4 {
wg.Go(func() {
for range 25 {
w.DoCycle()
}
})
}

// State reader goroutines
for range 4 {
wg.Go(func() {
for range 100 {
w.DoCycle()
}
})
}

wg.Wait()
}
Loading
Loading