diff --git a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt index 6d2b4e0bbc..f7f7880765 100644 --- a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt @@ -19,7 +19,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_Test_TestSource_GU [ new global::TUnit.Core.TestAttribute(), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -120,7 +121,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_GenericMethodDataS new global::TUnit.Core.TestAttribute(), new global::TUnit.Core.MethodDataSourceAttribute("Foo"), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -247,7 +249,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_NonGenericMethodDa new global::TUnit.Core.TestAttribute(), new global::TUnit.Core.MethodDataSourceAttribute(typeof(global::TUnit.TestProject.TestData), "Foo"), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -361,6 +364,105 @@ internal static class InheritedTestsFromDifferentProjectTests_NonGenericMethodDa } +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable + +// +#pragma warning disable +#nullable enable +namespace TUnit.Generated; +internal sealed class InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_TestSource_GUID : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource +{ + public async global::System.Collections.Generic.IAsyncEnumerable GetTestsAsync(string testSessionId, [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) + { + var metadata = new global::TUnit.Core.TestMetadata + { + TestName = "VerifyInheritedCategoriesAreAvailable", + TestClassType = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TestMethodName = "VerifyInheritedCategoriesAreAvailable", + Dependencies = global::System.Array.Empty(), + AttributeFactory = () => + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.TestProject.Attributes.EngineTest(Pass), + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") + ], + DataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + PropertyDataSources = new global::TUnit.Core.PropertyDataSource[] + { + }, + PropertyInjections = new global::TUnit.Core.PropertyInjectionData[] + { + }, + InheritanceDepth = 0, + FilePath = @"", + LineNumber = 26, + MethodMetadata = new global::TUnit.Core.MethodMetadata + { + Type = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.InheritedTestsFromDifferentProjectTests, TestsBase`1"), + Name = "VerifyInheritedCategoriesAreAvailable", + GenericTypeCount = 0, + ReturnType = typeof(global::System.Threading.Tasks.Task), + ReturnTypeReference = global::TUnit.Core.TypeReference.CreateConcrete("System.Threading.Tasks.Task, System.Private.CoreLib"), + Parameters = global::System.Array.Empty(), + Class = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests", () => + { + var classMetadata = new global::TUnit.Core.ClassMetadata + { + Type = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.InheritedTestsFromDifferentProjectTests, TestsBase`1"), + Name = "InheritedTestsFromDifferentProjectTests", + Namespace = "TUnit.TestProject", + Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", () => new global::TUnit.Core.AssemblyMetadata { Name = "TestsBase`1" }), + Parameters = global::System.Array.Empty(), + Properties = global::System.Array.Empty(), + Parent = null + }; + // Set ClassMetadata and ContainingTypeMetadata references on properties to avoid circular dependency + foreach (var prop in classMetadata.Properties) + { + prop.ClassMetadata = classMetadata; + prop.ContainingTypeMetadata = classMetadata; + } + return classMetadata; + }) + }, + InstanceFactory = (typeArgs, args) => new global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests(), + TestInvoker = async (instance, args) => + { + var typedInstance = (global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests)instance; + var context = global::TUnit.Core.TestContext.Current; + await typedInstance.VerifyInheritedCategoriesAreAvailable(); + }, + InvokeTypedTest = async (instance, args, cancellationToken) => + { + await instance.VerifyInheritedCategoriesAreAvailable(); + }, + }; + metadata.UseRuntimeDataGeneration(testSessionId); + yield return metadata; + yield break; + } +} +internal static class InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_ModuleInitializer_GUID +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + public static void Initialize() + { + global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), new InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_TestSource_GUID()); + } +} + + // ===== FILE SEPARATOR ===== // @@ -383,8 +485,10 @@ internal sealed class InheritedTestsFromDifferentProjectTests_BaseTest_TestSourc AttributeFactory = () => [ new global::TUnit.Core.TestAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategory"), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -461,6 +565,109 @@ internal static class InheritedTestsFromDifferentProjectTests_BaseTest_ModuleIni } +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable + +// +#pragma warning disable +#nullable enable +namespace TUnit.Generated; +internal sealed class InheritedTestsFromDifferentProjectTests_BaseTestWithMultipleCategories_TestSource_GUID : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource +{ + public async global::System.Collections.Generic.IAsyncEnumerable GetTestsAsync(string testSessionId, [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) + { + var metadata = new global::TUnit.Core.TestMetadata + { + TestName = "BaseTestWithMultipleCategories", + TestClassType = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TestMethodName = "BaseTestWithMultipleCategories", + Dependencies = global::System.Array.Empty(), + AttributeFactory = () => + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.Core.CategoryAttribute("AnotherBaseCategory"), + new global::TUnit.Core.CategoryAttribute("MultipleCategories"), + new global::TUnit.TestProject.Attributes.EngineTest(Pass), + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") + ], + DataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + PropertyDataSources = new global::TUnit.Core.PropertyDataSource[] + { + }, + PropertyInjections = new global::TUnit.Core.PropertyInjectionData[] + { + }, + InheritanceDepth = 1, + FilePath = @"", + LineNumber = 5, + MethodMetadata = new global::TUnit.Core.MethodMetadata + { + Type = typeof(global::TUnit.TestProject.Library.BaseTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.Library.BaseTests, TestsBase`1"), + Name = "BaseTestWithMultipleCategories", + GenericTypeCount = 0, + ReturnType = typeof(void), + ReturnTypeReference = global::TUnit.Core.TypeReference.CreateConcrete("void, System.Private.CoreLib"), + Parameters = global::System.Array.Empty(), + Class = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests", () => + { + var classMetadata = new global::TUnit.Core.ClassMetadata + { + Type = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.InheritedTestsFromDifferentProjectTests, TestsBase`1"), + Name = "InheritedTestsFromDifferentProjectTests", + Namespace = "TUnit.TestProject", + Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", () => new global::TUnit.Core.AssemblyMetadata { Name = "TestsBase`1" }), + Parameters = global::System.Array.Empty(), + Properties = global::System.Array.Empty(), + Parent = null + }; + // Set ClassMetadata and ContainingTypeMetadata references on properties to avoid circular dependency + foreach (var prop in classMetadata.Properties) + { + prop.ClassMetadata = classMetadata; + prop.ContainingTypeMetadata = classMetadata; + } + return classMetadata; + }) + }, + InstanceFactory = (typeArgs, args) => new global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests(), + TestInvoker = async (instance, args) => + { + var typedInstance = (global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests)instance; + var context = global::TUnit.Core.TestContext.Current; + typedInstance.BaseTestWithMultipleCategories(); + await global::System.Threading.Tasks.Task.CompletedTask; + }, + InvokeTypedTest = async (instance, args, cancellationToken) => + { + instance.BaseTestWithMultipleCategories(); + await global::System.Threading.Tasks.Task.CompletedTask; + }, + }; + metadata.UseRuntimeDataGeneration(testSessionId); + yield return metadata; + yield break; + } +} +internal static class InheritedTestsFromDifferentProjectTests_BaseTestWithMultipleCategories_ModuleInitializer_GUID +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + public static void Initialize() + { + global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), new InheritedTestsFromDifferentProjectTests_BaseTestWithMultipleCategories_TestSource_GUID()); + } +} + + // ===== FILE SEPARATOR ===== // @@ -484,7 +691,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_Test_TestSource_GU [ new global::TUnit.Core.TestAttribute(), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -585,7 +793,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_GenericMethodDataS new global::TUnit.Core.TestAttribute(), new global::TUnit.Core.MethodDataSourceAttribute("Foo"), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -712,7 +921,8 @@ internal sealed class InheritedTestsFromDifferentProjectTests_NonGenericMethodDa new global::TUnit.Core.TestAttribute(), new global::TUnit.Core.MethodDataSourceAttribute(typeof(global::TUnit.TestProject.TestData), "Foo"), new global::TUnit.TestProject.Attributes.EngineTest(Pass), - new global::TUnit.Core.InheritsTestsAttribute() + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") ], DataSources = new global::TUnit.Core.IDataSourceAttribute[] { @@ -824,3 +1034,102 @@ internal static class InheritedTestsFromDifferentProjectTests_NonGenericMethodDa global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), new InheritedTestsFromDifferentProjectTests_NonGenericMethodDataSource_TestSource_GUID()); } } + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable + +// +#pragma warning disable +#nullable enable +namespace TUnit.Generated; +internal sealed class InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_TestSource_GUID : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource +{ + public async global::System.Collections.Generic.IAsyncEnumerable GetTestsAsync(string testSessionId, [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default) + { + var metadata = new global::TUnit.Core.TestMetadata + { + TestName = "VerifyInheritedCategoriesAreAvailable", + TestClassType = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TestMethodName = "VerifyInheritedCategoriesAreAvailable", + Dependencies = global::System.Array.Empty(), + AttributeFactory = () => + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.TestProject.Attributes.EngineTest(Pass), + new global::TUnit.Core.InheritsTestsAttribute(), + new global::TUnit.Core.CategoryAttribute("BaseCategoriesOnClass") + ], + DataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[] + { + }, + PropertyDataSources = new global::TUnit.Core.PropertyDataSource[] + { + }, + PropertyInjections = new global::TUnit.Core.PropertyInjectionData[] + { + }, + InheritanceDepth = 0, + FilePath = @"", + LineNumber = 5, + MethodMetadata = new global::TUnit.Core.MethodMetadata + { + Type = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.InheritedTestsFromDifferentProjectTests, TestsBase`1"), + Name = "VerifyInheritedCategoriesAreAvailable", + GenericTypeCount = 0, + ReturnType = typeof(global::System.Threading.Tasks.Task), + ReturnTypeReference = global::TUnit.Core.TypeReference.CreateConcrete("System.Threading.Tasks.Task, System.Private.CoreLib"), + Parameters = global::System.Array.Empty(), + Class = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests", () => + { + var classMetadata = new global::TUnit.Core.ClassMetadata + { + Type = typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), + TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.InheritedTestsFromDifferentProjectTests, TestsBase`1"), + Name = "InheritedTestsFromDifferentProjectTests", + Namespace = "TUnit.TestProject", + Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", () => new global::TUnit.Core.AssemblyMetadata { Name = "TestsBase`1" }), + Parameters = global::System.Array.Empty(), + Properties = global::System.Array.Empty(), + Parent = null + }; + // Set ClassMetadata and ContainingTypeMetadata references on properties to avoid circular dependency + foreach (var prop in classMetadata.Properties) + { + prop.ClassMetadata = classMetadata; + prop.ContainingTypeMetadata = classMetadata; + } + return classMetadata; + }) + }, + InstanceFactory = (typeArgs, args) => new global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests(), + TestInvoker = async (instance, args) => + { + var typedInstance = (global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests)instance; + var context = global::TUnit.Core.TestContext.Current; + await typedInstance.VerifyInheritedCategoriesAreAvailable(); + }, + InvokeTypedTest = async (instance, args, cancellationToken) => + { + await instance.VerifyInheritedCategoriesAreAvailable(); + }, + }; + metadata.UseRuntimeDataGeneration(testSessionId); + yield return metadata; + yield break; + } +} +internal static class InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_ModuleInitializer_GUID +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + public static void Initialize() + { + global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.InheritedTestsFromDifferentProjectTests), new InheritedTestsFromDifferentProjectTests_VerifyInheritedCategoriesAreAvailable_TestSource_GUID()); + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.cs b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.cs index ce844fc540..3b8feb102f 100644 --- a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.cs @@ -26,5 +26,18 @@ public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName, }, async generatedFiles => { - }); + // Verify that inherited test methods have their categories properly included + var generatedCode = string.Join(Environment.NewLine, generatedFiles); + + // Check that the BaseTest method has the BaseCategory attribute + await Assert.That(generatedCode).Contains("new global::TUnit.Core.CategoryAttribute(\"BaseCategory\")"); + + // Check that the BaseTestWithMultipleCategories method has both category attributes + await Assert.That(generatedCode).Contains("new global::TUnit.Core.CategoryAttribute(\"AnotherBaseCategory\")"); + await Assert.That(generatedCode).Contains("new global::TUnit.Core.CategoryAttribute(\"MultipleCategories\")"); + + // Verify that the generated code includes the inherited test methods + await Assert.That(generatedCode).Contains("BaseTest"); + await Assert.That(generatedCode).Contains("BaseTestWithMultipleCategories"); + }); } diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs index f6a256e76b..ba95f94019 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs @@ -11,18 +11,33 @@ public class AttributeWriter public static void WriteAttributes(ICodeWriter sourceCodeWriter, Compilation compilation, ImmutableArray attributeDatas) { - for (var index = 0; index < attributeDatas.Length; index++) + var attributesToWrite = new List(); + + // Filter out attributes that we can write + foreach (var attributeData in attributeDatas) { - var attributeData = attributeDatas[index]; - - if (attributeData.ApplicationSyntaxReference is null) + // Include attributes with syntax reference (from current compilation) + // Include attributes without syntax reference (from other assemblies) as long as they have an AttributeClass + if (attributeData.ApplicationSyntaxReference is not null || attributeData.AttributeClass is not null) { - continue; + // Skip framework-specific attributes when targeting older frameworks + // We determine this by checking if we can compile the attribute + if (ShouldSkipFrameworkSpecificAttribute(compilation, attributeData)) + { + continue; + } + + attributesToWrite.Add(attributeData); } + } + + for (var index = 0; index < attributesToWrite.Count; index++) + { + var attributeData = attributesToWrite[index]; WriteAttribute(sourceCodeWriter, compilation, attributeData); - if (index != attributeDatas.Length - 1) + if (index != attributesToWrite.Count - 1) { sourceCodeWriter.AppendLine(","); } @@ -32,7 +47,17 @@ public static void WriteAttributes(ICodeWriter sourceCodeWriter, Compilation com public static void WriteAttribute(ICodeWriter sourceCodeWriter, Compilation compilation, AttributeData attributeData) { - sourceCodeWriter.Append(GetAttributeObjectInitializer(compilation, attributeData)); + if (attributeData.ApplicationSyntaxReference is null) + { + // For attributes from other assemblies (like inherited methods), + // use the WriteAttributeWithoutSyntax approach + WriteAttributeWithoutSyntax(sourceCodeWriter, attributeData); + } + else + { + // For attributes from the current compilation, use the syntax-based approach + sourceCodeWriter.Append(GetAttributeObjectInitializer(compilation, attributeData)); + } } public static void WriteAttributeMetadata(ICodeWriter sourceCodeWriter, Compilation compilation, @@ -212,14 +237,119 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att sourceCodeWriter.Append($"new {attributeName}({formattedConstructorArgs})"); - if (string.IsNullOrEmpty(formattedNamedArgs)) + // Check if we need to add properties (named arguments or data generator properties) + var hasNamedArgs = !string.IsNullOrEmpty(formattedNamedArgs); + var hasDataGeneratorProperties = HasNestedDataGeneratorProperties(attributeData); + + if (!hasNamedArgs && !hasDataGeneratorProperties) { return; } sourceCodeWriter.AppendLine(); sourceCodeWriter.Append("{"); - sourceCodeWriter.Append($"{formattedNamedArgs}"); + + if (hasNamedArgs) + { + sourceCodeWriter.Append($"{formattedNamedArgs}"); + if (hasDataGeneratorProperties) + { + sourceCodeWriter.Append(","); + } + } + + if (hasDataGeneratorProperties) + { + // For attributes without syntax, we still need to handle data generator properties + // but we can't rely on syntax analysis, so we'll use a simpler approach + WriteDataSourceGeneratorPropertiesWithoutSyntax(sourceCodeWriter, attributeData); + } + sourceCodeWriter.Append("}"); } + + private static void WriteDataSourceGeneratorPropertiesWithoutSyntax(ICodeWriter sourceCodeWriter, AttributeData attributeData) + { + foreach (var propertySymbol in attributeData.AttributeClass?.GetMembers().OfType() ?? []) + { + if (propertySymbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + if (propertySymbol.GetAttributes().FirstOrDefault(x => x.IsDataSourceAttribute()) is not { } dataSourceAttribute) + { + continue; + } + + sourceCodeWriter.Append($"{propertySymbol.Name} = "); + + var propertyType = propertySymbol.Type.GloballyQualified(); + var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated; + + if (propertySymbol.Type.IsReferenceType && !isNullable) + { + sourceCodeWriter.Append("null!,"); + } + else if (propertySymbol.Type.IsValueType && !isNullable) + { + sourceCodeWriter.Append($"default({propertyType}),"); + } + else + { + sourceCodeWriter.Append("null,"); + } + } + } + + private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation, AttributeData attributeData) + { + if (attributeData.AttributeClass == null) + { + return false; + } + + // Generic approach: Check if the attribute type is actually available in the target compilation + // This works by seeing if we can resolve the type from the compilation's references + var fullyQualifiedName = attributeData.AttributeClass.ToDisplayString(); + + // Check if this is a system/runtime attribute that might not exist on all frameworks + if (fullyQualifiedName.StartsWith("System.") || fullyQualifiedName.StartsWith("Microsoft.")) + { + // Try to get the type from the compilation + // If it doesn't exist in the compilation's references, we should skip it + var typeSymbol = compilation.GetTypeByMetadataName(fullyQualifiedName); + + // If the type doesn't exist in the compilation, skip it + if (typeSymbol == null) + { + return true; + } + + // Special handling for attributes that exist but may not be usable + // For example, nullable attributes exist in the reference assemblies but not at runtime for .NET Framework + if (IsNullableAttribute(fullyQualifiedName)) + { + // Check if we're targeting .NET Framework by looking at references + var isNetFramework = compilation.References.Any(r => + r.Display?.Contains("mscorlib") == true && + !r.Display.Contains("System.Runtime")); + + if (isNetFramework) + { + return true; // Skip nullable attributes on .NET Framework + } + } + } + + return false; + } + + private static bool IsNullableAttribute(string fullyQualifiedName) + { + return fullyQualifiedName.Contains("NullableAttribute") || + fullyQualifiedName.Contains("NullableContextAttribute") || + fullyQualifiedName.Contains("NullablePublicOnlyAttribute"); + } + } diff --git a/TUnit.TestProject.Library/BaseTests.cs b/TUnit.TestProject.Library/BaseTests.cs index a26d13fb9e..750243ef7f 100644 --- a/TUnit.TestProject.Library/BaseTests.cs +++ b/TUnit.TestProject.Library/BaseTests.cs @@ -1,9 +1,18 @@ namespace TUnit.TestProject.Library; +[Category("BaseCategoriesOnClass")] public abstract class BaseTests { [Test] + [Category("BaseCategory")] public void BaseTest() { } + + [Test] + [Category("AnotherBaseCategory")] + [Category("MultipleCategories")] + public void BaseTestWithMultipleCategories() + { + } } diff --git a/TUnit.TestProject/Bugs/1914/AsyncHookTests.cs b/TUnit.TestProject/Bugs/1914/AsyncHookTests.cs index 33d768fbf0..57e4ef93e4 100644 --- a/TUnit.TestProject/Bugs/1914/AsyncHookTests.cs +++ b/TUnit.TestProject/Bugs/1914/AsyncHookTests.cs @@ -47,6 +47,9 @@ public static async Task BeforeTestDiscovery2(BeforeTestDiscoveryContext context [Before(TestSession)] public static async Task BeforeTestSession(TestSessionContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTestSession"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -62,6 +65,9 @@ await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscov [Before(TestSession)] public static async Task BeforeTestSession2(TestSessionContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTestSession"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -77,6 +83,9 @@ await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscov [Before(Assembly)] public static async Task BeforeAssembly(AssemblyHookContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeAssembly"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -97,6 +106,9 @@ await Assert.That(_1BeforeTestSessionLocal2.Value).IsEqualTo("BeforeTestSession2 [Before(Assembly)] public static async Task BeforeAssembly2(AssemblyHookContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeAssembly"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -117,6 +129,9 @@ await Assert.That(_1BeforeTestSessionLocal2.Value).IsEqualTo("BeforeTestSession2 [Before(Class)] public static async Task BeforeClass(ClassHookContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeClass"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -142,6 +157,9 @@ await Assert.That(_2BeforeAssemblyLocal2.Value).IsEqualTo("BeforeAssembly2") [Before(Class)] public static async Task BeforeClass2(ClassHookContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeClass"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -167,6 +185,9 @@ await Assert.That(_2BeforeAssemblyLocal2.Value).IsEqualTo("BeforeAssembly2") [Before(Test)] public async Task BeforeTest(TestContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTest"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -197,6 +218,9 @@ await Assert.That(_3BeforeClassLocal2.Value).IsEqualTo("BeforeClass2") [Before(Test)] public async Task BeforeTest2(TestContext context) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTest"); await Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -236,6 +260,9 @@ await Assert.That(_3BeforeClassLocal2.Value).IsEqualTo("BeforeClass2") [Arguments(8)] public async Task TestAsyncLocal(int i) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery"); await Assert.That(_1BeforeTestSessionLocal.Value).IsEqualTo("BeforeTestSession"); await Assert.That(_2BeforeAssemblyLocal.Value).IsEqualTo("BeforeAssembly"); diff --git a/TUnit.TestProject/Bugs/1914/SyncHookTests.cs b/TUnit.TestProject/Bugs/1914/SyncHookTests.cs index 89d91adc10..7659b728c8 100644 --- a/TUnit.TestProject/Bugs/1914/SyncHookTests.cs +++ b/TUnit.TestProject/Bugs/1914/SyncHookTests.cs @@ -46,6 +46,9 @@ public static void BeforeTestDiscovery2(BeforeTestDiscoveryContext context) [Before(TestSession)] public static void BeforeTestSession(TestSessionContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTestSession").GetAwaiter().GetResult(); Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -60,6 +63,9 @@ public static void BeforeTestSession(TestSessionContext context) [Before(TestSession)] public static void BeforeTestSession2(TestSessionContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTestSession").GetAwaiter().GetResult(); Assert.That(_0BeforeTestDiscoveryLocal2.Value).IsEqualTo("BeforeTestDiscovery2") @@ -74,6 +80,9 @@ public static void BeforeTestSession2(TestSessionContext context) [Before(Assembly)] public static void BeforeAssembly(AssemblyHookContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeAssembly") .GetAwaiter().GetResult(); @@ -97,6 +106,9 @@ public static void BeforeAssembly(AssemblyHookContext context) [Before(Assembly)] public static void BeforeAssembly2(AssemblyHookContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeAssembly") .GetAwaiter().GetResult(); @@ -120,6 +132,9 @@ public static void BeforeAssembly2(AssemblyHookContext context) [Before(Class)] public static void BeforeClass(ClassHookContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeClass") .GetAwaiter().GetResult(); @@ -150,6 +165,9 @@ public static void BeforeClass(ClassHookContext context) [Before(Class)] public static void BeforeClass2(ClassHookContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeClass") .GetAwaiter().GetResult(); @@ -180,6 +198,9 @@ public static void BeforeClass2(ClassHookContext context) [Before(Test)] public void BeforeTest(TestContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTest") .GetAwaiter().GetResult(); @@ -217,6 +238,9 @@ public void BeforeTest(TestContext context) [Before(Test)] public void BeforeTest2(TestContext context) { +#if !NET + return; +#endif Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery") .Because("AsyncLocal should flow from BeforeTestDiscovery to BeforeTest") .GetAwaiter().GetResult(); @@ -263,6 +287,9 @@ public void BeforeTest2(TestContext context) [Arguments(8)] public async Task TestAsyncLocal(int i) { +#if !NET + return; +#endif await Assert.That(_0BeforeTestDiscoveryLocal.Value).IsEqualTo("BeforeTestDiscovery"); await Assert.That(_1BeforeTestSessionLocal.Value).IsEqualTo("BeforeTestSession"); await Assert.That(_2BeforeAssemblyLocal.Value).IsEqualTo("BeforeAssembly"); diff --git a/TUnit.TestProject/InheritedCategoryTestValidation.cs b/TUnit.TestProject/InheritedCategoryTestValidation.cs new file mode 100644 index 0000000000..2d65d822d9 --- /dev/null +++ b/TUnit.TestProject/InheritedCategoryTestValidation.cs @@ -0,0 +1,18 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject; + +[EngineTest(ExpectedResult.Pass)] +[InheritsTests] +public class InheritedCategoryTestValidation : Library.BaseTests +{ + [Test] + [Category("TestCategory")] + public async Task TestInheritedMultipleCategoriesMethod() + { + // This test verifies that the class inherits the BaseCategoriesOnClass category from the base class + // and has its own TestCategory + await Assert.That(TestContext.Current!.TestDetails.Categories).Contains("BaseCategoriesOnClass"); + await Assert.That(TestContext.Current!.TestDetails.Categories).Contains("TestCategory"); + } +} diff --git a/TUnit.TestProject/InheritedTestsFromDifferentProjectTests.cs b/TUnit.TestProject/InheritedTestsFromDifferentProjectTests.cs index c6f9c02a00..9a347004f8 100644 --- a/TUnit.TestProject/InheritedTestsFromDifferentProjectTests.cs +++ b/TUnit.TestProject/InheritedTestsFromDifferentProjectTests.cs @@ -22,4 +22,11 @@ public void GenericMethodDataSource(string value) public void NonGenericMethodDataSource(string value) { } + + [Test] + public async Task VerifyInheritedCategoriesAreAvailable() + { + var categories = TestContext.Current?.TestDetails.Categories; + await Assert.That(categories).Contains("BaseCategoriesOnClass"); + } }