diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 125a72f85d..b5acc046c2 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -242,6 +242,15 @@ public string GetDisplayName() return _cachedDisplayName; } + /// + /// Clears the cached display name, forcing it to be recomputed on next access. + /// This is called after discovery event receivers run to ensure custom argument formatters are applied. + /// + internal void InvalidateDisplayNameCache() + { + _cachedDisplayName = null; + } + public Dictionary ObjectBag => _testBuilderContext.ObjectBag; public bool ReportResult { get; set; } = true; diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 0545329b93..11ed1b0075 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -802,6 +802,11 @@ public async Task BuildTestAsync(TestMetadata metadata, await InvokeDiscoveryEventReceiversAsync(context); + // Clear the cached display name after discovery events + // This ensures that ArgumentDisplayFormatterAttribute and similar attributes + // have a chance to register their formatters before the display name is finalized + context.InvalidateDisplayNameCache(); + return test; } diff --git a/TUnit.TestProject/ArgumentDisplayFormatterTests.cs b/TUnit.TestProject/ArgumentDisplayFormatterTests.cs new file mode 100644 index 0000000000..c02c7f0d14 --- /dev/null +++ b/TUnit.TestProject/ArgumentDisplayFormatterTests.cs @@ -0,0 +1,72 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject; + +[EngineTest(ExpectedResult.Pass)] +public class ArgumentDisplayFormatterTests +{ + [Test] + [MethodDataSource(nameof(Data1))] + [ArgumentDisplayFormatter] + public async Task FormatterShouldBeAppliedToMethodDataSource(Foo foo) + { + // Verify the formatter was applied by checking the display name + var displayName = TestContext.Current!.GetDisplayName(); + await Assert.That(displayName).IsEqualTo("FormatterShouldBeAppliedToMethodDataSource(FooFormatterValue)"); + } + + [Test] + [Arguments(1, 2, 3)] + [ArgumentDisplayFormatter] + public async Task FormatterShouldBeAppliedToArguments(int a, int b, int c) + { + // Verify the formatter was applied by checking the display name + var displayName = TestContext.Current!.GetDisplayName(); + await Assert.That(displayName).IsEqualTo("FormatterShouldBeAppliedToArguments(INT:1, INT:2, INT:3)"); + } + + [Test] + [MethodDataSource(nameof(DataWithException))] + [ArgumentDisplayFormatter] + public async Task FormatterShouldPreventExceptionInToString(Bar bar) + { + // The Bar.ToString() throws, but the formatter should prevent that + var displayName = TestContext.Current!.GetDisplayName(); + await Assert.That(displayName).IsEqualTo("FormatterShouldPreventExceptionInToString(BarFormatterValue)"); + } + + public static IEnumerable Data1() => [new Foo()]; + + public static IEnumerable DataWithException() => [new Bar()]; +} + +public class Foo +{ + public override string ToString() => throw new Exception("Foo.ToString should not be called"); +} + +public class Bar +{ + public override string ToString() => throw new Exception("Bar.ToString should not be called"); +} + +public class FooFormatter : ArgumentDisplayFormatter +{ + public override bool CanHandle(object? value) => value is Foo; + + public override string FormatValue(object? value) => "FooFormatterValue"; +} + +public class BarFormatter : ArgumentDisplayFormatter +{ + public override bool CanHandle(object? value) => value is Bar; + + public override string FormatValue(object? value) => "BarFormatterValue"; +} + +public class IntFormatter : ArgumentDisplayFormatter +{ + public override bool CanHandle(object? value) => value is int; + + public override string FormatValue(object? value) => $"INT:{value}"; +} diff --git a/global.json b/global.json index 2e2218a904..08c5374eff 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,8 @@ { "sdk": { - "version": "10.0.100-rc.1.25451.107", - "rollForward": "latestFeature" + "version": "9.0.305", + "rollForward": "latestMajor", + "allowPrerelease": true }, "test": { "runner": "Microsoft.Testing.Platform"