Skip to content

fix: stabilize flaky tests across analyzer, OTel, and engine suites#5609

Merged
thomhurst merged 2 commits intomainfrom
fix/flaky-test-stability
Apr 17, 2026
Merged

fix: stabilize flaky tests across analyzer, OTel, and engine suites#5609
thomhurst merged 2 commits intomainfrom
fix/flaky-test-stability

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Analyzer verifier now normalizes line endings for TestCode, FixedCode, and stub sources to prevent CRLF/LF mismatches across platforms.
  • OTLP no-forward test replaces the racy Task.WhenAny timeout pattern with an explicit HasRequest check after a short settle delay.
  • HookExecutorHookTests_ClassLevel marked [NotInParallel] so the shared static hook counter cannot race.
  • KeyedDataSourceTests uses ConcurrentBag and asserts all captured instances share the same reference, removing index-order assumptions.

Test plan

  • CI green on Windows + Linux
  • Analyzer tests pass regardless of source line-ending style
  • OtlpReceiverForwardingTests.Receiver_WithoutUpstream_DoesNotForward passes under load
  • HookExecutorHookTests_ClassLevel stable across repeated runs
  • KeyedDataSourceTests.SameKey_ReturnsSameInstance stable across repeated runs

- 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
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NormalizeLineEndings internal static and calling it at the call site in VerifyCodeFixAsync is the right approach — normalizing early prevents subtle platform-specific comparison failures.
  • HasRequest method: Correctly reuses _stateLock and mirrors the pattern already in WaitForRequestAsync. Could use _requests.Any(r => r.Path == path) inside the lock for conciseness, but the explicit loop is equally correct.
  • [NotInParallel] on HookExecutorHookTests_ClassLevel: Proper fix for shared mutable static state. Using nameof for the constraint key is good practice.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NormalizeLineEndings internal static and calling it in VerifyCodeFixAsync is the right approach — normalizing early prevents subtle platform-specific comparison failures.
  • HasRequest method: Correctly reuses _stateLock and mirrors the pattern already in WaitForRequestAsync. Could use _requests.Any(r => r.Path == path) inside the lock for conciseness, but the explicit loop is equally correct.
  • [NotInParallel] on HookExecutorHookTests_ClassLevel: Proper fix for shared mutable static state. Using nameof for the constraint key is good practice.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 17, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

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
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (follow-up)

Previous review feedback has been fully addressed in commit e7cc031e1d:

  • Task.Delay(500) removed — test now relies correctly on WhenIdle() 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.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (follow-up)

Previous review feedback has been fully addressed in commit e7cc031e1d:

  • Task.Delay(500) removed — test now relies correctly on WhenIdle() 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 VerifyCodeFixAsync rather than buried inside the verifier — the right layer to normalize.
  • HasRequest method correctly acquires _stateLock and mirrors the existing thread-safety contract in WaitForRequestAsync.
  • [NotInParallel] with nameof for 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] and snapshot[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.

This was referenced Apr 23, 2026
BenjaminMichaelis pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 25, 2026
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>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.35.2&new-version=1.37.10)](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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant