diff --git a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index 8f888742792..8b264099829 100644 --- a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -2321,13 +2321,22 @@ private static TestMetadata CreateFailedMethodGenericMetadata( FilePath = ExtractFilePath(method) ?? "Unknown", LineNumber = ExtractLineNumber(method) ?? 0, MethodMetadata = ReflectionMetadataBuilder.CreateMethodMetadata(type, method), - AttributeFactory = () => method.GetCustomAttributes().ToArray(), + AttributeFactory = () => GetFailedTestAttributes(method), DataSources = [], ClassDataSources = [], PropertyDataSources = [] }; } + /// + /// Attributes for a test whose discovery failed. Such a test has no producing data row, so the + /// row-scoped data sources (such as ) are dropped, the same way + /// TestBuilder.ScopeRowAttributesToTest drops the rows that did not produce a test. + /// Otherwise every row's Skip, DisplayName and Categories would be applied to the failed test (#7076). + /// + private static Attribute[] GetFailedTestAttributes(MethodInfo method) + => TestBuilder.ScopeRowAttributesToTest(method.GetCustomAttributes().ToArray(), null, null); + private static string? ExtractFilePath(MethodInfo method) { return method.GetCustomAttribute()?.File; @@ -2387,8 +2396,7 @@ private static TestMetadata CreateFailedTestMetadata( FilePath = ExtractFilePath(method) ?? "Unknown", LineNumber = ExtractLineNumber(method) ?? 0, MethodMetadata = ReflectionMetadataBuilder.CreateMethodMetadata(type, method), - AttributeFactory = () => method.GetCustomAttributes() - .ToArray(), + AttributeFactory = () => GetFailedTestAttributes(method), DataSources = [ ], @@ -3350,7 +3358,7 @@ private static TestMetadata CreateFailedTestMetadataForDynamicBuilder( FilePath = ExtractFilePath(method) ?? "Unknown", LineNumber = ExtractLineNumber(method) ?? 0, MethodMetadata = ReflectionMetadataBuilder.CreateMethodMetadata(type, method), - AttributeFactory = () => method.GetCustomAttributes().ToArray(), + AttributeFactory = () => GetFailedTestAttributes(method), DataSources = [], ClassDataSources = [], PropertyDataSources = [] diff --git a/tests/TUnit.Engine.Tests/Issue7076Tests.cs b/tests/TUnit.Engine.Tests/Issue7076Tests.cs new file mode 100644 index 00000000000..b9af5b386fe --- /dev/null +++ b/tests/TUnit.Engine.Tests/Issue7076Tests.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using Shouldly; +using TUnit.Core; +using TUnit.Core.Interfaces; +using TUnit.Engine.Discovery; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +public class Issue7076Tests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Arguments_Skip_Only_Skips_Its_Own_Row() + { + await RunTestsWithFilter( + "/*/*/SkippableArgumentTests/Test1", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(3), + result => result.ResultSummary.Counters.Passed.ShouldBe(2), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(1) + ]); + } + + [Test] + public async Task Arguments_DisplayName_Only_Applies_To_Its_Own_Row() + { + await RunTestsWithFilter( + "/*/*/ArgumentsRowSkipTests/DisplayNameOnlyAppliesToItsOwnRow", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(2), + result => result.ResultSummary.Counters.Passed.ShouldBe(2), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0) + ]); + } + + [Test] + public async Task ClassArguments_Skip_Only_Skips_Its_Own_Row() + { + await RunTestsWithFilter( + "/*/*/ClassArgumentsRowSkipTests/OnlyMarkedClassRowIsSkipped", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(2), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(1) + ]); + } + + [Test] + public async Task InheritedArguments_Skip_Only_Skips_Its_Own_Row() + { + await RunTestsWithFilter( + "/*/*/InheritedArgumentsRowSkipTests/OnlyMarkedInheritedRowIsSkipped", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(2), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(1) + ]); + } + + [Test] + public async Task ClassConstructor_Uses_Method_DataSource() + { + await RunTestsWithFilter( + "/*/*/ClassConstructorDataSourceTest/MethodDataSourceIsPassedToClassConstructor", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0) + ]); + } + + [Test] + public Task Failed_Metadata_Filters_Method_DataSources() + { + var method = typeof(FailedMetadataDataSourceTest).GetMethod(nameof(FailedMetadataDataSourceTest.Test))!; + var collectorType = typeof(ReflectionTestDataCollector); + var exception = new InvalidOperationException("discovery failed"); + var factories = new (string Name, object?[] Arguments)[] + { + ("CreateFailedTestMetadata", [typeof(FailedMetadataDataSourceTest), method, exception]), + ("CreateFailedMethodGenericMetadata", [typeof(FailedMetadataDataSourceTest), method, "generic resolution failed"]), + ("CreateFailedTestMetadataForDynamicBuilder", [typeof(FailedMetadataDataSourceTest), method, exception]) + }; + + foreach (var (name, arguments) in factories) + { + var factory = collectorType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static); + factory.ShouldNotBeNull(name); + var metadata = (TestMetadata)factory.Invoke(null, arguments)!; + metadata.AttributeFactory().OfType().ShouldBeEmpty(name); + } + + return Task.CompletedTask; + } + + private sealed class FailedMetadataDataSourceTest + { + [Arguments(1)] + [Arguments(2, Skip = "wip")] + public static void Test(int value) + { + } + } +} diff --git a/tests/TUnit.TestProject/Bugs/_7076/ArgumentsRowSkipTests.cs b/tests/TUnit.TestProject/Bugs/_7076/ArgumentsRowSkipTests.cs new file mode 100644 index 00000000000..81feb04af4b --- /dev/null +++ b/tests/TUnit.TestProject/Bugs/_7076/ArgumentsRowSkipTests.cs @@ -0,0 +1,81 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._7076; + +[EngineTest(ExpectedResult.Pass)] +public class ArgumentsRowSkipTests +{ + [Test] + [Arguments(1, DisplayName = "custom name for 1")] + [Arguments(2)] + public async Task DisplayNameOnlyAppliesToItsOwnRow(int value) + { + var displayName = TestContext.Current!.Metadata.DisplayName; + + if (value == 1) + { + await Assert.That(displayName).IsEqualTo("custom name for 1"); + } + else + { + await Assert.That(displayName).IsEqualTo("DisplayNameOnlyAppliesToItsOwnRow(2)"); + } + } +} + +[EngineTest(ExpectedResult.Pass)] +[Arguments("a")] +[Arguments("b", Skip = "skip only b")] +public class ClassArgumentsRowSkipTests(string value) +{ + [Test] + public async Task OnlyMarkedClassRowIsSkipped() + { + await Assert.That(value).IsEqualTo("a"); + } +} + +[EngineTest(ExpectedResult.Pass)] +[InheritsTests] +public class InheritedArgumentsRowSkipTests : InheritedArgumentsRowSkipBase; + +public abstract class InheritedArgumentsRowSkipBase +{ + [Test] + [Arguments(1)] + [Arguments(2, Skip = "skip only 2")] + public async Task OnlyMarkedInheritedRowIsSkipped(int value) + { + await Assert.That(value).IsEqualTo(1); + } +} + +[EngineTest(ExpectedResult.Pass)] +[ClassConstructor] +[ClassDataSource] +public class ClassConstructorDataSourceTest(ClassConstructorFixture fixture) +{ + [Test] + [Arguments(42)] + public async Task MethodDataSourceIsPassedToClassConstructor(int value) + { + await Assert.That(value).IsEqualTo(42); + } +} + +public sealed class ClassConstructorDataSourceProbe : IClassConstructor +{ + public Task Create(Type type, ClassConstructorMetadata classConstructorMetadata) + { + if (type == typeof(ClassConstructorDataSourceTest) && + classConstructorMetadata.TestBuilderContext.DataSourceAttribute is not ArgumentsAttribute) + { + throw new InvalidOperationException("The class constructor did not receive the method data source."); + } + + return Task.FromResult(Activator.CreateInstance(type, [new ClassConstructorFixture()])!); + } +} + +public sealed class ClassConstructorFixture; diff --git a/tests/TUnit.TestProject/TestVariantTests.cs b/tests/TUnit.TestProject/TestVariantTests.cs index 2bbe51ba087..587558056d5 100644 --- a/tests/TUnit.TestProject/TestVariantTests.cs +++ b/tests/TUnit.TestProject/TestVariantTests.cs @@ -68,6 +68,25 @@ public async Task CreateTestVariant_ShouldCreateVariantWithDifferentArguments(in // ValueTask→ source generator doesn't support this return type yet // void and ValueTask are covered by unit tests in ExpressionHelperTests. + [Test] + [Arguments(1, DisplayName = "one")] + public async Task CreateTestVariant_DoesNotInheritParentRowSettings(int value) + { + if (!TestContext.Current!.Dependencies.IsVariant()) + { + var variantInfo = await TestContext.Current.CreateTestVariant( + methodArguments: [2], + displayName: "variant"); + + if (variantInfo.DisplayName != "variant") + { + throw new InvalidOperationException($"Expected DisplayName 'variant' but got '{variantInfo.DisplayName}'"); + } + } + + await Assert.That(value).IsEqualTo(1).Or.IsEqualTo(2); + } + [Test] public async Task CreateTestVariant_FromTaskMethod() {