Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for async NUnit tests #314

Merged
merged 2 commits into from
Dec 30, 2022
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
68 changes: 68 additions & 0 deletions Allure.NUnit.Examples/AllureAsyncLifeCycleTests.cs
Original file line number Diff line number Diff line change
@@ -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!");
}
}
}
72 changes: 71 additions & 1 deletion Allure.NUnit/Core/AllureExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Allure.Net.Commons;
using NUnit.Framework.Internal;
using System.Linq;
using System.Threading.Tasks;

namespace NUnit.Allure.Core
{
Expand Down Expand Up @@ -82,7 +83,7 @@ public static void WrapInStep(this AllureLifecycle lifecycle, Action action, str
throw;
}
}

/// <summary>
/// Wraps Func into AllureStep.
/// </summary>
Expand Down Expand Up @@ -113,6 +114,75 @@ public static T WrapInStep<T>(this AllureLifecycle lifecycle, Func<T> func, stri
}
}

/// <summary>
/// Wraps async Action into AllureStep.
/// </summary>
public static async Task WrapInStepAsync(
this AllureLifecycle lifecycle,
Func<Task> 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;
}
}

/// <summary>
/// Wraps async Func into AllureStep.
/// </summary>
public static async Task<T> WrapInStepAsync<T>(
this AllureLifecycle lifecycle,
Func<Task<T>> 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;
}
}

/// <summary>
/// AllureNUnit AddScreenDiff wrapper method.
Expand Down
25 changes: 20 additions & 5 deletions Allure.NUnit/Core/AllureNUnitAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Threading;
using System.Collections.Concurrent;
using Allure.Net.Commons;
using NUnit.Framework;
using NUnit.Framework.Interfaces;

Expand All @@ -8,23 +9,37 @@ namespace NUnit.Allure.Core
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
public class AllureNUnitAttribute : PropertyAttribute, ITestAction
{
private readonly ThreadLocal<AllureNUnitHelper> _allureNUnitHelper = new ThreadLocal<AllureNUnitHelper>(true);
private readonly ConcurrentDictionary<string, AllureNUnitHelper> _allureNUnitHelper = new ConcurrentDictionary<string, AllureNUnitHelper>();
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;
}

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;
Expand Down
15 changes: 9 additions & 6 deletions Allure.Net.Commons/AllureLifecycle.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +20,9 @@ public class AllureLifecycle
private readonly AllureStorage storage;
private readonly IAllureResultsWriter writer;

/// <summary> Method to get the key for separation the steps for different tests. </summary>
public static Func<string> CurrentTestIdGetter { get; set; } = () => Thread.CurrentThread.ManagedThreadId.ToString();

internal AllureLifecycle(): this(GetConfiguration())
{
}
Expand All @@ -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; }
Expand All @@ -49,7 +49,10 @@ public static AllureLifecycle Instance
lock (Lockobj)
{
if (instance == null)
new AllureLifecycle();
{
var localInstance = new AllureLifecycle();
Interlocked.Exchange(ref instance, localInstance);
}
}
}

Expand Down
15 changes: 8 additions & 7 deletions Allure.Net.Commons/Storage/AllureStorage.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace Allure.Net.Commons.Storage
{
internal class AllureStorage
{
private readonly ThreadLocal<LinkedList<string>> stepContext = new ThreadLocal<LinkedList<string>>(() =>
{
return new LinkedList<string>();
});
private readonly ConcurrentDictionary<string, LinkedList<string>> stepContext = new();

private readonly ConcurrentDictionary<string, object> storage = new ConcurrentDictionary<string, object>();
private readonly ConcurrentDictionary<string, object> storage = new();

private LinkedList<string> Steps => stepContext.Value;
private LinkedList<string> Steps => stepContext.GetOrAdd(
AllureLifecycle.CurrentTestIdGetter(),
new LinkedList<string>()
);

public T Get<T>(string uuid)
{
Expand Down