Skip to content

Commit 8884955

Browse files
authored
NUnit: Fix inconsistent one-time fixtures behavior (fixes #286, #374 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
1 parent 18679f7 commit 8884955

File tree

7 files changed

+252
-70
lines changed

7 files changed

+252
-70
lines changed

Allure.NUnit.Examples/AllureAsyncOneTimeSetUpTests.cs

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,40 @@
55

66
namespace Allure.NUnit.Examples
77
{
8-
[AllureSuite("Tests - Async OneTime SetUp")]
9-
[Parallelizable(ParallelScope.All)]
10-
public class AllureAsyncOneTimeSetUpTests: BaseTest
11-
{
12-
[OneTimeSetUp]
13-
public async Task OneTimeSetUp()
14-
{
15-
await AsyncStepsExamples.PrepareDough();
16-
await AsyncStepsExamples.CookPizza();
17-
await AsyncStepsExamples.CookPizza();
18-
await AsyncStepsExamples.CookPizza();
19-
}
8+
[AllureSuite("Tests - Async OneTime SetUp")]
9+
[Parallelizable(ParallelScope.All)]
10+
public class AllureAsyncOneTimeSetUpTests: BaseTest
11+
{
12+
[OneTimeSetUp]
13+
[AllureBefore]
14+
public async Task OneTimeSetUp()
15+
{
16+
await AsyncStepsExamples.PrepareDough();
17+
await AsyncStepsExamples.CookPizza();
18+
await AsyncStepsExamples.CookPizza();
19+
await AsyncStepsExamples.CookPizza();
20+
}
2021

21-
[SetUp]
22-
public async Task SetUp()
23-
{
24-
await AsyncStepsExamples.PrepareDough();
25-
}
22+
[SetUp]
23+
[AllureBefore]
24+
public async Task SetUp()
25+
{
26+
await AsyncStepsExamples.PrepareDough();
27+
}
2628

27-
[Test]
28-
[AllureName("Test1")]
29-
public async Task Test1()
30-
{
31-
await AsyncStepsExamples.DeliverPizza();
32-
await AsyncStepsExamples.Pay();
33-
}
34-
35-
[Test]
36-
[AllureName("Test2")]
37-
public async Task Test2()
38-
{
39-
await AsyncStepsExamples.DeliverPizza();
40-
}
41-
}
29+
[Test]
30+
[AllureName("Test1")]
31+
public async Task Test1()
32+
{
33+
await AsyncStepsExamples.DeliverPizza();
34+
await AsyncStepsExamples.Pay();
35+
}
36+
37+
[Test]
38+
[AllureName("Test2")]
39+
public async Task Test2()
40+
{
41+
await AsyncStepsExamples.DeliverPizza();
42+
}
43+
}
4244
}

Allure.NUnit.Examples/AllureSetUpTearDownTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ public void TearDown()
3636
}
3737

3838
[OneTimeSetUp]
39+
[AllureBefore("OneTimeSetUp AllureBefore attribute description")]
3940
public void OneTimeSetUp()
4041
{
4142
Console.WriteLine("I'm an unwrapped OneTimeSetUp");
4243
}
4344

4445
[OneTimeTearDown]
46+
[AllureAfter("OneTimeTearDown AllureAfter attribute description")]
4547
public void OneTimeTearDown()
4648
{
4749
Console.WriteLine("I'm an unwrapped OneTimeTearDown");

Allure.NUnit/Attributes/AllureAfterAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using Allure.Net.Commons.Steps;
2+
using AspectInjector.Broker;
23
using NUnit.Allure.Core;
34

45
namespace NUnit.Allure.Attributes
56
{
7+
[Injection(typeof(Internals.StopContainerAspect), Inherited = true)]
68
public class AllureAfterAttribute : AllureStepAttributes.AbstractAfterAttribute
79
{
810
public AllureAfterAttribute(string name = null) : base(name, AllureNUnitHelper.ExceptionTypes)
Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
22
using System.Collections.Concurrent;
3+
using System.Linq;
4+
using Allure.Net.Commons;
5+
using NUnit.Allure.Attributes;
36
using NUnit.Framework;
47
using NUnit.Framework.Interfaces;
58
using NUnit.Framework.Internal;
@@ -20,50 +23,96 @@ public AllureNUnitAttribute()
2023
{
2124
}
2225

23-
public void BeforeTest(ITest test)
24-
{
25-
var helper = new AllureNUnitHelper(test);
26-
_allureNUnitHelper.AddOrUpdate(
27-
test.Id,
28-
helper,
29-
(key, existing) => helper
30-
);
31-
32-
if (test.IsSuite)
33-
{
34-
helper.SaveOneTimeResultToContext();
35-
StepsHelper.StopFixture();
36-
}
37-
else
26+
public void BeforeTest(ITest test) =>
27+
RunHookInRestoredAllureContext(test, () =>
3828
{
39-
helper.StartTestContainer();
40-
helper.AddOneTimeSetupResult();
41-
helper.StartTestCase();
42-
}
43-
}
29+
var helper = new AllureNUnitHelper(test);
30+
_allureNUnitHelper.AddOrUpdate(
31+
test.Id,
32+
helper,
33+
(key, existing) => helper
34+
);
4435

45-
public void AfterTest(ITest test)
46-
{
47-
if (_allureNUnitHelper.TryGetValue(test.Id, out var helper))
48-
{
4936
if (!test.IsSuite)
5037
{
51-
helper.StopTestCase();
38+
helper.StartTestContainer(); // A container for SetUp/TearDown methods
39+
helper.StartTestCase();
5240
}
41+
});
5342

54-
helper.StopTestContainer();
55-
}
56-
}
43+
public void AfterTest(ITest test) =>
44+
RunHookInRestoredAllureContext(test, () =>
45+
{
46+
if (_allureNUnitHelper.TryGetValue(test.Id, out var helper))
47+
{
48+
if (!test.IsSuite)
49+
{
50+
helper.StopTestCase();
51+
helper.StopTestContainer();
52+
}
53+
else if (IsSuiteWithNoAfterFixtures(test))
54+
{
55+
// If a test fixture contains a OneTimeTearDown method
56+
// with the [AllureAfter] attribute, the corresponding
57+
// container is closed in StopContainerAspect instead.
58+
helper.StopTestContainer();
59+
}
60+
}
61+
});
5762

5863
public ActionTargets Targets =>
5964
ActionTargets.Test | ActionTargets.Suite;
6065

6166
public void ApplyToContext(TestExecutionContext context)
6267
{
6368
var test = context.CurrentTest;
64-
var helper = new AllureNUnitHelper(test);
65-
helper.StartTestContainer();
66-
StepsHelper.StartBeforeFixture($"fr-{test.Id}");
69+
// A container for OneTimeSetUp/OneTimeTearDown methods.
70+
new AllureNUnitHelper(test).StartTestContainer();
71+
CaptureGlobalAllureContext(test);
6772
}
73+
74+
static bool IsSuiteWithNoAfterFixtures(ITest test) =>
75+
test is TestSuite suite && !suite.OneTimeTearDownMethods.Any(
76+
m => IsDefined(m.MethodInfo, typeof(AllureAfterAttribute))
77+
);
78+
79+
#region Allure context manipulation
80+
81+
/*
82+
* The methods this region are to make sure the AllureContext
83+
* flows into setup/teardown/test methods correctly. This is needed
84+
* because NUnit might spread hooks of this class and user's code
85+
* across unrelated threads, hiding changes made to the allure context
86+
* in, say, BeforeTest from, say, a one-time tear down method.
87+
*/
88+
89+
static void RunHookInRestoredAllureContext(ITest test, Action action)
90+
{
91+
RestoreAssociatedAllureContext(test);
92+
try
93+
{
94+
action();
95+
}
96+
finally
97+
{
98+
CaptureGlobalAllureContext(test);
99+
}
100+
}
101+
102+
static void CaptureGlobalAllureContext(ITest test) =>
103+
test.Properties.Set(ALLURE_CONTEXT_KEY, AllureLifecycle.Instance.Context);
104+
105+
static void RestoreAssociatedAllureContext(ITest test) =>
106+
AllureLifecycle.Instance.RestoreContext(
107+
GetAssociatedAllureContext(test)
108+
);
109+
110+
static AllureContext GetAssociatedAllureContext(ITest test) =>
111+
(AllureContext)test.Properties.Get(ALLURE_CONTEXT_KEY)
112+
?? GetAssociatedAllureContext(test.Parent);
113+
114+
const string ALLURE_CONTEXT_KEY = "AllureContext";
115+
116+
#endregion
68117
}
69118
}

Allure.NUnit/Core/AllureNUnitHelper.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,34 @@ internal void StartTestCase()
5757
Label.Thread(),
5858
Label.Host(),
5959
Label.Package(
60-
_test.ClassName?.Substring(
61-
0,
62-
_test.ClassName.LastIndexOf('.')
63-
)
60+
GetNamespace(_test.ClassName)
6461
),
6562
Label.TestMethod(_test.MethodName),
6663
Label.TestClass(
67-
_test.ClassName?.Substring(
68-
_test.ClassName.LastIndexOf('.') + 1
69-
)
64+
GetClassName(_test.ClassName)
7065
)
7166
}
7267
};
7368
AllureLifecycle.StartTestCase(testResult);
7469
}
7570

71+
static string GetNamespace(string classFullName)
72+
{
73+
var lastDotIndex = classFullName?.LastIndexOf('.') ?? -1;
74+
return lastDotIndex == -1 ? null : classFullName.Substring(
75+
0,
76+
lastDotIndex
77+
);
78+
}
79+
80+
static string GetClassName(string classFullName)
81+
{
82+
var lastDotIndex = classFullName?.LastIndexOf('.') ?? -1;
83+
return lastDotIndex == -1 ? classFullName : classFullName.Substring(
84+
lastDotIndex + 1
85+
);
86+
}
87+
7688
private TestFixture GetTestFixture(ITest test)
7789
{
7890
var currentTest = test;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Threading.Tasks;
4+
using Allure.Net.Commons;
5+
using AspectInjector.Broker;
6+
using NUnit.Framework;
7+
using NUnit.Framework.Internal;
8+
9+
namespace NUnit.Allure.Internals
10+
{
11+
[Aspect(Scope.Global)]
12+
public class StopContainerAspect
13+
{
14+
[Advice(Kind.Around)]
15+
public object StopTestContainerAfterTheLastOneTimeTearDown(
16+
[Argument(Source.Target)] Func<object[], object> target,
17+
[Argument(Source.Metadata)] MethodBase metadata
18+
)
19+
{
20+
if (IsOneTimeTearDown(metadata))
21+
{
22+
CurrentTearDownCount++;
23+
if (IsLastTearDown)
24+
{
25+
return CallAndStopContainer(target);
26+
}
27+
}
28+
return target(Array.Empty<object>());
29+
}
30+
31+
static object CallAndStopContainer(Func<object[], object> target)
32+
{
33+
object returnValue = null;
34+
try
35+
{
36+
returnValue = target(Array.Empty<object>());
37+
}
38+
finally
39+
{
40+
if (returnValue is null)
41+
{
42+
StopContainer();
43+
}
44+
else
45+
{
46+
// This branch is executed only in case of an async one time tear down
47+
returnValue = StopContainerAfterAsyncTearDown(returnValue);
48+
}
49+
}
50+
return returnValue;
51+
}
52+
53+
async static Task StopContainerAfterAsyncTearDown(object awaitable)
54+
{
55+
await ((Task)awaitable).ConfigureAwait(false);
56+
StopContainer();
57+
}
58+
59+
static void StopContainer()
60+
{
61+
AllureLifecycle.Instance.StopTestContainer();
62+
AllureLifecycle.Instance.WriteTestContainer();
63+
}
64+
65+
static bool IsOneTimeTearDown(MethodBase metadata) =>
66+
Attribute.IsDefined(
67+
metadata,
68+
typeof(OneTimeTearDownAttribute)
69+
);
70+
71+
static bool IsLastTearDown
72+
{
73+
get => CurrentTearDownCount == TotalTearDownCount;
74+
}
75+
76+
static int CurrentTearDownCount
77+
{
78+
get => (int?) TestExecutionContext.CurrentContext
79+
.CurrentTest.Properties.Get(CurrentTearDownKey) ?? 0;
80+
set => TestExecutionContext.CurrentContext
81+
.CurrentTest.Properties.Set(
82+
CurrentTearDownKey,
83+
value
84+
);
85+
}
86+
87+
static int TotalTearDownCount
88+
{
89+
get => (
90+
(TestSuite)TestExecutionContext.CurrentContext.CurrentTest
91+
).OneTimeTearDownMethods.Length;
92+
}
93+
94+
const string CurrentTearDownKey = "CurrentTearDownCount";
95+
}
96+
}

0 commit comments

Comments
 (0)