diff --git a/CHANGELOG.md b/CHANGELOG.md index 011c2fe39..c4d703429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ ## Improvements: * Reqnroll.Verify: Support for Verify v24 (Verify.Xunit v24.2.0) for .NET 4.7.2+ and .NET 6.0+. For earlier versions of Verify or for .NET 4.6.2, use the latest 2.0.3 version of the plugin that is compatible with Reqnroll v2.*. (#151) +* Optimize creation of test-thread context using test framework independent resource pooling (#144) ## Bug fixes: -*Contributors of this release (in alphabetical order):* @ajeckmans +*Contributors of this release (in alphabetical order):* @ajeckmans, @obligaron # v2.0.3 - 2024-06-10 diff --git a/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/MSTest.AssemblyHooks.template.vb b/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/MSTest.AssemblyHooks.template.vb index d0af6f83a..14102a49e 100644 --- a/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/MSTest.AssemblyHooks.template.vb +++ b/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/MSTest.AssemblyHooks.template.vb @@ -16,7 +16,7 @@ Public NotInheritable Class PROJECT_ROOT_NAMESPACE_MSTestAssemblyHooks Public Shared Async Function AssemblyInitializeAsync(testContext As TestContext) As Task Dim currentAssembly As Assembly = GetType(PROJECT_ROOT_NAMESPACE_MSTestAssemblyHooks).Assembly Dim containerBuilder As New MsTestContainerBuilder(testContext) - Await Global.Reqnroll.TestRunnerManager.OnTestRunStartAsync(currentAssembly, Nothing, containerBuilder) + Await Global.Reqnroll.TestRunnerManager.OnTestRunStartAsync(currentAssembly, containerBuilder) End Function diff --git a/Plugins/Reqnroll.xUnit.ReqnrollPlugin/XUnitParallelWorkerTracker.cs b/Plugins/Reqnroll.xUnit.ReqnrollPlugin/XUnitParallelWorkerTracker.cs deleted file mode 100644 index fdcb3893a..000000000 --- a/Plugins/Reqnroll.xUnit.ReqnrollPlugin/XUnitParallelWorkerTracker.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace Reqnroll.xUnit.ReqnrollPlugin; - -public class XUnitParallelWorkerTracker -{ - public static readonly XUnitParallelWorkerTracker Instance = new(); - - private readonly ConcurrentBag _availableWorkers = new(); - private int _workerCount = 0; - - private XUnitParallelWorkerTracker() { } - - public string GetWorkerId() - { - if (!_availableWorkers.TryTake(out var workerId)) - { - workerId = $"XW{Interlocked.Increment(ref _workerCount):D3}"; - } - return workerId; - } - - public void ReleaseWorker(string workerId) - { - _availableWorkers.Add(workerId); - } -} \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index baba0e0b8..404d22d75 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -177,15 +177,9 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio //testRunner = TestRunnerManager.GetTestRunnerForAssembly(null, [test_worker_id]); var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); - var testRunnerParameters = new[] - { - new CodePrimitiveExpression(null), - _testGeneratorProvider.GetTestWorkerIdExpression() - }; - var getTestRunnerExpression = new CodeMethodInvokeExpression( new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))), - nameof(TestRunnerManager.GetTestRunnerForAssembly), testRunnerParameters); + nameof(TestRunnerManager.GetTestRunnerForAssembly)); testClassInitializeMethod.Statements.Add( new CodeAssignStatement( @@ -238,7 +232,14 @@ private void SetupTestClassCleanupMethod(TestClassGenerationContext generationCo _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); testClassCleanupMethod.Statements.Add(expression); - + + // + testClassCleanupMethod.Statements.Add( + new CodeMethodInvokeExpression( + new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))), + nameof(TestRunnerManager.ReleaseTestRunner), + testRunnerField)); + // testRunner = null; testClassCleanupMethod.Statements.Add( new CodeAssignStatement( diff --git a/Reqnroll.Generator/UnitTestProvider/IUnitTestGeneratorProvider.cs b/Reqnroll.Generator/UnitTestProvider/IUnitTestGeneratorProvider.cs index c4bdd3dd2..ea46151c4 100644 --- a/Reqnroll.Generator/UnitTestProvider/IUnitTestGeneratorProvider.cs +++ b/Reqnroll.Generator/UnitTestProvider/IUnitTestGeneratorProvider.cs @@ -28,7 +28,5 @@ public interface IUnitTestGeneratorProvider void SetTestMethodAsRow(TestClassGenerationContext generationContext, CodeMemberMethod testMethod, string scenarioTitle, string exampleSetName, string variantName, IEnumerable> arguments); void MarkCodeMethodInvokeExpressionAsAwait(CodeMethodInvokeExpression expression); - - CodeExpression GetTestWorkerIdExpression(); } } diff --git a/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs b/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs index beb0cb675..cb2b92a4d 100644 --- a/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs +++ b/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs @@ -228,14 +228,5 @@ public void MarkCodeMethodInvokeExpressionAsAwait(CodeMethodInvokeExpression exp { CodeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); } - - public CodeExpression GetTestWorkerIdExpression() - { - // System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() - return new CodeMethodInvokeExpression( - new CodeVariableReferenceExpression("System.Threading.Thread.CurrentThread.ManagedThreadId"), - nameof(ToString) - ); - } } } diff --git a/Reqnroll.Generator/UnitTestProvider/NUnit3TestGeneratorProvider.cs b/Reqnroll.Generator/UnitTestProvider/NUnit3TestGeneratorProvider.cs index 66fc9568f..237aab2f9 100644 --- a/Reqnroll.Generator/UnitTestProvider/NUnit3TestGeneratorProvider.cs +++ b/Reqnroll.Generator/UnitTestProvider/NUnit3TestGeneratorProvider.cs @@ -154,8 +154,5 @@ public void MarkCodeMethodInvokeExpressionAsAwait(CodeMethodInvokeExpression exp { CodeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); } - - public CodeExpression GetTestWorkerIdExpression() - => new CodePropertyReferenceExpression(GetTestContextExpression(), TESTCONTEXT_WORKERID_PROPERTY); } } diff --git a/Reqnroll.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs b/Reqnroll.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs index 1355cdffd..7b15c51c8 100644 --- a/Reqnroll.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs +++ b/Reqnroll.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs @@ -33,7 +33,6 @@ public class XUnit2TestGeneratorProvider : IUnitTestGeneratorProvider protected internal const string IGNORE_TEST_CLASS = "IgnoreTestClass"; protected internal const string NONPARALLELIZABLE_COLLECTION_NAME = "ReqnrollNonParallelizableFeatures"; protected internal const string IASYNCLIFETIME_INTERFACE = "Xunit.IAsyncLifetime"; - protected internal const string XUNITPARALLELWORKERTRACKER_INSTANCE = "Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance"; public XUnit2TestGeneratorProvider(CodeDomHelper codeDomHelper) { @@ -178,19 +177,6 @@ public virtual void FinalizeTestClass(TestClassGenerationContext generationConte nameof(IObjectContainer.RegisterInstanceAs), new CodeTypeReference(OUTPUT_INTERFACE)), new CodeVariableReferenceExpression(OUTPUT_INTERFACE_FIELD_NAME))); - - // Wrap FeatureTearDown: - // var testWorkerId = .TestWorkerId; - // - // XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId); - generationContext.TestClassCleanupMethod.Statements.Insert(0, - new CodeVariableDeclarationStatement(typeof(string), "testWorkerId", - new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(generationContext.TestRunnerField.Name), "TestWorkerId"))); - generationContext.TestClassCleanupMethod.Statements.Add( - new CodeMethodInvokeExpression( - new CodeVariableReferenceExpression(GlobalNamespaceIfCSharp(XUNITPARALLELWORKERTRACKER_INSTANCE)), - "ReleaseWorker", - new CodeVariableReferenceExpression("testWorkerId"))); } protected virtual void IgnoreFeature(TestClassGenerationContext generationContext) @@ -414,14 +400,6 @@ public void MarkCodeMethodInvokeExpressionAsAwait(CodeMethodInvokeExpression exp CodeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); } - public CodeExpression GetTestWorkerIdExpression() - { - // XUnitParallelWorkerTracker.Instance.GetWorkerId() - return new CodeMethodInvokeExpression( - new CodeVariableReferenceExpression(GlobalNamespaceIfCSharp(XUNITPARALLELWORKERTRACKER_INSTANCE)), - "GetWorkerId"); - } - private string GlobalNamespaceIfCSharp(string typeName) { return CodeDomHelper.TargetLanguage == CodeDomProviderLanguage.CSharp ? "global::" + typeName : typeName; diff --git a/Reqnroll/ISyncTestRunner.cs b/Reqnroll/ISyncTestRunner.cs index f26dc301b..a095376d8 100644 --- a/Reqnroll/ISyncTestRunner.cs +++ b/Reqnroll/ISyncTestRunner.cs @@ -5,7 +5,7 @@ namespace Reqnroll public interface ISyncTestRunner { /// - /// The ID of the parallel test worker processing the current scenario. How the worker ID is obtained is dependent on the test execution framework. + /// The ID of the parallel test worker processing the current scenario. /// string TestWorkerId { get; } diff --git a/Reqnroll/ITestRunner.cs b/Reqnroll/ITestRunner.cs index b8a7daff1..e949a177f 100644 --- a/Reqnroll/ITestRunner.cs +++ b/Reqnroll/ITestRunner.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Reqnroll @@ -5,13 +6,14 @@ namespace Reqnroll public interface ITestRunner { /// - /// The ID of the parallel test worker processing the current scenario. How the worker ID is obtained is dependent on the test execution framework. + /// The ID of the parallel test worker processing the current scenario. /// string TestWorkerId { get; } FeatureContext FeatureContext { get; } ScenarioContext ScenarioContext { get; } ITestThreadContext TestThreadContext { get; } + [Obsolete("TestWorkerId is now managed by Reqnroll internally - Method will be removed in v3")] void InitializeTestRunner(string testWorkerId); Task OnTestRunStartAsync(); diff --git a/Reqnroll/ITestRunnerManager.cs b/Reqnroll/ITestRunnerManager.cs index eb96f1d6a..815926d33 100644 --- a/Reqnroll/ITestRunnerManager.cs +++ b/Reqnroll/ITestRunnerManager.cs @@ -10,7 +10,8 @@ public interface ITestRunnerManager Assembly TestAssembly { get; } Assembly[] BindingAssemblies { get; } bool IsMultiThreaded { get; } - ITestRunner GetTestRunner(string workerId); + ITestRunner GetTestRunner(); + void ReleaseTestThreadContext(ITestThreadContext testThreadContext); void Initialize(Assembly testAssembly); Task FireTestRunEndAsync(); Task FireTestRunStartAsync(); diff --git a/Reqnroll/Infrastructure/ContainerBuilder.cs b/Reqnroll/Infrastructure/ContainerBuilder.cs index eacb28c13..2655e6025 100644 --- a/Reqnroll/Infrastructure/ContainerBuilder.cs +++ b/Reqnroll/Infrastructure/ContainerBuilder.cs @@ -108,7 +108,7 @@ public virtual IObjectContainer CreateScenarioContainer(IObjectContainer testThr return scenarioContainer; } - public IObjectContainer CreateFeatureContainer(IObjectContainer testThreadContainer, FeatureInfo featureInfo) + public virtual IObjectContainer CreateFeatureContainer(IObjectContainer testThreadContainer, FeatureInfo featureInfo) { if (testThreadContainer == null) throw new ArgumentNullException(nameof(testThreadContainer)); diff --git a/Reqnroll/Infrastructure/TestThreadContainerInfo.cs b/Reqnroll/Infrastructure/TestThreadContainerInfo.cs new file mode 100644 index 000000000..d7c8e31a1 --- /dev/null +++ b/Reqnroll/Infrastructure/TestThreadContainerInfo.cs @@ -0,0 +1,21 @@ +using Reqnroll.BoDi; + +namespace Reqnroll.Infrastructure; + +internal class TestThreadContainerInfo +{ + internal string Id { get; } + + internal TestThreadContainerInfo(string id) + { + Id = id; + } + + internal static string GetId(IObjectContainer objectContainer) + { + if (!objectContainer.IsRegistered()) + return null; + var testThreadContainerInfo = objectContainer.Resolve(); + return testThreadContainerInfo.Id; + } +} diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index b215f3823..82c5bff7a 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Reqnroll.Bindings; using Reqnroll.Infrastructure; @@ -8,7 +9,7 @@ public class TestRunner : ITestRunner { private readonly ITestExecutionEngine _executionEngine; - public string TestWorkerId { get; private set; } + public string TestWorkerId => TestThreadContainerInfo.GetId(TestThreadContext.TestThreadContainer); public TestRunner(ITestExecutionEngine executionEngine) { @@ -26,9 +27,10 @@ public async Task OnTestRunStartAsync() await _executionEngine.OnTestRunStartAsync(); } + [Obsolete("TestWorkerId is now managed by Reqnroll internally - Method will be removed in v3")] public void InitializeTestRunner(string testWorkerId) { - TestWorkerId = testWorkerId; + // do nothing method will be removed } public async Task OnFeatureStartAsync(FeatureInfo featureInfo) diff --git a/Reqnroll/TestRunnerManager.cs b/Reqnroll/TestRunnerManager.cs index 861339de0..5d196fe72 100644 --- a/Reqnroll/TestRunnerManager.cs +++ b/Reqnroll/TestRunnerManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -17,19 +18,21 @@ namespace Reqnroll; public class TestRunnerManager : ITestRunnerManager { - 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; - private readonly ConcurrentDictionary _testRunnerRegistry = new(); + private readonly ConcurrentDictionary _availableTestWorkerContainers = new(); + private readonly ConcurrentDictionary _usedTestWorkerContainers = new(); + private int _nextTestWorkerContainerId; + public bool IsTestRunInitialized { get; private set; } private int _wasDisposed = 0; private int _wasSingletonInstanceDisabled = 0; private readonly object _createTestRunnerLockObject = new(); + private volatile ITestRunner _globalTestRunner; public Assembly TestAssembly { get; private set; } public Assembly[] BindingAssemblies { get; private set; } @@ -48,14 +51,13 @@ public TestRunnerManager(IObjectContainer globalContainer, IContainerBuilder con private int GetWorkerTestRunnerCount() { - var hasTestRunStartWorker = _testRunnerRegistry.ContainsKey(TestRunStartWorkerId); - return _testRunnerRegistry.Count - (hasTestRunStartWorker ? 1 : 0); + var hasTestRunStartWorker = _globalTestRunner != null; + return _usedTestWorkerContainers.Count - (hasTestRunStartWorker ? 1 : 0); } - public virtual ITestRunner CreateTestRunner(string testWorkerId = "default-worker") + public virtual ITestRunner CreateTestRunner() { var testRunner = CreateTestRunnerInstance(); - testRunner.InitializeTestRunner(testWorkerId); if (!IsTestRunInitialized) { @@ -72,6 +74,15 @@ public virtual ITestRunner CreateTestRunner(string testWorkerId = "default-worke return testRunner; } + public virtual void ReleaseTestThreadContext(ITestThreadContext testThreadContext) + { + var testThreadContainer = testThreadContext.TestThreadContainer; + if (!_usedTestWorkerContainers.TryRemove(testThreadContainer, out _)) + throw new InvalidOperationException($"TestThreadContext with id {TestThreadContainerInfo.GetId(testThreadContainer)} was already released"); + if (!_availableTestWorkerContainers.TryAdd(testThreadContainer, null)) + throw new InvalidOperationException($"TestThreadContext with id {TestThreadContainerInfo.GetId(testThreadContainer)} was released twice"); + } + protected virtual void InitializeBindingRegistry(ITestRunner testRunner) { BindingAssemblies = _bindingRegistryBuilder.GetBindingAssemblies(TestAssembly); @@ -100,10 +111,23 @@ protected internal virtual async Task OnDomainUnloadAsync() await DisposeAsync(); } + ITestRunner GetOrCreateGlobalTestRunner() + { + if (_globalTestRunner == null) + { + lock (_availableTestWorkerContainers) + { + if (_globalTestRunner == null) + _globalTestRunner = GetTestRunner(); + } + } + return _globalTestRunner; + } + public async Task FireTestRunEndAsync() { // this method must not be called multiple times - var onTestRunnerEndExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); + var onTestRunnerEndExecutionHost = GetOrCreateGlobalTestRunner(); if (onTestRunnerEndExecutionHost != null) await onTestRunnerEndExecutionHost.OnTestRunEndAsync(); } @@ -111,14 +135,38 @@ public async Task FireTestRunEndAsync() public async Task FireTestRunStartAsync() { // this method must not be called multiple times - var onTestRunnerStartExecutionHost = _testRunnerRegistry.Values.FirstOrDefault(); + var onTestRunnerStartExecutionHost = GetOrCreateGlobalTestRunner(); if (onTestRunnerStartExecutionHost != null) await onTestRunnerStartExecutionHost.OnTestRunStartAsync(); } protected virtual ITestRunner CreateTestRunnerInstance() { - var testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); + IObjectContainer testThreadContainer = null; + for (int i = 0; i < 5 && testThreadContainer == null; i++) // Try to get a available Container max 5 times + { + var items = _availableTestWorkerContainers.ToArray(); + if (items.Length == 0) + break; // No Containers are available + foreach (var item in items) + { + if (!_availableTestWorkerContainers.TryRemove(item.Key, out _)) + continue; // Container was already taken by another thread + testThreadContainer = item.Key; + break; + } + } + + if (testThreadContainer == null) + { + testThreadContainer = _containerBuilder.CreateTestThreadContainer(_globalContainer); + var id = Interlocked.Increment(ref _nextTestWorkerContainerId); + var testThreadContainerInfo = new TestThreadContainerInfo(id.ToString(CultureInfo.InvariantCulture)); + testThreadContainer.RegisterInstanceAs(testThreadContainerInfo); + } + + if (!_usedTestWorkerContainers.TryAdd(testThreadContainer, null)) + throw new InvalidOperationException($"TestThreadContext with id {TestThreadContainerInfo.GetId(testThreadContainer)} is already in usage"); return testThreadContainer.Resolve(); } @@ -128,12 +176,11 @@ public void Initialize(Assembly assignedTestAssembly) TestAssembly = assignedTestAssembly; } - public virtual ITestRunner GetTestRunner(string testWorkerId) + public virtual ITestRunner GetTestRunner() { - testWorkerId ??= Guid.NewGuid().ToString(); //Creates a Test Runner with a unique test thread try { - return GetTestRunnerWithoutExceptionHandling(testWorkerId); + return GetTestRunnerWithoutExceptionHandling(); } catch (Exception ex) { @@ -142,22 +189,11 @@ public virtual ITestRunner GetTestRunner(string testWorkerId) } } - private ITestRunner GetTestRunnerWithoutExceptionHandling(string testWorkerId) + private ITestRunner GetTestRunnerWithoutExceptionHandling() { - if (testWorkerId == null) - throw new ArgumentNullException(nameof(testWorkerId)); + var testRunner = CreateTestRunner(); - bool wasAdded = false; - - var testRunner = _testRunnerRegistry.GetOrAdd( - testWorkerId, - workerId => - { - wasAdded = true; - return CreateTestRunner(workerId); - }); - - if (wasAdded && IsMultiThreaded && Interlocked.CompareExchange(ref _wasSingletonInstanceDisabled, 1, 0) == 0) + if (IsMultiThreaded && Interlocked.CompareExchange(ref _wasSingletonInstanceDisabled, 1, 0) == 0) { FeatureContext.DisableSingletonInstance(); ScenarioContext.DisableSingletonInstance(); @@ -173,15 +209,32 @@ public virtual async Task DisposeAsync() { await FireTestRunEndAsync(); - foreach (var testRunner in _testRunnerRegistry.Values) + if (_globalTestRunner != null) { - testRunner.TestThreadContext.TestThreadContainer.Dispose(); + ReleaseTestThreadContext(_globalTestRunner.TestThreadContext); } - // this call dispose on this object, but the disposeLockObj will avoid double execution + var items = _availableTestWorkerContainers.ToArray(); + while (items.Length > 0) + { + foreach (var item in items) + { + item.Key.Dispose(); + _availableTestWorkerContainers.TryRemove(item.Key, out _); + } + items = _availableTestWorkerContainers.ToArray(); + } + + var notReleasedRunner = _usedTestWorkerContainers.ToArray(); + if (notReleasedRunner.Length > 0) + { + var errorText = $"Found {notReleasedRunner.Length} not released TestRunners (ids: {string.Join(",", notReleasedRunner.Select(x => TestThreadContainerInfo.GetId(x.Key)))})"; + _globalContainer.Resolve().TraceWarning(errorText); + } + + // this call dispose on this object, but _wasDisposed will avoid double execution _globalContainer.Dispose(); - _testRunnerRegistry.Clear(); OnTestRunnerManagerDisposed(this); } } @@ -256,20 +309,24 @@ public static async Task OnTestRunEndAsync(Assembly testAssembly = null, IContai } } - public static async Task OnTestRunStartAsync(Assembly testAssembly = null, string testWorkerId = null, IContainerBuilder containerBuilder = null) + public static async Task OnTestRunStartAsync(Assembly testAssembly = 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) + public static ITestRunner GetTestRunnerForAssembly(Assembly testAssembly = null, IContainerBuilder containerBuilder = null) { testAssembly ??= GetCallingAssembly(); var testRunnerManager = GetTestRunnerManager(testAssembly, containerBuilder); - return testRunnerManager.GetTestRunner(testWorkerId); + return testRunnerManager.GetTestRunner(); + } + + public static void ReleaseTestRunner(ITestRunner testRunner) + { + testRunner.TestThreadContext.TestThreadContainer.Resolve().ReleaseTestThreadContext(testRunner.TestThreadContext); } internal static async Task ResetAsync() diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestFeatureGeneratorTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestFeatureGeneratorTests.cs index 0964fed34..855d66ddc 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestFeatureGeneratorTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestFeatureGeneratorTests.cs @@ -28,7 +28,6 @@ protected virtual void SetupInternal() { Container = new GeneratorContainerBuilder().CreateContainer(new ReqnrollConfigurationHolder(ConfigSource.Default, null), new ProjectSettings(), Enumerable.Empty()); UnitTestGeneratorProviderMock = new Mock(); - UnitTestGeneratorProviderMock.Setup(utp => utp.GetTestWorkerIdExpression()).Returns(new CodePrimitiveExpression(null)); Container.RegisterInstanceAs(UnitTestGeneratorProviderMock.Object); } diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs deleted file mode 100644 index 3305037fa..000000000 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Reqnroll.BoDi; -using FluentAssertions; -using Moq; -using Xunit; -using Reqnroll.Bindings.Discovery; -using Reqnroll.Configuration; -using Reqnroll.Infrastructure; -using Reqnroll.RuntimeTests.Infrastructure; -using Reqnroll.Tracing; - -namespace Reqnroll.RuntimeTests -{ - - public class TestRunnerManagerRunnerCreationTests - { - private readonly Mock testRunnerFake = new Mock(); - private readonly Mock objectContainerStub = new Mock(); - private readonly Mock globalObjectContainerStub = new Mock(); - private readonly Reqnroll.Configuration.ReqnrollConfiguration _reqnrollConfigurationStub = ConfigurationLoader.GetDefault(); - private readonly Assembly anAssembly = Assembly.GetExecutingAssembly(); - private readonly Assembly anotherAssembly = typeof(TestRunnerManager).Assembly; - - private TestRunnerManager CreateTestRunnerFactory() - { - objectContainerStub.Setup(o => o.Resolve()).Returns(testRunnerFake.Object); - globalObjectContainerStub.Setup(o => o.Resolve()).Returns(new BindingAssemblyLoader()); - globalObjectContainerStub.Setup(o => o.Resolve()).Returns(new Mock().Object); - - var testRunContainerBuilderStub = new Mock(); - testRunContainerBuilderStub.Setup(b => b.CreateTestThreadContainer(It.IsAny())) - .Returns(objectContainerStub.Object); - - var runtimeBindingRegistryBuilderMock = new Mock(); - - var testRunnerManager = new TestRunnerManager(globalObjectContainerStub.Object, testRunContainerBuilderStub.Object, _reqnrollConfigurationStub, runtimeBindingRegistryBuilderMock.Object, - Mock.Of()); - testRunnerManager.Initialize(anAssembly); - return testRunnerManager; - } - - [Fact] - public void Should_resolve_a_test_runner() - { - var factory = CreateTestRunnerFactory(); - - var testRunner = factory.CreateTestRunner("0"); - testRunner.Should().NotBeNull(); - } - - [Fact] - public void Should_initialize_test_runner_with_the_provided_assembly() - { - var factory = CreateTestRunnerFactory(); - factory.CreateTestRunner("0"); - - factory.IsTestRunInitialized.Should().BeTrue(); - } - - [Fact] - public void Should_initialize_test_runner_with_additional_step_assemblies() - { - var factory = CreateTestRunnerFactory(); - _reqnrollConfigurationStub.AddAdditionalStepAssembly(anotherAssembly); - - factory.CreateTestRunner("0"); - - factory.IsTestRunInitialized.Should().BeTrue(); - } - - [Fact] - public void Should_initialize_test_runner_with_the_provided_assembly_even_if_there_are_additional_ones() - { - var factory = CreateTestRunnerFactory(); - _reqnrollConfigurationStub.AddAdditionalStepAssembly(anotherAssembly); - - factory.CreateTestRunner("0"); - - factory.IsTestRunInitialized.Should().BeTrue(); - } - - - [Fact] - public async Task Should_resolve_a_test_runner_specific_test_tracer() - { - //This test can't run in NCrunch as when NCrunch runs the tests it will disable the ability to get different test runners for each thread - //as it manages the parallelisation - //see https://github.com/reqnroll/Reqnroll/issues/638 - if (!TestEnvironmentHelper.IsBeingRunByNCrunch()) - { - var testRunner1 = TestRunnerManager.GetTestRunnerForAssembly(anAssembly, "0", 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)); - await testRunner1.OnScenarioStartAsync(); - var tracer1 = testRunner1.ScenarioContext.ScenarioContainer.Resolve(); - - var testRunner2 = TestRunnerManager.GetTestRunnerForAssembly(anAssembly, "1", 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)); - await testRunner1.OnScenarioStartAsync(); - var tracer2 = testRunner2.ScenarioContext.ScenarioContainer.Resolve(); - - tracer1.Should().NotBeSameAs(tracer2); - } - } - } -} diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerStaticApiTest.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerStaticApiTest.cs index 64b406727..0c22ad7e3 100644 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerStaticApiTest.cs +++ b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerStaticApiTest.cs @@ -8,8 +8,8 @@ namespace Reqnroll.RuntimeTests { public class TestRunnerManagerStaticApiTest : IAsyncLifetime { - private readonly Assembly thisAssembly = Assembly.GetExecutingAssembly(); - private readonly Assembly anotherAssembly = typeof(TestRunnerManager).Assembly; + private readonly Assembly _anAssembly = Assembly.GetExecutingAssembly(); + private readonly Assembly _anotherAssembly = typeof(TestRunnerManager).Assembly; public async Task InitializeAsync() { @@ -19,19 +19,25 @@ public async Task InitializeAsync() [Fact] public async Task GetTestRunner_without_arguments_should_return_TestRunner_instance() { - var testRunner = TestRunnerManager.GetTestRunnerForAssembly(testWorkerId: "0", containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner = TestRunnerManager.GetTestRunnerForAssembly(containerBuilder: new RuntimeTestsContainerBuilder()); testRunner.Should().NotBeNull(); testRunner.Should().BeOfType(); + + TestRunnerManager.ReleaseTestRunner(testRunner); } [Fact] public async Task GetTestRunner_should_return_different_instances_for_different_assemblies() { - var testRunner1 = TestRunnerManager.GetTestRunnerForAssembly(thisAssembly, "0", containerBuilder: new RuntimeTestsContainerBuilder()); - var testRunner2 = TestRunnerManager.GetTestRunnerForAssembly(anotherAssembly, "0", containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner1 = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner2 = TestRunnerManager.GetTestRunnerForAssembly(_anotherAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); testRunner1.Should().NotBe(testRunner2); + testRunner1.TestThreadContext.Should().NotBe(testRunner2.TestThreadContext); + + TestRunnerManager.ReleaseTestRunner(testRunner1); + TestRunnerManager.ReleaseTestRunner(testRunner2); } [Fact] @@ -40,7 +46,7 @@ public async Task GetTestRunnerManager_without_arguments_should_return_an_instan var testRunnerManager = TestRunnerManager.GetTestRunnerManager(containerBuilder: new RuntimeTestsContainerBuilder()); testRunnerManager.Should().NotBeNull(); - testRunnerManager.TestAssembly.Should().BeSameAs(thisAssembly); + testRunnerManager.TestAssembly.Should().BeSameAs(_anAssembly); } [Fact] @@ -81,10 +87,11 @@ public static void BeforeTestRun() public async Task OnTestRunEnd_should_fire_AfterTestRun_events() { // make sure a test runner is initialized - TestRunnerManager.GetTestRunnerForAssembly(thisAssembly, "0", containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + TestRunnerManager.ReleaseTestRunner(testRunner); AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset - await TestRunnerManager.OnTestRunEndAsync(thisAssembly); + await TestRunnerManager.OnTestRunEndAsync(_anAssembly); AfterTestRunTestBinding.AfterTestRunCallCount.Should().Be(1); } @@ -93,7 +100,8 @@ public async Task OnTestRunEnd_should_fire_AfterTestRun_events() public async Task OnTestRunEnd_without_arguments_should_fire_AfterTestRun_events_for_calling_assembly() { // make sure a test runner is initialized - TestRunnerManager.GetTestRunnerForAssembly(thisAssembly, "0", containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + TestRunnerManager.ReleaseTestRunner(testRunner); AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset await TestRunnerManager.OnTestRunEndAsync(); @@ -105,20 +113,32 @@ public async Task OnTestRunEnd_without_arguments_should_fire_AfterTestRun_events public async Task OnTestRunEnd_should_not_fire_AfterTestRun_events_multiple_times() { // make sure a test runner is initialized - TestRunnerManager.GetTestRunnerForAssembly(thisAssembly, "0", containerBuilder: new RuntimeTestsContainerBuilder()); + var testRunner = TestRunnerManager.GetTestRunnerForAssembly(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + TestRunnerManager.ReleaseTestRunner(testRunner); AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset - await TestRunnerManager.OnTestRunEndAsync(thisAssembly); - await TestRunnerManager.OnTestRunEndAsync(thisAssembly); + await TestRunnerManager.OnTestRunEndAsync(_anAssembly); + await TestRunnerManager.OnTestRunEndAsync(_anAssembly); AfterTestRunTestBinding.AfterTestRunCallCount.Should().Be(1); } + [Fact] + public async Task OnTestRunStartAsync_should_initialize_binding_registry() + { + var testRunnerManager = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + testRunnerManager.IsTestRunInitialized.Should().BeFalse("binding registry should not be initialized initially"); + + await TestRunnerManager.OnTestRunStartAsync(_anAssembly); + + testRunnerManager.IsTestRunInitialized.Should().BeTrue("binding registry be initialized"); + } + [Fact] public async Task OnTestRunStart_should_fire_BeforeTestRun_events() { BeforeTestRunTestBinding.BeforeTestRunCallCount = 0; //reset - await TestRunnerManager.OnTestRunStartAsync(thisAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + await TestRunnerManager.OnTestRunStartAsync(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); BeforeTestRunTestBinding.BeforeTestRunCallCount.Should().Be(1); } @@ -136,28 +156,12 @@ public async Task OnTestRunStart_without_arguments_should_fire_BeforeTestRun_eve public async Task OnTestRunStart_should_not_fire_BeforeTestRun_events_multiple_times() { BeforeTestRunTestBinding.BeforeTestRunCallCount = 0; //reset - await TestRunnerManager.OnTestRunStartAsync(thisAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); - await TestRunnerManager.OnTestRunStartAsync(thisAssembly); + await TestRunnerManager.OnTestRunStartAsync(_anAssembly, containerBuilder: new RuntimeTestsContainerBuilder()); + await TestRunnerManager.OnTestRunStartAsync(_anAssembly); BeforeTestRunTestBinding.BeforeTestRunCallCount.Should().Be(1); } - //[Fact] - //public void DomainUnload_event_should_not_fire_AfterTestRun_events_multiple_times_after_OnTestRunEnd() - //{ - // // make sure a test runner is initialized - // TestRunnerManager.GetTestRunner(thisAssembly); - - // AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset - // TestRunnerManager.OnTestRunEnd(thisAssembly); - - // // simulating DomainUnload event - // var trm = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(thisAssembly); - // trm.OnDomainUnload(); - - // AfterTestRunTestBinding.AfterTestRunCallCount.Should().Be(1); - //} - public async Task DisposeAsync() { } diff --git a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs index cd2429250..355bbe724 100644 --- a/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs +++ b/Tests/Reqnroll.RuntimeTests/TestRunnerManagerTests.cs @@ -1,56 +1,74 @@ using System; +using System.Globalization; using System.Reflection; using System.Threading.Tasks; using FluentAssertions; +using Reqnroll.Tracing; using Xunit; namespace Reqnroll.RuntimeTests; -public class TestRunnerManagerTests +public class TestRunnerManagerTests : IAsyncLifetime { private readonly Assembly _anAssembly = Assembly.GetExecutingAssembly(); - private readonly TestRunnerManager _testRunnerManager; + private TestRunnerManager _testRunnerManager; - public TestRunnerManagerTests() + public async Task InitializeAsync() { + await TestRunnerManager.ResetAsync(); _testRunnerManager = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(_anAssembly, new RuntimeTestsContainerBuilder()); } + public async Task DisposeAsync() + { + //nop; + } + [Fact] public void CreateTestRunner_should_be_able_to_create_a_TestRunner() { - var testRunner = _testRunnerManager.CreateTestRunner("0"); + var testRunner = _testRunnerManager.CreateTestRunner(); testRunner.Should().NotBeNull(); testRunner.Should().BeOfType(); + + TestRunnerManager.ReleaseTestRunner(testRunner); } [Fact] public void GetTestRunner_should_be_able_to_create_a_TestRunner() { - var testRunner = _testRunnerManager.GetTestRunner("0"); + var testRunner = _testRunnerManager.GetTestRunner(); testRunner.Should().NotBeNull(); testRunner.Should().BeOfType(); + + TestRunnerManager.ReleaseTestRunner(testRunner); } [Fact] - public void GetTestRunner_should_cache_instance() + public void Should_return_different_thread_ids_for_different_instances() { - var testRunner1 = _testRunnerManager.GetTestRunner("0"); - var testRunner2 = _testRunnerManager.GetTestRunner("0"); + // Use an explicit new ITestRunnerManager to make sure that the Ids are created in a new way. + var container = new RuntimeTestsContainerBuilder().CreateGlobalContainer(_anAssembly); + var testRunnerManager = container.Resolve(); + testRunnerManager.Initialize(_anAssembly); + var testRunner1 = testRunnerManager.GetTestRunner(); + var testRunner2 = testRunnerManager.GetTestRunner(); - testRunner1.Should().Be(testRunner2); - } + testRunner1.Should().NotBe(testRunner2); + testRunner1.TestWorkerId.Should().Be("1"); + testRunner2.TestWorkerId.Should().Be("2"); - [Fact] - public void Should_return_different_instances_for_different_thread_ids() - { - var testRunner1 = _testRunnerManager.GetTestRunner("0"); - var testRunner2 = _testRunnerManager.GetTestRunner("1"); + TestRunnerManager.ReleaseTestRunner(testRunner1); + TestRunnerManager.ReleaseTestRunner(testRunner2); - testRunner1.Should().NotBe(testRunner2); + // TestRunner3 reused an existing TestThreadContainer, so the Id should be one of the previously created ones + var testRunner3 = testRunnerManager.GetTestRunner(); + testRunner3.TestWorkerId.Should().Match(x => x == "1" || x == "2"); + + TestRunnerManager.ReleaseTestRunner(testRunner3); } class DisposableClass : IDisposable @@ -65,8 +83,8 @@ public void Dispose() [Fact] public async Task Should_dispose_test_thread_container_at_after_test_run() { - var testRunner1 = _testRunnerManager.GetTestRunner("0"); - var testRunner2 = _testRunnerManager.GetTestRunner("1"); + var testRunner1 = _testRunnerManager.GetTestRunner(); + var testRunner2 = _testRunnerManager.GetTestRunner(); var disposableClass1 = new DisposableClass(); testRunner1.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass1, dispose: true); @@ -74,9 +92,43 @@ public async Task Should_dispose_test_thread_container_at_after_test_run() var disposableClass2 = new DisposableClass(); testRunner2.TestThreadContext.TestThreadContainer.RegisterInstanceAs(disposableClass2, dispose: true); + TestRunnerManager.ReleaseTestRunner(testRunner1); + TestRunnerManager.ReleaseTestRunner(testRunner2); + await TestRunnerManager.OnTestRunEndAsync(_anAssembly); disposableClass1.IsDisposed.Should().BeTrue(); disposableClass2.IsDisposed.Should().BeTrue(); } + + [Fact] + public void First_call_to_CreateTestRunner_should_initialize_binding_registry() + { + _testRunnerManager.IsTestRunInitialized.Should().BeFalse("binding registry should not be initialized initially"); + + _testRunnerManager.CreateTestRunner(); + + _testRunnerManager.IsTestRunInitialized.Should().BeTrue("binding registry be initialized"); + } + + [Fact] + 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)); + 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)); + await testRunner1.OnScenarioStartAsync(); + var tracer2 = testRunner2.ScenarioContext.ScenarioContainer.Resolve(); + + tracer1.Should().NotBeSameAs(tracer2); + + TestRunnerManager.ReleaseTestRunner(testRunner1); + TestRunnerManager.ReleaseTestRunner(testRunner2); + } } \ No newline at end of file