Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 26, 2025

Introduces a specialized test method attribute that wraps test execution in Task.Run with integrated timeout handling for non-cooperative cancellation scenarios. This provides an alternative to the global UseCooperativeCancellation setting by moving timeout logic into the attribute itself.

Changes

New Attribute

  • TaskRunTestMethodAttribute(timeout) - Wraps test invocation in Task.Run with timeout parameter (in milliseconds)
  • Implements non-cooperative timeout: stops awaiting after timeout, task continues in background
  • Handles STA threading requirements (creates STA thread on Windows when needed, otherwise uses thread pool)
  • Supports composition via constructor accepting TestMethodAttribute (follows STATestMethodAttribute pattern)
  • Timeout logic is self-contained in the attribute, no dependency on adapter

Analyzer (MSTEST0045)

  • Suppresses diagnostic when TaskRunTestMethodAttribute is present
  • Updated resources to suggest both CooperativeCancellation = true and [TaskRunTestMethod(n)]
  • Added symbol comparison logic with inheritance support

Code Fix

  • Provides two options:
    1. Set CooperativeCancellation = true on [Timeout] attribute
    2. Replace [TestMethod] and [Timeout(n)] with [TaskRunTestMethod(n)]
  • Extracts timeout value from [Timeout] attribute and passes to [TaskRunTestMethod]
  • Removes [Timeout] attribute when applying TaskRunTestMethod fix
  • New resource string: UseTaskRunTestMethodFix

API Surface

  • Added to PublicAPI.Unshipped.txt with timeout parameter and Timeout property
  • Added to WellKnownTypeNames.cs

Usage

[TestClass]
public class MyTests
{
    // Non-cooperative timeout - stops awaiting after 5 seconds, task continues in background
    [TaskRunTestMethod(5000)]
    public async Task TestWithNonCooperativeTimeout()
    {
        await SomeOperation();
    }
    
    // For cooperative timeout where test is awaited until completion:
    [TestMethod]
    [Timeout(5000, CooperativeCancellation = true)]
    public async Task TestWithCooperativeTimeout(CancellationToken ct)
    {
        await SomeOperation(ct);  // Observes cancellation cooperatively
    }
}

Key Differences

  • TaskRunTestMethod: Non-cooperative timeout. Test is marked as timed out and runner stops awaiting, but task may continue running in background. Useful when you don't want to block but accept potential dangling tasks.
  • Timeout with CooperativeCancellation = true: Cooperative timeout. Test is awaited until completion. Prevents dangling tasks if test observes cancellation properly.

Users can now explicitly choose the timeout behavior per-test rather than relying on global settings.

Original prompt

This section details on the original issue you should resolve

<issue_title>Add TaskRunTestMethod</issue_title>
<issue_description>## Summary

Add a specialized TestMethodAttribute to run the user code as part of a Task.Run allowing to stop wathching the task in case of timeout.

Background and Motivation

As of today, the TestMethod and derived attributes behavior is changing based on whether or not UseCooperativeCancellation is set to true. This is done to allow some back-compat where MSTest doesn't block if the user doesn't handle the cancellation token correctly in case of a timeout. But this is causing many danling tasks #2433 which can lead to confusions or even errors because the test method is still running.

As part of the effort to simplify the core MSTEst logic and to move each responsability to the right location, it seems more logical that this feature is moved down to a specialized test method attribute.

Proposed Feature

  • Introduce a new attribute to handle the logic on timeout
  • Update the diagnostic recommandign to use cooperative cancellation
  • Update related codefix.

Alternative Designs

None</issue_description>

Comments on the Issue (you are @copilot in this section)

@Youssef1313 With the current design of `TestMethodAttribute` where `Execute` only accepts `ITestMethod`, it might not be easy for us to flow the cancellation token to `TestMethodAttribute`.

I think it might be best to take a breaking change in v4 to change the ExecuteAsync signature to take some type which is easy to extend and add properties to as necessary.</comment_new>
<comment_new>@Youssef1313
Or, we can add TestContext/CancellationToken to ITestMethod. In fact, our concrete implementation of ITestMethod (TestMethodInfo) has ITestContext. But whether or not it should really belong there is something I'm not sure about, but I feel it doesn't belong there. Other than this small design concern around TestMethodAttribute, this makes perfect sense to me. Currently, the adapter is really concerned about too much stuff that should be taken out.</comment_new>


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 6 commits October 26, 2025 18:43
- Created TaskRunTestMethodAttribute that wraps test execution in Task.Run
- Updated UseCooperativeCancellationForTimeoutAnalyzer resources to mention TaskRunTestMethod
- Updated UseCooperativeCancellationForTimeoutFixer to offer TaskRunTestMethod as alternative
- Added test for new code fix option
- Updated PublicAPI.Unshipped.txt with new public API
- Updated all XLF localization files

Co-authored-by: Evangelink <[email protected]>
- Simplified ExecuteAsync to avoid duplicate helper methods
- Fixed logic for wrapping test invocation in Task.Run

Co-authored-by: Evangelink <[email protected]>
- Use ExecuteWithTaskRunAsync helper for both code paths to reduce duplication
- Ensures consistent Task.Run wrapping logic

Co-authored-by: Evangelink <[email protected]>
- Modified UseCooperativeCancellationForTimeoutAnalyzer to check for TaskRunTestMethodAttribute
- When TaskRunTestMethodAttribute is present, no diagnostic is reported since it provides Task.Run behavior
- Added test to verify TaskRunTestMethodAttribute with Timeout doesn't trigger diagnostic
- Fixed test to remove diagnostic marker from fixed code

Co-authored-by: Evangelink <[email protected]>
- Added TaskRunTestMethodAttribute to WellKnownTypeNames
- Updated analyzer to use SymbolEqualityComparer instead of string comparison
- Supports inheritance from TaskRunTestMethodAttribute

Co-authored-by: Evangelink <[email protected]>
- Improved comment about TaskRunTestMethodAttribute symbol handling
- Extracted IsTaskRunTestMethodAttribute helper method for cleaner code
- Better separation of concerns in the analyzer logic

Co-authored-by: Evangelink <[email protected]>
Copilot AI changed the title [WIP] Add TestMethodAttribute for Task.Run execution Add TaskRunTestMethodAttribute for cooperative timeout handling Oct 26, 2025
Copilot AI requested a review from Evangelink October 26, 2025 18:56
Copilot finished work on behalf of Evangelink October 26, 2025 18:56
/// <summary>
/// Initializes a new instance of the <see cref="TaskRunTestMethodAttribute"/> class.
/// </summary>
public TaskRunTestMethodAttribute([CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = -1)
Copy link
Member

Choose a reason for hiding this comment

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

Don't we need a timeout parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added timeout parameter to the constructor. The attribute now accepts TaskRunTestMethod(5000) where 5000 is the timeout in milliseconds. Commit: ac903bb

Comment on lines 467 to 468
[TaskRunTestMethod]
[{|#0:Timeout(5000)|}]
Copy link
Member

Choose a reason for hiding this comment

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

I think what we would need is [TaskRunTestMethod(5000)]. The whole implementation then is in the attribute, and the adapter knows nothing about this.

Copy link
Member

Choose a reason for hiding this comment

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

I have done a quick look through the app and we will need the adapter to inject a service so we can know about the timeout defined in run settings or testconfig.json. I have the feeling that to keep parity with the rest we will probably need to keep the Timeout attribute too.

Copy link
Member

Choose a reason for hiding this comment

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

Couldn't we add the timeout value on ITestMethod?

Well, the ITestMethod implementation already has the timeout info, it's just not part of the interface.

Copy link
Member

Choose a reason for hiding this comment

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

@copilot please handle the timeout

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented [TaskRunTestMethod(5000)] with timeout handling in the attribute itself. The code fix now extracts the timeout value from [Timeout] and creates [TaskRunTestMethod(timeout)], removing the need for a separate Timeout attribute. Commit: ac903bb

{
// Run the test method in Task.Run so that we can stop awaiting it on timeout
// while allowing it to complete in the background
Task<TestResult[]> testTask = Task.Run(executeFunc);
Copy link
Member

Choose a reason for hiding this comment

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

If STA with specified in runsettings, we are breaking it here because threadpool threads are not STA and we need to create our own custom thread.

I'm also not sure if we should simply just always create our custom thread and never use the threadpool here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added STA thread handling. The attribute now checks if the current thread is STA and creates a dedicated STA thread when needed (on Windows platforms), otherwise uses Task.Run with the thread pool. Commit: ac903bb

Evangelink and others added 2 commits October 27, 2025 08:20
Copy link
Member

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

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

I'll need to do some extra prompting because TaskRunTestMethod doesn't prevent dangling tasks. Only cooperative mode on the Test method does. Instead this one is supposed to help with the logic of non cooperative cancellation.

- Added timeout parameter to TaskRunTestMethodAttribute constructors
- Implemented timeout handling with Task.WhenAny for non-cooperative timeout
- Added STA thread handling to respect thread apartment state
- Updated code fix to extract timeout from Timeout attribute and pass to TaskRunTestMethod
- Updated tests to use TaskRunTestMethod(5000) instead of separate Timeout attribute
- Updated PublicAPI.Unshipped.txt with new signatures

Addresses comments from @Youssef1313 and @Evangelink about needing timeout parameter and STA thread handling.

Co-authored-by: Evangelink <[email protected]>
Copy link
Contributor Author

Copilot AI commented Oct 27, 2025

I'll need to do some extra prompting because TaskRunTestMethod doesn't prevent dangling tasks. Only cooperative mode on the Test method does. Instead this one is supposed to help with the logic of non...

Updated the documentation to clarify that TaskRunTestMethod provides non-cooperative timeout handling (stops awaiting but task continues in background). For cooperative mode that prevents dangling tasks, users should use [Timeout(n, CooperativeCancellation = true)] instead. Commit: ac903bb

Copilot AI changed the title Add TaskRunTestMethodAttribute for cooperative timeout handling Add TaskRunTestMethodAttribute with integrated timeout for non-cooperative cancellation Oct 27, 2025
Copilot AI requested a review from Evangelink October 27, 2025 07:50
Copilot finished work on behalf of Evangelink October 27, 2025 07:50

// Run with timeout
Task<TestResult[]> testTask = RunOnThreadPoolOrCustomThreadAsync(executeFunc);
Task completedTask = await Task.WhenAny(testTask, Task.Delay(Timeout)).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Is this pattern leaving the timer of Task.Delay running in the background?

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.

Add TaskRunTestMethod

3 participants