From dd4bbf3b24e8ac9923aee624d11f75f9fc8209cf Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:38:41 +0700 Subject: [PATCH 1/3] refactor(commons): tiny optimization of Format --- .../Functions/FormatFunctions.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Allure.Net.Commons/Functions/FormatFunctions.cs b/Allure.Net.Commons/Functions/FormatFunctions.cs index 420ac8f3..69d90d7a 100644 --- a/Allure.Net.Commons/Functions/FormatFunctions.cs +++ b/Allure.Net.Commons/Functions/FormatFunctions.cs @@ -25,7 +25,19 @@ public static class FormatFunctions /// public static string Format(object? value) { - return Format(value, new Dictionary()); + if (value is null) + { + return "null"; + } + + try + { + return JsonConvert.SerializeObject(value, SerializerSettings); + } + catch + { + return value.ToString(); + } } /// @@ -47,18 +59,6 @@ IReadOnlyDictionary formatters return formatter.Format(value); } - if (value is null) - { - return "null"; - } - - try - { - return JsonConvert.SerializeObject(value, SerializerSettings); - } - catch - { - return value.ToString(); - } + return Format(value); } } \ No newline at end of file From 82cd902276f2e6856388563ae12b7ad62b4aa19b Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:40:38 +0700 Subject: [PATCH 2/3] feat(commons): add separate methods for type and method fullName parts --- .../FunctionTests/IdTests.cs | 17 +++++- Allure.Net.Commons/Functions/IdFunctions.cs | 59 +++++++++++-------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/Allure.Net.Commons.Tests/FunctionTests/IdTests.cs b/Allure.Net.Commons.Tests/FunctionTests/IdTests.cs index 045f6eb7..8f81ac35 100644 --- a/Allure.Net.Commons.Tests/FunctionTests/IdTests.cs +++ b/Allure.Net.Commons.Tests/FunctionTests/IdTests.cs @@ -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() { } @@ -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] diff --git a/Allure.Net.Commons/Functions/IdFunctions.cs b/Allure.Net.Commons/Functions/IdFunctions.cs index 611ba8b8..49767d1f 100644 --- a/Allure.Net.Commons/Functions/IdFunctions.cs +++ b/Allure.Net.Commons/Functions/IdFunctions.cs @@ -70,44 +70,64 @@ static IEnumerable ExpandNestness(Type type) /// Unlike , it includes assembly names to /// the name of the type and its type arguments, which prevents collisions /// in some scenarios. + ///
+ /// For a generic parameters (like T in class Foo<T> { }), returns just its name. /// - public static string CreateFullName(Type type) => - SerializeNonParameterType(type); + public static string GetTypeId(Type type) => + type.IsGenericParameter ? type.Name : SerializeNonParameterType(type); /// - /// Creates a string that unuquely identifies a given method. + /// Creates a name that uniquely identifies a given method in its declaring type. /// /// /// A method. /// If it's a constructed generic method, its generic definition is used instead. /// /// - /// For a given test method the full name includes: + /// For a given test method, its id includes: /// - /// assembly name - /// namespace (if any) - /// name of type (including its declaring types, if any) - /// type parameters of the declaring type (for generic type definitions) - /// type arguments of the declaring type (for constructed generic types) - /// type parameters of the method (if any) - /// parameter types + /// name + /// type parameters (like T in void Foo<T>() { }) + /// parameter types (like System.String in void Foo(string bar) { }) /// /// - 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})"; } + /// + /// Creates a string that unuquely identifies a given method. + /// + /// + /// A method. + /// If it's a constructed generic method, its generic definition is used instead. + /// + /// + /// For a given test method the full name includes: + /// + /// assembly name + /// namespace (if any) + /// name of type (including its declaring types, if any) + /// type parameters of the declaring type (for generic type definitions) + /// type arguments of the declaring type (for constructed generic types) + /// the method's name + /// type parameters of the method (if any) + /// parameter types + /// + /// + public static string CreateFullName(MethodInfo method) => + $"{GetTypeId(method.DeclaringType)}.{GetMethodId(method)}"; + /// /// Creates a testCaseId value. testCaseId has a fixed length and depends /// only on a given fullName. The fullName shouldn't depend on test parameters. @@ -178,18 +198,9 @@ IEnumerable 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() From 2f5676a7dcc5006073fa8c2c5cacdd336892ae4a Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:43:43 +0700 Subject: [PATCH 3/3] feat(nunit): add test fixture args to fullNames --- Allure.NUnit/Core/AllureNUnitHelper.cs | 91 ++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/Allure.NUnit/Core/AllureNUnitHelper.cs b/Allure.NUnit/Core/AllureNUnitHelper.cs index cb54a79e..e6b0eeb9 100644 --- a/Allure.NUnit/Core/AllureNUnitHelper.cs +++ b/Allure.NUnit/Core/AllureNUnitHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using Allure.Net.Commons; @@ -18,6 +19,14 @@ namespace Allure.NUnit.Core { sealed class AllureNUnitHelper { + static Dictionary LiteralSuffixes { get; } = new() + { + { typeof(uint), "u" }, + { typeof(long), "L" }, + { typeof(ulong), "UL" }, + { typeof(float), "f" }, + }; + private readonly ITest _test; internal AllureNUnitHelper(ITest test) @@ -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(), @@ -142,16 +151,23 @@ static string ResolveDisplayName(ITest test) => _ => test.Name, }; - static IEnumerable EnumerateNamesFromTestFixtureToRoot(ITest test) + static IEnumerable 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 stack = []; + for (; test is not null; test = test.Parent) + { + stack.Push(test); + } + return stack; } + static IEnumerable EnumerateTitlePathElements(ITest test) => + EnumerateTestElements(test).Skip(1).Select(suite => suite switch + { + TestAssembly a => a.Assembly?.GetName()?.Name ?? a.Name, + _ => suite.Name, + }); + TestResultContainer CreateTestContainer() => new() { @@ -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); + } + + /// + /// For test fixtures with no parameters, returns the same value as + /// . + /// For test fixtures with parameters, inserts the arguments between the class and method IDs. + /// + 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}"; + } + + /// + /// Converts a test fixture argument to a string. Doesn't depend on the + /// currently installed locale. + /// + /// + /// For possible values and types, see here. + /// + 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().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)