-
-
Notifications
You must be signed in to change notification settings - Fork 108
feat: TUnit.AspNetCore #4123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: TUnit.AspNetCore #4123
Changes from 1 commit
c212f9d
2d1efb5
98b38b7
87f8211
2da7ad1
fac52d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -426,12 +426,23 @@ private static string GetPropertyAccessor(INamedTypeSymbol namedTypeSymbol, IPro | |||||||||||||||||||||||||||||||||||
| // For generic types with unresolved type parameters, we can't cast to the open generic type | ||||||||||||||||||||||||||||||||||||
| // We need to use dynamic or reflection | ||||||||||||||||||||||||||||||||||||
| var hasUnresolvedTypeParameters = namedTypeSymbol.IsGenericType && | ||||||||||||||||||||||||||||||||||||
| namedTypeSymbol.TypeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter); | ||||||||||||||||||||||||||||||||||||
| (namedTypeSymbol.TypeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter) || | ||||||||||||||||||||||||||||||||||||
| namedTypeSymbol.TypeArguments.OfType<ITypeParameterSymbol>().Any() || | ||||||||||||||||||||||||||||||||||||
| SymbolEqualityComparer.Default.Equals(namedTypeSymbol, namedTypeSymbol.OriginalDefinition)); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (hasUnresolvedTypeParameters && !property.IsStatic) | ||||||||||||||||||||||||||||||||||||
| if (hasUnresolvedTypeParameters) | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| // Use dynamic to avoid invalid cast to open generic type | ||||||||||||||||||||||||||||||||||||
| return $"o => ((dynamic)o).{property.Name}"; | ||||||||||||||||||||||||||||||||||||
| if (property.IsStatic) | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| // Can't access static members on an unbound generic type like WebApplicationTest<,> | ||||||||||||||||||||||||||||||||||||
| // Use reflection to get the value at runtime | ||||||||||||||||||||||||||||||||||||
| return $"_ => typeof({namedTypeSymbol.GloballyQualified()}).GetProperty(\"{property.Name}\")?.GetValue(null)"; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| // Use dynamic to avoid invalid cast to open generic type | ||||||||||||||||||||||||||||||||||||
| return $"o => ((dynamic)o).{property.Name}"; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| if (property.IsStatic) | |
| { | |
| // Can't access static members on an unbound generic type like WebApplicationTest<,> | |
| // Use reflection to get the value at runtime | |
| return $"_ => typeof({namedTypeSymbol.GloballyQualified()}).GetProperty(\"{property.Name}\")?.GetValue(null)"; | |
| } | |
| else | |
| { | |
| // Use dynamic to avoid invalid cast to open generic type | |
| return $"o => ((dynamic)o).{property.Name}"; | |
| } | |
| return property.IsStatic | |
| // Can't access static members on an unbound generic type like WebApplicationTest<,> | |
| // Use reflection to get the value at runtime | |
| ? $"_ => typeof({namedTypeSymbol.GloballyQualified()}).GetProperty(\"{property.Name}\")?.GetValue(null)" | |
| // Use dynamic to avoid invalid cast to open generic type | |
| : $"o => ((dynamic)o).{property.Name}"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,31 +4,15 @@ | |
| namespace TUnit.Core; | ||
|
|
||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] | ||
| public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T> | ||
| : DataSourceGeneratorAttribute<T> | ||
| public sealed class ClassDataSourceAttribute : UntypedDataSourceGeneratorAttribute | ||
| { | ||
| public SharedType Shared { get; set; } = SharedType.None; | ||
| public string Key { get; set; } = string.Empty; | ||
| public Type ClassType => typeof(T); | ||
| private Type[] _types; | ||
|
|
||
| protected override IEnumerable<Func<T>> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) | ||
| public ClassDataSourceAttribute() | ||
| { | ||
| var testClassType = TestClassTypeHelper.GetTestClassType(dataGeneratorMetadata); | ||
| yield return () => ClassDataSources.Get(dataGeneratorMetadata.TestSessionId) | ||
| .Get<T>(Shared, testClassType, Key, dataGeneratorMetadata); | ||
| _types = []; | ||
| } | ||
|
Comment on lines
+9
to
14
|
||
|
|
||
|
|
||
| public IEnumerable<SharedType> GetSharedTypes() => [Shared]; | ||
|
|
||
| public IEnumerable<string> GetKeys() => string.IsNullOrEmpty(Key) ? [] : [Key]; | ||
| } | ||
|
|
||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] | ||
| public sealed class ClassDataSourceAttribute : UntypedDataSourceGeneratorAttribute | ||
| { | ||
| private readonly Type[] _types; | ||
|
|
||
| [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Non-params constructor calls params one with proper annotations.")] | ||
| public ClassDataSourceAttribute( | ||
| [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] | ||
|
|
@@ -99,6 +83,25 @@ public ClassDataSourceAttribute(params Type[] types) | |
| { | ||
| yield return () => | ||
| { | ||
| if (_types.Length == 0) | ||
| { | ||
| _types = dataGeneratorMetadata.MembersToGenerate.Select(x => | ||
| { | ||
| if (x is ParameterMetadata parameterMetadata) | ||
| { | ||
| return parameterMetadata.Type; | ||
| } | ||
|
|
||
| if (x is PropertyMetadata propertyMetadata) | ||
| { | ||
| return propertyMetadata.Type; | ||
| } | ||
|
|
||
| throw new ArgumentOutOfRangeException(nameof(dataGeneratorMetadata), | ||
| "Member to generate must be either a parameter or a property."); | ||
| }).ToArray(); | ||
| } | ||
|
|
||
| var items = new object?[_types.Length]; | ||
|
|
||
| for (var i = 0; i < _types.Length; i++) | ||
|
|
@@ -117,3 +120,24 @@ public ClassDataSourceAttribute(params Type[] types) | |
| public IEnumerable<string> GetKeys() => Keys; | ||
|
|
||
| } | ||
|
|
||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] | ||
| public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T> | ||
| : DataSourceGeneratorAttribute<T> | ||
| { | ||
| public SharedType Shared { get; set; } = SharedType.None; | ||
| public string Key { get; set; } = string.Empty; | ||
| public Type ClassType => typeof(T); | ||
|
|
||
| protected override IEnumerable<Func<T>> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) | ||
| { | ||
| var testClassType = TestClassTypeHelper.GetTestClassType(dataGeneratorMetadata); | ||
| yield return () => ClassDataSources.Get(dataGeneratorMetadata.TestSessionId) | ||
| .Get<T>(Shared, testClassType, Key, dataGeneratorMetadata); | ||
| } | ||
|
|
||
|
|
||
| public IEnumerable<SharedType> GetSharedTypes() => [Shared]; | ||
|
|
||
| public IEnumerable<string> GetKeys() => string.IsNullOrEmpty(Key) ? [] : [Key]; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HttpCapture property uses a field-backed property with C# 13 syntax (
field ??= new()), but this creates a new instance every time it's accessed if HttpExchangeCapture is not enabled in Options. This is inconsistent with the documentation which states it "Returns null if HTTP exchange capture is not enabled."The current implementation will always return a non-null instance, but it won't be wired into the middleware unless EnableHttpExchangeCapture is true. This could be confusing for users who check if HttpCapture is null to determine if capture is enabled.
Consider either:
public HttpExchangeCapture? HttpCapture => Options.EnableHttpExchangeCapture ? (field ??= new()) : null;