Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToke
/// <param name="testContext">The test context to discover objects from.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The tracked objects dictionary (same as testContext.TrackedObjects).</returns>
public Dictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default)
public SortedList<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default)
{
var visitedObjects = testContext.TrackedObjects;

Expand Down Expand Up @@ -212,7 +212,7 @@ void Recurse(object value, int depth)
/// </summary>
private void DiscoverNestedObjectsForTracking(
object obj,
Dictionary<int, HashSet<object>> visitedObjects,
IDictionary<int, HashSet<object>> visitedObjects,
int currentDepth,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -264,7 +264,7 @@ private static bool ShouldSkipType(Type type)
/// <summary>
/// Add to HashSet at specified depth. Returns true if added (not duplicate).
/// </summary>
private static bool TryAddToHashSet(Dictionary<int, HashSet<object>> dict, int depth, object obj)
private static bool TryAddToHashSet(IDictionary<int, HashSet<object>> dict, int depth, object obj)
{
var hashSet = dict.GetOrAdd(depth, _ => new HashSet<object>(ReferenceComparer));
return hashSet.Add(obj);
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal interface IObjectGraphDiscoverer
/// This method modifies testContext.TrackedObjects directly. For pure query operations,
/// use <see cref="DiscoverObjectGraph"/> instead.
/// </remarks>
Dictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default);
SortedList<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ internal void InvalidateEventReceiverCaches()

internal AbstractExecutableTest InternalExecutableTest { get; set; } = null!;

internal Dictionary<int, HashSet<object>> TrackedObjects { get; } = new();
internal SortedList<int, HashSet<object>> TrackedObjects { get; } = new();

/// <summary>
/// Sets the output captured during test building phase.
Expand Down
14 changes: 9 additions & 5 deletions TUnit.Core/Tracking/ObjectTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private static Counter GetOrCreateCounter(object obj) =>
/// <summary>
/// Counts total tracked objects across all depth levels without allocating a new collection.
/// </summary>
private static int CountTrackedObjects(Dictionary<int, HashSet<object>> trackedObjects)
private static int CountTrackedObjects(SortedList<int, HashSet<object>> trackedObjects)
{
var count = 0;
foreach (var kvp in trackedObjects)
Expand All @@ -61,7 +61,7 @@ private static int CountTrackedObjects(Dictionary<int, HashSet<object>> trackedO
/// Takes a snapshot of currently tracked objects before new discovery mutates the dictionary.
/// Uses ReferenceEqualityComparer to match object identity semantics.
/// </summary>
private static HashSet<object> SnapshotTrackedObjects(Dictionary<int, HashSet<object>> trackedObjects)
private static HashSet<object> SnapshotTrackedObjects(SortedList<int, HashSet<object>> trackedObjects)
{
var snapshot = new HashSet<object>(Helpers.ReferenceEqualityComparer.Instance);
foreach (var kvp in trackedObjects)
Expand Down Expand Up @@ -112,11 +112,15 @@ public ValueTask UntrackObjects(TestContext testContext, List<Exception> cleanup
return UntrackObjectsAsync(cleanupExceptions, trackedObjects);
}

private async ValueTask UntrackObjectsAsync(List<Exception> cleanupExceptions, Dictionary<int, HashSet<object>> trackedObjects)
private async ValueTask UntrackObjectsAsync(List<Exception> cleanupExceptions, SortedList<int, HashSet<object>> 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<Task>? disposalTasks = null;

foreach (var obj in bucket)
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/Tracking/TrackableObjectGraphProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public TrackableObjectGraphProvider(IObjectGraphDiscoverer discoverer)
/// </summary>
/// <param name="testContext">The test context to get trackable objects from.</param>
/// <param name="cancellationToken">Optional cancellation token for long-running discovery.</param>
public Dictionary<int, HashSet<object>> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default)
public SortedList<int, HashSet<object>> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default)
{
// OCP-compliant: Use the interface method directly instead of type-checking
return _discoverer.DiscoverAndTrackObjects(testContext, cancellationToken);
Expand Down
34 changes: 12 additions & 22 deletions TUnit.Engine/Services/ObjectLifecycleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,34 +220,24 @@ private void SetCachedPropertiesOnInstance(object instance, TestContext testCont
/// </summary>
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<Task>(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<Task>(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);
}
}

Expand Down
Loading