From 496347dc6d50c3897880916c5f824fe23616bff8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:14:35 +0000 Subject: [PATCH 1/4] Initial plan From 1f877ad177af9fa80dab16d6ff0624be294bbb9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:31:33 +0000 Subject: [PATCH 2/4] Use enum names instead of numeric values for test display names in MatrixDataSource Change MatrixDataSourceAttribute to convert underlying type values back to enum instances using Enum.ToObject(), so ArgumentFormatter.FormatDefault() correctly identifies them as enums and displays their names (e.g., "ToEven") instead of numeric values (e.g., "0"). Fixes: thomhurst/TUnit#4059 Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs index 187cc03604f..4e2b82982a8 100644 --- a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs @@ -207,7 +207,8 @@ private bool IsExcluded(object?[] exclusion, object?[] rowArray) if (resolvedType.IsEnum) { var enumValues = Enum.GetValuesAsUnderlyingType(resolvedType) - .Cast(); + .Cast() + .Select(v => v != null ? Enum.ToObject(resolvedType, v) : null); if (isNullable) { @@ -219,7 +220,7 @@ private bool IsExcluded(object?[] exclusion, object?[] rowArray) } return enumValues - .Except(matrixAttribute?.Excluding?.Select(e => Convert.ChangeType(e, Enum.GetUnderlyingType(resolvedType))) ?? []) + .Except(matrixAttribute?.Excluding?.Select(e => e != null ? (object?)Enum.ToObject(resolvedType, e) : null) ?? []) .ToArray(); } From d467e113e8506bd509afed630aefdb7b36d3fb97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:30:41 +0000 Subject: [PATCH 3/4] Move enum display name logic to ArgumentFormatter instead of changing parameter injection Revert MatrixDataSourceAttribute.cs to original (no changes to parameter injection/exclusion logic). Instead, add a parameter-type-aware Format overload to ArgumentFormatter that converts numeric values to enum names for display only. Updated callers: TestContext.GetDisplayName(), DisplayNameAttribute, DisplayNameSubstitutor. Updated PublicAPI snapshots for the new public method. Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- .../TestData/MatrixDataSourceAttribute.cs | 5 ++- .../TestMetadata/DisplayNameAttribute.cs | 4 +-- TUnit.Core/Helpers/ArgumentFormatter.cs | 36 +++++++++++++++++++ TUnit.Core/Helpers/DisplayNameSubstitutor.cs | 5 +-- TUnit.Core/TestContext.Metadata.cs | 4 ++- ...Has_No_API_Changes.DotNet10_0.verified.txt | 1 + ..._Has_No_API_Changes.DotNet8_0.verified.txt | 1 + ..._Has_No_API_Changes.DotNet9_0.verified.txt | 1 + 8 files changed, 49 insertions(+), 8 deletions(-) diff --git a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs index 4e2b82982a8..187cc03604f 100644 --- a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs @@ -207,8 +207,7 @@ private bool IsExcluded(object?[] exclusion, object?[] rowArray) if (resolvedType.IsEnum) { var enumValues = Enum.GetValuesAsUnderlyingType(resolvedType) - .Cast() - .Select(v => v != null ? Enum.ToObject(resolvedType, v) : null); + .Cast(); if (isNullable) { @@ -220,7 +219,7 @@ private bool IsExcluded(object?[] exclusion, object?[] rowArray) } return enumValues - .Except(matrixAttribute?.Excluding?.Select(e => e != null ? (object?)Enum.ToObject(resolvedType, e) : null) ?? []) + .Except(matrixAttribute?.Excluding?.Select(e => Convert.ChangeType(e, Enum.GetUnderlyingType(resolvedType))) ?? []) .ToArray(); } diff --git a/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs b/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs index 25e2e324200..f76f14f0f7e 100644 --- a/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs +++ b/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs @@ -55,7 +55,7 @@ protected override string FormatDisplayName(DiscoveredTestContext context) foreach (var parameter in methodParameters) { mutableDisplayName = mutableDisplayName.Replace($"${parameter.ParameterInfo.Name}", - ArgumentFormatter.Format(parameter.TestArgument, context.ArgumentDisplayFormatters)); + ArgumentFormatter.Format(parameter.TestArgument, parameter.ParameterInfo.Type, context.ArgumentDisplayFormatters)); } // If there are still placeholders and we have class parameters, try to substitute them @@ -71,7 +71,7 @@ protected override string FormatDisplayName(DiscoveredTestContext context) foreach (var parameter in classParameters) { mutableDisplayName = mutableDisplayName.Replace($"${parameter.ParameterInfo.Name}", - ArgumentFormatter.Format(parameter.TestArgument, context.ArgumentDisplayFormatters)); + ArgumentFormatter.Format(parameter.TestArgument, parameter.ParameterInfo.Type, context.ArgumentDisplayFormatters)); } } diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index b1db5a72a66..63560d92856 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -18,6 +18,20 @@ public static string Format(object? o, List> formatters) return FormatDefault(o); } + public static string Format(object? o, Type? parameterType, List> formatters) + { + foreach (var formatter in formatters) + { + var result = formatter(o); + if (result != null) + { + return result; + } + } + + return FormatDefault(o, parameterType); + } + public static string GetConstantValue(TestContext testContext, object? o) { return Format(o, testContext.ArgumentDisplayFormatters); @@ -48,6 +62,11 @@ public static string FormatArguments(IEnumerable arguments) } private static string FormatDefault(object? o) + { + return FormatDefault(o, parameterType: null); + } + + private static string FormatDefault(object? o, Type? parameterType) { if (o is null) { @@ -56,6 +75,23 @@ private static string FormatDefault(object? o) var type = o.GetType(); + // If the value is a numeric type but the parameter type is an enum, + // convert to the enum for display purposes (e.g., MatrixDataSource + // stores enum values as their underlying numeric type) + var resolvedParameterType = parameterType != null ? Nullable.GetUnderlyingType(parameterType) ?? parameterType : null; + if (resolvedParameterType is { IsEnum: true } && !type.IsEnum) + { + try + { + var enumValue = Enum.ToObject(resolvedParameterType, o); + return enumValue.ToString()!; + } + catch + { + // Fall through to default formatting + } + } + if (TupleHelper.IsTupleType(type)) { return FormatTuple(o); diff --git a/TUnit.Core/Helpers/DisplayNameSubstitutor.cs b/TUnit.Core/Helpers/DisplayNameSubstitutor.cs index bd5d843f4cc..39170c117e5 100644 --- a/TUnit.Core/Helpers/DisplayNameSubstitutor.cs +++ b/TUnit.Core/Helpers/DisplayNameSubstitutor.cs @@ -37,7 +37,7 @@ public static string Substitute( var placeholder = $"${paramName}"; if (result.Contains(placeholder)) { - var formatted = ArgumentFormatter.Format(arguments[i], effectiveFormatters); + var formatted = ArgumentFormatter.Format(arguments[i], parameters[i].Type, effectiveFormatters); result = result.Replace(placeholder, formatted); } } @@ -49,7 +49,8 @@ public static string Substitute( var placeholder = $"$arg{i + 1}"; if (result.Contains(placeholder)) { - var formatted = ArgumentFormatter.Format(arguments[i], effectiveFormatters); + var parameterType = i < parameters.Length ? parameters[i].Type : null; + var formatted = ArgumentFormatter.Format(arguments[i], parameterType, effectiveFormatters); result = result.Replace(placeholder, formatted); } } diff --git a/TUnit.Core/TestContext.Metadata.cs b/TUnit.Core/TestContext.Metadata.cs index c1ff9b961cb..61082d8f4aa 100644 --- a/TUnit.Core/TestContext.Metadata.cs +++ b/TUnit.Core/TestContext.Metadata.cs @@ -35,6 +35,7 @@ internal string GetDisplayName() } var argsLength = TestDetails.TestMethodArguments.Length; + var parameters = TestDetails.MethodMetadata.Parameters; var sb = StringBuilderPool.Get(); try { @@ -47,7 +48,8 @@ internal string GetDisplayName() { sb.Append(", "); } - sb.Append(ArgumentFormatter.Format(TestDetails.TestMethodArguments[i], ArgumentDisplayFormatters)); + var parameterType = i < parameters.Length ? parameters[i].Type : null; + sb.Append(ArgumentFormatter.Format(TestDetails.TestMethodArguments[i], parameterType, ArgumentDisplayFormatters)); } sb.Append(')'); diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 771e4b26fea..b7877e01ac9 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -2067,6 +2067,7 @@ namespace .Helpers public static class ArgumentFormatter { public static string Format(object? o, .<> formatters) { } + public static string Format(object? o, ? parameterType, .<> formatters) { } public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index d1fb7c0a3f1..e71ba56dfd1 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -2067,6 +2067,7 @@ namespace .Helpers public static class ArgumentFormatter { public static string Format(object? o, .<> formatters) { } + public static string Format(object? o, ? parameterType, .<> formatters) { } public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 0c1d2db1535..70ae813e0cd 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -2067,6 +2067,7 @@ namespace .Helpers public static class ArgumentFormatter { public static string Format(object? o, .<> formatters) { } + public static string Format(object? o, ? parameterType, .<> formatters) { } public static string FormatArguments(. arguments) { } public static string GetConstantValue(.TestContext testContext, object? o) { } } From 4782296025586e326885515fb7f5d080299169a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:31:55 +0000 Subject: [PATCH 4/4] Address review feedback: handle null ToString and catch specific exception type Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- TUnit.Core/Helpers/ArgumentFormatter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index 63560d92856..cc836bc2baf 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -84,11 +84,11 @@ private static string FormatDefault(object? o, Type? parameterType) try { var enumValue = Enum.ToObject(resolvedParameterType, o); - return enumValue.ToString()!; + return enumValue.ToString() ?? type.Name; } - catch + catch (ArgumentException) { - // Fall through to default formatting + // Value cannot be converted to the enum type - fall through to default formatting } }