From c0fd75e0ad9a465a5c524cdfcb1cff400ca0b17a Mon Sep 17 00:00:00 2001 From: Overlord Date: Tue, 27 Dec 2022 00:59:13 +0300 Subject: [PATCH 1/2] Add support for async NUnit tests #303 --- .editorconfig | 24 +++++++ .../AllureAsyncLifeCycleTests.cs | 68 ++++++++++++++++++ Allure.NUnit/Core/AllureExtensions.cs | 72 ++++++++++++++++++- Allure.NUnit/Core/AllureNUnitAttribute.cs | 25 +++++-- Allure.Net.Commons/AllureLifecycle.cs | 15 ++-- Allure.Net.Commons/Storage/AllureStorage.cs | 15 ++-- 6 files changed, 200 insertions(+), 19 deletions(-) create mode 100644 .editorconfig create mode 100644 Allure.NUnit.Examples/AllureAsyncLifeCycleTests.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..85625ab2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# indent_style : { "tab", "space" } +# indent_size : { an integer } +# tab_width : { optional positive integer (defaults "indent_size" when "indent_size" is a number) } +# end_of_line : { "lf", "cr", "crlf" } +# charset : { "latin1", "utf-8", "utf-16be", "utf-16le" } +# trim_trailing_whitespace : { "true", "false" } +# insert_final_newline : { "true", "false" } +# max_line_length : { positive integers } + +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = false + +[*.md] +trim_trailing_whitespace = false + +[*.{cs,csproj}] +charset = utf-8-bom +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/Allure.NUnit.Examples/AllureAsyncLifeCycleTests.cs b/Allure.NUnit.Examples/AllureAsyncLifeCycleTests.cs new file mode 100644 index 00000000..ceba4b95 --- /dev/null +++ b/Allure.NUnit.Examples/AllureAsyncLifeCycleTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Allure.Net.Commons; +using NUnit.Allure.Attributes; +using NUnit.Allure.Core; +using NUnit.Framework; + +namespace Allure.NUnit.Examples +{ + [AllureNUnit] + public class AllureAsyncLifeCycleTests + { + [Test] + public async Task AsyncTest_With_AllureStepAttribute() + { + Console.WriteLine("> Сalling async method with [AllureStep] ..."); + await StepWithAttribute(); + Console.WriteLine(" Done!"); + } + + [AllureStep("Calling StepWithAttribute")] + private async Task StepWithAttribute() + { + // switching thread on async + Console.WriteLine($" > Delay..."); + await Task.Delay(1000); + Console.WriteLine($" Done!"); + + // use internally allure steps storage on different thread + Console.WriteLine($" > AddAttachment..."); + AllureLifecycle.Instance.AddAttachment("attachment-name", "text/plain", Encoding.UTF8.GetBytes("attachment-value")); + Console.WriteLine($" Done!"); + } + + [Test] + public async Task AsyncTest_With_WrapInStep() + { + Console.WriteLine("> StepLevel1..."); + await AllureLifecycle.Instance.WrapInStepAsync( + async () => await StepLevel1() + ); + Console.WriteLine(" Done!"); + } + + private async Task StepLevel1() + { + Console.WriteLine(" > StepLevel2..."); + await AllureLifecycle.Instance.WrapInStepAsync( + async () => await StepLevel2() + ); + Console.WriteLine(" Done!"); + } + + private async Task StepLevel2() + { + // switching thread on async + Console.WriteLine($" > Sleep..."); + await Task.Delay(1000); + Console.WriteLine($" Done!"); + + // use internally allure steps storage on different thread + Console.WriteLine($" > AddAttachment..."); + AllureLifecycle.Instance.AddAttachment("attachment-name", "text/plain", Encoding.UTF8.GetBytes("attachment-value")); + Console.WriteLine($" Done!"); + } + } +} \ No newline at end of file diff --git a/Allure.NUnit/Core/AllureExtensions.cs b/Allure.NUnit/Core/AllureExtensions.cs index bae37647..49d36d80 100644 --- a/Allure.NUnit/Core/AllureExtensions.cs +++ b/Allure.NUnit/Core/AllureExtensions.cs @@ -4,6 +4,7 @@ using Allure.Net.Commons; using NUnit.Framework.Internal; using System.Linq; +using System.Threading.Tasks; namespace NUnit.Allure.Core { @@ -82,7 +83,7 @@ public static void WrapInStep(this AllureLifecycle lifecycle, Action action, str throw; } } - + /// /// Wraps Func into AllureStep. /// @@ -113,6 +114,75 @@ public static T WrapInStep(this AllureLifecycle lifecycle, Func func, stri } } + /// + /// Wraps async Action into AllureStep. + /// + public static async Task WrapInStepAsync( + this AllureLifecycle lifecycle, + Func action, + string stepName = "", + [CallerMemberName] string callerName = "" + ) + { + if (string.IsNullOrEmpty(stepName)) stepName = callerName; + + var id = Guid.NewGuid().ToString(); + var stepResult = new StepResult { name = stepName }; + try + { + lifecycle.StartStep(id, stepResult); + await action(); + lifecycle.StopStep(step => stepResult.status = Status.passed); + } + catch (Exception e) + { + lifecycle.StopStep(step => + { + step.statusDetails = new StatusDetails + { + message = e.Message, + trace = e.StackTrace + }; + step.status = AllureNUnitHelper.GetNUnitStatus(); + }); + throw; + } + } + + /// + /// Wraps async Func into AllureStep. + /// + public static async Task WrapInStepAsync( + this AllureLifecycle lifecycle, + Func> func, + string stepName = "", + [CallerMemberName] string callerName = "" + ) + { + if (string.IsNullOrEmpty(stepName)) stepName = callerName; + var id = Guid.NewGuid().ToString(); + var stepResult = new StepResult { name = stepName }; + try + { + lifecycle.StartStep(id, stepResult); + var result = await func(); + lifecycle.StopStep(step => stepResult.status = Status.passed); + return result; + } + catch (Exception e) + { + lifecycle.StopStep(step => + { + step.statusDetails = new StatusDetails + { + message = e.Message, + trace = e.StackTrace + }; + step.status = AllureNUnitHelper.GetNUnitStatus(); + }); + throw; + } + } /// /// AllureNUnit AddScreenDiff wrapper method. diff --git a/Allure.NUnit/Core/AllureNUnitAttribute.cs b/Allure.NUnit/Core/AllureNUnitAttribute.cs index a4fa67b7..f0352f42 100644 --- a/Allure.NUnit/Core/AllureNUnitAttribute.cs +++ b/Allure.NUnit/Core/AllureNUnitAttribute.cs @@ -1,5 +1,6 @@ using System; -using System.Threading; +using System.Collections.Concurrent; +using Allure.Net.Commons; using NUnit.Framework; using NUnit.Framework.Interfaces; @@ -8,9 +9,17 @@ namespace NUnit.Allure.Core [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)] public class AllureNUnitAttribute : PropertyAttribute, ITestAction { - private readonly ThreadLocal _allureNUnitHelper = new ThreadLocal(true); + private readonly ConcurrentDictionary _allureNUnitHelper = new ConcurrentDictionary(); private readonly bool _isWrappedIntoStep; + static AllureNUnitAttribute() + { + //!_! This is essential for async tests. + //!_! Async tests are working on different threads, so + //!_! default ManagedThreadId-separated behaviour in some cases fails on cross-thread execution. + AllureLifecycle.CurrentTestIdGetter = () => TestContext.CurrentContext.Test.FullName; + } + public AllureNUnitAttribute(bool wrapIntoStep = true) { _isWrappedIntoStep = wrapIntoStep; @@ -18,13 +27,19 @@ public AllureNUnitAttribute(bool wrapIntoStep = true) public void BeforeTest(ITest test) { - _allureNUnitHelper.Value = new AllureNUnitHelper(test); - _allureNUnitHelper.Value.StartAll(_isWrappedIntoStep); + var value = new AllureNUnitHelper(test); + + _allureNUnitHelper.AddOrUpdate(test.Id, value, (key, existing) => value); + + value.StartAll(_isWrappedIntoStep); } public void AfterTest(ITest test) { - _allureNUnitHelper.Value.StopAll(_isWrappedIntoStep); + if(_allureNUnitHelper.TryGetValue(test.Id, out var value)) + { + value.StopAll(_isWrappedIntoStep); + } } public ActionTargets Targets => ActionTargets.Test; diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 7862dbde..91ba8ed9 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -1,8 +1,9 @@ using System; using System.IO; using System.Runtime.CompilerServices; -using Allure.Net.Commons.Helpers; +using System.Threading; using Allure.Net.Commons.Configuration; +using Allure.Net.Commons.Helpers; using Allure.Net.Commons.Storage; using Allure.Net.Commons.Writer; using HeyRed.Mime; @@ -19,6 +20,9 @@ public class AllureLifecycle private readonly AllureStorage storage; private readonly IAllureResultsWriter writer; + /// Method to get the key for separation the steps for different tests. + public static Func CurrentTestIdGetter { get; set; } = () => Thread.CurrentThread.ManagedThreadId.ToString(); + internal AllureLifecycle(): this(GetConfiguration()) { } @@ -29,10 +33,6 @@ internal AllureLifecycle(JObject config) AllureConfiguration = AllureConfiguration.ReadFromJObject(config); writer = new FileSystemResultsWriter(AllureConfiguration); storage = new AllureStorage(); - lock (Lockobj) - { - instance = this; - } } public string JsonConfiguration { get; private set; } @@ -49,7 +49,10 @@ public static AllureLifecycle Instance lock (Lockobj) { if (instance == null) - new AllureLifecycle(); + { + var localInstance = new AllureLifecycle(); + Interlocked.Exchange(ref instance, localInstance); + } } } diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 1aab9242..ef016e5f 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; @@ -6,14 +7,14 @@ namespace Allure.Net.Commons.Storage { internal class AllureStorage { - private readonly ThreadLocal> stepContext = new ThreadLocal>(() => - { - return new LinkedList(); - }); + private readonly ConcurrentDictionary> stepContext = new(); - private readonly ConcurrentDictionary storage = new ConcurrentDictionary(); + private readonly ConcurrentDictionary storage = new(); - private LinkedList Steps => stepContext.Value; + private LinkedList Steps => stepContext.GetOrAdd( + AllureLifecycle.CurrentTestIdGetter(), + new LinkedList() + ); public T Get(string uuid) { From 70b4d4bffe6f7da582e9e79c495577d747ea6561 Mon Sep 17 00:00:00 2001 From: Nikolay Laptev Date: Fri, 30 Dec 2022 16:04:53 +0400 Subject: [PATCH 2/2] Delete .editorconfig --- .editorconfig | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 85625ab2..00000000 --- a/.editorconfig +++ /dev/null @@ -1,24 +0,0 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# indent_style : { "tab", "space" } -# indent_size : { an integer } -# tab_width : { optional positive integer (defaults "indent_size" when "indent_size" is a number) } -# end_of_line : { "lf", "cr", "crlf" } -# charset : { "latin1", "utf-8", "utf-16be", "utf-16le" } -# trim_trailing_whitespace : { "true", "false" } -# insert_final_newline : { "true", "false" } -# max_line_length : { positive integers } - -root = true - -[*] -trim_trailing_whitespace = true -insert_final_newline = false - -[*.md] -trim_trailing_whitespace = false - -[*.{cs,csproj}] -charset = utf-8-bom -indent_style = space -indent_size = 4 \ No newline at end of file