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
82 changes: 14 additions & 68 deletions TUnit.Core/Discovery/ObjectGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@ namespace TUnit.Core.Discovery;
/// <remarks>
/// Internal collections are stored privately and exposed as read-only views
/// to prevent callers from corrupting internal state.
/// Uses Lazy&lt;T&gt; for thread-safe lazy initialization of read-only views.
/// </remarks>
internal sealed class ObjectGraph : IObjectGraph
internal readonly struct ObjectGraph
{
private readonly ConcurrentDictionary<int, HashSet<object>> _objectsByDepth;
private readonly HashSet<object> _allObjects;

// Thread-safe lazy initialization of read-only views
private readonly Lazy<IReadOnlyDictionary<int, IReadOnlyCollection<object>>> _lazyReadOnlyObjectsByDepth;
private readonly Lazy<IReadOnlyCollection<object>> _lazyReadOnlyAllObjects;

// Cached sorted depths (computed once in constructor)
private readonly int[] _sortedDepthsDescending;
Expand All @@ -29,96 +23,48 @@ internal sealed class ObjectGraph : IObjectGraph
/// </summary>
/// <param name="objectsByDepth">Objects organized by depth level.</param>
/// <param name="allObjects">All unique objects in the graph.</param>
public ObjectGraph(ConcurrentDictionary<int, HashSet<object>> objectsByDepth, HashSet<object> allObjects)
public ObjectGraph(ConcurrentDictionary<int, HashSet<object>> 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<T> with ExecutionAndPublication for thread-safe single initialization
_lazyReadOnlyObjectsByDepth = new Lazy<IReadOnlyDictionary<int, IReadOnlyCollection<object>>>(
CreateReadOnlyObjectsByDepth,
LazyThreadSafetyMode.ExecutionAndPublication);

_lazyReadOnlyAllObjects = new Lazy<IReadOnlyCollection<object>>(
() => _allObjects.ToArray(),
LazyThreadSafetyMode.ExecutionAndPublication);
}

/// <inheritdoc />
public IReadOnlyDictionary<int, IReadOnlyCollection<object>> ObjectsByDepth => _lazyReadOnlyObjectsByDepth.Value;

/// <inheritdoc />
public IReadOnlyCollection<object> AllObjects => _lazyReadOnlyAllObjects.Value;

/// <inheritdoc />
public int MaxDepth { get; }

/// <inheritdoc />
public IEnumerable<object> GetObjectsAtDepth(int depth)
/// <summary>
/// Gets objects at a specific depth level.
/// </summary>
/// <param name="depth">The depth level to retrieve objects from.</param>
/// <returns>An ReadOnlyCollection of objects at the specified depth, or empty if none exist.</returns>
public IReadOnlyCollection<object> 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;
}

/// <inheritdoc />
/// <summary>
/// Gets depth levels in descending order (deepest first).
/// </summary>
/// <returns>An enumerable of depth levels ordered from deepest to shallowest.</returns>
public IEnumerable<int> GetDepthsDescending()
{
// Return cached sorted depths (computed once in constructor)
return _sortedDepthsDescending;
}

/// <summary>
/// Creates a thread-safe read-only snapshot of objects by depth.
/// </summary>
private IReadOnlyDictionary<int, IReadOnlyCollection<object>> CreateReadOnlyObjectsByDepth()
{
var dict = new Dictionary<int, IReadOnlyCollection<object>>(_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<int, IReadOnlyCollection<object>>(dict);
}
}
8 changes: 4 additions & 4 deletions TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static void ClearDiscoveryErrors()
private delegate void RootObjectCallback(object obj);

/// <inheritdoc />
public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default)
public ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default)
{
var objectsByDepth = new ConcurrentDictionary<int, HashSet<object>>();
var allObjects = new HashSet<object>(ReferenceComparer);
Expand Down Expand Up @@ -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);
}

/// <inheritdoc />
public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default)
public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default)
{
var objectsByDepth = new ConcurrentDictionary<int, HashSet<object>>();
var allObjects = new HashSet<object>(ReferenceComparer);
Expand All @@ -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);
}

/// <summary>
Expand Down
49 changes: 3 additions & 46 deletions TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using TUnit.Core.Discovery;

namespace TUnit.Core.Interfaces;

Expand Down Expand Up @@ -36,7 +37,7 @@ internal interface IObjectGraphDiscoverer
/// Depth 0 contains root objects (arguments and property values).
/// Higher depths contain nested objects.
/// </returns>
IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default);
ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default);

/// <summary>
/// Discovers nested objects from a single root object, organized by depth.
Expand All @@ -48,7 +49,7 @@ internal interface IObjectGraphDiscoverer
/// Depth 0 contains the root object itself.
/// Higher depths contain nested objects.
/// </returns>
IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default);
ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default);

/// <summary>
/// Discovers objects and populates the test context's tracked objects dictionary directly.
Expand Down Expand Up @@ -86,47 +87,3 @@ internal interface IObjectGraphTracker : IObjectGraphDiscoverer
// All methods inherited from IObjectGraphDiscoverer
// This interface provides semantic clarity for tracking operations
}

/// <summary>
/// Represents a discovered object graph organized by depth level.
/// </summary>
/// <remarks>
/// Collections are exposed as read-only to prevent callers from corrupting internal state.
/// Use <see cref="GetObjectsAtDepth"/> and <see cref="GetDepthsDescending"/> for safe iteration.
/// </remarks>
internal interface IObjectGraph
{
/// <summary>
/// Gets objects organized by depth (0 = root arguments, 1+ = nested).
/// </summary>
/// <remarks>
/// Returns a read-only view. Use <see cref="GetObjectsAtDepth"/> for iteration.
/// </remarks>
IReadOnlyDictionary<int, IReadOnlyCollection<object>> ObjectsByDepth { get; }

/// <summary>
/// Gets all unique objects in the graph.
/// </summary>
/// <remarks>
/// Returns a read-only view to prevent modification.
/// </remarks>
IReadOnlyCollection<object> AllObjects { get; }

/// <summary>
/// Gets the maximum nesting depth (-1 if empty).
/// </summary>
int MaxDepth { get; }

/// <summary>
/// Gets objects at a specific depth level.
/// </summary>
/// <param name="depth">The depth level to retrieve objects from.</param>
/// <returns>An enumerable of objects at the specified depth, or empty if none exist.</returns>
IEnumerable<object> GetObjectsAtDepth(int depth);

/// <summary>
/// Gets depth levels in descending order (deepest first).
/// </summary>
/// <returns>An enumerable of depth levels ordered from deepest to shallowest.</returns>
IEnumerable<int> GetDepthsDescending();
}
4 changes: 2 additions & 2 deletions TUnit.Engine/Services/ObjectGraphDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public ObjectGraphDiscoveryService(IObjectGraphDiscoverer discoverer)
/// <summary>
/// Discovers all objects from test context arguments and properties, organized by depth level.
/// </summary>
public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default)
public ObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default)
{
return _discoverer.DiscoverObjectGraph(testContext, cancellationToken);
}

/// <summary>
/// Discovers nested objects from a single root object, organized by depth.
/// </summary>
public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default)
public ObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default)
{
return _discoverer.DiscoverNestedObjectGraph(rootObject, cancellationToken);
}
Expand Down
Loading