diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cb91f2..e29341a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,14 +21,15 @@ jobs: name: "Build" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.0.2 + - uses: actions/checkout@v3.5.3 - name: 'Setup .NET Core SDK' - uses: actions/setup-dotnet@v2.1.0 + uses: actions/setup-dotnet@v3.2.0 with: dotnet-version: | 3.1.x 6.0.x + 7.0.x - name: 'Restore packages' run: dotnet restore ${{ env.SOLUTION_PATH }} --packages ${{ env.RESTORE_OUTPUT_PATH }} @@ -38,5 +39,5 @@ jobs: - name: 'Run tests' run: | - dotnet test Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj --no-restore --configuration ${{ env.BUILD_CONFIGURATION }} - dotnet test Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj --no-restore --configuration ${{ env.BUILD_CONFIGURATION }} + dotnet test Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj --no-build --configuration ${{ env.BUILD_CONFIGURATION }} + dotnet test Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj --no-build --configuration ${{ env.BUILD_CONFIGURATION }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b62d4b8f..cc09f513 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,14 +14,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.0.2 + - uses: actions/checkout@v3.5.3 - name: 'Setup .NET Core SDK' - uses: actions/setup-dotnet@v2.1.0 + uses: actions/setup-dotnet@v3.2.0 with: dotnet-version: | 3.1.x 6.0.x + 7.0.x - name: 'Restore packages' run: dotnet restore ${{ env.SOLUTION_PATH }} --packages ${{ env.RESTORE_OUTPUT_PATH }} diff --git a/Allure.Features/Allure.Features.csproj b/Allure.Features/Allure.Features.csproj index b994f545..67af0001 100644 --- a/Allure.Features/Allure.Features.csproj +++ b/Allure.Features/Allure.Features.csproj @@ -1,6 +1,7 @@  netcoreapp3.1 + 11 false bin diff --git a/Allure.Features/TestData/After Feature Failure.feature b/Allure.Features/TestData/After Feature Failure.feature index d7aa18ad..33ef1299 100644 --- a/Allure.Features/TestData/After Feature Failure.feature +++ b/Allure.Features/TestData/After Feature Failure.feature @@ -5,7 +5,7 @@ Feature: After Feature Failure Scenario: After Feature Failure 1 Given Step is 'passed' - @broken + @failed Scenario: After Feature Failure 3 Given Step is 'failed' diff --git a/Allure.Features/TestData/Before Feature Failure.feature b/Allure.Features/TestData/Before Feature Failure.feature index e1159e4a..465ea4c4 100644 --- a/Allure.Features/TestData/Before Feature Failure.feature +++ b/Allure.Features/TestData/Before Feature Failure.feature @@ -2,4 +2,4 @@ Feature: Before Feature Failure @broken - Scenario: Unknown \ No newline at end of file + Scenario: Feature hook failure placeholder \ No newline at end of file diff --git a/Allure.Features/TestData/Invalid Steps.feature b/Allure.Features/TestData/Invalid Steps.feature index 9ad1dfa0..c94b65c6 100644 --- a/Allure.Features/TestData/Invalid Steps.feature +++ b/Allure.Features/TestData/Invalid Steps.feature @@ -12,7 +12,7 @@ Given Step is 'passed' And I don't have such step too - @broken + @failed Scenario: Failed step followed by invalid step Given Step is 'failed' Given I don't have such step diff --git a/Allure.NUnit.Examples/Allure.NUnit.Examples.csproj b/Allure.NUnit.Examples/Allure.NUnit.Examples.csproj index 1f775846..370c9fb1 100644 --- a/Allure.NUnit.Examples/Allure.NUnit.Examples.csproj +++ b/Allure.NUnit.Examples/Allure.NUnit.Examples.csproj @@ -2,6 +2,7 @@ net6.0 + 11 false Library diff --git a/Allure.NUnit.Examples/AllureSetUpFixture.cs b/Allure.NUnit.Examples/AllureSetUpFixture.cs new file mode 100644 index 00000000..08b224bd --- /dev/null +++ b/Allure.NUnit.Examples/AllureSetUpFixture.cs @@ -0,0 +1,17 @@ +using Allure.Net.Commons; +using NUnit.Allure.Core; +using NUnit.Framework; + +namespace Allure.NUnit.Examples +{ + [SetUpFixture] + public class AllureSetUpFixture + { + [OneTimeSetUp] + public static void CleanupResultDirectory() => + AllureExtensions.WrapSetUpTearDownParams( + AllureLifecycle.Instance.CleanupResultDirectory, + "Clear Allure Results Directory" + ); + } +} diff --git a/Allure.NUnit.Examples/BaseTest.cs b/Allure.NUnit.Examples/BaseTest.cs index 0687207d..e7e27a91 100644 --- a/Allure.NUnit.Examples/BaseTest.cs +++ b/Allure.NUnit.Examples/BaseTest.cs @@ -1,7 +1,5 @@ -using Allure.Net.Commons; -using NUnit.Allure.Attributes; +using NUnit.Allure.Attributes; using NUnit.Allure.Core; -using NUnit.Framework; namespace Allure.NUnit.Examples { @@ -9,11 +7,5 @@ namespace Allure.NUnit.Examples [AllureParentSuite("Root Suite")] public class BaseTest { - [OneTimeSetUp] - public void CleanupResultDirectory() - { - AllureExtensions.WrapSetUpTearDownParams(() => { AllureLifecycle.Instance.CleanupResultDirectory(); }, - "Clear Allure Results Directory"); - } } } \ No newline at end of file diff --git a/Allure.NUnit/Allure.NUnit.csproj b/Allure.NUnit/Allure.NUnit.csproj index 442ce783..2fce0102 100644 --- a/Allure.NUnit/Allure.NUnit.csproj +++ b/Allure.NUnit/Allure.NUnit.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 11 2.10-SNAPSHOT false Qameta Software diff --git a/Allure.NUnit/Attributes/AllureDisplayIgnoredAttribute.cs b/Allure.NUnit/Attributes/AllureDisplayIgnoredAttribute.cs index ebf5d5b1..fc055a1c 100644 --- a/Allure.NUnit/Attributes/AllureDisplayIgnoredAttribute.cs +++ b/Allure.NUnit/Attributes/AllureDisplayIgnoredAttribute.cs @@ -13,7 +13,6 @@ namespace NUnit.Allure.Attributes public class AllureDisplayIgnoredAttribute : NUnitAttribute, ITestAction { private readonly string _suiteName; - private string _ignoredContainerId; public AllureDisplayIgnoredAttribute(string suiteNameForIgnoredTests = "Ignored") { @@ -22,13 +21,11 @@ public AllureDisplayIgnoredAttribute(string suiteNameForIgnoredTests = "Ignored" public void BeforeTest(ITest suite) { - _ignoredContainerId = suite.Id + "-ignored"; - var fixture = new TestResultContainer + AllureLifecycle.Instance.StartTestContainer(new() { - uuid = _ignoredContainerId, + uuid = suite.Id + "-ignored", name = suite.ClassName - }; - AllureLifecycle.Instance.StartTestContainer(fixture); + }); } public void AfterTest(ITest suite) @@ -37,12 +34,19 @@ public void AfterTest(ITest suite) if (suite.HasChildren) { var ignoredTests = - GetAllTests(suite).Where(t => t.RunState == RunState.Ignored || t.RunState == RunState.Skipped); + GetAllTests(suite).Where( + t => t.RunState == RunState.Ignored + || t.RunState == RunState.Skipped + ); foreach (var test in ignoredTests) { - AllureLifecycle.Instance.UpdateTestContainer(_ignoredContainerId, t => t.children.Add(test.Id)); + AllureLifecycle.Instance.UpdateTestContainer( + t => t.children.Add(test.Id) + ); - var reason = test.Properties.Get(PropertyNames.SkipReason).ToString(); + var reason = test.Properties.Get( + PropertyNames.SkipReason + ).ToString(); var ignoredTestResult = new TestResult { @@ -66,12 +70,12 @@ public void AfterTest(ITest suite) } }; AllureLifecycle.Instance.StartTestCase(ignoredTestResult); - AllureLifecycle.Instance.StopTestCase(ignoredTestResult.uuid); - AllureLifecycle.Instance.WriteTestCase(ignoredTestResult.uuid); + AllureLifecycle.Instance.StopTestCase(); + AllureLifecycle.Instance.WriteTestCase(); } - AllureLifecycle.Instance.StopTestContainer(_ignoredContainerId); - AllureLifecycle.Instance.WriteTestContainer(_ignoredContainerId); + AllureLifecycle.Instance.StopTestContainer(); + AllureLifecycle.Instance.WriteTestContainer(); } } diff --git a/Allure.NUnit/Core/AllureExtendedConfiguration.cs b/Allure.NUnit/Core/AllureExtendedConfiguration.cs index dc00efdf..84c72fb3 100644 --- a/Allure.NUnit/Core/AllureExtendedConfiguration.cs +++ b/Allure.NUnit/Core/AllureExtendedConfiguration.cs @@ -6,10 +6,14 @@ namespace NUnit.Allure.Core { internal class AllureExtendedConfiguration : AllureConfiguration { - public HashSet BrokenTestData { get; set; } = new HashSet(); + public HashSet BrokenTestData { get; set; } = new(); [JsonConstructor] - protected AllureExtendedConfiguration(string title, string directory, HashSet links) : base(title, + protected AllureExtendedConfiguration( + string title, + string directory, + HashSet links + ) : base(title, directory, links) { } diff --git a/Allure.NUnit/Core/AllureExtensions.cs b/Allure.NUnit/Core/AllureExtensions.cs index 1d8a7dba..1776efb6 100644 --- a/Allure.NUnit/Core/AllureExtensions.cs +++ b/Allure.NUnit/Core/AllureExtensions.cs @@ -1,10 +1,9 @@ using System; -using System.Reflection; +using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Allure.Net.Commons; using NUnit.Framework.Internal; -using System.Linq; -using System.Threading.Tasks; namespace NUnit.Allure.Core { @@ -58,15 +57,22 @@ public static void WrapSetUpTearDownParams(Action action, string customName = "" /// Wraps Action into AllureStep. /// [Obsolete("Use [AllureStep] method attribute")] - public static void WrapInStep(this AllureLifecycle lifecycle, Action action, string stepName = "", [CallerMemberName] string callerName = "") + public static void WrapInStep( + this AllureLifecycle lifecycle, + Action action, + string stepName = "", + [CallerMemberName] string callerName = "" + ) { - if (string.IsNullOrEmpty(stepName)) stepName = callerName; + if (string.IsNullOrEmpty(stepName)) + { + stepName = callerName; + } - var id = Guid.NewGuid().ToString(); var stepResult = new StepResult {name = stepName}; try { - lifecycle.StartStep(id, stepResult); + lifecycle.StartStep(stepResult); action.Invoke(); lifecycle.StopStep(step => stepResult.status = Status.passed); } @@ -88,14 +94,22 @@ public static void WrapInStep(this AllureLifecycle lifecycle, Action action, str /// /// Wraps Func into AllureStep. /// - public static T WrapInStep(this AllureLifecycle lifecycle, Func func, string stepName = "", [CallerMemberName] string callerName = "") + public static T WrapInStep( + this AllureLifecycle lifecycle, + Func func, + string stepName = "", + [CallerMemberName] string callerName = "" + ) { - if (string.IsNullOrEmpty(stepName)) stepName = callerName; - var id = Guid.NewGuid().ToString(); + if (string.IsNullOrEmpty(stepName)) + { + stepName = callerName; + } + var stepResult = new StepResult {name = stepName}; try { - lifecycle.StartStep(id, stepResult); + lifecycle.StartStep(stepResult); var result = func.Invoke(); lifecycle.StopStep(step => stepResult.status = Status.passed); return result; @@ -125,13 +139,15 @@ public static async Task WrapInStepAsync( [CallerMemberName] string callerName = "" ) { - if (string.IsNullOrEmpty(stepName)) stepName = callerName; + if (string.IsNullOrEmpty(stepName)) + { + stepName = callerName; + } - var id = Guid.NewGuid().ToString(); var stepResult = new StepResult { name = stepName }; try { - lifecycle.StartStep(id, stepResult); + lifecycle.StartStep(stepResult); await action(); lifecycle.StopStep(step => stepResult.status = Status.passed); } @@ -160,12 +176,15 @@ public static async Task WrapInStepAsync( [CallerMemberName] string callerName = "" ) { - if (string.IsNullOrEmpty(stepName)) stepName = callerName; - var id = Guid.NewGuid().ToString(); + if (string.IsNullOrEmpty(stepName)) + { + stepName = callerName; + } + var stepResult = new StepResult { name = stepName }; try { - lifecycle.StartStep(id, stepResult); + lifecycle.StartStep(stepResult); var result = await func(); lifecycle.StopStep(step => stepResult.status = Status.passed); return result; @@ -185,15 +204,16 @@ public static async Task WrapInStepAsync( } } - /// - /// AllureNUnit AddScreenDiff wrapper method. - /// - public static void AddScreenDiff(this AllureLifecycle lifecycle, string expected, string actual, string diff) - { - var storageMain = lifecycle.GetType().GetField("storage", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(lifecycle); - var storageInternal = storageMain.GetType().GetField("storage", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(storageMain); - var keys = (storageInternal as System.Collections.Concurrent.ConcurrentDictionary).Keys.ToList(); - AllureLifecycle.Instance.AddScreenDiff(keys.Find(key => key.Contains("-tr-")), expected, actual, diff); - } + [Obsolete( + "Use AllureLifecycle.AddScreenDiff instance method instead to " + + "add a screen diff to the current test." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void AddScreenDiff( + this AllureLifecycle lifecycle, + string expected, + string actual, + string diff + ) => lifecycle.AddScreenDiff(expected, actual, diff); } } \ No newline at end of file diff --git a/Allure.NUnit/Core/AllureNUnitAttribute.cs b/Allure.NUnit/Core/AllureNUnitAttribute.cs index fae2f8ee..02db2585 100644 --- a/Allure.NUnit/Core/AllureNUnitAttribute.cs +++ b/Allure.NUnit/Core/AllureNUnitAttribute.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Concurrent; -using Allure.Net.Commons; -using NUnit.Engine; -using NUnit.Engine.Extensibility; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; @@ -12,21 +9,11 @@ namespace NUnit.Allure.Core [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)] public class AllureNUnitAttribute : PropertyAttribute, ITestAction, IApplyToContext { - 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; - } + private readonly ConcurrentDictionary _allureNUnitHelper = new(); [Obsolete("wrapIntoStep parameter is obsolete. Use [AllureStep] method attribute")] public AllureNUnitAttribute(bool wrapIntoStep = true) { - _isWrappedIntoStep = wrapIntoStep; } public AllureNUnitAttribute() @@ -36,7 +23,11 @@ public AllureNUnitAttribute() public void BeforeTest(ITest test) { var helper = new AllureNUnitHelper(test); - _allureNUnitHelper.AddOrUpdate(test.Id, helper, (key, existing) => helper); + _allureNUnitHelper.AddOrUpdate( + test.Id, + helper, + (key, existing) => helper + ); if (test.IsSuite) { @@ -64,7 +55,8 @@ public void AfterTest(ITest test) } } - public ActionTargets Targets => ActionTargets.Test | ActionTargets.Suite; + public ActionTargets Targets => + ActionTargets.Test | ActionTargets.Suite; public void ApplyToContext(TestExecutionContext context) { diff --git a/Allure.NUnit/Core/AllureNUnitHelper.cs b/Allure.NUnit/Core/AllureNUnitHelper.cs index 5da69d52..3d2756de 100644 --- a/Allure.NUnit/Core/AllureNUnitHelper.cs +++ b/Allure.NUnit/Core/AllureNUnitHelper.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using Allure.Net.Commons; -using Allure.Net.Commons.Storage; using Newtonsoft.Json.Linq; using NUnit.Allure.Attributes; using NUnit.Framework; @@ -15,14 +14,15 @@ namespace NUnit.Allure.Core { - public sealed class AllureNUnitHelper : ITestResultAccessor + public sealed class AllureNUnitHelper { - internal static List ExceptionTypes = new List { typeof(NUnitException), typeof(AssertionException) }; - public TestResultContainer TestResultContainer { get; set; } - public TestResult TestResult { get; set; } + internal static List ExceptionTypes = new() + { + typeof(NUnitException), + typeof(AssertionException) + }; private readonly ITest _test; - private string _testResultGuid; public AllureNUnitHelper(ITest test) { @@ -33,21 +33,22 @@ public AllureNUnitHelper(ITest test) internal void StartTestContainer() { - StepsHelper.TestResultAccessor = this; - TestResultContainer = new TestResultContainer + AllureLifecycle.StartTestContainer(new() { uuid = ContainerId, name = _test.FullName - }; - AllureLifecycle.StartTestContainer(TestResultContainer); + }); } internal void StartTestCase() { - _testResultGuid = string.Concat(Guid.NewGuid().ToString(), "-tr-", _test.Id); - TestResult = new TestResult + var testResult = new TestResult { - uuid = _testResultGuid, + uuid = string.Concat( + Guid.NewGuid().ToString(), + "-tr-", + _test.Id + ), name = _test.Name, historyId = _test.FullName, fullName = _test.FullName, @@ -55,12 +56,21 @@ internal void StartTestCase() { Label.Thread(), Label.Host(), - Label.Package(_test.ClassName?.Substring(0, _test.ClassName.LastIndexOf('.'))), + Label.Package( + _test.ClassName?.Substring( + 0, + _test.ClassName.LastIndexOf('.') + ) + ), Label.TestMethod(_test.MethodName), - Label.TestClass(_test.ClassName?.Substring(_test.ClassName.LastIndexOf('.') + 1)) + Label.TestClass( + _test.ClassName?.Substring( + _test.ClassName.LastIndexOf('.') + 1 + ) + ) } }; - AllureLifecycle.StartTestCase(ContainerId, TestResult); + AllureLifecycle.StartTestCase(testResult); } private TestFixture GetTestFixture(ITest test) @@ -70,7 +80,10 @@ private TestFixture GetTestFixture(ITest test) while (isTestSuite != true) { currentTest = currentTest.Parent; - if (currentTest is ParameterizedMethodSuite) currentTest = currentTest.Parent; + if (currentTest is ParameterizedMethodSuite) + { + currentTest = currentTest.Parent; + } isTestSuite = currentTest.IsSuite; } @@ -84,31 +97,42 @@ internal void StopTestCase() for (var i = 0; i < _test.Arguments.Length; i++) { - AllureLifecycle.UpdateTestCase(x => x.parameters.Add(new Parameter - { - // ReSharper disable once AccessToModifiedClosure - name = $"Param #{i}", - // ReSharper disable once AccessToModifiedClosure - value = _test.Arguments[i] == null ? "NULL" : _test.Arguments[i].ToString() - })); + AllureLifecycle.UpdateTestCase( + x => x.parameters.Add( + new Parameter + { + // ReSharper disable once AccessToModifiedClosure + name = $"Param #{i}", + // ReSharper disable once AccessToModifiedClosure + value = _test.Arguments[i] == null + ? "NULL" + : _test.Arguments[i].ToString() + } + ) + ); } - AllureLifecycle.UpdateTestCase(x => x.statusDetails = new StatusDetails - { - message = string.IsNullOrWhiteSpace(TestContext.CurrentContext.Result.Message) - ? TestContext.CurrentContext.Test.Name - : TestContext.CurrentContext.Result.Message, - trace = TestContext.CurrentContext.Result.StackTrace - }); + AllureLifecycle.UpdateTestCase( + x => x.statusDetails = new StatusDetails + { + message = string.IsNullOrWhiteSpace( + TestContext.CurrentContext.Result.Message + ) ? TestContext.CurrentContext.Test.Name + : TestContext.CurrentContext.Result.Message, + trace = TestContext.CurrentContext.Result.StackTrace + } + ); - AllureLifecycle.StopTestCase(testCase => testCase.status = GetNUnitStatus()); - AllureLifecycle.WriteTestCase(_testResultGuid); + AllureLifecycle.StopTestCase( + testCase => testCase.status = GetNUnitStatus() + ); + AllureLifecycle.WriteTestCase(); } internal void StopTestContainer() { - AllureLifecycle.StopTestContainer(ContainerId); - AllureLifecycle.WriteTestContainer(ContainerId); + AllureLifecycle.StopTestContainer(); + AllureLifecycle.WriteTestContainer(); } public static Status GetNUnitStatus() @@ -121,11 +145,18 @@ public static Status GetNUnitStatus() var allureSection = jo["allure"]; try { - var config = allureSection?.ToObject(); + var config = allureSection + ?.ToObject(); if (config?.BrokenTestData != null) + { foreach (var word in config.BrokenTestData) + { if (result.Message.Contains(word)) + { return Status.broken; + } + } + } } catch (Exception) { @@ -155,16 +186,34 @@ public static Status GetNUnitStatus() private void UpdateTestDataFromAttributes() { foreach (var p in GetTestProperties(PropertyNames.Description)) - AllureLifecycle.UpdateTestCase(x => x.description += $"{p}\n"); + { + AllureLifecycle.UpdateTestCase( + x => x.description += $"{p}\n" + ); + } foreach (var p in GetTestProperties(PropertyNames.Author)) - AllureLifecycle.UpdateTestCase(x => x.labels.Add(Label.Owner(p))); + { + AllureLifecycle.UpdateTestCase( + x => x.labels.Add(Label.Owner(p)) + ); + } foreach (var p in GetTestProperties(PropertyNames.Category)) - AllureLifecycle.UpdateTestCase(x => x.labels.Add(Label.Tag(p))); + { + AllureLifecycle.UpdateTestCase( + x => x.labels.Add(Label.Tag(p)) + ); + } - var attributes = _test.Method.GetCustomAttributes(true).ToList(); - attributes.AddRange(GetTestFixture(_test).GetCustomAttributes(true).ToList()); + var attributes = _test.Method + .GetCustomAttributes(true) + .ToList(); + attributes.AddRange( + GetTestFixture(_test) + .GetCustomAttributes(true) + .ToList() + ); attributes.ForEach(a => { @@ -174,21 +223,37 @@ private void UpdateTestDataFromAttributes() private void AddConsoleOutputAttachment() { - var output = TestExecutionContext.CurrentContext.CurrentResult.Output; - AllureLifecycle.AddAttachment("Console Output", "text/plain", - Encoding.UTF8.GetBytes(output), ".txt"); + var output = TestExecutionContext + .CurrentContext + .CurrentResult + .Output; + AllureLifecycle.AddAttachment( + "Console Output", + "text/plain", + Encoding.UTF8.GetBytes(output), + ".txt" + ); } private IEnumerable GetTestProperties(string name) { var list = new List(); var currentTest = _test; - while (currentTest.GetType() != typeof(TestSuite) && currentTest.GetType() != typeof(TestAssembly)) + while (currentTest.GetType() != typeof(TestSuite) + && currentTest.GetType() != typeof(TestAssembly)) { if (currentTest.Properties.ContainsKey(name)) + { if (currentTest.Properties[name].Count > 0) + { for (var i = 0; i < currentTest.Properties[name].Count; i++) - list.Add(currentTest.Properties[name][i].ToString()); + { + list.Add( + currentTest.Properties[name][i].ToString() + ); + } + } + } currentTest = currentTest.Parent; } @@ -206,12 +271,18 @@ public void WrapInStep(Action action, string stepName = "") public void SaveOneTimeResultToContext() { - var currentResult = TestExecutionContext.CurrentContext.CurrentResult; + var currentResult = TestExecutionContext + .CurrentContext + .CurrentResult; if (!string.IsNullOrEmpty(currentResult.Output)) { - AllureLifecycle.Instance.AddAttachment("Console Output", "text/plain", - Encoding.UTF8.GetBytes(currentResult.Output), ".txt"); + AllureLifecycle.Instance.AddAttachment( + "Console Output", + "text/plain", + Encoding.UTF8.GetBytes(currentResult.Output), + ".txt" + ); } FixtureResult fixtureResult = null; @@ -226,20 +297,26 @@ public void SaveOneTimeResultToContext() fixtureResult = fr; }); - var testFixture = GetTestFixture(TestExecutionContext.CurrentContext.CurrentTest); + var testFixture = GetTestFixture( + TestExecutionContext.CurrentContext.CurrentTest + ); testFixture.Properties.Set("OneTimeSetUpResult", fixtureResult); } public void AddOneTimeSetupResult() { - var testFixture = GetTestFixture(TestExecutionContext.CurrentContext.CurrentTest); + var testFixture = GetTestFixture( + TestExecutionContext.CurrentContext.CurrentTest + ); FixtureResult fixtureResult = null; - fixtureResult = testFixture.Properties.Get("OneTimeSetUpResult") as FixtureResult; + fixtureResult = testFixture.Properties.Get( + "OneTimeSetUpResult" + ) as FixtureResult; if (fixtureResult != null && fixtureResult.steps.Any()) { - AllureLifecycle.UpdateTestContainer(TestResultContainer.uuid, container => + AllureLifecycle.UpdateTestContainer(container => { container.befores.Add(fixtureResult); }); diff --git a/Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj b/Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj index a595fb3c..88452e2a 100644 --- a/Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj +++ b/Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj @@ -3,7 +3,7 @@ net6.0 false - default + 11 diff --git a/Allure.Net.Commons.Tests/AllureContextTests.cs b/Allure.Net.Commons.Tests/AllureContextTests.cs new file mode 100644 index 00000000..894aaaaa --- /dev/null +++ b/Allure.Net.Commons.Tests/AllureContextTests.cs @@ -0,0 +1,610 @@ +using NUnit.Framework; + +namespace Allure.Net.Commons.Tests +{ + class AllureContextTests + { + [Test] + public void TestEmptyContext() + { + var ctx = new AllureContext(); + + Assert.That(ctx.ContainerContext, Is.Empty); + Assert.That(ctx.FixtureContext, Is.Null); + Assert.That(ctx.TestContext, Is.Null); + Assert.That(ctx.StepContext, Is.Empty); + Assert.That(ctx.HasContainer, Is.False); + Assert.That(ctx.ContainerContextDepth, Is.Zero); + Assert.That(ctx.HasFixture, Is.False); + Assert.That(ctx.HasTest, Is.False); + Assert.That(ctx.HasStep, Is.False); + Assert.That(ctx.StepContextDepth, Is.Zero); + + Assert.That( + () => ctx.CurrentContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "No container context is active." + ) + ); + Assert.That( + () => ctx.CurrentFixture, + Throws.InvalidOperationException.With.Message.EqualTo( + "No fixture context is active." + ) + ); + Assert.That( + () => ctx.CurrentTest, + Throws.InvalidOperationException.With.Message.EqualTo( + "No test context is active." + ) + ); + Assert.That( + () => ctx.CurrentStep, + Throws.InvalidOperationException.With.Message.EqualTo( + "No step context is active." + ) + ); + Assert.That( + () => ctx.CurrentStepContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "No fixture, test, or step context is active." + ) + ); + } + + [Test] + public void TestContextOnly() + { + var test = new TestResult(); + + var ctx = new AllureContext().WithTestContext(test); + + Assert.That(ctx.HasTest, Is.True); + Assert.That(ctx.TestContext, Is.SameAs(test)); + Assert.That(ctx.CurrentTest, Is.SameAs(test)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(test)); + } + + [Test] + public void CanNotAddContainerIfTestIsSet() + { + var ctx = new AllureContext() + .WithTestContext(new()); + + Assert.That( + () => ctx.WithContainer(new()), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to change the container context because the " + + "test context is active." + ) + ); + } + + [Test] + public void CanNotAddContainerIfFixtureIsSet() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()); + + Assert.That( + () => ctx.WithContainer(new()), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to change the container context because the " + + "fixture context is active." + ) + ); + } + + [Test] + public void CanNotRemoveContainerIfTestIsSet() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(new()); + + Assert.That( + ctx.WithNoLastContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to change the container context because the " + + "test context is active." + ) + ); + } + + [Test] + public void TestContextCanBeRemoved() + { + var test = new TestResult(); + + var ctx = new AllureContext() + .WithTestContext(test) + .WithNoTestContext(); + + Assert.That(ctx.HasTest, Is.False); + Assert.That(ctx.TestContext, Is.Null); + Assert.That( + () => ctx.CurrentStepContainer, + Throws.InvalidOperationException + ); + } + + [Test] + public void ContainerCanNotBeNull() + { + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithContainer(null), + Throws.ArgumentNullException + ); + } + + [Test] + public void OneContainerInContainerContext() + { + var container = new TestResultContainer(); + + var ctx = new AllureContext().WithContainer(container); + + Assert.That(ctx.HasContainer, Is.True); + Assert.That(ctx.ContainerContextDepth, Is.EqualTo(1)); + Assert.That(ctx.ContainerContext, Is.EqualTo(new[] { container })); + Assert.That(ctx.CurrentContainer, Is.SameAs(container)); + } + + [Test] + public void SecondContainerIsPushedInFront() + { + var container1 = new TestResultContainer(); + var container2 = new TestResultContainer(); + + var ctx = new AllureContext() + .WithContainer(container1) + .WithContainer(container2); + + Assert.That( + ctx.ContainerContext, + Is.EqualTo(new[] { container2, container1 }) + ); + Assert.That(ctx.ContainerContextDepth, Is.EqualTo(2)); + Assert.That(ctx.CurrentContainer, Is.SameAs(container2)); + } + + [Test] + public void CanNotRemoveContainerIfNoneExist() + { + var ctx = new AllureContext(); + + Assert.That( + ctx.WithNoLastContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to deactivate the container context because " + + "it is not active." + ) + ); + } + + [Test] + public void LatestContainerCanBeRemoved() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithNoLastContainer(); + + Assert.That(ctx.HasContainer, Is.False); + Assert.That(ctx.ContainerContextDepth, Is.Zero); + Assert.That(ctx.ContainerContext, Is.Empty); + } + + [Test] + public void IfContainerIsRemovedThePreviousOneBecomesActive() + { + var container = new TestResultContainer(); + var ctx = new AllureContext() + .WithContainer(container) + .WithContainer(new()) + .WithNoLastContainer(); + + Assert.That(ctx.ContainerContext, Is.EqualTo(new[] { container })); + Assert.That(ctx.CurrentContainer, Is.SameAs(container)); + Assert.That(ctx.ContainerContextDepth, Is.EqualTo(1)); + } + + [Test] + public void FixtureContextRequiresContainer() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithFixtureContext(new()), + Throws.InvalidOperationException + .With.Message.EqualTo( + "Unable to activate the fixture context because " + + "the container context is not active." + ) + ); + } + + [Test] + public void FixtureCanNotBeNull() + { + var ctx = new AllureContext().WithContainer(new()); + + Assert.That( + () => ctx.WithFixtureContext(null), + Throws.ArgumentNullException + ); + } + + [Test] + public void FixtureContextIsSet() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(fixture); + + Assert.That(ctx.HasFixture, Is.True); + Assert.That(ctx.FixtureContext, Is.SameAs(fixture)); + Assert.That(ctx.CurrentFixture, Is.SameAs(fixture)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(fixture)); + } + + [Test] + public void CanNotRemoveContainerIfFixtureIsSet() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()); + + Assert.That( + ctx.WithNoLastContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to change the container context because the " + + "fixture context is active." + ) + ); + } + + [Test] + public void FixturesCanNotBeNested() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(fixture); + + Assert.That( + () => ctx.WithFixtureContext(new()), + Throws.InvalidOperationException + .With.Message.EqualTo( + "Unable to activate the fixture context because " + + "it's already active." + ) + ); + } + + [Test] + public void TestCanNotBeNull() + { + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithTestContext(null), + Throws.ArgumentNullException + ); + } + + [Test] + public void TestsCanNotBeNested() + { + var test = new TestResult(); + + var ctx = new AllureContext().WithTestContext(test); + + Assert.That( + () => ctx.WithTestContext(new()), + Throws.InvalidOperationException + .With.Message.EqualTo( + "Unable to activate the test context because " + + "it is already active." + ) + ); + } + + [Test] + public void CanNotSetTestContextIfFixtureContextIsActive() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()); + + Assert.That( + () => ctx.WithTestContext(new()), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to activate the test context because the " + + "fixture context is active." + ) + ); + } + + [Test] + public void ClearingTestContextClearsFixtureContext() + { + var test = new TestResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(test) + .WithFixtureContext(new()) + .WithNoTestContext(); + + Assert.That(ctx.HasFixture, Is.False); + Assert.That(ctx.FixtureContext, Is.Null); + Assert.That( + () => ctx.CurrentStepContainer, + Throws.InvalidOperationException + ); + } + + [Test] + public void SettingFixtureContextAfterTestAffectsStepContainer() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(new()) + .WithFixtureContext(fixture); + + Assert.That(ctx.CurrentStepContainer, Is.SameAs(fixture)); + } + + [Test] + public void FixtureContextCanBeCleared() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(fixture) + .WithNoFixtureContext(); + + Assert.That(ctx.HasFixture, Is.False); + Assert.That(ctx.FixtureContext, Is.Null); + } + + [Test] + public void StepCanNotBeNull() + { + var ctx = new AllureContext().WithTestContext(new()); + + Assert.That( + () => ctx.WithStep(null), + Throws.ArgumentNullException + ); + } + + [Test] + public void StepCanNotBeAddedIfNoTestOrFixtureExists() + { + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithStep(new()), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to activate the step context because neither " + + "test, nor fixture context is active." + ) + ); + } + + [Test] + public void StepCanNotBeRemovedIfNoStepExists() + { + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithNoLastStep(), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to deactivate the step context because it " + + "isn't active." + ) + ); + } + + [Test] + public void StepCanBeAddedIfFixtureExists() + { + var step = new StepResult(); + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()) + .WithStep(step); + + Assert.That(ctx.HasStep, Is.True); + Assert.That(ctx.StepContext, Is.EqualTo(new[] { step })); + Assert.That(ctx.StepContextDepth, Is.EqualTo(1)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(step)); + } + + [Test] + public void StepCanBeAddedIfTestExists() + { + var step = new StepResult(); + var ctx = new AllureContext() + .WithTestContext(new()) + .WithStep(step); + + Assert.That(ctx.HasStep, Is.True); + Assert.That(ctx.StepContext, Is.EqualTo(new[] { step })); + Assert.That(ctx.CurrentStep, Is.SameAs(step)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(step)); + } + + [Test] + public void TwoStepsCanBeAdded() + { + var step1 = new StepResult(); + var step2 = new StepResult(); + var ctx = new AllureContext() + .WithTestContext(new()) + .WithStep(step1) + .WithStep(step2); + + Assert.That(ctx.StepContext, Is.EqualTo(new[] { step2, step1 })); + Assert.That(ctx.StepContextDepth, Is.EqualTo(2)); + Assert.That(ctx.CurrentStep, Is.SameAs(step2)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(step2)); + } + + [Test] + public void RemovingStepRestoresPreviousStepAsStepContainer() + { + var step1 = new StepResult(); + var step2 = new StepResult(); + var ctx = new AllureContext() + .WithTestContext(new()) + .WithStep(step1) + .WithStep(step2) + .WithNoLastStep(); + + Assert.That(ctx.StepContext, Is.EqualTo(new[] { step1 })); + Assert.That(ctx.CurrentStep, Is.SameAs(step1)); + Assert.That(ctx.StepContextDepth, Is.EqualTo(1)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(step1)); + } + + [Test] + public void RemovingTheOnlyStepRestoresTestAsStepContainer() + { + var test = new TestResult(); + var ctx = new AllureContext() + .WithTestContext(test) + .WithStep(new()) + .WithNoLastStep(); + + Assert.That(ctx.HasStep, Is.False); + Assert.That(ctx.StepContext, Is.Empty); + Assert.That(ctx.StepContextDepth, Is.Zero); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(test)); + } + + [Test] + public void RemovingTheOnlyStepRestoresFixtureAsStepContainer() + { + var fixture = new FixtureResult(); + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(fixture) + .WithStep(new()) + .WithNoLastStep(); + + Assert.That(ctx.StepContext, Is.Empty); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(fixture)); + } + + [Test] + public void RemovingFixtureClearsStepContext() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()) + .WithStep(new()) + .WithNoFixtureContext(); + + Assert.That(ctx.HasStep, Is.False); + Assert.That(ctx.StepContext, Is.Empty); + } + + [Test] + public void RemovingTestClearsStepContext() + { + var ctx = new AllureContext() + .WithTestContext(new()) + .WithStep(new()) + .WithNoTestContext(); + + Assert.That(ctx.HasStep, Is.False); + Assert.That(ctx.StepContext, Is.Empty); + } + + [Test] + public void FixtureAfterTestClearsStepContext() + { + // It is typical for some tear down fixtures to overlap with a + // test. Once such a fixture is started, all steps left after the + // test should be removed from the context. + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(new()) + .WithStep(new()) + .WithFixtureContext(new()); + + Assert.That(ctx.HasStep, Is.False); + Assert.That(ctx.StepContext, Is.Empty); + } + + [Test] + public void ContextToString() + { + Assert.That( + new AllureContext().ToString(), + Is.EqualTo("AllureContext { Containers = [], Fixture = null, Test = null, Steps = [] }") + ); + Assert.That( + new AllureContext() + .WithContainer(new() { name = "c" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [c], Fixture = null, Test = null, Steps = [] }") + ); + Assert.That( + new AllureContext() + .WithContainer(new() { name = "c1" }) + .WithContainer(new() { name = "c2" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [c2 <- c1], Fixture = null, Test = null, Steps = [] }") + ); + Assert.That( + new AllureContext() + .WithTestContext(new() { name = "t" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [], Fixture = null, Test = t, Steps = [] }") + ); + Assert.That( + new AllureContext() + .WithContainer(new() { name = "c" }) + .WithFixtureContext(new() { name = "f" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [c], Fixture = f, Test = null, Steps = [] }") + ); + Assert.That( + new AllureContext() + .WithTestContext(new() { name = "t" }) + .WithStep(new() { name = "s" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [], Fixture = null, Test = t, Steps = [s] }") + ); + Assert.That( + new AllureContext() + .WithTestContext(new() { name = "t" }) + .WithStep(new() { name = "s1" }) + .WithStep(new() { name = "s2" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [], Fixture = null, Test = t, Steps = [s2 <- s1] }") + ); + Assert.That( + new AllureContext() + .WithContainer(new() { uuid = "c" }) + .WithTestContext(new() { uuid = "t" }) + .ToString(), + Is.EqualTo("AllureContext { Containers = [c], Fixture = null, Test = t, Steps = [] }") + ); + } + } +} diff --git a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs index 6cd08fd5..29a519ac 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -1,5 +1,6 @@ -using NUnit.Framework; +using System; using System.Threading.Tasks; +using NUnit.Framework; namespace Allure.Net.Commons.Tests { @@ -40,33 +41,33 @@ public void IntegrationTest() cycle .StartTestContainer(container) - .StartBeforeFixture(container.uuid, beforeFeature.uuid, beforeFeature.fixture) + .StartBeforeFixture(beforeFeature.fixture) - .StartStep(fixtureStep.uuid, fixtureStep.step) + .StartStep(fixtureStep.step) .StopStep(x => x.status = Status.passed) .AddAttachment("text file", "text/xml", txtAttach.path) .AddAttachment(txtAttach.path) - .UpdateFixture(beforeFeature.uuid, f => f.status = Status.passed) - .StopFixture(beforeFeature.uuid) + .UpdateFixture(f => f.status = Status.passed) + .StopFixture() - .StartBeforeFixture(container.uuid, beforeScenario.uuid, beforeScenario.fixture) - .UpdateFixture(beforeScenario.uuid, f => f.status = Status.passed) - .StopFixture(beforeScenario.uuid) + .StartBeforeFixture(beforeScenario.fixture) + .UpdateFixture(f => f.status = Status.passed) + .StopFixture() - .StartTestCase(container.uuid, test) + .StartTestCase(test) - .StartStep(step1.uuid, step1.step) + .StartStep(step1.step) .StopStep(x => x.status = Status.passed) - .StartStep(step2.uuid, step2.step) + .StartStep(step2.step) .AddAttachment("unknown file", "text/xml", txtAttachWithNoExt.content) .StopStep(x => x.status = Status.broken) - .StartStep(step3.uuid, step3.step) + .StartStep(step3.step) .StopStep(x => x.status = Status.skipped) - .AddScreenDiff(test.uuid, "expected.png", "actual.png", "diff.png") + .AddScreenDiff("expected.png", "actual.png", "diff.png") .StopTestCase(x => { @@ -81,18 +82,100 @@ public void IntegrationTest() }; }) - .StartAfterFixture(container.uuid, afterScenario.uuid, afterScenario.fixture) - .UpdateFixture(afterScenario.uuid, f => f.status = Status.passed) - .StopFixture(afterScenario.uuid) + .StartAfterFixture(afterScenario.fixture) + .UpdateFixture(f => f.status = Status.passed) + .StopFixture() - .StartAfterFixture(container.uuid, afterFeature.uuid, afterFeature.fixture) + .StartAfterFixture(afterFeature.fixture) .StopFixture(f => f.status = Status.passed) - .WriteTestCase(test.uuid) - .StopTestContainer(container.uuid) - .WriteTestContainer(container.uuid); + .WriteTestCase() + .StopTestContainer() + .WriteTestContainer(); }); + } + + [Test, Description("A test step should be correctly added even if a " + + "before fixture overlaps with the test")] + public void BeforeFixtureMayOverlapsWithTest() + { + var writer = new InMemoryResultsWriter(); + var lifecycle = new AllureLifecycle(_ => writer); + var container = new TestResultContainer + { + uuid = Guid.NewGuid().ToString() + }; + var testResult = new TestResult + { + uuid = Guid.NewGuid().ToString() + }; + var fixture = new FixtureResult { name = "fixture" }; + + lifecycle.StartTestContainer(container) + .StartTestCase(testResult) + .StartBeforeFixture(fixture) + .StopFixture() + .StartStep(new()) + .StopStep() + .StopTestCase() + .StopTestContainer() + .WriteTestCase() + .WriteTestContainer(); + + Assert.That(writer.testContainers.Count, Is.EqualTo(1)); + Assert.That(writer.testContainers[0].uuid, Is.EqualTo(container.uuid)); + + Assert.That(writer.testContainers[0].befores.Count, Is.EqualTo(1)); + Assert.That(writer.testContainers[0].befores[0].name, Is.EqualTo("fixture")); + + Assert.That(writer.testContainers[0].children.Count, Is.EqualTo(1)); + Assert.That(writer.testContainers[0].children[0], Is.EqualTo(testResult.uuid)); + + Assert.That(writer.testResults.Count, Is.EqualTo(1)); + Assert.That(writer.testResults[0].uuid, Is.EqualTo(testResult.uuid)); + } + [Test] + public async Task ContextCapturingTest() + { + var writer = new InMemoryResultsWriter(); + var lifecycle = new AllureLifecycle(_ => writer); + AllureContext context = null, modifiedContext = null; + await Task.Factory.StartNew(() => + { + lifecycle.StartTestCase(new() + { + uuid = Guid.NewGuid().ToString() + }); + context = lifecycle.Context; + }); + modifiedContext = lifecycle.RunInContext(context, () => + { + lifecycle.StopTestCase(); + lifecycle.WriteTestCase(); + }); + + Assert.That(writer.testResults, Is.Not.Empty); + Assert.That(modifiedContext.HasTest, Is.False); + } + + [Test] + public async Task ContextCapturingHasNoEffectIfContextIsNull() + { + var writer = new InMemoryResultsWriter(); + var lifecycle = new AllureLifecycle(_ => writer); + await Task.Factory.StartNew(() => + { + lifecycle.StartTestCase(new() + { + uuid = Guid.NewGuid().ToString() + }); + }); + + Assert.That(() => lifecycle.RunInContext(null, () => + { + lifecycle.StopTestCase(); + }), Throws.InvalidOperationException); } } } diff --git a/Allure.Net.Commons.Tests/ConcurrencyTests.cs b/Allure.Net.Commons.Tests/ConcurrencyTests.cs new file mode 100644 index 00000000..98a037c8 --- /dev/null +++ b/Allure.Net.Commons.Tests/ConcurrencyTests.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace Allure.Net.Commons.Tests +{ + internal class ConcurrencyTests + { + InMemoryResultsWriter writer; + AllureLifecycle lifecycle; + int writes = 0; + + [SetUp] + public void SetUp() + { + this.writer = new InMemoryResultsWriter(); + this.lifecycle = new AllureLifecycle(_ => this.writer); + } + + [Test] + public void ParallelTestsAreIsolated() + { + RunThreads( + () => this.AddTestWithSteps("test-1", "step-1-1", "step-1-2"), + () => this.AddTestWithSteps("test-2", "step-2-1", "step-2-2") + ); + + this.AssertTestWithSteps("test-1", "step-1-1", "step-1-2"); + this.AssertTestWithSteps("test-2", "step-2-1", "step-2-2"); + } + + [Test] + public async Task AsyncTestsAreIsolated() + { + await Task.WhenAll( + this.AddTestWithStepsAsync("test-1", "step-1-1", "step-1-2"), + this.AddTestWithStepsAsync("test-2", "step-2-1", "step-2-2"), + this.AddTestWithStepsAsync("test-3", "step-3-1", "step-3-2") + ); + + this.AssertTestWithSteps("test-1", "step-1-1", "step-1-2"); + this.AssertTestWithSteps("test-2", "step-2-1", "step-2-2"); + this.AssertTestWithSteps("test-3", "step-3-1", "step-3-2"); + } + + [Test] + public void ParallelStepsOfTestAreIsolated() + { + this.WrapInTest("test-1", () => RunThreads( + () => this.AddStep("step-1"), + () => this.AddStep("step-2") + )); + + this.AssertTestWithSteps("test-1", "step-1", "step-2"); + } + + [Test] + public async Task AsyncStepsOfTestAreIsolated() + { + await this.WrapInTestAsync("test-1", async () => await Task.WhenAll( + this.AddStepsAsync("step-1"), + this.AddStepsAsync("step-2"), + this.AddStepsAsync("step-3") + )); + + this.AssertTestWithSteps("test-1", "step-1", "step-2", "step-3"); + } + + [Test] + public void ContextCapturedBySubThreads() + { + /* + * test | Parent thread + * - outer | Parent thread + * - inner-1 | Child thread 1 + * - inner-1-1 | Child thread 1 + * - inner-1-2 | Child thread 1 + * - inner-2 | Child thread 2 + * - inner-2-1 | Child thread 2 + * - inner-2-2 | Child thread 2 + */ + var sync = new ManualResetEventSlim(); + + this.WrapInTest( + "test", + () => this.WrapInStep( + "outer", + () => RunThreads( + BindEventSet(() => this.AddSteps(( + "inner-1", + new object[] { "inner-1-1", "inner-1-2" } + )), sync), + BindEventWait (() => this.AddSteps(( + "inner-2", + new object[] { "inner-2-1", "inner-2-2" } + )), sync) + ) + ) + ); + + this.AssertTestWithSteps( + "test", + ("outer", new object[] + { + ("inner-1", new object[] { "inner-1-1", "inner-1-2" }), + ("inner-2", new object[] { "inner-2-1", "inner-2-2" }) + }) + ); + } + + [Test] + public async Task ContextCapturedBySubTasks() + { + /* + * test | Parent task + * - outer | Parent task + * - inner-1 | Child task 1 + * - inner-1-1 | Child task 1 + * - inner-1-2 | Child task 1 + * - inner-2 | Child task 2 + * - inner-2-1 | Child task 2 + * - inner-2-2 | Child task 2 + */ + await this.WrapInTestAsync( + "test", + async () => await this.WrapInStepAsync( + "outer", + async () => await Task.WhenAll( + this.AddStepsAsync( + ("inner-1", new object[] { "inner-1-1", "inner-1-2" }) + ), + this.AddStepsAsync( + ("inner-2", new object[] { "inner-2-1", "inner-2-2" }) + ) + ) + ) + ); + + this.AssertTestWithSteps( + "test", + ("outer", new object[] + { + ("inner-1", new object[] { "inner-1-1", "inner-1-2" }), + ("inner-2", new object[] { "inner-2-1", "inner-2-2" }) + }) + ); + } + + static Action BindEventSet(Action fn, ManualResetEventSlim @event) => () => + { + try + { + fn(); + } + finally + { + @event.Set(); + } + }; + + static Action BindEventWait(Action fn, ManualResetEventSlim @event) => () => + { + @event.Wait(); + fn(); + }; + + async Task AddTestWithStepsAsync(string name, params object[] steps) + { + this.lifecycle + .StartTestCase(new() + { + name = name, + uuid = Guid.NewGuid().ToString() + }); + await Task.Delay(1); + await this.AddStepsAsync(steps); + this.lifecycle + .StopTestCase() + .WriteTestCase(); + writes++; + } + + void WrapInTest(string testName, Action action) + { + this.lifecycle.StartTestCase( + new() { name = testName, uuid = Guid.NewGuid().ToString() } + ); + action(); + this.lifecycle + .StopTestCase() + .WriteTestCase(); + } + + void WrapInStep(string stepName, Action action) + { + this.lifecycle.StartStep( + new() { name = stepName } + ); + action(); + this.lifecycle + .StopStep(); + } + + async Task WrapInStepAsync(string stepName, Func action) + { + this.lifecycle.StartStep( + new() { name = stepName } + ); + await action(); + this.lifecycle + .StopStep(); + } + + async Task WrapInTestAsync(string testName, Func action) + { + this.lifecycle.StartTestCase( + new() { name = testName, uuid = Guid.NewGuid().ToString() } + ); + await Task.Delay(1); + await action(); + this.lifecycle + .StopTestCase() + .WriteTestCase(); + } + + void AddTestWithSteps(string name, params object[] steps) => + this.WrapInTest(name, () => this.AddSteps(steps)); + + async Task AddStepsAsync(params object[] steps) + { + foreach (var step in steps) + { + if (step is string simpleStepName) + { + this.AddStep(simpleStepName); + } + else if (step is (string complexStepName, object[] substeps)) + { + await this.AddStepWithSubstepsAsync(complexStepName, substeps); + } + + await Task.Delay(1); + } + } + + void AddSteps(params object[] steps) + { + foreach (var step in steps) + { + if (step is string simpleStepName) + { + this.AddStep(simpleStepName); + } + else if (step is (string complexStepName, object[] substeps)) + { + this.AddStepWithSubsteps(complexStepName, substeps); + } + } + } + + void AddStep(string name) + { + this.lifecycle.StartStep( + new() { name = name } + ).StopStep(); + } + + void AddStepWithSubsteps(string name, params object[] substeps) + { + this.lifecycle.StartStep(new() { name = name }); + this.AddSteps(substeps); + this.lifecycle.StopStep(); + } + + async Task AddStepWithSubstepsAsync(string name, params object[] substeps) + { + this.lifecycle.StartStep(new() { name = name }); + await this.AddStepsAsync(substeps); + this.lifecycle.StopStep(); + } + + void AssertTestWithSteps(string testName, params object[] steps) + { + Assert.That( + this.writer.testResults.Select(tr => tr.name), + Contains.Item(testName) + ); + var test = this.writer.testResults.Single(tr => tr.name == testName); + this.AssertSteps(test.steps, steps); + } + + void AssertSteps(List actualSteps, params object[] steps) + { + var expectedCount = steps.Length; + Assert.That(actualSteps.Count, Is.EqualTo(expectedCount)); + for (var i = 0; i < expectedCount; i++) + { + var actualStep = actualSteps[i]; + var step = steps.ElementAt(i); + if (!(step is (string expectedStepName, object[] substeps))) + { + expectedStepName = (string)step; + substeps = Array.Empty(); + } + Assert.That(actualStep.name, Is.EqualTo(expectedStepName)); + this.AssertSteps(actualStep.steps, substeps); + } + } + + static void RunThreads(params Action[] jobs) + { + var errors = new List(); + var threads = jobs.Select( + j => new Thread( + WrapThreadCallbackError(j, errors) + ) + ).ToList(); + foreach(var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.That(errors, Is.Empty); + } + + static ThreadStart WrapThreadCallbackError( + Action callback, + List errors + ) => () => + { + try + { + callback(); + } + catch (Exception ex) + { + errors.Add(ex); + } + }; + } +} diff --git a/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs b/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs new file mode 100644 index 00000000..29cd9223 --- /dev/null +++ b/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Allure.Net.Commons.Writer; + +namespace Allure.Net.Commons.Tests +{ + class InMemoryResultsWriter : IAllureResultsWriter + { + readonly object monitor = new(); + internal List testResults = new(); + internal List testContainers = new(); + internal List<(string Source, byte[] Content)> attachments = new(); + + public void CleanUp() + { + lock (this.monitor) + { + this.testResults.Clear(); + this.testContainers.Clear(); + this.attachments.Clear(); + } + } + + public void Write(TestResult testResult) + { + lock (this.monitor) + { + this.testResults.Add(testResult); + } + } + + public void Write(TestResultContainer testResult) + { + lock (this.monitor) + { + this.testContainers.Add(testResult); + } + } + + public void Write(string source, byte[] attachment) + { + lock (this.monitor) + { + this.attachments.Add((source, attachment)); + } + } + } +} diff --git a/Allure.Net.Commons/Allure.Net.Commons.csproj b/Allure.Net.Commons/Allure.Net.Commons.csproj index 4aaa813f..f988dae6 100644 --- a/Allure.Net.Commons/Allure.Net.Commons.csproj +++ b/Allure.Net.Commons/Allure.Net.Commons.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 11 2.10-SNAPSHOT false Alexander Bakanov, Nikolay Laptev @@ -37,6 +38,7 @@ + diff --git a/Allure.Net.Commons/AllureContext.cs b/Allure.Net.Commons/AllureContext.cs new file mode 100644 index 00000000..612b742c --- /dev/null +++ b/Allure.Net.Commons/AllureContext.cs @@ -0,0 +1,473 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; + +#nullable enable + +namespace Allure.Net.Commons; + +/// +/// Represents allure-related contextual information required to collect +/// the report data during a test execution. Comprises four contexts: +/// container, fxiture, test, and step, as well as methods to query and +/// modify them. +/// +/// +/// Instances of this class are immutable to ensure proper isolation +/// between different tests and steps that may potentially be run +/// cuncurrently either by a test framework or by an end user. +/// +[DebuggerDisplay( + "Containers = {ContainerContextDepth}, HasFixture = {HasFixture}, " + + "HasTest = {HasTest}, Steps = {StepContextDepth}" +)] +public record class AllureContext +{ + /// + /// Returns true if a container context is active. + /// + public bool HasContainer => !this.ContainerContext.IsEmpty; + + /// + /// Returns the number of containers in the container context. + /// + public int ContainerContextDepth => this.ContainerContext.Count(); + + /// + /// Returns true if a fixture context is active. + /// + public bool HasFixture => this.FixtureContext is not null; + + /// + /// Returns true if a test context is active. + /// + public bool HasTest => this.TestContext is not null; + + /// + /// Returns true if a step context is active. + /// + public bool HasStep => !this.StepContext.IsEmpty; + + /// + /// Returns the number of steps in the step context. + /// + public int StepContextDepth => this.StepContext.Count(); + + /// + /// A stack of fixture containers affecting subsequent tests. + /// + /// + /// Activating this context allows operations on the current container + /// (including adding a fixture to or removing a fixture from the + /// current container). + /// + internal IImmutableStack ContainerContext + { + get; + private init; + } = ImmutableStack.Empty; + + /// + /// A fixture that is being currently executed. + /// + /// + /// Activating this context allows operations on the current fixture + /// result.
+ /// This property differs from in that + /// instead of throwing it returns null if a fixture context isn't + /// active. + ///
+ internal FixtureResult? FixtureContext { get; private init; } + + /// + /// A test that is being executed. + /// + /// + /// Activating this context allows operations on the current test + /// result.
+ /// + /// This property differs from in that + /// instead of throwing it returns null if a test context isn't active. + ///
+ internal TestResult? TestContext { get; private init; } + + /// + /// A stack of nested steps that are being executed. + /// + /// + /// Activating this context allows operations on the current step. + /// + internal IImmutableStack StepContext + { + get; + private init; + } = ImmutableStack.Empty; + + /// + /// The most recently added container from the container context. + /// + /// + /// It throws if a container + /// context isn't active. + /// + /// + internal TestResultContainer CurrentContainer + { + get => this.ContainerContext.FirstOrDefault() + ?? throw new InvalidOperationException( + "No container context is active." + ); + } + + /// + /// A fixture that is being executed. + /// + /// + /// It throws if a fixture + /// context isn't active. + /// + /// + internal FixtureResult CurrentFixture => + this.FixtureContext ?? throw new InvalidOperationException( + "No fixture context is active." + ); + + /// + /// A test that is being executed. + /// + /// + /// It throws if a test context + /// isn't active. + /// + /// + internal TestResult CurrentTest => + this.TestContext ?? throw new InvalidOperationException( + "No test context is active." + ); + + /// + /// A step that is being executed. + /// + /// + /// It throws if a step context + /// isn't active. + /// + /// + internal StepResult CurrentStep => + this.StepContext.FirstOrDefault() + ?? throw new InvalidOperationException( + "No step context is active." + ); + + /// + /// A step container a next step should be put in. + /// + /// + /// A step container can be a fixture, a test of an another step.
+ /// It throws if neither + /// fixture, nor test, nor step context is active. + ///
+ /// + internal ExecutableItem CurrentStepContainer => + this.StepContext.FirstOrDefault() as ExecutableItem + ?? this.RootStepContainer + ?? throw new InvalidOperationException( + "No fixture, test, or step context is active." + ); + + /// + /// Used by to serialize proeprties of the + /// context. + /// + protected virtual bool PrintMembers(StringBuilder stringBuilder) + { + var containers = + RepresentStack(this.ContainerContext, c => c.name ?? c.uuid); + var fixture = this.FixtureContext?.name ?? "null"; + var test = this.TestContext?.name + ?? this.TestContext?.uuid + ?? "null"; + var steps = RepresentStack(this.StepContext, s => s.name); + + stringBuilder.AppendFormat("Containers = [{0}], ", containers); + stringBuilder.AppendFormat("Fixture = {0}, ", fixture); + stringBuilder.AppendFormat("Test = {0}, ", test); + stringBuilder.AppendFormat("Steps = [{0}]", steps); + return true; + } + + /// + /// Creates a new with the active container + /// context and the specified container pushed on top of it. + /// + /// + /// Can't be called if a fixture or a test context is active. + /// + /// + /// A container to push on top of the container context. + /// + /// + /// A new instance of with the modified + /// (always active) container context. + /// + /// + internal AllureContext WithContainer(TestResultContainer container) => + this.ValidateContainerContextCanBeModified() with + { + ContainerContext = this.ContainerContext.Push( + container ?? throw new ArgumentNullException( + nameof(container) + ) + ) + }; + + /// + /// Creates a new without the most recently + /// added container in its container context. Requires an active + /// container context. Deactivates a container context if it consists + /// of one container only before the call. + /// + /// + /// Can't be called if a fixture or a test context is active. + /// + /// + /// A new instance of with the modified + /// (possibly inactive) container context. + /// + /// + internal AllureContext WithNoLastContainer() => + this with + { + ContainerContext = this.ValidateContainerCanBeRemoved() + .ContainerContext.Pop() + }; + + /// + /// Creates a new with the active fixture + /// context that is set to the specified fixture. Requires the + /// container context to be active. + /// + /// + /// Only one fixture context can be active at a time. + /// + /// + /// A new fixture context. + /// + /// + /// A new instance of with the modified + /// (always active) fixture context. + /// + /// + /// + internal AllureContext WithFixtureContext(FixtureResult fixtureResult) => + this with + { + FixtureContext = this.ValidateNewFixtureContext( + fixtureResult ?? throw new ArgumentNullException( + nameof(fixtureResult) + ) + ), + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with inactive fixture and + /// step contexts. + /// + internal AllureContext WithNoFixtureContext() => + this with + { + FixtureContext = null, + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with the active test + /// context that is set to the specified test result. + /// Can't be used if a fixture context is active. + /// + /// + /// A new test context. + /// + /// + /// A new instance of with the modified + /// (always active) test context. + /// + /// + /// + internal AllureContext WithTestContext(TestResult testResult) => + this with + { + TestContext = this.ValidateNewTestContext( + testResult ?? throw new ArgumentNullException( + nameof(testResult) + ) + ) + }; + + /// + /// Creates a new with inactive test, + /// fixture and step contexts. + /// + internal AllureContext WithNoTestContext() => + this with + { + FixtureContext = null, + TestContext = null, + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with the active step + /// context and the specified step result pushed on top of it. + /// + /// + /// Can't be called if neither fixture, nor test context is active. + /// + /// + /// A new step result to push on top of the step context. + /// + /// + /// A new instance of with the modified + /// (always active) step context. + /// + /// + /// + internal AllureContext WithStep(StepResult stepResult) => + this with + { + StepContext = this.StepContext.Push( + this.ValidateNewStep( + stepResult ?? throw new ArgumentNullException( + nameof(stepResult) + ) + ) + ) + }; + + /// + /// Creates a new without the most recently + /// added step in its step context. Requires an active step context. + /// Deactivates a step context if it consists of one step only before + /// the call. + /// + /// + /// A new instance of with the modified + /// (possibly inactive) step context. + /// + /// + internal AllureContext WithNoLastStep() => + this with + { + StepContext = this.HasStep + ? this.StepContext.Pop() + : throw new InvalidOperationException( + "Unable to deactivate the step context because it " + + "isn't active." + ) + }; + + AllureContext ValidateContainerContextCanBeModified() + { + if (this.FixtureContext is not null) + { + throw new InvalidOperationException( + "Unable to change the container context because the " + + "fixture context is active." + ); + } + + if (this.TestContext is not null) + { + throw new InvalidOperationException( + "Unable to change the container context because the " + + "test context is active." + ); + } + + return this; + } + + AllureContext ValidateContainerCanBeRemoved() + { + if (!this.HasContainer) + { + throw new InvalidOperationException( + "Unable to deactivate the container context because it " + + "is not active." + ); + } + + return this.ValidateContainerContextCanBeModified(); + } + + ExecutableItem? RootStepContainer + { + get => this.FixtureContext as ExecutableItem ?? this.TestContext; + } + + FixtureResult ValidateNewFixtureContext(FixtureResult fixture) + { + if (!this.HasContainer) + { + throw new InvalidOperationException( + "Unable to activate the fixture context " + + "because the container context is not active." + ); + } + + if (this.HasFixture) + { + throw new InvalidOperationException( + "Unable to activate the fixture context " + + "because it's already active." + ); + } + + return fixture; + } + + TestResult ValidateNewTestContext(TestResult testResult) + { + if (this.HasFixture) + { + throw new InvalidOperationException( + "Unable to activate the test context " + + "because the fixture context is active." + ); + } + + if (this.HasTest) + { + throw new InvalidOperationException( + "Unable to activate the test context " + + "because it is already active." + ); + } + + return testResult; + } + + StepResult ValidateNewStep(StepResult stepResult) + { + if (!this.HasTest && !this.HasFixture) + { + throw new InvalidOperationException( + "Unable to activate the step context because neither " + + "test, nor fixture context is active." + ); + } + + return stepResult; + } + + static string RepresentStack( + IImmutableStack stack, + Func projection + ) => string.Join( + " <- ", + stack.Select(projection) + ); +} diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 8bd61249..a8878a43 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -1,369 +1,1018 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; 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; using Newtonsoft.Json.Linq; +#nullable enable + [assembly: InternalsVisibleTo("Allure.Net.Commons.Tests")] -namespace Allure.Net.Commons +namespace Allure.Net.Commons; + +/// +/// A facade that allows to control the Allure context, set up allure model +/// objects and emit output files. +/// +/// +/// This class is primarily intended to be used by a test framework +/// integration. We don't advice to use it from test code unless strictly +/// necessary.

+/// NOTE: Modifications of the Allure context persist until either some +/// method has affect them, or the execution context is restored to the +/// point beyond the call that had introduced them. +///
+public class AllureLifecycle { - public class AllureLifecycle + private readonly Dictionary typeFormatters = new(); + private static readonly Lazy instance = + new(Initialize); + + public IReadOnlyDictionary TypeFormatters => + new ReadOnlyDictionary(typeFormatters); + + readonly AllureStorage storage; + readonly AsyncLocal context = new(); + + readonly IAllureResultsWriter writer; + + /// + /// Protects mutations of shared allure model objects against data + /// races that may otherwise occur because of multithreaded access. + /// + readonly object modelMonitor = new(); + + + /// + /// Captures the current value of Allure context. + /// + public AllureContext Context { - private readonly Dictionary typeFormatters = new(); + get => this.context.Value ??= new AllureContext(); + private set => this.context.Value = value; + } - public IReadOnlyDictionary TypeFormatters => - new ReadOnlyDictionary(typeFormatters); + internal AllureLifecycle() : this(GetConfiguration()) + { + } - private static readonly object Lockobj = new(); - private static AllureLifecycle instance; - private readonly AllureStorage storage; - private readonly IAllureResultsWriter writer; + internal AllureLifecycle( + Func writerFactory + ) : this(GetConfiguration(), writerFactory) + { + } - /// Method to get the key for separation the steps for different tests. - public static Func CurrentTestIdGetter { get; set; } = () => Thread.CurrentThread.ManagedThreadId.ToString(); + internal AllureLifecycle(JObject config) + : this(config, c => new FileSystemResultsWriter(c)) + { + } - internal AllureLifecycle(): this(GetConfiguration()) - { - } - - internal AllureLifecycle(JObject config) - { - JsonConfiguration = config.ToString(); - AllureConfiguration = AllureConfiguration.ReadFromJObject(config); - writer = new FileSystemResultsWriter(AllureConfiguration); - storage = new AllureStorage(); - } + internal AllureLifecycle( + JObject config, + Func writerFactory + ) + { + JsonConfiguration = config.ToString(); + AllureConfiguration = AllureConfiguration.ReadFromJObject(config); + writer = writerFactory(AllureConfiguration); + storage = new AllureStorage(); + } - public string JsonConfiguration { get; private set; } - public AllureConfiguration AllureConfiguration { get; } + public string JsonConfiguration { get; private set; } - public string ResultsDirectory => writer.ToString(); + public AllureConfiguration AllureConfiguration { get; } - public static AllureLifecycle Instance - { - get - { - if (instance == null) - { - lock (Lockobj) - { - if (instance == null) - { - var localInstance = new AllureLifecycle(); - Interlocked.Exchange(ref instance, localInstance); - } - } - } - - return instance; - } - } + public string ResultsDirectory => writer.ToString(); - public void AddTypeFormatter(TypeFormatter typeFormatter) => - AddTypeFormatterImpl(typeof(T), typeFormatter); + public static AllureLifecycle Instance { get => instance.Value; } - private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => - typeFormatters[type] = formatter; + public void AddTypeFormatter(TypeFormatter typeFormatter) => + AddTypeFormatterImpl(typeof(T), typeFormatter); - #region TestContainer + private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => + typeFormatters[type] = formatter; - public virtual AllureLifecycle StartTestContainer(TestResultContainer container) + /// + /// Binds the provided value as the current Allure context and executes + /// the specified function. The context is then restored to the initial + /// value. This allows the Allure context to bypass .NET execution + /// context boundaries. + /// + /// + /// A context that was previously captured with . + /// If it is null, the code is executed in the current context. + /// + /// A code to run. + /// The context after the code is executed. + public AllureContext RunInContext( + AllureContext? context, + Action action + ) + { + if (context is null) { - container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.Put(container.uuid, container); - return this; + action(); + return this.Context; } - public virtual AllureLifecycle StartTestContainer(string parentUuid, TestResultContainer container) + var originalContext = this.Context; + try { - UpdateTestContainer(parentUuid, c => c.children.Add(container.uuid)); - StartTestContainer(container); - return this; + this.Context = context; + action(); + return this.Context; } - - public virtual AllureLifecycle UpdateTestContainer(string uuid, Action update) + finally { - update.Invoke(storage.Get(uuid)); - return this; + this.Context = originalContext; } + } - public virtual AllureLifecycle StopTestContainer(string uuid) - { - UpdateTestContainer(uuid, c => c.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds()); - return this; - } + #region TestContainer + + /// + /// Starts a new test container and pushes it into the container + /// context making the container context active. The container becomes + /// the current one in the current execution context. + /// + /// + /// This method modifies the Allure context.

+ /// Can't be called if the fixture or the test context is active. + ///
+ /// A new test container to start. + /// + public virtual AllureLifecycle StartTestContainer( + TestResultContainer container + ) + { + container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.storage.Put(container.uuid, container); + this.UpdateContext(c => c.WithContainer(container)); + return this; + } - public virtual AllureLifecycle WriteTestContainer(string uuid) + /// + /// Applies the specified update function to the current test container. + /// + /// + /// Requires the container context to be active. + /// + /// + public virtual AllureLifecycle UpdateTestContainer( + Action update + ) + { + var container = this.Context.CurrentContainer; + lock (this.modelMonitor) { - writer.Write(storage.Remove(uuid)); - return this; + update.Invoke(container); } + return this; + } - #endregion + /// + /// Stops the current test container. + /// + /// + /// Requires the container context to be active. + /// + /// + public virtual AllureLifecycle StopTestContainer() + { + UpdateTestContainer(stopContainer); + return this; + } - #region Fixture + /// + /// Writes the current test container and removes it from the context. + /// If there are another test containers in the context, the most + /// recently started one becomes the current container in the current + /// execution context. Otherwise the container context is deactivated. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the container context to be active. + ///
+ /// + public virtual AllureLifecycle WriteTestContainer() + { + var container = this.Context.CurrentContainer; + this.storage.Remove(container.uuid); + this.UpdateContext(c => c.WithNoLastContainer()); + this.writer.Write(container); + return this; + } - public virtual AllureLifecycle StartBeforeFixture(string parentUuid, FixtureResult result, out string uuid) - { - uuid = Guid.NewGuid().ToString("N"); - StartBeforeFixture(parentUuid, uuid, result); - return this; - } + #endregion + + #region Fixture + + /// + /// Starts a new before fixture and activates the fixture context with + /// it. The fixture is set as the current one in the current execution + /// context. Does nothing if the fixture context is already active. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the container context to be active. + ///
+ /// A new fixture. + /// + public virtual AllureLifecycle StartBeforeFixture(FixtureResult result) + { + this.UpdateTestContainer(c => c.befores.Add(result)); + this.StartFixture(result); + return this; + } - public virtual AllureLifecycle StartBeforeFixture(string parentUuid, string uuid, FixtureResult result) - { - UpdateTestContainer(parentUuid, container => container.befores.Add(result)); - StartFixture(uuid, result); - return this; - } + /// + /// Starts a new after fixture and activates the fixture context with + /// it. The fixture is set as the current one in the current execution + /// context. Does nothing if the fixture context is already active. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the container context to be active. + ///
+ /// A new fixture. + /// + public virtual AllureLifecycle StartAfterFixture(FixtureResult result) + { + this.UpdateTestContainer(c => c.afters.Add(result)); + this.StartFixture(result); + return this; + } - public virtual AllureLifecycle StartAfterFixture(string parentUuid, FixtureResult result, out string uuid) + /// + /// Applies the specified update function to the current fixture. + /// + /// + /// Requires the fixture context to be active. + /// + /// + public virtual AllureLifecycle UpdateFixture( + Action update + ) + { + var fixture = this.Context.CurrentFixture; + lock (this.modelMonitor) { - uuid = Guid.NewGuid().ToString("N"); - StartAfterFixture(parentUuid, uuid, result); - return this; + update.Invoke(fixture); } + return this; + } + + /// + /// Stops the current fixture and deactivates the fixture context. + /// + /// + /// A function applied to the fixture result before it is stopped. + /// + /// + /// This method modifies the Allure context.

+ /// Required the fixture context to be active. + ///
+ /// + public virtual AllureLifecycle StopFixture( + Action beforeStop + ) + { + this.UpdateFixture(beforeStop); + return this.StopFixture(); + } + + /// + /// Stops the current fixture and deactivates the fixture context. + /// + /// + /// This method modifies the Allure context.

+ /// Required the fixture context to be active. + ///
+ /// + public virtual AllureLifecycle StopFixture() + { + this.UpdateFixture(stopAllureItem); + this.UpdateContext(c => c.WithNoFixtureContext()); + return this; + } - public virtual AllureLifecycle StartAfterFixture(string parentUuid, string uuid, FixtureResult result) + #endregion + + #region TestCase + + /// + /// Starts a new test and activates the test context with it. The test + /// becomes the current one in the current execution context. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the test context to be active. + ///
+ /// A new test case. + /// + public virtual AllureLifecycle StartTestCase(TestResult testResult) + { + var uuid = testResult.uuid; + var containers = this.Context.ContainerContext; + lock (this.modelMonitor) { - UpdateTestContainer(parentUuid, container => container.afters.Add(result)); - StartFixture(uuid, result); - return this; + foreach (TestResultContainer container in containers) + { + container.children.Add(uuid); + } } + this.storage.Put(uuid, testResult); + this.UpdateContext(c => c.WithTestContext(testResult)); + this.UpdateTestCase(startAllureItem); + return this; + } - public virtual AllureLifecycle UpdateFixture(Action update) + /// + /// Applies the specified update function to the current test. + /// + /// + /// Requires the test context to be active. + /// + /// + public virtual AllureLifecycle UpdateTestCase( + Action update + ) + { + var testResult = this.Context.CurrentTest; + lock (this.modelMonitor) { - UpdateFixture(storage.GetRootStep(), update); - return this; + update(testResult); } + return this; + } - public virtual AllureLifecycle UpdateFixture(string uuid, Action update) + /// + /// Stops the current test. + /// + /// + /// Requires the test context to be active. + /// + /// + /// A function applied to the test result before it is stopped. + /// + /// + public virtual AllureLifecycle StopTestCase( + Action beforeStop + ) => this.UpdateTestCase( + Chain(beforeStop, stopAllureItem) + ); + + /// + /// Stops the current test. + /// + /// + /// Requires the test context to be active. + /// + /// + public virtual AllureLifecycle StopTestCase() => + this.UpdateTestCase(stopAllureItem); + + /// + /// Writes the current test and removes it from the context. The test + /// context is then deactivated. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the test context to be active. + ///
+ /// + public virtual AllureLifecycle WriteTestCase() + { + var testResult = this.Context.CurrentTest; + string uuid; + lock (this.modelMonitor) { - update.Invoke(storage.Get(uuid)); - return this; + uuid = testResult.uuid; } + this.storage.Remove(uuid); + this.UpdateContext(c => c.WithNoTestContext()); + this.writer.Write(testResult); + return this; + } - public virtual AllureLifecycle StopFixture(Action beforeStop) + #endregion + + #region Step + + /// + /// Starts a new step and pushes it into the step context making the + /// step context active. The step becomes the current one in the + /// current execution context. + /// + /// + /// This method modifies the Allure context.

+ /// Requires either the fixture or the test context to be active. + ///
+ /// A new step. + /// + public virtual AllureLifecycle StartStep(StepResult result) + { + var parent = this.Context.CurrentStepContainer; + lock (this.modelMonitor) { - UpdateFixture(beforeStop); - return StopFixture(storage.GetRootStep()); + parent.steps.Add(result); } + this.UpdateContext(c => c.WithStep(result)); + this.UpdateStep(startAllureItem); + return this; + } - public virtual AllureLifecycle StopFixture(string uuid) + /// + /// Applies the specified update function to the current step. + /// + /// + /// Requires the step context to be active. + /// + /// + public virtual AllureLifecycle UpdateStep(Action update) + { + var stepResult = this.Context.CurrentStep; + lock (this.modelMonitor) { - var fixture = storage.Remove(uuid); - storage.ClearStepContext(); - fixture.stage = Stage.finished; - fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - return this; + update.Invoke(stepResult); } + return this; + } - #endregion + /// + /// Stops the current step and removes it from the context. If there + /// are another steps in the context, the most recently started one + /// becomes the current step in the current execution context. + /// Otherwise the step context is deactivated. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the step context to be active. + ///
+ /// + /// A function that is applied to the step result before it is stopped. + /// + /// + public virtual AllureLifecycle StopStep(Action beforeStop) + { + this.UpdateStep(beforeStop); + return this.StopStep(); + } - #region TestCase + /// + /// Stops the current step and removes it from the context. If there + /// are another steps in the context, the most recently started one + /// becomes the current step in the current execution context. + /// Otherwise the step context is deactivated. + /// + /// + /// This method modifies the Allure context.

+ /// Requires the step context to be active. + ///
+ /// + public virtual AllureLifecycle StopStep() + { + this.UpdateStep(stopAllureItem); + this.UpdateContext(c => c.WithNoLastStep()); + return this; + } - public virtual AllureLifecycle StartTestCase(string containerUuid, TestResult testResult) - { - UpdateTestContainer(containerUuid, c => c.children.Add(testResult.uuid)); - return StartTestCase(testResult); - } + #endregion - public virtual AllureLifecycle StartTestCase(TestResult testResult) - { - testResult.stage = Stage.running; - testResult.start = testResult.start == 0L ? DateTimeOffset.Now.ToUnixTimeMilliseconds() : testResult.start; - storage.Put(testResult.uuid, testResult); - storage.ClearStepContext(); - storage.StartStep(testResult.uuid); - return this; - } + #region Attachment - public virtual AllureLifecycle UpdateTestCase(string uuid, Action update) - { - update.Invoke(storage.Get(uuid)); - return this; - } + // TODO: read file in background thread + public virtual AllureLifecycle AddAttachment( + string name, + string type, + string path + ) + { + var fileExtension = new FileInfo(path).Extension; + return this.AddAttachment( + name, + type, + File.ReadAllBytes(path), + fileExtension + ); + } - public virtual AllureLifecycle UpdateTestCase(Action update) + public virtual AllureLifecycle AddAttachment( + string name, + string type, + byte[] content, + string fileExtension = "" + ) + { + var suffix = AllureConstants.ATTACHMENT_FILE_SUFFIX; + var source = $"{CreateUuid()}{suffix}{fileExtension}"; + var attachment = new Attachment { - return UpdateTestCase(storage.GetRootStep(), update); - } - - public virtual AllureLifecycle StopTestCase(Action beforeStop) + name = name, + type = type, + source = source + }; + this.writer.Write(source, content); + var target = this.Context.CurrentStepContainer; + lock (this.modelMonitor) { - UpdateTestCase(beforeStop); - return StopTestCase(storage.GetRootStep()); + target.attachments.Add(attachment); } + return this; + } - public virtual AllureLifecycle StopTestCase(string uuid) - { - var testResult = storage.Get(uuid); - testResult.stage = Stage.finished; - testResult.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.ClearStepContext(); - return this; - } + public virtual AllureLifecycle AddAttachment( + string path, + string? name = null + ) + { + name ??= Path.GetFileName(path); + var type = MimeTypesMap.GetMimeType(path); + return AddAttachment(name, type, path); + } - public virtual AllureLifecycle WriteTestCase(string uuid) - { - writer.Write(storage.Remove(uuid)); - return this; - } + #endregion - #endregion + #region Extensions - #region Step + public virtual void CleanupResultDirectory() + { + writer.CleanUp(); + } - public virtual AllureLifecycle StartStep(StepResult result, out string uuid) - { - uuid = Guid.NewGuid().ToString("N"); - StartStep(storage.GetCurrentStep(), uuid, result); - return this; - } - - public virtual AllureLifecycle StartStep(string uuid, StepResult result) - { - StartStep(storage.GetCurrentStep(), uuid, result); - return this; - } + /// + /// Attaches screen diff images to the current test case. + /// + /// + /// Requires the test context to be active. + /// + /// A path to the actual screen. + /// A path to the expected screen. + /// A path to the screen diff. + /// + public virtual AllureLifecycle AddScreenDiff( + string expectedPng, + string actualPng, + string diffPng + ) => this.AddAttachment(expectedPng, "expected") + .AddAttachment(actualPng, "actual") + .AddAttachment(diffPng, "diff") + .UpdateTestCase( + x => x.labels.Add(Label.TestType("screenshotDiff")) + ); + + #endregion + + + #region Privates + + static AllureLifecycle Initialize() => new(); + + private static JObject GetConfiguration() + { + var configEnvVarName = AllureConstants.ALLURE_CONFIG_ENV_VARIABLE; + var jsonConfigPath = Environment.GetEnvironmentVariable( + configEnvVarName + ); - public virtual AllureLifecycle StartStep(string parentUuid, string uuid, StepResult stepResult) + if (jsonConfigPath != null && !File.Exists(jsonConfigPath)) { - stepResult.stage = Stage.running; - stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.StartStep(uuid); - storage.AddStep(parentUuid, uuid, stepResult); - return this; + throw new FileNotFoundException( + $"Couldn't find '{jsonConfigPath}' specified " + + $"in {configEnvVarName} environment variable" + ); } - public virtual AllureLifecycle UpdateStep(Action update) + if (File.Exists(jsonConfigPath)) { - update.Invoke(storage.Get(storage.GetCurrentStep())); - return this; + return JObject.Parse(File.ReadAllText(jsonConfigPath)); } - public virtual AllureLifecycle UpdateStep(string uuid, Action update) - { - update.Invoke(storage.Get(uuid)); - return this; - } + var defaultJsonConfigPath = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + AllureConstants.CONFIG_FILENAME + ); - public virtual AllureLifecycle StopStep(Action beforeStop) + if (File.Exists(defaultJsonConfigPath)) { - UpdateStep(beforeStop); - return StopStep(storage.GetCurrentStep()); + return JObject.Parse(File.ReadAllText(defaultJsonConfigPath)); } - public virtual AllureLifecycle StopStep(string uuid) + return JObject.Parse("{}"); + } + + private void StartFixture(FixtureResult fixtureResult) + { + this.UpdateContext(c => c.WithFixtureContext(fixtureResult)); + this.UpdateFixture(startAllureItem); + } + + static readonly Action stopContainer = + c => c.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + static readonly Action startAllureItem = + item => { - var step = storage.Remove(uuid); - step.stage = Stage.finished; - step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.StopStep(); - return this; - } + item.stage = Stage.running; + item.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }; - public virtual AllureLifecycle StopStep() + static readonly Action stopAllureItem = + item => { - StopStep(storage.GetCurrentStep()); - return this; - } + item.stage = Stage.finished; + item.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }; - #endregion + void UpdateContext(Func updateFn) + { + this.Context = updateFn(this.Context); + } - #region Attachment + static string CreateUuid() => + Guid.NewGuid().ToString("N"); - // TODO: read file in background thread - public virtual AllureLifecycle AddAttachment(string name, string type, string path) + static Action Chain(params Action[] actions) => v => + { + foreach (var action in actions) { - var fileExtension = new FileInfo(path).Extension; - return AddAttachment(name, type, File.ReadAllBytes(path), fileExtension); + action(v); } + }; - public virtual AllureLifecycle AddAttachment(string name, string type, byte[] content, - string fileExtension = "") - { - var source = $"{Guid.NewGuid().ToString("N")}{AllureConstants.ATTACHMENT_FILE_SUFFIX}{fileExtension}"; - var attachment = new Attachment - { - name = name, - type = type, - source = source - }; - writer.Write(source, content); - storage.Get(storage.GetCurrentStep()).attachments.Add(attachment); - return this; - } + #endregion + + #region Obsoleted + + internal const string EXPLICIT_STATE_MGMT_OBSOLETE = + "Explicit allure state management is obsolete. Methods with " + + "explicit uuid parameters will be removed in the future. Use " + + "their counterparts without uuids to manipulate the current" + + " context."; + + internal const string API_RUDIMENT_OBSOLETE_MSG = + "This is a rudimentary part of the API. It has no " + + "effect and will be removed in the future."; - public virtual AllureLifecycle AddAttachment(string path, string name = null) + [Obsolete(API_RUDIMENT_OBSOLETE_MSG)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static Func? CurrentTestIdGetter { get; set; } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartTestContainer( + string parentUuid, + TestResultContainer container + ) + { + this.UpdateTestContainer( + parentUuid, + c => c.children.Add(container.uuid) + ); + this.StartTestContainer(container); + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateTestContainer( + string uuid, + Action update + ) + { + var container = this.storage.Get(uuid); + lock (this.modelMonitor) { - name = name ?? Path.GetFileName(path); - var type = MimeTypesMap.GetMimeType(path); - return AddAttachment(name, type, path); + update.Invoke(container); } + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopTestContainer(string uuid) => + this.UpdateTestContainer(uuid, stopContainer); + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle WriteTestContainer(string uuid) + { + var container = this.storage.Remove(uuid); + this.UpdateContext(c => ContextWithNoContainer(c, uuid)); + this.writer.Write(container); + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + this.StartBeforeFixture(uuid, result); + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string uuid, + FixtureResult result + ) + { + this.UpdateTestContainer(c => c.befores.Add(result)); + this.StartFixture(uuid, result); + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string parentUuid, + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + this.StartBeforeFixture(parentUuid, uuid, result); + return this; + } + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string parentUuid, + string uuid, + FixtureResult result + ) + { + this.UpdateTestContainer(parentUuid, c => c.befores.Add(result)); + this.StartFixture(uuid, result); + return this; + } - #endregion + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartAfterFixture( + string parentUuid, + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + this.StartAfterFixture(parentUuid, uuid, result); + return this; + } - #region Extensions + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartAfterFixture( + string parentUuid, + string uuid, + FixtureResult result + ) + { + this.UpdateTestContainer(parentUuid, c => c.afters.Add(result)); + this.StartFixture(uuid, result); + return this; + } - public virtual void CleanupResultDirectory() + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateFixture( + string uuid, + Action update + ) + { + var fixture = this.storage.Get(uuid); + lock (this.modelMonitor) { - writer.CleanUp(); + update.Invoke(fixture); } + return this; + } - public virtual AllureLifecycle AddScreenDiff(string testCaseUuid, string expectedPng, string actualPng, - string diffPng) + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopFixture(string uuid) + { + this.UpdateFixture(uuid, stopAllureItem); + var fixture = this.storage.Remove(uuid); + if (ReferenceEquals(fixture, this.Context.FixtureContext)) { - AddAttachment(expectedPng, "expected") - .AddAttachment(actualPng, "actual") - .AddAttachment(diffPng, "diff") - .UpdateTestCase(testCaseUuid, x => x.labels.Add(Label.TestType("screenshotDiff"))); - - return this; + this.UpdateContext(c => c.WithNoFixtureContext()); } + return this; + } - #endregion + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartTestCase( + string containerUuid, + TestResult testResult + ) + { + this.UpdateTestContainer( + containerUuid, + c => c.children.Add(testResult.uuid) + ); + return this.StartTestCase(testResult); + } + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateTestCase( + string uuid, + Action update + ) + { + var testResult = this.storage.Get(uuid); + lock (this.modelMonitor) + { + update(testResult); + } + return this; + } - #region Privates + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopTestCase(string uuid) => + this.UpdateTestCase(uuid, stopAllureItem); - private static JObject GetConfiguration() + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle WriteTestCase(string uuid) + { + var testResult = this.storage.Remove(uuid); + if (this.Context.TestContext?.uuid == uuid) { - var jsonConfigPath = Environment.GetEnvironmentVariable(AllureConstants.ALLURE_CONFIG_ENV_VARIABLE); + this.UpdateContext(c => c.WithNoTestContext()); + } + this.writer.Write(testResult); + return this; + } - if (jsonConfigPath != null && !File.Exists(jsonConfigPath)) - throw new FileNotFoundException( - $"Couldn't find '{jsonConfigPath}' specified in {AllureConstants.ALLURE_CONFIG_ENV_VARIABLE} environment variable"); + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + StepResult result, + out string uuid + ) + { + uuid = CreateUuid(); + this.StartStep(this.Context.CurrentStepContainer, uuid, result); + return this; + } - if (File.Exists(jsonConfigPath)) - return JObject.Parse(File.ReadAllText(jsonConfigPath)); + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + string uuid, + StepResult result + ) => this.StartStep(this.Context.CurrentStepContainer, uuid, result); + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + string parentUuid, + string uuid, + StepResult stepResult + ) => this.StartStep( + this.storage.Get(parentUuid), + uuid, + stepResult + ); + + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateStep( + string uuid, + Action update + ) + { + var stepResult = storage.Get(uuid); + lock (this.modelMonitor) + { + update.Invoke(stepResult); + } + return this; + } - var defaultJsonConfigPath = - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AllureConstants.CONFIG_FILENAME); + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopStep(string uuid) + { + this.UpdateStep(uuid, stopAllureItem); + var stepResult = this.storage.Remove(uuid); + this.UpdateContext(c => ContextWithNoStep(c, stepResult)); + return this; + } - if (File.Exists(defaultJsonConfigPath)) - return JObject.Parse(File.ReadAllText(defaultJsonConfigPath)); + [Obsolete(EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle AddScreenDiff( + string testCaseUuid, + string expectedPng, + string actualPng, + string diffPng + ) => this.AddAttachment(expectedPng, "expected") + .AddAttachment(actualPng, "actual") + .AddAttachment(diffPng, "diff") + .UpdateTestCase( + testCaseUuid, + x => x.labels.Add(Label.TestType("screenshotDiff")) + ); + + [Obsolete] + void StartFixture(string uuid, FixtureResult fixtureResult) + { + this.storage.Put(uuid, fixtureResult); + this.UpdateContext(c => c.WithFixtureContext(fixtureResult)); + this.UpdateStep(uuid, startAllureItem); + } - return JObject.Parse("{}"); + [Obsolete] + AllureLifecycle StartStep( + ExecutableItem parent, + string uuid, + StepResult stepResult + ) + { + lock (this.modelMonitor) + { + parent.steps.Add(stepResult); } + this.storage.Put(uuid, stepResult); + this.UpdateContext(c => c.WithStep(stepResult)); + this.UpdateStep(uuid, startAllureItem); + return this; + } - private void StartFixture(string uuid, FixtureResult fixtureResult) + [Obsolete] + static AllureContext ContextWithNoContainer( + AllureContext context, + string uuid + ) + { + var containersToPushAgain = new Stack(); + while (context.CurrentContainer.uuid != uuid) + { + containersToPushAgain.Push(context.CurrentContainer); + context = context.WithNoLastContainer(); + if (context.ContainerContext.IsEmpty) + { + throw new InvalidOperationException( + $"Container {uuid} is not in the current context" + ); + } + } + context = context.WithNoLastContainer(); + while (containersToPushAgain.Any()) { - storage.Put(uuid, fixtureResult); - fixtureResult.stage = Stage.running; - fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.ClearStepContext(); - storage.StartStep(uuid); + context = context.WithContainer( + containersToPushAgain.Pop() + ); } + return context; + } - #endregion + [Obsolete] + static AllureContext ContextWithNoStep( + AllureContext context, + StepResult stepResult + ) + { + var stepsToPushAgain = new Stack(); + while (!ReferenceEquals(context.CurrentStep, stepResult)) + { + stepsToPushAgain.Push(context.CurrentStep); + context = context.WithNoLastStep(); + if (context.StepContext.IsEmpty) + { + throw new InvalidOperationException( + $"Step {stepResult.name} is not in the current context" + ); + } + } + context = context.WithNoLastStep(); + while (stepsToPushAgain.Any()) + { + context = context.WithStep( + stepsToPushAgain.Pop() + ); + } + return context; } + + #endregion } \ No newline at end of file diff --git a/Allure.Net.Commons/Internal/IsExternalInit.cs b/Allure.Net.Commons/Internal/IsExternalInit.cs new file mode 100644 index 00000000..21a4cae7 --- /dev/null +++ b/Allure.Net.Commons/Internal/IsExternalInit.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// This class serves as an init-only setter modreq to make a library that +/// uses init only setters compile against pre-net5.0 TFMs (including .NET +/// Standard). See +/// +/// this article +/// +/// and +/// +/// this answer +/// +/// for more details. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit { } diff --git a/Allure.Net.Commons/README.md b/Allure.Net.Commons/README.md index 1e86d968..62f72856 100644 --- a/Allure.Net.Commons/README.md +++ b/Allure.Net.Commons/README.md @@ -10,13 +10,14 @@ Allure lifecycle is configured via json file with default name `allureConfig.jso - set ALLURE_CONFIG environment variable to the full path of json config file. This option is preferable for .net core projects which utilize nuget libraries directly from nuget packages folder. See this example of setting it via code: https://github.com/allure-framework/allure-csharp/blob/bdf11bd3e1f41fd1e4a8fd22fa465b90b68e9d3f/Allure.Commons.NetCore.Tests/AllureConfigTests.cs#L13-L15 - place `allureConfig.json` to the location of `Allure.Commons.dll`. This option can be used with .net classic projects which copy all referenced package libraries into binary folder. Do not forget to set 'Copy to Output Directory' property to 'Copy always' or 'Copy if newer' in your test project or set it in .csproj: -``` - - -PreserveNewest - - -``` + ```xml + + + PreserveNewest + + + ``` + Allure lifecycle will start with default configuration settings if `allureConfig.json` is not found. Raw json configuration can be accessed from `AllureLifeCycle.Instance.JsonConfiguration` to extend configuration by adapters. See extension example here: https://github.com/allure-framework/allure-csharp/blob/bdf11bd3e1f41fd1e4a8fd22fa465b90b68e9d3f/Allure.SpecFlowPlugin/PluginHelper.cs#L20-L29 @@ -39,13 +40,14 @@ Allure configuration section is used to setup output directory and link patterns } } ``` -All -Link pattern placeholders will be replaced with URL value of corresponding link type, e.g. + +All link pattern placeholders will be replaced with URL value of corresponding link type, e.g. `link(type: "issue", url: "BUG-01") => https://example.org/BUG-01` ### AllureLifecycle -[AllureLifecycle](https://github.com/allure-framework/allure-csharp/blob/main/Allure.Commons/AllureLifecycle.cs) class provides methods for test engine events processing. +[AllureLifecycle](https://github.com/allure-framework/allure-csharp/blob/main/Allure.Commons/AllureLifecycle.cs) +class provides methods for test engine events processing. Use `AllureLifecycle.Instance` property to access. @@ -61,7 +63,7 @@ Use `AllureLifecycle.Instance` property to access. * StopTestCase * WriteTestCase -### Step Events +#### Step Events * StartStep * UpdateStep * StopStep @@ -73,5 +75,50 @@ Use `AllureLifecycle.Instance` property to access. #### Utility Methods * CleanupResultDirectory - can be used in test run setup to clean old result files +#### Context capturing +The methods above operate on the current Allure context. This context +flows naturally as a part of ExecutionContext and is subject to the same +constraints. Particularly, changes made in an async callee can't be observed +by the caller. + +Use the following methods of `AllureLifecycle` to capture Allure context and +to operate on a context later, after it has been captured: + +* Context +* RunInContext + +Example: + +```csharp +public static async Task Caller(ScenarioContext scenario) +{ + await Callee(scenario); + AllureLifecycle.Instance.RunInContext( + scenario.Get(), + () => + { + // The test context required by the below methods wouldn't be + // visible if they weren't wrapped with RunInContext. + AllureLifecycle.Instance.StopTestCase(); + AllureLifecycle.Instance.WriteTestCase(); + } + ); +} + +public static async Task Callee(ScenarioContext scenario) +{ + AllureLifecycle.Instance.StartTestCase( + new(){ uuid = Guid.NewGuid().ToString() } + ); + + // Pass Allure context to the caller via ScenarioContext + scenario.Set(AllureLifecycle.Instance.Context); +} +``` + +#### Obsoleted methods +Methods with explicit uuid parameters are deprecated. Migrate to their +uuid-less counterparts that operate on the current Allure context. + ### Troubleshooting ... diff --git a/Allure.Net.Commons/Steps/AllureStepAspect.cs b/Allure.Net.Commons/Steps/AllureStepAspect.cs index e5b6aaef..6faaed49 100644 --- a/Allure.Net.Commons/Steps/AllureStepAspect.cs +++ b/Allure.Net.Commons/Steps/AllureStepAspect.cs @@ -28,25 +28,23 @@ public abstract class AllureAbstractStepAspect public static List ExceptionTypes { get; set; } - private static string StartStep(MethodBase metadata, string stepName, List stepParameters) + private static void StartStep(MethodBase metadata, string stepName, List stepParameters) { if (metadata.GetCustomAttribute() != null) { - return CoreStepsHelper.StartStep(stepName, step => step.parameters = stepParameters); + CoreStepsHelper.StartStep(stepName, step => step.parameters = stepParameters); } - - return null; } - private static void PassStep(string uuid, MethodBase metadata) + private static void PassStep(MethodBase metadata) { if (metadata.GetCustomAttribute() != null) { - CoreStepsHelper.PassStep(uuid); + CoreStepsHelper.PassStep(); } } - private static void ThrowStep(string uuid, MethodBase metadata, Exception e) + private static void ThrowStep(MethodBase metadata, Exception e) { if (metadata.GetCustomAttribute() != null) { @@ -58,25 +56,23 @@ private static void ThrowStep(string uuid, MethodBase metadata, Exception e) if (ExceptionTypes.Any(exceptionType => exceptionType.IsInstanceOfType(e))) { - CoreStepsHelper.FailStep(uuid, result => result.statusDetails = exceptionStatusDetails); + CoreStepsHelper.FailStep(result => result.statusDetails = exceptionStatusDetails); return; } - CoreStepsHelper.BrokeStep(uuid, result => result.statusDetails = exceptionStatusDetails); + CoreStepsHelper.BrokeStep(result => result.statusDetails = exceptionStatusDetails); } } - private static void StartFixture(MethodBase metadata, string stepName) + private static void StartFixture(MethodBase metadata, string fixtureName) { if (metadata.GetCustomAttribute(inherit: true) != null) { - Console.Out.WriteLine("QWAQWA"); - // throw new Exception("BEFORE FIXTURE"); - CoreStepsHelper.StartBeforeFixture(stepName); + CoreStepsHelper.StartBeforeFixture(fixtureName); } if (metadata.GetCustomAttribute(inherit: true) != null) { - CoreStepsHelper.StartAfterFixture(stepName); + CoreStepsHelper.StartAfterFixture(fixtureName); } } @@ -85,15 +81,8 @@ private static void PassFixture(MethodBase metadata) if (metadata.GetCustomAttribute(inherit: true) != null || metadata.GetCustomAttribute(inherit: true) != null) { - if (metadata.Name == "InitializeAsync") - { - CoreStepsHelper.StopFixtureSuppressTestCase(result => result.status = Status.passed); - } - else - { - CoreStepsHelper.StopFixture(result => result.status = Status.passed); - } - + CoreStepsHelper.StopFixture(result => result.status = Status.passed); + // TODO: NUnit doing it this way: to be reviewed (!) DO NOT MERGE // CoreStepsHelper.StopFixtureSuppressTestCase(result => result.status = Status.passed); } @@ -110,47 +99,33 @@ private static void ThrowFixture(MethodBase metadata, Exception e) trace = e.StackTrace }; - if (metadata.Name == "InitializeAsync") + CoreStepsHelper.StopFixture(result => { - CoreStepsHelper.StopFixtureSuppressTestCase(result => - { - result.status = ExceptionTypes.Any(exceptionType => exceptionType.IsInstanceOfType(e)) - ? Status.failed - : Status.broken; - result.statusDetails = exceptionStatusDetails; - }); - } - else - { - CoreStepsHelper.StopFixture(result => - { - result.status = ExceptionTypes.Any(exceptionType => exceptionType.IsInstanceOfType(e)) - ? Status.failed - : Status.broken; - result.statusDetails = exceptionStatusDetails; - }); - } + result.status = ExceptionTypes.Any(exceptionType => exceptionType.IsInstanceOfType(e)) + ? Status.failed + : Status.broken; + result.statusDetails = exceptionStatusDetails; + }); } } // ------------------------------ - private static string BeforeTargetInvoke(MethodBase metadata, string stepName, List stepParameters) + private static void BeforeTargetInvoke(MethodBase metadata, string stepName, List stepParameters) { StartFixture(metadata, stepName); - var stepUuid = StartStep(metadata, stepName, stepParameters); - return stepUuid; + StartStep(metadata, stepName, stepParameters); } - private static void AfterTargetInvoke(string stepUuid, MethodBase metadata) + private static void AfterTargetInvoke(MethodBase metadata) { - PassStep(stepUuid, metadata); + PassStep(metadata); PassFixture(metadata); } - private static void OnTargetInvokeException(string stepUuid, MethodBase metadata, Exception e) + private static void OnTargetInvokeException(MethodBase metadata, Exception e) { - ThrowStep(stepUuid, metadata, e); + ThrowStep(metadata, e); ThrowFixture(metadata, e); } @@ -164,19 +139,17 @@ private static T WrapSync( List stepParameters ) { - string stepUuid = null; - try { - stepUuid = BeforeTargetInvoke(metadata, stepName, stepParameters); + BeforeTargetInvoke(metadata, stepName, stepParameters); var result = (T)target(args); - AfterTargetInvoke(stepUuid, metadata); + AfterTargetInvoke(metadata); return result; } catch (Exception e) { - OnTargetInvokeException(stepUuid, metadata, e); + OnTargetInvokeException(metadata, e); throw; } } @@ -189,17 +162,15 @@ private static void WrapSyncVoid( List stepParameters ) { - string stepUuid = null; - try { - stepUuid = BeforeTargetInvoke(metadata, stepName, stepParameters); + BeforeTargetInvoke(metadata, stepName, stepParameters); target(args); - AfterTargetInvoke(stepUuid, metadata); + AfterTargetInvoke(metadata); } catch (Exception e) { - OnTargetInvokeException(stepUuid, metadata, e); + OnTargetInvokeException(metadata, e); throw; } } @@ -212,17 +183,15 @@ private static async Task WrapAsync( List stepParameters ) { - string stepUuid = null; - try { - stepUuid = BeforeTargetInvoke(metadata, stepName, stepParameters); + BeforeTargetInvoke(metadata, stepName, stepParameters); await ((Task)target(args)).ConfigureAwait(false); - AfterTargetInvoke(stepUuid, metadata); + AfterTargetInvoke(metadata); } catch (Exception e) { - OnTargetInvokeException(stepUuid, metadata, e); + OnTargetInvokeException(metadata, e); throw; } } @@ -235,19 +204,17 @@ private static async Task WrapAsyncGeneric( List stepParameters ) { - string stepUuid = null; - try { - stepUuid = BeforeTargetInvoke(metadata, stepName, stepParameters); + BeforeTargetInvoke(metadata, stepName, stepParameters); var result = await ((Task)target(args)).ConfigureAwait(false); - AfterTargetInvoke(stepUuid, metadata); + AfterTargetInvoke(metadata); return result; } catch (Exception e) { - OnTargetInvokeException(stepUuid, metadata, e); + OnTargetInvokeException(metadata, e); throw; } } diff --git a/Allure.Net.Commons/Steps/CoreStepsHelper.cs b/Allure.Net.Commons/Steps/CoreStepsHelper.cs index 7f124ae2..8673446b 100644 --- a/Allure.Net.Commons/Steps/CoreStepsHelper.cs +++ b/Allure.Net.Commons/Steps/CoreStepsHelper.cs @@ -1,289 +1,304 @@ using System; -using System.Threading; +using System.ComponentModel; using System.Threading.Tasks; using Allure.Net.Commons.Storage; -namespace Allure.Net.Commons.Steps +#nullable enable + +namespace Allure.Net.Commons.Steps; + +public class CoreStepsHelper { - public class CoreStepsHelper + public static IStepLogger? StepLogger { get; set; } + + #region Fixtures + + public static void StartBeforeFixture(string name) { - public static IStepLogger StepLogger { get; set; } + AllureLifecycle.Instance.StartBeforeFixture(new() { name = name }); + StepLogger?.BeforeStarted?.Log(name); + } - private static readonly AsyncLocal TestResultAccessorAsyncLocal = new(); + public static void StartAfterFixture(string name) + { + AllureLifecycle.Instance.StartAfterFixture(new() { name = name }); + StepLogger?.AfterStarted?.Log(name); + } - public static ITestResultAccessor TestResultAccessor - { - get => TestResultAccessorAsyncLocal.Value; - set => TestResultAccessorAsyncLocal.Value = value; - } - - #region Fixtures + public static void StopFixture(Action updateResults) => + AllureLifecycle.Instance.StopFixture(updateResults); - public static string StartBeforeFixture(string name) - { - var fixtureResult = new FixtureResult() - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - - AllureLifecycle.Instance.StartBeforeFixture(TestResultAccessor.TestResultContainer.uuid, fixtureResult, out var uuid); - StepLogger?.BeforeStarted?.Log(name); - return uuid; - } + public static void StopFixture() => + AllureLifecycle.Instance.StopFixture(); - public static string StartAfterFixture(string name) - { - var fixtureResult = new FixtureResult() - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - - AllureLifecycle.Instance.StartAfterFixture(TestResultAccessor.TestResultContainer.uuid, fixtureResult, out var uuid); - StepLogger?.AfterStarted?.Log(name); - return uuid; - } + #endregion - public static void StopFixture(Action updateResults = null) - { - AllureLifecycle.Instance.StopFixture(result => - { - result.stage = Stage.finished; - result.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - updateResults?.Invoke(result); - }); - } - - public static void StopFixtureSuppressTestCase(Action updateResults = null) - { - var newTestResult = TestResultAccessor.TestResult; - StopFixture(updateResults); - AllureLifecycle.Instance.StartTestCase(TestResultAccessor.TestResultContainer.uuid, newTestResult); - } + #region Steps - #endregion + public static void StartStep(string name) + { + AllureLifecycle.Instance.StartStep(new() { name = name }); + StepLogger?.StepStarted?.Log(name); + } - #region Steps + public static void StartStep(string name, Action updateResults) + { + StartStep(name); + AllureLifecycle.Instance.UpdateStep(updateResults); + } - public static string StartStep(string name, Action updateResults = null) + public static void PassStep() => AllureLifecycle.Instance.StopStep( + result => { - var stepResult = new StepResult - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - updateResults?.Invoke(stepResult); - - AllureLifecycle.Instance.StartStep(stepResult, out var uuid); - StepLogger?.StepStarted?.Log(name); - return uuid; + result.status = Status.passed; + StepLogger?.StepPassed?.Log(result.name); } + ); - public static void PassStep(Action updateResults = null) - { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.passed; - updateResults?.Invoke(result); - StepLogger?.StepPassed?.Log(result.name); - }); - } + public static void PassStep(Action updateResults) + { + AllureLifecycle.Instance.UpdateStep(updateResults); + PassStep(); + } - public static void PassStep(string uuid, Action updateResults = null) + public static void FailStep() => AllureLifecycle.Instance.StopStep( + result => { - AllureLifecycle.Instance.UpdateStep(uuid, result => - { - result.status = Status.passed; - updateResults?.Invoke(result); - StepLogger?.StepPassed?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); + result.status = Status.failed; + StepLogger?.StepFailed?.Log(result.name); } + ); - public static void FailStep(Action updateResults = null) - { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.failed; - updateResults?.Invoke(result); - StepLogger?.StepFailed?.Log(result.name); - }); - } + public static void FailStep(Action updateResults) + { + AllureLifecycle.Instance.UpdateStep(updateResults); + FailStep(); + } - public static void FailStep(string uuid, Action updateResults = null) + public static void BrokeStep() => AllureLifecycle.Instance.StopStep( + result => { - AllureLifecycle.Instance.UpdateStep(uuid, result => - { - result.status = Status.failed; - updateResults?.Invoke(result); - StepLogger?.StepFailed?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); - } - - public static void BrokeStep(Action updateResults = null) - { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.broken; - updateResults?.Invoke(result); - StepLogger?.StepBroken?.Log(result.name); - }); - } - - public static void BrokeStep(string uuid, Action updateResults = null) - { - AllureLifecycle.Instance.UpdateStep(uuid, result => - { - result.status = Status.broken; - updateResults?.Invoke(result); - StepLogger?.StepBroken?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); + result.status = Status.broken; + StepLogger?.StepBroken?.Log(result.name); } + ); + + public static void BrokeStep(Action updateResults) + { + AllureLifecycle.Instance.UpdateStep(updateResults); + BrokeStep(); + } - #endregion + #endregion - #region Misc + #region Misc - public static void UpdateTestResult(Action update) - { - AllureLifecycle.Instance.UpdateTestCase(TestResultAccessor.TestResult.uuid, update); - } + public static void UpdateTestResult(Action update) => + AllureLifecycle.Instance.UpdateTestCase(update); - #endregion + #endregion - public static Task Step(string name, Func> action) + public static Task Step(string name, Func> action) + { + StartStep(name); + return Execute(action); + } + + public static T Step(string name, Func action) + { + StartStep(name); + return Execute(name, action); + } + + public static void Step(string name, Action action) + { + Step(name, (Func)(() => { - StartStep(name); - return Execute(action); - } + action(); + return null; + })); + } - public static T Step(string name, Func action) + public static Task Step(string name, Func action) + { + return Step(name, async () => { - StartStep(name); - return Execute(name, action); - } + await action(); + return Task.FromResult(null); + }); + } - public static void Step(string name, Action action) + public static void Step(string name) + { + Step(name, () => { }); + } + + public static Task Before(string name, Func> action) + { + StartBeforeFixture(name); + return Execute(action); + } + + public static T Before(string name, Func action) + { + StartBeforeFixture(name); + return Execute(name, action); + } + + public static void Before(string name, Action action) + { + Before(name, (Func)(() => { - Step(name, (Func) (() => - { - action(); - return null; - })); - } + action(); + return null; + })); + } - public static Task Step(string name, Func action) + public static Task Before(string name, Func action) + { + return Before(name, async () => { - return Step(name, async () => - { - await action(); - return Task.FromResult(null); - }); - } + await action(); + return Task.FromResult(null); + }); + } + + public static Task After(string name, Func> action) + { + StartAfterFixture(name); + return Execute(action); + } + + public static T After(string name, Func action) + { + StartAfterFixture(name); + return Execute(name, action); + } - public static void Step(string name) + public static void After(string name, Action action) + { + After(name, (Func)(() => { - Step(name, () => { }); - } + action(); + return null; + })); + } - public static Task Before(string name, Func> action) + public static Task After(string name, Func action) + { + return After(name, async () => { - StartBeforeFixture(name); - return Execute(action); - } + await action(); + return Task.FromResult(null); + }); + } - public static T Before(string name, Func action) + private static async Task Execute(Func> action) + { + T result; + try { - StartBeforeFixture(name); - return Execute(name, action); + result = await action(); } - - public static void Before(string name, Action action) + catch (Exception) { - Before(name, (Func) (() => - { - action(); - return null; - })); + FailStep(); + throw; } - public static Task Before(string name, Func action) + PassStep(); + return result; + } + + private static T Execute(string name, Func action) + { + T result; + try { - return Before(name, async () => - { - await action(); - return Task.FromResult(null); - }); + result = action(); } - - public static Task After(string name, Func> action) + catch (Exception e) { - StartAfterFixture(name); - return Execute(action); + FailStep(); + throw new StepFailedException(name, e); } - public static T After(string name, Func action) + PassStep(); + return result; + } + + #region Obsoleted + + [Obsolete(AllureLifecycle.API_RUDIMENT_OBSOLETE_MSG)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static ITestResultAccessor? TestResultAccessor { get; set; } + + [Obsolete( + "This method is a rudimentary part of the API and will be removed " + + "in the future. Use the StopFixture method instead." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void StopFixtureSuppressTestCase( + Action? updateResults = null + ) + { + if (updateResults == null) { - StartAfterFixture(name); - return Execute(name, action); + StopFixture(); } - - public static void After(string name, Action action) + else { - After(name, (Func) (() => - { - action(); - return null; - })); + StopFixture(updateResults); } + } - public static Task After(string name, Func action) + [Obsolete(AllureLifecycle.EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void PassStep( + string uuid, + Action? updateResults = null + ) + { + AllureLifecycle.Instance.UpdateStep(uuid, result => { - return After(name, async () => - { - await action(); - return Task.FromResult(null); - }); - } + result.status = Status.passed; + updateResults?.Invoke(result); + StepLogger?.StepPassed?.Log(result.name); + }); + AllureLifecycle.Instance.StopStep(uuid); + } - private static async Task Execute(Func> action) + [Obsolete(AllureLifecycle.EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void FailStep( + string uuid, + Action? updateResults = null + ) + { + AllureLifecycle.Instance.UpdateStep(uuid, result => { - T result; - try - { - result = await action(); - } - catch (Exception) - { - FailStep(); - throw; - } - - PassStep(); - return result; - } + result.status = Status.failed; + updateResults?.Invoke(result); + StepLogger?.StepFailed?.Log(result.name); + }); + AllureLifecycle.Instance.StopStep(uuid); + } - private static T Execute(string name, Func action) + [Obsolete(AllureLifecycle.EXPLICIT_STATE_MGMT_OBSOLETE)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void BrokeStep( + string uuid, + Action? updateResults = null + ) + { + AllureLifecycle.Instance.UpdateStep(uuid, result => { - T result; - try - { - result = action(); - } - catch (Exception e) - { - FailStep(); - throw new StepFailedException(name, e); - } - - PassStep(); - return result; - } + result.status = Status.broken; + updateResults?.Invoke(result); + StepLogger?.StepBroken?.Log(result.name); + }); + AllureLifecycle.Instance.StopStep(uuid); } + + #endregion } \ No newline at end of file diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 2e03247d..8ff9a727 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -1,65 +1,27 @@ using System.Collections.Concurrent; -using System.Collections.Generic; + +#nullable enable namespace Allure.Net.Commons.Storage { internal class AllureStorage { - private readonly ConcurrentDictionary> stepContext = new(); - - private readonly ConcurrentDictionary storage = new(); - - private LinkedList Steps => stepContext.GetOrAdd( - AllureLifecycle.CurrentTestIdGetter(), - new LinkedList() - ); + readonly ConcurrentDictionary storage = new(); public T Get(string uuid) { - return (T) storage[uuid]; + return (T)storage[uuid]; } - public T Put(string uuid, T item) + public T Put(string uuid, T item) where T : notnull { - return (T) storage.GetOrAdd(uuid, item); + return (T)storage.GetOrAdd(uuid, item); } public T Remove(string uuid) { storage.TryRemove(uuid, out var value); - return (T) value; - } - - public void ClearStepContext() - { - Steps.Clear(); - stepContext.TryRemove(AllureLifecycle.CurrentTestIdGetter(), out _); - } - - public void StartStep(string uuid) - { - Steps.AddFirst(uuid); - } - - public void StopStep() - { - Steps.RemoveFirst(); - } - - public string GetRootStep() - { - return Steps.Last?.Value; - } - - public string GetCurrentStep() - { - return Steps.First?.Value; - } - - public void AddStep(string parentUuid, string uuid, StepResult stepResult) - { - Put(uuid, stepResult); - Get(parentUuid).steps.Add(stepResult); + return (T)value; } } } \ No newline at end of file diff --git a/Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj b/Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj index b232b746..7e56a367 100644 --- a/Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj +++ b/Allure.SpecFlowPlugin.Tests/Allure.SpecFlowPlugin.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + 11 false diff --git a/Allure.SpecFlowPlugin.Tests/IntegrationTests.cs b/Allure.SpecFlowPlugin.Tests/IntegrationTests.cs index acf80f25..a8b2323d 100644 --- a/Allure.SpecFlowPlugin.Tests/IntegrationTests.cs +++ b/Allure.SpecFlowPlugin.Tests/IntegrationTests.cs @@ -14,30 +14,35 @@ namespace Allure.SpecFlowPlugin.Tests [TestFixture] public class IntegrationFixture { - private readonly HashSet allureContainers = new HashSet(); - private readonly HashSet allureTestResults = new HashSet(); - private IEnumerable> scenariosByStatus; + private readonly HashSet allureContainers = new(); + private readonly HashSet allureTestResults = new(); + private IDictionary> scenariosByStatus; - [OneTimeSetUp] - public void Init() - { - var featuresProjectPath = Path.GetFullPath( - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"./../../../../Allure.Features")); - Process.Start(new ProcessStartInfo - { - WorkingDirectory = featuresProjectPath, - FileName = "dotnet", - Arguments = $"test" - }).WaitForExit(); - var allureResultsDirectory = new DirectoryInfo(featuresProjectPath).GetDirectories("allure-results", SearchOption.AllDirectories) - .First(); - var featuresDirectory = Path.Combine(featuresProjectPath, "TestData"); - - - // parse allure suites - ParseAllureSuites(allureResultsDirectory.FullName); - ParseFeatures(featuresDirectory); - } + [OneTimeSetUp] + public void Init() + { + var featuresProjectPath = Path.GetFullPath( + Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + @"./../../../../Allure.Features" + ) + ); + Process.Start(new ProcessStartInfo + { + WorkingDirectory = featuresProjectPath, + FileName = "dotnet", + Arguments = $"test" + }).WaitForExit(); + var allureResultsDirectory = new DirectoryInfo(featuresProjectPath) + .GetDirectories("allure-results", SearchOption.AllDirectories) + .First(); + var featuresDirectory = Path.Combine(featuresProjectPath, "TestData"); + + + // parse allure suites + ParseAllureSuites(allureResultsDirectory.FullName); + ParseFeatures(featuresDirectory); + } [TestCase(Status.passed)] [TestCase(Status.failed)] @@ -45,38 +50,49 @@ public void Init() [TestCase(Status.skipped)] public void TestStatus(Status status) { - var expected = scenariosByStatus.FirstOrDefault(x => x.Key == status.ToString()).ToList(); + var expected = scenariosByStatus[status.ToString()]; var actual = allureTestResults.Where(x => x.status == status).Select(x => x.name).ToList(); Assert.That(actual, Is.EquivalentTo(expected)); } - private void ParseFeatures(string featuresDir) - { - var parser = new Parser(); - var scenarios = new List(); - var features = new DirectoryInfo(featuresDir).GetFiles("*.feature"); - scenarios.AddRange(features.SelectMany(f => - { - var children = parser.Parse(f.FullName).Feature.Children.ToList(); - var scenarioOutlines = children.Where(x => (x as dynamic).Examples.Length > 0).ToList(); - foreach (var s in scenarioOutlines) + private void ParseFeatures(string featuresDir) { - var examplesCount = ((s as dynamic).Examples as dynamic)[0].TableBody.Length; - for (int i = 1; i < examplesCount; i++) - { - children.Add(s); - } + var parser = new Parser(); + var scenarios = new List(); + var features = new DirectoryInfo(featuresDir).GetFiles("*.feature"); + scenarios.AddRange(features.SelectMany(f => + { + var children = parser.Parse(f.FullName).Feature.Children.ToList(); + var scenarioOutlines = children.Where( + x => (x as dynamic).Examples.Length > 0 + ).ToList(); + foreach (var s in scenarioOutlines) + { + var examplesCount = (s as dynamic).Examples[0] + .TableBody.Length; + for (int i = 1; i < examplesCount; i++) + { + children.Add(s); + } + } + return children; + }) + .Select(x => x as Scenario)); + + scenariosByStatus = scenarios.GroupBy( + x => x.Tags.FirstOrDefault( + x => Enum.GetNames( + typeof(Status) + ).Contains( + x.Name.Replace("@", "") + ) + )?.Name.Replace("@", "") ?? "_notag_", + x => x.Name + ).ToDictionary(g => g.Key, g => g.ToList()); + + // Extra placeholder scenario for testing an exception in AfterFeature + scenariosByStatus["broken"].Add("Feature hook failure placeholder"); } - return children; - }) - .Select(x => x as Scenario)); - - scenariosByStatus = - scenarios.GroupBy(x => x.Tags.FirstOrDefault(x => - Enum.GetNames(typeof(Status)).Contains(x.Name.Replace("@", "")))?.Name - .Replace("@", "") ?? - "_notag_", x => x.Name); - } private void ParseAllureSuites(string allureResultsDir) { diff --git a/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj b/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj index e2a3c73d..8a2f0ab4 100644 --- a/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj +++ b/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj @@ -2,6 +2,8 @@ net462;netstandard2.0 + 11 + enable Allure.SpecFlow 2.10-SNAPSHOT Alexander Bakanov @@ -14,7 +16,6 @@ https://github.com/allure-framework/allure-csharp specflow allure false - 8 true true snupkg diff --git a/Allure.SpecFlowPlugin/AllureBindingInvoker.cs b/Allure.SpecFlowPlugin/AllureBindingInvoker.cs index 71211c0d..b91d1249 100644 --- a/Allure.SpecFlowPlugin/AllureBindingInvoker.cs +++ b/Allure.SpecFlowPlugin/AllureBindingInvoker.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; +using System.Collections.Specialized; using Allure.Net.Commons; -using CsvHelper; using TechTalk.SpecFlow; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Configuration; @@ -12,245 +8,448 @@ using TechTalk.SpecFlow.Infrastructure; using TechTalk.SpecFlow.Tracing; + namespace Allure.SpecFlowPlugin { + using AllureBindingCall = Func< + IBinding, + IContextManager, + object[], + ITestTracer, + (object, TimeSpan) + >; + internal class AllureBindingInvoker : BindingInvoker { - private static readonly AllureLifecycle allure = AllureLifecycle.Instance; - - public AllureBindingInvoker(SpecFlowConfiguration specFlowConfiguration, IErrorProvider errorProvider, - ISynchronousBindingDelegateInvoker synchronousBindingDelegateInvoker) : base( - specFlowConfiguration, errorProvider, synchronousBindingDelegateInvoker) + const string PLACEHOLDER_TESTCASE_KEY = + "Allure.SpecFlowPlugin.HAS_PLACEHOLDER_TESTCASE"; + + static readonly AllureLifecycle allure = AllureLifecycle.Instance; + + public AllureBindingInvoker( + SpecFlowConfiguration specFlowConfiguration, + IErrorProvider errorProvider, + ISynchronousBindingDelegateInvoker synchronousBindingDelegateInvoker + ) : base( + specFlowConfiguration, + errorProvider, + synchronousBindingDelegateInvoker + ) { } - public override object InvokeBinding(IBinding binding, IContextManager contextManager, object[] arguments, - ITestTracer testTracer, out TimeSpan duration) + public override object InvokeBinding( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + out TimeSpan duration + ) { - // process hook if (binding is HookBinding hook) { - var featureContainerId = PluginHelper.GetFeatureContainerId(contextManager.FeatureContext?.FeatureInfo); - - switch (hook.HookType) - { - case HookType.BeforeFeature: - if (hook.HookOrder == int.MinValue) - { - // starting point - var featureContainer = new TestResultContainer - { - uuid = PluginHelper.GetFeatureContainerId(contextManager.FeatureContext?.FeatureInfo) - }; - allure.StartTestContainer(featureContainer); - - contextManager.FeatureContext.Set(new HashSet()); - contextManager.FeatureContext.Set(new HashSet()); - - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); - } - else - { - try - { - StartFixture(hook, featureContainerId); - var result = base.InvokeBinding(binding, contextManager, arguments, testTracer, - out duration); - allure.StopFixture(x => x.status = Status.passed); - return result; - } - catch (Exception ex) - { - allure.StopFixture(x => x.status = Status.broken); - - // if BeforeFeature is failed, execution is already stopped. We need to create, update, stop and write everything here. - - // create fake scenario container - var scenarioContainer = - PluginHelper.StartTestContainer(contextManager.FeatureContext, null); - - // start fake scenario - var scenario = PluginHelper.StartTestCase(scenarioContainer.uuid, - contextManager.FeatureContext, null); - - // update, stop and write - allure - .StopTestCase(x => - { - x.status = Status.broken; - x.statusDetails = PluginHelper.GetStatusDetails(ex); - }) - .WriteTestCase(scenario.uuid) - .StopTestContainer(scenarioContainer.uuid) - .WriteTestContainer(scenarioContainer.uuid) - .StopTestContainer(featureContainerId) - .WriteTestContainer(featureContainerId); - - throw; - } - } - - case HookType.BeforeStep: - case HookType.AfterStep: - { - var scenario = PluginHelper.GetCurrentTestCase(contextManager.ScenarioContext); - - try - { - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); - } - catch (Exception ex) - { - allure - .UpdateTestCase(scenario.uuid, - x => - { - x.status = Status.broken; - x.statusDetails = PluginHelper.GetStatusDetails(ex); - }); - throw; - } - } - - case HookType.BeforeScenario: - case HookType.AfterScenario: - if (hook.HookOrder == int.MinValue || hook.HookOrder == int.MaxValue) - { - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); - } - else - { - var scenarioContainer = PluginHelper.GetCurrentTestConainer(contextManager.ScenarioContext); - - try - { - StartFixture(hook, scenarioContainer.uuid); - var result = base.InvokeBinding(binding, contextManager, arguments, testTracer, - out duration); - allure.StopFixture(x => x.status = Status.passed); - return result; - } - catch (Exception ex) - { - var status = ex.GetType().Name.Contains(PluginHelper.IGNORE_EXCEPTION) - ? Status.skipped - : Status.broken; - - allure.StopFixture(x => x.status = status); - - // get or add new scenario - var scenario = PluginHelper.GetCurrentTestCase(contextManager.ScenarioContext) ?? - PluginHelper.StartTestCase(scenarioContainer.uuid, - contextManager.FeatureContext, contextManager.ScenarioContext); - - allure.UpdateTestCase(scenario.uuid, - x => - { - x.status = status; - x.statusDetails = PluginHelper.GetStatusDetails(ex); - }); - throw; - } - } - - case HookType.AfterFeature: - if (hook.HookOrder == int.MaxValue) - // finish point - { - WriteScenarios(contextManager); - allure - .StopTestContainer(featureContainerId) - .WriteTestContainer(featureContainerId); - - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); - } - else - { - try - { - StartFixture(hook, featureContainerId); - var result = base.InvokeBinding(binding, contextManager, arguments, testTracer, - out duration); - allure.StopFixture(x => x.status = Status.passed); - return result; - } - catch (Exception ex) - { - var scenario = contextManager.FeatureContext.Get>().Last(); - allure - .StopFixture(x => x.status = Status.broken) - .UpdateTestCase(scenario.uuid, - x => - { - x.status = Status.broken; - x.statusDetails = PluginHelper.GetStatusDetails(ex); - }); - - WriteScenarios(contextManager); - - allure - .StopTestContainer(featureContainerId) - .WriteTestContainer(featureContainerId); - - throw; - } - } - - case HookType.BeforeScenarioBlock: - case HookType.AfterScenarioBlock: - case HookType.BeforeTestRun: - case HookType.AfterTestRun: - default: - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); - } + (var result, duration) = this.ProcessHook( + binding, + contextManager, + arguments, + testTracer, + hook + ); + return result; } - - return base.InvokeBinding(binding, contextManager, arguments, testTracer, out duration); + return base.InvokeBinding( + binding, + contextManager, + arguments, + testTracer, + out duration + ); } - private void StartFixture(HookBinding hook, string containerId) + (object, TimeSpan) ProcessHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + IsAllureHook(hook) ? this.InvokeAllureBinding( + binding, + contextManager, + arguments, + testTracer, + hook + ) : hook.HookType switch + { + HookType.BeforeFeature => + this.MakeFixtureFromBeforeFeatureHook( + binding, + contextManager, + arguments, + testTracer, + hook + ), + HookType.BeforeScenario => + this.MakeFixtureFromBeforeScenarioHook( + binding, + contextManager, + arguments, + testTracer, + hook + ), + HookType.BeforeStep or HookType.AfterStep => + this.ProcessStepHook( + binding, + contextManager, + arguments, + testTracer + ), + HookType.AfterScenario => + this.MakeFixtureFromAfterScenarioHook( + binding, + contextManager, + arguments, + testTracer, + hook + ), + HookType.AfterFeature => + this.MakeFixtureFromAfterFeatureHook( + binding, + contextManager, + arguments, + testTracer, + hook + ), + _ => this.CallBaseInvokeBinding( + binding, + contextManager, + arguments, + testTracer + ) + }; + + (object, TimeSpan) ProcessStepHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer + ) { - if (hook.HookType.ToString().StartsWith("Before")) - allure.StartBeforeFixture(containerId, PluginHelper.NewId(), PluginHelper.GetFixtureResult(hook)); - else - allure.StartAfterFixture(containerId, PluginHelper.NewId(), PluginHelper.GetFixtureResult(hook)); + try + { + return this.CallBaseInvokeBinding( + binding, + contextManager, + arguments, + testTracer + ); + } + catch (Exception ex) + { + ReportStepError(ex); + throw; + } } - private static void StartStep(StepInfo stepInfo, string containerId) + (object, TimeSpan) MakeFixtureFromBeforeFeatureHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + this.MakeFixtureFromFeatureHook( + StartBeforeFixture, + _ => { }, + binding, + contextManager, + arguments, + testTracer, + hook + ); + + (object, TimeSpan) MakeFixtureFromAfterFeatureHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + PluginHelper.UseCapturedAllureContext( + contextManager.FeatureContext, + () => this.MakeFixtureFromFeatureHook( + StartAfterFixture, + _ => AllureBindings.LastAfterFeature(), + binding, + contextManager, + arguments, + testTracer, + hook + ) + ); + + (object, TimeSpan) MakeFixtureFromFeatureHook( + Action startFixture, + Action callLastHook, + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) { - var stepResult = new StepResult + object result; + TimeSpan duration; + + startFixture(hook); + try { - name = $"{stepInfo.StepDefinitionType} {stepInfo.Text}" - }; + result = base.InvokeBinding( + binding, + contextManager, + arguments, + testTracer, + out duration + ); + } + catch (Exception ex) + { + var featureContext = contextManager.FeatureContext; + ReportFeatureFixtureError(featureContext, ex); + callLastHook(featureContext); + throw; + } + allure.StopFixture(MakePassed); + + return (result, duration); + } - allure.StartStep(containerId, PluginHelper.NewId(), stepResult); + (object, TimeSpan) MakeFixtureFromBeforeScenarioHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + this.MakeFixtureFromScenarioHook( + AllureBindings.LastBeforeScenario, + StartBeforeFixture, + binding, + contextManager, + arguments, + testTracer, + hook + ); + + (object, TimeSpan) MakeFixtureFromAfterScenarioHook( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + this.MakeFixtureFromScenarioHook( + (_, sc) => AllureBindings.LastAfterScenario(sc), + StartAfterFixture, + binding, + contextManager, + arguments, + testTracer, + hook + ); + + (object, TimeSpan) MakeFixtureFromScenarioHook( + Action callLastHook, + Action startFixture, + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) + { + (object, TimeSpan) result; - if (stepInfo.Table != null) + startFixture(hook); + try { - var csvFile = $"{Guid.NewGuid().ToString()}.csv"; - using (var csv = new CsvWriter(File.CreateText(csvFile),CultureInfo.InvariantCulture)) - { - foreach (var item in stepInfo.Table.Header) csv.WriteField(item); - csv.NextRecord(); - foreach (var row in stepInfo.Table.Rows) - { - foreach (var item in row.Values) csv.WriteField(item); - csv.NextRecord(); - } - } - - allure.AddAttachment("table", "text/csv", csvFile); + result = this.CallBaseInvokeBinding( + binding, + contextManager, + arguments, + testTracer + ); } + catch (Exception ex) + { + ReportScenarioFixtureError(ex); + + // SpecFlow doesn't call the remained hooks in case of an + // exception is thrown. We have to call them explicitly to + // ensure side effects on the Allure context are properly + // applied. + callLastHook( + contextManager.FeatureContext, + contextManager.ScenarioContext + ); + + throw; + } + + allure.StopFixture(MakePassed); + return result; } - private static void WriteScenarios(IContextManager contextManager) + (object, TimeSpan) InvokeAllureBinding( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer, + HookBinding hook + ) => + this.ResolveAllureBindingCall(hook)( + binding, + contextManager, + arguments, + testTracer + ); + + AllureBindingCall ResolveAllureBindingCall(HookBinding hook) => + hook.HookType is HookType.AfterFeature + ? this.CallBaseInvokeBindingInFeatureContext + : this.CallBaseInvokeBinding; + + (object, TimeSpan) CallBaseInvokeBinding( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer + ) => + (base.InvokeBinding( + binding, + contextManager, + arguments, + testTracer, + out var duration + ), duration); + + (object, TimeSpan) CallBaseInvokeBindingInFeatureContext( + IBinding binding, + IContextManager contextManager, + object[] arguments, + ITestTracer testTracer + ) => + PluginHelper.UseCapturedAllureContext( + contextManager.FeatureContext, + () => this.CallBaseInvokeBinding( + binding, + contextManager, + arguments, + testTracer + ) + ); + + static void ReportFeatureFixtureError( + FeatureContext featureContext, + Exception error + ) { - foreach (var s in contextManager.FeatureContext.Get>()) allure.WriteTestCase(s.uuid); + var makeBroken = WrapMakeBroken(error); + allure.StopFixture(makeBroken); + + // Create one placeholder test case per failed feature-level hook + // to indicate the error. + if (!featureContext.ContainsKey(PLACEHOLDER_TESTCASE_KEY)) + { + PluginHelper.StartTestCase(featureContext.FeatureInfo, new( + "Feature hook failure placeholder", + string.Format( + "This is a placeholder scenario to indicate an " + + "exception occured in a feature-level fixture " + + "of '{0}'", + featureContext.FeatureInfo.Title + ), + Array.Empty(), + new OrderedDictionary() + )); - foreach (var c in contextManager.FeatureContext.Get>()) allure - .StopTestContainer(c.uuid) - .WriteTestContainer(c.uuid); + .StopTestCase(makeBroken) + .WriteTestCase(); + + featureContext.Add(PLACEHOLDER_TESTCASE_KEY, true); + } + } + + static void ReportScenarioFixtureError(Exception error) + { + var status = PluginHelper.IsIgnoreException(error) + ? Status.skipped + : Status.broken; + var statusDetails = PluginHelper.GetStatusDetails(error); + + allure.StopFixture( + PluginHelper.WrapStatusUpdate(status, statusDetails) + ); + + // If there is a scenario with no previous error, we update its + // status here (this is the case for AfterScenraio hooks). + // Otherwise (BeforeScenario) the scenario is updated later based + // on the information provided by SpecFlow. + if (allure.Context.HasTest) + { + allure.UpdateTestCase( + PluginHelper.WrapStatusOverwrite( + status, + statusDetails, + Status.none, + Status.passed + ) + ); + } } + + static void ReportStepError(Exception error) + { + if (allure.Context.HasStep) + { + MakeStepBroken(error); + } + MakeTestCaseBroken(error); + } + + static void StartBeforeFixture(HookBinding hook) => + allure.StartBeforeFixture( + PluginHelper.GetFixtureResult(hook) + ); + + static void StartAfterFixture(HookBinding hook) => + allure.StartAfterFixture( + PluginHelper.GetFixtureResult(hook) + ); + + static bool IsAllureHook(HookBinding hook) => + hook.Method.Type.FullName == typeof(AllureBindings).FullName; + + static Action WrapMakeBroken(Exception error) => + PluginHelper.WrapStatusOverwrite( + Status.broken, + PluginHelper.GetStatusDetails(error), + Status.none, + Status.passed + ); + + static void MakePassed(ExecutableItem item) => + item.status = Status.passed; + + static void MakeTestCaseBroken(Exception error) => + allure.UpdateTestCase( + WrapMakeBroken(error) + ); + + static void MakeStepBroken(Exception error) => + allure.UpdateStep( + WrapMakeBroken(error) + ); } } \ No newline at end of file diff --git a/Allure.SpecFlowPlugin/AllureBindings.cs b/Allure.SpecFlowPlugin/AllureBindings.cs index 6e9ef636..e16ade7d 100644 --- a/Allure.SpecFlowPlugin/AllureBindings.cs +++ b/Allure.SpecFlowPlugin/AllureBindings.cs @@ -4,56 +4,56 @@ namespace Allure.SpecFlowPlugin { [Binding] - public class AllureBindings + public static class AllureBindings { - private static readonly AllureLifecycle allure = AllureLifecycle.Instance; - - private readonly FeatureContext featureContext; - private readonly ScenarioContext scenarioContext; - - public AllureBindings(FeatureContext featureContext, ScenarioContext scenarioContext) - { - this.featureContext = featureContext; - this.scenarioContext = scenarioContext; - } + static readonly AllureLifecycle allure = AllureLifecycle.Instance; [BeforeFeature(Order = int.MinValue)] - public static void FirstBeforeFeature() - { - // start feature container in BindingInvoker - } + public static void FirstBeforeFeature(FeatureContext featureContext) => + // Capturing the context allows us to access the container later in + // AfterFeature hooks (it's executed by SpecFlow in a different + // execution context). + PluginHelper.CaptureAllureContext( + featureContext, + () => allure.StartTestContainer(new() + { + uuid = PluginHelper.GetFeatureContainerId( + featureContext.FeatureInfo + ) + }) + ); [AfterFeature(Order = int.MaxValue)] - public static void LastAfterFeature() - { - // write feature container in BindingInvoker - } + public static void LastAfterFeature() => + allure + .StopTestContainer() + .WriteTestContainer(); [BeforeScenario(Order = int.MinValue)] - public void FirstBeforeScenario() - { - PluginHelper.StartTestContainer(featureContext, scenarioContext); - //AllureHelper.StartTestCase(scenarioContainer.uuid, featureContext, scenarioContext); - } + public static void FirstBeforeScenario() => + PluginHelper.StartTestContainer(); [BeforeScenario(Order = int.MaxValue)] - public void LastBeforeScenario() - { - // start scenario after last fixture and before the first step to have valid current step context in allure storage - var scenarioContainer = PluginHelper.GetCurrentTestConainer(scenarioContext); - PluginHelper.StartTestCase(scenarioContainer.uuid, featureContext, scenarioContext); - } + public static void LastBeforeScenario( + FeatureContext featureContext, + ScenarioContext scenarioContext + ) => + PluginHelper.StartTestCase( + featureContext.FeatureInfo, + scenarioContext.ScenarioInfo + ); [AfterScenario(Order = int.MinValue)] - public void FirstAfterScenario() - { - var scenarioId = PluginHelper.GetCurrentTestCase(scenarioContext).uuid; - - // update status to passed if there were no step of binding failures - allure - .UpdateTestCase(scenarioId, - x => x.status = x.status != Status.none ? x.status : Status.passed) - .StopTestCase(scenarioId); - } + public static void FirstAfterScenario() => allure.StopTestCase(); + + [AfterScenario(Order = int.MaxValue)] + public static void LastAfterScenario( + ScenarioContext scenarioContext + ) => + allure.UpdateTestCase( + PluginHelper.TestStatusResolver(scenarioContext) + ).WriteTestCase() + .StopTestContainer() + .WriteTestContainer(); } } \ No newline at end of file diff --git a/Allure.SpecFlowPlugin/AllurePlugin.cs b/Allure.SpecFlowPlugin/AllurePlugin.cs index 3137943e..8921d593 100644 --- a/Allure.SpecFlowPlugin/AllurePlugin.cs +++ b/Allure.SpecFlowPlugin/AllurePlugin.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using Allure.SpecFlowPlugin; +using Allure.SpecFlowPlugin; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Plugins; using TechTalk.SpecFlow.Tracing; @@ -12,14 +10,19 @@ namespace Allure.SpecFlowPlugin { public class AllurePlugin : IRuntimePlugin { - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, - UnitTestProviderConfiguration unitTestProviderConfiguration) + public void Initialize( + RuntimePluginEvents runtimePluginEvents, + RuntimePluginParameters runtimePluginParameters, + UnitTestProviderConfiguration unitTestProviderConfiguration + ) { - runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => - args.ObjectContainer.RegisterTypeAs(); + runtimePluginEvents.CustomizeGlobalDependencies += + (sender, args) => args.ObjectContainer + .RegisterTypeAs(); - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - args.ObjectContainer.RegisterTypeAs(); + runtimePluginEvents.CustomizeTestThreadDependencies += + (sender, args) => args.ObjectContainer + .RegisterTypeAs(); } } } \ No newline at end of file diff --git a/Allure.SpecFlowPlugin/AllureTestTracerWrapper.cs b/Allure.SpecFlowPlugin/AllureTestTracerWrapper.cs index a56ec182..e3b19d48 100644 --- a/Allure.SpecFlowPlugin/AllureTestTracerWrapper.cs +++ b/Allure.SpecFlowPlugin/AllureTestTracerWrapper.cs @@ -13,61 +13,101 @@ using TechTalk.SpecFlow.Configuration; using TechTalk.SpecFlow.Tracing; + namespace Allure.SpecFlowPlugin { public class AllureTestTracerWrapper : TestTracer, ITestTracer { - private static readonly AllureLifecycle allure = AllureLifecycle.Instance; - private static readonly PluginConfiguration pluginConfiguration = PluginHelper.PluginConfiguration; - private readonly string noMatchingStepMessage = "No matching step definition found for the step"; - - public AllureTestTracerWrapper(ITraceListener traceListener, IStepFormatter stepFormatter, - IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider, SpecFlowConfiguration specFlowConfiguration) - : base(traceListener, stepFormatter, stepDefinitionSkeletonProvider, specFlowConfiguration) + static readonly AllureLifecycle allure = AllureLifecycle.Instance; + static readonly PluginConfiguration pluginConfiguration = + PluginHelper.PluginConfiguration; + readonly string noMatchingStepMessage = + "No matching definition found for this step"; + readonly string noMatchingStepMessageForTest = + "No matching definition found for the step '{0}'"; + + public AllureTestTracerWrapper( + ITraceListener traceListener, + IStepFormatter stepFormatter, + IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider, + SpecFlowConfiguration specFlowConfiguration + ) : base( + traceListener, + stepFormatter, + stepDefinitionSkeletonProvider, + specFlowConfiguration + ) { } - void ITestTracer.TraceStep(StepInstance stepInstance, bool showAdditionalArguments) + void ITestTracer.TraceStep( + StepInstance stepInstance, + bool showAdditionalArguments + ) { - TraceStep(stepInstance, showAdditionalArguments); - StartStep(stepInstance); + this.TraceStep(stepInstance, showAdditionalArguments); + this.StartStep(stepInstance); } - void ITestTracer.TraceStepDone(BindingMatch match, object[] arguments, TimeSpan duration) + void ITestTracer.TraceStepDone( + BindingMatch match, + object[] arguments, + TimeSpan duration + ) { - TraceStepDone(match, arguments, duration); + this.TraceStepDone(match, arguments, duration); allure.StopStep(x => x.status = Status.passed); } void ITestTracer.TraceError(Exception ex, TimeSpan duration) { - TraceError(ex, duration); - allure.StopStep(x => x.status = Status.failed); + this.TraceError(ex, duration); + allure.StopStep( + PluginHelper.WrapStatusInit(Status.failed, ex) + ); FailScenario(ex); } void ITestTracer.TraceStepSkipped() { - TraceStepSkipped(); + this.TraceStepSkipped(); allure.StopStep(x => x.status = Status.skipped); } void ITestTracer.TraceStepPending(BindingMatch match, object[] arguments) { - TraceStepPending(match, arguments); + this.TraceStepPending(match, arguments); allure.StopStep(x => x.status = Status.skipped); } - void ITestTracer.TraceNoMatchingStepDefinition(StepInstance stepInstance, ProgrammingLanguage targetLanguage, - CultureInfo bindingCulture, List matchesWithoutScopeCheck) + void ITestTracer.TraceNoMatchingStepDefinition( + StepInstance stepInstance, + ProgrammingLanguage targetLanguage, + CultureInfo bindingCulture, + List matchesWithoutScopeCheck + ) { - TraceNoMatchingStepDefinition(stepInstance, targetLanguage, bindingCulture, matchesWithoutScopeCheck); - allure.StopStep(x => x.status = Status.broken); - allure.UpdateTestCase(x => - { - x.status = Status.broken; - x.statusDetails = new StatusDetails {message = noMatchingStepMessage}; - }); + this.TraceNoMatchingStepDefinition( + stepInstance, + targetLanguage, + bindingCulture, + matchesWithoutScopeCheck + ); + allure.StopStep( + PluginHelper.WrapStatusUpdate(Status.broken, new() + { + message = noMatchingStepMessage + }) + ); + allure.UpdateTestCase( + PluginHelper.WrapStatusInit(Status.broken, new StatusDetails + { + message = string.Format( + noMatchingStepMessageForTest, + stepInstance.Text + ) + }) + ); } private void StartStep(StepInstance stepInstance) @@ -79,18 +119,23 @@ private void StartStep(StepInstance stepInstance) // parse MultilineTextArgument - if (stepInstance.MultilineTextArgument != null) + if (stepInstance.MultilineTextArgument is not null) + { allure.AddAttachment( "multiline argument", "text/plain", - Encoding.ASCII.GetBytes(stepInstance.MultilineTextArgument), - ".txt"); + Encoding.ASCII.GetBytes( + stepInstance.MultilineTextArgument + ), + ".txt" + ); + } var table = stepInstance.TableArgument; - var isTableProcessed = table == null; + var isTableProcessed = table is null; // parse table as step params - if (table != null) + if (table is not null) { var header = table.Header.ToArray(); if (pluginConfiguration.stepArguments.convertToParameters) @@ -100,13 +145,24 @@ private void StartStep(StepInstance stepInstance) // convert 2 column table into param-value if (table.Header.Count == 2) { - var paramNameMatch = Regex.IsMatch(header[0], pluginConfiguration.stepArguments.paramNameRegex); - var paramValueMatch = - Regex.IsMatch(header[1], pluginConfiguration.stepArguments.paramValueRegex); + var paramNameMatch = Regex.IsMatch( + header[0], + pluginConfiguration.stepArguments.paramNameRegex + ); + var paramValueMatch = Regex.IsMatch( + header[1], + pluginConfiguration.stepArguments.paramValueRegex + ); if (paramNameMatch && paramValueMatch) { for (var i = 0; i < table.RowCount; i++) - parameters.Add(new Parameter {name = table.Rows[i][0], value = table.Rows[i][1]}); + { + parameters.Add(new() + { + name = table.Rows[i][0], + value = table.Rows[i][1] + }); + } isTableProcessed = true; } @@ -115,7 +171,14 @@ private void StartStep(StepInstance stepInstance) else if (table.RowCount == 1) { for (var i = 0; i < table.Header.Count; i++) - parameters.Add(new Parameter {name = header[i], value = table.Rows[0][i]}); + { + parameters.Add(new() + { + name = header[i], + value = table.Rows[0][i] + }); + } + isTableProcessed = true; } @@ -123,18 +186,31 @@ private void StartStep(StepInstance stepInstance) } } - allure.StartStep(PluginHelper.NewId(), stepResult); + allure.StartStep(stepResult); + + // add csv table for multi-row table if was not processed as + // params already + if (isTableProcessed) + { + return; + } - // add csv table for multi-row table if was not processed as params already - if (isTableProcessed) return; using var ms = new MemoryStream(); using var sw = new StreamWriter(ms, System.Text.Encoding.UTF8); using var csv = new CsvWriter(sw, CultureInfo.InvariantCulture); - foreach (var item in table.Header) csv.WriteField(item); + foreach (var item in table!.Header) + { + csv.WriteField(item); + } + csv.NextRecord(); foreach (var row in table.Rows) { - foreach (var item in row.Values) csv.WriteField(item); + foreach (var item in row.Values) + { + csv.WriteField(item); + } + csv.NextRecord(); } @@ -144,12 +220,11 @@ private void StartStep(StepInstance stepInstance) private static void FailScenario(Exception ex) { - allure.UpdateTestCase( - x => - { - x.status = x.status != Status.none ? x.status : Status.failed; - x.statusDetails = PluginHelper.GetStatusDetails(ex); - }); + allure.UpdateTestCase(x => + { + x.status = x.status != Status.none ? x.status : Status.failed; + x.statusDetails = PluginHelper.GetStatusDetails(ex); + }); } } } diff --git a/Allure.SpecFlowPlugin/PluginConfiguration.cs b/Allure.SpecFlowPlugin/PluginConfiguration.cs index 964cc82a..3a5bf354 100644 --- a/Allure.SpecFlowPlugin/PluginConfiguration.cs +++ b/Allure.SpecFlowPlugin/PluginConfiguration.cs @@ -2,57 +2,57 @@ { public class PluginConfiguration { - public Steparguments stepArguments { get; set; } = new Steparguments(); - public Grouping grouping { get; set; } = new Grouping(); - public Labels labels { get; set; } = new Labels(); - public Links links { get; set; } = new Links(); + public Steparguments stepArguments { get; set; } = new(); + public Grouping grouping { get; set; } = new(); + public Labels labels { get; set; } = new(); + public Links links { get; set; } = new(); } public class Steparguments { public bool convertToParameters { get; set; } - public string paramNameRegex { get; set; } - public string paramValueRegex { get; set; } + public string? paramNameRegex { get; set; } + public string? paramValueRegex { get; set; } } public class Grouping { - public Suites suites { get; set; } = new Suites(); - public Behaviors behaviors { get; set; } = new Behaviors(); - public Packages packages { get; set; } = new Packages(); + public Suites suites { get; set; } = new(); + public Behaviors behaviors { get; set; } = new(); + public Packages packages { get; set; } = new(); } public class Suites { - public string parentSuite { get; set; } - public string suite { get; set; } - public string subSuite { get; set; } + public string? parentSuite { get; set; } + public string? suite { get; set; } + public string? subSuite { get; set; } } public class Behaviors { - public string epic { get; set; } - public string story { get; set; } + public string? epic { get; set; } + public string? story { get; set; } } public class Packages { - public string package { get; set; } - public string testClass { get; set; } - public string testMethod { get; set; } + public string? package { get; set; } + public string? testClass { get; set; } + public string? testMethod { get; set; } } public class Labels { - public string owner { get; set; } - public string severity { get; set; } - public string label { get; set; } + public string? owner { get; set; } + public string? severity { get; set; } + public string? label { get; set; } } public class Links { - public string link { get; set; } - public string issue { get; set; } - public string tms { get; set; } + public string? link { get; set; } + public string? issue { get; set; } + public string? tms { get; set; } } } \ No newline at end of file diff --git a/Allure.SpecFlowPlugin/PluginHelper.cs b/Allure.SpecFlowPlugin/PluginHelper.cs index 902f68b9..8fe1fecc 100644 --- a/Allure.SpecFlowPlugin/PluginHelper.cs +++ b/Allure.SpecFlowPlugin/PluginHelper.cs @@ -1,350 +1,520 @@ -using Allure.Net.Commons; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Allure.Net.Commons; +using Newtonsoft.Json.Linq; using TechTalk.SpecFlow; using TechTalk.SpecFlow.Bindings; namespace Allure.SpecFlowPlugin { - public static class PluginHelper - { - public static string IGNORE_EXCEPTION = "IgnoreException"; - private static readonly ScenarioInfo emptyScenarioInfo = new ScenarioInfo("Unknown", string.Empty, Array.Empty(), new OrderedDictionary()); + public static class PluginHelper + { + public static string IGNORE_EXCEPTION = "IgnoreException"; - private static readonly FeatureInfo emptyFeatureInfo = new FeatureInfo( - CultureInfo.CurrentCulture, string.Empty, string.Empty, string.Empty); + internal static PluginConfiguration PluginConfiguration = + GetConfiguration(AllureLifecycle.Instance.JsonConfiguration); - internal static PluginConfiguration PluginConfiguration = - GetConfiguration(AllureLifecycle.Instance.JsonConfiguration); + public static PluginConfiguration GetConfiguration( + string allureConfiguration + ) + { + var config = new PluginConfiguration(); + var configJson = JObject.Parse(allureConfiguration); + var specflowSection = configJson["specflow"]; + if (specflowSection != null) + { + config = specflowSection.ToObject() + ?? throw new NullReferenceException(); + } + + return config; + } - public static PluginConfiguration GetConfiguration(string allureConfiguration) - { - var config = new PluginConfiguration(); - var specflowSection = JObject.Parse(allureConfiguration)["specflow"]; - if (specflowSection != null) - config = specflowSection.ToObject(); - return config; - } + internal static string GetFeatureContainerId( + FeatureInfo featureInfo + ) => featureInfo.GetHashCode().ToString(); - internal static string GetFeatureContainerId(FeatureInfo featureInfo) - { - var id = featureInfo != null - ? featureInfo.GetHashCode().ToString() - : emptyFeatureInfo.GetHashCode().ToString(); + internal static string NewId() => Guid.NewGuid().ToString("N"); - return id; - } + internal static FixtureResult GetFixtureResult(HookBinding hook) => + new() + { + name = $"{hook.Method.Name} [{hook.HookOrder}]" + }; - internal static string NewId() - { - return Guid.NewGuid().ToString("N"); - } + internal static void StartTestContainer() => + AllureLifecycle.Instance.StartTestContainer(new() + { + uuid = NewId() + }); - internal static FixtureResult GetFixtureResult(HookBinding hook) - { - return new FixtureResult - { - name = $"{hook.Method.Name} [{hook.HookOrder}]" - }; - } - - internal static TestResult StartTestCase(string containerId, FeatureContext featureContext, - ScenarioContext scenarioContext) - { - var featureInfo = featureContext?.FeatureInfo ?? emptyFeatureInfo; - var scenarioInfo = scenarioContext?.ScenarioInfo ?? emptyScenarioInfo; - var tags = GetTags(featureInfo, scenarioInfo); - var parameters = GetParameters(scenarioInfo); - var title = scenarioInfo.Title; - var testResult = new TestResult - { - uuid = NewId(), - historyId = title + parameters.hash, - name = title, - fullName = title, - labels = new List