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