diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 5826707b2d..0f232f71d3 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -2341,11 +2341,83 @@ private static string GetDefaultValueString(IParameterSymbol parameter) } + private static bool IsMethodHiding(IMethodSymbol derivedMethod, IMethodSymbol baseMethod) + { + // Must have same name + if (derivedMethod.Name != baseMethod.Name) + { + return false; + } + + // Must NOT be an override (overrides are different from hiding) + if (derivedMethod.IsOverride) + { + return false; + } + + // Must have matching parameters + if (!ParametersMatch(derivedMethod.Parameters, baseMethod.Parameters)) + { + return false; + } + + // Derived method's containing type must be derived from base method's containing type + var derivedType = derivedMethod.ContainingType; + var baseType = baseMethod.ContainingType; + + // Can't hide yourself + if (SymbolEqualityComparer.Default.Equals(derivedType.OriginalDefinition, baseType.OriginalDefinition)) + { + return false; + } + + // Check if derived type inherits from base type + var current = derivedType.BaseType; + while (current is not null && current.SpecialType != SpecialType.System_Object) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, baseType.OriginalDefinition)) + { + return true; + } + current = current.BaseType; + } + + return false; + } + private static List CollectInheritedTestMethods(INamedTypeSymbol derivedClass) { - return derivedClass.GetMembersIncludingBase().OfType() + var allTestMethods = derivedClass.GetMembersIncludingBase() + .OfType() .Where(m => m.GetAttributes().Any(attr => attr.IsTestAttribute())) .ToList(); + + // Find methods declared directly on the derived class + var derivedClassMethods = allTestMethods + .Where(m => SymbolEqualityComparer.Default.Equals(m.ContainingType.OriginalDefinition, derivedClass.OriginalDefinition)) + .ToList(); + + // Filter out base methods that are hidden by derived class methods or declared directly on derived class + var result = new List(); + foreach (var method in allTestMethods) + { + // Skip methods declared directly on derived class + // (they're handled by regular test registration) + if (SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, derivedClass.OriginalDefinition)) + { + continue; + } + + // Check if this base method is hidden by any derived class method + var isHidden = derivedClassMethods.Any(derived => IsMethodHiding(derived, method)); + + if (!isHidden) + { + result.Add(method); + } + } + + return result; } private static IMethodSymbol? FindConcreteMethodImplementation(INamedTypeSymbol derivedClass, IMethodSymbol baseMethod) diff --git a/TUnit.TestProject/Bugs/3813/BaseServiceTests.cs b/TUnit.TestProject/Bugs/3813/BaseServiceTests.cs new file mode 100644 index 0000000000..f435689105 --- /dev/null +++ b/TUnit.TestProject/Bugs/3813/BaseServiceTests.cs @@ -0,0 +1,16 @@ +namespace TUnit.TestProject.Bugs._3813; + +public abstract class BaseServiceTests +{ + [Test] + public async Task BasicFeature() + { + await Task.CompletedTask; + } + + [Test] + public async Task AdvancedFeature() + { + await Task.CompletedTask; + } +} diff --git a/TUnit.TestProject/Bugs/3813/ImplementationATests.cs b/TUnit.TestProject/Bugs/3813/ImplementationATests.cs new file mode 100644 index 0000000000..75f2c52d8d --- /dev/null +++ b/TUnit.TestProject/Bugs/3813/ImplementationATests.cs @@ -0,0 +1,12 @@ +namespace TUnit.TestProject.Bugs._3813; + +[InheritsTests] +public class ImplementationATests : BaseServiceTests +{ + [Test] + [Skip("Implementation A does not support advanced feature")] + public new Task AdvancedFeature() + { + return Task.CompletedTask; + } +}