Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 80 additions & 11 deletions Allure.NUnit/Core/AllureNUnitHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Allure.Net.Commons;
Expand All @@ -18,6 +19,14 @@ namespace Allure.NUnit.Core
{
sealed class AllureNUnitHelper
{
static Dictionary<Type, string> LiteralSuffixes { get; } = new()
{
{ typeof(uint), "u" },
{ typeof(long), "L" },
{ typeof(ulong), "UL" },
{ typeof(float), "f" },
};

private readonly ITest _test;

internal AllureNUnitHelper(ITest test)
Expand Down Expand Up @@ -96,7 +105,7 @@ internal static TestResult CreateTestResult(ITest test)
var testResult = new TestResult
{
name = ResolveDisplayName(test),
titlePath = EnumerateNamesFromTestFixtureToRoot(test).Reverse().ToList(),
titlePath = [.. EnumerateTitlePathElements(test)],
labels = [
Label.Thread(),
Label.Host(),
Expand Down Expand Up @@ -142,16 +151,23 @@ static string ResolveDisplayName(ITest test) =>
_ => test.Name,
};

static IEnumerable<string> EnumerateNamesFromTestFixtureToRoot(ITest test)
static IEnumerable<ITest> EnumerateTestElements(ITest test)
{
for (ITest suite = GetTestFixture(test); suite is not null; suite = suite.Parent)
yield return suite switch
{
TestAssembly a => a.Assembly?.GetName()?.Name ?? a.Name,
_ => suite.Name,
};
Stack<ITest> stack = [];
for (; test is not null; test = test.Parent)
{
stack.Push(test);
}
return stack;
}

static IEnumerable<string> EnumerateTitlePathElements(ITest test) =>
EnumerateTestElements(test).Skip(1).Select(suite => suite switch
{
TestAssembly a => a.Assembly?.GetName()?.Name ?? a.Name,
_ => suite.Name,
});

TestResultContainer CreateTestContainer() =>
new()
{
Expand All @@ -175,9 +191,62 @@ static void SetIdentifiers(ITest test, TestResult testResult)
}

testResult.uuid = IdFunctions.CreateUUID();
testResult.fullName = IdFunctions.CreateFullName(
test.Method.MethodInfo
);
testResult.fullName = CreateFullName(test);
}

/// <summary>
/// For test fixtures with no parameters, returns the same value as
/// <see cref="IdFunctions.CreateFullName(System.Reflection.MethodInfo)"/>.
/// For test fixtures with parameters, inserts the arguments between the class and method IDs.
/// </summary>
static string CreateFullName(ITest test)
{
var testMethod = test.Method.MethodInfo;
var testFixtureClass = testMethod.DeclaringType;
var testFixtureArgs = GetTestFixture(test).Arguments;

var testFixtureClassPart = IdFunctions.GetTypeId(testFixtureClass);
var testFixtureArgsPart = testFixtureArgs.Any()
? $"({string.Join(",", testFixtureArgs.Select(FormatTestFixtureArg))})"
: "";
var methodPart = IdFunctions.GetMethodId(testMethod);


return $"{testFixtureClassPart}{testFixtureArgsPart}.{methodPart}";
}

/// <summary>
/// Converts a test fixture argument to a string. Doesn't depend on the
/// currently installed locale.
/// </summary>
/// <remarks>
/// 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>.
/// </remarks>
static string FormatTestFixtureArg(object value) => value switch
{
null => "null",
string text => FormatFunctions.Format(text),
Type type => $"<{IdFunctions.GetTypeId(type)}>",
Array array => FormatArray(array),
char c => FormatChar(c),
_ => FormatPrimitive(value),
};

static string FormatArray(Array array) =>
$"[{string.Join(",", array.Cast<object>().Select(FormatTestFixtureArg))}]";

static string FormatChar(char c)
{
var text = FormatFunctions.Format(c);
return $"'{text.Substring(1, text.Length - 2)}'";
}

static string FormatPrimitive(object value)
{
var text = Convert.ToString(value, CultureInfo.InvariantCulture);
return LiteralSuffixes.TryGetValue(value.GetType(), out var suffix)
? $"{text}{suffix}"
: text;
}

static void SetLegacyIdentifiers(ITest test, TestResult testResult)
Expand Down
17 changes: 16 additions & 1 deletion Allure.Net.Commons.Tests/FunctionTests/IdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,22 @@ public void TestUUIDGeneration()
public void TestFullNameFromClass(Type targetClass, string expectedFullName)
{
Assert.That(
IdFunctions.CreateFullName(targetClass),
IdFunctions.GetTypeId(targetClass),
Is.EqualTo(expectedFullName)
);
}

[Test]
public void TestIdOfGenericTypeParameter()
{
Assert.That(
IdFunctions.GetTypeId(
typeof(MyClass<>).GetGenericArguments()[0]
),
Is.EqualTo("T")
);
}

class MyClass
{
internal void ParameterlessMethod() { }
Expand Down Expand Up @@ -162,8 +173,12 @@ public void FullNameFromMethod(string methodName, string expectedFullName)
);

var actualFullName = IdFunctions.CreateFullName(method);
var declaringTypeId = IdFunctions.GetTypeId(method.DeclaringType);
var methodId = IdFunctions.GetMethodId(method);

Assert.That(actualFullName, Is.EqualTo(expectedFullName));
Assert.That(actualFullName, Does.StartWith(declaringTypeId));
Assert.That(actualFullName, Does.EndWith(methodId));
}

[Test]
Expand Down
28 changes: 14 additions & 14 deletions Allure.Net.Commons/Functions/FormatFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@ public static class FormatFunctions
/// </summary>
public static string Format(object? value)
{
return Format(value, new Dictionary<Type, ITypeFormatter>());
if (value is null)
{
return "null";
}

try
{
return JsonConvert.SerializeObject(value, SerializerSettings);
}
catch
{
return value.ToString();
}
}

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

if (value is null)
{
return "null";
}

try
{
return JsonConvert.SerializeObject(value, SerializerSettings);
}
catch
{
return value.ToString();
}
return Format(value);
}
}
59 changes: 35 additions & 24 deletions Allure.Net.Commons/Functions/IdFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,44 +70,64 @@ static IEnumerable<string> ExpandNestness(Type type)
/// Unlike <see cref="Type.ToString"/>, it includes assembly names to
/// the name of the type and its type arguments, which prevents collisions
/// in some scenarios.
/// <br/>
/// For a generic parameters (like <c>T</c> in <c>class Foo&lt;T> { }</c>), returns just its name.
/// </remarks>
public static string CreateFullName(Type type) =>
SerializeNonParameterType(type);
public static string GetTypeId(Type type) =>
type.IsGenericParameter ? type.Name : SerializeNonParameterType(type);

/// <summary>
/// Creates a string that unuquely identifies a given method.
/// Creates a name that uniquely identifies a given method in its declaring type.
/// </summary>
/// <param name="method">
/// A method.
/// If it's a constructed generic method, its generic definition is used instead.
/// </param>
/// <remarks>
/// For a given test method the full name includes:
/// For a given test method, its id includes:
/// <list type="bullet">
/// <item>assembly name</item>
/// <item>namespace (if any)</item>
/// <item>name of type (including its declaring types, if any)</item>
/// <item>type parameters of the declaring type (for generic type definitions)</item>
/// <item>type arguments of the declaring type (for constructed generic types)</item>
/// <item>type parameters of the method (if any)</item>
/// <item>parameter types</item>
/// <item>name</item>
/// <item>type parameters (like <c>T</c> in <c>void Foo&lt;T>() { }</c>)</item>
/// <item>parameter types (like <c>System.String</c> in <c>void Foo(string bar) { }</c>)</item>
/// </list>
/// </remarks>
public static string CreateFullName(MethodInfo method)
public static string GetMethodId(MethodInfo method)
{
if (method.IsGenericMethod && !method.IsGenericMethodDefinition)
{
method = method.GetGenericMethodDefinition();
}

var className = SerializeType(method.DeclaringType);
var methodName = method.Name;
var typeParameters = method.GetGenericArguments();
var typeParametersDecl = SerializeTypeParameterTypeList(typeParameters);
var parameterTypes = SerializeParameterTypes(method.GetParameters());
return $"{className}.{methodName}{typeParametersDecl}({parameterTypes})";
return $"{methodName}{typeParametersDecl}({parameterTypes})";
}

/// <summary>
/// Creates a string that unuquely identifies a given method.
/// </summary>
/// <param name="method">
/// A method.
/// If it's a constructed generic method, its generic definition is used instead.
/// </param>
/// <remarks>
/// For a given test method the full name includes:
/// <list type="bullet">
/// <item>assembly name</item>
/// <item>namespace (if any)</item>
/// <item>name of type (including its declaring types, if any)</item>
/// <item>type parameters of the declaring type (for generic type definitions)</item>
/// <item>type arguments of the declaring type (for constructed generic types)</item>
/// <item>the method's name</item>
/// <item>type parameters of the method (if any)</item>
/// <item>parameter types</item>
/// </list>
/// </remarks>
public static string CreateFullName(MethodInfo method) =>
$"{GetTypeId(method.DeclaringType)}.{GetMethodId(method)}";

/// <summary>
/// Creates a testCaseId value. testCaseId has a fixed length and depends
/// only on a given fullName. The fullName shouldn't depend on test parameters.
Expand Down Expand Up @@ -178,18 +198,9 @@ IEnumerable<Type> types
) =>
string.Join(
",",
types.Select(SerializeType)
types.Select(GetTypeId)
);

static string SerializeType(Type type)
{
if (type.IsGenericParameter)
{
return type.Name;
}
return SerializeNonParameterType(type);
}

static string SerializeNonParameterType(Type type) =>
GetUniqueTypeName(type) + SerializeTypeParameterTypeList(
type.GetGenericArguments()
Expand Down