vendor tsgo
This commit is contained in:
193
tools/tsgo/internal/fswatch/testutil_test.go
Normal file
193
tools/tsgo/internal/fswatch/testutil_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package fswatch
|
||||
|
||||
import "testing"
|
||||
|
||||
// testingT is the subset of [testing.T] (and [testing.TB]) used by every
|
||||
// test in this package. It exists so that the per-backend test bodies
|
||||
// dispatched through [runForEachWatcher] can be re-run by a fake T that
|
||||
// captures Fatal/Skip via panic+recover instead of terminating the
|
||||
// goroutine. macOS event-delivery stalls (which are not regressions but
|
||||
// environmental flakes) can then be transparently retried before
|
||||
// propagating to the real *testing.T.
|
||||
//
|
||||
// Restricted vs *testing.T:
|
||||
// - No Parallel, Run, or other subtest plumbing.
|
||||
// - No Setenv / Chdir (would race across retries).
|
||||
//
|
||||
// All helper functions in this file accept testingT rather than
|
||||
// *testing.T so they work with both the real test runner and the retry
|
||||
// wrapper.
|
||||
type testingT interface {
|
||||
Helper()
|
||||
Cleanup(fn func())
|
||||
TempDir() string
|
||||
Name() string
|
||||
|
||||
Log(args ...any)
|
||||
Logf(format string, args ...any)
|
||||
|
||||
Error(args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
Fatal(args ...any)
|
||||
Fatalf(format string, args ...any)
|
||||
|
||||
Skip(args ...any)
|
||||
Skipf(format string, args ...any)
|
||||
SkipNow()
|
||||
|
||||
Failed() bool
|
||||
}
|
||||
|
||||
// Compile-time assertion that *testing.T satisfies testingT.
|
||||
var _ testingT = (*testing.T)(nil)
|
||||
|
||||
// retryAttempts is the number of times runForEachWatcher will re-run a
|
||||
// failing per-backend test body before propagating the failure to the
|
||||
// real *testing.T. The per-event timeouts inside the body scale with
|
||||
// the attempt number (1×, 5×, 15×), so the fast-path is cheap and only
|
||||
// real environmental flakes pay the cost of longer waits.
|
||||
const retryAttempts = 3
|
||||
|
||||
// retryTimeoutScale returns the multiplier applied to per-event timeouts
|
||||
// on the given (1-based) attempt. 1× on first try, 5× on second,
|
||||
// 15× on third.
|
||||
func retryTimeoutScale(attempt int) int {
|
||||
switch attempt {
|
||||
case 1:
|
||||
return 1
|
||||
case 2:
|
||||
return 5
|
||||
default:
|
||||
return 15
|
||||
}
|
||||
}
|
||||
|
||||
// retryT is a fake [testingT] used to run a test body and decide
|
||||
// whether it passed without committing the verdict to the real
|
||||
// *testing.T on intermediate attempts.
|
||||
//
|
||||
// Most methods (Helper, Cleanup, TempDir, Name, Log, Logf) are direct
|
||||
// passthroughs to the real T so messages stream to test output as they
|
||||
// happen rather than waiting for a verdict. Error/Errorf record a
|
||||
// failure locally but also log to the real T (so the message is visible
|
||||
// even on a successful retry). Fatal/Fatalf/Skip[Now/f] additionally
|
||||
// panic with [retryBail] to unwind the goroutine; the retry driver
|
||||
// recovers and either retries or surfaces a final failure.
|
||||
type retryT struct {
|
||||
t *testing.T
|
||||
|
||||
// attempt is 1-based and increases on each retry. Per-event
|
||||
// timeouts in helpers (waitForEvent etc.) scale from this so the
|
||||
// fast-path uses a short deadline and only retries pay the cost of
|
||||
// longer waits.
|
||||
attempt int
|
||||
|
||||
failed bool
|
||||
skipped bool
|
||||
}
|
||||
|
||||
// retryBail is panicked by Fatal/Fatalf/SkipNow/Skip[f] to abort the
|
||||
// test body. The retry driver recovers it and inspects the retryT
|
||||
// state to decide whether to retry, surface a skip, or accept success.
|
||||
type retryBail struct{}
|
||||
|
||||
func newRetryT(t *testing.T, attempt int) *retryT {
|
||||
return &retryT{t: t, attempt: attempt}
|
||||
}
|
||||
|
||||
func (r *retryT) Helper() { r.t.Helper() }
|
||||
func (r *retryT) Cleanup(fn func()) { r.t.Cleanup(fn) }
|
||||
func (r *retryT) TempDir() string { return r.t.TempDir() }
|
||||
func (r *retryT) Name() string { return r.t.Name() }
|
||||
func (r *retryT) Log(args ...any) { r.t.Helper(); r.t.Log(args...) }
|
||||
func (r *retryT) Logf(format string, args ...any) { r.t.Helper(); r.t.Logf(format, args...) }
|
||||
func (r *retryT) Failed() bool { return r.failed }
|
||||
|
||||
func (r *retryT) Error(args ...any) {
|
||||
r.t.Helper()
|
||||
r.failed = true
|
||||
r.t.Log(args...)
|
||||
}
|
||||
|
||||
func (r *retryT) Errorf(format string, args ...any) {
|
||||
r.t.Helper()
|
||||
r.failed = true
|
||||
r.t.Logf(format, args...)
|
||||
}
|
||||
|
||||
func (r *retryT) Fatal(args ...any) {
|
||||
r.t.Helper()
|
||||
r.failed = true
|
||||
r.t.Log(args...)
|
||||
panic(retryBail{})
|
||||
}
|
||||
|
||||
func (r *retryT) Fatalf(format string, args ...any) {
|
||||
r.t.Helper()
|
||||
r.failed = true
|
||||
r.t.Logf(format, args...)
|
||||
panic(retryBail{})
|
||||
}
|
||||
|
||||
func (r *retryT) Skip(args ...any) {
|
||||
r.t.Helper()
|
||||
r.skipped = true
|
||||
r.t.Log(args...)
|
||||
panic(retryBail{})
|
||||
}
|
||||
|
||||
func (r *retryT) Skipf(format string, args ...any) {
|
||||
r.t.Helper()
|
||||
r.skipped = true
|
||||
r.t.Logf(format, args...)
|
||||
panic(retryBail{})
|
||||
}
|
||||
|
||||
func (r *retryT) SkipNow() {
|
||||
r.skipped = true
|
||||
panic(retryBail{})
|
||||
}
|
||||
|
||||
// runWithRetry runs body up to [retryAttempts] times. Each attempt uses
|
||||
// a fresh retryT whose attempt counter scales the per-event timeouts in
|
||||
// the test helpers. Returns on the first attempt that does not fail
|
||||
// (Skip and success both terminate the loop). On final failure the real
|
||||
// T is marked failed; intermediate failures are visible in test output
|
||||
// as Log messages (from Error/Errorf/Fatal/Fatalf streaming through)
|
||||
// followed by a "retry: ..." log noting the next attempt.
|
||||
//
|
||||
// On the fast-path (body passes first try), this is one call with
|
||||
// negligible overhead over a direct invocation.
|
||||
func runWithRetry(t *testing.T, body func(testingT)) {
|
||||
t.Helper()
|
||||
for attempt := 1; attempt <= retryAttempts; attempt++ {
|
||||
r := newRetryT(t, attempt)
|
||||
func() {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
if _, ok := rec.(retryBail); !ok {
|
||||
// Not our panic; resurface.
|
||||
panic(rec)
|
||||
}
|
||||
}
|
||||
}()
|
||||
body(r)
|
||||
}()
|
||||
|
||||
if r.skipped {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
if !r.failed {
|
||||
if attempt > 1 {
|
||||
t.Logf("retry: succeeded on attempt %d/%d", attempt, retryAttempts)
|
||||
}
|
||||
return
|
||||
}
|
||||
if attempt < retryAttempts {
|
||||
t.Logf("retry: attempt %d/%d failed, retrying with %d× timeout scale",
|
||||
attempt, retryAttempts, retryTimeoutScale(attempt+1))
|
||||
}
|
||||
}
|
||||
t.Errorf("retry: gave up after %d attempts", retryAttempts)
|
||||
}
|
||||
Reference in New Issue
Block a user