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) {