From 756ddfead6b22aeb6aac4d70e98bd502b3985d80 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 6 Oct 2025 23:06:16 +0100 Subject: [PATCH 1/3] fix: enhance AOT compatibility in CastHelper with additional checks --- TUnit.Core/Helpers/CastHelper.cs | 35 +++++++++++++------ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 6 ++-- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 6 ++-- .../TUnit.NugetTester/MyTests.cs | 10 +++++- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/TUnit.Core/Helpers/CastHelper.cs b/TUnit.Core/Helpers/CastHelper.cs index 14d7b5d1c5..bf852f20b3 100644 --- a/TUnit.Core/Helpers/CastHelper.cs +++ b/TUnit.Core/Helpers/CastHelper.cs @@ -1,16 +1,16 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using TUnit.Core.Converters; namespace TUnit.Core.Helpers; +[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] +[SuppressMessage("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.")] public static class CastHelper { - #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Type conversion uses reflection for custom conversions")] - [RequiresDynamicCode("Dynamic type conversion may require runtime code generation")] - #endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? Cast<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T>(object? value) { if (value is null) @@ -69,6 +69,7 @@ public static class CastHelper // Handle null -> empty array if (value is null) { + ThrowOnAot(value, underlyingType); return (T?)(object)Array.CreateInstance(targetElementType, 0); } @@ -77,6 +78,8 @@ public static class CastHelper { if (value is IConvertible) { + ThrowOnAot(value, underlyingType); + try { var convertedValue = Convert.ChangeType(value, targetElementType); @@ -104,6 +107,7 @@ public static class CastHelper // Otherwise, convert each element try { + ThrowOnAot(value, underlyingType); var targetArray = Array.CreateInstance(targetElementType, sourceArray.Length); for (var i = 0; i < sourceArray.Length; i++) { @@ -175,10 +179,7 @@ public static class CastHelper } } - #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Type conversion uses reflection for custom conversions")] - [RequiresDynamicCode("Dynamic type conversion may require runtime code generation")] - #endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object? Cast([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, object? value) { if (value is null) @@ -217,6 +218,7 @@ public static class CastHelper var firstItem = enumerable.Cast().FirstOrDefault(); if (firstItem != null) { + ThrowOnAot(value, underlyingType); // Use reflection to get the Value property var valueProperty = GetValuePropertySafe(firstItem.GetType()); if (valueProperty != null) @@ -252,6 +254,7 @@ public static class CastHelper // Handle null -> empty array if (value is null) { + ThrowOnAot(value, underlyingType); return Array.CreateInstance(targetElementType, 0); } @@ -260,6 +263,7 @@ public static class CastHelper { if (value is IConvertible) { + ThrowOnAot(value, underlyingType); try { var convertedValue = Convert.ChangeType(value, targetElementType); @@ -287,6 +291,7 @@ public static class CastHelper // Otherwise, convert each element try { + ThrowOnAot(value, underlyingType); var targetArray = Array.CreateInstance(targetElementType, sourceArray.Length); for (var i = 0; i < sourceArray.Length; i++) { @@ -363,6 +368,17 @@ public static class CastHelper mi.Name == "op_Explicit" && mi.ReturnType == targetType && HasCorrectInputType(baseType, mi)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ThrowOnAot(object? value, Type? targetType) + { +#if NET + if (!RuntimeFeature.IsDynamicCodeSupported) + { + throw new InvalidOperationException($"Cannot cast {value} to {targetType} in AOT mode as it requires reflection."); + } +#endif + } + private static bool HasCorrectInputType(Type baseType, MethodInfo mi) { var pi = mi.GetParameters().FirstOrDefault(); @@ -372,9 +388,6 @@ private static bool HasCorrectInputType(Type baseType, MethodInfo mi) /// /// Gets the "Value" property from a type in an AOT-safer manner. /// - #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Property access for CustomAttributeTypedArgument unwrapping")] - #endif private static PropertyInfo? GetValuePropertySafe([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) { return type.GetProperty("Value"); diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 8ee1c045a8..1261a65685 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1949,13 +1949,11 @@ namespace .Helpers public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } + [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + + "nctionality when AOT compiling.", Justification="")] public static class CastHelper { - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { } - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { } public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { } } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index f3e4cd113f..84728329af 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1949,13 +1949,11 @@ namespace .Helpers public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } + [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + + "nctionality when AOT compiling.", Justification="")] public static class CastHelper { - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { } - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { } public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { } } diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs index 0b6ade63bc..a63a972f3a 100644 --- a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs +++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs @@ -10,4 +10,12 @@ public void Test() { TestContext.Current!.GetDefaultLogger().LogInformation("Blah"); } -} \ No newline at end of file + + [Test] + [Arguments(1)] + [Arguments(2)] + public void DataTest(int value) + { + TestContext.Current!.GetDefaultLogger().LogInformation(value); + } +} From d9c3ec0fbd79bfe295e02da3c99958b9707e3808 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:49:09 +0100 Subject: [PATCH 2/3] fix: log integer value as string in DataTest method --- .../TUnit.NugetTester/TUnit.NugetTester/MyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs index a63a972f3a..0f44a4da7c 100644 --- a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs +++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/MyTests.cs @@ -16,6 +16,6 @@ public void Test() [Arguments(2)] public void DataTest(int value) { - TestContext.Current!.GetDefaultLogger().LogInformation(value); + TestContext.Current!.GetDefaultLogger().LogInformation(value.ToString()); } } From 5ab398bc88e0342f0633024b887e31b5c73a55a1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:53:46 +0100 Subject: [PATCH 3/3] UnconditionalSuppressMessage --- TUnit.Core/Helpers/CastHelper.cs | 8 +++++--- ...ibrary_Has_No_API_Changes.DotNet10_0.verified.txt | 12 +++++------- ...Library_Has_No_API_Changes.DotNet8_0.verified.txt | 5 ++++- ...Library_Has_No_API_Changes.DotNet9_0.verified.txt | 5 ++++- ...ibrary_Has_No_API_Changes.DotNet10_0.verified.txt | 8 -------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/TUnit.Core/Helpers/CastHelper.cs b/TUnit.Core/Helpers/CastHelper.cs index bf852f20b3..c0fb2ce73f 100644 --- a/TUnit.Core/Helpers/CastHelper.cs +++ b/TUnit.Core/Helpers/CastHelper.cs @@ -6,8 +6,8 @@ namespace TUnit.Core.Helpers; -[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] -[SuppressMessage("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("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.")] +[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.")] public static class CastHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -374,7 +374,9 @@ private static void ThrowOnAot(object? value, Type? targetType) #if NET if (!RuntimeFeature.IsDynamicCodeSupported) { - throw new InvalidOperationException($"Cannot cast {value} to {targetType} in AOT mode as it requires reflection."); + throw new InvalidOperationException( + $"Cannot cast {value?.GetType()?.Name ?? "null"} to {targetType?.Name} in AOT mode. " + + "Consider using AotConverterRegistry.Register() for custom type conversions."); } #endif } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 0796ca3247..8c4b77aa51 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1949,13 +1949,14 @@ namespace .Helpers public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } + [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + + "nctionality when AOT compiling.")] + [.("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttr" + + "ibute\' in call to target method. The return value of the source method does not " + + "have matching annotations.")] public static class CastHelper { - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { } - [.("Dynamic type conversion may require runtime code generation")] - [.("Type conversion uses reflection for custom conversions")] public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { } public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { } } @@ -2006,7 +2007,6 @@ namespace .Helpers public static class DataSourceHelpers { public static <.>[] HandleTupleValue(object? value, bool shouldUnwrap) { } - public static . InitializeDataSourcePropertiesAsync(object? instance, .MethodMetadata testInformation, string testSessionId) { } public static object? InvokeIfFunc(object? value) { } public static T InvokeIfFunc(object? value) { } public static bool IsTuple(object? obj) { } @@ -2014,12 +2014,10 @@ namespace .Helpers public static . ProcessDataSourceResultGeneric(T data) { } public static . ProcessEnumerableDataSource(. enumerable) { } public static <.>[] ProcessTestDataSource(T data, int expectedParameterCount = -1) { } - public static void RegisterPropertyInitializer( initializer) { } public static void RegisterTypeCreator(<.MethodMetadata, string, .> creator) { } [.("Data source resolution may require dynamic code generation")] [.("Property types are resolved through reflection")] public static . ResolveDataSourceForPropertyAsync([.(..None | ..PublicParameterlessConstructor | ..PublicFields | ..NonPublicFields | ..PublicProperties)] containingType, string propertyName, .MethodMetadata testInformation, string testSessionId) { } - public static . ResolveDataSourcePropertyAsync(object instance, string propertyName, .MethodMetadata testInformation, string testSessionId) { } public static object?[] ToObjectArray(this object? item) { } public static object?[] ToObjectArrayWithTypes(this object? item, []? expectedTypes) { } [return: .(new string[] { diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 1261a65685..dfb764c7d3 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -1950,7 +1950,10 @@ namespace .Helpers public static string GetConstantValue(.TestContext testContext, object? o) { } } [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + - "nctionality when AOT compiling.", Justification="")] + "nctionality when AOT compiling.")] + [.("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttr" + + "ibute\' in call to target method. The return value of the source method does not " + + "have matching annotations.")] public static class CastHelper { public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 84728329af..a098ac965d 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -1950,7 +1950,10 @@ namespace .Helpers public static string GetConstantValue(.TestContext testContext, object? o) { } } [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + - "nctionality when AOT compiling.", Justification="")] + "nctionality when AOT compiling.")] + [.("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttr" + + "ibute\' in call to target method. The return value of the source method does not " + + "have matching annotations.")] public static class CastHelper { public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { } diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 2af9d26627..83be6f8f35 100644 --- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -1,13 +1,5 @@ [assembly: .(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] namespace -{ - public static class AotMethodInvokers - { - public static string GetMethodKey(string typeName, string methodName, int parameterCount = 0) { } - public static . InvokeMethodAsync(string methodKey, object? instance, params object?[]? parameters) { } - } -} -namespace { public class BrowserTest : .PlaywrightTest {