diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a916eab..f86bbc0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # [vNext] ## Improvements: +* Added support for asynchronously disposing objects implementing IAsyncDisposable when the Reqnroll object container is disposed. ## Bug fixes: -*Contributors of this release (in alphabetical order):* +*Contributors of this release (in alphabetical order):* @Code-Grump # v3.1.0 - 2025-09-26 @@ -14,7 +15,7 @@ * Disabling parallel execution with the `addNonParallelizableMarkerForTags` efature now also applies to scenario-level tags for frameworks supporting method-level isolation (NUnit, MsTest V2, TUnit). (#826) * Generating "friendly names" for generated test methods by default can be disabled by the `generator/disableFriendlyTestNames` setting in `reqnroll.json`. This can help to avoid compatiblity issues with tools like VsTest retry. For MsTest this setting restores the behavior of Reqnroll v2. (#854) -## Improvements: +* Dependencies: Updated to Cucumber Gherkin v35.0.0, Cucumber Messages v29.0.0 and Cucumber CompatibilityKit v23.0.0 * Reqnroll.Verify: Support for Verify v29+ (Verify.Xunit v29.0.0 or later). For earlier versions use 3.0.3 version of the plugin that is compatible with Reqnroll v3.*. The support for custom snapshot files with global VerifySettings has been removed, see [plugin documentation](https://docs.reqnroll.net/latest/integrations/verify.html) for details and workarounds. (#572) * Dependencies: Updated to Cucumber Gherkin v35, Cucumber Messages v29 and Cucumber CompatibilityKit v23 (#841) diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 3f2f53bdb..0a08b0e0d 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -4,7 +4,7 @@ public class GeneratorConstants { public const string DEFAULT_NAMESPACE = "ReqnrollTests"; public const string TEST_NAME_FORMAT = "{0}"; - public const string SCENARIO_INITIALIZE_NAME = "ScenarioInitialize"; + public const string SCENARIO_INITIALIZE_NAME = "ScenarioInitializeAsync"; public const string SCENARIO_START_NAME = "ScenarioStartAsync"; public const string SCENARIO_CLEANUP_NAME = "ScenarioCleanupAsync"; public const string TEST_INITIALIZE_NAME = "TestInitializeAsync"; diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index d110f9b12..0438146c1 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -521,14 +521,19 @@ private void SetupScenarioInitializeMethod(TestClassGenerationContext generation scenarioInitializeMethod.Parameters.Add( new CodeParameterDeclarationExpression(new CodeTypeReference(typeof(RuleInfo), CodeTypeReferenceOptions.GlobalReference), "ruleInfo")); + _codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioInitializeMethod); + //testRunner.OnScenarioInitialize(scenarioInfo, ruleInfo); var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); - scenarioInitializeMethod.Statements.Add( - new CodeMethodInvokeExpression( - testRunnerField, - nameof(ITestRunner.OnScenarioInitialize), - new CodeVariableReferenceExpression("scenarioInfo"), - new CodeVariableReferenceExpression("ruleInfo"))); + var expression = new CodeMethodInvokeExpression( + testRunnerField, + nameof(ITestRunner.OnScenarioInitializeAsync), + new CodeVariableReferenceExpression("scenarioInfo"), + new CodeVariableReferenceExpression("ruleInfo")); + + _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); + + scenarioInitializeMethod.Statements.Add(expression); } private void SetupScenarioStartMethod(TestClassGenerationContext generationContext) diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index fb82d3573..558ededa4 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -346,12 +346,15 @@ internal void GenerateScenarioInitializeCall(TestClassGenerationContext generati using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, scenario.Location)) { - statements.Add(new CodeExpressionStatement( - new CodeMethodInvokeExpression( - new CodeThisReferenceExpression(), - generationContext.ScenarioInitializeMethod.Name, - new CodeVariableReferenceExpression("scenarioInfo"), - new CodeVariableReferenceExpression("ruleInfo")))); + var callScenarioInitializeExpression = new CodeMethodInvokeExpression( + new CodeThisReferenceExpression(), + generationContext.ScenarioInitializeMethod.Name, + new CodeVariableReferenceExpression("scenarioInfo"), + new CodeVariableReferenceExpression("ruleInfo")); + + _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(callScenarioInitializeExpression); + + statements.Add(new CodeExpressionStatement(callScenarioInitializeExpression)); } testMethod.Statements.AddRange(statements.ToArray()); diff --git a/Reqnroll.Tools.MsBuild.Generation/AsyncRunner.cs b/Reqnroll.Tools.MsBuild.Generation/AsyncRunner.cs new file mode 100644 index 000000000..03032d5e7 --- /dev/null +++ b/Reqnroll.Tools.MsBuild.Generation/AsyncRunner.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +#nullable enable + +namespace Reqnroll.Tools.MsBuild.Generation; + +internal static class AsyncRunner +{ + +#if NETFRAMEWORK + private static Func>, T>? TryCreateRunDelegate(object joinableTaskFactory) + { + // Find the Run method + var runMethod = joinableTaskFactory.GetType().GetMethods() + .FirstOrDefault(m => m.Name == "Run" && + m.IsGenericMethod && + m.GetParameters().Length == 1 && + typeof(Func>).IsAssignableFrom(m.GetParameters()[0].ParameterType.GetGenericArguments()[0])); + + if (runMethod == null) + { + return null; + } + + var genericRun = runMethod.MakeGenericMethod(typeof(T)); + + return (Func>, T>) + genericRun.CreateDelegate(typeof(Func>, T>)); + } + +#endif + + /// + /// Runs an asynchronous function and blocks until it completes, returning the result. + /// + /// The type returned by the function. + /// The function to invoke. + /// The value returned by the function. + public static T RunAndJoin(Func> func) + { +#if NETFRAMEWORK + // If we're running in Visual Studio, we want use its JoinableTaskFactory to avoid deadlocks. + // We can't guarantee the version of Visual Studio, so we use reflection to try to find + // ThreadHelper.JoinableTaskFactory at runtime + var threadHelperType = Type.GetType( + "Microsoft.VisualStudio.Shell.ThreadHelper, Microsoft.VisualStudio.Shell.15.0", + throwOnError: false); + + if (threadHelperType != null) + { + var joinableTaskFactoryProperty = threadHelperType.GetProperty( + "JoinableTaskFactory", + BindingFlags.Public | BindingFlags.Static); + + var joinableTaskFactory = joinableTaskFactoryProperty?.GetValue(null); + if (joinableTaskFactory != null) + { + var runDelegate = TryCreateRunDelegate(joinableTaskFactory); + if (runDelegate != null) + { + return runDelegate(joinableTaskFactory, func); + } + } + } + +#endif +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return func().GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } +} diff --git a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTask.cs b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTask.cs index 0276ef88f..b9002dea1 100644 --- a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTask.cs +++ b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTask.cs @@ -7,10 +7,11 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Reqnroll.Tools.MsBuild.Generation; -public class GenerateFeatureFileCodeBehindTask : Task +public class GenerateFeatureFileCodeBehindTask : Microsoft.Build.Utilities.Task { public const string CodeBehindFileMetadata = "CodeBehindFile"; //in public const string MessagesFileMetadata = "MessagesFile"; //in,out @@ -40,7 +41,9 @@ public class GenerateFeatureFileCodeBehindTask : Task public bool LaunchDebugger { get; set; } - public override bool Execute() + public override bool Execute() => AsyncRunner.RunAndJoin(ExecuteAsync); + + public async Task ExecuteAsync() { if (LaunchDebugger) Debugger.Launch(); @@ -55,20 +58,25 @@ public override bool Execute() var reqnrollProjectInfo = new ReqnrollProjectInfo(generatorPlugins, featureFiles, ProjectPath, ProjectFolder, ProjectGuid, AssemblyName, OutputPath, RootNamespace, TargetFrameworks, TargetFramework); var dependencyCustomizations = DependencyCustomizations ?? new NullGenerateFeatureFileCodeBehindTaskDependencyCustomizations(); - using var taskRootContainer = generateFeatureFileCodeBehindTaskContainerBuilder.BuildRootContainer(Log, reqnrollProjectInfo, msbuildInformationProvider, dependencyCustomizations); + await using var taskRootContainer = generateFeatureFileCodeBehindTaskContainerBuilder.BuildRootContainer( + Log, + reqnrollProjectInfo, + msbuildInformationProvider, + dependencyCustomizations); + var assemblyResolveLoggerFactory = taskRootContainer.Resolve(); using (assemblyResolveLoggerFactory.Build()) { var taskExecutor = taskRootContainer.Resolve(); - var executeResult = taskExecutor.Execute(); + var executeResult = await taskExecutor.ExecuteAsync(); if (executeResult is not ISuccess> success) { return false; } - GeneratedFiles = success.Result.ToArray(); + GeneratedFiles = [.. success.Result]; return true; } diff --git a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskExecutor.cs b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskExecutor.cs index 26865832b..16ae56ea9 100644 --- a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskExecutor.cs +++ b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskExecutor.cs @@ -1,12 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using Reqnroll.BoDi; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Reqnroll.BoDi; using Reqnroll.CommonModels; using Reqnroll.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Task = System.Threading.Tasks.Task; namespace Reqnroll.Tools.MsBuild.Generation; @@ -22,7 +23,7 @@ public class GenerateFeatureFileCodeBehindTaskExecutor( IExceptionTaskLogger exceptionTaskLogger) : IGenerateFeatureFileCodeBehindTaskExecutor { - public IResult> Execute() + public async Task>> ExecuteAsync() { processInfoDumper.DumpProcessInfo(); log.LogTaskMessage("Starting GenerateFeatureFileCodeBehind task"); @@ -31,7 +32,7 @@ public IResult> Execute() { var reqnrollProject = reqnrollProjectProvider.GetReqnrollProject(); - using var generatorContainer = wrappedGeneratorContainerBuilder.BuildGeneratorContainer( + await using var generatorContainer = wrappedGeneratorContainerBuilder.BuildGeneratorContainer( reqnrollProject.ProjectSettings.ConfigurationHolder, reqnrollProject.ProjectSettings, reqnrollProjectInfo.GeneratorPlugins, diff --git a/Reqnroll.Tools.MsBuild.Generation/IGenerateFeatureFileCodeBehindTaskExecutor.cs b/Reqnroll.Tools.MsBuild.Generation/IGenerateFeatureFileCodeBehindTaskExecutor.cs index dcfb1f96f..cb23dd3d2 100644 --- a/Reqnroll.Tools.MsBuild.Generation/IGenerateFeatureFileCodeBehindTaskExecutor.cs +++ b/Reqnroll.Tools.MsBuild.Generation/IGenerateFeatureFileCodeBehindTaskExecutor.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; using Microsoft.Build.Framework; using Reqnroll.CommonModels; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Reqnroll.Tools.MsBuild.Generation { public interface IGenerateFeatureFileCodeBehindTaskExecutor { - IResult> Execute(); + Task>> ExecuteAsync(); } } diff --git a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.csproj b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.csproj index 7e66bd999..e9ff45aae 100644 --- a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.csproj +++ b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.csproj @@ -85,7 +85,6 @@ Microsoft.Build.Utilities.Core - diff --git a/Reqnroll/BoDi/IObjectContainer.cs b/Reqnroll/BoDi/IObjectContainer.cs index a9651afc0..fdda73ba6 100644 --- a/Reqnroll/BoDi/IObjectContainer.cs +++ b/Reqnroll/BoDi/IObjectContainer.cs @@ -3,7 +3,7 @@ namespace Reqnroll.BoDi; -public interface IObjectContainer : IDisposable +public interface IObjectContainer : IAsyncDisposable { /// /// Fired when a new object is created directly by the container. It is not invoked for resolving instance and factory registrations. diff --git a/Reqnroll/BoDi/ObjectContainer.cs b/Reqnroll/BoDi/ObjectContainer.cs index 1c5fc60b1..b94576997 100644 --- a/Reqnroll/BoDi/ObjectContainer.cs +++ b/Reqnroll/BoDi/ObjectContainer.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; namespace Reqnroll.BoDi; @@ -744,15 +745,31 @@ private void AssertNotDisposed() throw new ObjectContainerException("Object container disposed", null); } - public void Dispose() + public async ValueTask DisposeAsync() { if (_isDisposed) + { return; + } _isDisposed = true; - foreach (var obj in _objectPool.Values.OfType().Where(o => !ReferenceEquals(o, this))) - obj.Dispose(); + foreach (var obj in _objectPool.Values) + { + if (ReferenceEquals(obj, this)) + { + continue; + } + + if (obj is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else if (obj is IDisposable disposable) + { + disposable.Dispose(); + } + } _objectPool.Clear(); _registrations.Clear(); diff --git a/Reqnroll/ITestRunner.cs b/Reqnroll/ITestRunner.cs index 766b12c32..d87c8422b 100644 --- a/Reqnroll/ITestRunner.cs +++ b/Reqnroll/ITestRunner.cs @@ -18,7 +18,7 @@ public interface ITestRunner Task OnFeatureStartAsync(FeatureInfo featureInfo); Task OnFeatureEndAsync(); - void OnScenarioInitialize(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); + Task OnScenarioInitializeAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); Task OnScenarioStartAsync(); Task CollectScenarioErrorsAsync(); diff --git a/Reqnroll/Infrastructure/ContextManager.cs b/Reqnroll/Infrastructure/ContextManager.cs index 34fda0dda..dd9be4cc5 100644 --- a/Reqnroll/Infrastructure/ContextManager.cs +++ b/Reqnroll/Infrastructure/ContextManager.cs @@ -5,52 +5,60 @@ using Reqnroll.Bindings; using Reqnroll.Configuration; using Reqnroll.Tracing; +using System.Threading.Tasks; namespace Reqnroll.Infrastructure; -public class ContextManager : IContextManager, IDisposable +public class ContextManager : IContextManager, IAsyncDisposable { - private class InternalContextManager(ITestTracer testTracer) : IDisposable + private class InternalContextManager(ITestTracer testTracer) : IAsyncDisposable where TContext : ReqnrollContext { private IObjectContainer _objectContainer; public TContext Instance { get; private set; } - public void Init(TContext newInstance, IObjectContainer newObjectContainer) + public async Task InitAsync(TContext newInstance, IObjectContainer newObjectContainer) { if (Instance != null) { testTracer.TraceWarning($"The previous {typeof(TContext).Name} was not disposed."); - DisposeInstance(); + await DisposeInstanceAsync(); } Instance = newInstance; _objectContainer = newObjectContainer; } - public void Cleanup() + public ValueTask CleanupAsync() { if (Instance == null) { testTracer.TraceWarning($"The previous {typeof(TContext).Name} was already disposed."); - return; + return default; } - DisposeInstance(); + + return DisposeInstanceAsync(); } - private void DisposeInstance() + private async ValueTask DisposeInstanceAsync() { - _objectContainer?.Dispose(); + if (_objectContainer != null) + { + await _objectContainer.DisposeAsync(); + } + Instance = null; _objectContainer = null; } - public void Dispose() + public ValueTask DisposeAsync() { if (Instance != null) { - DisposeInstance(); + return DisposeInstanceAsync(); } + + return default; } } @@ -151,32 +159,32 @@ private void InitializeTestThreadContext() TestThreadContext = testThreadContext; } - public void InitializeFeatureContext(FeatureInfo featureInfo) + public async Task InitializeFeatureContextAsync(FeatureInfo featureInfo) { var featureContainer = _containerBuilder.CreateFeatureContainer(_testThreadContainer, featureInfo); var reqnrollConfiguration = _testThreadContainer.Resolve(); var newContext = new FeatureContext(featureContainer, featureInfo, reqnrollConfiguration); // make sure that the FeatureContext can also be resolved through the interface as well RegisterInstanceAsInterfaceAndObjectType(featureContainer, newContext, dispose: true); - _featureContextManager.Init(newContext, featureContainer); + await _featureContextManager.InitAsync(newContext, featureContainer); #pragma warning disable 618 FeatureContext.Current = newContext; #pragma warning restore 618 } - public void CleanupFeatureContext() + public ValueTask CleanupFeatureContextAsync() { - _featureContextManager.Cleanup(); + return _featureContextManager.CleanupAsync(); } - public void InitializeScenarioContext(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) + public async Task InitializeScenarioContextAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) { var scenarioContainer = _containerBuilder.CreateScenarioContainer(FeatureContext.FeatureContainer, scenarioInfo); var testObjectResolver = scenarioContainer.Resolve(); var newContext = new ScenarioContext(scenarioContainer, scenarioInfo, ruleInfo, testObjectResolver); // make sure that the ScenarioContext can also be resolved through the interface as well RegisterInstanceAsInterfaceAndObjectType(scenarioContainer, newContext, dispose: true); - _scenarioContextManager.Init(newContext, scenarioContainer); + await _scenarioContextManager.InitAsync(newContext, scenarioContainer); #pragma warning disable 618 ScenarioContext.Current = newContext; #pragma warning restore 618 @@ -191,9 +199,9 @@ private void ResetCurrentStepStack() ScenarioStepContext.Current = null; } - public void CleanupScenarioContext() + public ValueTask CleanupScenarioContextAsync() { - _scenarioContextManager.Cleanup(); + return _scenarioContextManager.CleanupAsync(); } public void InitializeStepContext(StepInfo stepInfo) @@ -212,10 +220,18 @@ public void CleanupStepContext() // we do not reset CurrentTopLevelStepDefinitionType in order to "remember" last top level type for "And" and "But" steps } - public void Dispose() + public async ValueTask DisposeAsync() { - _featureContextManager?.Dispose(); - _scenarioContextManager?.Dispose(); + if (_featureContextManager != null) + { + await _featureContextManager.DisposeAsync(); + } + + if (_scenarioContextManager != null) + { + await _scenarioContextManager.DisposeAsync(); + } + _stepContextManager?.Dispose(); } } \ No newline at end of file diff --git a/Reqnroll/Infrastructure/IContextManager.cs b/Reqnroll/Infrastructure/IContextManager.cs index fba5919fd..3201dfa29 100644 --- a/Reqnroll/Infrastructure/IContextManager.cs +++ b/Reqnroll/Infrastructure/IContextManager.cs @@ -1,4 +1,5 @@ using Reqnroll.Bindings; +using System.Threading.Tasks; namespace Reqnroll.Infrastructure { @@ -10,11 +11,11 @@ public interface IContextManager ScenarioStepContext StepContext { get; } StepDefinitionType? CurrentTopLevelStepDefinitionType { get; } - void InitializeFeatureContext(FeatureInfo featureInfo); - void CleanupFeatureContext(); + Task InitializeFeatureContextAsync(FeatureInfo featureInfo); + ValueTask CleanupFeatureContextAsync(); - void InitializeScenarioContext(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); - void CleanupScenarioContext(); + Task InitializeScenarioContextAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); + ValueTask CleanupScenarioContextAsync(); void InitializeStepContext(StepInfo stepInfo); void CleanupStepContext(); diff --git a/Reqnroll/Infrastructure/ITestExecutionEngine.cs b/Reqnroll/Infrastructure/ITestExecutionEngine.cs index 0ebe77e73..fd9386ddd 100644 --- a/Reqnroll/Infrastructure/ITestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/ITestExecutionEngine.cs @@ -15,7 +15,7 @@ public interface ITestExecutionEngine Task OnFeatureStartAsync(FeatureInfo featureInfo); Task OnFeatureEndAsync(); - void OnScenarioInitialize(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); + Task OnScenarioInitializeAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo); Task OnScenarioStartAsync(); Task OnAfterLastStepAsync(); Task OnScenarioEndAsync(); diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index 5f6bfe88d..3fcfda7da 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -142,7 +142,7 @@ public virtual async Task OnTestRunEndAsync() public virtual async Task OnFeatureStartAsync(FeatureInfo featureInfo) { - _contextManager.InitializeFeatureContext(featureInfo); + await _contextManager.InitializeFeatureContextAsync(featureInfo); await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureStartedEvent(FeatureContext)); @@ -177,13 +177,13 @@ public virtual async Task OnFeatureEndAsync() await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureFinishedEvent(FeatureContext)); - _contextManager.CleanupFeatureContext(); + await _contextManager.CleanupFeatureContextAsync(); } } - public virtual void OnScenarioInitialize(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) + public virtual Task OnScenarioInitializeAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) { - _contextManager.InitializeScenarioContext(scenarioInfo, ruleInfo); + return _contextManager.InitializeScenarioContextAsync(scenarioInfo, ruleInfo); } public virtual async Task OnScenarioStartAsync() @@ -262,7 +262,7 @@ public virtual async Task OnScenarioEndAsync() { await _testThreadExecutionEventPublisher.PublishEventAsync(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); - _contextManager.CleanupScenarioContext(); + await _contextManager.CleanupScenarioContextAsync(); } } diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index 7b79456f5..c13988a96 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -37,9 +37,9 @@ public async Task OnFeatureEndAsync() await _executionEngine.OnFeatureEndAsync(); } - public void OnScenarioInitialize(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) + public async Task OnScenarioInitializeAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo) { - _executionEngine.OnScenarioInitialize(scenarioInfo, ruleInfo); + await _executionEngine.OnScenarioInitializeAsync(scenarioInfo, ruleInfo); } public async Task OnScenarioStartAsync() diff --git a/Reqnroll/TestRunnerManager.cs b/Reqnroll/TestRunnerManager.cs index fe1eb9ab7..f9cae6893 100644 --- a/Reqnroll/TestRunnerManager.cs +++ b/Reqnroll/TestRunnerManager.cs @@ -355,7 +355,7 @@ public virtual async Task DisposeAsync() { foreach (var item in testWorkerContainers) { - item.Key.Dispose(); + await item.Key.DisposeAsync(); _availableTestWorkerContainers.TryRemove(item.Key, out _); } testWorkerContainers = _availableTestWorkerContainers.ToArray(); @@ -369,7 +369,7 @@ public virtual async Task DisposeAsync() } // this call dispose on this object, but _wasDisposed will avoid double execution - _globalContainer.Dispose(); + await _globalContainer.DisposeAsync(); OnTestRunnerManagerDisposed(this); diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs index 2fbbf6fe0..3fbde9072 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs @@ -394,7 +394,7 @@ public void XUnit2TestGeneratorProvider_Should_register_testOutputHelper_on_scen // ASSERT code.Should().NotBeNull(); - var scenarioStartMethod = code.Class().Members().Single(m => m.Name == @"ScenarioInitialize"); + var scenarioStartMethod = code.Class().Members().Single(m => m.Name == @"ScenarioInitializeAsync"); scenarioStartMethod.Statements.Count.Should().Be(2); var expression = scenarioStartMethod.Statements[1].Should().BeOfType() diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 4b77d1970..5963128e8 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -204,7 +204,7 @@ private async Task PerformStepExecution(string methodName, strin await engine.OnTestRunStartAsync(); await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo(culture), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); await engine.OnScenarioStartAsync(); - engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null), null); + await engine.OnScenarioInitializeAsync(new ScenarioInfo("Sample scenario", null, null, null), null); await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); var contextManager = testThreadContainer.Resolve(); diff --git a/Tests/Reqnroll.RuntimeTests/BoDi/DisposeTests.cs b/Tests/Reqnroll.RuntimeTests/BoDi/DisposeTests.cs index 9d581bd46..26dbd996f 100644 --- a/Tests/Reqnroll.RuntimeTests/BoDi/DisposeTests.cs +++ b/Tests/Reqnroll.RuntimeTests/BoDi/DisposeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using FluentAssertions; using Reqnroll.BoDi; using Xunit; @@ -8,68 +9,68 @@ namespace Reqnroll.RuntimeTests.BoDi public class DisposeTests { [Fact] - public void ContainerShouldBeDisposable() + public void ContainerShouldBeAsyncDisposable() { var container = new ObjectContainer(); - container.Should().BeAssignableTo(); + container.Should().BeAssignableTo(); } [Fact] - public void ContainerShouldThrowExceptionWhenDisposedAndCallingResolve() + public async Task ContainerShouldThrowExceptionWhenAsyncDisposedAndCallingResolve() { var container = new ObjectContainer(); - container.Dispose(); + await container.DisposeAsync(); Action act = () => container.Resolve(); act.Should().ThrowExactly("Object container disposed"); } [Fact] - public void ContainerShouldThrowExceptionWhenDisposedAndCallingRegisterInstanceAs() + public async Task ContainerShouldThrowExceptionWhenAsyncDisposedAndCallingRegisterInstanceAs() { var container = new ObjectContainer(); - container.Dispose(); + await container.DisposeAsync(); - Action act = () => container.RegisterInstanceAs(new DisposableClass1()); + Action act = () => container.RegisterInstanceAs(new AsyncDisposableClass1()); act.Should().ThrowExactly("Object container disposed"); } [Fact] - public void ContainerShouldThrowExceptionWhenDisposedAndCallingRegisterFactoryAs() + public async Task ContainerShouldThrowExceptionWhenAsyncDisposedAndCallingRegisterFactoryAs() { var container = new ObjectContainer(); - container.Dispose(); + await container.DisposeAsync(); - Action act = () => container.RegisterFactoryAs(() => new DisposableClass1()); + Action act = () => container.RegisterFactoryAs(() => new AsyncDisposableClass1()); act.Should().ThrowExactly("Object container disposed"); } [Fact] - public void ContainerShouldThrowExceptionWhenDisposedAndCallingRegisterTypeAs() + public async Task ContainerShouldThrowExceptionWhenAsyncDisposedAndCallingRegisterTypeAs() { var container = new ObjectContainer(); - container.Dispose(); + await container.DisposeAsync(); - Action act = () => container.RegisterTypeAs(typeof(DisposableClass1)); + Action act = () => container.RegisterTypeAs(typeof(AsyncDisposableClass1)); act.Should().ThrowExactly("Object container disposed"); } [Fact] - public void ShouldDisposeCreatedObjects() + public async Task ShouldDisposeCreatedObjects() { var container = new ObjectContainer(); container.RegisterTypeAs(); var obj = container.Resolve(); - container.Dispose(); + await container.DisposeAsync(); obj.WasDisposed.Should().BeTrue(); } [Fact] - public void ShouldDisposeInstanceRegistrations() + public async Task ShouldDisposeInstanceRegistrations() { var container = new ObjectContainer(); var obj = new DisposableClass1(); @@ -77,13 +78,13 @@ public void ShouldDisposeInstanceRegistrations() container.Resolve(); - container.Dispose(); + await container.DisposeAsync(); obj.WasDisposed.Should().BeTrue(); } [Fact] - public void ShouldNotDisposeObjectsRegisteredAsInstance() + public async Task ShouldNotDisposeObjectsRegisteredAsInstance() { var container = new ObjectContainer(); var obj = new DisposableClass1(); @@ -91,13 +92,13 @@ public void ShouldNotDisposeObjectsRegisteredAsInstance() container.Resolve(); - container.Dispose(); + await container.DisposeAsync(); obj.WasDisposed.Should().BeFalse(); } [Fact] - public void ShouldNotDisposeObjectsFromBaseContainer() + public async Task ShouldNotDisposeObjectsFromBaseContainer() { var baseContainer = new ObjectContainer(); baseContainer.RegisterTypeAs(); @@ -106,9 +107,69 @@ public void ShouldNotDisposeObjectsFromBaseContainer() baseContainer.Resolve(); var obj = container.Resolve(); - container.Dispose(); + await container.DisposeAsync(); obj.WasDisposed.Should().BeFalse(); } + + [Fact] + public async Task ShouldAsyncDisposeCreatedObjects() + { + var container = new ObjectContainer(); + container.RegisterTypeAs(); + + var obj = container.Resolve(); + + await container.DisposeAsync(); + + obj.WasDisposed.Should().BeFalse(); + obj.WasDisposedAsync.Should().BeTrue(); + } + + [Fact] + public async Task ShouldAsyncDisposeInstanceRegistrations() + { + var container = new ObjectContainer(); + var obj = new AsyncDisposableClass1(); + container.RegisterInstanceAs(obj, dispose: true); + + container.Resolve(); + + await container.DisposeAsync(); + + obj.WasDisposed.Should().BeFalse(); + obj.WasDisposedAsync.Should().BeTrue(); + } + + [Fact] + public async Task ShouldNotAsyncDisposeObjectsRegisteredAsInstance() + { + var container = new ObjectContainer(); + var obj = new AsyncDisposableClass1(); + container.RegisterInstanceAs(obj); + + container.Resolve(); + + await container.DisposeAsync(); + + obj.WasDisposed.Should().BeFalse(); + obj.WasDisposedAsync.Should().BeFalse(); + } + + [Fact] + public async Task ShouldNotAsyncDisposeObjectsFromBaseContainer() + { + var baseContainer = new ObjectContainer(); + baseContainer.RegisterTypeAs(); + var container = new ObjectContainer(baseContainer); + + baseContainer.Resolve(); + var obj = container.Resolve(); + + await container.DisposeAsync(); + + obj.WasDisposed.Should().BeFalse(); + obj.WasDisposedAsync.Should().BeFalse(); + } } } diff --git a/Tests/Reqnroll.RuntimeTests/BoDi/TestClasses.cs b/Tests/Reqnroll.RuntimeTests/BoDi/TestClasses.cs index 133d45df5..6e764a7b6 100644 --- a/Tests/Reqnroll.RuntimeTests/BoDi/TestClasses.cs +++ b/Tests/Reqnroll.RuntimeTests/BoDi/TestClasses.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Reqnroll.RuntimeTests.BoDi { @@ -185,7 +186,7 @@ public interface IDisposableClass bool WasDisposed { get; } } - public class DisposableClass1 : IDisposableClass, IDisposable + public sealed class DisposableClass1 : IDisposableClass, IDisposable { public bool WasDisposed { get; private set; } @@ -195,6 +196,30 @@ public void Dispose() } } + public interface IAsyncDisposableClass + { + bool WasDisposed { get; } + bool WasDisposedAsync { get; } + } + + public sealed class AsyncDisposableClass1 : IAsyncDisposableClass, IDisposable, IAsyncDisposable + { + public bool WasDisposed { get; private set; } + + public bool WasDisposedAsync { get; private set; } + + public void Dispose() + { + WasDisposed = true; + } + + public ValueTask DisposeAsync() + { + WasDisposedAsync = true; + return ValueTask.CompletedTask; + } + } + public enum MyEnumKey { One, diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ContextManagerTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ContextManagerTests.cs index 67c43cdf7..ba8873d4c 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ContextManagerTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ContextManagerTests.cs @@ -4,6 +4,7 @@ using Reqnroll.Infrastructure; using Reqnroll.Tracing; using System.Globalization; +using System.Threading.Tasks; using Xunit; namespace Reqnroll.RuntimeTests.Infrastructure; @@ -15,15 +16,15 @@ public ContextManager CreateContextManager(IObjectContainer testThreadContainer return new ContextManager(new Mock().Object, testThreadContainer ?? TestThreadContainer, ContainerBuilderStub); } - private static void InitializeFeatureContext(ContextManager sut) + private static Task InitializeFeatureContext(ContextManager sut) { - sut.InitializeFeatureContext(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "F", null)); + return sut.InitializeFeatureContextAsync(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "F", null)); } - private void InitializeScenarioContext(ContextManager sut) + private Task InitializeScenarioContext(ContextManager sut) { InitializeFeatureContext(sut); - sut.InitializeScenarioContext(new ScenarioInfo("the next scenario", "description of the next scenario", null, null), null); + return sut.InitializeScenarioContextAsync(new ScenarioInfo("the next scenario", "description of the next scenario", null, null), null); } [Fact] @@ -52,7 +53,7 @@ public void Should_dispose_scenario_context_when_scenario_context_cleaned_up() var scenarioContext = sut.ScenarioContext; scenarioContext.Should().NotBeNull(); - sut.CleanupScenarioContext(); + sut.CleanupScenarioContextAsync(); scenarioContext.IsDisposed.Should().BeTrue(); } @@ -98,7 +99,7 @@ public void Should_dispose_feature_context_when_feature_context_cleaned_up() var featureContext = sut.FeatureContext; featureContext.Should().NotBeNull(); - sut.CleanupFeatureContext(); + sut.CleanupFeatureContextAsync(); featureContext.IsDisposed.Should().BeTrue(); } @@ -149,7 +150,7 @@ public void Should_be_able_to_resolve_test_thread_context_from_scenario_containe } [Fact] - public void Should_dispose_test_thread_context_when_test_thread_context_cleaned_up() + public async Task Should_dispose_test_thread_context_when_test_thread_context_cleaned_up() { var sut = CreateContextManager(); // the test thread context is already initialized in the constructor of ContextManager @@ -158,7 +159,7 @@ public void Should_dispose_test_thread_context_when_test_thread_context_cleaned_ testThreadContext.Should().NotBeNull(); var testThreadContainer = testThreadContext.TestThreadContainer; - testThreadContainer.Dispose(); + await testThreadContainer.DisposeAsync(); testThreadContext.IsDisposed.Should().BeTrue(); } diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs index d76734408..b3fc450bf 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs @@ -698,11 +698,11 @@ public async Task Should_cleanup_scenario_context_on_scenario_end() var testExecutionEngine = CreateTestExecutionEngine(); RegisterStepDefinition(); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); await testExecutionEngine.OnScenarioEndAsync(); - _contextManagerStub.Verify(cm => cm.CleanupScenarioContext(), Times.Once); + _contextManagerStub.Verify(cm => cm.CleanupScenarioContextAsync(), Times.Once); } [Fact] @@ -717,12 +717,12 @@ public async Task Should_cleanup_scenario_context_after_AfterScenario_hook_error .Throws(new Exception("simulated error")); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); Func act = async () => await testExecutionEngine.OnScenarioEndAsync(); await act.Should().ThrowAsync().WithMessage("simulated error"); - _contextManagerStub.Verify(cm => cm.CleanupScenarioContext(), Times.Once); + _contextManagerStub.Verify(cm => cm.CleanupScenarioContextAsync(), Times.Once); } [Fact] @@ -802,7 +802,7 @@ public async Task Should_resolve_BeforeAfterScenario_hook_parameter_from_scenari var beforeHook = CreateParametrizedHookMock(_beforeScenarioEvents, typeof(DummyClass)); var afterHook = CreateParametrizedHookMock(_afterScenarioEvents, typeof(DummyClass)); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); await testExecutionEngine.OnScenarioEndAsync(); @@ -826,7 +826,7 @@ public async Task Should_be_possible_to_register_instance_in_scenario_container_ It.IsAny(),It.IsAny(), It.IsAny())) .Callback(() => actualInstance = testExecutionEngine.ScenarioContext.ScenarioContainer.Resolve()); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); testExecutionEngine.ScenarioContext.ScenarioContainer.RegisterInstanceAs(instanceToAddBeforeScenarioEventFiring); await testExecutionEngine.OnScenarioStartAsync(); actualInstance.Should().BeSameAs(instanceToAddBeforeScenarioEventFiring); @@ -884,7 +884,7 @@ await FluentActions.Awaiting(testExecutionEngine.OnFeatureEndAsync) .Should().ThrowAsync("execution of the step should have failed because of the exception thrown by the before scenario block hook"); _methodBindingInvokerMock.Verify(i => i.InvokeBindingAsync(hookMock.Object, _contextManagerStub.Object, null, _testTracerStub.Object, It.IsAny()), Times.Once()); - _contextManagerStub.Verify(cm => cm.CleanupFeatureContext()); + _contextManagerStub.Verify(cm => cm.CleanupFeatureContextAsync()); } [Fact] diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadContextTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadContextTests.cs index c20c20cc8..bda461919 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadContextTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadContextTests.cs @@ -4,6 +4,7 @@ using Xunit; using Reqnroll.Infrastructure; using Reqnroll.Tracing; +using System.Threading.Tasks; namespace Reqnroll.RuntimeTests.Infrastructure { @@ -36,7 +37,7 @@ public void Should_expose_the_test_thread_container() } [Fact] - public void Should_disposing_event_fired_when_test_thread_container_disposes() + public async Task Should_disposing_event_fired_when_test_thread_container_disposes() { bool wasDisposingFired = false; ContextManagerStub.TestThreadContext.Should().NotBeNull(); @@ -46,21 +47,21 @@ public void Should_disposing_event_fired_when_test_thread_container_disposes() wasDisposingFired = true; }; - TestThreadContainer.Dispose(); + await TestThreadContainer.DisposeAsync(); wasDisposingFired.Should().BeTrue(); } [Fact] - public void Should_be_able_to_resolve_from_scenario_container() + public async Task Should_be_able_to_resolve_from_scenario_container() { // this basically tests the special registration in DefaultDependencyProvider var containerBuilder = new RuntimeTestsContainerBuilder(); var testThreadContainer = containerBuilder.CreateTestThreadContainer(containerBuilder.CreateGlobalContainer(typeof(TestThreadContextTests).Assembly)); var contextManager = CreateContextManager(testThreadContainer); - contextManager.InitializeFeatureContext(new FeatureInfo(FeatureLanguage, "", "test feature", null)); - contextManager.InitializeScenarioContext(new ScenarioInfo("test scenario", "test_description", null, null), null); + await contextManager.InitializeFeatureContextAsync(new FeatureInfo(FeatureLanguage, "", "test feature", null)); + await contextManager.InitializeScenarioContextAsync(new ScenarioInfo("test scenario", "test_description", null, null), null); contextManager.TestThreadContext.Should().NotBeNull(); diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs index de22adb11..9c9af3490 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs @@ -320,7 +320,7 @@ public async Task Should_publish_scenario_started_event() { var testExecutionEngine = CreateTestExecutionEngine(); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); _testThreadExecutionEventPublisher.Verify(te => @@ -335,7 +335,7 @@ public async Task Should_publish_scenario_finished_event() { var testExecutionEngine = CreateTestExecutionEngine(); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); await testExecutionEngine.OnAfterLastStepAsync(); await testExecutionEngine.OnScenarioEndAsync(); @@ -356,7 +356,7 @@ public async Task Should_publish_scenario_finished_event_even_if_the_after_scena _methodBindingInvokerMock.Setup(i => i.InvokeBindingAsync(hookMock.Object, _contextManagerStub.Object, null, _testTracerStub.Object, It.IsAny())) .Throws(new Exception("simulated hook error")); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); await testExecutionEngine.OnAfterLastStepAsync(); await FluentActions.Awaiting(testExecutionEngine.OnScenarioEndAsync) @@ -378,7 +378,7 @@ public async Task Should_publish_hook_started_finished_events() await testExecutionEngine.OnTestRunStartAsync(); await testExecutionEngine.OnFeatureStartAsync(_featureInfo); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioStartAsync(); await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "foo", null, null); await testExecutionEngine.OnAfterLastStepAsync(); @@ -402,7 +402,7 @@ public async Task Should_publish_scenario_skipped_event() { var testExecutionEngine = CreateTestExecutionEngine(); - testExecutionEngine.OnScenarioInitialize(_scenarioInfo, _ruleInfo); + await testExecutionEngine.OnScenarioInitializeAsync(_scenarioInfo, _ruleInfo); await testExecutionEngine.OnScenarioSkippedAsync(); await testExecutionEngine.OnAfterLastStepAsync(); await testExecutionEngine.OnScenarioEndAsync(); diff --git a/Tests/Reqnroll.RuntimeTests/ScenarioContext_BindingInstancesTest.cs b/Tests/Reqnroll.RuntimeTests/ScenarioContext_BindingInstancesTest.cs index bd5ce660d..fb24ca888 100644 --- a/Tests/Reqnroll.RuntimeTests/ScenarioContext_BindingInstancesTest.cs +++ b/Tests/Reqnroll.RuntimeTests/ScenarioContext_BindingInstancesTest.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Moq; using Reqnroll.Infrastructure; +using System.Threading.Tasks; namespace Reqnroll.RuntimeTests { @@ -89,13 +90,13 @@ public void GetBindingInstance_should_return_instance_through_TestObjectResolver } [Fact] - public void Should_dispose_disposable_binding_instances() + public async Task Should_dispose_disposable_binding_instances() { var scenarioContext = CreateScenarioContext(); var displosableInstance = (DisplosableClass)scenarioContext.GetBindingInstance(typeof(DisplosableClass)); - scenarioContext.ScenarioContainer.Dispose(); + await scenarioContext.ScenarioContainer.DisposeAsync(); displosableInstance.WasDisposed.Should().BeTrue(); } diff --git a/Tests/Reqnroll.RuntimeTests/ScenarioStepContextTests.cs b/Tests/Reqnroll.RuntimeTests/ScenarioStepContextTests.cs index 9b24bd938..7ea2b1d3b 100644 --- a/Tests/Reqnroll.RuntimeTests/ScenarioStepContextTests.cs +++ b/Tests/Reqnroll.RuntimeTests/ScenarioStepContextTests.cs @@ -7,6 +7,7 @@ using Reqnroll.Bindings; using Reqnroll.Infrastructure; using Reqnroll.Tracing; +using System.Threading.Tasks; namespace Reqnroll.RuntimeTests { @@ -282,11 +283,11 @@ public void CurrentTopLevelStepDefinitionType_AfterInitializingNewScenarioContex { var mockTracer = new Mock(); var contextManager = ResolveContextManager(mockTracer.Object); - contextManager.InitializeFeatureContext(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "F", null)); + contextManager.InitializeFeatureContextAsync(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "F", null)); contextManager.InitializeStepContext(CreateStepInfo("I have called initialize once")); //// Do not call CleanupStepContext, in order to simulate an inconsistent state - contextManager.InitializeScenarioContext(new ScenarioInfo("the next scenario", "description of the next scenario", null, null), null); + contextManager.InitializeScenarioContextAsync(new ScenarioInfo("the next scenario", "description of the next scenario", null, null), null); var actualCurrentTopLevelStepDefinitionType = contextManager.CurrentTopLevelStepDefinitionType; @@ -294,21 +295,21 @@ public void CurrentTopLevelStepDefinitionType_AfterInitializingNewScenarioContex } [Fact] - public void Dispose_InInconsistentState_ShouldNotThrowException() + public async Task DisposeAsync_InInconsistentState_ShouldNotThrowException() { var mockTracer = new Mock(); var contextManager = ResolveContextManager(mockTracer.Object); contextManager.InitializeStepContext(CreateStepInfo("I have called initialize once")); //// do not call CleanupStepContext to simulate inconsistent state + + Func disposeAction = async () => await ((IAsyncDisposable) contextManager).DisposeAsync(); - Action disposeAction = () => ((IDisposable) contextManager).Dispose(); - - disposeAction.Should().NotThrow(); + await disposeAction.Should().NotThrowAsync(); } [Fact] - public void Dispose_InConsistentState_ShouldNotThrowException() + public async Task DisposeAsync_InConsistentState_ShouldNotThrowException() { var mockTracer = new Mock(); var contextManager = ResolveContextManager(mockTracer.Object); @@ -317,9 +318,9 @@ public void Dispose_InConsistentState_ShouldNotThrowException() contextManager.CleanupStepContext(); - Action disposeAction = () => ((IDisposable)contextManager).Dispose(); + Func disposeAction = async () => await ((IAsyncDisposable)contextManager).DisposeAsync(); - disposeAction.Should().NotThrow(); + await disposeAction.Should().NotThrowAsync(); } [Fact] diff --git a/Tests/Reqnroll.RuntimeTests/StepExecutionTestsBase.cs b/Tests/Reqnroll.RuntimeTests/StepExecutionTestsBase.cs index 429781953..a528a1504 100644 --- a/Tests/Reqnroll.RuntimeTests/StepExecutionTestsBase.cs +++ b/Tests/Reqnroll.RuntimeTests/StepExecutionTestsBase.cs @@ -125,8 +125,8 @@ public async Task InitializeAsync() ContextManagerStub = new ContextManager(new Mock().Object, TestThreadContainer, ContainerBuilderStub); - ContextManagerStub.InitializeFeatureContext(new FeatureInfo(FeatureLanguage, string.Empty, "test feature", null)); - ContextManagerStub.InitializeScenarioContext(new ScenarioInfo("test scenario", "test scenario description", null, null), null); + await ContextManagerStub.InitializeFeatureContextAsync(new FeatureInfo(FeatureLanguage, string.Empty, "test feature", null)); + await ContextManagerStub.InitializeScenarioContextAsync(new ScenarioInfo("test scenario", "test scenario description", null, null), null); StepArgumentTypeConverterStub = new Mock(); } diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs index 81d20810f..890cbafc2 100644 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs +++ b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs @@ -291,13 +291,13 @@ public async Task Should_resolve_a_test_runner_specific_test_tracer() { var testRunner1 = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, new RuntimeTestsContainerBuilder()); await testRunner1.OnFeatureStartAsync(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "sds", "sss")); - testRunner1.OnScenarioInitialize(new ScenarioInfo("foo", "foo_desc", null, null), null); + await testRunner1.OnScenarioInitializeAsync(new ScenarioInfo("foo", "foo_desc", null, null), null); await testRunner1.OnScenarioStartAsync(); var tracer1 = testRunner1.ScenarioContext.ScenarioContainer.Resolve(); var testRunner2 = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, new RuntimeTestsContainerBuilder()); await testRunner2.OnFeatureStartAsync(new FeatureInfo(new CultureInfo("en-US", false), string.Empty, "sds", "sss")); - testRunner2.OnScenarioInitialize(new ScenarioInfo("foo", "foo_desc", null, null), null); + await testRunner2.OnScenarioInitializeAsync(new ScenarioInfo("foo", "foo_desc", null, null), null); await testRunner1.OnScenarioStartAsync(); var tracer2 = testRunner2.ScenarioContext.ScenarioContainer.Resolve(); @@ -316,7 +316,7 @@ public async Task Should_support_out_of_order_feature_execution() // Feature 1 started var testRunnerFeature1Scenario = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, new RuntimeTestsContainerBuilder(), featureHint: feature1); await testRunnerFeature1Scenario.OnFeatureStartAsync(feature1); - testRunnerFeature1Scenario.OnScenarioInitialize(new ScenarioInfo("foo1.1", "foo_desc", null, null), null); + await testRunnerFeature1Scenario.OnScenarioInitializeAsync(new ScenarioInfo("foo1.1", "foo_desc", null, null), null); await testRunnerFeature1Scenario.OnScenarioStartAsync(); await testRunnerFeature1Scenario.OnScenarioEndAsync(); TestRunnerManager.ReleaseTestRunner(testRunnerFeature1Scenario); @@ -326,7 +326,7 @@ public async Task Should_support_out_of_order_feature_execution() var testRunnerFeature2Scenario = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, new RuntimeTestsContainerBuilder(), featureHint: feature2); testRunnerFeature2Scenario.Should().NotBeSameAs(testRunnerFeature1Scenario, because: "because one testrunner should be reserved for feature1"); await testRunnerFeature2Scenario.OnFeatureStartAsync(feature2); - testRunnerFeature2Scenario.OnScenarioInitialize(new ScenarioInfo("foo2.1", "foo_desc", null, null), null); + await testRunnerFeature2Scenario.OnScenarioInitializeAsync(new ScenarioInfo("foo2.1", "foo_desc", null, null), null); await testRunnerFeature2Scenario.OnScenarioStartAsync(); await testRunnerFeature2Scenario.OnScenarioEndAsync(); TestRunnerManager.ReleaseTestRunner(testRunnerFeature2Scenario); diff --git a/docs/automation/context-injection.md b/docs/automation/context-injection.md index ae3dd52ad..77da476a1 100644 --- a/docs/automation/context-injection.md +++ b/docs/automation/context-injection.md @@ -11,7 +11,7 @@ To use context injection: Rules: * The life-time of these objects is limited to a scenario's execution. -* If the injected objects implement `IDisposable`, they will be disposed after the scenario is executed. +* If the injected objects implement `IDisposable` or `IAsyncDisposable`, they will be disposed after the scenario is executed. If they implement both, only `DisposeAsync` will be called. * The injection is resolved recursively, i.e. the injected class can also have dependencies. * Resolution is done using public constructors only. * If there are multiple public constructors, Reqnroll takes the first one. diff --git a/docs/execution/parallel-execution.md b/docs/execution/parallel-execution.md index 4a6d07aa7..a9b27432d 100644 --- a/docs/execution/parallel-execution.md +++ b/docs/execution/parallel-execution.md @@ -48,7 +48,7 @@ When using Reqnroll we can consider the parallel scheduling on the level of scen * However, if you still want to use method-level parallelism and a `FeatureContext` in your test suite, then **the following things will be true**: * The `FeatureContext` and feature-level DI container will remain consistent **per feature, per test thread**. This means that anything you register in the feature container will be resolvable in the `[AfterFeature]` **per test thread**. * A given `[BeforeFeature]` or `[AfterFeature]` will only be executed once **per test thread** that runs a scenario of a feature. - * Types you register in the feature-level DI container that implement `IDisposable` will still be disposed **per feature, per test thread**. (Keep this in mind if you try to work around this parallelism behavior to regain singleton-like behavior. E.g. by using static instances, `Lazy<>`, thread-safe collections, etc.) + * Types you register in the feature-level DI container that implement `IDisposable` or `IAsyncDisposable` will still be disposed **per feature, per test thread**. (Keep this in mind if you try to work around this parallelism behavior to regain singleton-like behavior. E.g. by using static instances, `Lazy<>`, thread-safe collections, etc.) * Scenarios and their related hooks (Before/After scenario, scenario block, step) are isolated in the different threads during execution and do not block each other. Each thread has a separate (and isolated) `ScenarioContext`. * The test trace listener (that outputs the scenario execution trace to the console by default) is invoked asynchronously from the multiple threads and the trace messages are queued and passed to the listener in serialized form. If the test trace listener implements `Reqnroll.Tracing.IThreadSafeTraceListener`, the messages are sent directly from the threads.