Skip to content

Commit 8826add

Browse files
committed
Add Slopwatch & fix flaky tests
1 parent 223705c commit 8826add

28 files changed

Lines changed: 723 additions & 178 deletions

.claude/settings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"hooks": {
3+
"PostToolUse": [
4+
{
5+
"matcher": "Write|Edit|MultiEdit",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "slopwatch analyze -d $(git rev-parse --show-toplevel) --hook",
10+
"timeout": 60000
11+
}
12+
]
13+
}
14+
]
15+
}
16+
}

.config/dotnet-tools.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"slopwatch.cmd": {
6+
"version": "0.2.0",
7+
"commands": ["slopwatch"],
8+
"rollForward": false
9+
}
10+
}
11+
}

.slopwatch/baseline.json

Lines changed: 575 additions & 0 deletions
Large diffs are not rendered by default.

.slopwatch/slopwatch.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"minSeverity": "warning",
3+
"rules": {
4+
"SW001": { "enabled": true, "severity": "error" },
5+
"SW002": { "enabled": true, "severity": "error" },
6+
"SW003": { "enabled": true, "severity": "error" },
7+
"SW004": { "enabled": true, "severity": "error" },
8+
"SW005": { "enabled": true, "severity": "error" },
9+
"SW006": { "enabled": true, "severity": "error" }
10+
},
11+
"exclude": [
12+
"**/Generated/**",
13+
"**/obj/**",
14+
"**/bin/**"
15+
]
16+
}

src/TurboHTTP.AcceptanceTests/H11/CacheSpec.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public async Task Cache_should_produce_different_cache_entries_per_vary_header_v
222222
Assert.Equal(body1, body3);
223223
}
224224

225-
[Fact(Timeout = 5000)]
225+
[Fact(Timeout = 10_000)]
226226
[Trait("RFC", "RFC9111-5.2.2.2")]
227227
public async Task Cache_should_force_revalidation_with_must_revalidate()
228228
{

src/TurboHTTP.AcceptanceTests/TLS/CacheSpec.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public async Task Cache_should_produce_different_cache_entries_per_vary_header_v
222222
Assert.Equal(body1, body3);
223223
}
224224

225-
[Fact(Timeout = 5000)]
225+
[Fact(Timeout = 10_000)]
226226
[Trait("RFC", "RFC9111-5.2.2.2")]
227227
public async Task Cache_should_force_revalidation_with_must_revalidate_over_https()
228228
{

src/TurboHTTP.StreamTests/Caching/CacheBidiAsyncBodySpec.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ public async Task CacheBidiStage_should_push_response_immediately_while_body_rea
9696
var result = Assert.Single(results);
9797
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
9898

99-
// Allow the PipeTo callback to fire
100-
await Task.Delay(200, TestContext.Current.CancellationToken);
99+
// Poll until the PipeTo callback fires and cache is populated
100+
await AwaitAssertAsync(
101+
() => Assert.NotNull(store.Get(new HttpRequestMessage(HttpMethod.Get, "http://example.com/slow"))),
102+
TimeSpan.FromSeconds(2), cancellationToken: TestContext.Current.CancellationToken);
101103

102104
var entry = store.Get(new HttpRequestMessage(HttpMethod.Get, "http://example.com/slow"));
103105
Assert.NotNull(entry);
@@ -129,8 +131,10 @@ public async Task CacheBidiStage_should_store_in_cache_after_async_body_complete
129131
var result = Assert.Single(results);
130132
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
131133

132-
// Allow the PipeTo callback to fire
133-
await Task.Delay(200, TestContext.Current.CancellationToken);
134+
// Poll until the PipeTo callback fires and cache is populated
135+
await AwaitAssertAsync(
136+
() => Assert.NotNull(store.Get(new HttpRequestMessage(HttpMethod.Get, "http://example.com/pending"))),
137+
TimeSpan.FromSeconds(2), cancellationToken: TestContext.Current.CancellationToken);
134138

135139
var entry = store.Get(new HttpRequestMessage(HttpMethod.Get, "http://example.com/pending"));
136140
Assert.NotNull(entry);

src/TurboHTTP.StreamTests/Semantics/TracingActivityLeakSpec.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using TurboHTTP.Diagnostics;
55
using TurboHTTP.Streams.Stages.Features;
66
using TurboHTTP.Tests.Shared;
7+
using Activity = System.Diagnostics.Activity;
78
using ActivityListener = System.Diagnostics.ActivityListener;
89
using ActivitySamplingResult = System.Diagnostics.ActivitySamplingResult;
910
using ActivitySource = System.Diagnostics.ActivitySource;
@@ -17,10 +18,20 @@ public sealed class TracingActivityLeakSpec : StreamTestBase
1718
public async Task TracingBidiStage_should_stop_activity_when_stage_tears_down_without_response()
1819
{
1920
var stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
21+
Activity? capturedActivity = null;
2022

2123
using var listener = new ActivityListener();
2224
listener.ShouldListenTo = source => source.Name == TurboHttpInstrumentation.SourceName;
2325
listener.Sample = (ref _) => ActivitySamplingResult.AllData;
26+
// Wire ActivityStopped before AddActivityListener so the callback is always
27+
// registered before the Akka dispatch thread can call PostStop.
28+
listener.ActivityStopped = stopped =>
29+
{
30+
if (capturedActivity is { } a && ReferenceEquals(stopped, a))
31+
{
32+
stoppedTcs.TrySetResult();
33+
}
34+
};
2435
ActivitySource.AddActivityListener(listener);
2536

2637
var stage = new TracingBidiStage();
@@ -55,13 +66,7 @@ public async Task TracingBidiStage_should_stop_activity_when_stage_tears_down_wi
5566
Assert.True(forwarded.Options.TryGetValue(TurboHttpInstrumentation.RequestActivityKey, out var activity));
5667
Assert.NotNull(activity);
5768

58-
listener.ActivityStopped = stopped =>
59-
{
60-
if (ReferenceEquals(stopped, activity))
61-
{
62-
stoppedTcs.TrySetResult();
63-
}
64-
};
69+
capturedActivity = activity;
6570

6671
// Tear down the stage — complete both inlets and cancel downstream
6772
reqInSub.SendComplete();

src/TurboHTTP.StreamTests/Streams/ConnectionStageSpec.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ public async Task ConnectionStage_should_release_with_no_reuse_when_connection_r
251251
var decision = ConnectionReuseDecision.Close("Connection: close");
252252
var reuseItem = new ConnectionReuseItem(decision) { Key = TestKey };
253253
await inputQueue.OfferAsync(reuseItem);
254-
await Task.Delay(300, TestContext.Current.CancellationToken);
254+
AwaitCondition(() => tracker.Released, TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
255255

256256
Assert.False(lease.Reusable);
257257
Assert.True(tracker.Released);
@@ -366,11 +366,9 @@ public async Task ConnectionStage_should_survive_and_continue_when_data_item_arr
366366

367367
var data = MakeData(0xFF);
368368
await inputQueue.OfferAsync(data);
369-
await Task.Delay(200, TestContext.Current.CancellationToken);
370369

371370
var data2 = MakeData(0xEE);
372371
await inputQueue.OfferAsync(data2);
373-
await Task.Delay(200, TestContext.Current.CancellationToken);
374372

375373
inputQueue.Complete();
376374
var results = await outputTask.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
@@ -409,7 +407,7 @@ public async Task
409407

410408
var data = MakeData(0xBB);
411409
await inputQueue.OfferAsync(data);
412-
await Task.Delay(300, TestContext.Current.CancellationToken);
410+
AwaitCondition(() => tracker.Released, TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
413411

414412
Assert.True(tracker.Released);
415413
Assert.False(tracker.ReleasedCanReuse);

src/TurboHTTP.StreamTests/Streams/GroupByEndpointFanOutSpec.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ public async Task GroupByEndpointFanOut_should_route_to_same_slot_when_request_h
136136
await queue.OfferAsync(firstRequest)
137137
.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
138138

139-
// Give the stage time to process the push and stamp the affinity tag.
140-
await Task.Delay(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
139+
// Poll until the stage stamps the affinity tag.
140+
AwaitCondition(
141+
() => firstRequest.Options.TryGetValue(
142+
GroupByRequestEndpointStage<HttpRequestMessage>.ConnectionAffinitySlot, out _), TimeSpan.FromSeconds(2),
143+
TestContext.Current.CancellationToken);
141144

142145
// Read back the slot ID stamped by the stage.
143146
var hasTag = firstRequest.Options.TryGetValue(

0 commit comments

Comments
 (0)