fix: stabilize flaky tests across analyzer, OTel, and engine suites#5609
fix: stabilize flaky tests across analyzer, OTel, and engine suites#5609
Conversation
- Normalize line endings on all analyzer test sources (including stubs) to prevent cross-platform mismatches - Replace racy WhenAny timeout with explicit HasRequest check in OTLP receiver no-forward test - Serialize HookExecutorHookTests_ClassLevel via NotInParallel to avoid hook-count races - Use ConcurrentBag and reference-equality loop for KeyedDataSource shared-instance assertion
There was a problem hiding this comment.
Code Review
Good set of targeted flakiness fixes. The changes are generally clean and address real root causes. A few observations:
OtlpReceiverForwardingTests — redundant delay
After , the receiver has already finished processing the request. If WhenIdle() provides that guarantee, the forwarding decision has already been made — no forwarding will happen later. The extra await Task.Delay(500) is then dead time:
await client.PostAsync($"http://127.0.0.1:{receiver.Port}/v1/traces", content);
await receiver.WhenIdle();
// If WhenIdle() guarantees processing is complete, this delay is redundant:
await Task.Delay(500);
await Assert.That(upstream.HasRequest("/v1/traces")).IsFalse();If WhenIdle() does guarantee completion, the delay can be dropped entirely for a faster, more deterministic test. If it doesn't — i.e., forwarding can race beyond the idle point — then that's a bug in WhenIdle()'s contract that should be fixed there rather than papered over with a sleep. A comment explaining why the delay is still needed would at minimum remove ambiguity.
KeyedDataSourceTests — weakened count assertion
// Before (precise):
await Assert.That(AlphaInstances).Count().IsEqualTo(2);
// After (weaker):
await Assert.That(snapshot.Length).IsGreaterThanOrEqualTo(2);The >= 2 relaxation means a future regression (e.g., 3 or 4 instances created when only 2 are expected) would go undetected. Using ConcurrentBag was the right move for thread-safety, but the count check doesn't need to be loosened — ConcurrentBag.Count is safe to read. Consider keeping IsEqualTo(2) (or the exact expected count) to preserve regression-catching precision.
The rest looks solid
- Line ending normalization: Making
NormalizeLineEndingsinternal staticand calling it at the call site inVerifyCodeFixAsyncis the right approach — normalizing early prevents subtle platform-specific comparison failures. HasRequestmethod: Correctly reuses_stateLockand mirrors the pattern already inWaitForRequestAsync. Could use_requests.Any(r => r.Path == path)inside the lock for conciseness, but the explicit loop is equally correct.[NotInParallel]onHookExecutorHookTests_ClassLevel: Proper fix for shared mutable static state. Usingnameoffor the constraint key is good practice.
There was a problem hiding this comment.
Code Review
Good set of targeted flakiness fixes. The changes are generally clean and address real root causes. A few observations:
OtlpReceiverForwardingTests — redundant delay
After await receiver.WhenIdle(), the receiver has already finished processing the request. If WhenIdle() provides that guarantee, the forwarding decision has already been made — no forwarding will happen later. The extra await Task.Delay(500) is then dead time:
await client.PostAsync($"http://127.0.0.1:{receiver.Port}/v1/traces", content);
await receiver.WhenIdle();
// If WhenIdle() guarantees processing is complete, this delay is redundant:
await Task.Delay(500);
await Assert.That(upstream.HasRequest("/v1/traces")).IsFalse();If WhenIdle() does guarantee completion, the delay can be dropped entirely for a faster, more deterministic test. If it doesn't — i.e., forwarding can race beyond the idle point — then that's a bug in WhenIdle()'s contract that should be fixed there rather than papered over with a sleep. A comment explaining why the delay is still needed would at minimum remove ambiguity.
KeyedDataSourceTests — weakened count assertion
// Before (precise):
await Assert.That(AlphaInstances).Count().IsEqualTo(2);
// After (weaker):
await Assert.That(snapshot.Length).IsGreaterThanOrEqualTo(2);The >= 2 relaxation means a future regression (e.g., 3 or 4 instances created when only 2 are expected) would go undetected. Using ConcurrentBag was the right move for thread-safety, but the count check doesn't need to be loosened — ConcurrentBag.Count is safe to read. Consider keeping IsEqualTo(2) (or the exact expected count) to preserve regression-catching precision.
The rest looks solid
- Line ending normalization: Making
NormalizeLineEndingsinternal staticand calling it inVerifyCodeFixAsyncis the right approach — normalizing early prevents subtle platform-specific comparison failures. HasRequestmethod: Correctly reuses_stateLockand mirrors the pattern already inWaitForRequestAsync. Could use_requests.Any(r => r.Path == path)inside the lock for conciseness, but the explicit loop is equally correct.[NotInParallel]onHookExecutorHookTests_ClassLevel: Proper fix for shared mutable static state. Usingnameoffor the constraint key is good practice.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
TIP This summary will be updated as you push new changes. Give us feedback
- WhenIdle() already awaits in-flight forwarding tasks; Task.Delay(500) was dead time - Restore IsEqualTo(2) precision on AlphaInstances snapshot — ConcurrentBag preserves exact count
There was a problem hiding this comment.
Code Review (follow-up)
Previous review feedback has been fully addressed in commit e7cc031e1d:
Task.Delay(500)removed — test now relies correctly onWhenIdle()to guarantee forwarding decisions are complete before asserting.- Count assertion restored to
IsEqualTo(2)— preserves regression-catching precision.
Overall assessment
This is a clean set of targeted fixes for four distinct flakiness root causes. The changes are minimal, correct, and avoid papering over races with sleeps or relaxed assertions.
What's solid:
- Line-ending normalization applied at the call site in rather than buried inside the verifier — the right layer to normalize.
- ** method** correctly acquires and mirrors the existing thread-safety contract in .
- **** with for the key is the proper fix for shared mutable static state.
- ** + snapshot** correctly eliminates the index-race; since both alpha tests inject the same keyed instance, and reference-equality is a valid (and now race-free) check.
One minor observation:
could use inside the lock for conciseness, but the explicit loop is equally correct and readable. This is purely cosmetic.
No blockers. This PR is good to merge.
There was a problem hiding this comment.
Code Review (follow-up)
Previous review feedback has been fully addressed in commit e7cc031e1d:
Task.Delay(500)removed — test now relies correctly onWhenIdle()to guarantee forwarding decisions are complete before asserting.- Count assertion restored to
IsEqualTo(2)— preserves regression-catching precision.
Overall assessment
This is a clean set of targeted fixes for four distinct flakiness root causes. The changes are minimal, correct, and avoid papering over races with sleeps or relaxed assertions.
What's solid:
- Line-ending normalization applied at the call site in
VerifyCodeFixAsyncrather than buried inside the verifier — the right layer to normalize. HasRequestmethod correctly acquires_stateLockand mirrors the existing thread-safety contract inWaitForRequestAsync.[NotInParallel]withnameoffor the key is the proper fix for shared mutable static state.ConcurrentBag+ snapshot correctly eliminates the index-race; since both alpha tests inject the same keyed instance,snapshot[0]andsnapshot[1]reference-equality is a valid (and now race-free) check.
One minor observation:
HasRequest could use _requests.Any(r => r.Path == path) inside the lock for conciseness, but the explicit loop is equally correct and readable. Purely cosmetic.
No blockers. This PR is good to merge.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.35.2 to 1.37.10. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.37.10 <!-- Release notes generated using configuration in .github/release.yml at v1.37.10 --> ## What's Changed ### Other Changes * docs(test-filters): add migration callout for --filter → --treenode-filter by @johnkattenhorn in thomhurst/TUnit#5628 * fix: re-enable RPC tests and modernize harness (#5540) by @thomhurst in thomhurst/TUnit#5632 * fix(mocks): propagate [Obsolete] and null-forgiving raise dispatch (#5626) by @JohnVerheij in thomhurst/TUnit#5631 * ci: use setup-dotnet built-in NuGet cache by @thomhurst in thomhurst/TUnit#5635 * feat(playwright): propagate W3C trace context into browser contexts by @thomhurst in thomhurst/TUnit#5636 ### Dependencies * chore(deps): update tunit to 1.37.0 by @thomhurst in thomhurst/TUnit#5625 ## New Contributors * @johnkattenhorn made their first contribution in thomhurst/TUnit#5628 * @JohnVerheij made their first contribution in thomhurst/TUnit#5631 **Full Changelog**: thomhurst/TUnit@v1.37.0...v1.37.10 ## 1.37.0 <!-- Release notes generated using configuration in .github/release.yml at v1.37.0 --> ## What's Changed ### Other Changes * fix: stabilize flaky tests across analyzer, OTel, and engine suites by @thomhurst in thomhurst/TUnit#5609 * perf: engine hot-path allocation wins (#5528 B) by @thomhurst in thomhurst/TUnit#5610 * feat(analyzers): detect collection IsEqualTo reference equality (TUnitAssertions0016) by @thomhurst in thomhurst/TUnit#5615 * perf: consolidate test dedup + hook register guards (#5528 A) by @thomhurst in thomhurst/TUnit#5612 * perf: engine discovery/init path cleanup (#5528 C) by @thomhurst in thomhurst/TUnit#5611 * fix(assertions): render collection contents in IsEqualTo failure messages (#5613 B) by @thomhurst in thomhurst/TUnit#5619 * feat(analyzers): code-fix for TUnit0015 to insert CancellationToken (#5613 D) by @thomhurst in thomhurst/TUnit#5621 * fix(assertions): add Task reference forwarders on AsyncDelegateAssertion by @thomhurst in thomhurst/TUnit#5618 * test(asp-net): fix race in FactoryMethodOrderTests by @thomhurst in thomhurst/TUnit#5623 * feat(analyzers): code-fix for TUnit0049 to insert [MatrixDataSource] (#5613 C) by @thomhurst in thomhurst/TUnit#5620 * fix(pipeline): isolate AOT publish outputs to stop clobbering pack DLLs (#5622) by @thomhurst in thomhurst/TUnit#5624 ### Dependencies * chore(deps): update tunit to 1.36.0 by @thomhurst in thomhurst/TUnit#5608 * chore(deps): update modularpipelines to 3.2.8 by @thomhurst in thomhurst/TUnit#5614 **Full Changelog**: thomhurst/TUnit@v1.36.0...v1.37.0 ## 1.36.0 <!-- Release notes generated using configuration in .github/release.yml at v1.36.0 --> ## What's Changed ### Other Changes * fix: don't render test's own trace as Linked Trace in HTML report by @thomhurst in thomhurst/TUnit#5580 * fix(docs): benchmark index links 404 by @thomhurst in thomhurst/TUnit#5587 * docs: replace repeated benchmark link suffix with per-test descriptions by @thomhurst in thomhurst/TUnit#5588 * docs: clearer distributed tracing setup and troubleshooting by @thomhurst in thomhurst/TUnit#5597 * fix: auto-suppress ExecutionContext flow for hosted services (#5589) by @thomhurst in thomhurst/TUnit#5598 * feat: auto-align DistributedContextPropagator to W3C by @thomhurst in thomhurst/TUnit#5599 * feat: TUnit0064 analyzer + code fix for direct WebApplicationFactory inheritance by @thomhurst in thomhurst/TUnit#5601 * feat: auto-propagate test trace context through IHttpClientFactory by @thomhurst in thomhurst/TUnit#5603 * feat: TUnit.OpenTelemetry zero-config tracing package by @thomhurst in thomhurst/TUnit#5602 * fix: restore [Obsolete] members removed in v1.27 (#5539) by @thomhurst in thomhurst/TUnit#5605 * feat: generalize OTLP receiver for use outside TUnit.Aspire by @thomhurst in thomhurst/TUnit#5606 * feat: auto-configure OpenTelemetry in TestWebApplicationFactory SUT by @thomhurst in thomhurst/TUnit#5607 ### Dependencies * chore(deps): update tunit to 1.35.2 by @thomhurst in thomhurst/TUnit#5581 * chore(deps): update dependency typescript to ~6.0.3 by @thomhurst in thomhurst/TUnit#5582 * chore(deps): update dependency coverlet.collector to v10 by @thomhurst in thomhurst/TUnit#5600 **Full Changelog**: thomhurst/TUnit@v1.35.2...v1.36.0 Commits viewable in [compare view](thomhurst/TUnit@v1.35.2...v1.37.10). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
TestCode,FixedCode, and stub sources to prevent CRLF/LF mismatches across platforms.Task.WhenAnytimeout pattern with an explicitHasRequestcheck after a short settle delay.HookExecutorHookTests_ClassLevelmarked[NotInParallel]so the shared static hook counter cannot race.KeyedDataSourceTestsusesConcurrentBagand asserts all captured instances share the same reference, removing index-order assumptions.Test plan
OtlpReceiverForwardingTests.Receiver_WithoutUpstream_DoesNotForwardpasses under loadHookExecutorHookTests_ClassLevelstable across repeated runsKeyedDataSourceTests.SameKey_ReturnsSameInstancestable across repeated runs