Skip to content

Commit e64e12c

Browse files
committed
## TASK-041-003: Migrate StreamTests RFC9113 → Http2/ component folders
Migrated all 13 stream test files from `TurboHttp.StreamTests/RFC9113/` to component-based `Http2/` subfolders following the conventions introduced in Feature 040. ### Files created **Http2/Encoding/** - `Http2EncoderSpec.cs` (from `01_Http20EncoderStageTests.cs`) - `Http2EncoderFrameSerializationSpec.cs` (from `02_Http20EncoderStageFrameSerializationTests.cs`) - `Http2BatchEncodingSpec.cs` (from `03_Http20BatchEncodingTests.cs`) **Http2/Decoding/** - `Http2DecoderSpec.cs` (from `04_Http20DecoderStageTests.cs`) - `Http2DecoderFrameParsingSpec.cs` (from `05_Http20DecoderStageFrameParsingTests.cs`) **Http2/Connection/** - `Http2ConnectionSettingsSpec.cs` (from `06_Http20ConnectionStageSettingsTests.cs`) - `Http2ConnectionPingSpec.cs` (from `07_Http20ConnectionStagePingTests.cs`) - `Http2ConnectionGoAwaySpec.cs` (from `08_Http20ConnectionStageGoAwayTests.cs`) - `Http2ConnectionFlowControlSpec.cs` (from `09_Http20ConnectionStageFlowControlTests.cs`) - `Http2ConnectionBackpressureSpec.cs` (from `10_Http20ConnectionStageBackpressureTests.cs`) - `Http2ConnectionStreamAcquireSpec.cs` (from `11_Http20ConnectionStageStreamAcquireTests.cs`) - `Http2ConnectionFlowControlBatchingSpec.cs` (from `24_Http20FlowControlBatchingTests.cs`) **Http2/** - `Http2EngineEndToEndSpec.cs` (from `22_Http20EngineEndToEndTests.cs`) ### Changes applied to each file - Namespace: `TurboHttp.StreamTests.RFC9113` → `TurboHttp.StreamTests.Http2.*` - Class names: `*Tests` → `*Spec` (sealed) - Method names: converted to BDD style `Subject_should_behavior_when_condition()` - RFC traceability: `[Fact(DisplayName = "RFC9113-...")]` → `[Trait("RFC", "RFC9113-X.Y")]` - Timeouts: `[Fact(Timeout = ...)]` preserved on all async tests ### Deleted - `src/TurboHttp.StreamTests/RFC9113/` (all 13 files + empty folder) ### Verification - `dotnet test --project TurboHttp.StreamTests/TurboHttp.StreamTests.csproj` → 658 tests, all green - `dotnet test --filter-trait "RFC=RFC9113-6.5"` → 10 tests found (Settings spec) - `dotnet test --filter-trait "RFC=RFC9113"` → 7 tests found (Engine end-to-end spec)
1 parent 47c7da1 commit e64e12c

14 files changed

Lines changed: 282 additions & 432 deletions

.maggus/features/feature_041.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ moved to Http2/ subfolders so that HTTP/2 stage tests are organized by component
124124
**Parallel:** yes — can run alongside TASK-041-002
125125

126126
**Acceptance Criteria:**
127-
- [ ] All remaining files in `TurboHttp.StreamTests/RFC9113/` moved to correct `Http2/` subfolder
128-
- [ ] All remaining files in `TurboHttp.StreamTests/RFC7541/` moved to `Http2/Hpack/`
129-
- [ ] All namespaces updated to `TurboHttp.StreamTests.Http2.*`
130-
- [ ] All class names end with `Spec`
131-
- [ ] All method names follow BDD pattern
132-
- [ ] No `DisplayName` with RFC tags remain in migrated files
133-
- [ ] `[Trait("RFC", ...)]` present on each test
134-
- [ ] `dotnet test --project TurboHttp.StreamTests/TurboHttp.StreamTests.csproj` green
135-
- [ ] `dotnet test --filter "Trait~RFC9113"` finds all migrated stream tests
127+
- [x] All remaining files in `TurboHttp.StreamTests/RFC9113/` moved to correct `Http2/` subfolder
128+
- [x] All remaining files in `TurboHttp.StreamTests/RFC7541/` moved to `Http2/Hpack/`
129+
- [x] All namespaces updated to `TurboHttp.StreamTests.Http2.*`
130+
- [x] All class names end with `Spec`
131+
- [x] All method names follow BDD pattern
132+
- [x] No `DisplayName` with RFC tags remain in migrated files
133+
- [x] `[Trait("RFC", ...)]` present on each test
134+
- [x] `dotnet test --project TurboHttp.StreamTests/TurboHttp.StreamTests.csproj` green (658 tests)
135+
- [x] `dotnet test --filter-trait "RFC=RFC9113"` finds all migrated stream tests
136136

137137
---
138138

src/TurboHttp.StreamTests/RFC9113/10_Http20ConnectionStageBackpressureTests.cs renamed to src/TurboHttp.StreamTests/Http2/Connection/Http2ConnectionBackpressureSpec.cs

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,15 @@
55
using TurboHttp.Protocol.RFC9113;
66
using TurboHttp.Streams.Stages.Decoding;
77

8-
namespace TurboHttp.StreamTests.RFC9113;
8+
namespace TurboHttp.StreamTests.Http2.Connection;
99

1010
/// <summary>
1111
/// Tests backpressure behaviour in the HTTP/2 connection stage per RFC 9113.
1212
/// Verifies that the stage correctly applies flow control and does not emit frames faster than the downstream can consume.
1313
/// </summary>
14-
/// <remarks>
15-
/// Stage under test: <see cref="Http20ConnectionStage"/>.
16-
/// RFC 9113 §5.2: HTTP/2 flow control and backpressure in connection-level frame processing.
17-
/// </remarks>
18-
public sealed class Http20ConnectionStageBackpressureTests : StreamTestBase
14+
[Trait("RFC", "RFC9113-5.2")]
15+
public sealed class Http2ConnectionBackpressureSpec : StreamTestBase
1916
{
20-
/// <summary>
21-
/// Creates an Http20ConnectionStage graph with a <see cref="Source.Queue{T}"/> for InApp
22-
/// and a <see cref="TestPublisher.ManualProbe{T}"/> for InServer.
23-
/// Subscriber probes capture all three outlets for assertion.
24-
/// </summary>
2517
private (
2618
ISourceQueueWithComplete<HttpRequestMessage> RequestQueue,
2719
TestPublisher.ManualProbe<Http2Frame> ServerProbe,
@@ -57,20 +49,12 @@ public sealed class Http20ConnectionStageBackpressureTests : StreamTestBase
5749
return (requestQueue, serverProbe, serverBoundProbe, appOutProbe, signalProbe);
5850
}
5951

60-
/// <summary>
61-
/// Offers a request to the queue and asserts it is enqueued (accepted).
62-
/// </summary>
6352
private static async Task OfferAsync(ISourceQueueWithComplete<HttpRequestMessage> queue, HttpRequestMessage request)
6453
{
6554
var result = await queue.OfferAsync(request).WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
6655
Assert.IsType<QueueOfferResult.Enqueued>(result);
6756
}
6857

69-
/// <summary>
70-
/// Sends <paramref name="count"/> GET requests through the queue and verifies
71-
/// each one is forwarded to OutServer and emits a StreamAcquireItem signal.
72-
/// Returns the next available odd stream ID.
73-
/// </summary>
7458
private static async Task<int> FillStreamsAsync(
7559
ISourceQueueWithComplete<HttpRequestMessage> queue,
7660
TestSubscriber.ManualProbe<Http2Frame> serverBoundProbe,
@@ -89,8 +73,9 @@ private static async Task<int> FillStreamsAsync(
8973
return streamId;
9074
}
9175

92-
[Fact(Timeout = 10_000, DisplayName = "RFC9113-5.1.2-20CS-BP-001: Backpressure gates request inlet at max concurrent streams")]
93-
public async Task Should_Stop_Pulling_When_At_MaxConcurrentStreams_Limit()
76+
[Fact(Timeout = 10_000)]
77+
[Trait("RFC", "RFC9113-5.1.2")]
78+
public async Task Http2ConnectionBackpressure_should_stop_pulling_when_at_max_concurrent_streams_limit()
9479
{
9580
var (requestQueue, _, serverBoundProbe, appOutProbe, signalProbe) = CreateProbes(3);
9681

@@ -102,18 +87,16 @@ public async Task Should_Stop_Pulling_When_At_MaxConcurrentStreams_Limit()
10287
serverBoundSub.Request(100);
10388
signalSub.Request(100);
10489

105-
// Fill 3 streams (the limit)
106-
var nextId = await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
90+
await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
10791

108-
// Offer a 4th request — it enters the queue buffer but the stage won't pull it
10992
await OfferAsync(requestQueue, new HttpRequestMessage(HttpMethod.Get, "http://example.com/"));
11093

111-
// The 4th frame should NOT appear on OutServer because the stage is gating _inApp
11294
serverBoundProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
11395
}
11496

115-
[Fact(Timeout = 10_000, DisplayName = "RFC9113-5.1.2-20CS-BP-002: END_STREAM decrements active streams and resumes pull")]
116-
public async Task Should_Decrement_And_Resume_Pull_When_EndStream_Received()
97+
[Fact(Timeout = 10_000)]
98+
[Trait("RFC", "RFC9113-5.1.2")]
99+
public async Task Http2ConnectionBackpressure_should_decrement_and_resume_pull_when_end_stream_received()
117100
{
118101
var (requestQueue, serverProbe, serverBoundProbe, appOutProbe, signalProbe) = CreateProbes(3);
119102

@@ -127,27 +110,23 @@ public async Task Should_Decrement_And_Resume_Pull_When_EndStream_Received()
127110

128111
var srvSub = serverProbe.ExpectSubscription(TestContext.Current.CancellationToken);
129112

130-
// Fill 3 streams (the limit)
131-
var nextId = await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
113+
await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
132114

133-
// Offer a 4th request — queued but not yet pulled by stage
134115
await OfferAsync(requestQueue, new HttpRequestMessage(HttpMethod.Get, "http://example.com/"));
135116

136-
// Verify gated
137117
serverBoundProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
138118

139-
// Server sends END_STREAM on stream 1 (zero-length DataFrame).
140119
// DATA without prior HEADERS produces no HttpResponseMessage on OutResponse;
141120
// CloseStream still decrements _activeStreams and TryPullRequest resumes the gate.
142121
srvSub.SendNext(new DataFrame(streamId: 1, data: Array.Empty<byte>(), endStream: true));
143122

144-
// Pull resumes — the 4th frame now appears on OutServer
145123
serverBoundProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
146124
signalProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
147125
}
148126

149-
[Fact(Timeout = 10_000, DisplayName = "RFC9113-5.1.2-20CS-BP-003: RstStreamFrame decrements active streams and resumes pull")]
150-
public async Task Should_Decrement_And_Resume_Pull_When_RstStream_Received()
127+
[Fact(Timeout = 10_000)]
128+
[Trait("RFC", "RFC9113-5.1.2")]
129+
public async Task Http2ConnectionBackpressure_should_decrement_and_resume_pull_when_rst_stream_received()
151130
{
152131
var (requestQueue, serverProbe, serverBoundProbe, appOutProbe, signalProbe) = CreateProbes(3);
153132

@@ -161,26 +140,21 @@ public async Task Should_Decrement_And_Resume_Pull_When_RstStream_Received()
161140

162141
var srvSub = serverProbe.ExpectSubscription(TestContext.Current.CancellationToken);
163142

164-
// Fill 3 streams (the limit)
165-
var nextId = await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
143+
await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 3);
166144

167-
// Offer a 4th request — queued but gated
168145
await OfferAsync(requestQueue, new HttpRequestMessage(HttpMethod.Get, "http://example.com/"));
169146
serverBoundProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
170147

171-
// Server sends RST_STREAM on stream 3.
172-
// RST_STREAM is consumed internally; CloseStream decrements _activeStreams and resumes the gate.
173148
srvSub.SendNext(new RstStreamFrame(streamId: 3, Http2ErrorCode.Cancel));
174149

175-
// Pull resumes — the 4th frame now appears on OutServer
176150
serverBoundProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
177151
signalProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
178152
}
179153

180-
[Fact(Timeout = 10_000, DisplayName = "RFC9113-5.1.2-20CS-BP-004: SETTINGS MAX_CONCURRENT_STREAMS mid-session enforces new limit immediately")]
181-
public async Task Should_Enforce_New_ConcurrentStreams_Limit_When_Settings_Updated_MidSession()
154+
[Fact(Timeout = 10_000)]
155+
[Trait("RFC", "RFC9113-5.1.2")]
156+
public async Task Http2ConnectionBackpressure_should_enforce_new_concurrent_streams_limit_when_settings_updated_mid_session()
182157
{
183-
// Start with limit=100, open 2 streams, then SETTINGS lowers limit to 2
184158
var (requestQueue, serverProbe, serverBoundProbe, appOutProbe, signalProbe) = CreateProbes(100);
185159

186160
var appOutSub = appOutProbe.ExpectSubscription(TestContext.Current.CancellationToken);
@@ -193,11 +167,8 @@ public async Task Should_Enforce_New_ConcurrentStreams_Limit_When_Settings_Updat
193167

194168
var srvSub = serverProbe.ExpectSubscription(TestContext.Current.CancellationToken);
195169

196-
// Open 2 streams
197170
await FillStreamsAsync(requestQueue, serverBoundProbe, signalProbe, 2);
198171

199-
// Server sends SETTINGS lowering MAX_CONCURRENT_STREAMS to 2.
200-
// SETTINGS is consumed internally; OutResponse receives nothing.
201172
srvSub.SendNext(new SettingsFrame(
202173
[(SettingsParameter.MaxConcurrentStreams, 2u)]));
203174

@@ -208,7 +179,6 @@ public async Task Should_Enforce_New_ConcurrentStreams_Limit_When_Settings_Updat
208179

209180
// The stage had an outstanding pull from when limit was 100.
210181
// That in-flight pull will be satisfied by the next offered element regardless of the new limit.
211-
// Offer the 3rd request — it passes through on the pre-existing pull.
212182
await OfferAsync(requestQueue, new HttpRequestMessage(HttpMethod.Get, "http://example.com/"));
213183
serverBoundProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
214184
signalProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
@@ -218,11 +188,9 @@ public async Task Should_Enforce_New_ConcurrentStreams_Limit_When_Settings_Updat
218188
serverBoundProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
219189

220190
// Close streams 1 and 3 to drop to activeStreams=1 < limit=2 → pull resumes.
221-
// DATA/RST_STREAM are consumed internally; CloseStream manages _activeStreams.
222191
srvSub.SendNext(new DataFrame(streamId: 1, data: Array.Empty<byte>(), endStream: true));
223192
srvSub.SendNext(new RstStreamFrame(streamId: 3, Http2ErrorCode.Cancel));
224193

225-
// The 4th frame should now flow through
226194
serverBoundProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
227195
signalProbe.ExpectNext(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
228196
}

src/TurboHttp.StreamTests/RFC9113/24_Http20FlowControlBatchingTests.cs renamed to src/TurboHttp.StreamTests/Http2/Connection/Http2ConnectionFlowControlBatchingSpec.cs

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@
66
using TurboHttp.Streams;
77
using TurboHttp.Streams.Stages.Decoding;
88

9-
namespace TurboHttp.StreamTests.RFC9113;
9+
namespace TurboHttp.StreamTests.Http2.Connection;
1010

1111
/// <summary>
12-
/// Tests for HTTP/2 flow-control batching: C1 (1 MB initial window) and
13-
/// C2 (WINDOW_UPDATE accumulation with threshold flush).
12+
/// Tests HTTP/2 flow-control batching: initial window configuration and
13+
/// WINDOW_UPDATE accumulation with threshold flush per RFC 9113.
1414
/// </summary>
15-
/// <remarks>
16-
/// Stage under test: <see cref="Http20ConnectionStage"/>, <see cref="Http20Engine"/>.
17-
/// RFC 9113 §6.9: Flow control, WINDOW_UPDATE semantics and batching strategy.
18-
/// </remarks>
19-
public sealed class Http20FlowControlBatchingTests : StreamTestBase
15+
[Trait("RFC", "RFC9113-6.9")]
16+
public sealed class Http2ConnectionFlowControlBatchingSpec : StreamTestBase
2017
{
2118
// Default window is 65535 → threshold = max(16384, 65535/4) = 16384.
2219
private const int DefaultThreshold = 16384;
@@ -58,16 +55,16 @@ public sealed class Http20FlowControlBatchingTests : StreamTestBase
5855
return (downstream, serverBound);
5956
}
6057

61-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-001: Http20Engine default initial window is 1 MiB")]
62-
public void Should_HaveOneMibInitialWindow_When_DefaultConstructorUsed()
58+
[Fact(Timeout = 5_000)]
59+
public void Http2Engine_should_have_one_mib_initial_window_when_default_constructor_used()
6360
{
6461
var engine = new Http20Engine();
6562

6663
Assert.Equal(1_048_576, engine.InitialWindowSize);
6764
}
6865

69-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-002: No WINDOW_UPDATE sent when response has no DATA frames")]
70-
public async Task Should_SendNoWindowUpdate_When_ResponseIsHeadersOnly()
66+
[Fact(Timeout = 5_000)]
67+
public async Task Http2ConnectionFlowControlBatching_should_send_no_window_update_when_response_is_headers_only()
7168
{
7269
var headers = new HeadersFrame(
7370
streamId: 1,
@@ -82,8 +79,8 @@ public async Task Should_SendNoWindowUpdate_When_ResponseIsHeadersOnly()
8279
Assert.Empty(windowUpdates);
8380
}
8481

85-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-003: Stream WINDOW_UPDATE flushed on stream close below threshold")]
86-
public async Task Should_FlushStreamPending_OnStreamClose_WhenBelowThreshold()
82+
[Fact(Timeout = 5_000)]
83+
public async Task Http2ConnectionFlowControlBatching_should_flush_stream_pending_on_stream_close_when_below_threshold()
8784
{
8885
// 1024 bytes is well below the 16384 threshold → no immediate WINDOW_UPDATE.
8986
// On stream close the stream-level pending is flushed; connection-level is NOT.
@@ -99,8 +96,8 @@ public async Task Should_FlushStreamPending_OnStreamClose_WhenBelowThreshold()
9996
Assert.DoesNotContain(windowUpdates, f => f.StreamId == 0);
10097
}
10198

102-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-004: Both WINDOW_UPDATEs sent immediately when threshold crossed")]
103-
public async Task Should_SendBothWindowUpdates_When_ThresholdCrossed_InSingleFrame()
99+
[Fact(Timeout = 5_000)]
100+
public async Task Http2ConnectionFlowControlBatching_should_send_both_window_updates_when_threshold_crossed_in_single_frame()
104101
{
105102
// Exactly 16384 bytes crosses both connection and stream threshold at once.
106103
var data = new DataFrame(streamId: 1, data: new byte[DefaultThreshold], endStream: true);
@@ -114,8 +111,8 @@ public async Task Should_SendBothWindowUpdates_When_ThresholdCrossed_InSingleFra
114111
Assert.Contains(windowUpdates, f => f.StreamId == 1 && f.Increment == DefaultThreshold);
115112
}
116113

117-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-005: Pending batched across multiple DATA frames until threshold")]
118-
public async Task Should_SendSingleBatchedWindowUpdate_When_MultipleFramesAccumulateToThreshold()
114+
[Fact(Timeout = 5_000)]
115+
public async Task Http2ConnectionFlowControlBatching_should_send_single_batched_window_update_when_multiple_frames_accumulate_to_threshold()
119116
{
120117
// Two 8192-byte frames accumulate to 16384 → threshold crossed on second frame.
121118
var frame1 = new DataFrame(streamId: 1, data: new byte[8192], endStream: false);
@@ -139,8 +136,8 @@ public async Task Should_SendSingleBatchedWindowUpdate_When_MultipleFramesAccumu
139136
Assert.Equal(DefaultThreshold, streamUpdate.Increment);
140137
}
141138

142-
[Fact(Timeout = 5_000, DisplayName = "RFC9113-6.9-H2FC-006: Two streams accumulate WINDOW_UPDATE independently")]
143-
public async Task Should_BatchStreamsIndependently_When_TwoStreamsSendDataBelowThreshold()
139+
[Fact(Timeout = 5_000)]
140+
public async Task Http2ConnectionFlowControlBatching_should_batch_streams_independently_when_two_streams_send_data_below_threshold()
144141
{
145142
// Stream 1: 16384 bytes → hits threshold on its own → stream WU(1) sent.
146143
// Stream 3: 8192 bytes → below threshold → stream WU(3) flushed only at close.

0 commit comments

Comments
 (0)