diff --git a/TUnit.Core/Discovery/ObjectGraph.cs b/TUnit.Core/Discovery/ObjectGraph.cs index 85a1b6f960..8ae2ab5016 100644 --- a/TUnit.Core/Discovery/ObjectGraph.cs +++ b/TUnit.Core/Discovery/ObjectGraph.cs @@ -10,16 +10,10 @@ namespace TUnit.Core.Discovery; /// /// Internal collections are stored privately and exposed as read-only views /// to prevent callers from corrupting internal state. -/// Uses Lazy<T> for thread-safe lazy initialization of read-only views. /// -internal sealed class ObjectGraph : IObjectGraph +internal readonly struct ObjectGraph { private readonly ConcurrentDictionary> _objectsByDepth; - private readonly HashSet _allObjects; - - // Thread-safe lazy initialization of read-only views - private readonly Lazy>> _lazyReadOnlyObjectsByDepth; - private readonly Lazy> _lazyReadOnlyAllObjects; // Cached sorted depths (computed once in constructor) private readonly int[] _sortedDepthsDescending; @@ -29,96 +23,48 @@ internal sealed class ObjectGraph : IObjectGraph /// /// Objects organized by depth level. /// All unique objects in the graph. - public ObjectGraph(ConcurrentDictionary> objectsByDepth, HashSet allObjects) + public ObjectGraph(ConcurrentDictionary> objectsByDepth) { _objectsByDepth = objectsByDepth; - _allObjects = allObjects; // Compute MaxDepth and sorted depths without LINQ to reduce allocations var keyCount = objectsByDepth.Count; if (keyCount == 0) { - MaxDepth = -1; _sortedDepthsDescending = []; } else { - var keys = new int[keyCount]; - objectsByDepth.Keys.CopyTo(keys, 0); - - // Find max manually - var maxDepth = int.MinValue; - foreach (var key in keys) - { - if (key > maxDepth) - { - maxDepth = key; - } - } - MaxDepth = maxDepth; + var keys = objectsByDepth.Keys.ToArray(); // Sort in descending order using Array.Sort with reverse comparison Array.Sort(keys, (a, b) => b.CompareTo(a)); _sortedDepthsDescending = keys; } - - // Use Lazy with ExecutionAndPublication for thread-safe single initialization - _lazyReadOnlyObjectsByDepth = new Lazy>>( - CreateReadOnlyObjectsByDepth, - LazyThreadSafetyMode.ExecutionAndPublication); - - _lazyReadOnlyAllObjects = new Lazy>( - () => _allObjects.ToArray(), - LazyThreadSafetyMode.ExecutionAndPublication); } - /// - public IReadOnlyDictionary> ObjectsByDepth => _lazyReadOnlyObjectsByDepth.Value; - - /// - public IReadOnlyCollection AllObjects => _lazyReadOnlyAllObjects.Value; - - /// - public int MaxDepth { get; } - - /// - public IEnumerable GetObjectsAtDepth(int depth) + /// + /// Gets objects at a specific depth level. + /// + /// The depth level to retrieve objects from. + /// An ReadOnlyCollection of objects at the specified depth, or empty if none exist. + public IReadOnlyCollection GetObjectsAtDepth(int depth) { if (!_objectsByDepth.TryGetValue(depth, out var objects)) { return []; } - // Lock and copy to prevent concurrent modification issues - lock (objects) - { - return objects.ToArray(); - } + return objects; } - /// + /// + /// Gets depth levels in descending order (deepest first). + /// + /// An enumerable of depth levels ordered from deepest to shallowest. public IEnumerable GetDepthsDescending() { // Return cached sorted depths (computed once in constructor) return _sortedDepthsDescending; } - - /// - /// Creates a thread-safe read-only snapshot of objects by depth. - /// - private IReadOnlyDictionary> CreateReadOnlyObjectsByDepth() - { - var dict = new Dictionary>(_objectsByDepth.Count); - - foreach (var kvp in _objectsByDepth) - { - // Lock each HashSet while copying to ensure consistency - lock (kvp.Value) - { - dict[kvp.Key] = kvp.Value.ToArray(); - } - } - - return new ReadOnlyDictionary>(dict); - } } diff --git a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs index b281c19b8f..a24396c037 100644 --- a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs +++ b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs @@ -97,7 +97,7 @@ public static void ClearDiscoveryErrors() private delegate void RootObjectCallback(object obj); /// - public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) + public ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) { var objectsByDepth = new ConcurrentDictionary>(); var allObjects = new HashSet(ReferenceComparer); @@ -128,11 +128,11 @@ bool TryAddStandard(object obj, int depth) obj => DiscoverNestedObjects(obj, objectsByDepth, visitedObjects, allObjects, allObjectsLock, currentDepth: 1, cancellationToken), cancellationToken); - return new ObjectGraph(objectsByDepth, allObjects); + return new ObjectGraph(objectsByDepth); } /// - public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) + public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) { var objectsByDepth = new ConcurrentDictionary>(); var allObjects = new HashSet(ReferenceComparer); @@ -150,7 +150,7 @@ public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationTok DiscoverNestedObjects(rootObject, objectsByDepth, visitedObjects, allObjects, allObjectsLock, currentDepth: 1, cancellationToken); } - return new ObjectGraph(objectsByDepth, allObjects); + return new ObjectGraph(objectsByDepth); } /// diff --git a/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs index 8ac4586be3..01eb24af48 100644 --- a/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs +++ b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using TUnit.Core.Discovery; namespace TUnit.Core.Interfaces; @@ -36,7 +37,7 @@ internal interface IObjectGraphDiscoverer /// Depth 0 contains root objects (arguments and property values). /// Higher depths contain nested objects. /// - IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default); + ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default); /// /// Discovers nested objects from a single root object, organized by depth. @@ -48,7 +49,7 @@ internal interface IObjectGraphDiscoverer /// Depth 0 contains the root object itself. /// Higher depths contain nested objects. /// - IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default); + ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default); /// /// Discovers objects and populates the test context's tracked objects dictionary directly. @@ -86,47 +87,3 @@ internal interface IObjectGraphTracker : IObjectGraphDiscoverer // All methods inherited from IObjectGraphDiscoverer // This interface provides semantic clarity for tracking operations } - -/// -/// Represents a discovered object graph organized by depth level. -/// -/// -/// Collections are exposed as read-only to prevent callers from corrupting internal state. -/// Use and for safe iteration. -/// -internal interface IObjectGraph -{ - /// - /// Gets objects organized by depth (0 = root arguments, 1+ = nested). - /// - /// - /// Returns a read-only view. Use for iteration. - /// - IReadOnlyDictionary> ObjectsByDepth { get; } - - /// - /// Gets all unique objects in the graph. - /// - /// - /// Returns a read-only view to prevent modification. - /// - IReadOnlyCollection AllObjects { get; } - - /// - /// Gets the maximum nesting depth (-1 if empty). - /// - int MaxDepth { get; } - - /// - /// Gets objects at a specific depth level. - /// - /// The depth level to retrieve objects from. - /// An enumerable of objects at the specified depth, or empty if none exist. - IEnumerable GetObjectsAtDepth(int depth); - - /// - /// Gets depth levels in descending order (deepest first). - /// - /// An enumerable of depth levels ordered from deepest to shallowest. - IEnumerable GetDepthsDescending(); -} diff --git a/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs b/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs index 1a0636f605..b959549b6e 100644 --- a/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs +++ b/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs @@ -30,7 +30,7 @@ public ObjectGraphDiscoveryService(IObjectGraphDiscoverer discoverer) /// /// Discovers all objects from test context arguments and properties, organized by depth level. /// - public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) + public ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) { return _discoverer.DiscoverObjectGraph(testContext, cancellationToken); } @@ -38,7 +38,7 @@ public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationTok /// /// Discovers nested objects from a single root object, organized by depth. /// - public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) + public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) { return _discoverer.DiscoverNestedObjectGraph(rootObject, cancellationToken); }