Repository navigation
Description
What happens
When object-graph tracking for a test throws, the test's data-source objects are never disposed, including disposable root arguments that were created successfully.
ObjectTracker.TrackObjects (src/TUnit.Core/Tracking/ObjectTracker.cs) asks ObjectGraphDiscoverer.DiscoverAndTrackObjects for the test's object graph and only counts references after discovery finishes. If a nested property getter throws during discovery (for example an IAsyncInitializer-typed or injectable property on a class-data object), no reference is counted. Since PR #7191, the objects that discovery had already added to TestContext.TrackedObjects are also removed, so completing the test does not release references it never took.
As a result the root argument objects are neither counted nor tracked. Neither UntrackObjects at test completion nor the session-end sweep (DisposeAndClearStaticTrackingAsync) disposes them.
This affects both registration paths:
- Normal registration:
TestFilterService.RegisterTest→RegisterTestArgumentsAsync→ObjectLifecycleService.RegisterTestAsync→TrackObjects. The exception marks the test failed, and its roots leak. The same leak exists onmainbefore fix(engine): dispose data-source objects of skipped tests and rows #7191. - Skipped tests (Data-source objects for skipped tests/rows are never disposed #7156 / PR fix(engine): dispose data-source objects of skipped tests and rows #7191):
TrackSkippedTestArguments. The exception is caught and logged, the test stays skipped, and its roots leak. That is no worse than before Data-source objects for skipped tests/rows are never disposed #7156, when skipped tests' objects were never tracked.
Repro
public sealed class Root : IDisposable
{
public static bool Disposed;
public Nested Child => throw new InvalidOperationException("getter");
public void Dispose() => Disposed = true;
}
public sealed class Nested : IAsyncInitializer { public Task InitializeAsync() => Task.CompletedTask; }
[ClassDataSource<Root>(Shared = SharedType.None)]
public class T(Root root) { [Test] public void A() { } }From reading the code (not yet run): A fails during registration, and Root.Disposed stays false at session end, in both modes. Adding [Skip] to A should give the same result.
Expected
Roots that discovery has already produced are counted before nested traversal starts, or are kept when discovery fails, so they are disposed when the test completes. Rollback should remove only what nested discovery added. Shared objects must keep correct reference counts.
Found while reviewing CodeRabbit feedback on PR #7191 (src/TUnit.Engine/Services/TestFilterService.cs around line 126).
Activity
fix(core): keep and dispose data-source roots when object-graph track…
fix(core): keep the discovery exception if root tracking fails, share…
test(7213): tag the disposal observer as an expected failure (#7213)
fix(core): keep and dispose data-source roots when object-graph track…