package resilience import ( "context" "errors" "io" "net/http" "strings" "testing" "time" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestBreakerProbeAndLateResult(t *testing.T) { b, _ := New(Config{FailureThreshold: 1, MaxConcurrent: 2, OpenTimeout: time.Second}) now := time.Now() b.now = func() time.Time { return now } ctx := context.Background() first, _ := b.Acquire(ctx) late, _ := b.Acquire(ctx) first(false) late(true) if b.Snapshot().State != "open" { t.Fatal(b.Snapshot()) } if _, err := b.Acquire(ctx); !errors.Is(err, ErrOpen) { t.Fatal(err) } now = now.Add(time.Second) probe, err := b.Acquire(ctx) if err != nil { t.Fatal(err) } if _, err = b.Acquire(ctx); !errors.Is(err, ErrOpen) { t.Fatal(err) } probe(true) probe(false) if s := b.Snapshot(); s.State != "closed" || s.Active != 0 { t.Fatal(s) } } func TestTransportHoldsSlotAndReleasesPanic(t *testing.T) { b, _ := New(Config{MaxConcurrent: 1}) tr := Transport{Breaker: b, Base: roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("hello"))}, nil })} req, _ := http.NewRequest("GET", "http://example.test", nil) r, err := tr.RoundTrip(req) if err != nil { t.Fatal(err) } if _, err = tr.RoundTrip(req); !errors.Is(err, ErrBusy) { t.Fatal(err) } io.ReadAll(r.Body) r.Body.Close() if b.Snapshot().Active != 0 { t.Fatal(b.Snapshot()) } tr.Base = roundTripFunc(func(*http.Request) (*http.Response, error) { panic("fixture") }) func() { defer func() { if recover() == nil { t.Error("panic lost") } }() tr.RoundTrip(req) }() if b.Snapshot().Active != 0 { t.Fatal(b.Snapshot()) } }