From d56b04fbafea3b0a609f3be4382c0b1293683c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Thu, 3 Oct 2024 18:30:06 +0200 Subject: [PATCH] Prototype to support method-level parallelization for MsTest --- .../Generation/UnitTestFeatureGenerator.cs | 122 ++++++++++++++++-- .../MsTestGeneratorProvider.cs | 15 ++- .../MsTest/MethodLevelParallelisation.feature | 16 +++ 3 files changed, 140 insertions(+), 13 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index a8b581679..a64f44611 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -189,16 +189,16 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio //FeatureInfo featureInfo = new FeatureInfo("xxxx"); testClassInitializeMethod.Statements.Add( new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo", - new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), - new CodeObjectCreateExpression(typeof(CultureInfo), - new CodePrimitiveExpression(generationContext.Feature.Language)), - new CodePrimitiveExpression(generationContext.Document.DocumentLocation?.FeatureFolderPath), - new CodePrimitiveExpression(generationContext.Feature.Name), - new CodePrimitiveExpression(generationContext.Feature.Description), - new CodeFieldReferenceExpression( - new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(Reqnroll.ProgrammingLanguage))), - _codeDomHelper.TargetLanguage.ToString()), - new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)))); + new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), + new CodeObjectCreateExpression(typeof(CultureInfo), + new CodePrimitiveExpression(generationContext.Feature.Language)), + new CodePrimitiveExpression(generationContext.Document.DocumentLocation?.FeatureFolderPath), + new CodePrimitiveExpression(generationContext.Feature.Name), + new CodePrimitiveExpression(generationContext.Feature.Description), + new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(Reqnroll.ProgrammingLanguage))), + _codeDomHelper.TargetLanguage.ToString()), + new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)))); //await testRunner.OnFeatureStartAsync(featureInfo); var onFeatureStartExpression = new CodeMethodInvokeExpression( @@ -257,6 +257,97 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont _codeDomHelper.MarkCodeMemberMethodAsAsync(testInitializeMethod); _testGeneratorProvider.SetTestInitializeMethod(generationContext); + + if (generationContext.UnitTestGeneratorProvider is not MsTestGeneratorProvider) + return; // only MsTest is implemented in this prototype + + // Step 4: Obtain the test runner for executing a single test + + // testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(); + + var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); + + var getTestRunnerExpression = new CodeMethodInvokeExpression( + new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))), + nameof(TestRunnerManager.GetTestRunnerForAssembly)); + + testInitializeMethod.Statements.Add( + new CodeAssignStatement( + testRunnerField, + getTestRunnerExpression)); + + + // Step 5 (part 1): "Finish" current feature if needed & "Start" feature if needed + // The similar code in custom test runner codes is not needed + // The feature initialization steps are copied from TestClassInitializeMethod + + //if (testRunner.FeatureContext != null && testRunner.FeatureContext.FeatureInfo.Title != "") + // await testRunner.OnFeatureEndAsync(); // finish if different + + var featureContextExpression = new CodePropertyReferenceExpression( + testRunnerField, + "FeatureContext"); + + var onFeatureEndAsyncExpression = new CodeMethodInvokeExpression( + testRunnerField, + nameof(ITestRunner.OnFeatureEndAsync)); + _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(onFeatureEndAsyncExpression); + + //if (testRunner.FeatureContext != null && testRunner.FeatureContext.FeatureInfo.Title != "") + // await testRunner.OnFeatureEndAsync(); // finish if different + testInitializeMethod.Statements.Add( + new CodeConditionStatement( + new CodeBinaryOperatorExpression( + new CodeBinaryOperatorExpression( + featureContextExpression, + CodeBinaryOperatorType.IdentityInequality, + new CodePrimitiveExpression(null)), + CodeBinaryOperatorType.BooleanAnd, + new CodeBinaryOperatorExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + featureContextExpression, + "FeatureInfo"), + "Title"), + CodeBinaryOperatorType.IdentityInequality, + new CodePrimitiveExpression(generationContext.Feature.Name))), + new CodeExpressionStatement( + onFeatureEndAsyncExpression))); + + + //if (testRunner.FeatureContext == null) { + // FeatureInfo featureInfo = new FeatureInfo("xxxx"); + // await testRunner.OnFeatureStartAsync(featureInfo); + //} + + var featureInfoInitializeStatement = + new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo", + new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), + new CodeObjectCreateExpression(typeof(CultureInfo), + new CodePrimitiveExpression(generationContext.Feature.Language)), + new CodePrimitiveExpression(generationContext.Document.DocumentLocation?.FeatureFolderPath), + new CodePrimitiveExpression(generationContext.Feature.Name), + new CodePrimitiveExpression(generationContext.Feature.Description), + new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(Reqnroll.ProgrammingLanguage))), + _codeDomHelper.TargetLanguage.ToString()), + new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME))); + + var onFeatureStartExpression = new CodeMethodInvokeExpression( + testRunnerField, + nameof(ITestRunner.OnFeatureStartAsync), + new CodeVariableReferenceExpression("featureInfo")); + _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(onFeatureStartExpression); + + testInitializeMethod.Statements.Add( + new CodeConditionStatement( + new CodeBinaryOperatorExpression( + featureContextExpression, + CodeBinaryOperatorType.IdentityEquality, + new CodePrimitiveExpression(null)), + featureInfoInitializeStatement, + new CodeExpressionStatement( + onFeatureStartExpression))); } private void SetupTestCleanupMethod(TestClassGenerationContext generationContext) @@ -280,6 +371,17 @@ private void SetupTestCleanupMethod(TestClassGenerationContext generationContext _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); testCleanupMethod.Statements.Add(expression); + + if (generationContext.UnitTestGeneratorProvider is not MsTestGeneratorProvider) + return; // only MsTest is implemented in this prototype + + // Step 6: "Release" the TestRunner, so that other threads can pick it up (moved from TestClassCleanupMethod) + // TestRunnerManager.ReleaseTestRunner(testRunner); + testCleanupMethod.Statements.Add( + new CodeMethodInvokeExpression( + new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))), + nameof(TestRunnerManager.ReleaseTestRunner), + testRunnerField)); } private void SetupScenarioInitializeMethod(TestClassGenerationContext generationContext) diff --git a/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs b/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs index dcce50673..df8e4a57d 100644 --- a/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs +++ b/Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs @@ -103,12 +103,16 @@ public virtual void SetTestClassNonParallelizable(TestClassGenerationContext gen public virtual void SetTestClassInitializeMethod(TestClassGenerationContext generationContext) { generationContext.TestClassInitializeMethod.Attributes |= MemberAttributes.Static; - generationContext.TestRunnerField.Attributes |= MemberAttributes.Static; + // Step 1: make 'testRunner' instance field + //generationContext.TestRunnerField.Attributes |= MemberAttributes.Static; generationContext.TestClassInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression( TESTCONTEXT_TYPE, "testContext")); CodeDomHelper.AddAttribute(generationContext.TestClassInitializeMethod, TESTFIXTURESETUP_ATTR); + + // Step 2: Remove TestClassInitializeMethod (not needed) + generationContext.TestClass.Members.Remove(generationContext.TestClassInitializeMethod); } public void SetTestClassCleanupMethod(TestClassGenerationContext generationContext) @@ -117,13 +121,18 @@ public void SetTestClassCleanupMethod(TestClassGenerationContext generationConte // [Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupBehavior.EndOfClass)] var attribute = CodeDomHelper.AddAttribute(generationContext.TestClassCleanupMethod, TESTFIXTURETEARDOWN_ATTR); attribute.Arguments.Add(new CodeAttributeArgument(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(CLASSCLEANUPBEHAVIOR_ENUM), CLASSCLEANUPBEHAVIOR_ENDOFCLASS))); + + // Step 3: Remove TestClassCleanupMethod (not needed) + generationContext.TestClass.Members.Remove(generationContext.TestClassCleanupMethod); } public virtual void SetTestInitializeMethod(TestClassGenerationContext generationContext) { CodeDomHelper.AddAttribute(generationContext.TestInitializeMethod, TESTSETUP_ATTR); - FixTestRunOrderingIssue(generationContext); + + // Step 5 (part 2): remove feature handling from here as it is moved to UnitTestGenerator.SetupTestInitializeMethod + //FixTestRunOrderingIssue(generationContext); } protected virtual void FixTestRunOrderingIssue(TestClassGenerationContext generationContext) @@ -146,7 +155,7 @@ protected virtual void FixTestRunOrderingIssue(TestClassGenerationContext genera new CodePrimitiveExpression(null)); CodeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(callTestClassInitializeMethodExpression); - + generationContext.TestInitializeMethod.Statements.Add( new CodeConditionStatement( new CodeBinaryOperatorExpression( diff --git a/Tests/Reqnroll.Specs/Features/UnitTestProviderSpecific/MsTest/MethodLevelParallelisation.feature b/Tests/Reqnroll.Specs/Features/UnitTestProviderSpecific/MsTest/MethodLevelParallelisation.feature index 6ff86a21d..72b9b5e61 100644 --- a/Tests/Reqnroll.Specs/Features/UnitTestProviderSpecific/MsTest/MethodLevelParallelisation.feature +++ b/Tests/Reqnroll.Specs/Features/UnitTestProviderSpecific/MsTest/MethodLevelParallelisation.feature @@ -66,6 +66,8 @@ public class TraceSteps featureData.Duration.ElapsedMilliseconds.Should().BeLessThan(9 * WaitTimeInMS, because: "Test should be processed (parallel) in time"); } + public static int startIndex = 0; + [When(@"I do something in Scenario '(.*)'")] void WhenIDoSomething(string scenario) { @@ -82,6 +84,10 @@ public class TraceSteps featureData.BindingInstances.TryAdd(this, 1); var currentStartIndex = Interlocked.Increment(ref featureData.StepCount); _traceListener.WriteTestOutput($"Start index: {currentStartIndex}, Worker: {_testRunner.TestWorkerId}"); + + var currentStartIndex2 = System.Threading.Interlocked.Increment(ref startIndex); + _traceListener.WriteTestOutput($"Start index2: {currentStartIndex2}, Worker: {_testRunner.TestWorkerId}"); + Thread.Sleep(WaitTimeInMS); var afterStartIndex = featureData.StepCount; if (afterStartIndex == currentStartIndex) @@ -92,6 +98,16 @@ public class TraceSteps { _traceListener.WriteTestOutput("Was parallel"); } + + var afterStartIndex2 = startIndex; + if (afterStartIndex2 == currentStartIndex2) + { + _traceListener.WriteTestOutput("XWas not parallel"); + } + else + { + _traceListener.WriteTestOutput("XWas parallel"); + } } } """