Skip to content

Honor cancellation tokens linked by custom test executors - #6565

Merged
thomhurst merged 1 commit into
mainfrom
agent/link-executor-cancellation
Aug 8, 2026
Merged

Honor cancellation tokens linked by custom test executors#6565
thomhurst merged 1 commit into
mainfrom
agent/link-executor-cancellation

Conversation

@thomhurst

Copy link
Copy Markdown
Owner

Summary

  • resolve the test-body cancellation token when a custom ITestExecutor invokes its action
  • add regression coverage for source-generated and reflection execution

Root cause

The executor action captured the engine cancellation token before the custom executor ran. Calling context.Execution.AddLinkedCancellationToken(...) updated the context, but the test method still received the stale captured token.

User impact

Custom test executors can now link and cancel a token before invoking the test body, and an injected CancellationToken observes that cancellation. This enables the background-failure executor pattern discussed in discussion #3099.

Validation

  • dotnet build tests/TUnit.TestProject/TUnit.TestProject.csproj -c Release --no-restore --nologo
  • source-generated filtered test on net8.0, net9.0, and net10.0
  • reflection filtered engine test on net10.0

AOT runtime coverage remains enabled for CI; it is skipped locally by the existing test harness.

Resolve the test body token when a custom executor invokes the action so tokens linked by that executor reach injected CancellationToken parameters.
@thomhurst
thomhurst enabled auto-merge (squash) August 8, 2026 15:09
@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR changes custom test-executor invocation to resolve the test-body cancellation token when the executor invokes its action, allowing tokens linked by that executor to reach injected CancellationToken parameters.

  • Resolves the current execution-context token instead of using the earlier captured token.
  • Adds a custom executor that links and cancels a token before invoking the test.
  • Adds reflection and source-generated/AOT-oriented regression coverage with result-counter validation.

Confidence Score: 5/5

The PR appears safe to merge, with the cancellation-token propagation change covered by reflection and source-generated execution tests.

The custom-executor path now reads the execution context’s current linked token at action invocation, while the context token retains the original engine cancellation source and the regression test verifies the intended observable behavior.

Important Files Changed

Filename Overview
src/TUnit.Engine/TestExecutor.cs Defers cancellation-token resolution until the custom executor invokes the test action, preserving engine cancellation while incorporating newly linked tokens.
tests/TUnit.Engine.Tests/TestExecutorCancellationTokenTests.cs Adds filtered engine-level coverage that requires exactly one cancellation regression test to execute and pass across configured execution modes.
tests/TUnit.TestProject/TestExecutorCancellationTokenTests.cs Adds a custom executor that links and cancels a token before invocation and verifies that the injected test-body token observes cancellation.

Sequence Diagram

sequenceDiagram
  participant Engine as TUnit Engine
  participant Executor as Custom ITestExecutor
  participant Context as TestContext.Execution
  participant Test as Test Method
  Engine->>Executor: ExecuteTest(context, action)
  Executor->>Context: AddLinkedCancellationToken(token)
  Executor->>Executor: Cancel linked token
  Executor->>Engine: Invoke action
  Engine->>Context: Read current CancellationToken
  Engine->>Test: InvokeTestAsync(current token)
  Test-->>Engine: Observes cancellation
Loading

Reviews (1): Last reviewed commit: "fix(engine): honor executor-linked cance..." | Re-trigger Greptile

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Reviewed a23a7b3 (Honor cancellation tokens linked by custom test executors).

Summary: TestExecutor.ExecuteTestAsync previously captured the raw engine cancellationToken into the closure passed to a custom ITestExecutor, before that executor had a chance to call context.Execution.AddLinkedCancellationToken(...). Since the closure captured the token by value at declaration time, any linking done by the executor was invisible to the test body. The fix defers the read to executableTest.Context.Execution.CancellationToken inside the closure, so it's evaluated when the executor actually invokes action() — after linking has occurred.

Verification:

  • Traced Context.Executionthis (TestContext.cs:76) and confirmed ITestExecution.CancellationToken (TestContext.Execution.cs:44) reads the same internal backing field that AddLinkedCancellationToken (TestContext.Execution.cs:143-161) mutates under lock (Lock). The fix correctly picks up the linked composite token.
  • The non-custom-executor branch is untouched and doesn't need the fix — it has no intervening executor callback where a token could be linked mid-flight.
  • Regression coverage is added at both levels required by this repo's dual-mode rule: tests/TUnit.TestProject/TestExecutorCancellationTokenTests.cs (exercised via both TestMode.Reflection and TestMode.AOT/source-gen through InvokableTestBase) and tests/TUnit.Engine.Tests/TestExecutorCancellationTokenTests.cs, following the existing patterns in that test project (e.g. CanCancelTests.cs).
  • No source-generator output or public API changed, so no snapshot updates are needed.
  • Built tests/TUnit.TestProject in Release — compiles cleanly (0 errors); the new CancellingTestExecutor correctly implements ITestExecutor.ExecuteTest(TestContext, Func<ValueTask>).

Non-blocking observation: ExecuteTestAsync unconditionally does executableTest.Context.CancellationToken = cancellationToken; before dispatching to the executor. If a [Before(Test)] hook (rather than a custom ITestExecutor) calls context.Execution.AddLinkedCancellationToken(...), that linked token would be overwritten by this reset before the test body runs — the same class of bug this PR fixes, just via a different entry point. This is pre-existing behavior outside this diff, not something introduced here, but worth a follow-up issue if hook-based linking is a supported use case for discussion #3099's background-failure pattern.

No blocking issues found. The change is minimal, well-targeted, and the root-cause explanation in the PR description matches what's in the code.

@thomhurst
thomhurst merged commit 8e7266e into main Aug 8, 2026
13 checks passed
@thomhurst
thomhurst deleted the agent/link-executor-cancellation branch August 8, 2026 15:33
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