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
28 changes: 8 additions & 20 deletions TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ bool TryAddStandard(object obj, int depth)
return false;
}

AddToDepth(objectsByDepth, depth, obj);
TryAddToHashSet(objectsByDepth, depth, obj);

return true;
}
Expand All @@ -133,7 +133,7 @@ public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToke

if (visitedObjects.Add(rootObject))
{
AddToDepth(objectsByDepth, 0, rootObject);
TryAddToHashSet(objectsByDepth, 0, rootObject);

DiscoverNestedObjects(rootObject, objectsByDepth, visitedObjects, currentDepth: 1, cancellationToken);
}
Expand All @@ -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 ConcurrentDictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default)
public Dictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default)
{
var visitedObjects = testContext.TrackedObjects;

Expand Down Expand Up @@ -188,7 +188,7 @@ bool TryAddStandard(object value, int depth)
return false;
}

AddToDepth(objectsByDepth, depth, value);
TryAddToHashSet(objectsByDepth, depth, value);

return true;
}
Expand All @@ -212,7 +212,7 @@ void Recurse(object value, int depth)
/// </summary>
private void DiscoverNestedObjectsForTracking(
object obj,
ConcurrentDictionary<int, HashSet<object>> visitedObjects,
Dictionary<int, HashSet<object>> visitedObjects,
int currentDepth,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -262,24 +262,12 @@ private static bool ShouldSkipType(Type type)
}

/// <summary>
/// Adds an object to the specified depth level.
/// </summary>
private static void AddToDepth(Dictionary<int, HashSet<object>> objectsByDepth, int depth, object obj)
{
var hashSet = objectsByDepth.GetOrAdd(depth, _ => new HashSet<object>(ReferenceComparer));
hashSet.Add(obj);
}

/// <summary>
/// Thread-safe add to HashSet at specified depth. Returns true if added (not duplicate).
/// Add to HashSet at specified depth. Returns true if added (not duplicate).
/// </summary>
private static bool TryAddToHashSet(ConcurrentDictionary<int, HashSet<object>> dict, int depth, object obj)
private static bool TryAddToHashSet(Dictionary<int, HashSet<object>> dict, int depth, object obj)
{
var hashSet = dict.GetOrAdd(depth, _ => new HashSet<object>(ReferenceComparer));
lock (hashSet)
{
return hashSet.Add(obj);
}
return hashSet.Add(obj);
}

#region Consolidated Traversal Methods (DRY)
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>
ConcurrentDictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default);
Dictionary<int, HashSet<object>> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down
11 changes: 1 addition & 10 deletions TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,20 +222,11 @@ internal void InvalidateEventReceiverCaches()
CachedClassInstance = null;
}


internal ConcurrentDictionary<string, object?> ObjectBag => _testBuilderContext.StateBag;


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

private ConcurrentDictionary<int, HashSet<object>>? _trackedObjects;

/// <summary>
/// Thread-safe lazy initialization of TrackedObjects using LazyInitializer
/// to prevent race conditions when multiple threads access this property simultaneously.
/// </summary>
internal ConcurrentDictionary<int, HashSet<object>> TrackedObjects =>
LazyInitializer.EnsureInitialized(ref _trackedObjects)!;
internal Dictionary<int, HashSet<object>> TrackedObjects { get; } = new();

/// <summary>
/// Sets the output captured during test building phase.
Expand Down
13 changes: 5 additions & 8 deletions TUnit.Core/Tracking/ObjectTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ private static Counter GetOrCreateCounter(object obj) =>
/// Thread-safe: locks each HashSet while copying.
/// Pre-calculates capacity to avoid HashSet resizing during population.
/// </summary>
private static ISet<object> FlattenTrackedObjects(ConcurrentDictionary<int, HashSet<object>> trackedObjects)
private static ISet<object> FlattenTrackedObjects(Dictionary<int, HashSet<object>> trackedObjects)
{
if (trackedObjects.IsEmpty)
if (trackedObjects.Count == 0)
{
return ImmutableHashSet<object>.Empty;
}
Expand All @@ -64,12 +64,9 @@ private static ISet<object> FlattenTrackedObjects(ConcurrentDictionary<int, Hash

foreach (var kvp in trackedObjects)
{
lock (kvp.Value)
foreach (var obj in kvp.Value)
{
foreach (var obj in kvp.Value)
{
result.Add(obj);
}
result.Add(obj);
}
}

Expand All @@ -83,7 +80,7 @@ public void TrackObjects(TestContext testContext)

// Get new trackable objects
var trackableDict = trackableObjectGraphProvider.GetTrackableObjects(testContext);
if (trackableDict.IsEmpty && alreadyTracked.Count == 0)
if (trackableDict.Count == 0 && alreadyTracked.Count == 0)
{
return;
}
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 ConcurrentDictionary<int, HashSet<object>> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default)
public Dictionary<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
12 changes: 2 additions & 10 deletions TUnit.Engine/Services/ObjectLifecycleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,9 @@ private async Task InitializeTrackedObjectsAsync(TestContext testContext, Cancel
continue;
}

// Copy to array under lock to prevent concurrent modification
object[] objectsCopy;
lock (objectsAtLevel)
{
objectsCopy = new object[objectsAtLevel.Count];
objectsAtLevel.CopyTo(objectsCopy);
}

// Initialize all objects at this level in parallel
var tasks = new List<Task>(objectsCopy.Length);
foreach (var obj in objectsCopy)
var tasks = new List<Task>(objectsAtLevel.Count);
foreach (var obj in objectsAtLevel)
{
tasks.Add(InitializeObjectWithNestedAsync(obj, cancellationToken));
}
Expand Down
Loading