Skip to content

Commit

Permalink
NUnit: Fix inconsistent one-time fixtures behavior (fixes #286, #374
Browse files Browse the repository at this point in the history
…and #375) (#380)

* Fix inconsistent one-time fixtures behavior (fixes #374)

* Fix crash when running test from empty namespace (fixes #375)

* Fix indentation for AllureAsyncOneTimeSetUoTests.cs
  • Loading branch information
delatrie authored Sep 15, 2023
1 parent 18679f7 commit 8884955
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 70 deletions.
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

0 comments on commit 8884955

Please sign in to comment.