Skip to content

Commit 5352f7f

Browse files
ANcpLuaclaude
andcommitted
Clean up coverage artifacts and fix CI workflow
- Remove stale coverage files from git tracking - Add coverage directories to .gitignore to prevent future tracking - Update CI workflow to filter zero-coverage reports before merge - Add NET10 cancellation test for iterator completion coverage - Add comprehensive SSE fallback tests This fixes the coverage reporting by ensuring only non-zero coverage files are merged, preventing stale reports from skewing results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7f1fb4c commit 5352f7f

File tree

5 files changed

+428
-1242
lines changed

5 files changed

+428
-1242
lines changed

.github/workflows/tests.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,22 @@ jobs:
5959
- name: Merge coverage reports
6060
run: |
6161
dotnet tool install -g dotnet-reportgenerator-globaltool
62-
reportgenerator \
63-
-reports:"./coverage-net9/**/coverage.cobertura.xml;./coverage-net10/**/coverage.cobertura.xml" \
64-
-targetdir:./coverage-merged \
65-
-reporttypes:Cobertura
62+
export PATH="$PATH:$HOME/.dotnet/tools"
63+
# Collect coverage files with lines-covered > 0 and join with semicolons
64+
REPORTS=$(python3 - <<'PY'
65+
import xml.etree.ElementTree as ET, pathlib, sys
66+
files = sorted(pathlib.Path(".").glob("coverage-net*/**/coverage.cobertura.xml"))
67+
keep = []
68+
for f in files:
69+
root = ET.parse(f).getroot()
70+
if int(root.get("lines-covered", "0")) > 0:
71+
keep.append(str(f))
72+
if not keep:
73+
sys.exit("No non-empty coverage files found")
74+
print(";".join(keep))
75+
PY
76+
)
77+
reportgenerator -reports:"$REPORTS" -targetdir:./coverage-merged -reporttypes:Cobertura
6678
6779
- name: Upload coverage to Codecov
6880
uses: codecov/codecov-action@v5

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ CodeCoverage/
5858
[Tt]est[Rr]esult*/
5959
[Bb]uild[Ll]og.*
6060

61+
# Test coverage
62+
coverage-*/
63+
TestResults/
64+
coverage-local/
65+
coverage-merged/
66+
coverage-net*/
67+
6168
# NUnit
6269
*.VisualState.xml
6370
TestResult.xml

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

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace SWEN3.Paperless.RabbitMq.Tests.Unit;
22

3+
[SuppressMessage("Design", "MA0051:Method is too long")]
34
public class SseExtensionsFallbackTests
45
{
56
#if !NET10_0_OR_GREATER
@@ -73,5 +74,172 @@ public async Task MapSse_Fallback_ShouldWriteCorrectSseFormat()
7374
await publisherTask;
7475
}
7576
}
77+
78+
[Fact]
79+
public async Task MapSse_Fallback_ShouldSetCorrectHeaders()
80+
{
81+
// Arrange
82+
var hostBuilder = new WebHostBuilder()
83+
.ConfigureServices(services =>
84+
{
85+
services.AddRouting();
86+
services.AddSseStream<Messages.SseTestEvent>();
87+
})
88+
.Configure(app =>
89+
{
90+
app.UseRouting();
91+
app.UseEndpoints(endpoints =>
92+
{
93+
endpoints.MapSse<Messages.SseTestEvent>("/sse",
94+
e => new { id = e.Id, msg = e.Message },
95+
_ => "test-event");
96+
});
97+
});
98+
99+
using var server = new TestServer(hostBuilder);
100+
var client = server.CreateClient();
101+
client.Timeout = Timeout.InfiniteTimeSpan;
102+
var sseStream = server.Host.Services.GetRequiredService<ISseStream<Messages.SseTestEvent>>();
103+
104+
// Act
105+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
106+
cts.CancelAfter(TimeSpan.FromSeconds(30));
107+
108+
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, cts.Token);
109+
110+
// Publish events so the connection doesn't hang
111+
var publisherTask = Task.Run(async () =>
112+
{
113+
try
114+
{
115+
while (!cts.Token.IsCancellationRequested)
116+
{
117+
await Task.Delay(50, cts.Token);
118+
sseStream.Publish(new Messages.SseTestEvent { Id = 1, Message = "Test" });
119+
}
120+
}
121+
catch (OperationCanceledException)
122+
{
123+
// Expected
124+
}
125+
}, CancellationToken.None);
126+
127+
try
128+
{
129+
var response = await responseTask;
130+
131+
// Assert
132+
response.EnsureSuccessStatusCode();
133+
response.Headers.CacheControl?.NoCache.Should().BeTrue();
134+
response.Headers.Connection.Should().Contain("keep-alive");
135+
response.Content.Headers.ContentType?.MediaType.Should().Be("text/event-stream");
136+
}
137+
finally
138+
{
139+
await cts.CancelAsync();
140+
await publisherTask;
141+
}
142+
}
143+
144+
[Fact]
145+
public async Task MapSse_Fallback_ShouldStreamMultipleEvents()
146+
{
147+
// Arrange
148+
var hostBuilder = new WebHostBuilder()
149+
.ConfigureServices(services =>
150+
{
151+
services.AddRouting();
152+
services.AddSseStream<Messages.SseTestEvent>();
153+
})
154+
.Configure(app =>
155+
{
156+
app.UseRouting();
157+
app.UseEndpoints(endpoints =>
158+
{
159+
endpoints.MapSse<Messages.SseTestEvent>("/sse",
160+
e => new { id = e.Id, msg = e.Message },
161+
_ => "test-event");
162+
});
163+
});
164+
165+
using var server = new TestServer(hostBuilder);
166+
var client = server.CreateClient();
167+
client.Timeout = Timeout.InfiniteTimeSpan;
168+
var sseStream = server.Host.Services.GetRequiredService<ISseStream<Messages.SseTestEvent>>();
169+
170+
// Act
171+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
172+
cts.CancelAfter(TimeSpan.FromSeconds(30));
173+
174+
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, cts.Token);
175+
176+
var events = new[]
177+
{
178+
new Messages.SseTestEvent { Id = 1, Message = "First" },
179+
new Messages.SseTestEvent { Id = 2, Message = "Second" },
180+
new Messages.SseTestEvent { Id = 3, Message = "Third" }
181+
};
182+
183+
// Publish repeatedly until subscriber connects and receives events
184+
var publisherTask = Task.Run(async () =>
185+
{
186+
try
187+
{
188+
while (!cts.Token.IsCancellationRequested)
189+
{
190+
await Task.Delay(50, cts.Token);
191+
foreach (var evt in events)
192+
{
193+
sseStream.Publish(evt);
194+
}
195+
}
196+
}
197+
catch (OperationCanceledException)
198+
{
199+
// Expected
200+
}
201+
}, CancellationToken.None);
202+
203+
try
204+
{
205+
using var response = await responseTask;
206+
response.EnsureSuccessStatusCode();
207+
await using var stream = await response.Content.ReadAsStreamAsync(cts.Token);
208+
using var reader = new StreamReader(stream, Encoding.UTF8);
209+
210+
// Read first event
211+
var event1Line1 = await reader.ReadLineAsync(cts.Token);
212+
var data1Line1 = await reader.ReadLineAsync(cts.Token);
213+
var blank1 = await reader.ReadLineAsync(cts.Token);
214+
215+
// Read second event
216+
var event2Line1 = await reader.ReadLineAsync(cts.Token);
217+
var data2Line1 = await reader.ReadLineAsync(cts.Token);
218+
var blank2 = await reader.ReadLineAsync(cts.Token);
219+
220+
// Read third event
221+
var event3Line1 = await reader.ReadLineAsync(cts.Token);
222+
var data3Line1 = await reader.ReadLineAsync(cts.Token);
223+
var blank3 = await reader.ReadLineAsync(cts.Token);
224+
225+
// Assert
226+
event1Line1.Should().Be("event: test-event");
227+
data1Line1.Should().Be("data: {\"id\":1,\"msg\":\"First\"}");
228+
blank1.Should().BeEmpty();
229+
230+
event2Line1.Should().Be("event: test-event");
231+
data2Line1.Should().Be("data: {\"id\":2,\"msg\":\"Second\"}");
232+
blank2.Should().BeEmpty();
233+
234+
event3Line1.Should().Be("event: test-event");
235+
data3Line1.Should().Be("data: {\"id\":3,\"msg\":\"Third\"}");
236+
blank3.Should().BeEmpty();
237+
}
238+
finally
239+
{
240+
await cts.CancelAsync();
241+
await publisherTask;
242+
}
243+
}
76244
#endif
77245
}

0 commit comments

Comments
 (0)