From 5c44e2b52ae80ddff4506093296b921bdcc5f4bb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:33:06 +0000 Subject: [PATCH] test: make CanRestartStream tolerant of cancellation errors on .NET Framework On .NET Framework 4.6.2, cancelling a stream read can surface as an IOException rather than OperationCanceledException. This causes an Error event to fire before the Closed event during restart, which is a race condition that makes the test flaky. Add an allowSkip predicate overload to EventSink.ExpectActions that deterministically drains matching events from the blocking queue rather than using delays or timeouts. The CanRestartStream test uses this to skip Error events that are an expected side effect of cancellation on .NET Framework. Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../EventSink.cs | 18 +++++++++++++++--- .../EventSourceStreamReadingTest.cs | 7 +++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/LaunchDarkly.EventSource.Tests/EventSink.cs b/test/LaunchDarkly.EventSource.Tests/EventSink.cs index 45940e3..f702293 100644 --- a/test/LaunchDarkly.EventSource.Tests/EventSink.cs +++ b/test/LaunchDarkly.EventSource.Tests/EventSink.cs @@ -107,13 +107,25 @@ public Action ExpectAction() return ret; } - public void ExpectActions(params Action[] expectedActions) + public void ExpectActions(params Action[] expectedActions) => + ExpectActions(null, expectedActions); + + public void ExpectActions(Func allowSkip, params Action[] expectedActions) { int i = 0; foreach (var a in expectedActions) { - Assert.True(_actions.TryTake(out var actual, WaitForActionTimeout), - "timed out waiting for action " + i + " (" + a + ")"); + Action actual; + while (true) + { + Assert.True(_actions.TryTake(out actual, WaitForActionTimeout), + "timed out waiting for action " + i + " (" + a + ")"); + if (allowSkip != null && allowSkip(actual)) + { + continue; + } + break; + } // The MessageEvent.Equals method takes Origin into account, which is inconvenient for // our tests because the origin will vary for each embedded test server. So, ignore it. diff --git a/test/LaunchDarkly.EventSource.Tests/EventSourceStreamReadingTest.cs b/test/LaunchDarkly.EventSource.Tests/EventSourceStreamReadingTest.cs index 9c9c4d7..1a23578 100644 --- a/test/LaunchDarkly.EventSource.Tests/EventSourceStreamReadingTest.cs +++ b/test/LaunchDarkly.EventSource.Tests/EventSourceStreamReadingTest.cs @@ -204,6 +204,12 @@ public void CanRestartStream(bool resetBackoff) var backoffs = new List(); + // On .NET Framework, cancelling a stream read can surface as an IOException + // rather than OperationCanceledException, which causes an Error event to fire + // before the Closed event. We skip these transient error events since they are + // an expected side effect of cancellation on that platform. + Func skipCancellationErrors = a => a.Kind == "Error"; + using (var server = HttpServer.Start(handler)) { using (var es = MakeEventSource(server.Uri, config => config.InitialRetryDelay(initialDelay))) @@ -225,6 +231,7 @@ public void CanRestartStream(bool resetBackoff) es.Restart(resetBackoff); sink.ExpectActions( + skipCancellationErrors, EventSink.ClosedAction(), EventSink.OpenedAction(), EventSink.MessageReceivedAction(anEvent)