Skip to content

Failed data-row tests drop builder-context events, so DI scopes of failed rows are never disposed #7326

Closed

Description

@thomhurst

Summary

When a data row fails after its data source factories ran, the engine builds a failed test with a fresh TestBuilderContext (CreateFailedTestContext in src/TUnit.Engine/Building/TestBuilder.cs). That context has no events, so anything the data sources subscribed on TestBuilderContext.Current.Events while the row was fetched is dropped. For example, the scope a DependencyInjectionDataSourceAttribute<TScope> creates is never disposed.

#7205 routes these events to tests built from successful rows. Failed-row tests still track the row's objects for disposal (#7123), but they do not receive the row's events.

Where

CreateFailedTestForDataRowError / CreateFailedTestForDataGenerationError → CreateFailedTestContext in TestBuilder.cs. The callers have the per-iteration context (contextAccessor.Current) available, and the eager path also has unclaimedClassRow.TakeEvents().

Repro

Use a DependencyInjectionDataSourceAttribute<TScope> on a class whose method rows fail generic resolution (or any row that hits CreateFailedTestForDataRowError after the class factory ran). Count created and disposed scopes in a dependent test: the failed row's scope is created but not disposed.

Expected

A failed-row test raises the OnDispose events its row's data sources subscribed, the same way it disposes the objects it tracks.

Activity

  1. added
    P2Medium: edge case, mode divergence, flaky test, or workaround exists
    on Nov 1, 2026
  2. thomhurst commented on Nov 3, 2026

    @thomhurst
    OwnerAuthor

    Rechecked the earlier blocker against freshly fetched origin/main at 10a0af7. It no longer holds: #7417 was closed and split into #7476 (merged as e30a1bb) and #7521. Both native prerequisites, #7308 and #7381, are closed.

    #7476 already delivers the remaining failed-row fix. In src/TUnit.Engine/Building/TestBuilder.cs, ordinary row failures pass contextAccessor.Current into CreateFailedTestForDataRowError; CreateFailedTestContext copies the owning StateBag and clones its events (lines 1919–1934). The previously missing eager empty-row generic-resolution failure now creates skippedRowContext via unclaimedClassRow.CreateEnumerationFailureContext and passes it into CreateFailedTestForDataGenerationError (lines 505–530). Thus the row callbacks and cleanup follow the synthetic failed test rather than being dropped.

    tests/TUnit.UnitTests/ClassRowCleanupTests.cs:371, NonRunnableRowsPreservePublicCallbacksAndState, covers both a throwing factory and an empty skipped row, asserting owning state and all four public callbacks including OnDispose. #7476 records generated/reflection UnitTests (823 passed, 3 skipped per mode), failed/static/deferred cleanup regressions and successful Native AOT publishing; its hosted checks have no failures. The merge commit is an ancestor of current main.

    Closing as already implemented by #7476. This audit inspected current source, regression coverage, ancestry and hosted check state; no code changes or local tests were needed. No open replacement PR, remaining acceptance criterion, human decision or external dependency requires deferral.

  3. added and removed on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium: edge case, mode divergence, flaky test, or workaround existsbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions