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
5 changes: 4 additions & 1 deletion TUnit.Core/DynamicTestBuilderContext.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;

namespace TUnit.Core;

/// <summary>
Expand All @@ -20,6 +22,7 @@ public DynamicTestBuilderContext(string filePath, int lineNumber)

public IReadOnlyList<AbstractDynamicTest> Tests => _tests.AsReadOnly();

[RequiresDynamicCode("Adding dynamic tests requires reflection which is not supported in native AOT scenarios.")]
public void AddTest(AbstractDynamicTest test)
{
// Set creator location if the test implements IDynamicTestCreatorLocation
Expand All @@ -28,7 +31,7 @@ public void AddTest(AbstractDynamicTest test)
testWithLocation.CreatorFilePath = FilePath;
testWithLocation.CreatorLineNumber = LineNumber;
}

_tests.Add(test);
}
}
1 change: 1 addition & 0 deletions TUnit.Core/Extensions/TestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static string GetClassTypeName(this TestContext context)
return $"{context.TestDetails.ClassType.Name}({string.Join(", ", context.TestDetails.TestClassArguments.Select(a => ArgumentFormatter.Format(a, context.ArgumentDisplayFormatters)))})";
}

[RequiresDynamicCode("Adding dynamic tests requires reflection which is not supported in native AOT scenarios.")]
public static async Task AddDynamicTest<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Core/Interfaces/ITestRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ public interface ITestRegistry
/// <param name="context">The current test context</param>
/// <param name="dynamicTest">The dynamic test instance to add</param>
/// <returns>A task that completes when the test has been queued for execution</returns>
[RequiresDynamicCode("Adding dynamic tests requires runtime compilation and reflection which are not supported in native AOT scenarios.")]
Task AddDynamicTest<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.PublicMethods
| DynamicallyAccessedMemberTypes.NonPublicMethods
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields)] T>(TestContext context, DynamicTest<T> dynamicTest)
| DynamicallyAccessedMemberTypes.NonPublicFields)] T>(TestContext context, DynamicTest<T> dynamicTest)
where T : class;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using TUnit.Core.DataSources;
using TUnit.Core.Initialization;
Expand Down Expand Up @@ -33,6 +34,13 @@ public bool CanHandle(PropertyInitializationContext context)
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Reflection mode support")]
public async Task InitializePropertyAsync(PropertyInitializationContext context)
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
}
#endif

if (context.PropertyInfo == null || context.DataSource == null)
{
return;
Expand All @@ -58,4 +66,4 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context)
PropertyTrackingService.AddToTestContext(context, resolvedValue);
}

}
}
5 changes: 5 additions & 0 deletions TUnit.Engine/Building/Collectors/AotTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ private async IAsyncEnumerable<TestMetadata> CollectDynamicTestsStreaming(
}
}

[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
Justification = "Dynamic tests are opt-in and users are warned via RequiresDynamicCode on the method they call")]
private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreaming(
AbstractDynamicTest abstractDynamicTest,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
Expand All @@ -92,6 +94,7 @@ private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreami
}
}

[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic tests require runtime compilation of lambda expressions and are not supported in native AOT scenarios.")]
private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDiscoveryResult result)
{
if (result.TestClassType == null || result.TestMethod == null)
Expand Down Expand Up @@ -144,6 +147,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
});
}

[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic test instance creation requires Activator.CreateInstance and MakeGenericType which are not supported in native AOT scenarios.")]
[UnconditionalSuppressMessage("Trimming",
"IL2070:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'System.Type.GetConstructors()'",
Justification = "AOT mode uses source-generated factories")]
Expand Down Expand Up @@ -187,6 +191,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
};
}

[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic test invocation requires LambdaExpression.Compile() which is not supported in native AOT scenarios.")]
private static Func<object, object?[], Task> CreateAotDynamicTestInvoker(DynamicDiscoveryResult result)
{
return async (instance, args) =>
Expand Down
1 change: 1 addition & 0 deletions TUnit.Engine/Building/ReflectionMetadataBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using TUnit.Core;

namespace TUnit.Engine.Building;
Expand Down
4 changes: 0 additions & 4 deletions TUnit.Engine/Building/TestDataCollectorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ internal static class TestDataCollectorFactory
/// Creates a test data collector based on the specified or detected mode.
/// Source generation mode is preferred for AOT compatibility.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code", Justification = "Reflection mode is explicitly chosen and cannot support trimming")]
[UnconditionalSuppressMessage("AOT", "IL3050:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling", Justification = "Reflection mode is explicitly chosen and cannot support AOT")]
public static ITestDataCollector Create(bool? useSourceGeneration = null, Assembly[]? assembliesToScan = null)
{
var isSourceGenerationEnabled = useSourceGeneration ?? SourceRegistrar.IsEnabled;
Expand All @@ -43,8 +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>
[UnconditionalSuppressMessage("Trimming", "IL2026:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code", Justification = "Reflection mode is a fallback and cannot support trimming")]
[UnconditionalSuppressMessage("AOT", "IL3050:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling", Justification = "Reflection mode is a fallback and cannot support AOT")]
public static async Task<ITestDataCollector> CreateAutoDetectAsync(string testSessionId, Assembly[]? assembliesToScan = null)
{
// Try AOT mode first (check if any tests were registered)
Expand Down
27 changes: 21 additions & 6 deletions TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using TUnit.Core;

namespace TUnit.Engine.Discovery;
Expand Down Expand Up @@ -59,8 +60,15 @@ public override int GetHashCode()
/// </summary>
public static T? GetAttribute<T>(Type testClass, MethodInfo? testMethod = null) where T : Attribute
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
}
#endif

var cacheKey = new AttributeCacheKey(testClass, testMethod, typeof(T));

return (T?)_attributeCache.GetOrAdd(cacheKey, key =>
{
// Original lookup logic preserved
Expand Down Expand Up @@ -88,6 +96,13 @@ public override int GetHashCode()
/// </summary>
public static IEnumerable<T> GetAttributes<T>(Type testClass, MethodInfo? testMethod = null) where T : Attribute
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
}
#endif

var attributes = new List<T>();

attributes.AddRange(testClass.Assembly.GetCustomAttributes<T>());
Expand All @@ -104,7 +119,7 @@ public static IEnumerable<T> GetAttributes<T>(Type testClass, MethodInfo? testMe
public static string[] ExtractCategories(Type testClass, MethodInfo testMethod)
{
var categories = new HashSet<string>();

foreach (var attr in GetAttributes<CategoryAttribute>(testClass, testMethod))
{
categories.Add(attr.Category);
Expand All @@ -128,7 +143,7 @@ public static bool CanRunInParallel(Type testClass, MethodInfo testMethod)
public static TestDependency[] ExtractDependencies(Type testClass, MethodInfo testMethod)
{
var dependencies = new List<TestDependency>();

foreach (var attr in GetAttributes<DependsOnAttribute>(testClass, testMethod))
{
dependencies.Add(attr.ToTestDependency());
Expand All @@ -155,13 +170,13 @@ public static IDataSourceAttribute[] ExtractDataSources(ICustomAttributeProvider
public static Attribute[] GetAllAttributes(Type testClass, MethodInfo testMethod)
{
var attributes = new List<Attribute>();

// Add in reverse order of precedence so method attributes come first
// This ensures ScopedAttributeFilter will keep method-level attributes over class/assembly
attributes.AddRange(testMethod.GetCustomAttributes());
attributes.AddRange(testClass.GetCustomAttributes());
attributes.AddRange(testClass.Assembly.GetCustomAttributes());

return attributes.ToArray();
}

Expand Down Expand Up @@ -193,4 +208,4 @@ public static PropertyDataSource[] ExtractPropertyDataSources([DynamicallyAccess

return propertyDataSources.ToArray();
}
}
}
28 changes: 16 additions & 12 deletions TUnit.Engine/Discovery/ReflectionGenericTypeResolver.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using TUnit.Core;

namespace TUnit.Engine.Discovery;

/// <summary>
/// Handles generic type resolution and instantiation for reflection-based test discovery
/// </summary>
[RequiresUnreferencedCode("Reflection-based generic type resolution requires unreferenced code")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Reflection mode cannot support trimming")]
[UnconditionalSuppressMessage("Trimming", "IL2055:Call to 'System.Type.MakeGenericType' can not be statically analyzed", Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("Trimming", "IL2065:Value passed to implicit 'this' parameter of method can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements", Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter does not satisfy annotation requirements", Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("Trimming", "IL2070:Target method does not satisfy annotation requirements", Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods(BindingFlags)'", Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Reflection mode cannot support AOT")]
internal static class ReflectionGenericTypeResolver
{
/// <summary>
/// Determines generic type arguments from data row values
/// </summary>
public static Type[]? DetermineGenericTypeArguments(Type genericTypeDefinition, object?[] dataRow)
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
}
#endif

var genericParameters = genericTypeDefinition.GetGenericArguments();

// If no data row or empty data, can't determine types
Expand Down Expand Up @@ -68,9 +82,6 @@ internal static class ReflectionGenericTypeResolver
/// <summary>
/// Extracts generic type information including constraints
/// </summary>
[UnconditionalSuppressMessage("Trimming",
"IL2065:Value passed to implicit 'this' parameter of method 'System.Type.GetInterfaces()' can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements",
Justification = "Reflection mode requires dynamic access")]
public static GenericTypeInfo? ExtractGenericTypeInfo(Type testClass)
{
if (!testClass.IsGenericTypeDefinition)
Expand Down Expand Up @@ -106,9 +117,6 @@ internal static class ReflectionGenericTypeResolver
/// <summary>
/// Extracts generic method information including parameter positions
/// </summary>
[UnconditionalSuppressMessage("Trimming",
"IL2065:Value passed to implicit 'this' parameter of method 'System.Type.GetInterfaces()' can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements",
Justification = "Reflection mode requires dynamic access")]
public static GenericMethodInfo? ExtractGenericMethodInfo(MethodInfo method)
{
if (!method.IsGenericMethodDefinition)
Expand Down Expand Up @@ -157,10 +165,6 @@ internal static class ReflectionGenericTypeResolver
/// <summary>
/// Creates a concrete type from a generic type definition and validates the type arguments
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2055:Call to 'System.Type.MakeGenericType' can not be statically analyzed",
Justification = "Reflection mode requires dynamic access")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
Justification = "Reflection mode cannot support AOT")]
public static Type CreateConcreteType(Type genericTypeDefinition, Type[] typeArguments)
{
var genericParams = genericTypeDefinition.GetGenericArguments();
Expand All @@ -174,4 +178,4 @@ public static Type CreateConcreteType(Type genericTypeDefinition, Type[] typeArg

return genericTypeDefinition.MakeGenericType(typeArguments);
}
}
}
Loading
Loading