Skip to content

Commit 81d1146

Browse files
committed
refactor(formatting): escape dots in strings to prevent namespace interpretation in VS Test Explorer
1 parent c74d6a7 commit 81d1146

8 files changed

+96
-4
lines changed

TUnit.Core.Tests/Helpers/ArgumentFormatterTests.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,39 @@ public void FormatDefault_LargeTuple_FormatsAllElements()
5858
public void FormatDefault_TupleWithNull_HandlesNullCorrectly()
5959
{
6060
var tuple = (1, null, "test");
61-
61+
6262
var result = ArgumentFormatter.Format(tuple, []);
63-
63+
6464
Assert.That(result, Is.EqualTo("(1, null, test)"));
6565
}
66+
67+
[Test]
68+
public void FormatDefault_StringWithDots_EscapesDotsWithMiddleDot()
69+
{
70+
var stringWithDots = "1.2.3";
71+
72+
var result = ArgumentFormatter.Format(stringWithDots, []);
73+
74+
Assert.That(result, Is.EqualTo("1·2·3"));
75+
}
76+
77+
[Test]
78+
public void FormatDefault_StringWithoutDots_ReturnsUnchanged()
79+
{
80+
var stringWithoutDots = "hello world";
81+
82+
var result = ArgumentFormatter.Format(stringWithoutDots, []);
83+
84+
Assert.That(result, Is.EqualTo("hello world"));
85+
}
86+
87+
[Test]
88+
public void FormatArguments_WithStringsContainingDots_EscapesDots()
89+
{
90+
var args = new object?[] { "hello", "with.dot", "1.2.3" };
91+
92+
var result = ArgumentFormatter.FormatArguments(args);
93+
94+
Assert.That(result, Is.EqualTo("hello, with·dot, 1·2·3"));
95+
}
6696
}

TUnit.Core/Attributes/TestData/ClassDataSources.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,24 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
7373

7474
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, DataGeneratorMetadata dataGeneratorMetadata)
7575
{
76+
return CreateWithNestedDependencies(type, dataGeneratorMetadata, recursionDepth: 0);
77+
}
78+
79+
private const int MaxRecursionDepth = 10;
80+
81+
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' requirements",
82+
Justification = "PropertyType from PropertyInjectionMetadata has the required DynamicallyAccessedMembers annotations")]
83+
private static object CreateWithNestedDependencies([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth)
84+
{
85+
if (recursionDepth >= MaxRecursionDepth)
86+
{
87+
throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency.");
88+
}
89+
90+
object instance;
7691
try
7792
{
78-
return Activator.CreateInstance(type)!;
93+
instance = Activator.CreateInstance(type)!;
7994
}
8095
catch (TargetInvocationException targetInvocationException)
8196
{
@@ -86,5 +101,21 @@ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemb
86101

87102
throw;
88103
}
104+
105+
// Populate nested ClassDataSource properties recursively
106+
var propertySource = PropertySourceRegistry.GetSource(type);
107+
if (propertySource?.ShouldInitialize == true)
108+
{
109+
var propertyMetadata = propertySource.GetPropertyMetadata();
110+
foreach (var metadata in propertyMetadata)
111+
{
112+
// Recursively create the property value using CreateWithNestedDependencies
113+
// This will handle nested ClassDataSource properties
114+
var propertyValue = CreateWithNestedDependencies(metadata.PropertyType, dataGeneratorMetadata, recursionDepth + 1);
115+
metadata.SetProperty(instance, propertyValue);
116+
}
117+
}
118+
119+
return instance;
89120
}
90121
}

TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,10 @@ private bool IsExcluded(object?[] exclusion, IEnumerable<object?> row)
175175
var enumValues = Enum.GetValuesAsUnderlyingType(resolvedType)
176176
.Cast<object?>();
177177
#else
178+
#pragma warning disable IL3050 // Enum.GetValues is used for test data generation at discovery time, not in AOT scenarios
178179
var enumValues = Enum.GetValues(resolvedType)
179180
.Cast<object?>();
181+
#pragma warning restore IL3050
180182
#endif
181183
if (isNullable)
182184
{

TUnit.Core/Helpers/ArgumentFormatter.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,18 @@ private static string FormatDefault(object? o)
6363
return toString;
6464
}
6565

66-
if (type.IsPrimitive || o is string)
66+
if (type.IsPrimitive)
6767
{
6868
return toString;
6969
}
7070

71+
if (o is string str)
72+
{
73+
// Replace dots with middle dot (·) to prevent VS Test Explorer from interpreting them as namespace separators
74+
// Only do this if the string contains dots, to avoid unnecessary allocations
75+
return str.Contains('.') ? str.Replace(".", "·") : str;
76+
}
77+
7178
if (toString == type.FullName || toString == type.AssemblyQualifiedName)
7279
{
7380
return type.Name;

TUnit.Core/Helpers/CastHelper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace TUnit.Core.Helpers;
1111
public static class CastHelper
1212
{
1313
[MethodImpl(MethodImplOptions.AggressiveInlining)]
14+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
15+
Justification = "Array.CreateInstance is used for test data generation at discovery time, not in AOT-compiled test execution.")]
1416
public static T? Cast<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T>(object? value)
1517
{
1618
if (value is null)
@@ -180,6 +182,8 @@ public static class CastHelper
180182
}
181183

182184
[MethodImpl(MethodImplOptions.AggressiveInlining)]
185+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
186+
Justification = "Array.CreateInstance is used for test data generation at discovery time, not in AOT-compiled test execution.")]
183187
public static object? Cast([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, object? value)
184188
{
185189
if (value is null)

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,7 +1943,13 @@ namespace .Helpers
19431943
"have matching annotations.")]
19441944
public static class CastHelper
19451945
{
1946+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1947+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1948+
"OT-compiled test execution.")]
19461949
public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { }
1950+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1951+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1952+
"OT-compiled test execution.")]
19471953
public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { }
19481954
public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { }
19491955
}

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,7 +1943,13 @@ namespace .Helpers
19431943
"have matching annotations.")]
19441944
public static class CastHelper
19451945
{
1946+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1947+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1948+
"OT-compiled test execution.")]
19461949
public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { }
1950+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1951+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1952+
"OT-compiled test execution.")]
19471953
public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { }
19481954
public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { }
19491955
}

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,7 +1943,13 @@ namespace .Helpers
19431943
"have matching annotations.")]
19441944
public static class CastHelper
19451945
{
1946+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1947+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1948+
"OT-compiled test execution.")]
19461949
public static object? Cast([.(..None | ..PublicMethods | ..NonPublicMethods)] type, object? value) { }
1950+
[.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" +
1951+
"nctionality when AOT compiling.", Justification=" is used for test data generation at discovery time, not in A" +
1952+
"OT-compiled test execution.")]
19471953
public static T? Cast<[.(..None | ..PublicMethods | ..NonPublicMethods)] T>(object? value) { }
19481954
public static .MethodInfo? GetConversionMethod([.(..None | ..PublicMethods | ..NonPublicMethods)] baseType, [.(..None | ..PublicMethods | ..NonPublicMethods)] targetType) { }
19491955
}

0 commit comments

Comments
 (0)