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
31 changes: 22 additions & 9 deletions TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
Expand Down Expand Up @@ -48,16 +51,26 @@ internal sealed class ObjectGraphDiscoverer : IObjectGraphTracker
// Reference equality comparer for object tracking (ignores Equals overrides)
private static readonly Helpers.ReferenceEqualityComparer ReferenceComparer = Helpers.ReferenceEqualityComparer.Instance;

// Types to skip during discovery (primitives, strings, system types)
// Types to skip during discovery (primitives, strings, system types).
// Frozen on net8+ for faster lookups; this set is read-many during per-test traversal.
#if NET8_0_OR_GREATER
private static readonly System.Collections.Frozen.FrozenSet<Type> SkipTypes =
#else
private static readonly HashSet<Type> SkipTypes =
[
typeof(string),
typeof(decimal),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
];
#endif
new HashSet<Type>
{
typeof(string),
typeof(decimal),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
}
#if NET8_0_OR_GREATER
.ToFrozenSet()
#endif
;

// Thread-safe collection of discovery errors for diagnostics
private static readonly ConcurrentBag<DiscoveryError> DiscoveryErrors = [];
Expand Down
35 changes: 24 additions & 11 deletions TUnit.Core/Helpers/TupleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,41 @@ namespace TUnit.Core.Helpers;
/// </summary>
public static class TupleFactory
{
private static readonly Dictionary<Type, Func<object?[], object?>> TypedFactories = new();

// Read-only after the static initializer completes; TryGetValue is called per
// tuple-arg conversion. Frozen on net8+ for faster lookups.
#if NET8_0_OR_GREATER
private static readonly System.Collections.Frozen.FrozenDictionary<Type, Func<object?[], object?>> TypedFactories;
#else
private static readonly Dictionary<Type, Func<object?[], object?>> TypedFactories;
#endif

static TupleFactory()
{
// Register factories for common tuple types with object elements
RegisterFactory<ValueTuple<object?, object?>>((args) =>
var factories = new Dictionary<Type, Func<object?[], object?>>();
RegisterFactory<ValueTuple<object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?>(args[0], args[1]));
RegisterFactory<ValueTuple<object?, object?, object?>>((args) =>
RegisterFactory<ValueTuple<object?, object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?, object?>(args[0], args[1], args[2]));
RegisterFactory<ValueTuple<object?, object?, object?, object?>>((args) =>
RegisterFactory<ValueTuple<object?, object?, object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?, object?, object?>(args[0], args[1], args[2], args[3]));
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?>>((args) =>
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?, object?, object?, object?>(args[0], args[1], args[2], args[3], args[4]));
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?, object?>>((args) =>
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?, object?, object?, object?, object?>(args[0], args[1], args[2], args[3], args[4], args[5]));
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?, object?, object?>>((args) =>
RegisterFactory<ValueTuple<object?, object?, object?, object?, object?, object?, object?>>(factories, (args) =>
new ValueTuple<object?, object?, object?, object?, object?, object?, object?>(args[0], args[1], args[2], args[3], args[4], args[5], args[6]));

#if NET8_0_OR_GREATER
TypedFactories = System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(factories);
#else
TypedFactories = factories;
#endif
}
private static void RegisterFactory<T>(Func<object?[], T> factory) where T : struct

private static void RegisterFactory<T>(Dictionary<Type, Func<object?[], object?>> factories, Func<object?[], T> factory) where T : struct
{
TypedFactories[typeof(T)] = args => factory(args);
factories[typeof(T)] = args => factory(args);
}

/// <summary>
Expand Down
20 changes: 18 additions & 2 deletions TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Buffers;
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
Expand Down Expand Up @@ -206,8 +209,17 @@ private static IEnumerable<MethodInfo> GetAllTestMethods([DynamicallyAccessedMem
});
}

// Read-many during reflection discovery (one Contains per assembly).
// Frozen on net8+ for faster lookups.
#if NET8_0_OR_GREATER
private static readonly System.Collections.Frozen.FrozenSet<string> ExcludedAssemblyNames =
new HashSet<string>
{
#else
private static readonly HashSet<string> ExcludedAssemblyNames =
[
new()
{
#endif
"mscorlib",
"System",
"System.Core",
Expand Down Expand Up @@ -269,7 +281,11 @@ private static IEnumerable<MethodInfo> GetAllTestMethods([DynamicallyAccessedMem
"Shouldly",
"NSubstitute",
"Rhino.Mocks"
];
}
#if NET8_0_OR_GREATER
.ToFrozenSet()
#endif
;

private static bool ShouldScanAssembly(Assembly assembly)
{
Expand Down
Loading