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

NUnit: Fix inconsistent one-time fixtures behavior (fixes #286, #374 and #375) #380

Merged
merged 3 commits into from
Sep 15, 2023
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
66 changes: 34 additions & 32 deletions Allure.NUnit.Examples/AllureAsyncOneTimeSetUpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,40 @@

namespace Allure.NUnit.Examples
{
[AllureSuite("Tests - Async OneTime SetUp")]
[Parallelizable(ParallelScope.All)]
public class AllureAsyncOneTimeSetUpTests: BaseTest
{
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
await AsyncStepsExamples.PrepareDough();
await AsyncStepsExamples.CookPizza();
await AsyncStepsExamples.CookPizza();
await AsyncStepsExamples.CookPizza();
}
[AllureSuite("Tests - Async OneTime SetUp")]
[Parallelizable(ParallelScope.All)]
public class AllureAsyncOneTimeSetUpTests: BaseTest
{
[OneTimeSetUp]
[AllureBefore]
public async Task OneTimeSetUp()
{
await AsyncStepsExamples.PrepareDough();
await AsyncStepsExamples.CookPizza();
await AsyncStepsExamples.CookPizza();
await AsyncStepsExamples.CookPizza();
}

[SetUp]
public async Task SetUp()
{
await AsyncStepsExamples.PrepareDough();
}
[SetUp]
[AllureBefore]
public async Task SetUp()
{
await AsyncStepsExamples.PrepareDough();
}

[Test]
[AllureName("Test1")]
public async Task Test1()
{
await AsyncStepsExamples.DeliverPizza();
await AsyncStepsExamples.Pay();
}
[Test]
[AllureName("Test2")]
public async Task Test2()
{
await AsyncStepsExamples.DeliverPizza();
}
}
[Test]
[AllureName("Test1")]
public async Task Test1()
{
await AsyncStepsExamples.DeliverPizza();
await AsyncStepsExamples.Pay();
}
[Test]
[AllureName("Test2")]
public async Task Test2()
{
await AsyncStepsExamples.DeliverPizza();
}
}
}
2 changes: 2 additions & 0 deletions Allure.NUnit.Examples/AllureSetUpTearDownTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ public void TearDown()
}

[OneTimeSetUp]
[AllureBefore("OneTimeSetUp AllureBefore attribute description")]
public void OneTimeSetUp()
{
Console.WriteLine("I'm an unwrapped OneTimeSetUp");
}

[OneTimeTearDown]
[AllureAfter("OneTimeTearDown AllureAfter attribute description")]
public void OneTimeTearDown()
{
Console.WriteLine("I'm an unwrapped OneTimeTearDown");
Expand Down
2 changes: 2 additions & 0 deletions Allure.NUnit/Attributes/AllureAfterAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Allure.Net.Commons.Steps;
using AspectInjector.Broker;
using NUnit.Allure.Core;

namespace NUnit.Allure.Attributes
{
[Injection(typeof(Internals.StopContainerAspect), Inherited = true)]
public class AllureAfterAttribute : AllureStepAttributes.AbstractAfterAttribute
{
public AllureAfterAttribute(string name = null) : base(name, AllureNUnitHelper.ExceptionTypes)
Expand Down
111 changes: 80 additions & 31 deletions Allure.NUnit/Core/AllureNUnitAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Allure.Net.Commons;
using NUnit.Allure.Attributes;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
Expand All @@ -20,50 +23,96 @@ public AllureNUnitAttribute()
{
}

public void BeforeTest(ITest test)
{
var helper = new AllureNUnitHelper(test);
_allureNUnitHelper.AddOrUpdate(
test.Id,
helper,
(key, existing) => helper
);

if (test.IsSuite)
{
helper.SaveOneTimeResultToContext();
StepsHelper.StopFixture();
}
else
public void BeforeTest(ITest test) =>
RunHookInRestoredAllureContext(test, () =>
{
helper.StartTestContainer();
helper.AddOneTimeSetupResult();
helper.StartTestCase();
}
}
var helper = new AllureNUnitHelper(test);
_allureNUnitHelper.AddOrUpdate(
test.Id,
helper,
(key, existing) => helper
);

public void AfterTest(ITest test)
{
if (_allureNUnitHelper.TryGetValue(test.Id, out var helper))
{
if (!test.IsSuite)
{
helper.StopTestCase();
helper.StartTestContainer(); // A container for SetUp/TearDown methods
helper.StartTestCase();
}
});

helper.StopTestContainer();
}
}
public void AfterTest(ITest test) =>
RunHookInRestoredAllureContext(test, () =>
{
if (_allureNUnitHelper.TryGetValue(test.Id, out var helper))
{
if (!test.IsSuite)
{
helper.StopTestCase();
helper.StopTestContainer();
}
else if (IsSuiteWithNoAfterFixtures(test))
{
// If a test fixture contains a OneTimeTearDown method
// with the [AllureAfter] attribute, the corresponding
// container is closed in StopContainerAspect instead.
helper.StopTestContainer();
}
}
});

public ActionTargets Targets =>
ActionTargets.Test | ActionTargets.Suite;

public void ApplyToContext(TestExecutionContext context)
{
var test = context.CurrentTest;
var helper = new AllureNUnitHelper(test);
helper.StartTestContainer();
StepsHelper.StartBeforeFixture($"fr-{test.Id}");
// A container for OneTimeSetUp/OneTimeTearDown methods.
new AllureNUnitHelper(test).StartTestContainer();
CaptureGlobalAllureContext(test);
}

static bool IsSuiteWithNoAfterFixtures(ITest test) =>
test is TestSuite suite && !suite.OneTimeTearDownMethods.Any(
m => IsDefined(m.MethodInfo, typeof(AllureAfterAttribute))
);

#region Allure context manipulation

/*
* The methods this region are to make sure the AllureContext
* flows into setup/teardown/test methods correctly. This is needed
* because NUnit might spread hooks of this class and user's code
* across unrelated threads, hiding changes made to the allure context
* in, say, BeforeTest from, say, a one-time tear down method.
*/

static void RunHookInRestoredAllureContext(ITest test, Action action)
{
RestoreAssociatedAllureContext(test);
try
{
action();
}
finally
{
CaptureGlobalAllureContext(test);
}
}

static void CaptureGlobalAllureContext(ITest test) =>
test.Properties.Set(ALLURE_CONTEXT_KEY, AllureLifecycle.Instance.Context);

static void RestoreAssociatedAllureContext(ITest test) =>
AllureLifecycle.Instance.RestoreContext(
GetAssociatedAllureContext(test)
);

static AllureContext GetAssociatedAllureContext(ITest test) =>
(AllureContext)test.Properties.Get(ALLURE_CONTEXT_KEY)
?? GetAssociatedAllureContext(test.Parent);

const string ALLURE_CONTEXT_KEY = "AllureContext";

#endregion
}
}
26 changes: 19 additions & 7 deletions Allure.NUnit/Core/AllureNUnitHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,34 @@ internal void StartTestCase()
Label.Thread(),
Label.Host(),
Label.Package(
_test.ClassName?.Substring(
0,
_test.ClassName.LastIndexOf('.')
)
GetNamespace(_test.ClassName)
),
Label.TestMethod(_test.MethodName),
Label.TestClass(
_test.ClassName?.Substring(
_test.ClassName.LastIndexOf('.') + 1
)
GetClassName(_test.ClassName)
)
}
};
AllureLifecycle.StartTestCase(testResult);
}

static string GetNamespace(string classFullName)
{
var lastDotIndex = classFullName?.LastIndexOf('.') ?? -1;
return lastDotIndex == -1 ? null : classFullName.Substring(
0,
lastDotIndex
);
}

static string GetClassName(string classFullName)
{
var lastDotIndex = classFullName?.LastIndexOf('.') ?? -1;
return lastDotIndex == -1 ? classFullName : classFullName.Substring(
lastDotIndex + 1
);
}

private TestFixture GetTestFixture(ITest test)
{
var currentTest = test;
Expand Down
96 changes: 96 additions & 0 deletions Allure.NUnit/Internals/StopContainerAspect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Allure.Net.Commons;
using AspectInjector.Broker;
using NUnit.Framework;
using NUnit.Framework.Internal;

namespace NUnit.Allure.Internals
{
[Aspect(Scope.Global)]
public class StopContainerAspect
{
[Advice(Kind.Around)]
public object StopTestContainerAfterTheLastOneTimeTearDown(
[Argument(Source.Target)] Func<object[], object> target,
[Argument(Source.Metadata)] MethodBase metadata
)
{
if (IsOneTimeTearDown(metadata))
{
CurrentTearDownCount++;
if (IsLastTearDown)
{
return CallAndStopContainer(target);
}
}
return target(Array.Empty<object>());
}

static object CallAndStopContainer(Func<object[], object> target)
{
object returnValue = null;
try
{
returnValue = target(Array.Empty<object>());
}
finally
{
if (returnValue is null)
{
StopContainer();
}
else
{
// This branch is executed only in case of an async one time tear down
returnValue = StopContainerAfterAsyncTearDown(returnValue);
}
}
return returnValue;
}

async static Task StopContainerAfterAsyncTearDown(object awaitable)
{
await ((Task)awaitable).ConfigureAwait(false);
StopContainer();
}

static void StopContainer()
{
AllureLifecycle.Instance.StopTestContainer();
AllureLifecycle.Instance.WriteTestContainer();
}

static bool IsOneTimeTearDown(MethodBase metadata) =>
Attribute.IsDefined(
metadata,
typeof(OneTimeTearDownAttribute)
);

static bool IsLastTearDown
{
get => CurrentTearDownCount == TotalTearDownCount;
}

static int CurrentTearDownCount
{
get => (int?) TestExecutionContext.CurrentContext
.CurrentTest.Properties.Get(CurrentTearDownKey) ?? 0;
set => TestExecutionContext.CurrentContext
.CurrentTest.Properties.Set(
CurrentTearDownKey,
value
);
}

static int TotalTearDownCount
{
get => (
(TestSuite)TestExecutionContext.CurrentContext.CurrentTest
).OneTimeTearDownMethods.Length;
}

const string CurrentTearDownKey = "CurrentTearDownCount";
}
}
Loading