Skip to content

Commit 25b06e9

Browse files
authored
feat(nunit): parameterized fixtures support (#601)
1 parent 85dacc7 commit 25b06e9

File tree

4 files changed

+145
-50
lines changed

4 files changed

+145
-50
lines changed

Allure.NUnit/Core/AllureNUnitHelper.cs

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Linq;
45
using System.Text;
56
using Allure.Net.Commons;
@@ -18,6 +19,14 @@ namespace Allure.NUnit.Core
1819
{
1920
sealed class AllureNUnitHelper
2021
{
22+
static Dictionary<Type, string> LiteralSuffixes { get; } = new()
23+
{
24+
{ typeof(uint), "u" },
25+
{ typeof(long), "L" },
26+
{ typeof(ulong), "UL" },
27+
{ typeof(float), "f" },
28+
};
29+
2130
private readonly ITest _test;
2231

2332
internal AllureNUnitHelper(ITest test)
@@ -96,7 +105,7 @@ internal static TestResult CreateTestResult(ITest test)
96105
var testResult = new TestResult
97106
{
98107
name = ResolveDisplayName(test),
99-
titlePath = EnumerateNamesFromTestFixtureToRoot(test).Reverse().ToList(),
108+
titlePath = [.. EnumerateTitlePathElements(test)],
100109
labels = [
101110
Label.Thread(),
102111
Label.Host(),
@@ -142,16 +151,23 @@ static string ResolveDisplayName(ITest test) =>
142151
_ => test.Name,
143152
};
144153

145-
static IEnumerable<string> EnumerateNamesFromTestFixtureToRoot(ITest test)
154+
static IEnumerable<ITest> EnumerateTestElements(ITest test)
146155
{
147-
for (ITest suite = GetTestFixture(test); suite is not null; suite = suite.Parent)
148-
yield return suite switch
149-
{
150-
TestAssembly a => a.Assembly?.GetName()?.Name ?? a.Name,
151-
_ => suite.Name,
152-
};
156+
Stack<ITest> stack = [];
157+
for (; test is not null; test = test.Parent)
158+
{
159+
stack.Push(test);
160+
}
161+
return stack;
153162
}
154163

164+
static IEnumerable<string> EnumerateTitlePathElements(ITest test) =>
165+
EnumerateTestElements(test).Skip(1).Select(suite => suite switch
166+
{
167+
TestAssembly a => a.Assembly?.GetName()?.Name ?? a.Name,
168+
_ => suite.Name,
169+
});
170+
155171
TestResultContainer CreateTestContainer() =>
156172
new()
157173
{
@@ -175,9 +191,62 @@ static void SetIdentifiers(ITest test, TestResult testResult)
175191
}
176192

177193
testResult.uuid = IdFunctions.CreateUUID();
178-
testResult.fullName = IdFunctions.CreateFullName(
179-
test.Method.MethodInfo
180-
);
194+
testResult.fullName = CreateFullName(test);
195+
}
196+
197+
/// <summary>
198+
/// For test fixtures with no parameters, returns the same value as
199+
/// <see cref="IdFunctions.CreateFullName(System.Reflection.MethodInfo)"/>.
200+
/// For test fixtures with parameters, inserts the arguments between the class and method IDs.
201+
/// </summary>
202+
static string CreateFullName(ITest test)
203+
{
204+
var testMethod = test.Method.MethodInfo;
205+
var testFixtureClass = testMethod.DeclaringType;
206+
var testFixtureArgs = GetTestFixture(test).Arguments;
207+
208+
var testFixtureClassPart = IdFunctions.GetTypeId(testFixtureClass);
209+
var testFixtureArgsPart = testFixtureArgs.Any()
210+
? $"({string.Join(",", testFixtureArgs.Select(FormatTestFixtureArg))})"
211+
: "";
212+
var methodPart = IdFunctions.GetMethodId(testMethod);
213+
214+
215+
return $"{testFixtureClassPart}{testFixtureArgsPart}.{methodPart}";
216+
}
217+
218+
/// <summary>
219+
/// Converts a test fixture argument to a string. Doesn't depend on the
220+
/// currently installed locale.
221+
/// </summary>
222+
/// <remarks>
223+
/// For possible values and types, see <see href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/attributes#2324-attribute-parameter-types">here</see>.
224+
/// </remarks>
225+
static string FormatTestFixtureArg(object value) => value switch
226+
{
227+
null => "null",
228+
string text => FormatFunctions.Format(text),
229+
Type type => $"<{IdFunctions.GetTypeId(type)}>",
230+
Array array => FormatArray(array),
231+
char c => FormatChar(c),
232+
_ => FormatPrimitive(value),
233+
};
234+
235+
static string FormatArray(Array array) =>
236+
$"[{string.Join(",", array.Cast<object>().Select(FormatTestFixtureArg))}]";
237+
238+
static string FormatChar(char c)
239+
{
240+
var text = FormatFunctions.Format(c);
241+
return $"'{text.Substring(1, text.Length - 2)}'";
242+
}
243+
244+
static string FormatPrimitive(object value)
245+
{
246+
var text = Convert.ToString(value, CultureInfo.InvariantCulture);
247+
return LiteralSuffixes.TryGetValue(value.GetType(), out var suffix)
248+
? $"{text}{suffix}"
249+
: text;
181250
}
182251

183252
static void SetLegacyIdentifiers(ITest test, TestResult testResult)

Allure.Net.Commons.Tests/FunctionTests/IdTests.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,22 @@ public void TestUUIDGeneration()
6666
public void TestFullNameFromClass(Type targetClass, string expectedFullName)
6767
{
6868
Assert.That(
69-
IdFunctions.CreateFullName(targetClass),
69+
IdFunctions.GetTypeId(targetClass),
7070
Is.EqualTo(expectedFullName)
7171
);
7272
}
7373

74+
[Test]
75+
public void TestIdOfGenericTypeParameter()
76+
{
77+
Assert.That(
78+
IdFunctions.GetTypeId(
79+
typeof(MyClass<>).GetGenericArguments()[0]
80+
),
81+
Is.EqualTo("T")
82+
);
83+
}
84+
7485
class MyClass
7586
{
7687
internal void ParameterlessMethod() { }
@@ -162,8 +173,12 @@ public void FullNameFromMethod(string methodName, string expectedFullName)
162173
);
163174

164175
var actualFullName = IdFunctions.CreateFullName(method);
176+
var declaringTypeId = IdFunctions.GetTypeId(method.DeclaringType);
177+
var methodId = IdFunctions.GetMethodId(method);
165178

166179
Assert.That(actualFullName, Is.EqualTo(expectedFullName));
180+
Assert.That(actualFullName, Does.StartWith(declaringTypeId));
181+
Assert.That(actualFullName, Does.EndWith(methodId));
167182
}
168183

169184
[Test]

Allure.Net.Commons/Functions/FormatFunctions.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,19 @@ public static class FormatFunctions
2525
/// </summary>
2626
public static string Format(object? value)
2727
{
28-
return Format(value, new Dictionary<Type, ITypeFormatter>());
28+
if (value is null)
29+
{
30+
return "null";
31+
}
32+
33+
try
34+
{
35+
return JsonConvert.SerializeObject(value, SerializerSettings);
36+
}
37+
catch
38+
{
39+
return value.ToString();
40+
}
2941
}
3042

3143
/// <summary>
@@ -47,18 +59,6 @@ IReadOnlyDictionary<Type, ITypeFormatter> formatters
4759
return formatter.Format(value);
4860
}
4961

50-
if (value is null)
51-
{
52-
return "null";
53-
}
54-
55-
try
56-
{
57-
return JsonConvert.SerializeObject(value, SerializerSettings);
58-
}
59-
catch
60-
{
61-
return value.ToString();
62-
}
62+
return Format(value);
6363
}
6464
}

Allure.Net.Commons/Functions/IdFunctions.cs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,44 +70,64 @@ static IEnumerable<string> ExpandNestness(Type type)
7070
/// Unlike <see cref="Type.ToString"/>, it includes assembly names to
7171
/// the name of the type and its type arguments, which prevents collisions
7272
/// in some scenarios.
73+
/// <br/>
74+
/// For a generic parameters (like <c>T</c> in <c>class Foo&lt;T> { }</c>), returns just its name.
7375
/// </remarks>
74-
public static string CreateFullName(Type type) =>
75-
SerializeNonParameterType(type);
76+
public static string GetTypeId(Type type) =>
77+
type.IsGenericParameter ? type.Name : SerializeNonParameterType(type);
7678

7779
/// <summary>
78-
/// Creates a string that unuquely identifies a given method.
80+
/// Creates a name that uniquely identifies a given method in its declaring type.
7981
/// </summary>
8082
/// <param name="method">
8183
/// A method.
8284
/// If it's a constructed generic method, its generic definition is used instead.
8385
/// </param>
8486
/// <remarks>
85-
/// For a given test method the full name includes:
87+
/// For a given test method, its id includes:
8688
/// <list type="bullet">
87-
/// <item>assembly name</item>
88-
/// <item>namespace (if any)</item>
89-
/// <item>name of type (including its declaring types, if any)</item>
90-
/// <item>type parameters of the declaring type (for generic type definitions)</item>
91-
/// <item>type arguments of the declaring type (for constructed generic types)</item>
92-
/// <item>type parameters of the method (if any)</item>
93-
/// <item>parameter types</item>
89+
/// <item>name</item>
90+
/// <item>type parameters (like <c>T</c> in <c>void Foo&lt;T>() { }</c>)</item>
91+
/// <item>parameter types (like <c>System.String</c> in <c>void Foo(string bar) { }</c>)</item>
9492
/// </list>
9593
/// </remarks>
96-
public static string CreateFullName(MethodInfo method)
94+
public static string GetMethodId(MethodInfo method)
9795
{
9896
if (method.IsGenericMethod && !method.IsGenericMethodDefinition)
9997
{
10098
method = method.GetGenericMethodDefinition();
10199
}
102100

103-
var className = SerializeType(method.DeclaringType);
104101
var methodName = method.Name;
105102
var typeParameters = method.GetGenericArguments();
106103
var typeParametersDecl = SerializeTypeParameterTypeList(typeParameters);
107104
var parameterTypes = SerializeParameterTypes(method.GetParameters());
108-
return $"{className}.{methodName}{typeParametersDecl}({parameterTypes})";
105+
return $"{methodName}{typeParametersDecl}({parameterTypes})";
109106
}
110107

108+
/// <summary>
109+
/// Creates a string that unuquely identifies a given method.
110+
/// </summary>
111+
/// <param name="method">
112+
/// A method.
113+
/// If it's a constructed generic method, its generic definition is used instead.
114+
/// </param>
115+
/// <remarks>
116+
/// For a given test method the full name includes:
117+
/// <list type="bullet">
118+
/// <item>assembly name</item>
119+
/// <item>namespace (if any)</item>
120+
/// <item>name of type (including its declaring types, if any)</item>
121+
/// <item>type parameters of the declaring type (for generic type definitions)</item>
122+
/// <item>type arguments of the declaring type (for constructed generic types)</item>
123+
/// <item>the method's name</item>
124+
/// <item>type parameters of the method (if any)</item>
125+
/// <item>parameter types</item>
126+
/// </list>
127+
/// </remarks>
128+
public static string CreateFullName(MethodInfo method) =>
129+
$"{GetTypeId(method.DeclaringType)}.{GetMethodId(method)}";
130+
111131
/// <summary>
112132
/// Creates a testCaseId value. testCaseId has a fixed length and depends
113133
/// only on a given fullName. The fullName shouldn't depend on test parameters.
@@ -178,18 +198,9 @@ IEnumerable<Type> types
178198
) =>
179199
string.Join(
180200
",",
181-
types.Select(SerializeType)
201+
types.Select(GetTypeId)
182202
);
183203

184-
static string SerializeType(Type type)
185-
{
186-
if (type.IsGenericParameter)
187-
{
188-
return type.Name;
189-
}
190-
return SerializeNonParameterType(type);
191-
}
192-
193204
static string SerializeNonParameterType(Type type) =>
194205
GetUniqueTypeName(type) + SerializeTypeParameterTypeList(
195206
type.GetGenericArguments()

0 commit comments

Comments
 (0)