Repository navigation
Description
Summary
Confirmed on main (04786b3) in both modes. For every class data row, TestBuilder calls the class data factory once on its own (to initialize class data before method data sources are evaluated) and then again for each test. The instance from that first call is never tracked by the object tracker. With SharedType.None the first call creates a separate instance, so every test method leaks one undisposed instance.
Repro
public class ClassCtorData : IDisposable
{
private static int _next;
public int Id { get; } = Interlocked.Increment(ref _next);
public ClassCtorData() => Log($"CREATE #{Id}");
public void Dispose() => Log($"DISPOSE #{Id}");
public override string ToString() => $"#{Id}";
private static void Log(string s) => File.AppendAllText("suspect.log", s + Environment.NewLine);
}
[ClassDataSource<ClassCtorData>(Shared = SharedType.None)]
public class ClassDataLeak(ClassCtorData data)
{
[Test] public void One() => Console.WriteLine(data);
[Test] public void Two() => Console.WriteLine(data);
}Observed (same in source-gen and --reflection)
CREATE #1
CREATE #2
CREATE #3
CREATE #4
(One ran with #2, Two ran with #4)
DISPOSE #2
DISPOSE #4
Two tests create four instances. The two instances the tests actually use are disposed. The two from the first fetch (#1, #3) are never disposed.
Expected
Either the first fetch's instance is reused for the first test, or it is tracked and disposed. Ideally a SharedType.None class data source should not create an extra instance per test at all. Creating the extra instance also runs its constructor and its discovery-phase initialization (IAsyncDiscoveryInitializer), which costs extra for expensive fixtures.
Where
src/TUnit.Engine/Building/TestBuilder.cs:228-239: firstclassDataFactory()call, thenInitializeClassDataAsync(classData). The result is used forCreateInitializedInstanceForMethodDataSourcesand asclassInstanceArgumentsfor method data sources, then replaced.src/TUnit.Engine/Building/TestBuilder.cs:300-301:classDataFactory()is called again for every test and replacesclassData. Only this instance reachesTestData.ClassDataand is tracked.- The comment at lines 320-322 already notes that the second call "may return a different instance for non-shared objects".
- The streaming path has the same pattern:
TestBuilder.cs:1933(first fetch) andTestBuilder.cs:2138(BuildSingleTestAsync).
Both modes share this builder, so source-gen and reflection leak the same way.
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: a similar untracked-object leak for rows that end as failed tests.
- Data-source objects for skipped tests/rows are never disposed #7156: the same kind of leak for skipped tests/rows.
Activity
fix(engine): keep the first class-data fetch with the temporary insta…
fix(engine): reuse the first class-data fetch for the first test of a…
fix(core): keep the discovery exception if root tracking fails, share…
fix(core): keep and dispose data-source roots when object-graph track…