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
32 changes: 25 additions & 7 deletions TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ private static string GenerateCustomDataProvider(AttributeData attr)


/// <summary>
/// Generates all test-related attributes for the TestMetadata.Attributes field.
/// Generates all test-related attributes for the TestMetadata.AttributesByType field as a dictionary.
/// </summary>
public static string GenerateTestAttributes(IMethodSymbol methodSymbol)
{
Expand All @@ -552,24 +552,42 @@ public static string GenerateTestAttributes(IMethodSymbol methodSymbol)

if (allAttributes.Count == 0)
{
return "System.Array.Empty<System.Attribute>()";
return "new System.Collections.Generic.Dictionary<System.Type, System.Collections.Generic.IReadOnlyList<System.Attribute>>().AsReadOnly()";
}

// Generate as a single line array to avoid CS8802 parser issues
// Group attributes by type
var attributesByType = allAttributes
.GroupBy(attr => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? "System.Attribute")
.ToList();

using var writer = new CodeWriter("", includeHeader: false);

// Generate inline array to avoid parser issues
using (writer.BeginArrayInitializer("new System.Attribute[]", terminator: ""))
// Generate dictionary initializer
writer.Append("new System.Collections.Generic.Dictionary<System.Type, System.Collections.Generic.IReadOnlyList<System.Attribute>>()");
writer.AppendLine();
writer.AppendLine("{");
writer.Indent();

foreach (var group in attributesByType)
{
var typeString = group.Key;
var attrs = group.ToList();

writer.Append($"[typeof({typeString})] = new System.Attribute[] {{ ");

var attributeStrings = new List<string>();
foreach (var attr in allAttributes)
foreach (var attr in attrs)
{
// Use unified approach for all attributes
attributeStrings.Add(GenerateAttributeInstantiation(attr));
}

writer.Append(string.Join(", ", attributeStrings));
writer.AppendLine(" },");
}

writer.Unindent();
writer.Append("}.AsReadOnly()");

return writer.ToString().Trim();
}

Expand Down
46 changes: 46 additions & 0 deletions TUnit.Core/Helpers/AttributeDictionaryHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.ObjectModel;

namespace TUnit.Core.Helpers;

/// <summary>
/// Helper methods for working with attribute dictionaries.
/// </summary>
public static class AttributeDictionaryHelper
{
private static readonly IReadOnlyDictionary<Type, IReadOnlyList<Attribute>> EmptyDictionary =
new ReadOnlyDictionary<Type, IReadOnlyList<Attribute>>(new Dictionary<Type, IReadOnlyList<Attribute>>());

/// <summary>
/// Converts an array of attributes to a read-only dictionary grouped by type.
/// </summary>
public static IReadOnlyDictionary<Type, IReadOnlyList<Attribute>> ToAttributeDictionary(this Attribute[] attributes)
{
if (attributes.Length == 0)
{
return EmptyDictionary;
}

var result = new Dictionary<Type, IReadOnlyList<Attribute>>();

foreach (var attr in attributes)
{
var type = attr.GetType();
if (!result.TryGetValue(type, out var list))
{
var newList = new List<Attribute> { attr };
result[type] = newList;
}
else
{
((List<Attribute>)list).Add(attr);
}
}

return new ReadOnlyDictionary<Type, IReadOnlyList<Attribute>>(result);
}

/// <summary>
/// Gets an empty read-only attribute dictionary.
/// </summary>
public static IReadOnlyDictionary<Type, IReadOnlyList<Attribute>> Empty => EmptyDictionary;
}
47 changes: 44 additions & 3 deletions TUnit.Core/TestDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,56 @@ public class TestDetails
public Dictionary<string, List<string>> CustomProperties { get; } = new();
public Type[]? TestClassParameterTypes { get; set; }

public required IReadOnlyList<Attribute> Attributes { get; init; }
public required IReadOnlyDictionary<Type, IReadOnlyList<Attribute>> AttributesByType { get; init; }

private readonly Lazy<IReadOnlyList<Attribute>> _cachedAllAttributes;

public TestDetails()
{
_cachedAllAttributes = new Lazy<IReadOnlyList<Attribute>>(() =>
{
var allAttrs = new List<Attribute>();
foreach (var attrList in AttributesByType?.Values ?? [])
{
allAttrs.AddRange(attrList);
}
return allAttrs;
});
}

/// <summary>
/// Checks if the test has an attribute of the specified type.
/// </summary>
/// <typeparam name="T">The attribute type to check for.</typeparam>
/// <returns>True if the test has at least one attribute of the specified type; otherwise, false.</returns>
public bool HasAttribute<T>() where T : Attribute
=> AttributesByType.ContainsKey(typeof(T));

/// <summary>
/// Gets all attributes of the specified type.
/// </summary>
/// <typeparam name="T">The attribute type to retrieve.</typeparam>
/// <returns>An enumerable of attributes of the specified type.</returns>
public IEnumerable<T> GetAttributes<T>() where T : Attribute
=> AttributesByType.TryGetValue(typeof(T), out var attrs)
? attrs.OfType<T>()
: Enumerable.Empty<T>();

/// <summary>
/// Gets all attributes as a flattened collection.
/// Cached after first access for performance.
/// </summary>
/// <returns>All attributes associated with this test.</returns>
public IReadOnlyList<Attribute> GetAllAttributes() => _cachedAllAttributes.Value;

public object?[] ClassMetadataArguments => TestClassArguments;

/// <summary>
/// Resolved generic type arguments for the test method.
/// Will be Type.EmptyTypes if the method is not generic.
/// </summary>
public Type[] MethodGenericArguments { get; set; } = Type.EmptyTypes;

/// <summary>
/// Resolved generic type arguments for the test class.
/// Will be Type.EmptyTypes if the class is not generic.
Expand Down
4 changes: 2 additions & 2 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ private async ValueTask<TestContext> CreateTestContextAsync(string testId, TestM
TestLineNumber = metadata.LineNumber,
ReturnType = metadata.MethodMetadata.ReturnType ?? typeof(void),
MethodMetadata = metadata.MethodMetadata,
Attributes = attributes,
AttributesByType = attributes.ToAttributeDictionary(),
MethodGenericArguments = testData.ResolvedMethodGenericArguments,
ClassGenericArguments = testData.ResolvedClassGenericArguments,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
Expand Down Expand Up @@ -973,7 +973,7 @@ private async Task<TestDetails> CreateFailedTestDetails(TestMetadata metadata, s
TestLineNumber = metadata.LineNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
Attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()),
AttributesByType = (await InitializeAttributesAsync(metadata.AttributeFactory.Invoke())).ToAttributeDictionary(),
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
};
}
Expand Down
9 changes: 5 additions & 4 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using EnumerableAsyncProcessor.Extensions;
using TUnit.Core;
using TUnit.Core.Helpers;
using TUnit.Core.Interfaces;
using TUnit.Core.Services;
using TUnit.Engine.Building.Interfaces;
Expand Down Expand Up @@ -173,7 +174,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
TestLineNumber = metadata.LineNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
Attributes = attributes,
AttributesByType = attributes.ToAttributeDictionary(),
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
// Don't set RetryLimit here - let discovery event receivers set it
};
Expand Down Expand Up @@ -293,7 +294,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
TestLineNumber = resolvedMetadata.LineNumber,
ReturnType = typeof(Task),
MethodMetadata = resolvedMetadata.MethodMetadata,
Attributes = attributes,
AttributesByType = attributes.ToAttributeDictionary(),
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
// Don't set Timeout and RetryLimit here - let discovery event receivers set them
};
Expand Down Expand Up @@ -367,7 +368,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
TestLineNumber = metadata.LineNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
Attributes = [],
AttributesByType = AttributeDictionaryHelper.Empty,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout
};

Expand Down Expand Up @@ -419,7 +420,7 @@ private AbstractExecutableTest CreateFailedTestForGenericResolutionError(TestMet
TestLineNumber = metadata.LineNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
Attributes = [],
AttributesByType = AttributeDictionaryHelper.Empty,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout
};

Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Discovery/ReflectionTestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async Task<object> CreateInstance(TestContext testContext)

// Otherwise fall back to creating instance normally
// Try to create instance with ClassConstructor attribute
var attributes = testContext.TestDetails.Attributes;
var attributes = testContext.TestDetails.GetAllAttributes();
var classConstructorInstance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor(
attributes,
TestClassType,
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Extensions/TestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal static class TestContextExtensions
testContext.Events,
..testContext.TestDetails.TestClassArguments,
testContext.TestDetails.ClassInstance,
..testContext.TestDetails.Attributes,
..testContext.TestDetails.GetAllAttributes(),
..testContext.TestDetails.TestMethodArguments,
..testContext.TestDetails.TestClassInjectedPropertyArguments.Select(x => x.Value),
];
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Services/TestFilterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private PropertyBag BuildPropertyBag(AbstractExecutableTest test)

private bool IsExplicitTest(AbstractExecutableTest test)
{
if (test.Context.TestDetails.Attributes.OfType<ExplicitAttribute>().Any())
if (test.Context.TestDetails.HasAttribute<ExplicitAttribute>())
{
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Services/TestRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public async Task CreateTestVariant(
}

var lambda = Expression.Lambda<Func<T, Task>>(body, parameter);
var attributes = new List<Attribute>(currentContext.TestDetails.Attributes);
var attributes = new List<Attribute>(currentContext.TestDetails.GetAllAttributes());

var discoveryResult = new DynamicDiscoveryResult
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,7 @@ namespace
public class TestDetails
{
public TestDetails() { }
public required .<> Attributes { get; init; }
public required .<, .<>> AttributesByType { get; init; }
public .<string> Categories { get; }
public [] ClassGenericArguments { get; set; }
public required object ClassInstance { get; set; }
Expand All @@ -1396,6 +1396,11 @@ namespace
public required object?[] TestMethodArguments { get; set; }
public required string TestName { get; init; }
public ? Timeout { get; set; }
public .<> GetAllAttributes() { }
public .<T> GetAttributes<T>()
where T : { }
public bool HasAttribute<T>()
where T : { }
}
public class TestDetails<T> : .TestDetails
where T : class
Expand Down Expand Up @@ -1907,6 +1912,11 @@ namespace .Helpers
public static string FormatArguments(.<object?> arguments) { }
public static string GetConstantValue(.TestContext testContext, object? o) { }
}
public static class AttributeDictionaryHelper
{
public static .<, .<>> Empty { get; }
public static .<, .<>> ToAttributeDictionary(this [] attributes) { }
}
public static class CastHelper
{
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,7 @@ namespace
public class TestDetails
{
public TestDetails() { }
public required .<> Attributes { get; init; }
public required .<, .<>> AttributesByType { get; init; }
public .<string> Categories { get; }
public [] ClassGenericArguments { get; set; }
public required object ClassInstance { get; set; }
Expand All @@ -1396,6 +1396,11 @@ namespace
public required object?[] TestMethodArguments { get; set; }
public required string TestName { get; init; }
public ? Timeout { get; set; }
public .<> GetAllAttributes() { }
public .<T> GetAttributes<T>()
where T : { }
public bool HasAttribute<T>()
where T : { }
}
public class TestDetails<T> : .TestDetails
where T : class
Expand Down Expand Up @@ -1907,6 +1912,11 @@ namespace .Helpers
public static string FormatArguments(.<object?> arguments) { }
public static string GetConstantValue(.TestContext testContext, object? o) { }
}
public static class AttributeDictionaryHelper
{
public static .<, .<>> Empty { get; }
public static .<, .<>> ToAttributeDictionary(this [] attributes) { }
}
public static class CastHelper
{
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,7 @@ namespace
public class TestDetails
{
public TestDetails() { }
public required .<> Attributes { get; init; }
public required .<, .<>> AttributesByType { get; init; }
public .<string> Categories { get; }
public [] ClassGenericArguments { get; set; }
public required object ClassInstance { get; set; }
Expand All @@ -1396,6 +1396,11 @@ namespace
public required object?[] TestMethodArguments { get; set; }
public required string TestName { get; init; }
public ? Timeout { get; set; }
public .<> GetAllAttributes() { }
public .<T> GetAttributes<T>()
where T : { }
public bool HasAttribute<T>()
where T : { }
}
public class TestDetails<T> : .TestDetails
where T : class
Expand Down Expand Up @@ -1907,6 +1912,11 @@ namespace .Helpers
public static string FormatArguments(.<object?> arguments) { }
public static string GetConstantValue(.TestContext testContext, object? o) { }
}
public static class AttributeDictionaryHelper
{
public static .<, .<>> Empty { get; }
public static .<, .<>> ToAttributeDictionary(this [] attributes) { }
}
public static class CastHelper
{
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1329,7 +1329,7 @@ namespace
public class TestDetails
{
public TestDetails() { }
public required .<> Attributes { get; init; }
public required .<, .<>> AttributesByType { get; init; }
public .<string> Categories { get; }
public [] ClassGenericArguments { get; set; }
public required object ClassInstance { get; set; }
Expand All @@ -1350,6 +1350,11 @@ namespace
public required object?[] TestMethodArguments { get; set; }
public required string TestName { get; init; }
public ? Timeout { get; set; }
public .<> GetAllAttributes() { }
public .<T> GetAttributes<T>()
where T : { }
public bool HasAttribute<T>()
where T : { }
}
public class TestDetails<T> : .TestDetails
where T : class
Expand Down Expand Up @@ -1857,6 +1862,11 @@ namespace .Helpers
public static string FormatArguments(.<object?> arguments) { }
public static string GetConstantValue(.TestContext testContext, object? o) { }
}
public static class AttributeDictionaryHelper
{
public static .<, .<>> Empty { get; }
public static .<, .<>> ToAttributeDictionary(this [] attributes) { }
}
public static class CastHelper
{
public static object? Cast( type, object? value) { }
Expand Down
Loading
Loading