Skip to content

Refactor: Extract shared cooperative-cancellation timeout helper to eliminate duplication#9308

Open
Copilot wants to merge 2 commits into
mainfrom
copilot/duplicate-code-fix
Open

Refactor: Extract shared cooperative-cancellation timeout helper to eliminate duplication#9308
Copilot wants to merge 2 commits into
mainfrom
copilot/duplicate-code-fix

Conversation

Copilot AI commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

The cooperative-cancellation timeout block (create timeout CancellationTokenSource, link it to the parent token, run the action, catch OperationCanceledException, distinguish timeout vs. user-cancel) was copy-pasted verbatim — including the multi-sentence explanatory comment — between FixtureMethodRunner and TestMethodInfo.Execution. The two sites also had an inconsistency: FixtureMethodRunner filtered with IsOperationCanceledExceptionFromToken, while TestMethodInfo.Execution caught bare OperationCanceledException.

Changes

  • FixtureMethodRunner.cs — Adds internal static async Task<T> RunWithCooperativeCancellationAsync<T> encapsulating the shared pattern. Callers supply onAlreadyCancelled and onCancelled(bool isTimeout) delegates to produce the appropriate result type:
internal static async Task<T> RunWithCooperativeCancellationAsync<T>(
    CancellationTokenSource cancellationTokenSource,
    int timeout,
    Func<CancellationTokenSource, Task<T>> action,
    Func<T> onAlreadyCancelled,
    Func<bool, T> onCancelled)

The private RunWithCooperativeCancellationAsync (fixture-specific) now delegates to this generic helper.

  • TestMethodInfo.Execution.cs — Replaces the ~40-line duplicated cooperative-cancellation block in ExecuteInternalWithTimeoutAsync with a call to FixtureMethodRunner.RunWithCooperativeCancellationAsync<TestResult>.

  • Bug fix: The TestMethodInfo.Execution path now consistently uses IsOperationCanceledExceptionFromToken (matching FixtureMethodRunner) instead of indiscriminately catching all OperationCanceledException instances.

Copilot AI requested review from Copilot and removed request for Copilot June 21, 2026 06:00
…r to eliminate code duplication

Closes #9306

The cooperative-cancellation timeout block (create a timeout
CancellationTokenSource, chain it to the existing token, run the action,
catch OperationCanceledException, distinguish timeout from cancellation)
was copy-pasted — comment and all — between FixtureMethodRunner and
TestMethodInfo.Execution.

This change:
- Adds `FixtureMethodRunner.RunWithCooperativeCancellationAsync<T>`, a
  generic internal helper that encapsulates the shared pattern.
- Updates the private RunWithCooperativeCancellationAsync to delegate to
  the generic helper.
- Updates TestMethodInfo.Execution.ExecuteInternalWithTimeoutAsync to use
  the shared helper, removing ~40 lines of duplicated code.
- Fixes an inconsistency: the TestMethodInfo path previously caught bare
  OperationCanceledException; the shared helper now consistently uses
  IsOperationCanceledExceptionFromToken in both paths.

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 21, 2026 06:12
Copilot AI changed the title [WIP] Refactor duplicate cooperative cancellation timeout blocks Refactor: Extract shared cooperative-cancellation timeout helper to eliminate duplication Jun 21, 2026
Copilot AI requested a review from Evangelink June 21, 2026 06:13
@Evangelink Evangelink marked this pull request as ready for review June 21, 2026 07:21
Copilot AI review requested due to automatic review settings June 21, 2026 07:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors duplicated “cooperative cancellation timeout” logic by extracting a shared helper into FixtureMethodRunner, and then reusing it from TestMethodInfo.Execution to unify behavior and reduce copy/paste.

Changes:

  • Add a generic FixtureMethodRunner.RunWithCooperativeCancellationAsync<T> helper to encapsulate cooperative-timeout token wiring and cancellation handling.
  • Replace the duplicated cooperative-cancellation block in TestMethodInfo.Execution.ExecuteInternalWithTimeoutAsync with a call to the new helper.
  • Tighten cancellation handling in the TestMethodInfo.Execution path to only treat OperationCanceledException as a timeout/cancel when it originates from the expected token (via IsOperationCanceledExceptionFromToken).
Show a summary per file
File Description
src/Adapter/MSTestAdapter.PlatformServices/Helpers/FixtureMethodRunner.cs Introduces a shared generic cooperative-cancellation helper and updates the fixture-specific helper to delegate to it.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Execution.cs Replaces local duplicated logic with a call into the shared helper, aligning cancellation filtering behavior.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment on lines 291 to +296
if (TimeoutInfo.CooperativeCancellation)
{
CancellationTokenSource? timeoutTokenSource = null;
try
{
timeoutTokenSource = new(TimeoutInfo.Timeout);
timeoutTokenSource.Token.Register(TestContext.Context.CancellationTokenSource.Cancel);
if (timeoutTokenSource.Token.IsCancellationRequested)
return await FixtureMethodRunner.RunWithCooperativeCancellationAsync<TestResult>(
TestContext.Context.CancellationTokenSource,
TimeoutInfo.Timeout,
timeoutCts => ExecuteInternalAsync(arguments, timeoutCts),
Comment on lines 99 to 108
try
{
await ExecutionContextHelpers.RunOnContextAsync(executionContext, action).ConfigureAwait(false);
return null;
return await action(timeoutTokenSource).ConfigureAwait(false);
}
catch (Exception ex) when (ex.IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
// Ideally we would like to check that the token of the exception matches cancellationTokenSource but TestContext
// instances are not well defined so we have to handle the exception entirely.
return new(
UnitTestOutcome.Timeout,
timeoutTokenSource.Token.IsCancellationRequested
? string.Format(
CultureInfo.InvariantCulture,
methodTimedOutMessageFormat,
methodInfo.DeclaringType!.FullName,
methodInfo.Name,
timeout)
: string.Format(
CultureInfo.InvariantCulture,
methodCanceledMessageFormat,
methodInfo.DeclaringType!.FullName,
methodInfo.Name));
return onCancelled(timeoutTokenSource.Token.IsCancellationRequested);
}
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.

3 participants