Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
81d1146
refactor(formatting): escape dots in strings to prevent namespace int…
thomhurst Oct 23, 2025
f1b749d
test(aot): add tests for AOT compatibility with enum arguments to pre…
thomhurst Oct 23, 2025
36a6f49
refactor(assertions): simplify assertion calls by removing unnecessar…
thomhurst Oct 23, 2025
fe7681d
refactor(matrix): streamline enum handling and remove unnecessary dyn…
thomhurst Oct 23, 2025
fa548ef
refactor(attributes): replace RequiresDynamicCode with RequiresUnrefe…
thomhurst Oct 23, 2025
735f0aa
test(aot): enhance AOT compatibility with property injection and dyna…
thomhurst Oct 23, 2025
a1d045b
refactor(tests): remove unused methods and improve AOT compatibility …
thomhurst Oct 23, 2025
3701435
chore(dependencies): update TUnit package version to 0.75.38-PullRequ…
thomhurst Oct 23, 2025
e581a0c
refactor(attributes): skip compiler-internal attributes during attrib…
thomhurst Oct 23, 2025
abb79d3
refactor(PropertyInjection): use non-nullable type names for improved…
thomhurst Oct 23, 2025
3b3f622
refactor(CastHelper): remove unnecessary suppress message for AOT com…
thomhurst Oct 23, 2025
4062403
refactor(CastHelper): simplify casting logic by removing unnecessary …
thomhurst Oct 23, 2025
099962b
refactor(CastHelper): enhance casting logic with AOT-safe conversions…
thomhurst Oct 23, 2025
a2c182b
refactor(CastHelper): optimize casting method by adding direct type c…
thomhurst Oct 23, 2025
2e31d58
refactor(CastHelper): improve AOT handling in TryReflectionConversion…
thomhurst Oct 24, 2025
c4052e9
feat: enhance AOT converter by scanning for conversion operators in s…
thomhurst Oct 24, 2025
9376b1e
feat: improve AOT converter by scanning all types in compilation for …
thomhurst Oct 24, 2025
f24825d
feat: add support for scanning closed generic types in method paramet…
thomhurst Oct 24, 2025
05063b5
feat: add AOT and single file publishing steps to CI pipeline
thomhurst Oct 24, 2025
0cdc612
feat: update TUnit package version references to use TUnitVersion var…
thomhurst Oct 24, 2025
917ea59
feat: add additional suppression message for trimming in reflection mode
thomhurst Oct 24, 2025
4a8ca00
feat: introduce IMemberMetadata interface and update related metadata…
thomhurst Oct 24, 2025
37e1e0a
fix: refine DynamicallyAccessedMembers attributes in Cast methods for…
thomhurst Oct 24, 2025
ecabc37
fix: update DynamicallyAccessedMembers attributes in Cast methods for…
thomhurst Oct 24, 2025
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
Prev Previous commit
Next Next commit
test(aot): enhance AOT compatibility with property injection and dyna…
…mic test handling
  • Loading branch information
thomhurst committed Oct 24, 2025
commit 735f0aa6491e4e1d856a4d6a6833f56e554d1690
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using TUnit.Assertions.Core;

Expand All @@ -6,6 +7,7 @@ namespace TUnit.Assertions.Conditions;
/// <summary>
/// Asserts that two objects are NOT structurally equivalent.
/// </summary>
[RequiresUnreferencedCode("Uses reflection for structural equivalency comparison")]
public class NotStructuralEquivalencyAssertion<TValue> : Assertion<TValue>
{
private readonly object? _notExpected;
Expand Down
2 changes: 2 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ public static StringLengthAssertion HasLength(
/// Performs deep comparison of properties and fields.
/// Supports .WithPartialEquivalency() and .IgnoringMember() for advanced scenarios.
/// </summary>
[RequiresUnreferencedCode("Uses reflection to compare members")]
public static StructuralEquivalencyAssertion<TValue> IsEquivalentTo<TValue>(
this IAssertionSource<TValue> source,
object? expected,
Expand All @@ -562,6 +563,7 @@ public static StructuralEquivalencyAssertion<TValue> IsEquivalentTo<TValue>(
/// Performs deep comparison of properties and fields.
/// Supports .WithPartialEquivalency() and .IgnoringMember() for advanced scenarios.
/// </summary>
[RequiresUnreferencedCode("Uses reflection to compare members")]
public static NotStructuralEquivalencyAssertion<TValue> IsNotEquivalentTo<TValue>(
this IAssertionSource<TValue> source,
object? expected,
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/AotCompatibility/GenericTestRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static void MarkAsAotCompatible(MethodInfo method)
AotCompatibleMethods.Add(method);
}

private static readonly HashSet<MethodInfo> AotCompatibleMethods = new();
private static readonly HashSet<MethodInfo> AotCompatibleMethods = [];

/// <summary>
/// Checks if a method has been marked as AOT-compatible.
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public ClassDataSourceAttribute(params Type[] types)
public SharedType[] Shared { get; set; } = [SharedType.None];
public string[] Keys { get; set; } = [];

[SuppressMessage("Trimming", "IL2062:The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed.",
[UnconditionalSuppressMessage("Trimming", "IL2062:The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed.",
Justification = "Constructor parameter is annotated with DynamicallyAccessedMembers, so _types elements have the required annotations.")]
protected override IEnumerable<Func<object?[]?>> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata)
{
Expand Down
19 changes: 16 additions & 3 deletions TUnit.Core/DataGeneratorMetadataCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,15 @@ public static DataGeneratorMetadata CreateForPropertyInjection(

/// <summary>
/// Creates DataGeneratorMetadata for property injection using PropertyInfo (reflection mode).
/// This method is only called in reflection mode, not in source-generated/AOT scenarios.
/// </summary>
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access",
Justification = "This method is only used in reflection mode. In AOT/source-gen mode, property injection uses compile-time generated PropertyMetadata.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling",
Justification = "This method is only used in reflection mode. In AOT/source-gen mode, property injection uses compile-time generated PropertyMetadata.")]
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")]
#endif
public static DataGeneratorMetadata CreateForPropertyInjection(
PropertyInfo property,
Type containingType,
Expand Down Expand Up @@ -248,8 +256,13 @@ private static ParameterMetadata[] FilterOutCancellationToken(ParameterMetadata[
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Class metadata creation requires reflection")]
#endif
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access",
Justification = "This helper is only used in reflection mode. In AOT/source-gen mode, class metadata is generated at compile time.")]
[UnconditionalSuppressMessage("Trimming", "IL2070:Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute'",
Justification = "This helper is only used in reflection mode. In AOT/source-gen mode, class metadata is generated at compile time.")]
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")]
[UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The parameter of method does not have matching annotations.")]
#endif
private static ClassMetadata GetClassMetadataForType(Type type)
{
return ClassMetadata.GetOrAdd(type.FullName ?? type.Name, () =>
Expand All @@ -262,7 +275,7 @@ private static ClassMetadata GetClassMetadataForType(Type type)
Name = p.Name ?? $"param{i}",
TypeInfo = new ConcreteType(p.ParameterType),
ReflectionInfo = p
}).ToArray() ?? Array.Empty<ParameterMetadata>();
}).ToArray() ?? [];

return new ClassMetadata
{
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/GenericTestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
typeArgs = testContext.TestDetails.TestClassArguments?.OfType<Type>().ToArray() ?? Type.EmptyTypes;
}

var instance = InstanceFactory(typeArgs, context.ClassArguments ?? Array.Empty<object?>());
var instance = InstanceFactory(typeArgs, context.ClassArguments ?? []);

// Property injection is handled by SingleTestExecutor after instance creation
return instance;
Expand Down
7 changes: 7 additions & 0 deletions TUnit.Core/Helpers/DataSourceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,14 @@ public static void RegisterTypeCreator<T>(Func<MethodMetadata, string, Task<T>>
/// <summary>
/// Resolves a data source property value at runtime.
/// This method handles all IDataSourceAttribute implementations generically.
/// Only used in reflection mode - in AOT/source-gen mode, property injection is handled by generated code.
/// </summary>
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access",
Justification = "This method is only used in reflection mode. In AOT/source-gen mode, property injection uses compile-time generated code.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling",
Justification = "This method is only used in reflection mode. In AOT/source-gen mode, property injection uses compile-time generated code.")]
#endif
public static async Task<object?> ResolveDataSourceForPropertyAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] Type containingType, string propertyName, MethodMetadata testInformation, string testSessionId)
{
// Use PropertyInjectionService to resolve the data source attribute
Expand Down
6 changes: 3 additions & 3 deletions TUnit.Core/Models/TestSessionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal set
Name = "StaticPropertyInitialization",
TypeInfo = new ConcreteType(typeof(object)),
ReturnTypeInfo = new ConcreteType(typeof(void)),
Parameters = Array.Empty<ParameterMetadata>(),
Parameters = [],
GenericTypeCount = 0,
Class = new ClassMetadata
{
Expand All @@ -36,8 +36,8 @@ internal set
Namespace = "TUnit.Core",
TypeInfo = new ConcreteType(typeof(object)),
Assembly = AssemblyMetadata.GetOrAdd("TUnit.Core", () => new AssemblyMetadata { Name = "TUnit.Core" }),
Properties = Array.Empty<PropertyMetadata>(),
Parameters = Array.Empty<ParameterMetadata>(),
Properties = [],
Parameters = [],
Parent = null
}
},
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/PropertyInjection/ClassMetadataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static ClassMetadata GetOrCreateClassMetadata(
Name = p.Name ?? $"param{i}",
TypeInfo = new ConcreteType(p.ParameterType),
ReflectionInfo = p
}).ToArray() ?? Array.Empty<ParameterMetadata>();
}).ToArray() ?? [];

return new ClassMetadata
{
Expand Down
4 changes: 2 additions & 2 deletions TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type)
{
Type = type,
SourceGeneratedProperties = sourceGenProps,
ReflectionProperties = Array.Empty<(PropertyInfo, IDataSourceAttribute)>(),
ReflectionProperties = [],
HasProperties = sourceGenProps.Length > 0
};
}
Expand Down Expand Up @@ -94,7 +94,7 @@ public static PropertyInjectionPlan BuildReflectionPlan(Type type)
return new PropertyInjectionPlan
{
Type = type,
SourceGeneratedProperties = Array.Empty<PropertyInjectionMetadata>(),
SourceGeneratedProperties = [],
ReflectionProperties = propertyDataSourcePairs.ToArray(),
HasProperties = propertyDataSourcePairs.Count > 0
};
Expand Down
12 changes: 6 additions & 6 deletions TUnit.Core/Sources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ public static class Sources

public static readonly ConcurrentDictionary<Type, ConcurrentBag<Hooks.InstanceHookMethod>> BeforeTestHooks = new();
public static readonly ConcurrentDictionary<Type, ConcurrentBag<Hooks.InstanceHookMethod>> AfterTestHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeTestHookMethod> BeforeEveryTestHooks = new();
public static readonly ConcurrentBag<Hooks.AfterTestHookMethod> AfterEveryTestHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeTestHookMethod> BeforeEveryTestHooks = [];
public static readonly ConcurrentBag<Hooks.AfterTestHookMethod> AfterEveryTestHooks = [];

public static readonly ConcurrentDictionary<Type, ConcurrentBag<Hooks.BeforeClassHookMethod>> BeforeClassHooks = new();
public static readonly ConcurrentDictionary<Type, ConcurrentBag<Hooks.AfterClassHookMethod>> AfterClassHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeClassHookMethod> BeforeEveryClassHooks = new();
public static readonly ConcurrentBag<Hooks.AfterClassHookMethod> AfterEveryClassHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeClassHookMethod> BeforeEveryClassHooks = [];
public static readonly ConcurrentBag<Hooks.AfterClassHookMethod> AfterEveryClassHooks = [];

public static readonly ConcurrentDictionary<Assembly, ConcurrentBag<Hooks.BeforeAssemblyHookMethod>> BeforeAssemblyHooks = new();
public static readonly ConcurrentDictionary<Assembly, ConcurrentBag<Hooks.AfterAssemblyHookMethod>> AfterAssemblyHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeAssemblyHookMethod> BeforeEveryAssemblyHooks = new();
public static readonly ConcurrentBag<Hooks.AfterAssemblyHookMethod> AfterEveryAssemblyHooks = new();
public static readonly ConcurrentBag<Hooks.BeforeAssemblyHookMethod> BeforeEveryAssemblyHooks = [];
public static readonly ConcurrentBag<Hooks.AfterAssemblyHookMethod> AfterEveryAssemblyHooks = [];

public static readonly ConcurrentBag<Hooks.BeforeTestSessionHookMethod> BeforeTestSessionHooks = [];
public static readonly ConcurrentBag<Hooks.AfterTestSessionHookMethod> AfterTestSessionHooks = [];
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/StaticProperties/StaticPropertyRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace TUnit.Core.StaticProperties;
/// </summary>
public static class StaticPropertyRegistry
{
private static readonly ConcurrentBag<StaticPropertyMetadata> _registeredProperties = new();
private static readonly ConcurrentBag<StaticPropertyMetadata> _registeredProperties = [];
private static readonly ConcurrentDictionary<string, object> _initializedValues = new();

/// <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 @@ -195,7 +195,7 @@ internal override void SetAsyncLocalContext()

public object Lock { get; } = new();

public ConcurrentBag<Timing> Timings { get; } = new();
public ConcurrentBag<Timing> Timings { get; } = [];

public IReadOnlyList<Artifact> Artifacts { get; } = new List<Artifact>();

Expand Down
18 changes: 6 additions & 12 deletions TUnit.Engine/Building/Collectors/AotTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ public async Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessio
return [..standardTestMetadatas, ..dynamicTestMetadatas];
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic test conversion requires expression compilation")]
[RequiresUnreferencedCode("Method extraction from expressions uses reflection")]
#endif
[RequiresUnreferencedCode("Dynamic test collection requires expression compilation and reflection")]
private async IAsyncEnumerable<TestMetadata> CollectDynamicTestsStreaming(
string testSessionId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -88,7 +85,6 @@ private async IAsyncEnumerable<TestMetadata> CollectDynamicTestsStreaming(

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic test conversion requires expression compilation")]
[RequiresUnreferencedCode("Method extraction from expressions uses reflection")]
#endif
private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreaming(
AbstractDynamicTest abstractDynamicTest,
Expand All @@ -108,7 +104,6 @@ private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreami

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic test metadata creation requires expression extraction and reflection")]
[RequiresUnreferencedCode("Method extraction from expressions uses reflection")]
#endif
private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDiscoveryResult result)
{
Expand Down Expand Up @@ -158,11 +153,9 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
});
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic instance creation uses Activator.CreateInstance and MakeGenericType")]
[RequiresUnreferencedCode("Dynamic type instantiation requires access to constructors")]
#endif
private static Func<Type[], object?[], object>? CreateAotDynamicInstanceFactory(Type testClass, object?[]? predefinedClassArgs)
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break functionality when AOT compiling.")]
[UnconditionalSuppressMessage("Trimming", "IL2055:Either the type on which the MakeGenericType is called can\'t be statically determined, or the type parameters to be used for generic arguments can\'t be statically determined.")]
private static Func<Type[], object?[], object>? CreateAotDynamicInstanceFactory([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type testClass, object?[]? predefinedClassArgs)
{
// Check if we have predefined args to use as defaults
var hasPredefinedArgs = predefinedClassArgs is { Length: > 0 };
Expand All @@ -179,20 +172,21 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
{
return Activator.CreateInstance(closedType)!;
}

return Activator.CreateInstance(closedType, effectiveArgs)!;
}

if (effectiveArgs.Length == 0)
{
return Activator.CreateInstance(testClass)!;
}

return Activator.CreateInstance(testClass, effectiveArgs)!;
};
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic test invocation requires LambdaExpression.Compile")]
[RequiresUnreferencedCode("Expression compilation and MethodInfo.Invoke use reflection")]
#endif
private static Func<object, object?[], Task> CreateAotDynamicTestInvoker(DynamicDiscoveryResult result)
{
Expand Down
4 changes: 0 additions & 4 deletions TUnit.Engine/Building/Interfaces/ITestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,5 @@ internal interface ITestDataCollector
/// Collects all test metadata from the configured source
/// </summary>
/// <returns>Collection of test metadata ready for processing</returns>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Reflection-based implementation uses assembly scanning")]
[RequiresUnreferencedCode("Reflection-based implementation uses MakeGenericType")]
#endif
Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessionId);
}
4 changes: 0 additions & 4 deletions TUnit.Engine/Building/TestDataCollectorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ public static ITestDataCollector Create(bool? useSourceGeneration = null, Assemb
/// Attempts AOT mode first, falls back to reflection if no source-generated tests found.
/// This provides automatic mode selection for optimal performance and compatibility.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Falls back to reflection mode if no source-generated tests found")]
[RequiresUnreferencedCode("Falls back to reflection mode if no source-generated tests found")]
#endif
public static async Task<ITestDataCollector> CreateAutoDetectAsync(string testSessionId, Assembly[]? assembliesToScan = null)
{
// Try AOT mode first (check if any tests were registered)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace TUnit.Engine.Discovery;
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Hook discovery uses reflection to scan assemblies and types")]
[RequiresUnreferencedCode("Hook delegate creation requires dynamic code generation")]
#endif
internal sealed class ReflectionBasedHookDiscoveryService : IHookDiscoveryService
{
Expand Down
Loading