From d0e332ccde5b3258a67ff06f654c2534a7f92dd5 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 12 Jul 2023 20:35:47 +0700 Subject: [PATCH 01/24] Add AllureContext lass to isolate allure state from threads and async tasks --- .../StorageTests/AllureContextTests.cs | 503 ++++++++++++++++++ Allure.Net.Commons/Allure.Net.Commons.csproj | 1 + Allure.Net.Commons/Internal/IsExternalInit.cs | 20 + Allure.Net.Commons/Storage/AllureContext.cs | 408 ++++++++++++++ 4 files changed, 932 insertions(+) create mode 100644 Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs create mode 100644 Allure.Net.Commons/Internal/IsExternalInit.cs create mode 100644 Allure.Net.Commons/Storage/AllureContext.cs diff --git a/Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs b/Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs new file mode 100644 index 00000000..f4755cc4 --- /dev/null +++ b/Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Allure.Net.Commons.Storage; +using NUnit.Framework; + +namespace Allure.Net.Commons.Tests.StorageTests +{ + 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.CurrentContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "No container context has been set up." + ) + ); + Assert.That( + () => ctx.CurrentFixture, + Throws.InvalidOperationException.With.Message.EqualTo( + "No fixture context has been set up." + ) + ); + Assert.That( + () => ctx.CurrentTest, + Throws.InvalidOperationException.With.Message.EqualTo( + "No test context has been set up." + ) + ); + Assert.That( + () => ctx.CurrentStep, + Throws.InvalidOperationException.With.Message.EqualTo( + "No step context has been set up." + ) + ); + Assert.That( + () => ctx.CurrentStepContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "No fixture, test, or step context has been set up." + ) + ); + } + + [Test] + public void TestContextOnly() + { + var test = new TestResult(); + + var ctx = new AllureContext().WithTestContext(test); + + Assert.That(ctx.TestContext, Is.SameAs(test)); + Assert.That(ctx.CurrentTest, Is.SameAs(test)); + Assert.That(ctx.CurrentStepContainer, Is.SameAs(test)); + } + + [Test] + public void CanNotRemoveContainerIfTestIsSet() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(new()); + + Assert.That( + ctx.WithNoLastContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to exclude the latest container from the " + + "context because a test context exists." + ) + ); + } + + [Test] + public void TestContextCanBeRemoved() + { + var test = new TestResult(); + + var ctx = new AllureContext() + .WithTestContext(test) + .WithNoTestContext(); + + 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.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.CurrentContainer, Is.SameAs(container2)); + } + + [Test] + public void CanNotRemoveContainerIfNoneExist() + { + var ctx = new AllureContext(); + + Assert.That( + ctx.WithNoLastContainer, + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to exclude the latest container from the " + + "context because no container context has been set up." + ) + ); + } + + [Test] + public void LatestContainerCanBeRemoved() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithNoLastContainer(); + + 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)); + } + + [Test] + public void FixtureContextRequiresContainer() + { + var fixture = new FixtureResult(); + + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithFixtureContext(new()), + Throws.InvalidOperationException + .With.Message.EqualTo( + "Unable to set up the fixture context because there " + + "is no container context." + ) + ); + } + + [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.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 exclude the latest container from the " + + "context because a fixture context exists." + ) + ); + } + + [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 set up the fixture context " + + "because another fixture context already exists." + ) + ); + } + + [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 set up the test context " + + "because another test context already exists." + ) + ); + } + + [Test] + public void CanNotSetTestContextIfFixtureContextIsActive() + { + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()); + + Assert.That( + () => ctx.WithTestContext(new()), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to set up the test context " + + "because a fixture context is currently active." + ) + ); + } + + [Test] + public void ClearingTestContextClearsFixtureContext() + { + var test = new TestResult(); + + var ctx = new AllureContext() + .WithContainer(new()) + .WithTestContext(test) + .WithFixtureContext(new()) + .WithNoTestContext(); + + 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.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 set up the step context because no test or" + + "fixture context exists." + ) + ); + } + + [Test] + public void StepCanNotBeRemovedIfNoStepExists() + { + var ctx = new AllureContext(); + + Assert.That( + () => ctx.WithNoLastStep(), + Throws.InvalidOperationException.With.Message.EqualTo( + "Unable to exclude the latest step from the context " + + "because no step context has been set up." + ) + ); + } + + [Test] + public void StepCanBeAddedIfFixtureExists() + { + var step = new StepResult(); + var ctx = new AllureContext() + .WithContainer(new()) + .WithFixtureContext(new()) + .WithStep(step); + + Assert.That(ctx.StepContext, Is.EqualTo(new[] { step })); + 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.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.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.CurrentStepContainer, Is.SameAs(step1)); + } + + [Test] + public void RemovingTheOnlyStepRestoresTestAsStepContainer() + { + var test = new TestResult(); + var ctx = new AllureContext() + .WithTestContext(test) + .WithStep(new()) + .WithNoLastStep(); + + Assert.That(ctx.StepContext, Is.Empty); + 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.StepContext, Is.Empty); + } + + [Test] + public void RemovingTestClearsStepContext() + { + var ctx = new AllureContext() + .WithTestContext(new()) + .WithStep(new()) + .WithNoTestContext(); + + 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.StepContext, Is.Empty); + } + } +} diff --git a/Allure.Net.Commons/Allure.Net.Commons.csproj b/Allure.Net.Commons/Allure.Net.Commons.csproj index 4aaa813f..24d93e9d 100644 --- a/Allure.Net.Commons/Allure.Net.Commons.csproj +++ b/Allure.Net.Commons/Allure.Net.Commons.csproj @@ -37,6 +37,7 @@ + diff --git a/Allure.Net.Commons/Internal/IsExternalInit.cs b/Allure.Net.Commons/Internal/IsExternalInit.cs new file mode 100644 index 00000000..30cde23d --- /dev/null +++ b/Allure.Net.Commons/Internal/IsExternalInit.cs @@ -0,0 +1,20 @@ +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/Storage/AllureContext.cs b/Allure.Net.Commons/Storage/AllureContext.cs new file mode 100644 index 00000000..4c329031 --- /dev/null +++ b/Allure.Net.Commons/Storage/AllureContext.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Immutable; +using System.Linq; + +#nullable enable + +namespace Allure.Net.Commons.Storage +{ + /// + /// Represents information related to a particular test at all stages of + /// its life cycle. + /// + /// + /// + /// 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.
+ /// + /// Methods in this class don't mutate allure model. + ///
+ public record class AllureContext + { + /// + /// A stack of fixture containers affecting subsequent tests. + /// + /// + /// Setting up this context allows operations on the current container + /// (including adding a fixture to or removing a fixture from the + /// current container). + /// + public IImmutableStack ContainerContext + { + get; + private init; + } = ImmutableStack.Empty; + + /// + /// A fixture that is being currently executed. + /// + /// + /// Setting up this context allows operations on the current fixture + /// result.
+ /// This property differs from in that it + /// returns null if no fixture context exists instead of throwing. + ///
+ public FixtureResult? FixtureContext { get; private init; } + + /// + /// A test that is being executed. + /// + /// + /// Setting up this context allows operations on the current test + /// result.
+ /// + /// This property differs from in that it + /// returns null if no test context exists instead of throwing. + ///
+ public TestResult? TestContext { get; private init; } + + /// + /// A stack of nested steps that are being executed. + /// + /// + /// Setting up this context allows operations on the current step. + /// + public IImmutableStack StepContext + { + get; + private init; + } = ImmutableStack.Empty; + + /// + /// The most recently added container from the container context. + /// + /// + /// It throws if there is no + /// container context. + /// + /// + public TestResultContainer CurrentContainer + { + get => this.ContainerContext.FirstOrDefault() + ?? throw new InvalidOperationException( + "No container context has been set up." + ); + } + + /// + /// A fixture that is being executed. + /// + /// + /// It throws if there is no + /// fixture context. + /// + /// + public FixtureResult CurrentFixture + { + get => this.FixtureContext ?? throw new InvalidOperationException( + "No fixture context has been set up." + ); + } + + /// + /// A test that is being executed. + /// + /// + /// It throws if there is no + /// test context. + /// + /// + public TestResult CurrentTest + { + get => this.TestContext ?? throw new InvalidOperationException( + "No test context has been set up." + ); + } + + /// + /// A step that is being executed. + /// + /// + /// It throws if there is no + /// step context. + /// + /// + public StepResult CurrentStep + { + get => this.StepContext.FirstOrDefault() + ?? throw new InvalidOperationException( + "No step context has been set up." + ); + } + + /// + /// 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 has been set up. + ///
+ /// + public ExecutableItem CurrentStepContainer + { + get => this.StepContext.FirstOrDefault() as ExecutableItem + ?? this.RootStepContainer + ?? throw new InvalidOperationException( + "No fixture, test, or step context has been set up." + ); + } + + /// + /// Creates a new with the specified + /// container pushed into the container context. + /// + /// + /// A container to push into the container context. + /// + /// + /// A new instance of with the modified + /// container context. + /// + /// + public AllureContext WithContainer(TestResultContainer container) => + this 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 a nonempty + /// container context. + /// + /// + /// Can't be called if a fixture or a test context exists. + /// + /// + /// A new instance of with the modified + /// container context. + /// + /// + public AllureContext WithNoLastContainer() => + this with + { + ContainerContext = this.ValidateContainerCanBeRemoved() + .ContainerContext.Pop() + }; + + /// + /// Creates a new with the specified + /// fixture result as the fixture context. Requires at least one + /// container in the container context. + /// + /// + /// A new fixture context. + /// + /// + /// A new instance of with the modified + /// fixture context. + /// + /// + /// + public AllureContext WithFixtureContext(FixtureResult fixtureResult) => + this with + { + FixtureContext = this.ValidateNewFixtureContext( + fixtureResult ?? throw new ArgumentNullException( + nameof(fixtureResult) + ) + ), + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with no fixture and step + /// contexts. + /// + /// + /// A new instance of without a fixture or + /// a step contexts. + /// + public AllureContext WithNoFixtureContext() => + this with + { + FixtureContext = null, + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with the specified + /// test result as the test context. Can't be used if a fixture context + /// exists. + /// + /// + /// A new test context. + /// + /// + /// A new instance of with the modified + /// test context. + /// + /// + /// + public AllureContext WithTestContext(TestResult testResult) => + this with + { + TestContext = this.ValidateNewTestContext( + testResult ?? throw new ArgumentNullException( + nameof(testResult) + ) + ) + }; + + /// + /// Creates a new with no test, fixture + /// and step contexts. + /// + /// + /// A new instance of without a test, + /// a fixture and a step contexts. + /// + public AllureContext WithNoTestContext() => + this with + { + FixtureContext = null, + TestContext = null, + StepContext = this.StepContext.Clear() + }; + + /// + /// Creates a new with the specified + /// step result pushed into the step context. Requires either a test or + /// a fixture context to exists. + /// + /// + /// A new step result to push into the step context. + /// + /// + /// A new instance of with the modified + /// step context. + /// + /// + /// + public 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 a nonempty step context. + /// + /// + /// A new instance of with the modified + /// step context. + /// + /// + public AllureContext WithNoLastStep() => + this with + { + StepContext = this.StepContext.IsEmpty + ? throw new InvalidOperationException( + "Unable to exclude the latest step from the context " + + "because no step context has been set up." + ) : this.StepContext.Pop() + }; + + AllureContext ValidateContainerCanBeRemoved() + { + if (this.ContainerContext.IsEmpty) + { + throw new InvalidOperationException( + "Unable to exclude the latest container from the " + + "context because no container context has been set up." + ); + } + + if (this.FixtureContext is not null) + { + throw new InvalidOperationException( + "Unable to exclude the latest container from the " + + "context because a fixture context exists." + ); + } + + if (this.TestContext is not null) + { + throw new InvalidOperationException( + "Unable to exclude the latest container from the " + + "context because a test context exists." + ); + } + + return this; + } + + ExecutableItem? RootStepContainer + { + get => this.FixtureContext as ExecutableItem ?? this.TestContext; + } + + FixtureResult ValidateNewFixtureContext(FixtureResult fixture) + { + if (this.ContainerContext.IsEmpty) + { + throw new InvalidOperationException( + "Unable to set up the fixture context " + + "because there is no container context." + ); + } + + if (this.FixtureContext is not null) + { + throw new InvalidOperationException( + "Unable to set up the fixture context " + + "because another fixture context already exists." + ); + } + + return fixture; + } + + TestResult ValidateNewTestContext(TestResult testResult) + { + if (this.FixtureContext is not null) + { + throw new InvalidOperationException( + "Unable to set up the test context " + + "because a fixture context is currently active." + ); + } + + if (this.TestContext is not null) + { + throw new InvalidOperationException( + "Unable to set up the test context " + + "because another test context already exists." + ); + } + + return testResult; + } + + StepResult ValidateNewStep(StepResult stepResult) + { + if (this.RootStepContainer is null) + { + throw new InvalidOperationException( + "Unable to set up the step context because no test or" + + "fixture context exists." + ); + } + + return stepResult; + } + } +} From 03aa7d9e38a3ec2b82c74a4c719b337a7dcc6dbc Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:09:04 +0700 Subject: [PATCH 02/24] Add concurrency tests. Reformulate context in terms of active/inactive --- .../{StorageTests => }/AllureContextTests.cs | 88 ++++--- .../AllureLifeCycleTest.cs | 43 ++++ Allure.Net.Commons.Tests/ConcurrencyTests.cs | 229 ++++++++++++++++++ .../InMemoryResultsWriter.cs | 34 +++ Allure.Net.Commons/AllureLifecycle.cs | 19 +- Allure.Net.Commons/Storage/AllureContext.cs | 181 +++++++------- 6 files changed, 471 insertions(+), 123 deletions(-) rename Allure.Net.Commons.Tests/{StorageTests => }/AllureContextTests.cs (84%) create mode 100644 Allure.Net.Commons.Tests/ConcurrencyTests.cs create mode 100644 Allure.Net.Commons.Tests/InMemoryResultsWriter.cs diff --git a/Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs b/Allure.Net.Commons.Tests/AllureContextTests.cs similarity index 84% rename from Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs rename to Allure.Net.Commons.Tests/AllureContextTests.cs index f4755cc4..f0087344 100644 --- a/Allure.Net.Commons.Tests/StorageTests/AllureContextTests.cs +++ b/Allure.Net.Commons.Tests/AllureContextTests.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Allure.Net.Commons.Storage; +using Allure.Net.Commons.Storage; using NUnit.Framework; -namespace Allure.Net.Commons.Tests.StorageTests +namespace Allure.Net.Commons.Tests { class AllureContextTests { @@ -25,31 +18,31 @@ public void TestEmptyContext() Assert.That( () => ctx.CurrentContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "No container context has been set up." + "No container context is active." ) ); Assert.That( () => ctx.CurrentFixture, Throws.InvalidOperationException.With.Message.EqualTo( - "No fixture context has been set up." + "No fixture context is active." ) ); Assert.That( () => ctx.CurrentTest, Throws.InvalidOperationException.With.Message.EqualTo( - "No test context has been set up." + "No test context is active." ) ); Assert.That( () => ctx.CurrentStep, Throws.InvalidOperationException.With.Message.EqualTo( - "No step context has been set up." + "No step context is active." ) ); Assert.That( () => ctx.CurrentStepContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "No fixture, test, or step context has been set up." + "No fixture, test, or step context is active." ) ); } @@ -66,6 +59,37 @@ public void TestContextOnly() 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 a container context because a 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 a container context because a fixture " + + "context is active." + ) + ); + } + [Test] public void CanNotRemoveContainerIfTestIsSet() { @@ -76,8 +100,8 @@ public void CanNotRemoveContainerIfTestIsSet() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to exclude the latest container from the " + - "context because a test context exists." + "Unable to change a container context because a test " + + "context is active." ) ); } @@ -145,8 +169,8 @@ public void CanNotRemoveContainerIfNoneExist() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to exclude the latest container from the " + - "context because no container context has been set up." + "Unable to deactivate a container context " + + "because it's inactive." ) ); } @@ -185,8 +209,8 @@ public void FixtureContextRequiresContainer() () => ctx.WithFixtureContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to set up the fixture context because there " + - "is no container context." + "Unable to activate a fixture context because a " + + "container context is inactive." ) ); } @@ -226,8 +250,8 @@ public void CanNotRemoveContainerIfFixtureIsSet() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to exclude the latest container from the " + - "context because a fixture context exists." + "Unable to change a container context because " + + "a fixture context is active." ) ); } @@ -245,8 +269,8 @@ public void FixturesCanNotBeNested() () => ctx.WithFixtureContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to set up the fixture context " + - "because another fixture context already exists." + "Unable to activate a fixture context because " + + "another fixture context is active." ) ); } @@ -273,8 +297,8 @@ public void TestsCanNotBeNested() () => ctx.WithTestContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to set up the test context " + - "because another test context already exists." + "Unable to activate a test context because another " + + "test context is active." ) ); } @@ -289,8 +313,8 @@ public void CanNotSetTestContextIfFixtureContextIsActive() Assert.That( () => ctx.WithTestContext(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to set up the test context " + - "because a fixture context is currently active." + "Unable to activate a test context because a fixture " + + "context is active." ) ); } @@ -358,8 +382,8 @@ public void StepCanNotBeAddedIfNoTestOrFixtureExists() Assert.That( () => ctx.WithStep(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to set up the step context because no test or" + - "fixture context exists." + "Unable to activate a step context because neither " + + "test, nor fixture context is active." ) ); } @@ -372,8 +396,8 @@ public void StepCanNotBeRemovedIfNoStepExists() Assert.That( () => ctx.WithNoLastStep(), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to exclude the latest step from the context " + - "because no step context has been set up." + "Unable to deactivate a step context because it's " + + "already inactive." ) ); } diff --git a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs index 6cd08fd5..08a100a0 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using System; using System.Threading.Tasks; namespace Allure.Net.Commons.Tests @@ -92,7 +93,49 @@ public void IntegrationTest() .StopTestContainer(container.uuid) .WriteTestContainer(container.uuid); }); + } + [Test, Description("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( + container.uuid, + new(), + out var fixtureId + ).StopFixture(fixtureId) + .StartStep(new(), out var stepId) + .StopStep() + .StopTestCase(testResult.uuid) + .StopTestContainer(container.uuid) + .WriteTestCase(testResult.uuid) + .WriteTestContainer(container.uuid); + + 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)); } } } diff --git a/Allure.Net.Commons.Tests/ConcurrencyTests.cs b/Allure.Net.Commons.Tests/ConcurrencyTests.cs new file mode 100644 index 00000000..f1d4331f --- /dev/null +++ b/Allure.Net.Commons.Tests/ConcurrencyTests.cs @@ -0,0 +1,229 @@ +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; + + [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"); + } + + async Task AddTestWithStepsAsync(string name, params object[] steps) + { + var uuid = Guid.NewGuid().ToString(); + this.lifecycle + .StartTestCase(new() { name = name, uuid = uuid }); + await Task.Delay(1); + await this.AddStepsAsync(steps); + this.lifecycle + .StopTestCase(uuid) + .WriteTestCase(uuid); + } + + void WrapInTest(string testName, Action action) + { + var uuid = Guid.NewGuid().ToString(); + this.lifecycle.StartTestCase( + new() { name = testName, uuid = uuid } + ); + action(uuid); + this.lifecycle + .StopTestCase(uuid) + .WriteTestCase(uuid); + } + + async Task WrapInTestAsync(string testName, Func action) + { + var uuid = Guid.NewGuid().ToString(); + this.lifecycle.StartTestCase( + new() { name = testName, uuid = uuid } + ); + await Task.Delay(1); + await action(uuid); + this.lifecycle + .StopTestCase(uuid) + .WriteTestCase(uuid); + } + + 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 }, + out var _ + ).StopStep(); + } + + void AddStepWithSubsteps(string name, params object[] substeps) + { + this.lifecycle.StartStep(new() { name = name }, out var _); + this.AddSteps(substeps); + this.lifecycle.StopStep(); + } + + async Task AddStepWithSubstepsAsync(string name, params object[] substeps) + { + this.lifecycle.StartStep(new() { name = name }, out var _); + 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.Count(); + 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..ab619d1e --- /dev/null +++ b/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Allure.Net.Commons.Writer; + +namespace Allure.Net.Commons.Tests +{ + class InMemoryResultsWriter : IAllureResultsWriter + { + internal List testResults = new(); + internal List testContainers = new(); + internal List<(string Source, byte[] Content)> attachments = new(); + + public void CleanUp() + { + this.testResults.Clear(); + this.testContainers.Clear(); + this.attachments.Clear(); + } + + public void Write(TestResult testResult) + { + this.testResults.Add(testResult); + } + + public void Write(TestResultContainer testResult) + { + this.testContainers.Add(testResult); + } + + public void Write(string source, byte[] attachment) + { + this.attachments.Add((source, attachment)); + } + } +} diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 8bd61249..97a1b295 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -33,12 +33,25 @@ public class AllureLifecycle internal AllureLifecycle(): this(GetConfiguration()) { } - - internal AllureLifecycle(JObject config) + + internal AllureLifecycle( + Func writerFactory + ) : this(GetConfiguration(), writerFactory) + { + } + + internal AllureLifecycle(JObject config): this(config, c => new FileSystemResultsWriter(c)) + { + } + + internal AllureLifecycle( + JObject config, + Func writerFactory + ) { JsonConfiguration = config.ToString(); AllureConfiguration = AllureConfiguration.ReadFromJObject(config); - writer = new FileSystemResultsWriter(AllureConfiguration); + writer = writerFactory(AllureConfiguration); storage = new AllureStorage(); } diff --git a/Allure.Net.Commons/Storage/AllureContext.cs b/Allure.Net.Commons/Storage/AllureContext.cs index 4c329031..984d9bff 100644 --- a/Allure.Net.Commons/Storage/AllureContext.cs +++ b/Allure.Net.Commons/Storage/AllureContext.cs @@ -7,24 +7,23 @@ namespace Allure.Net.Commons.Storage { /// - /// Represents information related to a particular test at all stages of - /// its life cycle. + /// 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.
- /// - /// Methods in this class don't mutate allure model. + /// cuncurrently either by a test framework or by an end user. ///
- public record class AllureContext + internal record class AllureContext { /// /// A stack of fixture containers affecting subsequent tests. /// /// - /// Setting up this context allows operations on the current container + /// Activating this context allows operations on the current container /// (including adding a fixture to or removing a fixture from the /// current container). /// @@ -38,10 +37,11 @@ public IImmutableStack ContainerContext /// A fixture that is being currently executed. /// /// - /// Setting up this context allows operations on the current fixture + /// Activating this context allows operations on the current fixture /// result.
- /// This property differs from in that it - /// returns null if no fixture context exists instead of throwing. + /// This property differs from in that + /// instead of throwing it returns null if a fixture context isn't + /// active. ///
public FixtureResult? FixtureContext { get; private init; } @@ -49,11 +49,11 @@ public IImmutableStack ContainerContext /// A test that is being executed. /// /// - /// Setting up this context allows operations on the current test + /// Activating this context allows operations on the current test /// result.
/// - /// This property differs from in that it - /// returns null if no test context exists instead of throwing. + /// This property differs from in that + /// instead of throwing it returns null if a test context isn't active. ///
public TestResult? TestContext { get; private init; } @@ -61,7 +61,7 @@ public IImmutableStack ContainerContext /// A stack of nested steps that are being executed. /// /// - /// Setting up this context allows operations on the current step. + /// Activating this context allows operations on the current step. /// public IImmutableStack StepContext { @@ -73,15 +73,15 @@ public IImmutableStack StepContext /// The most recently added container from the container context. /// /// - /// It throws if there is no - /// container context. + /// It throws if a container + /// context isn't active. /// /// public TestResultContainer CurrentContainer { get => this.ContainerContext.FirstOrDefault() ?? throw new InvalidOperationException( - "No container context has been set up." + "No container context is active." ); } @@ -89,14 +89,14 @@ public TestResultContainer CurrentContainer /// A fixture that is being executed. /// /// - /// It throws if there is no - /// fixture context. + /// It throws if a fixture + /// context isn't active. /// /// public FixtureResult CurrentFixture { get => this.FixtureContext ?? throw new InvalidOperationException( - "No fixture context has been set up." + "No fixture context is active." ); } @@ -104,14 +104,14 @@ public FixtureResult CurrentFixture /// A test that is being executed. /// /// - /// It throws if there is no - /// test context. + /// It throws if a test context + /// isn't active. /// /// public TestResult CurrentTest { get => this.TestContext ?? throw new InvalidOperationException( - "No test context has been set up." + "No test context is active." ); } @@ -119,15 +119,15 @@ public TestResult CurrentTest /// A step that is being executed. /// /// - /// It throws if there is no - /// step context. + /// It throws if a step context + /// isn't active. /// /// public StepResult CurrentStep { get => this.StepContext.FirstOrDefault() ?? throw new InvalidOperationException( - "No step context has been set up." + "No step context is active." ); } @@ -137,7 +137,7 @@ public StepResult CurrentStep /// /// A step container can be a fixture, a test of an another step.
/// It throws if neither - /// fixture nor test nor step context has been set up. + /// fixture, nor test, nor step context is active. ///
/// public ExecutableItem CurrentStepContainer @@ -145,24 +145,27 @@ public ExecutableItem CurrentStepContainer get => this.StepContext.FirstOrDefault() as ExecutableItem ?? this.RootStepContainer ?? throw new InvalidOperationException( - "No fixture, test, or step context has been set up." + "No fixture, test, or step context is active." ); } /// - /// Creates a new with the specified - /// container pushed into the container context. + /// 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 into the container context. + /// A container to push on top of the container context. /// /// /// A new instance of with the modified - /// container context. + /// (always active) container context. /// /// public AllureContext WithContainer(TestResultContainer container) => - this with + this.ValidateContainerContextCanBeModified() with { ContainerContext = this.ContainerContext.Push( container ?? throw new ArgumentNullException( @@ -173,15 +176,16 @@ this with /// /// Creates a new without the most recently - /// added container in its container context. Requires a nonempty - /// container context. + /// 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 exists. + /// Can't be called if a fixture or a test context is active. /// /// /// A new instance of with the modified - /// container context. + /// (possibly inactive) container context. /// /// public AllureContext WithNoLastContainer() => @@ -192,16 +196,16 @@ this with }; /// - /// Creates a new with the specified - /// fixture result as the fixture context. Requires at least one - /// container in the container context. + /// Creates a new with the active fixture + /// context that is set to the specified fixture. Requires an active + /// container context. /// /// /// A new fixture context. /// /// /// A new instance of with the modified - /// fixture context. + /// (always active) fixture context. /// /// /// @@ -217,13 +221,9 @@ this with }; /// - /// Creates a new with no fixture and step - /// contexts. + /// Creates a new with inactive fixture and + /// step contexts. /// - /// - /// A new instance of without a fixture or - /// a step contexts. - /// public AllureContext WithNoFixtureContext() => this with { @@ -232,16 +232,16 @@ this with }; /// - /// Creates a new with the specified - /// test result as the test context. Can't be used if a fixture context - /// exists. + /// 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 - /// test context. + /// (always active) test context. /// /// /// @@ -256,13 +256,9 @@ this with }; /// - /// Creates a new with no test, fixture - /// and step contexts. + /// Creates a new with inactive test, + /// fixture and step contexts. /// - /// - /// A new instance of without a test, - /// a fixture and a step contexts. - /// public AllureContext WithNoTestContext() => this with { @@ -272,16 +268,18 @@ this with }; /// - /// Creates a new with the specified - /// step result pushed into the step context. Requires either a test or - /// a fixture context to exists. + /// 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 into the step context. + /// A new step result to push on top of the step context. /// /// /// A new instance of with the modified - /// step context. + /// (always active) step context. /// /// /// @@ -299,11 +297,13 @@ this with /// /// Creates a new without the most recently - /// added step in its step context. Requires a nonempty step context. + /// 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 - /// step context. + /// (possibly inactive) step context. /// /// public AllureContext WithNoLastStep() => @@ -311,38 +311,43 @@ this with { StepContext = this.StepContext.IsEmpty ? throw new InvalidOperationException( - "Unable to exclude the latest step from the context " + - "because no step context has been set up." + "Unable to deactivate a step context because it's " + + "already inactive." ) : this.StepContext.Pop() }; - AllureContext ValidateContainerCanBeRemoved() + AllureContext ValidateContainerContextCanBeModified() { - if (this.ContainerContext.IsEmpty) + if (this.FixtureContext is not null) { throw new InvalidOperationException( - "Unable to exclude the latest container from the " + - "context because no container context has been set up." + "Unable to change a container context because a " + + "fixture context is active." ); } - if (this.FixtureContext is not null) + if (this.TestContext is not null) { throw new InvalidOperationException( - "Unable to exclude the latest container from the " + - "context because a fixture context exists." + "Unable to change a container context because a test " + + "context is active." ); } - if (this.TestContext is not null) + return this; + } + + AllureContext ValidateContainerCanBeRemoved() + { + if (this.ContainerContext.IsEmpty) { throw new InvalidOperationException( - "Unable to exclude the latest container from the " + - "context because a test context exists." + "Unable to deactivate a container context because it's " + + "inactive." ); } - return this; + return this.ValidateContainerContextCanBeModified(); } ExecutableItem? RootStepContainer @@ -355,16 +360,16 @@ FixtureResult ValidateNewFixtureContext(FixtureResult fixture) if (this.ContainerContext.IsEmpty) { throw new InvalidOperationException( - "Unable to set up the fixture context " + - "because there is no container context." + "Unable to activate a fixture context " + + "because a container context is inactive." ); } if (this.FixtureContext is not null) { throw new InvalidOperationException( - "Unable to set up the fixture context " + - "because another fixture context already exists." + "Unable to activate a fixture context " + + "because another fixture context is active." ); } @@ -376,16 +381,16 @@ TestResult ValidateNewTestContext(TestResult testResult) if (this.FixtureContext is not null) { throw new InvalidOperationException( - "Unable to set up the test context " + - "because a fixture context is currently active." + "Unable to activate a test context " + + "because a fixture context is active." ); } if (this.TestContext is not null) { throw new InvalidOperationException( - "Unable to set up the test context " + - "because another test context already exists." + "Unable to activate a test context " + + "because another test context is active." ); } @@ -397,8 +402,8 @@ StepResult ValidateNewStep(StepResult stepResult) if (this.RootStepContainer is null) { throw new InvalidOperationException( - "Unable to set up the step context because no test or" + - "fixture context exists." + "Unable to activate a step context because neither test, " + + "nor fixture context is active." ); } From 152b66c496c031818bc4c9f3d021086ac14b11c0 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Sat, 15 Jul 2023 07:19:49 +0700 Subject: [PATCH 03/24] Add tests on the context capturing by child threads/tasks --- Allure.Net.Commons.Tests/ConcurrencyTests.cs | 99 ++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/Allure.Net.Commons.Tests/ConcurrencyTests.cs b/Allure.Net.Commons.Tests/ConcurrencyTests.cs index f1d4331f..5c712fef 100644 --- a/Allure.Net.Commons.Tests/ConcurrencyTests.cs +++ b/Allure.Net.Commons.Tests/ConcurrencyTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using NUnit.Framework; using NUnit.Framework.Internal; @@ -69,6 +70,82 @@ await this.WrapInTestAsync("test-1", async _ => await Task.WhenAll( 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 + */ + this.WrapInTest( + "test", + _ => this.WrapInStep( + "outer", + _ => RunThreads( + () => this.AddSteps( + ("inner-1", new object[] { "inner-1-1", "inner-1-2" }) + ), + () => this.AddSteps( + ("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" }) + }) + ); + } + + [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" }) + }) + ); + } + async Task AddTestWithStepsAsync(string name, params object[] steps) { var uuid = Guid.NewGuid().ToString(); @@ -93,6 +170,28 @@ void WrapInTest(string testName, Action action) .WriteTestCase(uuid); } + void WrapInStep(string stepName, Action action) + { + this.lifecycle.StartStep( + new() { name = stepName }, + out var stepId + ); + action(stepId); + this.lifecycle + .StopStep(); + } + + async Task WrapInStepAsync(string stepName, Func action) + { + this.lifecycle.StartStep( + new() { name = stepName }, + out var stepId + ); + await action(stepId); + this.lifecycle + .StopStep(); + } + async Task WrapInTestAsync(string testName, Func action) { var uuid = Guid.NewGuid().ToString(); From 5abf894820fcbcb8979b18cbc05741c15e32313e Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 06:11:27 +0700 Subject: [PATCH 04/24] Modify lifecycle/storage to use new context model. Add context get/set API --- .../AllureLifeCycleTest.cs | 58 +++- Allure.Net.Commons.Tests/ConcurrencyTests.cs | 59 ++-- Allure.Net.Commons/AllureLifecycle.cs | 276 +++++++++++++----- Allure.Net.Commons/Storage/AllureContext.cs | 38 +-- Allure.Net.Commons/Storage/AllureStorage.cs | 213 ++++++++++++-- 5 files changed, 483 insertions(+), 161 deletions(-) diff --git a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs index 08a100a0..085268f5 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -1,5 +1,7 @@ -using NUnit.Framework; +using Allure.Net.Commons.Storage; +using NUnit.Framework; using System; +using System.Threading; using System.Threading.Tasks; namespace Allure.Net.Commons.Tests @@ -19,6 +21,19 @@ public void ShouldSetDefaultStateAsNone() Assert.AreEqual(Status.none, new TestResult().status); } + [Test] + public void Way() + { + AsyncLocal myContext = new(); + myContext.Value = "Outer"; + Parallel.For(0, 2, i => + { + Console.WriteLine(myContext.Value); + myContext.Value = $"Inner {i}"; + }); + Console.WriteLine(myContext.Value); + } + [Test, Description("Integration Test")] public void IntegrationTest() { @@ -95,7 +110,7 @@ public void IntegrationTest() }); } - [Test, Description("Test step should be correctly added even if a " + + [Test, Description("A test step should be correctly added even if a " + "before fixture overlaps with the test")] public void BeforeFixtureMayOverlapsWithTest() { @@ -113,17 +128,14 @@ public void BeforeFixtureMayOverlapsWithTest() lifecycle.StartTestContainer(container) .StartTestCase(testResult) - .StartBeforeFixture( - container.uuid, - new(), - out var fixtureId - ).StopFixture(fixtureId) - .StartStep(new(), out var stepId) + .StartBeforeFixture(fixture) + .StopFixture() + .StartStep(new()) .StopStep() - .StopTestCase(testResult.uuid) - .StopTestContainer(container.uuid) - .WriteTestCase(testResult.uuid) - .WriteTestContainer(container.uuid); + .StopTestCase() + .StopTestContainer() + .WriteTestCase() + .WriteTestContainer(); Assert.That(writer.testContainers.Count, Is.EqualTo(1)); Assert.That(writer.testContainers[0].uuid, Is.EqualTo(container.uuid)); @@ -137,5 +149,27 @@ out var fixtureId Assert.That(writer.testResults.Count, Is.EqualTo(1)); Assert.That(writer.testResults[0].uuid, Is.EqualTo(testResult.uuid)); } + + [Test] + public async Task AllureContextCouldBeAssigned() + { + var writer = new InMemoryResultsWriter(); + var lifecycle = new AllureLifecycle(_ => writer); + AllureContext context = null; + await Task.Factory.StartNew(() => + { + lifecycle.StartTestCase(new() + { + uuid = Guid.NewGuid().ToString() + }); + context = lifecycle.Context; + }); + lifecycle.Context = context; + + lifecycle.StopTestCase(); + lifecycle.WriteTestCase(); + + Assert.That(writer.testResults, Is.Not.Empty); + } } } diff --git a/Allure.Net.Commons.Tests/ConcurrencyTests.cs b/Allure.Net.Commons.Tests/ConcurrencyTests.cs index 5c712fef..0809edd2 100644 --- a/Allure.Net.Commons.Tests/ConcurrencyTests.cs +++ b/Allure.Net.Commons.Tests/ConcurrencyTests.cs @@ -50,7 +50,7 @@ await Task.WhenAll( [Test] public void ParallelStepsOfTestAreIsolated() { - this.WrapInTest("test-1", _ => RunThreads( + this.WrapInTest("test-1", () => RunThreads( () => this.AddStep("step-1"), () => this.AddStep("step-2") )); @@ -61,7 +61,7 @@ public void ParallelStepsOfTestAreIsolated() [Test] public async Task AsyncStepsOfTestAreIsolated() { - await this.WrapInTestAsync("test-1", async _ => await Task.WhenAll( + await this.WrapInTestAsync("test-1", async () => await Task.WhenAll( this.AddStepsAsync("step-1"), this.AddStepsAsync("step-2"), this.AddStepsAsync("step-3") @@ -85,9 +85,9 @@ public void ContextCapturedBySubThreads() */ this.WrapInTest( "test", - _ => this.WrapInStep( + () => this.WrapInStep( "outer", - _ => RunThreads( + () => RunThreads( () => this.AddSteps( ("inner-1", new object[] { "inner-1-1", "inner-1-2" }) ), @@ -123,9 +123,9 @@ public async Task ContextCapturedBySubTasks() */ await this.WrapInTestAsync( "test", - async _ => await this.WrapInStepAsync( + async () => await this.WrapInStepAsync( "outer", - async _ => await Task.WhenAll( + async () => await Task.WhenAll( this.AddStepsAsync( ("inner-1", new object[] { "inner-1-1", "inner-1-2" }) ), @@ -158,55 +158,51 @@ async Task AddTestWithStepsAsync(string name, params object[] steps) .WriteTestCase(uuid); } - void WrapInTest(string testName, Action action) + void WrapInTest(string testName, Action action) { - var uuid = Guid.NewGuid().ToString(); this.lifecycle.StartTestCase( - new() { name = testName, uuid = uuid } + new() { name = testName, uuid = Guid.NewGuid().ToString() } ); - action(uuid); + action(); this.lifecycle - .StopTestCase(uuid) - .WriteTestCase(uuid); + .StopTestCase() + .WriteTestCase(); } - void WrapInStep(string stepName, Action action) + void WrapInStep(string stepName, Action action) { this.lifecycle.StartStep( - new() { name = stepName }, - out var stepId + new() { name = stepName } ); - action(stepId); + action(); this.lifecycle .StopStep(); } - async Task WrapInStepAsync(string stepName, Func action) + async Task WrapInStepAsync(string stepName, Func action) { this.lifecycle.StartStep( - new() { name = stepName }, - out var stepId + new() { name = stepName } ); - await action(stepId); + await action(); this.lifecycle .StopStep(); } - async Task WrapInTestAsync(string testName, Func action) + async Task WrapInTestAsync(string testName, Func action) { - var uuid = Guid.NewGuid().ToString(); this.lifecycle.StartTestCase( - new() { name = testName, uuid = uuid } + new() { name = testName, uuid = Guid.NewGuid().ToString() } ); await Task.Delay(1); - await action(uuid); + await action(); this.lifecycle - .StopTestCase(uuid) - .WriteTestCase(uuid); + .StopTestCase() + .WriteTestCase(); } void AddTestWithSteps(string name, params object[] steps) => - this.WrapInTest(name, _ => this.AddSteps(steps)); + this.WrapInTest(name, () => this.AddSteps(steps)); async Task AddStepsAsync(params object[] steps) { @@ -243,21 +239,20 @@ void AddSteps(params object[] steps) void AddStep(string name) { this.lifecycle.StartStep( - new() { name = name }, - out var _ + new() { name = name } ).StopStep(); } void AddStepWithSubsteps(string name, params object[] substeps) { - this.lifecycle.StartStep(new() { name = name }, out var _); + 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 }, out var _); + this.lifecycle.StartStep(new() { name = name }); await this.AddStepsAsync(substeps); this.lifecycle.StopStep(); } @@ -274,7 +269,7 @@ void AssertTestWithSteps(string testName, params object[] steps) void AssertSteps(List actualSteps, params object[] steps) { - var expectedCount = steps.Count(); + var expectedCount = steps.Length; Assert.That(actualSteps.Count, Is.EqualTo(expectedCount)); for (var i = 0; i < expectedCount; i++) { diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 97a1b295..7c7d260d 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -5,12 +5,13 @@ 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 @@ -22,13 +23,30 @@ public class AllureLifecycle public IReadOnlyDictionary TypeFormatters => new ReadOnlyDictionary(typeFormatters); - private static readonly object Lockobj = new(); - private static AllureLifecycle instance; + private static readonly Lazy instance = + new(Initialize); private readonly AllureStorage storage; private readonly IAllureResultsWriter writer; + + /// + /// Gets or sets an execution context of Allure. Use this property if + /// the context is set not in the same async domain where a + /// test/fixture function is executed. + /// + /// + /// This property is intended to be used by Allure integrations with + /// test frameworks, not by end user's code. + /// + public AllureContext Context + { + get => this.storage.CurrentContext; + set => this.storage.CurrentContext = value; + } + /// Method to get the key for separation the steps for different tests. - public static Func CurrentTestIdGetter { get; set; } = () => Thread.CurrentThread.ManagedThreadId.ToString(); + public static Func CurrentTestIdGetter { get; set; } = + () => Thread.CurrentThread.ManagedThreadId.ToString(); internal AllureLifecycle(): this(GetConfiguration()) { @@ -56,29 +74,12 @@ Func writerFactory } public string JsonConfiguration { get; private set; } + public AllureConfiguration AllureConfiguration { get; } public string ResultsDirectory => writer.ToString(); - 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 static AllureLifecycle Instance { get => instance.Value; } public void AddTypeFormatter(TypeFormatter typeFormatter) => AddTypeFormatterImpl(typeof(T), typeFormatter); @@ -91,7 +92,10 @@ private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => public virtual AllureLifecycle StartTestContainer(TestResultContainer container) { container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.Put(container.uuid, container); + this.storage.CurrentTestContainerOrNull?.children.Add( + container.uuid + ); + storage.PutTestContainer(container); return this; } @@ -102,21 +106,43 @@ public virtual AllureLifecycle StartTestContainer(string parentUuid, TestResultC return this; } + public virtual AllureLifecycle UpdateTestContainer(Action update) + { + update.Invoke(storage.CurrentTestContainer); + return this; + } + public virtual AllureLifecycle UpdateTestContainer(string uuid, Action update) { update.Invoke(storage.Get(uuid)); return this; } + public virtual AllureLifecycle StopTestContainer() + { + UpdateTestContainer(stopContainer); + return this; + } + public virtual AllureLifecycle StopTestContainer(string uuid) { - UpdateTestContainer(uuid, c => c.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds()); + UpdateTestContainer(uuid, stopContainer); + return this; + } + + public virtual AllureLifecycle WriteTestContainer() + { + writer.Write( + storage.RemoveTestContainer() + ); return this; } public virtual AllureLifecycle WriteTestContainer(string uuid) { - writer.Write(storage.Remove(uuid)); + writer.Write( + storage.RemoveTestContainer(uuid) + ); return this; } @@ -124,6 +150,27 @@ public virtual AllureLifecycle WriteTestContainer(string uuid) #region Fixture + public virtual AllureLifecycle StartBeforeFixture(FixtureResult result) + { + UpdateTestContainer(container => container.befores.Add(result)); + this.StartFixture(result); + return this; + } + + public virtual AllureLifecycle StartBeforeFixture(FixtureResult result, out string uuid) + { + uuid = CreateUuid(); + StartBeforeFixture(uuid, result); + return this; + } + + public virtual AllureLifecycle StartBeforeFixture(string uuid, FixtureResult result) + { + UpdateTestContainer(container => container.befores.Add(result)); + StartFixture(uuid, result); + return this; + } + public virtual AllureLifecycle StartBeforeFixture(string parentUuid, FixtureResult result, out string uuid) { uuid = Guid.NewGuid().ToString("N"); @@ -138,9 +185,16 @@ public virtual AllureLifecycle StartBeforeFixture(string parentUuid, string uuid return this; } + 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) { - uuid = Guid.NewGuid().ToString("N"); + uuid = CreateUuid(); StartAfterFixture(parentUuid, uuid, result); return this; } @@ -154,7 +208,7 @@ public virtual AllureLifecycle StartAfterFixture(string parentUuid, string uuid, public virtual AllureLifecycle UpdateFixture(Action update) { - UpdateFixture(storage.GetRootStep(), update); + update?.Invoke(storage.CurrentFixture); return this; } @@ -167,13 +221,20 @@ public virtual AllureLifecycle UpdateFixture(string uuid, Action public virtual AllureLifecycle StopFixture(Action beforeStop) { UpdateFixture(beforeStop); - return StopFixture(storage.GetRootStep()); + return this.StopFixture(); + } + + public virtual AllureLifecycle StopFixture() + { + var fixture = this.storage.RemoveFixture(); + fixture.stage = Stage.finished; + fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + return this; } public virtual AllureLifecycle StopFixture(string uuid) { - var fixture = storage.Remove(uuid); - storage.ClearStepContext(); + var fixture = this.storage.RemoveFixture(uuid); fixture.stage = Stage.finished; fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); return this; @@ -191,43 +252,62 @@ public virtual AllureLifecycle StartTestCase(string containerUuid, TestResult te public virtual AllureLifecycle StartTestCase(TestResult testResult) { + this.storage.CurrentTestContainerOrNull?.children.Add(testResult.uuid); 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); + testResult.start = testResult.start == 0L + ? DateTimeOffset.Now.ToUnixTimeMilliseconds() + : testResult.start; + this.storage.PutTestCase(testResult); return this; } - public virtual AllureLifecycle UpdateTestCase(string uuid, Action update) + public virtual AllureLifecycle UpdateTestCase( + string uuid, + Action update + ) { - update.Invoke(storage.Get(uuid)); + var testResult = this.storage.Get(uuid); + update(testResult); return this; } - public virtual AllureLifecycle UpdateTestCase(Action update) + public virtual AllureLifecycle UpdateTestCase( + Action update + ) { - return UpdateTestCase(storage.GetRootStep(), update); + update(this.storage.CurrentTest); + return this; } - public virtual AllureLifecycle StopTestCase(Action beforeStop) + public virtual AllureLifecycle StopTestCase( + Action beforeStop + ) { - UpdateTestCase(beforeStop); - return StopTestCase(storage.GetRootStep()); + var testResult = this.storage.CurrentTest; + beforeStop(testResult); + stopTestCase(testResult); + return this; } - public virtual AllureLifecycle StopTestCase(string uuid) + public virtual AllureLifecycle StopTestCase() => + this.UpdateTestCase(stopTestCase); + + public virtual AllureLifecycle StopTestCase(string uuid) => + this.UpdateTestCase(uuid, stopTestCase); + + public virtual AllureLifecycle WriteTestCase() { - var testResult = storage.Get(uuid); - testResult.stage = Stage.finished; - testResult.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.ClearStepContext(); + this.writer.Write( + this.storage.RemoveTestCase() + ); return this; } public virtual AllureLifecycle WriteTestCase(string uuid) { - writer.Write(storage.Remove(uuid)); + this.writer.Write( + this.storage.RemoveTestCase(uuid) + ); return this; } @@ -235,31 +315,40 @@ public virtual AllureLifecycle WriteTestCase(string uuid) #region Step - public virtual AllureLifecycle StartStep(StepResult result, out string uuid) + public virtual AllureLifecycle StartStep(StepResult result) { - 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); + result.stage = Stage.running; + result.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.storage.CurrentStepContainer.steps.Add(result); + this.storage.PutStep(result); return this; } - public virtual AllureLifecycle StartStep(string parentUuid, string uuid, StepResult stepResult) + public virtual AllureLifecycle StartStep(StepResult result, out string uuid) { - stepResult.stage = Stage.running; - stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.StartStep(uuid); - storage.AddStep(parentUuid, uuid, stepResult); + uuid = CreateUuid(); + StartStep(this.storage.CurrentStepContainer, uuid, result); return this; } + + public virtual AllureLifecycle StartStep( + string uuid, + StepResult result + ) => this.StartStep(this.storage.CurrentStepContainer, uuid, result); + + public virtual AllureLifecycle StartStep( + string parentUuid, + string uuid, + StepResult stepResult + ) => this.StartStep( + this.storage.Get(parentUuid), + uuid, + stepResult + ); public virtual AllureLifecycle UpdateStep(Action update) { - update.Invoke(storage.Get(storage.GetCurrentStep())); + update.Invoke(this.storage.CurrentStep); return this; } @@ -271,22 +360,23 @@ public virtual AllureLifecycle UpdateStep(string uuid, Action update public virtual AllureLifecycle StopStep(Action beforeStop) { - UpdateStep(beforeStop); - return StopStep(storage.GetCurrentStep()); + this.UpdateStep(beforeStop); + return this.StopStep(); } public virtual AllureLifecycle StopStep(string uuid) { - var step = storage.Remove(uuid); + var step = this.storage.RemoveStep(uuid); step.stage = Stage.finished; step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.StopStep(); return this; } public virtual AllureLifecycle StopStep() { - StopStep(storage.GetCurrentStep()); + var step = this.storage.RemoveStep(); + step.stage = Stage.finished; + step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); return this; } @@ -304,7 +394,7 @@ public virtual AllureLifecycle AddAttachment(string name, string type, string pa public virtual AllureLifecycle AddAttachment(string name, string type, byte[] content, string fileExtension = "") { - var source = $"{Guid.NewGuid().ToString("N")}{AllureConstants.ATTACHMENT_FILE_SUFFIX}{fileExtension}"; + var source = $"{CreateUuid()}{AllureConstants.ATTACHMENT_FILE_SUFFIX}{fileExtension}"; var attachment = new Attachment { name = name, @@ -312,13 +402,13 @@ public virtual AllureLifecycle AddAttachment(string name, string type, byte[] co source = source }; writer.Write(source, content); - storage.Get(storage.GetCurrentStep()).attachments.Add(attachment); + this.storage.CurrentStepContainer.attachments.Add(attachment); return this; } - public virtual AllureLifecycle AddAttachment(string path, string name = null) + public virtual AllureLifecycle AddAttachment(string path, string? name = null) { - name = name ?? Path.GetFileName(path); + name ??= Path.GetFileName(path); var type = MimeTypesMap.GetMimeType(path); return AddAttachment(name, type, path); } @@ -348,6 +438,8 @@ public virtual AllureLifecycle AddScreenDiff(string testCaseUuid, string expecte #region Privates + static AllureLifecycle Initialize() => new(); + private static JObject GetConfiguration() { var jsonConfigPath = Environment.GetEnvironmentVariable(AllureConstants.ALLURE_CONFIG_ENV_VARIABLE); @@ -368,15 +460,47 @@ private static JObject GetConfiguration() return JObject.Parse("{}"); } - private void StartFixture(string uuid, FixtureResult fixtureResult) + private void StartFixture(FixtureResult fixtureResult) + { + storage.PutFixture(fixtureResult); + fixtureResult.stage = Stage.running; + fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + } + + void StartFixture(string uuid, FixtureResult fixtureResult) { - storage.Put(uuid, fixtureResult); + storage.PutFixture(uuid, fixtureResult); fixtureResult.stage = Stage.running; fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - storage.ClearStepContext(); - storage.StartStep(uuid); } + AllureLifecycle StartStep( + ExecutableItem parent, + string uuid, + StepResult stepResult + ) + { + stepResult.stage = Stage.running; + stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + parent.steps.Add(stepResult); + this.storage.PutStep(uuid, stepResult); + return this; + } + + static readonly Action stopContainer = + c => c.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + static readonly Action stopTestCase = + tr => + { + tr.stage = Stage.finished; + tr.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }; + + + static string CreateUuid() => + Guid.NewGuid().ToString("N"); + #endregion } } \ No newline at end of file diff --git a/Allure.Net.Commons/Storage/AllureContext.cs b/Allure.Net.Commons/Storage/AllureContext.cs index 984d9bff..a6b0c780 100644 --- a/Allure.Net.Commons/Storage/AllureContext.cs +++ b/Allure.Net.Commons/Storage/AllureContext.cs @@ -17,7 +17,7 @@ namespace Allure.Net.Commons.Storage /// between different tests and steps that may potentially be run /// cuncurrently either by a test framework or by an end user. /// - internal record class AllureContext + public record class AllureContext { /// /// A stack of fixture containers affecting subsequent tests. @@ -27,7 +27,7 @@ internal record class AllureContext /// (including adding a fixture to or removing a fixture from the /// current container). /// - public IImmutableStack ContainerContext + internal IImmutableStack ContainerContext { get; private init; @@ -43,7 +43,7 @@ public IImmutableStack ContainerContext /// instead of throwing it returns null if a fixture context isn't /// active. /// - public FixtureResult? FixtureContext { get; private init; } + internal FixtureResult? FixtureContext { get; private init; } /// /// A test that is being executed. @@ -55,7 +55,7 @@ public IImmutableStack ContainerContext /// This property differs from in that /// instead of throwing it returns null if a test context isn't active. /// - public TestResult? TestContext { get; private init; } + internal TestResult? TestContext { get; private init; } /// /// A stack of nested steps that are being executed. @@ -63,7 +63,7 @@ public IImmutableStack ContainerContext /// /// Activating this context allows operations on the current step. /// - public IImmutableStack StepContext + internal IImmutableStack StepContext { get; private init; @@ -77,7 +77,7 @@ public IImmutableStack StepContext /// context isn't active. /// /// - public TestResultContainer CurrentContainer + internal TestResultContainer CurrentContainer { get => this.ContainerContext.FirstOrDefault() ?? throw new InvalidOperationException( @@ -93,7 +93,7 @@ public TestResultContainer CurrentContainer /// context isn't active. /// /// - public FixtureResult CurrentFixture + internal FixtureResult CurrentFixture { get => this.FixtureContext ?? throw new InvalidOperationException( "No fixture context is active." @@ -108,7 +108,7 @@ public FixtureResult CurrentFixture /// isn't active. /// /// - public TestResult CurrentTest + internal TestResult CurrentTest { get => this.TestContext ?? throw new InvalidOperationException( "No test context is active." @@ -123,7 +123,7 @@ public TestResult CurrentTest /// isn't active. /// /// - public StepResult CurrentStep + internal StepResult CurrentStep { get => this.StepContext.FirstOrDefault() ?? throw new InvalidOperationException( @@ -140,7 +140,7 @@ public StepResult CurrentStep /// fixture, nor test, nor step context is active. /// /// - public ExecutableItem CurrentStepContainer + internal ExecutableItem CurrentStepContainer { get => this.StepContext.FirstOrDefault() as ExecutableItem ?? this.RootStepContainer @@ -164,7 +164,7 @@ public ExecutableItem CurrentStepContainer /// (always active) container context. /// /// - public AllureContext WithContainer(TestResultContainer container) => + internal AllureContext WithContainer(TestResultContainer container) => this.ValidateContainerContextCanBeModified() with { ContainerContext = this.ContainerContext.Push( @@ -188,7 +188,7 @@ public AllureContext WithContainer(TestResultContainer container) => /// (possibly inactive) container context. /// /// - public AllureContext WithNoLastContainer() => + internal AllureContext WithNoLastContainer() => this with { ContainerContext = this.ValidateContainerCanBeRemoved() @@ -209,7 +209,7 @@ this with /// /// /// - public AllureContext WithFixtureContext(FixtureResult fixtureResult) => + internal AllureContext WithFixtureContext(FixtureResult fixtureResult) => this with { FixtureContext = this.ValidateNewFixtureContext( @@ -224,7 +224,7 @@ this with /// Creates a new with inactive fixture and /// step contexts. /// - public AllureContext WithNoFixtureContext() => + internal AllureContext WithNoFixtureContext() => this with { FixtureContext = null, @@ -245,7 +245,7 @@ this with /// /// /// - public AllureContext WithTestContext(TestResult testResult) => + internal AllureContext WithTestContext(TestResult testResult) => this with { TestContext = this.ValidateNewTestContext( @@ -259,7 +259,7 @@ this with /// Creates a new with inactive test, /// fixture and step contexts. /// - public AllureContext WithNoTestContext() => + internal AllureContext WithNoTestContext() => this with { FixtureContext = null, @@ -283,7 +283,7 @@ this with /// /// /// - public AllureContext WithStep(StepResult stepResult) => + internal AllureContext WithStep(StepResult stepResult) => this with { StepContext = this.StepContext.Push( @@ -306,7 +306,7 @@ this with /// (possibly inactive) step context. /// /// - public AllureContext WithNoLastStep() => + internal AllureContext WithNoLastStep() => this with { StepContext = this.StepContext.IsEmpty @@ -330,7 +330,7 @@ AllureContext ValidateContainerContextCanBeModified() { throw new InvalidOperationException( "Unable to change a container context because a test " + - "context is active." + "context is active." ); } diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 2e03247d..741ca4e6 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -1,25 +1,65 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Threading; + +#nullable enable namespace Allure.Net.Commons.Storage { internal class AllureStorage { - private readonly ConcurrentDictionary> stepContext = new(); + readonly ConcurrentDictionary storage = new(); + readonly AsyncLocal context = new(); - private readonly ConcurrentDictionary storage = new(); + public AllureContext CurrentContext + { + get => this.context.Value ??= new(); + set => this.context.Value = value; + } + + public AllureStorage() + { + this.CurrentContext = new(); + } + + public TestResultContainer? CurrentTestContainerOrNull + { + get => this.CurrentContext.ContainerContext.FirstOrDefault(); + } - private LinkedList Steps => stepContext.GetOrAdd( - AllureLifecycle.CurrentTestIdGetter(), - new LinkedList() - ); + public TestResultContainer CurrentTestContainer + { + get => this.CurrentContext.CurrentContainer; + } + + public FixtureResult CurrentFixture + { + get => this.CurrentContext.CurrentFixture; + } + + public TestResult CurrentTest + { + get => this.CurrentContext.CurrentTest; + } + + public ExecutableItem CurrentStepContainer + { + get => this.CurrentContext.CurrentStepContainer; + } + + public StepResult CurrentStep + { + get => this.CurrentContext.CurrentStep; + } public T Get(string 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); } @@ -30,36 +70,165 @@ public T Remove(string uuid) return (T) value; } - public void ClearStepContext() + public void PutTestContainer(TestResultContainer container) => + this.PutAndUpdateContext( + container.uuid, + container, + c => c.WithContainer(container) + ); + + public TestResultContainer RemoveTestContainer() => + this.RemoveAndUpdateContext( + this.CurrentTestContainer.uuid, + c => c.WithNoLastContainer() + ); + + public TestResultContainer RemoveTestContainer(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => ContextWithNoContainer(c, uuid) + ); + + public void PutFixture(FixtureResult fixture) => + this.UpdateContext(c => c.WithFixtureContext(fixture)); + + public void PutFixture(string uuid, FixtureResult fixture) => + this.PutAndUpdateContext( + uuid, + fixture, + c => c.WithFixtureContext(fixture) + ); + + public FixtureResult RemoveFixture() { - Steps.Clear(); - stepContext.TryRemove(AllureLifecycle.CurrentTestIdGetter(), out _); + var fixture = this.CurrentFixture; + this.UpdateContext(c => c.WithNoFixtureContext()); + return fixture; } - public void StartStep(string uuid) + public FixtureResult RemoveFixture(string uuid) + => this.RemoveAndUpdateContext( + uuid, + c => ReferenceEquals( + c.CurrentFixture, + this.Get(uuid) + ) ? c.WithNoFixtureContext() : c + ); + + public void PutTestCase(TestResult testResult) => + this.PutAndUpdateContext( + testResult.uuid, + testResult, + c => c.WithTestContext(testResult) + ); + + public TestResult RemoveTestCase() => + this.RemoveAndUpdateContext( + this.CurrentTest.uuid, + c => c.WithNoTestContext() + ); + + public TestResult RemoveTestCase(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => c.CurrentTest.uuid == uuid ? c.WithNoTestContext() : c + ); + + public void PutStep(StepResult stepResult) => + this.UpdateContext( + c => c.WithStep(stepResult) + ); + + public void PutStep(string uuid, StepResult stepResult) => + this.PutAndUpdateContext( + uuid, + stepResult, + c => c.WithStep(stepResult) + ); + + public StepResult RemoveStep() + { + var step = this.CurrentStep; + this.UpdateContext(c => c.WithNoLastStep()); + return step; + } + + public StepResult RemoveStep(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => this.ContextWithNoStep(c, uuid) + ); + + T PutAndUpdateContext( + string uuid, + T value, + Func updateFn + ) where T : notnull { - Steps.AddFirst(uuid); + var result = this.Put(uuid, value); + this.UpdateContext(updateFn); + return result; } - public void StopStep() + T RemoveAndUpdateContext(string uuid, Func updateFn) { - Steps.RemoveFirst(); + this.UpdateContext(updateFn); + return this.Remove(uuid); } - public string GetRootStep() + void UpdateContext(Func updateFn) { - return Steps.Last?.Value; + this.CurrentContext = updateFn(this.CurrentContext); } - public string GetCurrentStep() + AllureContext ContextWithNoStep(AllureContext context, string uuid) { - return Steps.First?.Value; + var stepResult = this.Get(uuid); + 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" + ); + } + } + while (stepsToPushAgain.Any()) + { + context = context.WithStep( + stepsToPushAgain.Pop() + ); + } + return context; } - public void AddStep(string parentUuid, string uuid, StepResult stepResult) + static AllureContext ContextWithNoContainer( + AllureContext context, + string uuid + ) { - Put(uuid, stepResult); - Get(parentUuid).steps.Add(stepResult); + 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" + ); + } + } + while (containersToPushAgain.Any()) + { + context = context.WithContainer( + containersToPushAgain.Pop() + ); + } + return context; } } } \ No newline at end of file From ce1efb73502d9185c7a11fb5bc6a590b8317c801 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:16:45 +0700 Subject: [PATCH 05/24] Make CoreStepsHelper use AllureLifecycle's context --- Allure.Net.Commons/AllureLifecycle.cs | 2 +- Allure.Net.Commons/Steps/CoreStepsHelper.cs | 26 ++++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 7c7d260d..398dfe7c 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -173,7 +173,7 @@ public virtual AllureLifecycle StartBeforeFixture(string uuid, FixtureResult res public virtual AllureLifecycle StartBeforeFixture(string parentUuid, FixtureResult result, out string uuid) { - uuid = Guid.NewGuid().ToString("N"); + uuid = CreateUuid(); StartBeforeFixture(parentUuid, uuid, result); return this; } diff --git a/Allure.Net.Commons/Steps/CoreStepsHelper.cs b/Allure.Net.Commons/Steps/CoreStepsHelper.cs index 7f124ae2..0b1e14a4 100644 --- a/Allure.Net.Commons/Steps/CoreStepsHelper.cs +++ b/Allure.Net.Commons/Steps/CoreStepsHelper.cs @@ -16,7 +16,7 @@ public static ITestResultAccessor TestResultAccessor get => TestResultAccessorAsyncLocal.Value; set => TestResultAccessorAsyncLocal.Value = value; } - + #region Fixtures public static string StartBeforeFixture(string name) @@ -28,7 +28,11 @@ public static string StartBeforeFixture(string name) start = DateTimeOffset.Now.ToUnixTimeMilliseconds() }; - AllureLifecycle.Instance.StartBeforeFixture(TestResultAccessor.TestResultContainer.uuid, fixtureResult, out var uuid); + AllureLifecycle.Instance.StartBeforeFixture( + AllureLifecycle.Instance.Context.CurrentContainer.uuid, + fixtureResult, + out var uuid + ); StepLogger?.BeforeStarted?.Log(name); return uuid; } @@ -42,7 +46,11 @@ public static string StartAfterFixture(string name) start = DateTimeOffset.Now.ToUnixTimeMilliseconds() }; - AllureLifecycle.Instance.StartAfterFixture(TestResultAccessor.TestResultContainer.uuid, fixtureResult, out var uuid); + AllureLifecycle.Instance.StartAfterFixture( + AllureLifecycle.Instance.Context.CurrentContainer.uuid, + fixtureResult, + out var uuid + ); StepLogger?.AfterStarted?.Log(name); return uuid; } @@ -59,9 +67,12 @@ public static void StopFixture(Action updateResults = null) public static void StopFixtureSuppressTestCase(Action updateResults = null) { - var newTestResult = TestResultAccessor.TestResult; + var newTestResult = AllureLifecycle.Instance.Context.CurrentTest; StopFixture(updateResults); - AllureLifecycle.Instance.StartTestCase(TestResultAccessor.TestResultContainer.uuid, newTestResult); + AllureLifecycle.Instance.StartTestCase( + AllureLifecycle.Instance.Context.CurrentContainer.uuid, + newTestResult + ); } #endregion @@ -152,7 +163,10 @@ public static void BrokeStep(string uuid, Action updateResults = nul public static void UpdateTestResult(Action update) { - AllureLifecycle.Instance.UpdateTestCase(TestResultAccessor.TestResult.uuid, update); + AllureLifecycle.Instance.UpdateTestCase( + AllureLifecycle.Instance.Context.CurrentTest.uuid, + update + ); } #endregion From a08046dc16ee98c4957b6c1d4a25f980a22b880a Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:37:19 +0700 Subject: [PATCH 06/24] Make lifecycle thread safe. Fix multithreading issues in lifecycle tests --- .../AllureLifeCycleTest.cs | 38 ++-- Allure.Net.Commons.Tests/ConcurrencyTests.cs | 47 ++++- .../InMemoryResultsWriter.cs | 25 ++- Allure.Net.Commons/AllureLifecycle.cs | 187 +++++++++++++----- Allure.Net.Commons/Storage/AllureStorage.cs | 122 ++++++------ 5 files changed, 271 insertions(+), 148 deletions(-) diff --git a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs index 085268f5..05c4a461 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -56,33 +56,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 => { @@ -97,16 +97,16 @@ 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(); }); } diff --git a/Allure.Net.Commons.Tests/ConcurrencyTests.cs b/Allure.Net.Commons.Tests/ConcurrencyTests.cs index 0809edd2..f014eb20 100644 --- a/Allure.Net.Commons.Tests/ConcurrencyTests.cs +++ b/Allure.Net.Commons.Tests/ConcurrencyTests.cs @@ -13,6 +13,7 @@ internal class ConcurrencyTests { InMemoryResultsWriter writer; AllureLifecycle lifecycle; + int writes = 0; [SetUp] public void SetUp() @@ -83,17 +84,21 @@ public void ContextCapturedBySubThreads() * - inner-2-1 | Child thread 2 * - inner-2-2 | Child thread 2 */ + var sync = new ManualResetEventSlim(); + this.WrapInTest( "test", () => this.WrapInStep( "outer", () => RunThreads( - () => this.AddSteps( - ("inner-1", new object[] { "inner-1-1", "inner-1-2" }) - ), - () => this.AddSteps( - ("inner-2", new object[] { "inner-2-1", "inner-2-2" }) - ) + 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) ) ) ); @@ -146,16 +151,38 @@ await this.WrapInTestAsync( ); } + 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) { - var uuid = Guid.NewGuid().ToString(); this.lifecycle - .StartTestCase(new() { name = name, uuid = uuid }); + .StartTestCase(new() + { + name = name, + uuid = Guid.NewGuid().ToString() + }); await Task.Delay(1); await this.AddStepsAsync(steps); this.lifecycle - .StopTestCase(uuid) - .WriteTestCase(uuid); + .StopTestCase() + .WriteTestCase(); + writes++; } void WrapInTest(string testName, Action action) diff --git a/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs b/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs index ab619d1e..29cd9223 100644 --- a/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs +++ b/Allure.Net.Commons.Tests/InMemoryResultsWriter.cs @@ -5,30 +5,43 @@ 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() { - this.testResults.Clear(); - this.testContainers.Clear(); - this.attachments.Clear(); + lock (this.monitor) + { + this.testResults.Clear(); + this.testContainers.Clear(); + this.attachments.Clear(); + } } public void Write(TestResult testResult) { - this.testResults.Add(testResult); + lock (this.monitor) + { + this.testResults.Add(testResult); + } } public void Write(TestResultContainer testResult) { - this.testContainers.Add(testResult); + lock (this.monitor) + { + this.testContainers.Add(testResult); + } } public void Write(string source, byte[] attachment) { - this.attachments.Add((source, attachment)); + lock (this.monitor) + { + this.attachments.Add((source, attachment)); + } } } } diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 398dfe7c..8c913cdc 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -28,11 +28,18 @@ public class AllureLifecycle private readonly AllureStorage storage; private readonly IAllureResultsWriter writer; + /// + /// Protects mutations of shared allure model objects against data + /// races that may otherwise occur because of multithreaded access to + /// the AllureLifecycle's singleton. + /// + readonly object monitor = new(); + /// /// Gets or sets an execution context of Allure. Use this property if - /// the context is set not in the same async domain where a - /// test/fixture function is executed. + /// the context is set not in the same async domain where it should + /// later be accessed. /// /// /// This property is intended to be used by Allure integrations with @@ -92,9 +99,14 @@ private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => public virtual AllureLifecycle StartTestContainer(TestResultContainer container) { container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - this.storage.CurrentTestContainerOrNull?.children.Add( - container.uuid - ); + var parent = this.storage.CurrentTestContainerOrNull; + if (parent is not null) + { + lock (this.monitor) + { + parent.children.Add(container.uuid); + } + } storage.PutTestContainer(container); return this; } @@ -108,13 +120,21 @@ public virtual AllureLifecycle StartTestContainer(string parentUuid, TestResultC public virtual AllureLifecycle UpdateTestContainer(Action update) { - update.Invoke(storage.CurrentTestContainer); + var container = this.storage.CurrentTestContainer; + lock (this.monitor) + { + update.Invoke(container); + } return this; } public virtual AllureLifecycle UpdateTestContainer(string uuid, Action update) { - update.Invoke(storage.Get(uuid)); + var container = this.storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(container); + } return this; } @@ -132,17 +152,17 @@ public virtual AllureLifecycle StopTestContainer(string uuid) public virtual AllureLifecycle WriteTestContainer() { - writer.Write( - storage.RemoveTestContainer() - ); + var container = this.storage.CurrentTestContainer; + this.storage.RemoveTestContainer(); + this.writer.Write(container); return this; } public virtual AllureLifecycle WriteTestContainer(string uuid) { - writer.Write( - storage.RemoveTestContainer(uuid) - ); + var container = this.storage.Get(uuid); + this.storage.RemoveTestContainer(uuid); + this.writer.Write(container); return this; } @@ -208,35 +228,49 @@ public virtual AllureLifecycle StartAfterFixture(string parentUuid, string uuid, public virtual AllureLifecycle UpdateFixture(Action update) { - update?.Invoke(storage.CurrentFixture); + var fixture = this.storage.CurrentFixture; + lock (this.monitor) + { + update.Invoke(fixture); + } return this; } public virtual AllureLifecycle UpdateFixture(string uuid, Action update) { - update.Invoke(storage.Get(uuid)); + var fixture = this.storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(fixture); + } return this; } public virtual AllureLifecycle StopFixture(Action beforeStop) { - UpdateFixture(beforeStop); + this.UpdateFixture(beforeStop); return this.StopFixture(); } public virtual AllureLifecycle StopFixture() { - var fixture = this.storage.RemoveFixture(); - fixture.stage = Stage.finished; - fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.UpdateFixture(fixture => + { + fixture.stage = Stage.finished; + fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveFixture(); return this; } public virtual AllureLifecycle StopFixture(string uuid) { - var fixture = this.storage.RemoveFixture(uuid); - fixture.stage = Stage.finished; - fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.UpdateFixture(fixture => + { + fixture.stage = Stage.finished; + fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveFixture(uuid); return this; } @@ -252,7 +286,14 @@ public virtual AllureLifecycle StartTestCase(string containerUuid, TestResult te public virtual AllureLifecycle StartTestCase(TestResult testResult) { - this.storage.CurrentTestContainerOrNull?.children.Add(testResult.uuid); + var container = this.storage.CurrentTestContainerOrNull; + if (container is not null) + { + lock (this.monitor) + { + container.children.Add(testResult.uuid); + } + } testResult.stage = Stage.running; testResult.start = testResult.start == 0L ? DateTimeOffset.Now.ToUnixTimeMilliseconds() @@ -267,7 +308,10 @@ Action update ) { var testResult = this.storage.Get(uuid); - update(testResult); + lock (this.monitor) + { + update(testResult); + } return this; } @@ -275,19 +319,21 @@ public virtual AllureLifecycle UpdateTestCase( Action update ) { - update(this.storage.CurrentTest); + var testResult = this.storage.CurrentTest; + lock (this.monitor) + { + update(testResult); + } return this; } public virtual AllureLifecycle StopTestCase( Action beforeStop - ) + ) => this.UpdateTestCase(testResult => { - var testResult = this.storage.CurrentTest; beforeStop(testResult); stopTestCase(testResult); - return this; - } + }); public virtual AllureLifecycle StopTestCase() => this.UpdateTestCase(stopTestCase); @@ -297,17 +343,17 @@ public virtual AllureLifecycle StopTestCase(string uuid) => public virtual AllureLifecycle WriteTestCase() { - this.writer.Write( - this.storage.RemoveTestCase() - ); + var testResult = this.storage.CurrentTest; + this.storage.RemoveTestCase(); + this.writer.Write(testResult); return this; } public virtual AllureLifecycle WriteTestCase(string uuid) { - this.writer.Write( - this.storage.RemoveTestCase(uuid) - ); + var testResult = this.storage.Get(uuid); + this.storage.RemoveTestCase(uuid); + this.writer.Write(testResult); return this; } @@ -319,7 +365,11 @@ public virtual AllureLifecycle StartStep(StepResult result) { result.stage = Stage.running; result.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - this.storage.CurrentStepContainer.steps.Add(result); + var parent = this.storage.CurrentStepContainer; + lock (this.monitor) + { + parent.steps.Add(result); + } this.storage.PutStep(result); return this; } @@ -348,13 +398,21 @@ StepResult stepResult public virtual AllureLifecycle UpdateStep(Action update) { - update.Invoke(this.storage.CurrentStep); + var stepResult = this.storage.CurrentStep; + lock (this.monitor) + { + update.Invoke(stepResult); + } return this; } public virtual AllureLifecycle UpdateStep(string uuid, Action update) { - update.Invoke(storage.Get(uuid)); + var stepResult = storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(stepResult); + } return this; } @@ -366,17 +424,23 @@ public virtual AllureLifecycle StopStep(Action beforeStop) public virtual AllureLifecycle StopStep(string uuid) { - var step = this.storage.RemoveStep(uuid); - step.stage = Stage.finished; - step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.UpdateStep(uuid, step => + { + step.stage = Stage.finished; + step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveStep(uuid); return this; } public virtual AllureLifecycle StopStep() { - var step = this.storage.RemoveStep(); - step.stage = Stage.finished; - step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.UpdateStep(step => + { + step.stage = Stage.finished; + step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveStep(); return this; } @@ -401,8 +465,12 @@ public virtual AllureLifecycle AddAttachment(string name, string type, byte[] co type = type, source = source }; - writer.Write(source, content); - this.storage.CurrentStepContainer.attachments.Add(attachment); + this.writer.Write(source, content); + var target = this.storage.CurrentStepContainer; + lock (this.monitor) + { + target.attachments.Add(attachment); + } return this; } @@ -433,6 +501,17 @@ public virtual AllureLifecycle AddScreenDiff(string testCaseUuid, string expecte return this; } + 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 @@ -462,16 +541,19 @@ private static JObject GetConfiguration() private void StartFixture(FixtureResult fixtureResult) { - storage.PutFixture(fixtureResult); + this.storage.PutFixture(fixtureResult); fixtureResult.stage = Stage.running; fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); } void StartFixture(string uuid, FixtureResult fixtureResult) { - storage.PutFixture(uuid, fixtureResult); - fixtureResult.stage = Stage.running; - fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + this.storage.PutFixture(uuid, fixtureResult); + lock (this.monitor) + { + fixtureResult.stage = Stage.running; + fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + } } AllureLifecycle StartStep( @@ -482,7 +564,10 @@ StepResult stepResult { stepResult.stage = Stage.running; stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - parent.steps.Add(stepResult); + lock (this.monitor) + { + parent.steps.Add(stepResult); + } this.storage.PutStep(uuid, stepResult); return this; } diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 741ca4e6..a0db2f80 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -71,19 +71,24 @@ public T Remove(string uuid) } public void PutTestContainer(TestResultContainer container) => - this.PutAndUpdateContext( - container.uuid, - container, + this.UpdateContext( c => c.WithContainer(container) ); - public TestResultContainer RemoveTestContainer() => - this.RemoveAndUpdateContext( - this.CurrentTestContainer.uuid, - c => c.WithNoLastContainer() - ); - - public TestResultContainer RemoveTestContainer(string uuid) => + public void PutTestContainer( + string uuid, + TestResultContainer container + ) => this.PutAndUpdateContext( + uuid, + container, + c => c.WithContainer(container) + ); + + public void RemoveTestContainer() => this.UpdateContext( + c => c.WithNoLastContainer() + ); + + public void RemoveTestContainer(string uuid) => this.RemoveAndUpdateContext( uuid, c => ContextWithNoContainer(c, uuid) @@ -99,39 +104,36 @@ public void PutFixture(string uuid, FixtureResult fixture) => c => c.WithFixtureContext(fixture) ); - public FixtureResult RemoveFixture() - { - var fixture = this.CurrentFixture; + public void RemoveFixture() => this.UpdateContext(c => c.WithNoFixtureContext()); - return fixture; - } - public FixtureResult RemoveFixture(string uuid) + public void RemoveFixture(string uuid) => this.RemoveAndUpdateContext( uuid, - c => ReferenceEquals( - c.CurrentFixture, - this.Get(uuid) - ) ? c.WithNoFixtureContext() : c + c => c.WithNoFixtureContext() ); public void PutTestCase(TestResult testResult) => + this.UpdateContext( + c => c.WithTestContext(testResult) + ); + + public void PutTestCase(string uuid, TestResult testResult) => this.PutAndUpdateContext( - testResult.uuid, + uuid, testResult, c => c.WithTestContext(testResult) ); - public TestResult RemoveTestCase() => - this.RemoveAndUpdateContext( - this.CurrentTest.uuid, + public void RemoveTestCase() => + this.UpdateContext( c => c.WithNoTestContext() ); - public TestResult RemoveTestCase(string uuid) => + public void RemoveTestCase(string uuid) => this.RemoveAndUpdateContext( uuid, - c => c.CurrentTest.uuid == uuid ? c.WithNoTestContext() : c + c => c.WithNoTestContext() ); public void PutStep(StepResult stepResult) => @@ -146,34 +148,30 @@ public void PutStep(string uuid, StepResult stepResult) => c => c.WithStep(stepResult) ); - public StepResult RemoveStep() - { - var step = this.CurrentStep; - this.UpdateContext(c => c.WithNoLastStep()); - return step; - } + public void RemoveStep() => this.UpdateContext( + c => c.WithNoLastStep() + ); - public StepResult RemoveStep(string uuid) => + public void RemoveStep(string uuid) => this.RemoveAndUpdateContext( uuid, c => this.ContextWithNoStep(c, uuid) ); - T PutAndUpdateContext( + void PutAndUpdateContext( string uuid, T value, Func updateFn ) where T : notnull { - var result = this.Put(uuid, value); + this.Put(uuid, value); this.UpdateContext(updateFn); - return result; } - T RemoveAndUpdateContext(string uuid, Func updateFn) + void RemoveAndUpdateContext(string uuid, Func updateFn) { this.UpdateContext(updateFn); - return this.Remove(uuid); + this.Remove(uuid); } void UpdateContext(Func updateFn) @@ -181,30 +179,6 @@ void UpdateContext(Func updateFn) this.CurrentContext = updateFn(this.CurrentContext); } - AllureContext ContextWithNoStep(AllureContext context, string uuid) - { - var stepResult = this.Get(uuid); - 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" - ); - } - } - while (stepsToPushAgain.Any()) - { - context = context.WithStep( - stepsToPushAgain.Pop() - ); - } - return context; - } - static AllureContext ContextWithNoContainer( AllureContext context, string uuid @@ -230,5 +204,29 @@ string uuid } return context; } + + AllureContext ContextWithNoStep(AllureContext context, string uuid) + { + var stepResult = this.Get(uuid); + 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" + ); + } + } + while (stepsToPushAgain.Any()) + { + context = context.WithStep( + stepsToPushAgain.Pop() + ); + } + return context; + } } } \ No newline at end of file From 7ffe0bc8e725d0c3fad1a743a5d5ff9b3a409976 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:16:55 +0700 Subject: [PATCH 07/24] Several improvements on context. Allure-xunit migration - Add bool props to test context to context's public API - Fix context string conversion - Migrate allure-xunit's code to the new context implementation --- .../AllureContextTests.cs | 67 +++++++ .../AllureLifeCycleTest.cs | 30 ++- Allure.Net.Commons/AllureLifecycle.cs | 46 ++++- Allure.Net.Commons/Steps/AllureStepAspect.cs | 105 ++++------ Allure.Net.Commons/Steps/CoreStepsHelper.cs | 147 +++++--------- Allure.Net.Commons/Storage/AllureContext.cs | 89 ++++++--- Allure.Net.Commons/Storage/AllureStorage.cs | 45 ++--- Allure.XUnit/AllureAfter.cs | 13 +- Allure.XUnit/AllureBefore.cs | 13 +- Allure.XUnit/AllureMessageSink.cs | 183 +++++++++++------- Allure.XUnit/AllureStep.cs | 13 +- Allure.XUnit/AllureStepBase.cs | 30 +-- Allure.XUnit/AllureXunitHelper.cs | 127 ++++++------ Allure.XUnit/AllureXunitTestData.cs | 10 + Allure.XUnit/AllureXunitTestResultAccessor.cs | 14 -- 15 files changed, 498 insertions(+), 434 deletions(-) create mode 100644 Allure.XUnit/AllureXunitTestData.cs delete mode 100644 Allure.XUnit/AllureXunitTestResultAccessor.cs diff --git a/Allure.Net.Commons.Tests/AllureContextTests.cs b/Allure.Net.Commons.Tests/AllureContextTests.cs index f0087344..668668c5 100644 --- a/Allure.Net.Commons.Tests/AllureContextTests.cs +++ b/Allure.Net.Commons.Tests/AllureContextTests.cs @@ -14,6 +14,10 @@ public void TestEmptyContext() 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.HasFixture, Is.False); + Assert.That(ctx.HasTest, Is.False); + Assert.That(ctx.HasStep, Is.False); Assert.That( () => ctx.CurrentContainer, @@ -54,6 +58,7 @@ public void TestContextOnly() 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)); @@ -115,6 +120,7 @@ public void TestContextCanBeRemoved() .WithTestContext(test) .WithNoTestContext(); + Assert.That(ctx.HasTest, Is.False); Assert.That(ctx.TestContext, Is.Null); Assert.That( () => ctx.CurrentStepContainer, @@ -140,6 +146,7 @@ public void OneContainerInContainerContext() var ctx = new AllureContext().WithContainer(container); + Assert.That(ctx.HasContainer, Is.True); Assert.That(ctx.ContainerContext, Is.EqualTo(new[] { container })); Assert.That(ctx.CurrentContainer, Is.SameAs(container)); } @@ -182,6 +189,7 @@ public void LatestContainerCanBeRemoved() .WithContainer(new()) .WithNoLastContainer(); + Assert.That(ctx.HasContainer, Is.False); Assert.That(ctx.ContainerContext, Is.Empty); } @@ -235,6 +243,7 @@ public void FixtureContextIsSet() .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)); @@ -330,6 +339,7 @@ public void ClearingTestContextClearsFixtureContext() .WithFixtureContext(new()) .WithNoTestContext(); + Assert.That(ctx.HasFixture, Is.False); Assert.That(ctx.FixtureContext, Is.Null); Assert.That( () => ctx.CurrentStepContainer, @@ -360,6 +370,7 @@ public void FixtureContextCanBeCleared() .WithFixtureContext(fixture) .WithNoFixtureContext(); + Assert.That(ctx.HasFixture, Is.False); Assert.That(ctx.FixtureContext, Is.Null); } @@ -411,6 +422,7 @@ public void StepCanBeAddedIfFixtureExists() .WithFixtureContext(new()) .WithStep(step); + Assert.That(ctx.HasStep, Is.True); Assert.That(ctx.StepContext, Is.EqualTo(new[] { step })); Assert.That(ctx.CurrentStepContainer, Is.SameAs(step)); } @@ -423,6 +435,7 @@ public void StepCanBeAddedIfTestExists() .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)); @@ -468,6 +481,7 @@ public void RemovingTheOnlyStepRestoresTestAsStepContainer() .WithStep(new()) .WithNoLastStep(); + Assert.That(ctx.HasStep, Is.False); Assert.That(ctx.StepContext, Is.Empty); Assert.That(ctx.CurrentStepContainer, Is.SameAs(test)); } @@ -495,6 +509,7 @@ public void RemovingFixtureClearsStepContext() .WithStep(new()) .WithNoFixtureContext(); + Assert.That(ctx.HasStep, Is.False); Assert.That(ctx.StepContext, Is.Empty); } @@ -506,6 +521,7 @@ public void RemovingTestClearsStepContext() .WithStep(new()) .WithNoTestContext(); + Assert.That(ctx.HasStep, Is.False); Assert.That(ctx.StepContext, Is.Empty); } @@ -521,7 +537,58 @@ public void FixtureAfterTestClearsStepContext() .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] }") + ); + } } } diff --git a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs index 05c4a461..c0394bad 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -151,7 +151,7 @@ public void BeforeFixtureMayOverlapsWithTest() } [Test] - public async Task AllureContextCouldBeAssigned() + public async Task ContextCapturingTest() { var writer = new InMemoryResultsWriter(); var lifecycle = new AllureLifecycle(_ => writer); @@ -164,12 +164,32 @@ await Task.Factory.StartNew(() => }); context = lifecycle.Context; }); - lifecycle.Context = context; - - lifecycle.StopTestCase(); - lifecycle.WriteTestCase(); + lifecycle.RunInContext(context, () => + { + lifecycle.StopTestCase(); + lifecycle.WriteTestCase(); + }); Assert.That(writer.testResults, Is.Not.Empty); } + + [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/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index 8c913cdc..da1ea844 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -30,16 +30,13 @@ public class AllureLifecycle /// /// Protects mutations of shared allure model objects against data - /// races that may otherwise occur because of multithreaded access to - /// the AllureLifecycle's singleton. + /// races that may otherwise occur because of multithreaded access. /// readonly object monitor = new(); /// - /// Gets or sets an execution context of Allure. Use this property if - /// the context is set not in the same async domain where it should - /// later be accessed. + /// Captures the current context of Allure's execution. /// /// /// This property is intended to be used by Allure integrations with @@ -48,7 +45,44 @@ public class AllureLifecycle public AllureContext Context { get => this.storage.CurrentContext; - set => this.storage.CurrentContext = value; + private set => this.storage.CurrentContext = value; + } + + /// + /// Runs the specified code in the specified context restoring it + /// before returning. Use this method if you need to access the context + /// somewhere outside the async execution context the allure context + /// has been set in. + /// + /// + /// This method is intended to be used by Allure integrations with + /// test frameworks, not by end user's code. + /// + /// + /// A context that was previously captured with . + /// + /// A code to run. + public void RunInContext( + AllureContext? context, + Action action + ) + { + if (context is null || context == this.Context) + { + action(this.Context); + return; + } + + var originalContext = this.Context; + try + { + this.Context = context; + action(context); + } + finally + { + this.Context = originalContext; + } } /// Method to get the key for separation the steps for different tests. 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 0b1e14a4..6a51100e 100644 --- a/Allure.Net.Commons/Steps/CoreStepsHelper.cs +++ b/Allure.Net.Commons/Steps/CoreStepsHelper.cs @@ -19,155 +19,96 @@ public static ITestResultAccessor TestResultAccessor #region Fixtures - public static string StartBeforeFixture(string name) + public static void StartBeforeFixture(string name) { - var fixtureResult = new FixtureResult() - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - - AllureLifecycle.Instance.StartBeforeFixture( - AllureLifecycle.Instance.Context.CurrentContainer.uuid, - fixtureResult, - out var uuid - ); + AllureLifecycle.Instance.StartBeforeFixture(new() { name = name }); StepLogger?.BeforeStarted?.Log(name); - return uuid; } - public static string StartAfterFixture(string name) + public static void StartAfterFixture(string name) { - var fixtureResult = new FixtureResult() - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - - AllureLifecycle.Instance.StartAfterFixture( - AllureLifecycle.Instance.Context.CurrentContainer.uuid, - fixtureResult, - out var uuid - ); + AllureLifecycle.Instance.StartAfterFixture(new() { name = name }); StepLogger?.AfterStarted?.Log(name); - return uuid; } - 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 StopFixture(Action updateResults) => + AllureLifecycle.Instance.StopFixture(updateResults); + + public static void StopFixture() => + AllureLifecycle.Instance.StopFixture(); + + [Obsolete] public static void StopFixtureSuppressTestCase(Action updateResults = null) { var newTestResult = AllureLifecycle.Instance.Context.CurrentTest; StopFixture(updateResults); - AllureLifecycle.Instance.StartTestCase( - AllureLifecycle.Instance.Context.CurrentContainer.uuid, - newTestResult - ); + AllureLifecycle.Instance.StartTestCase(newTestResult); } #endregion #region Steps - public static string StartStep(string name, Action updateResults = null) + public static void StartStep(string name) { - var stepResult = new StepResult - { - name = name, - stage = Stage.running, - start = DateTimeOffset.Now.ToUnixTimeMilliseconds() - }; - updateResults?.Invoke(stepResult); - - AllureLifecycle.Instance.StartStep(stepResult, out var uuid); + AllureLifecycle.Instance.StartStep(new() { name = name }); StepLogger?.StepStarted?.Log(name); - return uuid; } - public static void PassStep(Action updateResults = null) + public static void StartStep(string name, Action updateResults) { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.passed; - updateResults?.Invoke(result); - StepLogger?.StepPassed?.Log(result.name); - }); + StartStep(name); + AllureLifecycle.Instance.UpdateStep(updateResults); } - public static void PassStep(string uuid, Action updateResults = null) - { - AllureLifecycle.Instance.UpdateStep(uuid, result => + public static void PassStep() => AllureLifecycle.Instance.StopStep( + result => { result.status = Status.passed; - updateResults?.Invoke(result); StepLogger?.StepPassed?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); - } + } + ); - public static void FailStep(Action updateResults = null) + public static void PassStep(Action updateResults) { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.failed; - updateResults?.Invoke(result); - StepLogger?.StepFailed?.Log(result.name); - }); + AllureLifecycle.Instance.UpdateStep(updateResults); + PassStep(); } - public static void FailStep(string uuid, Action updateResults = null) - { - AllureLifecycle.Instance.UpdateStep(uuid, result => + public static void FailStep() => AllureLifecycle.Instance.StopStep( + result => { result.status = Status.failed; - updateResults?.Invoke(result); StepLogger?.StepFailed?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); - } - - public static void BrokeStep(Action updateResults = null) + } + ); + + public static void FailStep(Action updateResults) { - AllureLifecycle.Instance.StopStep(result => - { - result.status = Status.broken; - updateResults?.Invoke(result); - StepLogger?.StepBroken?.Log(result.name); - }); + AllureLifecycle.Instance.UpdateStep(updateResults); + FailStep(); } - - public static void BrokeStep(string uuid, Action updateResults = null) - { - AllureLifecycle.Instance.UpdateStep(uuid, result => + + public static void BrokeStep() => AllureLifecycle.Instance.StopStep( + result => { result.status = Status.broken; - updateResults?.Invoke(result); StepLogger?.StepBroken?.Log(result.name); - }); - AllureLifecycle.Instance.StopStep(uuid); + } + ); + + public static void BrokeStep(Action updateResults) + { + AllureLifecycle.Instance.UpdateStep(updateResults); + BrokeStep(); } #endregion #region Misc - public static void UpdateTestResult(Action update) - { - AllureLifecycle.Instance.UpdateTestCase( - AllureLifecycle.Instance.Context.CurrentTest.uuid, - update - ); - } + public static void UpdateTestResult(Action update) => + AllureLifecycle.Instance.UpdateTestCase(update); #endregion diff --git a/Allure.Net.Commons/Storage/AllureContext.cs b/Allure.Net.Commons/Storage/AllureContext.cs index a6b0c780..54157cfc 100644 --- a/Allure.Net.Commons/Storage/AllureContext.cs +++ b/Allure.Net.Commons/Storage/AllureContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Text; #nullable enable @@ -19,6 +20,26 @@ namespace Allure.Net.Commons.Storage /// public record class AllureContext { + /// + /// Returns true if a container context is active. + /// + public bool HasContainer => !this.ContainerContext.IsEmpty; + + /// + /// 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; + /// /// A stack of fixture containers affecting subsequent tests. /// @@ -93,12 +114,10 @@ internal TestResultContainer CurrentContainer /// context isn't active. /// /// - internal FixtureResult CurrentFixture - { - get => this.FixtureContext ?? throw new InvalidOperationException( + internal FixtureResult CurrentFixture => + this.FixtureContext ?? throw new InvalidOperationException( "No fixture context is active." ); - } /// /// A test that is being executed. @@ -108,12 +127,10 @@ internal FixtureResult CurrentFixture /// isn't active. /// /// - internal TestResult CurrentTest - { - get => this.TestContext ?? throw new InvalidOperationException( + internal TestResult CurrentTest => + this.TestContext ?? throw new InvalidOperationException( "No test context is active." ); - } /// /// A step that is being executed. @@ -123,13 +140,11 @@ internal TestResult CurrentTest /// isn't active. /// /// - internal StepResult CurrentStep - { - get => this.StepContext.FirstOrDefault() + internal StepResult CurrentStep => + this.StepContext.FirstOrDefault() ?? throw new InvalidOperationException( "No step context is active." ); - } /// /// A step container a next step should be put in. @@ -140,13 +155,26 @@ internal StepResult CurrentStep /// fixture, nor test, nor step context is active. /// /// - internal ExecutableItem CurrentStepContainer - { - get => this.StepContext.FirstOrDefault() as ExecutableItem + internal ExecutableItem CurrentStepContainer => + this.StepContext.FirstOrDefault() as ExecutableItem ?? this.RootStepContainer ?? throw new InvalidOperationException( "No fixture, test, or step context is active." ); + + protected virtual bool PrintMembers(StringBuilder stringBuilder) + { + var containers = + RepresentStack(this.ContainerContext, c => c.name); + var fixture = this.FixtureContext?.name ?? "null"; + var test = this.TestContext?.name ?? "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; } /// @@ -309,11 +337,12 @@ this with internal AllureContext WithNoLastStep() => this with { - StepContext = this.StepContext.IsEmpty - ? throw new InvalidOperationException( + StepContext = this.HasStep + ? this.StepContext.Pop() + : throw new InvalidOperationException( "Unable to deactivate a step context because it's " + "already inactive." - ) : this.StepContext.Pop() + ) }; AllureContext ValidateContainerContextCanBeModified() @@ -339,7 +368,7 @@ AllureContext ValidateContainerContextCanBeModified() AllureContext ValidateContainerCanBeRemoved() { - if (this.ContainerContext.IsEmpty) + if (!this.HasContainer) { throw new InvalidOperationException( "Unable to deactivate a container context because it's " + @@ -357,7 +386,7 @@ AllureContext ValidateContainerCanBeRemoved() FixtureResult ValidateNewFixtureContext(FixtureResult fixture) { - if (this.ContainerContext.IsEmpty) + if (!this.HasContainer) { throw new InvalidOperationException( "Unable to activate a fixture context " + @@ -365,7 +394,7 @@ FixtureResult ValidateNewFixtureContext(FixtureResult fixture) ); } - if (this.FixtureContext is not null) + if (this.HasFixture) { throw new InvalidOperationException( "Unable to activate a fixture context " + @@ -378,7 +407,7 @@ FixtureResult ValidateNewFixtureContext(FixtureResult fixture) TestResult ValidateNewTestContext(TestResult testResult) { - if (this.FixtureContext is not null) + if (this.HasFixture) { throw new InvalidOperationException( "Unable to activate a test context " + @@ -386,7 +415,7 @@ TestResult ValidateNewTestContext(TestResult testResult) ); } - if (this.TestContext is not null) + if (this.HasTest) { throw new InvalidOperationException( "Unable to activate a test context " + @@ -399,15 +428,23 @@ TestResult ValidateNewTestContext(TestResult testResult) StepResult ValidateNewStep(StepResult stepResult) { - if (this.RootStepContainer is null) + if (!this.HasTest && !this.HasFixture) { throw new InvalidOperationException( - "Unable to activate a step context because neither test, " + - "nor fixture context is active." + "Unable to activate a 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/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index a0db2f80..042d824f 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -16,7 +16,8 @@ internal class AllureStorage public AllureContext CurrentContext { get => this.context.Value ??= new(); - set => this.context.Value = value; + set => this.context.Value = value + ?? throw new ArgumentNullException(nameof(CurrentContext)); } public AllureStorage() @@ -24,35 +25,23 @@ public AllureStorage() this.CurrentContext = new(); } - public TestResultContainer? CurrentTestContainerOrNull - { - get => this.CurrentContext.ContainerContext.FirstOrDefault(); - } + public TestResultContainer? CurrentTestContainerOrNull => + this.CurrentContext.ContainerContext.FirstOrDefault(); - public TestResultContainer CurrentTestContainer - { - get => this.CurrentContext.CurrentContainer; - } + public TestResultContainer CurrentTestContainer => + this.CurrentContext.CurrentContainer; - public FixtureResult CurrentFixture - { - get => this.CurrentContext.CurrentFixture; - } - - public TestResult CurrentTest - { - get => this.CurrentContext.CurrentTest; - } - - public ExecutableItem CurrentStepContainer - { - get => this.CurrentContext.CurrentStepContainer; - } - - public StepResult CurrentStep - { - get => this.CurrentContext.CurrentStep; - } + public FixtureResult CurrentFixture => + this.CurrentContext.CurrentFixture; + + public TestResult CurrentTest => + this.CurrentContext.CurrentTest; + + public ExecutableItem CurrentStepContainer => + this.CurrentContext.CurrentStepContainer; + + public StepResult CurrentStep => + this.CurrentContext.CurrentStep; public T Get(string uuid) { diff --git a/Allure.XUnit/AllureAfter.cs b/Allure.XUnit/AllureAfter.cs index 4162fb6b..66690605 100644 --- a/Allure.XUnit/AllureAfter.cs +++ b/Allure.XUnit/AllureAfter.cs @@ -6,18 +6,9 @@ namespace Allure.Xunit public sealed class AllureAfter : AllureStepBase { [Obsolete("Use AllureAfterAttribute")] - public AllureAfter(string name) : base(Init(name)) + public AllureAfter(string name) { - } - - /// - /// Starts After fixture and return it's UUID - /// - /// The name of created fixture - /// string: UUID - private static string Init(string name) - { - return CoreStepsHelper.StartAfterFixture(name); + CoreStepsHelper.StartAfterFixture(name); } } } \ No newline at end of file diff --git a/Allure.XUnit/AllureBefore.cs b/Allure.XUnit/AllureBefore.cs index caafb68c..72b75904 100644 --- a/Allure.XUnit/AllureBefore.cs +++ b/Allure.XUnit/AllureBefore.cs @@ -6,18 +6,9 @@ namespace Allure.Xunit public sealed class AllureBefore : AllureStepBase { [Obsolete("Use AllureBeforeAttribute")] - public AllureBefore(string name) : base(Init(name)) + public AllureBefore(string name) { - } - - /// - /// Starts Before fixture and return it's UUID - /// - /// The name of created fixture - /// string: UUID - private static string Init(string name) - { - return CoreStepsHelper.StartBeforeFixture(name); + CoreStepsHelper.StartBeforeFixture(name); } } } \ No newline at end of file diff --git a/Allure.XUnit/AllureMessageSink.cs b/Allure.XUnit/AllureMessageSink.cs index 248f2e7d..879178ff 100644 --- a/Allure.XUnit/AllureMessageSink.cs +++ b/Allure.XUnit/AllureMessageSink.cs @@ -1,9 +1,10 @@ -using Allure.Net.Commons; -using Allure.Net.Commons.Steps; -using Allure.Xunit; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Allure.Net.Commons; +using Allure.Net.Commons.Storage; +using Allure.Xunit; using Xunit; using Xunit.Abstractions; @@ -11,8 +12,9 @@ namespace Allure.XUnit { public class AllureMessageSink : TestMessageSink { - IRunnerLogger logger; - Dictionary allureTestData = new(); + readonly IRunnerLogger logger; + readonly ConcurrentDictionary allureTestData + = new(); public AllureMessageSink(IRunnerLogger logger) { @@ -26,8 +28,36 @@ public AllureMessageSink(IRunnerLogger logger) this.OnTestClassConstructionFinished; this.Execution.TestFailedEvent += this.OnTestFailed; this.Execution.TestPassedEvent += this.OnTestPassed; + this.Execution.TestSkippedEvent += this.OnTestSkipped; this.Execution.TestFinishedEvent += this.OnTestFinished; - this.Execution.TestCaseFinishedEvent+= this.OnTestCaseFinished; + } + + public override bool OnMessageWithTypes( + IMessageSinkMessage message, + HashSet messageTypes + ) + { + try + { + this.logger.LogMessage(message.GetType().Name); + return base.OnMessageWithTypes(message, messageTypes); + } + catch (Exception e) + { + if (message is ITestCaseMessage testCaseMessage) + { + this.logger.LogError( + "Error during execution of {0}: {1}", + testCaseMessage.TestCase.DisplayName, + e + ); + } + else + { + this.logger.LogError(e.ToString()); + } + return false; + } } void OnTestAssemblyExecutionStarting( @@ -37,27 +67,22 @@ MessageHandlerArgs args args.Message.ExecutionOptions.SetSynchronousMessageReporting(true); } - internal void OnTestArgumentsCreated(ITest test, object[] arguments) - { - var accessor = this.GetOrCreateAllureResultAccessor(test); - accessor.Arguments = arguments; - } + internal void OnTestArgumentsCreated(ITest test, object[] arguments) => + this.GetOrCreateTestData(test).Arguments = arguments; void OnTestStarting(MessageHandlerArgs args) { var message = args.Message; var test = message.Test; - var accessor = this.GetOrCreateAllureResultAccessor(test); - - CoreStepsHelper.TestResultAccessor = accessor; - if (message.TestMethod.Method.IsStatic) + if (IsStaticTestMethod(message)) { - accessor.TestResult = AllureXunitHelper.StartStaticAllureTestCase(test); + AllureXunitHelper.StartStaticAllureTestCase(test); + this.CaptureTestContext(test); } else { - accessor.TestResultContainer = AllureXunitHelper.StartNewAllureContainer( + AllureXunitHelper.StartNewAllureContainer( message.TestClass.Class.Name ); } @@ -66,90 +91,77 @@ void OnTestStarting(MessageHandlerArgs args) void OnTestClassConstructionFinished( MessageHandlerArgs args ) - { - var test = args.Message.Test; - var accessor = this.allureTestData[test]; - var container = accessor.TestResultContainer; - if (accessor.TestResult is null && container is not null) - { - accessor.TestResult = AllureXunitHelper.StartAllureTestCase( - test, - container - ); - } - } - - void OnTestFailed(MessageHandlerArgs args) { var message = args.Message; var test = message.Test; - var testResult = this.allureTestData[test].TestResult; - - if (testResult is not null) + if (!IsStaticTestMethod(message)) { - AllureXunitHelper.ApplyTestFailure(testResult, message); + AllureXunitHelper.StartAllureTestCase(test); + this.CaptureTestContext(test); } } - void OnTestPassed(MessageHandlerArgs args) + void OnTestFailed(MessageHandlerArgs args) => + this.RunInTestContext( + args.Message.Test, + _ => AllureXunitHelper.ApplyTestFailure(args.Message) + ); + + void OnTestPassed(MessageHandlerArgs args) => + this.RunInTestContext( + args.Message.Test, + _ => AllureXunitHelper.ApplyTestSuccess(args.Message) + ); + + void OnTestSkipped(MessageHandlerArgs args) { var message = args.Message; var test = message.Test; - var testResult = this.allureTestData[test].TestResult; - - if (testResult is not null) + this.UpdateTestContext(test, ctx => { - AllureXunitHelper.ApplyTestSuccess(testResult, message); - } + if (!ctx.HasTest) + { + AllureXunitHelper.StartAllureTestCase(test); + } + AllureXunitHelper.ApplyTestSkip(message); + }); } void OnTestFinished(MessageHandlerArgs args) { + var message = args.Message; var test = args.Message.Test; - var accessor = this.allureTestData[test]; - this.allureTestData.Remove(test); - var testResult = accessor.TestResult; - if (testResult is not null) - { - this.AddAllureParameters(testResult, test, accessor.Arguments); - AllureXunitHelper.ReportTestCase(testResult); + var arguments = this.allureTestData[test].Arguments; - var container = accessor.TestResultContainer; - if (container is not null) + this.RunInTestContext(test, _ => + { + this.AddAllureParameters(test, arguments); + AllureXunitHelper.ReportCurrentTestCase(); + if (!IsStaticTestMethod(message)) { - AllureXunitHelper.ReportTestContainer(container); + AllureXunitHelper.ReportCurrentTestContainer(); } - } - } + }); - void OnTestCaseFinished(MessageHandlerArgs args) - { - var testCase = args.Message.TestCase; - if (testCase.SkipReason != null) - { - AllureXunitHelper.ReportSkippedTestCase(testCase); - } + this.allureTestData.Remove(test, out _); } - AllureXunitTestResultAccessor GetOrCreateAllureResultAccessor(ITest test) + AllureXunitTestData GetOrCreateTestData(ITest test) { - if (!this.allureTestData.TryGetValue(test, out var accessor)) + if (!this.allureTestData.TryGetValue(test, out var data)) { - accessor = new AllureXunitTestResultAccessor(); - this.allureTestData[test] = accessor; + data = new AllureXunitTestData(); + this.allureTestData[test] = data; } - return accessor; + return data; } - void AddAllureParameters( - TestResult testResult, - ITest test, - object[] arguments - ) + void AddAllureParameters(ITest test, object[] arguments) { var testCase = test.TestCase; var parameters = testCase.TestMethod.Method.GetParameters(); - arguments ??= testCase.TestMethodArguments ?? Array.Empty(); + arguments ??= testCase.TestMethodArguments + ?? Array.Empty(); if (parameters.Any() && !arguments.Any()) { @@ -157,10 +169,34 @@ object[] arguments } else { - AllureXunitHelper.ApplyTestParameters(testResult, parameters, arguments); + AllureXunitHelper.ApplyTestParameters(parameters, arguments); } } + AllureContext GetTestContext(ITest test) => + this.GetOrCreateTestData(test).Context + ?? AllureLifecycle.Instance.Context; + + void CaptureTestContext(ITest test) => + this.GetOrCreateTestData(test).Context = + AllureLifecycle.Instance.Context; + + void RunInTestContext(ITest test, Action action) => + AllureLifecycle.Instance.RunInContext( + this.GetOrCreateTestData(test).Context, + action + ); + + void UpdateTestContext(ITest test, Action action) => + this.RunInTestContext( + test, + ctx => + { + action(ctx); + this.CaptureTestContext(test); + } + ); + void LogUnreportedTheoryArgs(string testName) { var message = $"Unable to attach arguments of {testName} to " + @@ -171,5 +207,8 @@ void LogUnreportedTheoryArgs(string testName) #endif this.logger.LogWarning(message); } + + static bool IsStaticTestMethod(ITestMethodMessage message) => + message.TestMethod.Method.IsStatic; } } \ No newline at end of file diff --git a/Allure.XUnit/AllureStep.cs b/Allure.XUnit/AllureStep.cs index 6a75a427..828997fd 100644 --- a/Allure.XUnit/AllureStep.cs +++ b/Allure.XUnit/AllureStep.cs @@ -6,18 +6,9 @@ namespace Allure.Xunit public sealed class AllureStep : AllureStepBase { [Obsolete("Use AllureStepAttribute")] - public AllureStep(string name) : base(Init(name)) + public AllureStep(string name) { - } - - /// - /// Creates a new step and return it's UUID - /// - /// The name of created step - /// string: UUID - private static string Init(string name) - { - return CoreStepsHelper.StartStep(name); + CoreStepsHelper.StartStep(name); } } } \ No newline at end of file diff --git a/Allure.XUnit/AllureStepBase.cs b/Allure.XUnit/AllureStepBase.cs index e964f6f5..169547f6 100644 --- a/Allure.XUnit/AllureStepBase.cs +++ b/Allure.XUnit/AllureStepBase.cs @@ -12,12 +12,7 @@ namespace Allure.Xunit { public abstract class AllureStepBase : IDisposable where T : AllureStepBase { - protected AllureStepBase(string uuid) - { - UUID = uuid; - } - - private string UUID { get; } + protected AllureStepBase() { } public void Dispose() { @@ -28,37 +23,24 @@ public void Dispose() #endif if (failed) { - if (this is AllureBefore || this is AllureAfter) - { - CoreStepsHelper.StopFixtureSuppressTestCase(result => result.status = Status.failed); - } - else - { - CoreStepsHelper.FailStep(UUID); - } + CoreStepsHelper.FailStep(); } else { - if (this is AllureBefore || this is AllureAfter) - { - CoreStepsHelper.StopFixtureSuppressTestCase(result => result.status = Status.passed); - } - else - { - CoreStepsHelper.PassStep(UUID); - } + CoreStepsHelper.PassStep(); } } [Obsolete("For named parameters use NameAttribute; For skipped parameters use SkipAttribute")] public T SetParameter(string name, object value) { - AllureLifecycle.Instance.UpdateStep(UUID, + AllureLifecycle.Instance.UpdateStep( result => { result.parameters ??= new List(); result.parameters.Add(new Parameter { name = name, value = value?.ToString() }); - }); + } + ); return (T) this; } diff --git a/Allure.XUnit/AllureXunitHelper.cs b/Allure.XUnit/AllureXunitHelper.cs index 8148bfd2..d18085ce 100644 --- a/Allure.XUnit/AllureXunitHelper.cs +++ b/Allure.XUnit/AllureXunitHelper.cs @@ -45,13 +45,6 @@ static AllureXunitHelper() ); } - internal static TestResult StartStaticAllureTestCase(ITest test) - { - var testResult = CreateTestResultByTest(test); - AllureLifecycle.Instance.StartTestCase(testResult); - return testResult; - } - internal static TestResultContainer StartNewAllureContainer( string className ) @@ -65,63 +58,93 @@ string className return container; } - internal static TestResult StartAllureTestCase( - ITest test, - TestResultContainer container - ) + internal static TestResult StartStaticAllureTestCase(ITest test) { var testResult = CreateTestResultByTest(test); - AllureLifecycle.Instance.StartTestCase(container.uuid, testResult); + AllureLifecycle.Instance.StartTestCase(testResult); return testResult; } - internal static void ApplyTestFailure( - TestResult testResult, - IFailureInformation failure - ) + internal static TestResult StartAllureTestCase(ITest test) { - var statusDetails = testResult.statusDetails ??= new(); - statusDetails.trace = string.Join('\n', failure.StackTraces); - statusDetails.message = string.Join('\n', failure.Messages); + var testResult = CreateTestResultByTest(test); + AllureLifecycle.Instance.StartTestCase(testResult); + return testResult; + } - testResult.status = failure.ExceptionTypes.Any( + internal static void ApplyTestFailure(IFailureInformation failure) + { + var trace = string.Join('\n', failure.StackTraces); + var message = string.Join('\n', failure.Messages); + var status = failure.ExceptionTypes.Any( exceptionType => !exceptionType.StartsWith("Xunit.Sdk.") ) ? Status.broken : Status.failed; + + AllureLifecycle.Instance.UpdateTestCase(testResult => + { + var statusDetails = testResult.statusDetails ??= new(); + statusDetails.trace = trace; + statusDetails.message = message; + testResult.status = status; + }); } - internal static void ApplyTestSuccess( - TestResult testResult, - ITestResultMessage message - ) + internal static void ApplyTestSuccess(ITestResultMessage success) { - var statusDetails = testResult.statusDetails ??= new(); - statusDetails.message = message.Output; - testResult.status = Status.passed; + var message = success.Output; + var status = Status.passed; + + AllureLifecycle.Instance.UpdateTestCase(testResult => + { + var statusDetails = testResult.statusDetails ??= new(); + statusDetails.message = message; + testResult.status = status; + }); + } + + internal static void ApplyTestSkip(ITestSkipped skip) + { + var message = skip.Reason; + var status = Status.skipped; + + AllureLifecycle.Instance.UpdateTestCase(testResult => + { + var statusDetails = testResult.statusDetails ??= new(); + statusDetails.message = message; + testResult.status = status; + }); } internal static void ApplyTestParameters( - TestResult testResult, IEnumerable parameters, object[] arguments - ) => testResult.parameters = parameters.Zip( - arguments, - (param, value) => new Parameter + ) + { + var parametersList = parameters.Zip( + arguments, + (param, value) => new Parameter + { + name = param.Name, + value = value?.ToString() ?? "null" + } + ).ToList(); + + AllureLifecycle.Instance.UpdateTestCase(testResult => { - name = param.Name, - value = value?.ToString() ?? "null" - } - ).ToList(); + testResult.parameters = parametersList; + }); + } - internal static void ReportTestCase(TestResult testResult) + internal static void ReportCurrentTestCase() { - AllureLifecycle.Instance.StopTestCase(testResult.uuid); - AllureLifecycle.Instance.WriteTestCase(testResult.uuid); + AllureLifecycle.Instance.StopTestCase(); + AllureLifecycle.Instance.WriteTestCase(); } - internal static void ReportTestContainer(TestResultContainer container) + internal static void ReportCurrentTestContainer() { - AllureLifecycle.Instance.StopTestContainer(container.uuid); - AllureLifecycle.Instance.WriteTestContainer(container.uuid); + AllureLifecycle.Instance.StopTestContainer(); + AllureLifecycle.Instance.WriteTestContainer(); } internal static void ReportSkippedTestCase(ITestCase testCase) @@ -129,7 +152,7 @@ internal static void ReportSkippedTestCase(ITestCase testCase) var testResult = CreateTestResultByTestCase(testCase); ApplyTestSkip(testResult, testCase.SkipReason); AllureLifecycle.Instance.StartTestCase(testResult); - ReportTestCase(testResult); + ReportCurrentTestCase(); } static TestResult CreateTestResultByTest(ITest test) => @@ -389,39 +412,35 @@ public static void StartTestCase(ITestCaseMessage testCaseMessage) } testResults.TestResult = CreateTestResultByTestCase(testCase); + AllureLifecycle.Instance.StartTestCase(testResults.TestResult); ApplyTestParameters( - testResults.TestResult, testCase.TestMethod.Method.GetParameters(), testCase.TestMethodArguments ); - AllureLifecycle.Instance.StartTestCase( - testResults.TestResultContainer.uuid, - testResults.TestResult - ); } [Obsolete(OBS_MSG_UNINTENDED_PUBLIC)] [EditorBrowsable(EditorBrowsableState.Never)] public static void MarkTestCaseAsFailedOrBroken(ITestFailed testFailed) { - if (testFailed.TestCase is not ITestResultAccessor testResults) + if (testFailed.TestCase is not ITestResultAccessor) { return; } - ApplyTestFailure(testResults.TestResult, testFailed); + ApplyTestFailure(testFailed); } [Obsolete(OBS_MSG_UNINTENDED_PUBLIC)] [EditorBrowsable(EditorBrowsableState.Never)] public static void MarkTestCaseAsPassed(ITestPassed testPassed) { - if (testPassed.TestCase is not ITestResultAccessor testResults) + if (testPassed.TestCase is not ITestResultAccessor) { return; } - ApplyTestSuccess(testResults.TestResult, testPassed); + ApplyTestSuccess(testPassed); } [Obsolete(OBS_MSG_UNINTENDED_PUBLIC)] @@ -444,13 +463,13 @@ ITestCaseMessage testCaseMessage public static void FinishTestCase(ITestCaseMessage testCaseMessage) { var testCase = testCaseMessage.TestCase; - if (testCase is not ITestResultAccessor testResults) + if (testCase is not ITestResultAccessor) { return; } - ReportTestCase(testResults.TestResult); - ReportTestContainer(testResults.TestResultContainer); + ReportCurrentTestCase(); + ReportCurrentTestContainer(); } #endregion } diff --git a/Allure.XUnit/AllureXunitTestData.cs b/Allure.XUnit/AllureXunitTestData.cs new file mode 100644 index 00000000..94f96073 --- /dev/null +++ b/Allure.XUnit/AllureXunitTestData.cs @@ -0,0 +1,10 @@ +using Allure.Net.Commons.Storage; + +namespace Allure.XUnit +{ + class AllureXunitTestData + { + public AllureContext Context { get; set; } + public object[] Arguments { get; set; } + } +} \ No newline at end of file diff --git a/Allure.XUnit/AllureXunitTestResultAccessor.cs b/Allure.XUnit/AllureXunitTestResultAccessor.cs deleted file mode 100644 index 760b9ea6..00000000 --- a/Allure.XUnit/AllureXunitTestResultAccessor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Allure.Net.Commons; -using Allure.Net.Commons.Storage; - -namespace Allure.XUnit -{ - class AllureXunitTestResultAccessor : ITestResultAccessor - { - public TestResultContainer TestResultContainer { get; set; } - - public TestResult TestResult { get; set; } - - public object[] Arguments { get; set; } - } -} \ No newline at end of file From b89cb8bf6b57f7c54944ef2d30f340b0b7007dee Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:26:33 +0700 Subject: [PATCH 08/24] Bump C# language version to 11 for all projects --- Allure.Features/Allure.Features.csproj | 1 + Allure.NUnit.Examples/Allure.NUnit.Examples.csproj | 1 + Allure.NUnit/Allure.NUnit.csproj | 1 + Allure.Net.Commons.Tests/Allure.Net.Commons.Tests.csproj | 2 +- Allure.Net.Commons/Allure.Net.Commons.csproj | 1 + .../Allure.SpecFlowPlugin.Tests.csproj | 1 + Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj | 2 +- Allure.XUnit.Examples/Allure.XUnit.Examples.csproj | 1 + Allure.XUnit/Allure.XUnit.csproj | 6 +++--- 9 files changed, 11 insertions(+), 5 deletions(-) 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.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/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.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/Allure.Net.Commons.csproj b/Allure.Net.Commons/Allure.Net.Commons.csproj index 24d93e9d..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 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/Allure.SpecFlowPlugin.csproj b/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj index e2a3c73d..019ff73d 100644 --- a/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj +++ b/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj @@ -2,6 +2,7 @@ net462;netstandard2.0 + 11 Allure.SpecFlow 2.10-SNAPSHOT Alexander Bakanov @@ -14,7 +15,6 @@ https://github.com/allure-framework/allure-csharp specflow allure false - 8 true true snupkg diff --git a/Allure.XUnit.Examples/Allure.XUnit.Examples.csproj b/Allure.XUnit.Examples/Allure.XUnit.Examples.csproj index b021672b..13cf3e3a 100644 --- a/Allure.XUnit.Examples/Allure.XUnit.Examples.csproj +++ b/Allure.XUnit.Examples/Allure.XUnit.Examples.csproj @@ -3,6 +3,7 @@ false net6.0 + 11 diff --git a/Allure.XUnit/Allure.XUnit.csproj b/Allure.XUnit/Allure.XUnit.csproj index c0252748..298c724e 100644 --- a/Allure.XUnit/Allure.XUnit.csproj +++ b/Allure.XUnit/Allure.XUnit.csproj @@ -1,8 +1,9 @@ - + - true netcoreapp2.0;netstandard2.1;net5.0;net6.0 + 11 + true Qameta Software 2.10-SNAPSHOT Allure.XUnit @@ -14,7 +15,6 @@ https://github.com/allure-framework/allure-csharp allure xunit false - 9 true true true From 50ef9ed32282eae1332b0a8bef4b997dbe8e8063 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:56:26 +0700 Subject: [PATCH 09/24] Enable null checks in some classes of allure-xunit --- Allure.XUnit/AllureMessageSink.cs | 6 ++---- Allure.XUnit/AllureXunitHelper.cs | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Allure.XUnit/AllureMessageSink.cs b/Allure.XUnit/AllureMessageSink.cs index 879178ff..08b54b39 100644 --- a/Allure.XUnit/AllureMessageSink.cs +++ b/Allure.XUnit/AllureMessageSink.cs @@ -8,6 +8,8 @@ using Xunit; using Xunit.Abstractions; +#nullable enable + namespace Allure.XUnit { public class AllureMessageSink : TestMessageSink @@ -173,10 +175,6 @@ void AddAllureParameters(ITest test, object[] arguments) } } - AllureContext GetTestContext(ITest test) => - this.GetOrCreateTestData(test).Context - ?? AllureLifecycle.Instance.Context; - void CaptureTestContext(ITest test) => this.GetOrCreateTestData(test).Context = AllureLifecycle.Instance.Context; diff --git a/Allure.XUnit/AllureXunitHelper.cs b/Allure.XUnit/AllureXunitHelper.cs index d18085ce..98d3a3fa 100644 --- a/Allure.XUnit/AllureXunitHelper.cs +++ b/Allure.XUnit/AllureXunitHelper.cs @@ -12,6 +12,8 @@ using Xunit.Abstractions; using Xunit.Sdk; +#nullable enable + namespace Allure.Xunit { public static class AllureXunitHelper From fbea57a5f3bf8fb63488eac7545e1dbc212ce38e Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Jul 2023 20:57:18 +0700 Subject: [PATCH 10/24] Make lifecycle methods with explicit uuids obsoleted --- Allure.Net.Commons/AllureLifecycle.cs | 595 +++++++++++++------- Allure.Net.Commons/Steps/CoreStepsHelper.cs | 14 +- Allure.Net.Commons/Storage/AllureStorage.cs | 153 +++-- 3 files changed, 504 insertions(+), 258 deletions(-) diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index da1ea844..afaacea8 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; -using System.Threading; using Allure.Net.Commons.Configuration; using Allure.Net.Commons.Storage; using Allure.Net.Commons.Writer; @@ -85,10 +85,6 @@ Action action } } - /// Method to get the key for separation the steps for different tests. - public static Func CurrentTestIdGetter { get; set; } = - () => Thread.CurrentThread.ManagedThreadId.ToString(); - internal AllureLifecycle(): this(GetConfiguration()) { } @@ -99,7 +95,8 @@ Func writerFactory { } - internal AllureLifecycle(JObject config): this(config, c => new FileSystemResultsWriter(c)) + internal AllureLifecycle(JObject config) + : this(config, c => new FileSystemResultsWriter(c)) { } @@ -130,7 +127,9 @@ private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => #region TestContainer - public virtual AllureLifecycle StartTestContainer(TestResultContainer container) + public virtual AllureLifecycle StartTestContainer( + TestResultContainer container + ) { container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var parent = this.storage.CurrentTestContainerOrNull; @@ -145,14 +144,9 @@ public virtual AllureLifecycle StartTestContainer(TestResultContainer container) return this; } - public virtual AllureLifecycle StartTestContainer(string parentUuid, TestResultContainer container) - { - UpdateTestContainer(parentUuid, c => c.children.Add(container.uuid)); - StartTestContainer(container); - return this; - } - - public virtual AllureLifecycle UpdateTestContainer(Action update) + public virtual AllureLifecycle UpdateTestContainer( + Action update + ) { var container = this.storage.CurrentTestContainer; lock (this.monitor) @@ -162,28 +156,12 @@ public virtual AllureLifecycle UpdateTestContainer(Action u return this; } - public virtual AllureLifecycle UpdateTestContainer(string uuid, Action update) - { - var container = this.storage.Get(uuid); - lock (this.monitor) - { - update.Invoke(container); - } - return this; - } - public virtual AllureLifecycle StopTestContainer() { UpdateTestContainer(stopContainer); return this; } - public virtual AllureLifecycle StopTestContainer(string uuid) - { - UpdateTestContainer(uuid, stopContainer); - return this; - } - public virtual AllureLifecycle WriteTestContainer() { var container = this.storage.CurrentTestContainer; @@ -192,14 +170,6 @@ public virtual AllureLifecycle WriteTestContainer() return this; } - public virtual AllureLifecycle WriteTestContainer(string uuid) - { - var container = this.storage.Get(uuid); - this.storage.RemoveTestContainer(uuid); - this.writer.Write(container); - return this; - } - #endregion #region Fixture @@ -211,34 +181,6 @@ public virtual AllureLifecycle StartBeforeFixture(FixtureResult result) return this; } - public virtual AllureLifecycle StartBeforeFixture(FixtureResult result, out string uuid) - { - uuid = CreateUuid(); - StartBeforeFixture(uuid, result); - return this; - } - - public virtual AllureLifecycle StartBeforeFixture(string uuid, FixtureResult result) - { - UpdateTestContainer(container => container.befores.Add(result)); - StartFixture(uuid, result); - return this; - } - - public virtual AllureLifecycle StartBeforeFixture(string parentUuid, FixtureResult result, out string uuid) - { - uuid = CreateUuid(); - StartBeforeFixture(parentUuid, uuid, 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; - } - public virtual AllureLifecycle StartAfterFixture(FixtureResult result) { this.UpdateTestContainer(c => c.afters.Add(result)); @@ -246,21 +188,9 @@ public virtual AllureLifecycle StartAfterFixture(FixtureResult result) return this; } - public virtual AllureLifecycle StartAfterFixture(string parentUuid, FixtureResult result, out string uuid) - { - uuid = CreateUuid(); - StartAfterFixture(parentUuid, uuid, result); - return this; - } - - public virtual AllureLifecycle StartAfterFixture(string parentUuid, string uuid, FixtureResult result) - { - UpdateTestContainer(parentUuid, container => container.afters.Add(result)); - StartFixture(uuid, result); - return this; - } - - public virtual AllureLifecycle UpdateFixture(Action update) + public virtual AllureLifecycle UpdateFixture( + Action update + ) { var fixture = this.storage.CurrentFixture; lock (this.monitor) @@ -270,17 +200,9 @@ public virtual AllureLifecycle UpdateFixture(Action update) return this; } - public virtual AllureLifecycle UpdateFixture(string uuid, Action update) - { - var fixture = this.storage.Get(uuid); - lock (this.monitor) - { - update.Invoke(fixture); - } - return this; - } - - public virtual AllureLifecycle StopFixture(Action beforeStop) + public virtual AllureLifecycle StopFixture( + Action beforeStop + ) { this.UpdateFixture(beforeStop); return this.StopFixture(); @@ -297,27 +219,10 @@ public virtual AllureLifecycle StopFixture() return this; } - public virtual AllureLifecycle StopFixture(string uuid) - { - this.UpdateFixture(fixture => - { - fixture.stage = Stage.finished; - fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - }); - this.storage.RemoveFixture(uuid); - return this; - } - #endregion #region TestCase - public virtual AllureLifecycle StartTestCase(string containerUuid, TestResult testResult) - { - UpdateTestContainer(containerUuid, c => c.children.Add(testResult.uuid)); - return StartTestCase(testResult); - } - public virtual AllureLifecycle StartTestCase(TestResult testResult) { var container = this.storage.CurrentTestContainerOrNull; @@ -336,19 +241,6 @@ public virtual AllureLifecycle StartTestCase(TestResult testResult) return this; } - public virtual AllureLifecycle UpdateTestCase( - string uuid, - Action update - ) - { - var testResult = this.storage.Get(uuid); - lock (this.monitor) - { - update(testResult); - } - return this; - } - public virtual AllureLifecycle UpdateTestCase( Action update ) @@ -372,9 +264,6 @@ Action beforeStop public virtual AllureLifecycle StopTestCase() => this.UpdateTestCase(stopTestCase); - public virtual AllureLifecycle StopTestCase(string uuid) => - this.UpdateTestCase(uuid, stopTestCase); - public virtual AllureLifecycle WriteTestCase() { var testResult = this.storage.CurrentTest; @@ -383,14 +272,6 @@ public virtual AllureLifecycle WriteTestCase() return this; } - public virtual AllureLifecycle WriteTestCase(string uuid) - { - var testResult = this.storage.Get(uuid); - this.storage.RemoveTestCase(uuid); - this.writer.Write(testResult); - return this; - } - #endregion #region Step @@ -408,28 +289,6 @@ public virtual AllureLifecycle StartStep(StepResult result) return this; } - public virtual AllureLifecycle StartStep(StepResult result, out string uuid) - { - uuid = CreateUuid(); - StartStep(this.storage.CurrentStepContainer, uuid, result); - return this; - } - - public virtual AllureLifecycle StartStep( - string uuid, - StepResult result - ) => this.StartStep(this.storage.CurrentStepContainer, uuid, result); - - public virtual AllureLifecycle StartStep( - string parentUuid, - string uuid, - StepResult stepResult - ) => this.StartStep( - this.storage.Get(parentUuid), - uuid, - stepResult - ); - public virtual AllureLifecycle UpdateStep(Action update) { var stepResult = this.storage.CurrentStep; @@ -440,33 +299,12 @@ public virtual AllureLifecycle UpdateStep(Action update) return this; } - public virtual AllureLifecycle UpdateStep(string uuid, Action update) - { - var stepResult = storage.Get(uuid); - lock (this.monitor) - { - update.Invoke(stepResult); - } - return this; - } - public virtual AllureLifecycle StopStep(Action beforeStop) { this.UpdateStep(beforeStop); return this.StopStep(); } - public virtual AllureLifecycle StopStep(string uuid) - { - this.UpdateStep(uuid, step => - { - step.stage = Stage.finished; - step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - }); - this.storage.RemoveStep(uuid); - return this; - } - public virtual AllureLifecycle StopStep() { this.UpdateStep(step => @@ -483,16 +321,25 @@ public virtual AllureLifecycle StopStep() #region Attachment // TODO: read file in background thread - public virtual AllureLifecycle AddAttachment(string name, string type, string path) + public virtual AllureLifecycle AddAttachment( + string name, + string type, + string path + ) { var fileExtension = new FileInfo(path).Extension; return AddAttachment(name, type, File.ReadAllBytes(path), fileExtension); } - public virtual AllureLifecycle AddAttachment(string name, string type, byte[] content, - string fileExtension = "") + public virtual AllureLifecycle AddAttachment( + string name, + string type, + byte[] content, + string fileExtension = "" + ) { - var source = $"{CreateUuid()}{AllureConstants.ATTACHMENT_FILE_SUFFIX}{fileExtension}"; + var suffix = AllureConstants.ATTACHMENT_FILE_SUFFIX; + var source = $"{CreateUuid()}{suffix}{fileExtension}"; var attachment = new Attachment { name = name, @@ -508,7 +355,10 @@ public virtual AllureLifecycle AddAttachment(string name, string type, byte[] co return this; } - public virtual AllureLifecycle AddAttachment(string path, string? name = null) + public virtual AllureLifecycle AddAttachment( + string path, + string? name = null + ) { name ??= Path.GetFileName(path); var type = MimeTypesMap.GetMimeType(path); @@ -524,17 +374,6 @@ public virtual void CleanupResultDirectory() writer.CleanUp(); } - public virtual AllureLifecycle AddScreenDiff(string testCaseUuid, string expectedPng, string actualPng, - string diffPng) - { - AddAttachment(expectedPng, "expected") - .AddAttachment(actualPng, "actual") - .AddAttachment(diffPng, "diff") - .UpdateTestCase(testCaseUuid, x => x.labels.Add(Label.TestType("screenshotDiff"))); - - return this; - } - public virtual AllureLifecycle AddScreenDiff( string expectedPng, string actualPng, @@ -555,20 +394,33 @@ string diffPng private static JObject GetConfiguration() { - var jsonConfigPath = Environment.GetEnvironmentVariable(AllureConstants.ALLURE_CONFIG_ENV_VARIABLE); + var configEnvVarName = AllureConstants.ALLURE_CONFIG_ENV_VARIABLE; + var jsonConfigPath = Environment.GetEnvironmentVariable( + configEnvVarName + ); if (jsonConfigPath != null && !File.Exists(jsonConfigPath)) + { throw new FileNotFoundException( - $"Couldn't find '{jsonConfigPath}' specified in {AllureConstants.ALLURE_CONFIG_ENV_VARIABLE} environment variable"); + $"Couldn't find '{jsonConfigPath}' specified " + + $"in {configEnvVarName} environment variable" + ); + } if (File.Exists(jsonConfigPath)) + { return JObject.Parse(File.ReadAllText(jsonConfigPath)); + } - var defaultJsonConfigPath = - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AllureConstants.CONFIG_FILENAME); + var defaultJsonConfigPath = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + AllureConstants.CONFIG_FILENAME + ); if (File.Exists(defaultJsonConfigPath)) + { return JObject.Parse(File.ReadAllText(defaultJsonConfigPath)); + } return JObject.Parse("{}"); } @@ -621,5 +473,354 @@ static string CreateUuid() => Guid.NewGuid().ToString("N"); #endregion + + #region Obsolete + + [Obsolete( + "This property is a rudimentary part of the API. It has no " + + "effect and will be removed soon" + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public static Func? CurrentTestIdGetter { get; set; } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartTestContainer( + string parentUuid, + TestResultContainer container + ) + { + UpdateTestContainer(parentUuid, c => c.children.Add(container.uuid)); + StartTestContainer(container); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateTestContainer( + string uuid, + Action update + ) + { + var container = this.storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(container); + } + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopTestContainer(string uuid) + { + UpdateTestContainer(uuid, stopContainer); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle WriteTestContainer(string uuid) + { + var container = this.storage.Get(uuid); + this.storage.RemoveTestContainer(uuid); + this.writer.Write(container); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + StartBeforeFixture(uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string uuid, + FixtureResult result + ) + { + UpdateTestContainer(container => container.befores.Add(result)); + StartFixture(uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string parentUuid, + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + StartBeforeFixture(parentUuid, uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartBeforeFixture( + string parentUuid, + string uuid, + FixtureResult result + ) + { + UpdateTestContainer(parentUuid, container => container.befores.Add(result)); + StartFixture(uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartAfterFixture( + string parentUuid, + FixtureResult result, + out string uuid + ) + { + uuid = CreateUuid(); + StartAfterFixture(parentUuid, uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartAfterFixture( + string parentUuid, + string uuid, + FixtureResult result + ) + { + UpdateTestContainer( + parentUuid, + container => container.afters.Add(result) + ); + StartFixture(uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateFixture( + string uuid, + Action update + ) + { + var fixture = this.storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(fixture); + } + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopFixture(string uuid) + { + this.UpdateFixture(fixture => + { + fixture.stage = Stage.finished; + fixture.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveFixture(uuid); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartTestCase( + string containerUuid, + TestResult testResult + ) + { + UpdateTestContainer( + containerUuid, + c => c.children.Add(testResult.uuid) + ); + return StartTestCase(testResult); + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateTestCase( + string uuid, + Action update + ) + { + var testResult = this.storage.Get(uuid); + lock (this.monitor) + { + update(testResult); + } + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopTestCase(string uuid) => + this.UpdateTestCase(uuid, stopTestCase); + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle WriteTestCase(string uuid) + { + var testResult = this.storage.Get(uuid); + this.storage.RemoveTestCase(uuid); + this.writer.Write(testResult); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + StepResult result, + out string uuid + ) + { + uuid = CreateUuid(); + StartStep(this.storage.CurrentStepContainer, uuid, result); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + string uuid, + StepResult result + ) => this.StartStep(this.storage.CurrentStepContainer, uuid, result); + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StartStep( + string parentUuid, + string uuid, + StepResult stepResult + ) => this.StartStep( + this.storage.Get(parentUuid), + uuid, + stepResult + ); + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle UpdateStep( + string uuid, + Action update + ) + { + var stepResult = storage.Get(uuid); + lock (this.monitor) + { + update.Invoke(stepResult); + } + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle StopStep(string uuid) + { + this.UpdateStep(uuid, step => + { + step.stage = Stage.finished; + step.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + }); + this.storage.RemoveStep(uuid); + return this; + } + + [Obsolete( + "Lifecycle methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual AllureLifecycle AddScreenDiff( + string testCaseUuid, + string expectedPng, + string actualPng, + string diffPng + ) + { + AddAttachment(expectedPng, "expected") + .AddAttachment(actualPng, "actual") + .AddAttachment(diffPng, "diff") + .UpdateTestCase(testCaseUuid, x => x.labels.Add(Label.TestType("screenshotDiff"))); + + return this; + } + + #endregion } } \ No newline at end of file diff --git a/Allure.Net.Commons/Steps/CoreStepsHelper.cs b/Allure.Net.Commons/Steps/CoreStepsHelper.cs index 6a51100e..93e28087 100644 --- a/Allure.Net.Commons/Steps/CoreStepsHelper.cs +++ b/Allure.Net.Commons/Steps/CoreStepsHelper.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Allure.Net.Commons.Storage; @@ -9,13 +10,12 @@ public class CoreStepsHelper { public static IStepLogger StepLogger { get; set; } - private static readonly AsyncLocal TestResultAccessorAsyncLocal = new(); - - public static ITestResultAccessor TestResultAccessor - { - get => TestResultAccessorAsyncLocal.Value; - set => TestResultAccessorAsyncLocal.Value = value; - } + [Obsolete( + "This property is a rudimentary part of the API. It has no " + + "effect and will be removed soon" + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public static ITestResultAccessor TestResultAccessor { get; set; } #region Fixtures diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 042d824f..3d684bf7 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading; @@ -64,89 +65,35 @@ public void PutTestContainer(TestResultContainer container) => c => c.WithContainer(container) ); - public void PutTestContainer( - string uuid, - TestResultContainer container - ) => this.PutAndUpdateContext( - uuid, - container, - c => c.WithContainer(container) - ); - public void RemoveTestContainer() => this.UpdateContext( c => c.WithNoLastContainer() ); - public void RemoveTestContainer(string uuid) => - this.RemoveAndUpdateContext( - uuid, - c => ContextWithNoContainer(c, uuid) - ); - public void PutFixture(FixtureResult fixture) => this.UpdateContext(c => c.WithFixtureContext(fixture)); - public void PutFixture(string uuid, FixtureResult fixture) => - this.PutAndUpdateContext( - uuid, - fixture, - c => c.WithFixtureContext(fixture) - ); - public void RemoveFixture() => this.UpdateContext(c => c.WithNoFixtureContext()); - public void RemoveFixture(string uuid) - => this.RemoveAndUpdateContext( - uuid, - c => c.WithNoFixtureContext() - ); - public void PutTestCase(TestResult testResult) => this.UpdateContext( c => c.WithTestContext(testResult) ); - public void PutTestCase(string uuid, TestResult testResult) => - this.PutAndUpdateContext( - uuid, - testResult, - c => c.WithTestContext(testResult) - ); - public void RemoveTestCase() => this.UpdateContext( c => c.WithNoTestContext() ); - public void RemoveTestCase(string uuid) => - this.RemoveAndUpdateContext( - uuid, - c => c.WithNoTestContext() - ); - public void PutStep(StepResult stepResult) => this.UpdateContext( c => c.WithStep(stepResult) ); - public void PutStep(string uuid, StepResult stepResult) => - this.PutAndUpdateContext( - uuid, - stepResult, - c => c.WithStep(stepResult) - ); - public void RemoveStep() => this.UpdateContext( c => c.WithNoLastStep() ); - public void RemoveStep(string uuid) => - this.RemoveAndUpdateContext( - uuid, - c => this.ContextWithNoStep(c, uuid) - ); - void PutAndUpdateContext( string uuid, T value, @@ -217,5 +164,103 @@ AllureContext ContextWithNoStep(AllureContext context, string uuid) } return context; } + + #region Obsolete + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void PutTestContainer( + string uuid, + TestResultContainer container + ) => this.PutAndUpdateContext( + uuid, + container, + c => c.WithContainer(container) + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void RemoveTestContainer(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => ContextWithNoContainer(c, uuid) + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void PutFixture(string uuid, FixtureResult fixture) => + this.PutAndUpdateContext( + uuid, + fixture, + c => c.WithFixtureContext(fixture) + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void RemoveFixture(string uuid) + => this.RemoveAndUpdateContext( + uuid, + c => c.WithNoFixtureContext() + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void PutTestCase(string uuid, TestResult testResult) => + this.PutAndUpdateContext( + uuid, + testResult, + c => c.WithTestContext(testResult) + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void RemoveTestCase(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => c.WithNoTestContext() + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void PutStep(string uuid, StepResult stepResult) => + this.PutAndUpdateContext( + uuid, + stepResult, + c => c.WithStep(stepResult) + ); + + [Obsolete( + "Storage methods with explicit uuids are obsolete. Use " + + "their counterparts without uuids to manipulate the context." + )] + [EditorBrowsable(EditorBrowsableState.Never)] + public void RemoveStep(string uuid) => + this.RemoveAndUpdateContext( + uuid, + c => this.ContextWithNoStep(c, uuid) + ); + + #endregion } } \ No newline at end of file From 6fd9380d183ef5516025b89a9f4fede7880eddbc Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:12:01 +0700 Subject: [PATCH 11/24] Minor changes in context and lifecycle - Improve error messages and doc comments - Add DebuggerDisplay for context - Uuid in string representation of context if names are empty - Old storage uuid-based methods obsoleted - Make RunInContext return modified context - Replace unsupported container nesting with test nested into each container in chain - Container and test starting now add objects into storage by uuids (transition period) --- .../AllureContextTests.cs | 62 ++-- .../AllureLifeCycleTest.cs | 18 +- Allure.Net.Commons/AllureLifecycle.cs | 333 +++++++++++++----- Allure.Net.Commons/Storage/AllureContext.cs | 64 ++-- Allure.Net.Commons/Storage/AllureStorage.cs | 169 +++++---- 5 files changed, 432 insertions(+), 214 deletions(-) diff --git a/Allure.Net.Commons.Tests/AllureContextTests.cs b/Allure.Net.Commons.Tests/AllureContextTests.cs index 668668c5..645bed5d 100644 --- a/Allure.Net.Commons.Tests/AllureContextTests.cs +++ b/Allure.Net.Commons.Tests/AllureContextTests.cs @@ -1,4 +1,5 @@ -using Allure.Net.Commons.Storage; +using System; +using Allure.Net.Commons.Storage; using NUnit.Framework; namespace Allure.Net.Commons.Tests @@ -15,9 +16,11 @@ public void TestEmptyContext() 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, @@ -73,8 +76,8 @@ public void CanNotAddContainerIfTestIsSet() Assert.That( () => ctx.WithContainer(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to change a container context because a test " + - "context is active." + "Unable to change the container context because the " + + "test context is active." ) ); } @@ -89,8 +92,8 @@ public void CanNotAddContainerIfFixtureIsSet() Assert.That( () => ctx.WithContainer(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to change a container context because a fixture " + - "context is active." + "Unable to change the container context because the " + + "fixture context is active." ) ); } @@ -105,8 +108,8 @@ public void CanNotRemoveContainerIfTestIsSet() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to change a container context because a test " + - "context is active." + "Unable to change the container context because the " + + "test context is active." ) ); } @@ -147,6 +150,7 @@ public void OneContainerInContainerContext() 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)); } @@ -165,6 +169,7 @@ public void SecondContainerIsPushedInFront() ctx.ContainerContext, Is.EqualTo(new[] { container2, container1 }) ); + Assert.That(ctx.ContainerContextDepth, Is.EqualTo(2)); Assert.That(ctx.CurrentContainer, Is.SameAs(container2)); } @@ -176,8 +181,8 @@ public void CanNotRemoveContainerIfNoneExist() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to deactivate a container context " + - "because it's inactive." + "Unable to deactivate the container context because " + + "it is not active." ) ); } @@ -190,6 +195,7 @@ public void LatestContainerCanBeRemoved() .WithNoLastContainer(); Assert.That(ctx.HasContainer, Is.False); + Assert.That(ctx.ContainerContextDepth, Is.Zero); Assert.That(ctx.ContainerContext, Is.Empty); } @@ -204,6 +210,7 @@ public void IfContainerIsRemovedThePreviousOneBecomesActive() Assert.That(ctx.ContainerContext, Is.EqualTo(new[] { container })); Assert.That(ctx.CurrentContainer, Is.SameAs(container)); + Assert.That(ctx.ContainerContextDepth, Is.EqualTo(1)); } [Test] @@ -217,8 +224,8 @@ public void FixtureContextRequiresContainer() () => ctx.WithFixtureContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to activate a fixture context because a " + - "container context is inactive." + "Unable to activate the fixture context because " + + "the container context is not active." ) ); } @@ -259,8 +266,8 @@ public void CanNotRemoveContainerIfFixtureIsSet() Assert.That( ctx.WithNoLastContainer, Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to change a container context because " + - "a fixture context is active." + "Unable to change the container context because the " + + "fixture context is active." ) ); } @@ -278,8 +285,8 @@ public void FixturesCanNotBeNested() () => ctx.WithFixtureContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to activate a fixture context because " + - "another fixture context is active." + "Unable to activate the fixture context because " + + "it's already active." ) ); } @@ -306,8 +313,8 @@ public void TestsCanNotBeNested() () => ctx.WithTestContext(new()), Throws.InvalidOperationException .With.Message.EqualTo( - "Unable to activate a test context because another " + - "test context is active." + "Unable to activate the test context because " + + "it is already active." ) ); } @@ -322,8 +329,8 @@ public void CanNotSetTestContextIfFixtureContextIsActive() Assert.That( () => ctx.WithTestContext(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to activate a test context because a fixture " + - "context is active." + "Unable to activate the test context because the " + + "fixture context is active." ) ); } @@ -393,7 +400,7 @@ public void StepCanNotBeAddedIfNoTestOrFixtureExists() Assert.That( () => ctx.WithStep(new()), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to activate a step context because neither " + + "Unable to activate the step context because neither " + "test, nor fixture context is active." ) ); @@ -407,8 +414,8 @@ public void StepCanNotBeRemovedIfNoStepExists() Assert.That( () => ctx.WithNoLastStep(), Throws.InvalidOperationException.With.Message.EqualTo( - "Unable to deactivate a step context because it's " + - "already inactive." + "Unable to deactivate the step context because it " + + "isn't active." ) ); } @@ -424,6 +431,7 @@ public void StepCanBeAddedIfFixtureExists() 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)); } @@ -452,6 +460,7 @@ public void TwoStepsCanBeAdded() .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)); } @@ -469,6 +478,7 @@ public void RemovingStepRestoresPreviousStepAsStepContainer() 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)); } @@ -483,6 +493,7 @@ public void RemovingTheOnlyStepRestoresTestAsStepContainer() 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)); } @@ -589,6 +600,13 @@ public void ContextToString() .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 c0394bad..a8df397d 100644 --- a/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs +++ b/Allure.Net.Commons.Tests/AllureLifeCycleTest.cs @@ -21,19 +21,6 @@ public void ShouldSetDefaultStateAsNone() Assert.AreEqual(Status.none, new TestResult().status); } - [Test] - public void Way() - { - AsyncLocal myContext = new(); - myContext.Value = "Outer"; - Parallel.For(0, 2, i => - { - Console.WriteLine(myContext.Value); - myContext.Value = $"Inner {i}"; - }); - Console.WriteLine(myContext.Value); - } - [Test, Description("Integration Test")] public void IntegrationTest() { @@ -155,7 +142,7 @@ public async Task ContextCapturingTest() { var writer = new InMemoryResultsWriter(); var lifecycle = new AllureLifecycle(_ => writer); - AllureContext context = null; + AllureContext context = null, modifiedContext = null; await Task.Factory.StartNew(() => { lifecycle.StartTestCase(new() @@ -164,13 +151,14 @@ await Task.Factory.StartNew(() => }); context = lifecycle.Context; }); - lifecycle.RunInContext(context, () => + modifiedContext = lifecycle.RunInContext(context, () => { lifecycle.StopTestCase(); lifecycle.WriteTestCase(); }); Assert.That(writer.testResults, Is.Not.Empty); + Assert.That(modifiedContext.HasTest, Is.False); } [Test] diff --git a/Allure.Net.Commons/AllureLifecycle.cs b/Allure.Net.Commons/AllureLifecycle.cs index afaacea8..1e15dbfe 100644 --- a/Allure.Net.Commons/AllureLifecycle.cs +++ b/Allure.Net.Commons/AllureLifecycle.cs @@ -16,6 +16,18 @@ 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 { private readonly Dictionary typeFormatters = new(); @@ -32,16 +44,12 @@ public class AllureLifecycle /// Protects mutations of shared allure model objects against data /// races that may otherwise occur because of multithreaded access. /// - readonly object monitor = new(); + readonly object modelMonitor = new(); /// - /// Captures the current context of Allure's execution. + /// Captures the current value of Allure context. /// - /// - /// This property is intended to be used by Allure integrations with - /// test frameworks, not by end user's code. - /// public AllureContext Context { get => this.storage.CurrentContext; @@ -49,35 +57,34 @@ public AllureContext Context } /// - /// Runs the specified code in the specified context restoring it - /// before returning. Use this method if you need to access the context - /// somewhere outside the async execution context the allure context - /// has been set in. + /// 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. /// - /// - /// This method is intended to be used by Allure integrations with - /// test frameworks, not by end user's code. - /// /// /// A context that was previously captured with . + /// If it is null, the code is executed in the current context. /// /// A code to run. - public void RunInContext( + /// The context after the code is executed. + public AllureContext RunInContext( AllureContext? context, - Action action + Action action ) { - if (context is null || context == this.Context) + if (context is null) { - action(this.Context); - return; + action(); + return this.Context; } var originalContext = this.Context; try { this.Context = context; - action(context); + action(); + return this.Context; } finally { @@ -127,45 +134,73 @@ private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) => #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(); - var parent = this.storage.CurrentTestContainerOrNull; - if (parent is not null) - { - lock (this.monitor) - { - parent.children.Add(container.uuid); - } - } - storage.PutTestContainer(container); + this.storage.PutTestContainer(container.uuid, container); return this; } + /// + /// 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.storage.CurrentTestContainer; - lock (this.monitor) + lock (this.modelMonitor) { update.Invoke(container); } return this; } + /// + /// Stops the current test container. + /// + /// + /// Requires the container context to be active. + /// + /// public virtual AllureLifecycle StopTestContainer() { UpdateTestContainer(stopContainer); return this; } + /// + /// 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.storage.CurrentTestContainer; - this.storage.RemoveTestContainer(); + this.storage.RemoveTestContainer(container.uuid); this.writer.Write(container); return this; } @@ -174,13 +209,35 @@ public virtual AllureLifecycle WriteTestContainer() #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) { - UpdateTestContainer(container => container.befores.Add(result)); + this.UpdateTestContainer(container => container.befores.Add(result)); this.StartFixture(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)); @@ -188,18 +245,36 @@ public virtual AllureLifecycle StartAfterFixture(FixtureResult result) return this; } + /// + /// 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.storage.CurrentFixture; - lock (this.monitor) + lock (this.modelMonitor) { 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 ) @@ -208,6 +283,14 @@ Action 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(fixture => @@ -223,36 +306,64 @@ public virtual AllureLifecycle StopFixture() #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 container = this.storage.CurrentTestContainerOrNull; - if (container is not null) + var uuid = testResult.uuid; + var containers = this.storage.CurrentContext.ContainerContext; + lock (this.modelMonitor) { - lock (this.monitor) + foreach (TestResultContainer container in containers) { - container.children.Add(testResult.uuid); + container.children.Add(uuid); } } testResult.stage = Stage.running; testResult.start = testResult.start == 0L ? DateTimeOffset.Now.ToUnixTimeMilliseconds() : testResult.start; - this.storage.PutTestCase(testResult); + this.storage.PutTestCase(uuid, testResult); return this; } + /// + /// 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.storage.CurrentTest; - lock (this.monitor) + lock (this.modelMonitor) { update(testResult); } return this; } + /// + /// 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(testResult => @@ -261,13 +372,34 @@ Action beforeStop stopTestCase(testResult); }); + /// + /// Stops the current test. + /// + /// + /// Requires the test context to be active. + /// + /// public virtual AllureLifecycle StopTestCase() => this.UpdateTestCase(stopTestCase); + /// + /// 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.storage.CurrentTest; - this.storage.RemoveTestCase(); + string uuid; + lock (this.modelMonitor) + { + uuid = testResult.uuid; + } + this.storage.RemoveTestCase(uuid); this.writer.Write(testResult); return this; } @@ -276,12 +408,23 @@ public virtual AllureLifecycle WriteTestCase() #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) { result.stage = Stage.running; result.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var parent = this.storage.CurrentStepContainer; - lock (this.monitor) + lock (this.modelMonitor) { parent.steps.Add(result); } @@ -289,22 +432,54 @@ public virtual AllureLifecycle StartStep(StepResult result) return this; } + /// + /// 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.storage.CurrentStep; - lock (this.monitor) + lock (this.modelMonitor) { update.Invoke(stepResult); } return this; } + /// + /// 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(); } + /// + /// 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(step => @@ -348,7 +523,7 @@ public virtual AllureLifecycle AddAttachment( }; this.writer.Write(source, content); var target = this.storage.CurrentStepContainer; - lock (this.monitor) + lock (this.modelMonitor) { target.attachments.Add(attachment); } @@ -432,32 +607,6 @@ private void StartFixture(FixtureResult fixtureResult) fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); } - void StartFixture(string uuid, FixtureResult fixtureResult) - { - this.storage.PutFixture(uuid, fixtureResult); - lock (this.monitor) - { - fixtureResult.stage = Stage.running; - fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - } - } - - AllureLifecycle StartStep( - ExecutableItem parent, - string uuid, - StepResult stepResult - ) - { - stepResult.stage = Stage.running; - stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - lock (this.monitor) - { - parent.steps.Add(stepResult); - } - this.storage.PutStep(uuid, stepResult); - return this; - } - static readonly Action stopContainer = c => c.stop = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -474,7 +623,7 @@ static string CreateUuid() => #endregion - #region Obsolete + #region Obsoleted [Obsolete( "This property is a rudimentary part of the API. It has no " + @@ -493,8 +642,9 @@ public virtual AllureLifecycle StartTestContainer( TestResultContainer container ) { + container.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); UpdateTestContainer(parentUuid, c => c.children.Add(container.uuid)); - StartTestContainer(container); + this.storage.PutTestContainer(container.uuid, container); return this; } @@ -509,7 +659,7 @@ Action update ) { var container = this.storage.Get(uuid); - lock (this.monitor) + lock (this.modelMonitor) { update.Invoke(container); } @@ -521,11 +671,8 @@ Action update "their counterparts without uuids to manipulate the context." )] [EditorBrowsable(EditorBrowsableState.Never)] - public virtual AllureLifecycle StopTestContainer(string uuid) - { + public virtual AllureLifecycle StopTestContainer(string uuid) => UpdateTestContainer(uuid, stopContainer); - return this; - } [Obsolete( "Lifecycle methods with explicit uuids are obsolete. Use " + @@ -648,7 +795,7 @@ Action update ) { var fixture = this.storage.Get(uuid); - lock (this.monitor) + lock (this.modelMonitor) { update.Invoke(fixture); } @@ -699,7 +846,7 @@ Action update ) { var testResult = this.storage.Get(uuid); - lock (this.monitor) + lock (this.modelMonitor) { update(testResult); } @@ -778,7 +925,7 @@ Action update ) { var stepResult = storage.Get(uuid); - lock (this.monitor) + lock (this.modelMonitor) { update.Invoke(stepResult); } @@ -821,6 +968,34 @@ string diffPng return this; } + [Obsolete] + void StartFixture(string uuid, FixtureResult fixtureResult) + { + this.storage.PutFixture(uuid, fixtureResult); + lock (this.modelMonitor) + { + fixtureResult.stage = Stage.running; + fixtureResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + } + } + + [Obsolete] + AllureLifecycle StartStep( + ExecutableItem parent, + string uuid, + StepResult stepResult + ) + { + stepResult.stage = Stage.running; + stepResult.start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + lock (this.modelMonitor) + { + parent.steps.Add(stepResult); + } + this.storage.PutStep(uuid, stepResult); + return this; + } + #endregion } } \ No newline at end of file diff --git a/Allure.Net.Commons/Storage/AllureContext.cs b/Allure.Net.Commons/Storage/AllureContext.cs index 54157cfc..3ebb7df1 100644 --- a/Allure.Net.Commons/Storage/AllureContext.cs +++ b/Allure.Net.Commons/Storage/AllureContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Text; @@ -18,6 +19,10 @@ namespace Allure.Net.Commons.Storage /// 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 { /// @@ -25,6 +30,11 @@ public record class AllureContext /// 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. /// @@ -40,6 +50,11 @@ public record class AllureContext /// 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. /// @@ -162,12 +177,18 @@ internal TestResultContainer CurrentContainer "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); + RepresentStack(this.ContainerContext, c => c.name ?? c.uuid); var fixture = this.FixtureContext?.name ?? "null"; - var test = this.TestContext?.name ?? "null"; + var test = this.TestContext?.name + ?? this.TestContext?.uuid + ?? "null"; var steps = RepresentStack(this.StepContext, s => s.name); stringBuilder.AppendFormat("Containers = [{0}], ", containers); @@ -225,9 +246,12 @@ this with /// /// Creates a new with the active fixture - /// context that is set to the specified fixture. Requires an active - /// container context. + /// 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. /// @@ -340,8 +364,8 @@ this with StepContext = this.HasStep ? this.StepContext.Pop() : throw new InvalidOperationException( - "Unable to deactivate a step context because it's " + - "already inactive." + "Unable to deactivate the step context because it " + + "isn't active." ) }; @@ -350,7 +374,7 @@ AllureContext ValidateContainerContextCanBeModified() if (this.FixtureContext is not null) { throw new InvalidOperationException( - "Unable to change a container context because a " + + "Unable to change the container context because the " + "fixture context is active." ); } @@ -358,8 +382,8 @@ AllureContext ValidateContainerContextCanBeModified() if (this.TestContext is not null) { throw new InvalidOperationException( - "Unable to change a container context because a test " + - "context is active." + "Unable to change the container context because the " + + "test context is active." ); } @@ -371,8 +395,8 @@ AllureContext ValidateContainerCanBeRemoved() if (!this.HasContainer) { throw new InvalidOperationException( - "Unable to deactivate a container context because it's " + - "inactive." + "Unable to deactivate the container context because it " + + "is not active." ); } @@ -389,16 +413,16 @@ FixtureResult ValidateNewFixtureContext(FixtureResult fixture) if (!this.HasContainer) { throw new InvalidOperationException( - "Unable to activate a fixture context " + - "because a container context is inactive." + "Unable to activate the fixture context " + + "because the container context is not active." ); } if (this.HasFixture) { throw new InvalidOperationException( - "Unable to activate a fixture context " + - "because another fixture context is active." + "Unable to activate the fixture context " + + "because it's already active." ); } @@ -410,16 +434,16 @@ TestResult ValidateNewTestContext(TestResult testResult) if (this.HasFixture) { throw new InvalidOperationException( - "Unable to activate a test context " + - "because a fixture context is active." + "Unable to activate the test context " + + "because the fixture context is active." ); } if (this.HasTest) { throw new InvalidOperationException( - "Unable to activate a test context " + - "because another test context is active." + "Unable to activate the test context " + + "because it is already active." ); } @@ -431,7 +455,7 @@ StepResult ValidateNewStep(StepResult stepResult) if (!this.HasTest && !this.HasFixture) { throw new InvalidOperationException( - "Unable to activate a step context because neither " + + "Unable to activate the step context because neither " + "test, nor fixture context is active." ); } diff --git a/Allure.Net.Commons/Storage/AllureStorage.cs b/Allure.Net.Commons/Storage/AllureStorage.cs index 3d684bf7..26abafba 100644 --- a/Allure.Net.Commons/Storage/AllureStorage.cs +++ b/Allure.Net.Commons/Storage/AllureStorage.cs @@ -11,14 +11,15 @@ namespace Allure.Net.Commons.Storage { internal class AllureStorage { - readonly ConcurrentDictionary storage = new(); readonly AsyncLocal context = new(); public AllureContext CurrentContext { get => this.context.Value ??= new(); set => this.context.Value = value - ?? throw new ArgumentNullException(nameof(CurrentContext)); + ?? throw new ArgumentNullException( + nameof(this.CurrentContext) + ); } public AllureStorage() @@ -44,22 +45,6 @@ public AllureStorage() public StepResult CurrentStep => this.CurrentContext.CurrentStep; - public T Get(string uuid) - { - return (T) storage[uuid]; - } - - public T Put(string uuid, T item) where T: notnull - { - return (T) storage.GetOrAdd(uuid, item); - } - - public T Remove(string uuid) - { - storage.TryRemove(uuid, out var value); - return (T) value; - } - public void PutTestContainer(TestResultContainer container) => this.UpdateContext( c => c.WithContainer(container) @@ -94,79 +79,35 @@ public void RemoveStep() => this.UpdateContext( c => c.WithNoLastStep() ); - void PutAndUpdateContext( - string uuid, - T value, - Func updateFn - ) where T : notnull + void UpdateContext(Func updateFn) { - this.Put(uuid, value); - this.UpdateContext(updateFn); + this.CurrentContext = updateFn(this.CurrentContext); } - void RemoveAndUpdateContext(string uuid, Func updateFn) - { - this.UpdateContext(updateFn); - this.Remove(uuid); - } + #region Obsoleted - void UpdateContext(Func updateFn) + [Obsolete] + readonly ConcurrentDictionary storage = new(); + + [Obsolete] + public T Get(string uuid) { - this.CurrentContext = updateFn(this.CurrentContext); + return (T)storage[uuid]; } - static AllureContext ContextWithNoContainer( - AllureContext context, - string uuid - ) + [Obsolete] + public T Put(string uuid, T item) where T : notnull { - 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" - ); - } - } - while (containersToPushAgain.Any()) - { - context = context.WithContainer( - containersToPushAgain.Pop() - ); - } - return context; + return (T)storage.GetOrAdd(uuid, item); } - AllureContext ContextWithNoStep(AllureContext context, string uuid) + [Obsolete] + public T Remove(string uuid) { - var stepResult = this.Get(uuid); - 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" - ); - } - } - while (stepsToPushAgain.Any()) - { - context = context.WithStep( - stepsToPushAgain.Pop() - ); - } - return context; + storage.TryRemove(uuid, out var value); + return (T)value; } - #region Obsolete - [Obsolete( "Storage methods with explicit uuids are obsolete. Use " + "their counterparts without uuids to manipulate the context." @@ -261,6 +202,78 @@ public void RemoveStep(string uuid) => c => this.ContextWithNoStep(c, uuid) ); + [Obsolete] + void PutAndUpdateContext( + string uuid, + T value, + Func updateFn + ) where T : notnull + { + this.Put(uuid, value); + this.UpdateContext(updateFn); + } + + [Obsolete] + void RemoveAndUpdateContext(string uuid, Func updateFn) + { + this.UpdateContext(updateFn); + this.Remove(uuid); + } + + [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()) + { + context = context.WithContainer( + containersToPushAgain.Pop() + ); + } + return context; + } + + [Obsolete] + AllureContext ContextWithNoStep(AllureContext context, string uuid) + { + var stepResult = this.Get(uuid); + 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 From 98d2d228b9d66fd727ae6e6bbc878df4def4557c Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:23:00 +0700 Subject: [PATCH 12/24] Rewrite Allure.NUnit to support the new context scheme --- Allure.NUnit.Examples/BaseTest.cs | 10 +- .../AllureDisplayIgnoredAttribute.cs | 30 +-- .../Core/AllureExtendedConfiguration.cs | 8 +- Allure.NUnit/Core/AllureExtensions.cs | 59 +++--- Allure.NUnit/Core/AllureNUnitAttribute.cs | 24 +-- Allure.NUnit/Core/AllureNUnitHelper.cs | 183 +++++++++++++----- 6 files changed, 196 insertions(+), 118 deletions(-) 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/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..c149d88d 100644 --- a/Allure.NUnit/Core/AllureExtensions.cs +++ b/Allure.NUnit/Core/AllureExtensions.cs @@ -58,15 +58,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 +95,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 +140,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 +177,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; @@ -184,16 +204,5 @@ public static async Task WrapInStepAsync( throw; } } - - /// - /// 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); - } } } \ 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); }); From 618843db6c794ac6d2dfe9fc1faebb355b55be18 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:24:54 +0700 Subject: [PATCH 13/24] Fix Allure.XUnit usage of RunInContext --- Allure.XUnit.Examples/ExampleUnitTests.cs | 1 + Allure.XUnit/AllureMessageSink.cs | 22 +++++++++------------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Allure.XUnit.Examples/ExampleUnitTests.cs b/Allure.XUnit.Examples/ExampleUnitTests.cs index 2741bf25..fd10d4d0 100644 --- a/Allure.XUnit.Examples/ExampleUnitTests.cs +++ b/Allure.XUnit.Examples/ExampleUnitTests.cs @@ -5,6 +5,7 @@ using Allure.Net.Commons; using Allure.Xunit; using Allure.Xunit.Attributes; +using Allure.XUnit.Attributes.Steps; using Xunit; namespace Allure.XUnit.Examples diff --git a/Allure.XUnit/AllureMessageSink.cs b/Allure.XUnit/AllureMessageSink.cs index 08b54b39..19358aa9 100644 --- a/Allure.XUnit/AllureMessageSink.cs +++ b/Allure.XUnit/AllureMessageSink.cs @@ -106,22 +106,22 @@ MessageHandlerArgs args void OnTestFailed(MessageHandlerArgs args) => this.RunInTestContext( args.Message.Test, - _ => AllureXunitHelper.ApplyTestFailure(args.Message) + () => AllureXunitHelper.ApplyTestFailure(args.Message) ); void OnTestPassed(MessageHandlerArgs args) => this.RunInTestContext( args.Message.Test, - _ => AllureXunitHelper.ApplyTestSuccess(args.Message) + () => AllureXunitHelper.ApplyTestSuccess(args.Message) ); void OnTestSkipped(MessageHandlerArgs args) { var message = args.Message; var test = message.Test; - this.UpdateTestContext(test, ctx => + this.UpdateTestContext(test, () => { - if (!ctx.HasTest) + if (!AllureLifecycle.Instance.Context.HasTest) { AllureXunitHelper.StartAllureTestCase(test); } @@ -135,7 +135,7 @@ void OnTestFinished(MessageHandlerArgs args) var test = args.Message.Test; var arguments = this.allureTestData[test].Arguments; - this.RunInTestContext(test, _ => + this.RunInTestContext(test, () => { this.AddAllureParameters(test, arguments); AllureXunitHelper.ReportCurrentTestCase(); @@ -179,20 +179,16 @@ void CaptureTestContext(ITest test) => this.GetOrCreateTestData(test).Context = AllureLifecycle.Instance.Context; - void RunInTestContext(ITest test, Action action) => + AllureContext RunInTestContext(ITest test, Action action) => AllureLifecycle.Instance.RunInContext( this.GetOrCreateTestData(test).Context, action ); - void UpdateTestContext(ITest test, Action action) => - this.RunInTestContext( + void UpdateTestContext(ITest test, Action action) => + this.GetOrCreateTestData(test).Context = this.RunInTestContext( test, - ctx => - { - action(ctx); - this.CaptureTestContext(test); - } + action ); void LogUnreportedTheoryArgs(string testName) From 4e43ea0a07f329d01161c9155a0fdc99d2bbbaf3 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:45:19 +0700 Subject: [PATCH 14/24] Rewrite Allure.SpecFlowPlugin to support new context scheme Additionally: - Change broken status to failed for some scenario and step conditions - Add extra test case instead of changing state of existing one in case after feature failed - Enable nullable check project-wide --- .../TestData/After Feature Failure.feature | 2 +- .../TestData/Invalid Steps.feature | 2 +- .../IntegrationTests.cs | 114 +-- .../Allure.SpecFlowPlugin.csproj | 1 + Allure.SpecFlowPlugin/AllureBindingInvoker.cs | 622 +++++++++----- Allure.SpecFlowPlugin/AllureBindings.cs | 80 +- Allure.SpecFlowPlugin/AllurePlugin.cs | 21 +- .../AllureTestTracerWrapper.cs | 167 ++-- Allure.SpecFlowPlugin/PluginConfiguration.cs | 46 +- Allure.SpecFlowPlugin/PluginHelper.cs | 762 +++++++++++------- 10 files changed, 1145 insertions(+), 672 deletions(-) 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/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.SpecFlowPlugin.Tests/IntegrationTests.cs b/Allure.SpecFlowPlugin.Tests/IntegrationTests.cs index acf80f25..af351499 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 unknown scenario for testing an exception in AfterFeature + scenariosByStatus["broken"].Add("Unknown"); } - 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 019ff73d..feb9ea18 100644 --- a/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj +++ b/Allure.SpecFlowPlugin/Allure.SpecFlowPlugin.csproj @@ -3,6 +3,7 @@ net462;netstandard2.0 11 + enable Allure.SpecFlow 2.10-SNAPSHOT Alexander Bakanov diff --git a/Allure.SpecFlowPlugin/AllureBindingInvoker.cs b/Allure.SpecFlowPlugin/AllureBindingInvoker.cs index 71211c0d..3aa9d9bb 100644 --- a/Allure.SpecFlowPlugin/AllureBindingInvoker.cs +++ b/Allure.SpecFlowPlugin/AllureBindingInvoker.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; using Allure.Net.Commons; -using CsvHelper; using TechTalk.SpecFlow; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Configuration; @@ -12,245 +7,438 @@ 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, null); - 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..9a0d5206 100644 --- a/Allure.SpecFlowPlugin/PluginHelper.cs +++ b/Allure.SpecFlowPlugin/PluginHelper.cs @@ -1,350 +1,540 @@ -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 Allure.Net.Commons.Storage; +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 ScenarioInfo emptyScenarioInfo = new( + "Unknown", + string.Empty, + Array.Empty(), + new OrderedDictionary() + ); + + private static readonly FeatureInfo emptyFeatureInfo = new( + CultureInfo.CurrentCulture, + string.Empty, + string.Empty, + string.Empty + ); + + 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; + } - private static readonly FeatureInfo emptyFeatureInfo = new FeatureInfo( - CultureInfo.CurrentCulture, string.Empty, string.Empty, string.Empty); + internal static string GetFeatureContainerId( + FeatureInfo featureInfo + ) => featureInfo != null + ? featureInfo.GetHashCode().ToString() + : emptyFeatureInfo.GetHashCode().ToString(); + + internal static string NewId() => Guid.NewGuid().ToString("N"); + + internal static FixtureResult GetFixtureResult(HookBinding hook) => + new() + { + name = $"{hook.Method.Name} [{hook.HookOrder}]" + }; + + internal static void StartTestContainer() => + AllureLifecycle.Instance.StartTestContainer(new() + { + uuid = NewId() + }); + + internal static void StartTestCase( + FeatureInfo featureInfo, + ScenarioInfo? scenarioInfo + ) + { + featureInfo ??= emptyFeatureInfo; + 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