From 0a6400417e550d3c51010ff2967137a3257dac3d Mon Sep 17 00:00:00 2001 From: obligaron Date: Fri, 17 May 2024 21:16:29 +0200 Subject: [PATCH 1/5] Dispose TestThread-Container correctly --- CHANGELOG.md | 1 + .../VSTestExecutionDriver.cs | 9 ++ Reqnroll/TestRunnerManager.cs | 10 +- .../Generation/GenerationTestBase.cs | 92 +++++++++++++++++++ Tests/Reqnroll.SystemTests/SystemTestBase.cs | 12 +++ 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbbfa808b..80ef0c8db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Support for JSON files added to SpecFlow.ExternalData * Fix: #120 Capture ExecutionContext after every binding invoke * MsTest: Use ClassCleanupBehavior.EndOfClass instead of custom implementation (preparation for MsTest v4.0) +* Fix: #123 Dispose objects registred in TestThread-Container correctly # v1.0.1 - 2024-02-16 diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs index c1edcc62b..50a9b2392 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs @@ -76,6 +76,15 @@ public void CheckAnyOutputContainsText(string text) containsAtAll.Should().BeTrue($"either Trx output or program output should contain '{text}'. Trx Output is: {LastTestExecutionResult.TrxOutput}"); } + public void CheckAnyOutputDoesNotContainsText(string text) + { + var textWithoutWhitespace = WithoutWhitespace(text); + bool trxContainsEntry = WithoutWhitespace(LastTestExecutionResult.TrxOutput).Contains(textWithoutWhitespace); + bool outputContainsEntry = WithoutWhitespace(LastTestExecutionResult.Output).Contains(textWithoutWhitespace); + bool containsAtAll = trxContainsEntry || outputContainsEntry; + containsAtAll.Should().BeFalse($"nether Trx output or program output should contain '{text}'. Trx Output is: {LastTestExecutionResult.TrxOutput}"); + } + public static string WithoutWhitespace(string input) { return sWhitespace.Replace(input, string.Empty); diff --git a/Reqnroll/TestRunnerManager.cs b/Reqnroll/TestRunnerManager.cs index 32b3f8e28..30186713e 100644 --- a/Reqnroll/TestRunnerManager.cs +++ b/Reqnroll/TestRunnerManager.cs @@ -26,6 +26,7 @@ public class TestRunnerManager : ITestRunnerManager protected readonly ITestTracer _testTracer; private readonly ConcurrentDictionary _testRunnerRegistry = new(); + private readonly ConcurrentBag _testThreadContainers = new(); public bool IsTestRunInitialized { get; private set; } private int _wasDisposed = 0; private int _wasSingletonInstanceDisabled = 0; @@ -119,7 +120,7 @@ public async Task FireTestRunStartAsync() protected virtual ITestRunner CreateTestRunnerInstance() { var testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); - + _testThreadContainers.Add(testThreadContainer); return testThreadContainer.Resolve(); } @@ -173,10 +174,17 @@ public virtual async Task DisposeAsync() { await FireTestRunEndAsync(); + foreach (var objectContainer in _testThreadContainers) + { + objectContainer.Dispose(); + } + // this call dispose on this object, but the disposeLockObj will avoid double execution _globalContainer.Dispose(); _testRunnerRegistry.Clear(); + while (_testThreadContainers.TryTake(out _)) + ; // TryTake() used instead of Clear(), because Clear() is not available in .NET Standard OnTestRunnerManagerDisposed(this); } } diff --git a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs index 1df2aff18..985213593 100644 --- a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs +++ b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs @@ -289,5 +289,97 @@ public void Scenario_outline_examples_gather_tags_and_parameters() #endregion + [TestMethod] + public void Check_pooled_pbjects_are_reused_and_disposed() + { + AddFeatureFile(""" +Feature: DispoableFeatureA + +Scenario: ScenarioA1 + When something happens + +Scenario: ScenarioA2 + When something happens +"""); + + AddFeatureFile(""" +Feature: DispoableFeatureB + +Scenario: ScenarioB1 + When something happens + +Scenario: ScenarioB2 + When something happens +"""); + + AddBindingClass(""" +using Reqnroll; +using Reqnroll.Tracing; + +[Binding] +public sealed class DispoableStepDefinitions +{ + private readonly ITestRunner _TestRunner; + + sealed class MyDummyResource : IDisposable + { + public static int CreatedCount { get; private set; } + public static int DisposedCount { get; private set; } + private bool _Disposed; + private readonly ITraceListener _traceListener; + + public MyDummyResource(ITraceListener traceListener) + { + _traceListener = traceListener; + CreatedCount += 1; + + WriteLine(FormattableString.Invariant($"CreatedCount: {CreatedCount}.")); + } + public void Dispose() + { + if (_Disposed) + throw new ObjectDisposedException(GetType().FullName); + _Disposed = true; + DisposedCount += 1; + + WriteLine(FormattableString.Invariant($"DisposedCount: {DisposedCount}.")); + } + + void WriteLine(string line) + { + _traceListener.WriteTestOutput(line); + System.Diagnostics.Trace.WriteLine(line); + // Write to StandardOutput, to ensure that SystemTests see the output when disposing TestThreadContext + using var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.WriteLine(line); + } + } + + public DispoableStepDefinitions(ITestRunner testRunner) + { + _TestRunner = testRunner; + } + + [When(".*"), BeforeScenario, AfterScenario] + public void ScenarioHooks() + { + _TestRunner.ScenarioContext.ScenarioContainer.Resolve().TestThreadContainer.Resolve(); + } + + [BeforeFeature, AfterFeature] + public static void FeatureHooks(ITestRunner testRunner) + { + testRunner.FeatureContext.FeatureContainer.Resolve().TestThreadContainer.Resolve(); + } +} +"""); + ExecuteTests(); + ShouldAllScenariosPass(4); + CheckAnyOutputContainsText("CreatedCount: 1."); + CheckAnyOutputDoesNotContainsText("CreatedCount: 2."); + CheckAnyOutputContainsText("DisposedCount: 1."); + CheckAnyOutputDoesNotContainsText("DisposedCount: 2."); + } + //TODO: test parallel execution (details TBD) - maybe this should be in a separate test class } diff --git a/Tests/Reqnroll.SystemTests/SystemTestBase.cs b/Tests/Reqnroll.SystemTests/SystemTestBase.cs index e0b8d9013..f908495b1 100644 --- a/Tests/Reqnroll.SystemTests/SystemTestBase.cs +++ b/Tests/Reqnroll.SystemTests/SystemTestBase.cs @@ -229,6 +229,18 @@ protected int ConfirmAllTestsRan(int? expectedNrOfTestsSpec) return expectedNrOfTests; } + protected void CheckAnyOutputContainsText(string text) + { + _vsTestExecutionDriver.LastTestExecutionResult.Should().NotBeNull(); + _vsTestExecutionDriver.CheckAnyOutputContainsText(text); + } + + protected void CheckAnyOutputDoesNotContainsText(string text) + { + _vsTestExecutionDriver.LastTestExecutionResult.Should().NotBeNull(); + _vsTestExecutionDriver.CheckAnyOutputDoesNotContainsText(text); + } + protected void AddHookBinding(string eventType, string? name = null, string code = "") { _projectsDriver.AddHookBinding(eventType, name, code: code); From 4f27c6d0736449fc0c7736f2455b0f3b41d48ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Wed, 22 May 2024 14:50:30 +0200 Subject: [PATCH 2/5] use _testRunnerRegistry directly to access test thread containers, add unit test --- Reqnroll/ITestRunner.cs | 1 + .../Infrastructure/ITestExecutionEngine.cs | 1 + Reqnroll/TestRunner.cs | 12 +++----- Reqnroll/TestRunnerManager.cs | 9 ++---- .../TestRunnerManagerTest.cs | 30 ++++++++++++++++--- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/Reqnroll/ITestRunner.cs b/Reqnroll/ITestRunner.cs index 4d5cbffb0..b8a7daff1 100644 --- a/Reqnroll/ITestRunner.cs +++ b/Reqnroll/ITestRunner.cs @@ -10,6 +10,7 @@ public interface ITestRunner string TestWorkerId { get; } FeatureContext FeatureContext { get; } ScenarioContext ScenarioContext { get; } + ITestThreadContext TestThreadContext { get; } void InitializeTestRunner(string testWorkerId); diff --git a/Reqnroll/Infrastructure/ITestExecutionEngine.cs b/Reqnroll/Infrastructure/ITestExecutionEngine.cs index 387510b15..fdb15e429 100644 --- a/Reqnroll/Infrastructure/ITestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/ITestExecutionEngine.cs @@ -7,6 +7,7 @@ public interface ITestExecutionEngine { FeatureContext FeatureContext { get; } ScenarioContext ScenarioContext { get; } + ITestThreadContext TestThreadContext { get; } Task OnTestRunStartAsync(); Task OnTestRunEndAsync(); diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index 2cb29ff3b..b215f3823 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -15,15 +15,11 @@ public TestRunner(ITestExecutionEngine executionEngine) _executionEngine = executionEngine; } - public FeatureContext FeatureContext - { - get { return _executionEngine.FeatureContext; } - } + public FeatureContext FeatureContext => _executionEngine.FeatureContext; - public ScenarioContext ScenarioContext - { - get { return _executionEngine.ScenarioContext; } - } + public ScenarioContext ScenarioContext => _executionEngine.ScenarioContext; + + public ITestThreadContext TestThreadContext => _executionEngine.TestThreadContext; public async Task OnTestRunStartAsync() { diff --git a/Reqnroll/TestRunnerManager.cs b/Reqnroll/TestRunnerManager.cs index 30186713e..948514938 100644 --- a/Reqnroll/TestRunnerManager.cs +++ b/Reqnroll/TestRunnerManager.cs @@ -26,7 +26,6 @@ public class TestRunnerManager : ITestRunnerManager protected readonly ITestTracer _testTracer; private readonly ConcurrentDictionary _testRunnerRegistry = new(); - private readonly ConcurrentBag _testThreadContainers = new(); public bool IsTestRunInitialized { get; private set; } private int _wasDisposed = 0; private int _wasSingletonInstanceDisabled = 0; @@ -120,7 +119,7 @@ public async Task FireTestRunStartAsync() protected virtual ITestRunner CreateTestRunnerInstance() { var testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); - _testThreadContainers.Add(testThreadContainer); + return testThreadContainer.Resolve(); } @@ -174,17 +173,15 @@ public virtual async Task DisposeAsync() { await FireTestRunEndAsync(); - foreach (var objectContainer in _testThreadContainers) + foreach (var testRunner in _testRunnerRegistry.Values) { - objectContainer.Dispose(); + testRunner.TestThreadContext.TestThreadContainer.Dispose(); } // this call dispose on this object, but the disposeLockObj will avoid double execution _globalContainer.Dispose(); _testRunnerRegistry.Clear(); - while (_testThreadContainers.TryTake(out _)) - ; // TryTake() used instead of Clear(), because Clear() is not available in .NET Standard OnTestRunnerManagerDisposed(this); } } diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs index e59e62106..0e7693e72 100644 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs +++ b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs @@ -1,7 +1,8 @@ +using System; using System.Reflection; +using System.Threading.Tasks; using FluentAssertions; using Xunit; -using Reqnroll.Infrastructure; namespace Reqnroll.RuntimeTests { @@ -16,9 +17,7 @@ public class TestRunnerManagerTest public TestRunnerManagerTest() { - var globalContainer = new RuntimeTestsContainerBuilder().CreateGlobalContainer(typeof(TestRunnerManagerTest).Assembly); - testRunnerManager = globalContainer.Resolve(); - testRunnerManager.Initialize(anAssembly); + testRunnerManager = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(anAssembly, new RuntimeTestsContainerBuilder()); } [Fact] @@ -57,5 +56,28 @@ public void Should_return_different_instances_for_different_thread_ids() testRunner1.Should().NotBe(testRunner2); } + + class DisposableClass : IDisposable + { + public bool IsDisposed { get; private set; } + public void Dispose() + { + IsDisposed = true; + } + } + + [Fact] + public async Task Should_dispose_test_thread_container_at_after_test_run() + { + var testRunner1 = testRunnerManager.GetTestRunner("0"); + var testRunner2 = testRunnerManager.GetTestRunner("1"); + + var disposableClass = new DisposableClass(); + testRunner1.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass, dispose: true); + + await TestRunnerManager.OnTestRunEndAsync(anAssembly); + + disposableClass.IsDisposed.Should().BeTrue(); + } } } \ No newline at end of file From f7f9435a3ad298c91835e470b66af92a209e34fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Wed, 22 May 2024 14:51:51 +0200 Subject: [PATCH 3/5] undo system tests --- .../Generation/GenerationTestBase.cs | 92 ------------------- Tests/Reqnroll.SystemTests/SystemTestBase.cs | 12 --- .../VSTestExecutionDriver.cs | 9 -- 3 files changed, 113 deletions(-) diff --git a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs index 985213593..1df2aff18 100644 --- a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs +++ b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs @@ -289,97 +289,5 @@ public void Scenario_outline_examples_gather_tags_and_parameters() #endregion - [TestMethod] - public void Check_pooled_pbjects_are_reused_and_disposed() - { - AddFeatureFile(""" -Feature: DispoableFeatureA - -Scenario: ScenarioA1 - When something happens - -Scenario: ScenarioA2 - When something happens -"""); - - AddFeatureFile(""" -Feature: DispoableFeatureB - -Scenario: ScenarioB1 - When something happens - -Scenario: ScenarioB2 - When something happens -"""); - - AddBindingClass(""" -using Reqnroll; -using Reqnroll.Tracing; - -[Binding] -public sealed class DispoableStepDefinitions -{ - private readonly ITestRunner _TestRunner; - - sealed class MyDummyResource : IDisposable - { - public static int CreatedCount { get; private set; } - public static int DisposedCount { get; private set; } - private bool _Disposed; - private readonly ITraceListener _traceListener; - - public MyDummyResource(ITraceListener traceListener) - { - _traceListener = traceListener; - CreatedCount += 1; - - WriteLine(FormattableString.Invariant($"CreatedCount: {CreatedCount}.")); - } - public void Dispose() - { - if (_Disposed) - throw new ObjectDisposedException(GetType().FullName); - _Disposed = true; - DisposedCount += 1; - - WriteLine(FormattableString.Invariant($"DisposedCount: {DisposedCount}.")); - } - - void WriteLine(string line) - { - _traceListener.WriteTestOutput(line); - System.Diagnostics.Trace.WriteLine(line); - // Write to StandardOutput, to ensure that SystemTests see the output when disposing TestThreadContext - using var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - standardOutput.WriteLine(line); - } - } - - public DispoableStepDefinitions(ITestRunner testRunner) - { - _TestRunner = testRunner; - } - - [When(".*"), BeforeScenario, AfterScenario] - public void ScenarioHooks() - { - _TestRunner.ScenarioContext.ScenarioContainer.Resolve().TestThreadContainer.Resolve(); - } - - [BeforeFeature, AfterFeature] - public static void FeatureHooks(ITestRunner testRunner) - { - testRunner.FeatureContext.FeatureContainer.Resolve().TestThreadContainer.Resolve(); - } -} -"""); - ExecuteTests(); - ShouldAllScenariosPass(4); - CheckAnyOutputContainsText("CreatedCount: 1."); - CheckAnyOutputDoesNotContainsText("CreatedCount: 2."); - CheckAnyOutputContainsText("DisposedCount: 1."); - CheckAnyOutputDoesNotContainsText("DisposedCount: 2."); - } - //TODO: test parallel execution (details TBD) - maybe this should be in a separate test class } diff --git a/Tests/Reqnroll.SystemTests/SystemTestBase.cs b/Tests/Reqnroll.SystemTests/SystemTestBase.cs index f908495b1..e0b8d9013 100644 --- a/Tests/Reqnroll.SystemTests/SystemTestBase.cs +++ b/Tests/Reqnroll.SystemTests/SystemTestBase.cs @@ -229,18 +229,6 @@ protected int ConfirmAllTestsRan(int? expectedNrOfTestsSpec) return expectedNrOfTests; } - protected void CheckAnyOutputContainsText(string text) - { - _vsTestExecutionDriver.LastTestExecutionResult.Should().NotBeNull(); - _vsTestExecutionDriver.CheckAnyOutputContainsText(text); - } - - protected void CheckAnyOutputDoesNotContainsText(string text) - { - _vsTestExecutionDriver.LastTestExecutionResult.Should().NotBeNull(); - _vsTestExecutionDriver.CheckAnyOutputDoesNotContainsText(text); - } - protected void AddHookBinding(string eventType, string? name = null, string code = "") { _projectsDriver.AddHookBinding(eventType, name, code: code); diff --git a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs index 50a9b2392..c1edcc62b 100644 --- a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs +++ b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/VSTestExecutionDriver.cs @@ -76,15 +76,6 @@ public void CheckAnyOutputContainsText(string text) containsAtAll.Should().BeTrue($"either Trx output or program output should contain '{text}'. Trx Output is: {LastTestExecutionResult.TrxOutput}"); } - public void CheckAnyOutputDoesNotContainsText(string text) - { - var textWithoutWhitespace = WithoutWhitespace(text); - bool trxContainsEntry = WithoutWhitespace(LastTestExecutionResult.TrxOutput).Contains(textWithoutWhitespace); - bool outputContainsEntry = WithoutWhitespace(LastTestExecutionResult.Output).Contains(textWithoutWhitespace); - bool containsAtAll = trxContainsEntry || outputContainsEntry; - containsAtAll.Should().BeFalse($"nether Trx output or program output should contain '{text}'. Trx Output is: {LastTestExecutionResult.TrxOutput}"); - } - public static string WithoutWhitespace(string input) { return sWhitespace.Replace(input, string.Empty); From 43a00968fb7578f9eb212c3b814f0fb7fee8bc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Wed, 22 May 2024 14:57:36 +0200 Subject: [PATCH 4/5] cleanup TestRunnerManagerTests --- .../TestRunnerManagerTest.cs | 83 ------------------- .../TestRunnerManagerTests.cs | 82 ++++++++++++++++++ 2 files changed, 82 insertions(+), 83 deletions(-) delete mode 100644 Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs create mode 100644 Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs deleted file mode 100644 index 0e7693e72..000000000 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTest.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; -using FluentAssertions; -using Xunit; - -namespace Reqnroll.RuntimeTests -{ - /// - /// Testing instance members of TestRunnerManager - /// - - public class TestRunnerManagerTest - { - private readonly Assembly anAssembly = Assembly.GetExecutingAssembly(); - private TestRunnerManager testRunnerManager; - - public TestRunnerManagerTest() - { - testRunnerManager = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(anAssembly, new RuntimeTestsContainerBuilder()); - } - - [Fact] - public void CreateTestRunner_should_be_able_to_create_a_testrunner() - { - var testRunner = testRunnerManager.CreateTestRunner("0"); - - testRunner.Should().NotBeNull(); - testRunner.Should().BeOfType(); - } - - [Fact] - public void GetTestRunner_should_be_able_to_create_a_testrunner() - { - var testRunner = testRunnerManager.GetTestRunner("0"); - - testRunner.Should().NotBeNull(); - testRunner.Should().BeOfType(); - } - - [Fact] - public void GetTestRunner_should_cache_instance() - { - var testRunner1 = testRunnerManager.GetTestRunner("0"); - var testRunner2 = testRunnerManager.GetTestRunner("0"); - - - testRunner1.Should().Be(testRunner2); - } - - [Fact] - public void Should_return_different_instances_for_different_thread_ids() - { - var testRunner1 = testRunnerManager.GetTestRunner("0"); - var testRunner2 = testRunnerManager.GetTestRunner("1"); - - testRunner1.Should().NotBe(testRunner2); - } - - class DisposableClass : IDisposable - { - public bool IsDisposed { get; private set; } - public void Dispose() - { - IsDisposed = true; - } - } - - [Fact] - public async Task Should_dispose_test_thread_container_at_after_test_run() - { - var testRunner1 = testRunnerManager.GetTestRunner("0"); - var testRunner2 = testRunnerManager.GetTestRunner("1"); - - var disposableClass = new DisposableClass(); - testRunner1.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass, dispose: true); - - await TestRunnerManager.OnTestRunEndAsync(anAssembly); - - disposableClass.IsDisposed.Should().BeTrue(); - } - } -} \ No newline at end of file diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs new file mode 100644 index 000000000..cd2429250 --- /dev/null +++ b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Reqnroll.RuntimeTests; + +public class TestRunnerManagerTests +{ + private readonly Assembly _anAssembly = Assembly.GetExecutingAssembly(); + private readonly TestRunnerManager _testRunnerManager; + + public TestRunnerManagerTests() + { + _testRunnerManager = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(_anAssembly, new RuntimeTestsContainerBuilder()); + } + + [Fact] + public void CreateTestRunner_should_be_able_to_create_a_TestRunner() + { + var testRunner = _testRunnerManager.CreateTestRunner("0"); + + testRunner.Should().NotBeNull(); + testRunner.Should().BeOfType(); + } + + [Fact] + public void GetTestRunner_should_be_able_to_create_a_TestRunner() + { + var testRunner = _testRunnerManager.GetTestRunner("0"); + + testRunner.Should().NotBeNull(); + testRunner.Should().BeOfType(); + } + + [Fact] + public void GetTestRunner_should_cache_instance() + { + var testRunner1 = _testRunnerManager.GetTestRunner("0"); + var testRunner2 = _testRunnerManager.GetTestRunner("0"); + + + testRunner1.Should().Be(testRunner2); + } + + [Fact] + public void Should_return_different_instances_for_different_thread_ids() + { + var testRunner1 = _testRunnerManager.GetTestRunner("0"); + var testRunner2 = _testRunnerManager.GetTestRunner("1"); + + testRunner1.Should().NotBe(testRunner2); + } + + class DisposableClass : IDisposable + { + public bool IsDisposed { get; private set; } + public void Dispose() + { + IsDisposed = true; + } + } + + [Fact] + public async Task Should_dispose_test_thread_container_at_after_test_run() + { + var testRunner1 = _testRunnerManager.GetTestRunner("0"); + var testRunner2 = _testRunnerManager.GetTestRunner("1"); + + var disposableClass1 = new DisposableClass(); + testRunner1.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass1, dispose: true); + + var disposableClass2 = new DisposableClass(); + testRunner2.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass2, dispose: true); + + await TestRunnerManager.OnTestRunEndAsync(_anAssembly); + + disposableClass1.IsDisposed.Should().BeTrue(); + disposableClass2.IsDisposed.Should().BeTrue(); + } +} \ No newline at end of file From 6ae091ed9ae6728e457f9acbe5825d663b728484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Wed, 22 May 2024 14:58:53 +0200 Subject: [PATCH 5/5] cleanup TestRunnerManager --- Reqnroll/TestRunnerManager.cs | 411 +++++++++++++++++----------------- 1 file changed, 205 insertions(+), 206 deletions(-) diff --git a/Reqnroll/TestRunnerManager.cs b/Reqnroll/TestRunnerManager.cs index 948514938..861339de0 100644 --- a/Reqnroll/TestRunnerManager.cs +++ b/Reqnroll/TestRunnerManager.cs @@ -13,284 +13,283 @@ using Reqnroll.Infrastructure; using Reqnroll.Tracing; -namespace Reqnroll +namespace Reqnroll; + +public class TestRunnerManager : ITestRunnerManager { - public class TestRunnerManager : ITestRunnerManager - { - public const string TestRunStartWorkerId = "TestRunStart"; + public const string TestRunStartWorkerId = "TestRunStart"; - protected readonly IObjectContainer _globalContainer; - protected readonly IContainerBuilder _containerBuilder; - protected readonly ReqnrollConfiguration _reqnrollConfiguration; - protected readonly IRuntimeBindingRegistryBuilder _bindingRegistryBuilder; - protected readonly ITestTracer _testTracer; + protected readonly IObjectContainer _globalContainer; + protected readonly IContainerBuilder _containerBuilder; + protected readonly ReqnrollConfiguration _reqnrollConfiguration; + protected readonly IRuntimeBindingRegistryBuilder _bindingRegistryBuilder; + protected readonly ITestTracer _testTracer; - private readonly ConcurrentDictionary _testRunnerRegistry = new(); - public bool IsTestRunInitialized { get; private set; } - private int _wasDisposed = 0; - private int _wasSingletonInstanceDisabled = 0; - private readonly object createTestRunnerLockObject = new(); + private readonly ConcurrentDictionary _testRunnerRegistry = new(); + public bool IsTestRunInitialized { get; private set; } + private int _wasDisposed = 0; + private int _wasSingletonInstanceDisabled = 0; + private readonly object _createTestRunnerLockObject = new(); - public Assembly TestAssembly { get; private set; } - public Assembly[] BindingAssemblies { get; private set; } + public Assembly TestAssembly { get; private set; } + public Assembly[] BindingAssemblies { get; private set; } - public bool IsMultiThreaded => GetWorkerTestRunnerCount() > 1; + public bool IsMultiThreaded => GetWorkerTestRunnerCount() > 1; - public TestRunnerManager(IObjectContainer globalContainer, IContainerBuilder containerBuilder, ReqnrollConfiguration reqnrollConfiguration, IRuntimeBindingRegistryBuilder bindingRegistryBuilder, - ITestTracer testTracer) - { - _globalContainer = globalContainer; - _containerBuilder = containerBuilder; - _reqnrollConfiguration = reqnrollConfiguration; - _bindingRegistryBuilder = bindingRegistryBuilder; - _testTracer = testTracer; - } + public TestRunnerManager(IObjectContainer globalContainer, IContainerBuilder containerBuilder, ReqnrollConfiguration reqnrollConfiguration, IRuntimeBindingRegistryBuilder bindingRegistryBuilder, + ITestTracer testTracer) + { + _globalContainer = globalContainer; + _containerBuilder = containerBuilder; + _reqnrollConfiguration = reqnrollConfiguration; + _bindingRegistryBuilder = bindingRegistryBuilder; + _testTracer = testTracer; + } - private int GetWorkerTestRunnerCount() - { - var hasTestRunStartWorker = _testRunnerRegistry.ContainsKey(TestRunStartWorkerId); - return _testRunnerRegistry.Count - (hasTestRunStartWorker ? 1 : 0); - } + private int GetWorkerTestRunnerCount() + { + var hasTestRunStartWorker = _testRunnerRegistry.ContainsKey(TestRunStartWorkerId); + return _testRunnerRegistry.Count - (hasTestRunStartWorker ? 1 : 0); + } - public virtual ITestRunner CreateTestRunner(string testWorkerId = "default-worker") - { - var testRunner = CreateTestRunnerInstance(); - testRunner.InitializeTestRunner(testWorkerId); + public virtual ITestRunner CreateTestRunner(string testWorkerId = "default-worker") + { + var testRunner = CreateTestRunnerInstance(); + testRunner.InitializeTestRunner(testWorkerId); - if (!IsTestRunInitialized) + if (!IsTestRunInitialized) + { + lock (_createTestRunnerLockObject) { - lock (createTestRunnerLockObject) + if (!IsTestRunInitialized) { - if (!IsTestRunInitialized) - { - InitializeBindingRegistry(testRunner); - IsTestRunInitialized = true; - } + InitializeBindingRegistry(testRunner); + IsTestRunInitialized = true; } } - - return testRunner; } - protected virtual void InitializeBindingRegistry(ITestRunner testRunner) - { - BindingAssemblies = _bindingRegistryBuilder.GetBindingAssemblies(TestAssembly); - BuildBindingRegistry(BindingAssemblies); - - void DomainUnload(object sender, EventArgs e) - { - OnDomainUnloadAsync().Wait(); - } + return testRunner; + } - AppDomain.CurrentDomain.DomainUnload += DomainUnload; - AppDomain.CurrentDomain.ProcessExit += DomainUnload; - } + protected virtual void InitializeBindingRegistry(ITestRunner testRunner) + { + BindingAssemblies = _bindingRegistryBuilder.GetBindingAssemblies(TestAssembly); + BuildBindingRegistry(BindingAssemblies); - protected virtual void BuildBindingRegistry(IEnumerable bindingAssemblies) + void DomainUnload(object sender, EventArgs e) { - foreach (Assembly assembly in bindingAssemblies) - { - _bindingRegistryBuilder.BuildBindingsFromAssembly(assembly); - } - _bindingRegistryBuilder.BuildingCompleted(); + OnDomainUnloadAsync().Wait(); } - protected internal virtual async Task OnDomainUnloadAsync() - { - await DisposeAsync(); - } + AppDomain.CurrentDomain.DomainUnload += DomainUnload; + AppDomain.CurrentDomain.ProcessExit += DomainUnload; + } - public async Task FireTestRunEndAsync() + protected virtual void BuildBindingRegistry(IEnumerable bindingAssemblies) + { + foreach (Assembly assembly in bindingAssemblies) { - // this method must not be called multiple times - var onTestRunnerEndExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); - if (onTestRunnerEndExecutionHost != null) - await onTestRunnerEndExecutionHost.OnTestRunEndAsync(); + _bindingRegistryBuilder.BuildBindingsFromAssembly(assembly); } + _bindingRegistryBuilder.BuildingCompleted(); + } - public async Task FireTestRunStartAsync() - { - // this method must not be called multiple times - var onTestRunnerStartExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); - if (onTestRunnerStartExecutionHost != null) - await onTestRunnerStartExecutionHost.OnTestRunStartAsync(); - } + protected internal virtual async Task OnDomainUnloadAsync() + { + await DisposeAsync(); + } - protected virtual ITestRunner CreateTestRunnerInstance() - { - var testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); + public async Task FireTestRunEndAsync() + { + // this method must not be called multiple times + var onTestRunnerEndExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); + if (onTestRunnerEndExecutionHost != null) + await onTestRunnerEndExecutionHost.OnTestRunEndAsync(); + } - return testThreadContainer.Resolve(); - } + public async Task FireTestRunStartAsync() + { + // this method must not be called multiple times + var onTestRunnerStartExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); + if (onTestRunnerStartExecutionHost != null) + await onTestRunnerStartExecutionHost.OnTestRunStartAsync(); + } - public void Initialize(Assembly assignedTestAssembly) - { - TestAssembly = assignedTestAssembly; - } + protected virtual ITestRunner CreateTestRunnerInstance() + { + var testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); + + return testThreadContainer.Resolve(); + } + + public void Initialize(Assembly assignedTestAssembly) + { + TestAssembly = assignedTestAssembly; + } - public virtual ITestRunner GetTestRunner(string testWorkerId) + public virtual ITestRunner GetTestRunner(string testWorkerId) + { + testWorkerId ??= Guid.NewGuid().ToString(); //Creates a Test Runner with a unique test thread + try { - testWorkerId ??= Guid.NewGuid().ToString(); //Creates a Test Runner with a unique test thread - try - { - return GetTestRunnerWithoutExceptionHandling(testWorkerId); - } - catch (Exception ex) - { - _testTracer.TraceError(ex,TimeSpan.Zero); - throw; - } + return GetTestRunnerWithoutExceptionHandling(testWorkerId); } - - private ITestRunner GetTestRunnerWithoutExceptionHandling(string testWorkerId) + catch (Exception ex) { - if (testWorkerId == null) - throw new ArgumentNullException(nameof(testWorkerId)); + _testTracer.TraceError(ex,TimeSpan.Zero); + throw; + } + } - bool wasAdded = false; + private ITestRunner GetTestRunnerWithoutExceptionHandling(string testWorkerId) + { + if (testWorkerId == null) + throw new ArgumentNullException(nameof(testWorkerId)); - var testRunner = _testRunnerRegistry.GetOrAdd( - testWorkerId, - workerId => - { - wasAdded = true; - return CreateTestRunner(workerId); - }); + bool wasAdded = false; - if (wasAdded && IsMultiThreaded && Interlocked.CompareExchange(ref _wasSingletonInstanceDisabled, 1, 0) == 0) + var testRunner = _testRunnerRegistry.GetOrAdd( + testWorkerId, + workerId => { - FeatureContext.DisableSingletonInstance(); - ScenarioContext.DisableSingletonInstance(); - ScenarioStepContext.DisableSingletonInstance(); - } - - return testRunner; - } + wasAdded = true; + return CreateTestRunner(workerId); + }); - public virtual async Task DisposeAsync() + if (wasAdded && IsMultiThreaded && Interlocked.CompareExchange(ref _wasSingletonInstanceDisabled, 1, 0) == 0) { - if (Interlocked.CompareExchange(ref _wasDisposed, 1, 0) == 0) - { - await FireTestRunEndAsync(); - - foreach (var testRunner in _testRunnerRegistry.Values) - { - testRunner.TestThreadContext.TestThreadContainer.Dispose(); - } - - // this call dispose on this object, but the disposeLockObj will avoid double execution - _globalContainer.Dispose(); - - _testRunnerRegistry.Clear(); - OnTestRunnerManagerDisposed(this); - } + FeatureContext.DisableSingletonInstance(); + ScenarioContext.DisableSingletonInstance(); + ScenarioStepContext.DisableSingletonInstance(); } - #region Static API - - private static readonly ConcurrentDictionary _testRunnerManagerRegistry = new(); + return testRunner; + } - public static ITestRunnerManager GetTestRunnerManager(Assembly testAssembly = null, IContainerBuilder containerBuilder = null, bool createIfMissing = true) + public virtual async Task DisposeAsync() + { + if (Interlocked.CompareExchange(ref _wasDisposed, 1, 0) == 0) { - testAssembly ??= GetCallingAssembly(); + await FireTestRunEndAsync(); - if (!createIfMissing) + foreach (var testRunner in _testRunnerRegistry.Values) { - return _testRunnerManagerRegistry.TryGetValue(testAssembly, out var value) ? value : null; + testRunner.TestThreadContext.TestThreadContainer.Dispose(); } - var testRunnerManager = _testRunnerManagerRegistry.GetOrAdd( - testAssembly, - assembly => CreateTestRunnerManager(assembly, containerBuilder)); - return testRunnerManager; + // this call dispose on this object, but the disposeLockObj will avoid double execution + _globalContainer.Dispose(); + + _testRunnerRegistry.Clear(); + OnTestRunnerManagerDisposed(this); } + } + + #region Static API - /// - /// This is a workaround method solving not correctly working Assembly.GetCallingAssembly() when called from async method (due to state machine). - /// - private static Assembly GetCallingAssembly([CallerMemberName] string callingMethodName = null) + private static readonly ConcurrentDictionary _testRunnerManagerRegistry = new(); + + public static ITestRunnerManager GetTestRunnerManager(Assembly testAssembly = null, IContainerBuilder containerBuilder = null, bool createIfMissing = true) + { + testAssembly ??= GetCallingAssembly(); + + if (!createIfMissing) { - var stackTrace = new StackTrace(); + return _testRunnerManagerRegistry.TryGetValue(testAssembly, out var value) ? value : null; + } - var callingMethodIndex = -1; + var testRunnerManager = _testRunnerManagerRegistry.GetOrAdd( + testAssembly, + assembly => CreateTestRunnerManager(assembly, containerBuilder)); + return testRunnerManager; + } - for (var i = 0; i < stackTrace.FrameCount; i++) - { - var frame = stackTrace.GetFrame(i); + /// + /// This is a workaround method solving not correctly working Assembly.GetCallingAssembly() when called from async method (due to state machine). + /// + private static Assembly GetCallingAssembly([CallerMemberName] string callingMethodName = null) + { + var stackTrace = new StackTrace(); - if (frame.GetMethod().Name == callingMethodName) - { - callingMethodIndex = i; - break; - } - } + var callingMethodIndex = -1; - Assembly result = null; + for (var i = 0; i < stackTrace.FrameCount; i++) + { + var frame = stackTrace.GetFrame(i); - if (callingMethodIndex >= 0 && callingMethodIndex + 1 < stackTrace.FrameCount) + if (frame.GetMethod().Name == callingMethodName) { - result = stackTrace.GetFrame(callingMethodIndex + 1).GetMethod().DeclaringType?.Assembly; + callingMethodIndex = i; + break; } - - return result ?? GetCallingAssembly(); } - - private static ITestRunnerManager CreateTestRunnerManager(Assembly testAssembly, IContainerBuilder containerBuilder = null) - { - containerBuilder ??= new ContainerBuilder(); - var container = containerBuilder.CreateGlobalContainer(testAssembly); - var testRunnerManager = container.Resolve(); - testRunnerManager.Initialize(testAssembly); - return testRunnerManager; - } + Assembly result = null; - public static async Task OnTestRunEndAsync(Assembly testAssembly = null, IContainerBuilder containerBuilder = null) + if (callingMethodIndex >= 0 && callingMethodIndex + 1 < stackTrace.FrameCount) { - testAssembly ??= GetCallingAssembly(); - var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: false, containerBuilder: containerBuilder); - if (testRunnerManager != null) - { - await testRunnerManager.FireTestRunEndAsync(); - await testRunnerManager.DisposeAsync(); - } + result = stackTrace.GetFrame(callingMethodIndex + 1).GetMethod().DeclaringType?.Assembly; } - public static async Task OnTestRunStartAsync(Assembly testAssembly = null, string testWorkerId = null, IContainerBuilder containerBuilder = null) - { - testAssembly ??= GetCallingAssembly(); - var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: true, containerBuilder: containerBuilder); - testRunnerManager.GetTestRunner(testWorkerId ?? TestRunStartWorkerId); + return result ?? GetCallingAssembly(); + } + + private static ITestRunnerManager CreateTestRunnerManager(Assembly testAssembly, IContainerBuilder containerBuilder = null) + { + containerBuilder ??= new ContainerBuilder(); - await testRunnerManager.FireTestRunStartAsync(); - } + var container = containerBuilder.CreateGlobalContainer(testAssembly); + var testRunnerManager = container.Resolve(); + testRunnerManager.Initialize(testAssembly); + return testRunnerManager; + } - public static ITestRunner GetTestRunnerForAssembly(Assembly testAssembly = null, string testWorkerId = null, IContainerBuilder containerBuilder = null) + public static async Task OnTestRunEndAsync(Assembly testAssembly = null, IContainerBuilder containerBuilder = null) + { + testAssembly ??= GetCallingAssembly(); + var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: false, containerBuilder: containerBuilder); + if (testRunnerManager != null) { - testAssembly ??= GetCallingAssembly(); - var testRunnerManager = GetTestRunnerManager(testAssembly, containerBuilder); - return testRunnerManager.GetTestRunner(testWorkerId); + // DisposeAsync invokes FireTestRunEndAsync + await testRunnerManager.DisposeAsync(); } + } - internal static async Task ResetAsync() + public static async Task OnTestRunStartAsync(Assembly testAssembly = null, string testWorkerId = null, IContainerBuilder containerBuilder = null) + { + testAssembly ??= GetCallingAssembly(); + var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: true, containerBuilder: containerBuilder); + testRunnerManager.GetTestRunner(testWorkerId ?? TestRunStartWorkerId); + + await testRunnerManager.FireTestRunStartAsync(); + } + + public static ITestRunner GetTestRunnerForAssembly(Assembly testAssembly = null, string testWorkerId = null, IContainerBuilder containerBuilder = null) + { + testAssembly ??= GetCallingAssembly(); + var testRunnerManager = GetTestRunnerManager(testAssembly, containerBuilder); + return testRunnerManager.GetTestRunner(testWorkerId); + } + + internal static async Task ResetAsync() + { + while (!_testRunnerManagerRegistry.IsEmpty) { - while (!_testRunnerManagerRegistry.IsEmpty) + foreach (var assembly in _testRunnerManagerRegistry.Keys.ToArray()) { - foreach (var assembly in _testRunnerManagerRegistry.Keys.ToArray()) + if (_testRunnerManagerRegistry.TryRemove(assembly, out var testRunnerManager)) { - if (_testRunnerManagerRegistry.TryRemove(assembly, out var testRunnerManager)) - { - await testRunnerManager.DisposeAsync(); - } + await testRunnerManager.DisposeAsync(); } } } + } - private static void OnTestRunnerManagerDisposed(TestRunnerManager testRunnerManager) - { - _testRunnerManagerRegistry.TryRemove(testRunnerManager.TestAssembly, out _); - } - - #endregion + private static void OnTestRunnerManagerDisposed(TestRunnerManager testRunnerManager) + { + _testRunnerManagerRegistry.TryRemove(testRunnerManager.TestAssembly, out _); } + + #endregion } \ No newline at end of file