-
-
Notifications
You must be signed in to change notification settings - Fork 109
Better reactive test cancellation #4158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
CRITICAL ISSUE - Resource Leak in TimeoutHelper.cs:60-78 When a timeout occurs, the code awaits executionTask without any timeout or cancellation. If the task does not respond to cancellation and hangs indefinitely, this will wait forever, defeating the purpose of the timeout. RECOMMENDATION: Add a grace period (e.g., 1 second) using Task.WaitAsync() before throwing TimeoutException. PERFORMANCE ISSUE - Unnecessary allocations in hot path (TimeoutHelper.cs:46-49) When timeout is null, we still create Task.Delay(Timeout.Infinite), a CTS, and use Task.WhenAny with 3 tasks. This adds overhead for every test without a timeout. RECOMMENDATION: Add early return when timeout is null to minimize allocations in the hot path. Per CLAUDE.md Rule 4, TUnit processes millions of tests daily and performance is critical in test execution paths. POSITIVE NOTES:
ASSESSMENT: Good refactoring but needs fixes before merge. The resource leak is critical and the performance optimization is highly recommended for a test framework. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request enhances TUnit's test cancellation mechanism to be more reactive by wiring up the engine's cancellation token throughout the execution pipeline and refactoring timeout handling. The main improvement allows graceful cancellation requests (e.g., Ctrl+C) to immediately propagate to running tests rather than waiting for them to complete.
Key Changes
- Added
_serviceProvider.CancellationToken.Tokento the linked cancellation token source inTestSessionCoordinator, enabling engine cancellation to flow through test execution - Refactored
TimeoutHelperto monitor cancellation token alongside timeout, allowing immediate response to cancellation requests - Added test infrastructure and test cases to verify graceful cancellation works correctly
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
TUnit.Engine/TestSessionCoordinator.cs |
Links the engine's cancellation token to test execution, enabling reactive cancellation |
TUnit.Engine/Helpers/TimeoutHelper.cs |
Refactored to use TaskCompletionSource pattern for monitoring cancellation alongside timeout |
TUnit.TestProject/CanCancelTests.cs |
Added integration test with long-running task to verify cancellation behavior |
TUnit.Engine.Tests/CanCancelTests.cs |
Added test that verifies graceful cancellation terminates tests within expected time |
TUnit.Engine.Tests/InvokableTestBase.cs |
Enhanced test infrastructure to support graceful/forceful cancellation tokens and execution callbacks |
| var tcs = new TaskCompletionSource<bool>(); | ||
| using var registration = cancellationToken.Register(() => tcs.TrySetResult(true)); |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TaskCompletionSource and its cancellation token registration could cause a memory leak if the executionTask completes before cancellation. The registration is disposed when the using statement exits, but if executionTask completes first, the registration remains active until the method returns. Consider disposing the registration as soon as the executionTask completes to free resources immediately, or use a try-finally pattern to ensure proper cleanup.
| { | ||
| return await taskFactory(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| var executionTask = taskFactory(cancellationToken); |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking behavioral change from the previous implementation. The old code passed a linked cancellation token (timeoutCts.Token) to the taskFactory, which would be automatically cancelled when the timeout elapsed. The new code passes the original cancellationToken, which means the task won't be automatically cancelled on timeout. This could cause tasks to continue running in the background after timeout, leading to resource leaks and unexpected behavior. Restore the previous behavior by creating a linked CancellationTokenSource and passing its token to taskFactory.
| public static async Task ExecuteWithTimeoutAsync( | ||
| Func<CancellationToken, Task> taskFactory, | ||
| TimeSpan? timeout, | ||
| CancellationToken cancellationToken, | ||
| string? timeoutMessage = null) |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important XML documentation for parameters and return values has been removed. According to TUnit development guidelines, code documentation should be maintained. The removed documentation described the parameters (taskFactory, timeout, cancellationToken, timeoutMessage) and return value, as well as the TimeoutException that can be thrown. Restore this documentation.
| public static async Task<T> ExecuteWithTimeoutAsync<T>( | ||
| Func<CancellationToken, Task<T>> taskFactory, | ||
| TimeSpan? timeout, | ||
| CancellationToken cancellationToken, | ||
| string? timeoutMessage = null) |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important XML documentation for the generic type parameter and method parameters has been removed. According to TUnit development guidelines, code documentation should be maintained. The removed documentation described the type parameter T and the parameters (taskFactory, timeout, cancellationToken, timeoutMessage), return value, and TimeoutException. Restore this documentation.
TUnit.Engine.Tests/CanCancelTests.cs
Outdated
| public class CanCancelTests(TestMode testMode) : InvokableTestBase(testMode) | ||
| { | ||
| [Test, Timeout(30_000)] | ||
| public async Task Test(CancellationToken ct) |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test method name "Test" is ambiguous and doesn't describe what it's testing. According to TUnit development guidelines and best practices, test methods should have descriptive names that explain the scenario and expected outcome. Consider renaming to something like "CanCancelTests_WhenGracefullyCancelled_FailsWithinTimeout" or "GracefulCancellation_ShouldTerminateTestBeforeTimeout".
| public async Task Test(CancellationToken ct) | |
| public async Task CanCancelTests_WhenGracefullyCancelled_FailsWithinTimeout(CancellationToken ct) |
TUnit.Engine.Tests/CanCancelTests.cs
Outdated
| [Test, Timeout(30_000)] | ||
| public async Task Test(CancellationToken ct) | ||
| { | ||
| using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CancellationToken parameter 'ct' is declared but never used in the test method. Either remove the unused parameter or use it if cancellation support was intended (e.g., passing it to RunTestsWithFilter if that method supports it).
| using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); | |
| using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); | |
| cts.CancelAfter(TimeSpan.FromSeconds(5)); |
Improvements based on Copilot and Claude code review feedback: - Add fast path for no-timeout case (performance) - Create linked token when timeout is specified so task can observe both timeout and external cancellation (fixes potential resource leak) - Add 1-second grace period on timeout using Task.WaitAsync (NET8+) or Task.WhenAny fallback for older frameworks - Restore complete XML documentation for both method overloads - Rename test method to be more descriptive - Extract magic numbers to named constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Code Review: Better Reactive Test CancellationI've reviewed PR #4158 and have the following feedback: ✅ Strengths
🐛 Critical Issues1. Allocation Concern in Hot Path (TimeoutHelper.cs:54-71) The fast path now allocates a TaskCompletionSource and registers a cancellation callback on EVERY call, even when cancellation is unlikely. This is a performance regression. According to CLAUDE.md Rule 4, TUnit processes millions of tests daily. Test execution is a hot path where allocations must be minimized. Suggested Fix: Add a check for 2. Unused Variable Name (TimeoutHelper.cs:87)
3. Resource Leak on Early Return (TimeoutHelper.cs:90-93) When cancellation wins, executionTask is left running without waiting. The task could hold resources (connections, files). Suggested Fix: Add the same grace period handling as the timeout path before throwing OperationCanceledException.
|
- Add CanBeCanceled check to avoid allocations in fast path when token cannot be cancelled (performance optimization) - Rename winningTask2 to winner for clarity - Link test CancellationToken parameter with the CTS to properly use the injected token 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
PR Review: Better Reactive Test CancellationThis PR significantly improves test cancellation handling in TUnit with better responsiveness and resource management. Overall, the implementation is solid and follows TUnit's development guidelines. Below are my findings: ✅ Strengths
|
- Use await await Task.WhenAny for cleaner cancellation handling - TrySetCanceled on TCS automatically throws OperationCanceledException - Avoid redundant Task.WhenAny by checking winner identity directly - Extract GracePeriod to named constant - Add explicit timeoutCts.Cancel() before grace period - Add clarifying comments explaining dual-token mechanism - Document why cancelled tests report as "Failed" in TRX Performance: Removed redundant WhenAny allocation in normal completion path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
…flow Performance improvements: - Remove duplicate timeout mechanism (was using both CancelAfter AND Task.Delay) - Single linked CTS now handles both external cancellation and timeout - Reduced WhenAny from 3 tasks to 2 tasks - Fewer allocations: removed 1 CTS and 1 Task.Delay Safety improvement: - Use TaskCreationOptions.RunContinuationsAsynchronously on all TCS - Prevents potential stack overflow from synchronous continuation chains 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Pull Request Review: Better Reactive Test CancellationThis PR improves test cancellation handling by making cancellation more reactive and adding proper grace period handling. Overall well-implemented with good test coverage. Strengths
Critical Issue: Resource Leak (TimeoutHelper.cs:71-75)The fast path has a timing issue where registration.Dispose() happens too late: Current code awaits the winner task while still holding the registration, which delays cleanup. Fix by disposing immediately after WhenAny determines the winner. Medium Priority Issues
Minor Issues
TUnit Compliance
Overall: 7.5/10Excellent architectural approach with strong testing. Fix the resource leak and duplicate registration before merge. With these changes, this will be a solid improvement. Great work on the reactive cancellation improvement! |
Code Review: Better Reactive Test CancellationThank you for this PR! Overall score: 8.5/10 - High-quality implementation with excellent performance optimizations. ✅ Strengths
|
Graceful cancellation (SIGINT) may not work reliably in all CI environments. Add forceful cancellation (SIGKILL) at 15s as a backup to ensure process termination if graceful shutdown doesn't respond within 10 seconds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Pull Request Review: Better reactive test cancellationThis PR improves test cancellation handling by refactoring the TimeoutHelper class and adding graceful cancellation support. Strengths
Issues FoundCritical: Potential Race Condition (TimeoutHelper.cs:100-103) The code checks cancellationToken.IsCancellationRequested AFTER cancelledTcs.Task completes. There is a race window where external cancellation and timeout fire simultaneously, leading to inconsistent exception types. Recommendation: Check timeoutCts.IsCancellationRequested instead to see if timeout fired. Medium: Inefficient No-Timeout Path (TimeoutHelper.cs:60-78) When no timeout is specified but cancellation IS possible, this allocates a TCS and uses Task.WhenAny to race against cancellation. However, the task already observes cancellationToken, so this is redundant and creates unnecessary allocations (violates Performance First principle). Recommendation: Just await the task directly - it will throw OperationCanceledException naturally. Medium: Grace Period Exception Swallowing (TimeoutHelper.cs:122) All exceptions during grace period are silently caught. Consider logging at debug level. Minor Issues:
RecommendationsPriority 1: Fix race condition in cancellation vs timeout detection Overall: Great improvements to cancellation handling. Request changes to address Priority 1 and 2 before merging. |
…ations Graceful cancellation via SIGINT is unreliable in CI environments. The test can still be run manually to verify reactive cancellation works, but won't run automatically in CI where signal handling may not work. The core TimeoutHelper reactive cancellation changes are still in place. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Code Review: Better Reactive Test CancellationThank you for this PR! I've reviewed the changes and here's my feedback: ✅ Strengths
|
No description provided.