Skip to content

Commit 6fd3144

Browse files
ANcpLuaclaude
andcommitted
refactor(tests): use CreateLinkedTokenSource for timeout patterns
Replace manual DateTime-based timeout loops with the built-in CancellationTokenSource.CreateLinkedTokenSource pattern for cleaner, more efficient timeout handling in SSE tests. - Add WaitForClientsAsync<T> helper to SseTestHelpers with: - Configurable timeout (default 10s) and polling interval (50ms) - Optional stabilization delay (100ms) - Proper OperationCanceledException → TimeoutException translation - Update OcrEventStreamIntegrationTests to use shared helper - Update GenAIEventStreamIntegrationTests to use shared helper - Remove duplicate private helper from SseExtensionsNet10Tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b07060d commit 6fd3144

File tree

4 files changed

+61
-43
lines changed

4 files changed

+61
-43
lines changed

SWEN3.Paperless.RabbitMq.Tests/Helpers/SseTestHelpers.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,60 @@ namespace SWEN3.Paperless.RabbitMq.Tests.Helpers;
55
/// </summary>
66
internal static class SseTestHelpers
77
{
8+
/// <summary>
9+
/// Default polling interval for client connection checks.
10+
/// </summary>
11+
private const int PollingIntervalMs = 50;
12+
13+
/// <summary>
14+
/// Default timeout for waiting for clients to connect.
15+
/// </summary>
16+
private static readonly TimeSpan DefaultClientTimeout = TimeSpan.FromSeconds(10);
17+
18+
/// <summary>
19+
/// Default stabilization delay after client connects.
20+
/// </summary>
21+
private static readonly TimeSpan ConnectionStabilizationDelay = TimeSpan.FromMilliseconds(100);
22+
23+
/// <summary>
24+
/// Waits for expected number of clients to connect to the SSE stream using linked cancellation tokens.
25+
/// </summary>
26+
/// <typeparam name="T">The event type of the stream</typeparam>
27+
/// <param name="stream">The SSE stream to monitor</param>
28+
/// <param name="expectedClients">Number of clients to wait for (default: 1)</param>
29+
/// <param name="timeout">Timeout duration (default: 10 seconds)</param>
30+
/// <param name="stabilize">Whether to add stabilization delay after connection (default: true)</param>
31+
/// <param name="cancellationToken">Parent cancellation token</param>
32+
/// <exception cref="TimeoutException">Thrown when clients don't connect within timeout</exception>
33+
public static async Task WaitForClientsAsync<T>(
34+
ISseStream<T> stream,
35+
int expectedClients = 1,
36+
TimeSpan? timeout = null,
37+
bool stabilize = true,
38+
CancellationToken cancellationToken = default) where T : class
39+
{
40+
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
41+
timeoutCts.CancelAfter(timeout ?? DefaultClientTimeout);
42+
43+
try
44+
{
45+
while (stream.ClientCount < expectedClients)
46+
{
47+
await Task.Delay(PollingIntervalMs, timeoutCts.Token);
48+
}
49+
50+
if (stabilize)
51+
{
52+
await Task.Delay(ConnectionStabilizationDelay, timeoutCts.Token);
53+
}
54+
}
55+
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
56+
{
57+
throw new TimeoutException(
58+
$"Expected {expectedClients} client(s) but only {stream.ClientCount} connected within timeout");
59+
}
60+
}
61+
862
/// <summary>
963
/// Creates a test server with SSE stream configured, using modern IHost and TestServer.
1064
/// </summary>

SWEN3.Paperless.RabbitMq.Tests/Integration/GenAIEventStreamIntegrationTests.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public async Task MapGenAIEventStream_ShouldEmitCorrectEventType(string status,
3333
}
3434
}, ct);
3535

36-
while (sseStream.ClientCount == 0)
37-
await Task.Delay(50, ct);
36+
await SseTestHelpers.WaitForClientsAsync(sseStream, stabilize: false, cancellationToken: ct);
3837

3938
var genAiEvent = status == "Completed"
4039
? new GenAIEvent(Guid.NewGuid(), "Test summary", DateTimeOffset.UtcNow)
@@ -60,17 +59,7 @@ public async Task MapGenAIEventStream_WithMultipleEvents_ShouldStreamInOrder()
6059

6160
var readTask = Task.Run(() => ReadEventsAsync(client, 3, ct), ct);
6261

63-
// Wait for client to connect with timeout
64-
var waitStart = DateTime.UtcNow;
65-
while (sseStream.ClientCount == 0)
66-
{
67-
if (DateTime.UtcNow - waitStart > TimeSpan.FromSeconds(10))
68-
throw new TimeoutException("Client did not connect within timeout");
69-
await Task.Delay(50, ct);
70-
}
71-
72-
// Give the HTTP connection a moment to stabilize
73-
await Task.Delay(100, ct);
62+
await SseTestHelpers.WaitForClientsAsync(sseStream, cancellationToken: ct);
7463

7564
sseStream.Publish(new GenAIEvent(Guid.NewGuid(), "Summary 1", DateTimeOffset.UtcNow));
7665
await Task.Delay(50, ct);

SWEN3.Paperless.RabbitMq.Tests/Integration/OcrEventStreamIntegrationTests.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,7 @@ public async Task MapOcrEventStream_ShouldEmitCorrectEventType(string status, st
3434
}
3535
}, ct);
3636

37-
// Wait for client to connect with timeout
38-
var waitStart = DateTime.UtcNow;
39-
while (sseStream.ClientCount == 0)
40-
{
41-
if (DateTime.UtcNow - waitStart > TimeSpan.FromSeconds(10))
42-
throw new TimeoutException("Client did not connect within timeout");
43-
await Task.Delay(50, ct);
44-
}
45-
46-
// Give the HTTP connection a moment to stabilize
47-
await Task.Delay(100, ct);
37+
await SseTestHelpers.WaitForClientsAsync(sseStream, cancellationToken: ct);
4838

4939
var ocrEvent = new OcrEvent(Guid.NewGuid(), status, status is "Completed" ? "Text" : null,
5040
DateTimeOffset.UtcNow);

SWEN3.Paperless.RabbitMq.Tests/Unit/SseExtensionsNet10Tests.cs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,6 @@ namespace SWEN3.Paperless.RabbitMq.Tests.Unit;
44
public static class SseExtensionsNet10Tests
55
{
66
#if NET10_0_OR_GREATER
7-
private static async Task WaitForClientsAsync(ISseStream<Messages.SseTestEvent> stream, int expected, CancellationToken ct)
8-
{
9-
var start = DateTime.UtcNow;
10-
while (stream.ClientCount < expected)
11-
{
12-
if (DateTime.UtcNow - start > TimeSpan.FromSeconds(5))
13-
{
14-
throw new TimeoutException("Client did not connect within timeout");
15-
}
16-
17-
await Task.Delay(50, ct);
18-
}
19-
}
20-
217
[Fact]
228
public static async Task MapSse_Net10_ShouldStreamEventsUsingNativeApi()
239
{
@@ -39,7 +25,7 @@ public static async Task MapSse_Net10_ShouldStreamEventsUsingNativeApi()
3925
// Act
4026
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, cts.Token);
4127

42-
await WaitForClientsAsync(sseStream, 1, cts.Token);
28+
await SseTestHelpers.WaitForClientsAsync(sseStream, cancellationToken: cts.Token);
4329

4430
// Publish once
4531
sseStream.Publish(new Messages.SseTestEvent { Id = 1, Message = "Hello" });
@@ -88,7 +74,7 @@ public static async Task MapSse_Net10_ShouldStreamMultipleEvents()
8874
new Messages.SseTestEvent { Id = 3, Message = "Third" }
8975
};
9076

91-
await WaitForClientsAsync(sseStream, 1, cts.Token);
77+
await SseTestHelpers.WaitForClientsAsync(sseStream, cancellationToken: cts.Token);
9278

9379
// Publish events once
9480
foreach (var evt in events)
@@ -202,7 +188,7 @@ public static async Task MapSse_Net10_RequestAborted_CompletesStream()
202188

203189
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, cts.Token);
204190

205-
await WaitForClientsAsync(sseStream, 1, cts.Token);
191+
await SseTestHelpers.WaitForClientsAsync(sseStream, cancellationToken: cts.Token);
206192

207193
sseStream.Publish(new Messages.SseTestEvent { Id = 2, Message = "Abort" });
208194

@@ -249,8 +235,7 @@ public static async Task MapSse_Net10_CompletesNaturallyWhenStreamEnds()
249235
// Act - Connect without cancellation token to avoid cancellation-based termination
250236
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
251237

252-
// Wait for connection to establish
253-
await Task.Delay(100, cts.Token);
238+
await SseTestHelpers.WaitForClientsAsync(fakeStream, cancellationToken: cts.Token);
254239

255240
// Publish one event
256241
fakeStream.Publish(new Messages.SseTestEvent { Id = 99, Message = "Done" });

0 commit comments

Comments
 (0)