Repository navigation
Description
Summary
Confirmed on main (04786b3) in both modes, for skipped tests. Disposable data-source objects created for a test that ends up skipped are never disposed. This covers a [Skip] test with a non-shared ClassDataSource argument and a TestDataRow with Skip set. The test builder creates the objects at discovery, but skipped tests never register them with the object tracker, so nothing ever disposes them.
Repro
public abstract class TrackedDisposable : IDisposable
{
private static int _next;
public int Id { get; } = Interlocked.Increment(ref _next);
protected TrackedDisposable() => Log($"CREATE {GetType().Name}#{Id}");
public void Dispose() => Log($"DISPOSE {GetType().Name}#{Id}");
public override string ToString() => $"{GetType().Name}#{Id}";
private static void Log(string s) => File.AppendAllText("suspect.log", s + Environment.NewLine);
}
public class SkipMethodClassDs : TrackedDisposable;
public class NormalMethodClassDs : TrackedDisposable;
public class SkipRowMds : TrackedDisposable;
public class SkippedRowsLeak
{
[Test, Skip("skipped")]
[ClassDataSource<SkipMethodClassDs>(Shared = SharedType.None)]
public void SkippedWithClassDs(SkipMethodClassDs d) { }
[Test]
[ClassDataSource<NormalMethodClassDs>(Shared = SharedType.None)]
public void NormalWithClassDs(NormalMethodClassDs d) { }
public static IEnumerable<TestDataRow<SkipRowMds>> Rows()
{
yield return new TestDataRow<SkipRowMds>(new SkipRowMds(), Skip: "row skipped");
}
[Test]
[MethodDataSource(nameof(Rows))]
public void SkippedRow(SkipRowMds d) { }
}Observed (same in source-gen and --reflection)
CREATE SkipMethodClassDs#1
CREATE NormalMethodClassDs#2
CREATE SkipRowMds#3
DISPOSE NormalMethodClassDs#2
The test displays as SkippedWithClassDs(SkipMethodClassDs#1), which shows the object was built. SkipMethodClassDs and SkipRowMds are never disposed. The non-skipped control is disposed.
Expected
Objects created for a row whose test is skipped are disposed, for example when the skipped test completes or at session end. As in #7123, shared or keyed instances that other tests still hold must not be disposed early, so this should go through the object tracker's reference counting.
Where
src/TUnit.Engine/Services/TestFilterService.cs:121-128: whenSkipReasonis set,RegisterTestreturns beforeRegisterTestArgumentsAsync, so the row's objects are never tracked. Skipping argument registration is intentional: it avoids initializing expensive shared fixtures. But the non-shared objects the builder already created are then orphaned.src/TUnit.Engine/Services/TestExecution/TestCoordinator.cs:77-83: the skip short-circuit. TheUntrackObjectscall at line 224 then has nothing to release.src/TUnit.Engine/Building/TestBuilder.cs:300-306: class and method data factories run for every row, including rows that end up skipped.
Also checked: rows dropped as incompatible with a concrete generic instantiation (unconfirmed)
TestBuilder.cs:338-345 drops a row with continue when IsDataCompatibleWithExpectedTypes returns false. By that point methodDataFactory() (line 306) and InitializeClassDataAsync(methodData) (line 335) have already run, and the objects are not tracked. Reading the code, those objects would leak the same way. I could not reach that branch, though. With [GenerateGenericTest(typeof(A))] [GenerateGenericTest(typeof(B))] plus either a MethodDataSource yielding mixed A/B rows or [ClassDataSource<A>] [ClassDataSource<B>], incompatible rows were not dropped. They ran and failed with a cast error, and their objects were disposed. Treat the dropped-row variant as found by code reading only.
Related
- Data-source objects for rows that end as failed tests are never disposed #7123 / PR fix(engine): dispose data-source objects of rows that end as failed tests #7150: fixes the same leak for rows that end as failed tests during building. It adds
FailedDataRowObjectstracking roots. Skipped rows (and dropped rows) are not covered, but the same mechanism could probably be reused.
Activity
fix(engine): contain errors when tracking a skipped test's objects
fix(engine): dispose data-source objects of skipped tests and rows (t…
thomhurst commentedon Nov 2, 2026
Confirmed the incompatible-row limitation while validating #7417 at 2ba3c66, SDK 10.0.403 / net10.0, generated mode. A custom
DataSourceGeneratorAttribute<object>yielding factories for"incompatible"and42, applied to[Test, GenerateGenericTest(typeof(int))] public void Example<T>(T value), executes the string row and fails with InvalidCastException in the generated invoker rather than filtering it. Run the test with a targeted--treenode-filter. The integer row passes. This confirms the final section's existing observation; it prevents using that integration reproduction to verify rejected-row callback isolation in #7417.