vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,456 @@
package lsp
import (
"context"
"sync"
"testing"
"testing/synctest"
"time"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
type progressCall struct {
method string // "create", "begin", "report", "end"
token string
title string // begin only
msg string // begin/report only
}
type fakeProgressReporter struct {
mu sync.Mutex
calls []progressCall
ctx context.Context
}
func (f *fakeProgressReporter) done() <-chan struct{} {
return f.ctx.Done()
}
func (f *fakeProgressReporter) localize(msg *diagnostics.Message, args ...any) string {
return msg.Localize(locale.Default, args...)
}
func (f *fakeProgressReporter) createWorkDoneProgress(token string) {
f.mu.Lock()
defer f.mu.Unlock()
f.calls = append(f.calls, progressCall{method: "create", token: token})
}
func (f *fakeProgressReporter) sendProgress(token string, value lsproto.WorkDoneProgressBeginOrReportOrEnd) {
f.mu.Lock()
defer f.mu.Unlock()
switch {
case value.Begin != nil:
msg := ""
if value.Begin.Message != nil {
msg = *value.Begin.Message
}
f.calls = append(f.calls, progressCall{method: "begin", token: token, title: value.Begin.Title, msg: msg})
case value.Report != nil:
msg := ""
if value.Report.Message != nil {
msg = *value.Report.Message
}
f.calls = append(f.calls, progressCall{method: "report", token: token, msg: msg})
case value.End != nil:
f.calls = append(f.calls, progressCall{method: "end", token: token})
}
}
func (f *fakeProgressReporter) getCalls() []progressCall {
f.mu.Lock()
defer f.mu.Unlock()
return append([]progressCall(nil), f.calls...)
}
func TestProgress(t *testing.T) {
t.Parallel()
t.Run("StartFinishBeforeDelay", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 500*time.Millisecond)
p.start(diagnostics.Project_0, "myProject")
synctest.Wait()
// Finish before the delay fires — no UI should appear.
p.finish(diagnostics.Project_0, "myProject")
synctest.Wait()
// Advance time past the delay to ensure no progress is sent.
time.Sleep(600 * time.Millisecond)
synctest.Wait()
calls := reporter.getCalls()
if len(calls) != 0 {
t.Fatalf("expected no progress calls for fast operation, got %v", calls)
}
cancel()
})
})
t.Run("ShowsAfterDelay", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 500*time.Millisecond)
p.start(diagnostics.Project_0, "myProject")
synctest.Wait()
// Let the delay fire.
time.Sleep(500 * time.Millisecond)
synctest.Wait()
calls := reporter.getCalls()
if len(calls) != 2 {
t.Fatalf("expected 2 calls (create + begin), got %d: %v", len(calls), calls)
}
if calls[0].method != "create" {
t.Fatalf("expected create, got %v", calls[0])
}
if calls[1].method != "begin" {
t.Fatalf("expected begin, got %v", calls[1])
}
if calls[1].title != diagnostics.Loading.String() {
t.Fatalf("expected title %q, got %q", diagnostics.Loading.String(), calls[1].title)
}
// Finish the operation.
p.finish(diagnostics.Project_0, "myProject")
synctest.Wait()
calls = reporter.getCalls()
last := calls[len(calls)-1]
if last.method != "end" {
t.Fatalf("expected end, got %v", last)
}
cancel()
})
})
t.Run("ReportsMultipleOperations", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 100*time.Millisecond)
// Start two different operations.
p.start(diagnostics.Project_0, "projA")
p.start(diagnostics.Project_0, "projB")
synctest.Wait()
// Let the delay fire.
time.Sleep(100 * time.Millisecond)
synctest.Wait()
calls := reporter.getCalls()
// Should have: create, begin (with first message).
if len(calls) < 2 {
t.Fatalf("expected at least 2 calls, got %d: %v", len(calls), calls)
}
if calls[0].method != "create" {
t.Fatalf("expected create, got %v", calls[0])
}
if calls[1].method != "begin" {
t.Fatalf("expected begin, got %v", calls[1])
}
// Finish one — should send a report with the remaining operation.
p.finish(diagnostics.Project_0, "projA")
synctest.Wait()
calls = reporter.getCalls()
found := false
for _, c := range calls {
if c.method == "report" {
found = true
break
}
}
if !found {
t.Fatalf("expected a report after partial finish, got %v", calls)
}
// Finish the second — should send end.
p.finish(diagnostics.Project_0, "projB")
synctest.Wait()
calls = reporter.getCalls()
last := calls[len(calls)-1]
if last.method != "end" {
t.Fatalf("expected end, got %v", last)
}
cancel()
})
})
t.Run("RefCounting", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 100*time.Millisecond)
// Start the same operation twice (ref count = 2).
p.start(diagnostics.Project_0, "proj")
p.start(diagnostics.Project_0, "proj")
synctest.Wait()
time.Sleep(100 * time.Millisecond)
synctest.Wait()
// Finish once (ref count = 1) — should NOT end.
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
calls := reporter.getCalls()
for _, c := range calls {
if c.method == "end" {
t.Fatalf("unexpected end with ref count > 0: %v", calls)
}
}
// Finish again (ref count = 0) — should end.
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
calls = reporter.getCalls()
last := calls[len(calls)-1]
if last.method != "end" {
t.Fatalf("expected end when ref count reaches 0, got %v", last)
}
cancel()
})
})
t.Run("NewTokenAfterEnd", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 100*time.Millisecond)
// First cycle.
p.start(diagnostics.Project_0, "proj")
synctest.Wait()
time.Sleep(100 * time.Millisecond)
synctest.Wait()
calls := reporter.getCalls()
firstToken := calls[0].token
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
// Second cycle — should get a new token.
p.start(diagnostics.Project_0, "proj2")
synctest.Wait()
time.Sleep(100 * time.Millisecond)
synctest.Wait()
calls = reporter.getCalls()
var secondToken string
for _, c := range calls {
if c.method == "create" && c.token != firstToken {
secondToken = c.token
break
}
}
if secondToken == "" {
t.Fatalf("expected a new token for second cycle, got calls: %v", calls)
}
if firstToken == secondToken {
t.Fatalf("expected different tokens, both were %q", firstToken)
}
p.finish(diagnostics.Project_0, "proj2")
synctest.Wait()
cancel()
})
})
t.Run("StartBeforeDelayThenMoreAfterDelay", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 200*time.Millisecond)
// Start before delay.
p.start(diagnostics.Project_0, "projA")
synctest.Wait()
// Let delay fire.
time.Sleep(200 * time.Millisecond)
synctest.Wait()
calls := reporter.getCalls()
if len(calls) < 2 {
t.Fatalf("expected create + begin after delay, got %v", calls)
}
// Start another operation after delay — should send a report immediately.
p.start(diagnostics.Project_0, "projB")
synctest.Wait()
calls = reporter.getCalls()
last := calls[len(calls)-1]
if last.method != "report" {
t.Fatalf("expected report for new start after delay, got %v", last)
}
// Clean up.
p.finish(diagnostics.Project_0, "projA")
p.finish(diagnostics.Project_0, "projB")
synctest.Wait()
cancel()
})
})
t.Run("FinishWithNoActiveToken", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 100*time.Millisecond)
// Finish without any prior start — should be a no-op.
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
calls := reporter.getCalls()
if len(calls) != 0 {
t.Fatalf("expected no calls for orphan finish, got %v", calls)
}
cancel()
})
})
t.Run("ShutdownDuringStartAndFinish", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 100*time.Millisecond)
// Cancel context so the run goroutine exits.
cancel()
synctest.Wait()
// Fill the channel buffer so start/finish block on send.
for range cap(p.ch) {
p.ch <- progressEvent{message: diagnostics.Project_0, args: []any{"fill"}}
}
// These should return immediately via the done() path
// since the channel is full and the context is cancelled.
p.start(diagnostics.Project_0, "proj")
p.finish(diagnostics.Project_0, "proj")
})
})
t.Run("ShutdownWithActiveTimer", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 500*time.Millisecond)
// Start an operation so the delay timer is created.
p.start(diagnostics.Project_0, "proj")
synctest.Wait()
// Shutdown while the delay timer is still pending.
cancel()
synctest.Wait()
})
})
t.Run("ZeroDelay", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 0)
// With zero delay, progress should begin immediately.
p.start(diagnostics.Project_0, "proj")
synctest.Wait()
calls := reporter.getCalls()
if len(calls) != 2 {
t.Fatalf("expected 2 calls (create + begin), got %d: %v", len(calls), calls)
}
if calls[0].method != "create" {
t.Fatalf("expected create, got %v", calls[0])
}
if calls[1].method != "begin" {
t.Fatalf("expected begin, got %v", calls[1])
}
if calls[1].msg != "Project 'proj'" {
t.Fatalf("expected message %q, got %q", "Project 'proj'", calls[1].msg)
}
// Start+finish should still produce begin and end.
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
calls = reporter.getCalls()
last := calls[len(calls)-1]
if last.method != "end" {
t.Fatalf("expected end, got %v", last)
}
cancel()
})
})
t.Run("FinishBeforeDelayNoBegun", func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reporter := &fakeProgressReporter{ctx: ctx}
p := newProjectLoadingProgressFromReporter(reporter, 500*time.Millisecond)
// Start, then finish before delay — begun is false, so no end is sent.
p.start(diagnostics.Project_0, "proj")
synctest.Wait()
p.finish(diagnostics.Project_0, "proj")
synctest.Wait()
calls := reporter.getCalls()
for _, c := range calls {
if c.method == "end" {
t.Fatalf("unexpected end when begun=false: %v", calls)
}
}
cancel()
})
})
}