diff --git a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs index 96fb88747d..5d77386df0 100644 --- a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs +++ b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs @@ -148,7 +148,7 @@ public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToke /// The test context to discover objects from. /// Cancellation token for the operation. /// The tracked objects dictionary (same as testContext.TrackedObjects). - public Dictionary> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default) + public SortedList> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default) { var visitedObjects = testContext.TrackedObjects; @@ -212,7 +212,7 @@ void Recurse(object value, int depth) /// private void DiscoverNestedObjectsForTracking( object obj, - Dictionary> visitedObjects, + IDictionary> visitedObjects, int currentDepth, CancellationToken cancellationToken) { @@ -264,7 +264,7 @@ private static bool ShouldSkipType(Type type) /// /// Add to HashSet at specified depth. Returns true if added (not duplicate). /// - private static bool TryAddToHashSet(Dictionary> dict, int depth, object obj) + private static bool TryAddToHashSet(IDictionary> dict, int depth, object obj) { var hashSet = dict.GetOrAdd(depth, _ => new HashSet(ReferenceComparer)); return hashSet.Add(obj); diff --git a/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs index b4b67880b5..2c45a6007c 100644 --- a/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs +++ b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs @@ -64,7 +64,7 @@ internal interface IObjectGraphDiscoverer /// This method modifies testContext.TrackedObjects directly. For pure query operations, /// use instead. /// - Dictionary> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default); + SortedList> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default); } /// diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 770565e145..51b3f1fa00 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -332,7 +332,7 @@ internal void InvalidateEventReceiverCaches() internal AbstractExecutableTest InternalExecutableTest { get; set; } = null!; - internal Dictionary> TrackedObjects { get; } = new(); + internal SortedList> TrackedObjects { get; } = new(); /// /// Sets the output captured during test building phase. diff --git a/TUnit.Core/Tracking/ObjectTracker.cs b/TUnit.Core/Tracking/ObjectTracker.cs index 472b55377f..3d841e4b02 100644 --- a/TUnit.Core/Tracking/ObjectTracker.cs +++ b/TUnit.Core/Tracking/ObjectTracker.cs @@ -46,7 +46,7 @@ private static Counter GetOrCreateCounter(object obj) => /// /// Counts total tracked objects across all depth levels without allocating a new collection. /// - private static int CountTrackedObjects(Dictionary> trackedObjects) + private static int CountTrackedObjects(SortedList> trackedObjects) { var count = 0; foreach (var kvp in trackedObjects) @@ -61,7 +61,7 @@ private static int CountTrackedObjects(Dictionary> trackedO /// Takes a snapshot of currently tracked objects before new discovery mutates the dictionary. /// Uses ReferenceEqualityComparer to match object identity semantics. /// - private static HashSet SnapshotTrackedObjects(Dictionary> trackedObjects) + private static HashSet SnapshotTrackedObjects(SortedList> trackedObjects) { var snapshot = new HashSet(Helpers.ReferenceEqualityComparer.Instance); foreach (var kvp in trackedObjects) @@ -112,11 +112,15 @@ public ValueTask UntrackObjects(TestContext testContext, List cleanup return UntrackObjectsAsync(cleanupExceptions, trackedObjects); } - private async ValueTask UntrackObjectsAsync(List cleanupExceptions, Dictionary> trackedObjects) + private async ValueTask UntrackObjectsAsync(List cleanupExceptions, SortedList> trackedObjects) { - foreach (var depth in trackedObjects.Keys.OrderByDescending(k => k)) + // SortedList keeps keys in ascending order; iterate by index in reverse for descending depth. + var keys = trackedObjects.Keys; + var values = trackedObjects.Values; + + for (var i = keys.Count - 1; i >= 0; i--) { - var bucket = trackedObjects[depth]; + var bucket = values[i]; List? disposalTasks = null; foreach (var obj in bucket) diff --git a/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs b/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs index 2b51fc7dac..e11cbdc8d6 100644 --- a/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs +++ b/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs @@ -34,7 +34,7 @@ public TrackableObjectGraphProvider(IObjectGraphDiscoverer discoverer) /// /// The test context to get trackable objects from. /// Optional cancellation token for long-running discovery. - public Dictionary> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default) + public SortedList> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default) { // OCP-compliant: Use the interface method directly instead of type-checking return _discoverer.DiscoverAndTrackObjects(testContext, cancellationToken); diff --git a/TUnit.Engine/Services/ObjectLifecycleService.cs b/TUnit.Engine/Services/ObjectLifecycleService.cs index 4e10d5f7f6..bbc0f0590b 100644 --- a/TUnit.Engine/Services/ObjectLifecycleService.cs +++ b/TUnit.Engine/Services/ObjectLifecycleService.cs @@ -220,34 +220,24 @@ private void SetCachedPropertiesOnInstance(object instance, TestContext testCont /// private async Task InitializeTrackedObjectsAsync(TestContext testContext, CancellationToken cancellationToken) { - // Get levels without LINQ - use Array.Sort with reverse comparison for descending order + // SortedList keeps keys in ascending order; iterate by index in reverse for descending depth. var trackedObjects = testContext.TrackedObjects; - var levelCount = trackedObjects.Count; + var values = trackedObjects.Values; - if (levelCount > 0) + for (var i = values.Count - 1; i >= 0; i--) { - var levels = new int[levelCount]; - trackedObjects.Keys.CopyTo(levels, 0); - Array.Sort(levels, (a, b) => b.CompareTo(a)); // Descending order + var objectsAtLevel = values[i]; - foreach (var level in levels) + // Initialize all objects at this level in parallel + var tasks = new List(objectsAtLevel.Count); + foreach (var obj in objectsAtLevel) { - if (!trackedObjects.TryGetValue(level, out var objectsAtLevel)) - { - continue; - } - - // Initialize all objects at this level in parallel - var tasks = new List(objectsAtLevel.Count); - foreach (var obj in objectsAtLevel) - { - tasks.Add(InitializeObjectWithNestedAsync(obj, cancellationToken)); - } + tasks.Add(InitializeObjectWithNestedAsync(obj, cancellationToken)); + } - if (tasks.Count > 0) - { - await Task.WhenAll(tasks); - } + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); } }