vendor tsgo
This commit is contained in:
151
tools/tsgo/internal/project/watchtimeout_test.go
Normal file
151
tools/tsgo/internal/project/watchtimeout_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package project_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/project"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestUpdateWatchTimeoutAndRollback(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !bundled.Embedded {
|
||||
t.Skip("bundled files are not embedded")
|
||||
}
|
||||
|
||||
files := map[string]any{
|
||||
"/home/projects/TS/p1/tsconfig.json": `{
|
||||
"compilerOptions": { "noLib": true, "strict": true }
|
||||
}`,
|
||||
"/home/projects/TS/p1/src/index.ts": `export const x = 1;`,
|
||||
}
|
||||
|
||||
t.Run("watch retries on next snapshot update after timeout with same watcher identity", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
init, utils := projecttestutil.GetSessionInitOptions(files, nil, &projecttestutil.TypingsInstallerOptions{})
|
||||
|
||||
// Track WatchFiles calls: record which watcher IDs were attempted
|
||||
// and which succeeded.
|
||||
var mu sync.Mutex
|
||||
var attemptedIDs []project.WatcherID
|
||||
var successfulIDs []project.WatcherID
|
||||
var firstBatchDone atomic.Bool
|
||||
utils.Client().WatchFilesFunc = func(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
|
||||
mu.Lock()
|
||||
attemptedIDs = append(attemptedIDs, id)
|
||||
mu.Unlock()
|
||||
if !firstBatchDone.Load() {
|
||||
// Block until the context times out to simulate a slow client.
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
// After the first batch, succeed immediately.
|
||||
mu.Lock()
|
||||
successfulIDs = append(successfulIDs, id)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
session := project.NewSession(init)
|
||||
defer session.Close()
|
||||
|
||||
uri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
||||
|
||||
// Step 1: Open the file. This creates the project and triggers
|
||||
// updateWatches. All WatchFiles calls block and time out because
|
||||
// the client is slow, so the registry is rolled back and the
|
||||
// watchers are marked as pending.
|
||||
session.DidOpenFile(context.Background(), uri, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
// Let the background goroutine block on WatchFiles, then advance
|
||||
// fake time past the 1s watchRequestTimeout.
|
||||
synctest.Wait()
|
||||
time.Sleep(2 * time.Second)
|
||||
synctest.Wait()
|
||||
|
||||
mu.Lock()
|
||||
firstAttemptIDs := append([]project.WatcherID(nil), attemptedIDs...)
|
||||
mu.Unlock()
|
||||
assert.Assert(t, len(firstAttemptIDs) >= 1, "expected at least one WatchFiles call during initial open, got %d", len(firstAttemptIDs))
|
||||
|
||||
// No watcher IDs should have succeeded.
|
||||
mu.Lock()
|
||||
assert.Equal(t, len(successfulIDs), 0, "expected no successful watches after timeout")
|
||||
mu.Unlock()
|
||||
|
||||
// Step 2: Allow subsequent WatchFiles calls to succeed.
|
||||
firstBatchDone.Store(true)
|
||||
|
||||
// Step 3: Make a single character change to the open file. This
|
||||
// doesn't change any watcher identities — the program files remain
|
||||
// the same, so the programFilesWatch ID is unchanged. The pending
|
||||
// tracking ensures updateWatches retries the failed registrations.
|
||||
session.DidChangeFile(context.Background(), uri, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
|
||||
{
|
||||
Partial: &lsproto.TextDocumentContentChangePartial{
|
||||
Range: lsproto.Range{
|
||||
Start: lsproto.Position{Line: 0, Character: 18},
|
||||
End: lsproto.Position{Line: 0, Character: 19},
|
||||
},
|
||||
Text: "2",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Step 4: Flush the pending change by requesting the language service.
|
||||
// This triggers getSnapshot → updateSnapshot → updateWatches.
|
||||
_, err := session.GetLanguageService(context.Background(), uri)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Let the background task run updateWatches.
|
||||
synctest.Wait()
|
||||
time.Sleep(2 * time.Second)
|
||||
synctest.Wait()
|
||||
|
||||
// Verify: WatchFiles was called again with the same watcher IDs,
|
||||
// and this time the calls succeeded.
|
||||
mu.Lock()
|
||||
retryIDs := attemptedIDs[len(firstAttemptIDs):]
|
||||
assert.Assert(t, len(retryIDs) >= 1,
|
||||
"expected WatchFiles to be retried after character change, got %d new calls (total %d, first batch %d)",
|
||||
len(retryIDs), len(attemptedIDs), len(firstAttemptIDs))
|
||||
|
||||
// Verify that the retry used the same watcher IDs as the first attempt.
|
||||
firstAttemptSet := make(map[project.WatcherID]struct{}, len(firstAttemptIDs))
|
||||
for _, id := range firstAttemptIDs {
|
||||
firstAttemptSet[id] = struct{}{}
|
||||
}
|
||||
retrySet := make(map[project.WatcherID]struct{}, len(retryIDs))
|
||||
for _, id := range retryIDs {
|
||||
retrySet[id] = struct{}{}
|
||||
}
|
||||
for id := range retrySet {
|
||||
_, ok := firstAttemptSet[id]
|
||||
assert.Assert(t, ok,
|
||||
"retry watcher ID %v was not in the first attempt; first attempt IDs=%v retry IDs=%v",
|
||||
id, firstAttemptIDs, retryIDs)
|
||||
}
|
||||
|
||||
// Verify at least one retried watcher succeeded.
|
||||
successfulRetriedCount := 0
|
||||
for _, id := range successfulIDs {
|
||||
if _, ok := retrySet[id]; ok {
|
||||
successfulRetriedCount++
|
||||
}
|
||||
}
|
||||
assert.Assert(t, successfulRetriedCount >= 1,
|
||||
"expected at least one retried watcher to succeed, got %d (successful IDs=%v, retry IDs=%v)",
|
||||
successfulRetriedCount, successfulIDs, retryIDs)
|
||||
mu.Unlock()
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user