Skip to content

Fix flaky BeLessThanOrEqualTo execution time test#3200

Merged
dennisdoomen merged 2 commits into
mainfrom
copilot/fix-flaky-execution-time-test
Apr 23, 2026
Merged

Fix flaky BeLessThanOrEqualTo execution time test#3200
dennisdoomen merged 2 commits into
mainfrom
copilot/fix-flaky-execution-time-test

Conversation

Copilot AI commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Thread.Sleep(100) only guarantees a minimum sleep duration — on a loaded CI runner, the OS scheduler can delay wakeup well beyond 100ms, causing the 1-second limit to be exceeded intermittently.

  • Increase the BeLessThanOrEqualTo "should not throw" test limit from 1.Seconds() to 2.Seconds(), aligning it with the identical setup in the BeLessThan equivalent test which already uses that margin.

Copilot AI changed the title [WIP] Fix flaky execution time assertions test Fix flaky BeLessThanOrEqualTo execution time test Apr 23, 2026
Copilot AI requested a review from dennisdoomen April 23, 2026 12:31

@dennisdoomen dennisdoomen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot do a thorough investigation to see why this test is flaky. Is it the test itself or is it the implementation of the assertion API in FA

Copilot AI commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot do a thorough investigation to see why this test is flaky. Is it the test itself or is it the implementation of the assertion API in FA

Here's the full analysis:

The implementation is correct. The flakiness was entirely in the test.

How the timing path works

  1. someAction.ExecutionTime() calls new ExecutionTime(action, createTimer) which immediately calls Task.Run(...). Inside that task, the action runs wrapped in a using (timer = createTimer()) block — the StopwatchTimer starts right before action() and stops (via Dispose) right after it completes.

  2. BeLessThanOrEqualTo(maxDuration) calls PollUntil(duration => duration <= maxDuration, expectedResult: false, rate: maxDuration). For the success path with a 1-second limit:

    • Initial check: elapsed ≈ 0, condition is 0 <= 1s = true, expectedResult is falsetrue == false → don't break early
    • Calls execution.Task.Wait(1.Second()) — waits up to 1 second for the task to finish
    • After the wait returns true, elapsed = execution.ElapsedTime reads the stopped stopwatch (stopped inside the task before IsRunning = false)
    • Final check: elapsed <= maxDuration
  3. Thread safety is sound: Task.Wait() provides the happens-before guarantee so that after it returns true, all writes from the task thread (including timer.Dispose() and IsRunning = false) are visible to the calling thread.

Why it was flaky

The StopwatchTimer measures actual wall-clock time inside the task, which includes:

  • Thread pool queue delay before the task starts running
  • The full actual duration of Thread.Sleep(100), which only guarantees a minimum sleep — on a loaded CI runner, the OS scheduler may not wake the thread for well over 100ms

With a 1-second limit and a 100ms sleep, there's nominally 900ms of slack. But under CI load, Thread.Sleep(100) can easily sleep for much longer, and if the measured elapsed time exceeds 1 second, the assertion fails.

Why the fix is correct

The equivalent BeLessThan test already used 2.Seconds() with the same Thread.Sleep(100) action. Aligning BeLessThanOrEqualTo to also use 2.Seconds() (20× the sleep duration) provides the same headroom and brings it in line with the sibling test's conventions. Fixed in c784841.

@github-actions

github-actions Bot commented Apr 23, 2026

Copy link
Copy Markdown

Test Results

    37 files  ±0      37 suites  ±0   2m 56s ⏱️ +20s
 6 363 tests ±0   6 362 ✅ ±0  1 💤 ±0  0 ❌ ±0 
39 524 runs  ±0  39 518 ✅ ±0  6 💤 ±0  0 ❌ ±0 

Results for commit c784841. ± Comparison against base commit 79050a9.

This pull request removes 10 and adds 8 tests. Note that renamed tests count towards both.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)

♻️ This comment has been updated with latest results.

@coveralls

coveralls commented Apr 23, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 24835291706

Coverage remained the same at 97.172%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 13367
Covered Lines: 13142
Line Coverage: 98.32%
Relevant Branches: 4346
Covered Branches: 4070
Branch Coverage: 93.65%
Branches in Coverage %: Yes
Coverage Strength: 66871.11 hits per line

💛 - Coveralls

@github-actions

Copy link
Copy Markdown

Qodana for .NET

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com

@dennisdoomen
dennisdoomen marked this pull request as ready for review April 23, 2026 14:04
This was referenced Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants