Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for scenario-level parallel execution #277

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d682eb2
import tests from PR 119
gasparnagy Oct 3, 2024
83624ed
small code cleanup
gasparnagy Oct 3, 2024
d56b04f
Prototype to support method-level parallelization for MsTest
gasparnagy Oct 3, 2024
cb697fc
Add SystemTests for parallel execution
gasparnagy Oct 4, 2024
5290445
Remove obsolete Generator unit test
gasparnagy Oct 4, 2024
195f11d
make sure that all remaining after feature hooks are called
gasparnagy Oct 4, 2024
f2503f6
remove invalid expectations of specs test about feature context
gasparnagy Oct 4, 2024
1888c88
Support method-level parallel execution for NUnit
gasparnagy Oct 4, 2024
c4880d7
Support new execution structure for xUnit
gasparnagy Oct 7, 2024
37bbfd5
code cleanup
gasparnagy Oct 7, 2024
3a4dc53
generate featureInfo as field
gasparnagy Oct 7, 2024
f7b0d77
slow down parallel test to avoid false errors (tests are so fast on L…
gasparnagy Oct 7, 2024
6a19c96
Merge remote-tracking branch 'origin/main' into scenario-level-parall…
gasparnagy Oct 7, 2024
8fe31a5
fix VB compatibility
gasparnagy Oct 7, 2024
8a529ae
update CHANGELOG
gasparnagy Oct 7, 2024
f7a0126
update CHANGELOG
gasparnagy Oct 7, 2024
c93614b
remove obsolete specs feature
gasparnagy Oct 7, 2024
89760f7
Update docs
gasparnagy Oct 7, 2024
ba07c63
Merge branch 'main' into scenario-level-parallel-prototype
gasparnagy Oct 14, 2024
de0bc79
fix CHANGELOG
gasparnagy Oct 14, 2024
dea281c
refactor Log class generation for system tests
gasparnagy Oct 14, 2024
1e4dacb
Improve System Tests parallel handling
gasparnagy Oct 14, 2024
5456a0f
apply hinting to choose the test runners in more optimal way
gasparnagy Oct 14, 2024
18cc5b4
fix unit test warning
gasparnagy Oct 14, 2024
b989531
Add extra check to ensure scenario-level parallelization
gasparnagy Oct 14, 2024
0e71e2c
Merge branch 'main' into scenario-level-parallel-prototype
gasparnagy Oct 14, 2024
c569e42
Use feature info to optimize test runner reuse
gasparnagy Oct 15, 2024
971e6bf
fix a comment
gasparnagy Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

## Improvements:

* Support scenario-level (method-level) parallel execution (#119, #277)

## Bug fixes:

*Contributors of this release (in alphabetical order): @obligaron*
*Contributors of this release (in alphabetical order):* @gasparnagy, @obligaron

# v2.1.1 - 2024-10-08

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@
using global::System.Runtime.CompilerServices;
using System.Threading.Tasks;

[assembly: NUnit.Framework.FixtureLifeCycle(NUnit.Framework.LifeCycle.InstancePerTestCase)]

[GeneratedCode("Reqnroll", "REQNROLL_VERSION")]
[global::NUnit.Framework.SetUpFixture]
public class PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks
public static class PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks
{
[global::NUnit.Framework.OneTimeSetUp]
[MethodImpl(MethodImplOptions.NoInlining)]
public async Task AssemblyInitializeAsync()
public static async Task AssemblyInitializeAsync()
{
var currentAssembly = typeof(PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks).Assembly;
await global::Reqnroll.TestRunnerManager.OnTestRunStartAsync(currentAssembly);
}

[global::NUnit.Framework.OneTimeTearDown]
[MethodImpl(MethodImplOptions.NoInlining)]
public async ValueTask AssemblyCleanupAsync()
public static async ValueTask AssemblyCleanupAsync()
{
var currentAssembly = typeof(PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks).Assembly;
await global::Reqnroll.TestRunnerManager.OnTestRunEndAsync(currentAssembly);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ Imports System.CodeDom.Compiler
Imports System.Reflection
Imports System.Runtime.CompilerServices

<assembly: NUnit.Framework.FixtureLifeCycle(NUnit.Framework.LifeCycle.InstancePerTestCase)>

<GeneratedCode("Reqnroll", "REQNROLL_VERSION")>
<SetUpFixture>
Public NotInheritable Class PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks
<OneTimeSetUp>
<MethodImpl(MethodImplOptions.NoInlining)>
Public Async Function AssemblyInitializeAsync() As Task
Public Shared Async Function AssemblyInitializeAsync() As Task
Dim currentAssembly As Assembly = GetType(PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks).Assembly
Await Global.Reqnroll.TestRunnerManager.OnTestRunStartAsync(currentAssembly)
End Function

<OneTimeTearDown>
<MethodImpl(MethodImplOptions.NoInlining)>
Public Async Function AssemblyCleanupAsync() As Task
Public Shared Async Function AssemblyCleanupAsync() As Task
Dim currentAssembly As Assembly = GetType(PROJECT_ROOT_NAMESPACE_NUnitAssemblyHooks).Assembly
Await Global.Reqnroll.TestRunnerManager.OnTestRunEndAsync(currentAssembly)
End Function
Expand Down
12 changes: 12 additions & 0 deletions Reqnroll.Generator/CodeDom/CodeDomHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,18 @@ public string GetGlobalizedTypeName(Type type)
// Global namespaces not yet supported in VB
return type.FullName!;
}

public CodeExpression CreateOptionalArgumentExpression(string parameterName, CodeVariableReferenceExpression valueExpression)
{
switch (TargetLanguage)
{
case CodeDomProviderLanguage.CSharp:
return new CodeSnippetExpression($"{parameterName}: {valueExpression.VariableName}");
case CodeDomProviderLanguage.VB:
return new CodeSnippetExpression($"{parameterName} := {valueExpression.VariableName}");
}
return valueExpression;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions Reqnroll.Generator/Generation/GeneratorConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class GeneratorConstants
public const string TESTCLASS_CLEANUP_NAME = "FeatureTearDownAsync";
public const string BACKGROUND_NAME = "FeatureBackgroundAsync";
public const string TESTRUNNER_FIELD = "testRunner";
public const string FEATUREINFO_FIELD = "featureInfo";
public const string REQNROLL_NAMESPACE = "Reqnroll";
public const string SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER = "exampleTags";
public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario";
Expand Down
176 changes: 111 additions & 65 deletions Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.CodeDom;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
Expand All @@ -12,6 +13,7 @@

namespace Reqnroll.Generator.Generation
{
[SuppressMessage("ReSharper", "BitwiseOperatorOnEnumWithoutFlags")]
public class UnitTestFeatureGenerator : IFeatureGenerator
{
private readonly CodeDomHelper _codeDomHelper;
Expand Down Expand Up @@ -149,11 +151,8 @@ private void SetupTestClass(TestClassGenerationContext generationContext)
_testGeneratorProvider.SetTestClassCategories(generationContext, featureCategories);
}

var featureTagsField = new CodeMemberField(typeof(string[]), GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME);
featureTagsField.Attributes |= MemberAttributes.Static;
featureTagsField.InitExpression = _scenarioPartHelper.GetStringArrayExpression(generationContext.Feature.Tags);

generationContext.TestClass.Members.Add(featureTagsField);
DeclareFeatureTagsField(generationContext);
DeclareFeatureInfoMember(generationContext);
}

private CodeMemberField DeclareTestRunnerMember(CodeTypeDeclaration type)
Expand All @@ -163,6 +162,33 @@ private CodeMemberField DeclareTestRunnerMember(CodeTypeDeclaration type)
return testRunnerField;
}

private void DeclareFeatureTagsField(TestClassGenerationContext generationContext)
{
var featureTagsField = new CodeMemberField(typeof(string[]), GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME);
featureTagsField.Attributes |= MemberAttributes.Static;
featureTagsField.InitExpression = _scenarioPartHelper.GetStringArrayExpression(generationContext.Feature.Tags);
generationContext.TestClass.Members.Add(featureTagsField);
}

private void DeclareFeatureInfoMember(TestClassGenerationContext generationContext)
{
var featureInfoField = new CodeMemberField(
_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), GeneratorConstants.FEATUREINFO_FIELD);
featureInfoField.Attributes |= MemberAttributes.Static;
featureInfoField.InitExpression = 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(ProgrammingLanguage))),
_codeDomHelper.TargetLanguage.ToString()),
new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME));

generationContext.TestClass.Members.Add(featureInfoField);
}

private void SetupTestClassInitializeMethod(TestClassGenerationContext generationContext)
{
var testClassInitializeMethod = generationContext.TestClassInitializeMethod;
Expand All @@ -173,42 +199,6 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio
_codeDomHelper.MarkCodeMemberMethodAsAsync(testClassInitializeMethod);

_testGeneratorProvider.SetTestClassInitializeMethod(generationContext);

//testRunner = TestRunnerManager.GetTestRunnerForAssembly(null, [test_worker_id]);
var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();

var getTestRunnerExpression = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))),
nameof(TestRunnerManager.GetTestRunnerForAssembly));

testClassInitializeMethod.Statements.Add(
new CodeAssignStatement(
testRunnerField,
getTestRunnerExpression));

//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))));

//await testRunner.OnFeatureStartAsync(featureInfo);
var onFeatureStartExpression = new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnFeatureStartAsync),
new CodeVariableReferenceExpression("featureInfo"));

_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(onFeatureStartExpression);

testClassInitializeMethod.Statements.Add(onFeatureStartExpression);
}

private void SetupTestClassCleanupMethod(TestClassGenerationContext generationContext)
Expand All @@ -221,30 +211,6 @@ private void SetupTestClassCleanupMethod(TestClassGenerationContext generationCo
_codeDomHelper.MarkCodeMemberMethodAsAsync(testClassCleanupMethod);

_testGeneratorProvider.SetTestClassCleanupMethod(generationContext);

var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();

// await testRunner.OnFeatureEndAsync();
var expression = new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnFeatureEndAsync));

_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(
testRunnerField,
new CodePrimitiveExpression(null)));
}

private void SetupTestInitializeMethod(TestClassGenerationContext generationContext)
Expand All @@ -257,6 +223,78 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont
_codeDomHelper.MarkCodeMemberMethodAsAsync(testInitializeMethod);

_testGeneratorProvider.SetTestInitializeMethod(generationContext);

// Obtain the test runner for executing a single test
// testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(featureHint: featureInfo);

var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();

var getTestRunnerExpression = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))),
nameof(TestRunnerManager.GetTestRunnerForAssembly),
_codeDomHelper.CreateOptionalArgumentExpression("featureHint",
new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD)));

testInitializeMethod.Statements.Add(
new CodeAssignStatement(
testRunnerField,
getTestRunnerExpression));


// "Finish" current feature if needed

var featureContextExpression = new CodePropertyReferenceExpression(
testRunnerField,
"FeatureContext");

var onFeatureEndAsyncExpression = new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnFeatureEndAsync));
_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(onFeatureEndAsyncExpression);

//if (testRunner.FeatureContext != null && !testRunner.FeatureContext.FeatureInfo.Equals(featureInfo))
// 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 CodeMethodInvokeExpression(
new CodePropertyReferenceExpression(
featureContextExpression,
"FeatureInfo"),
nameof(object.Equals),
new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD)),
CodeBinaryOperatorType.ValueEquality,
new CodePrimitiveExpression(false))),
new CodeExpressionStatement(
onFeatureEndAsyncExpression)));


// "Start" the feature if needed

//if (testRunner.FeatureContext == null) {
// await testRunner.OnFeatureStartAsync(featureInfo);
//}
obligaron marked this conversation as resolved.
Show resolved Hide resolved

var onFeatureStartExpression = new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnFeatureStartAsync),
new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD));
_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(onFeatureStartExpression);

testInitializeMethod.Statements.Add(
new CodeConditionStatement(
new CodeBinaryOperatorExpression(
featureContextExpression,
CodeBinaryOperatorType.IdentityEquality,
new CodePrimitiveExpression(null)),
new CodeExpressionStatement(
onFeatureStartExpression)));
}

private void SetupTestCleanupMethod(TestClassGenerationContext generationContext)
Expand All @@ -280,6 +318,14 @@ private void SetupTestCleanupMethod(TestClassGenerationContext generationContext
_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression);

testCleanupMethod.Statements.Add(expression);

// "Release" the TestRunner, so that other threads can pick it up
// TestRunnerManager.ReleaseTestRunner(testRunner);
testCleanupMethod.Statements.Add(
new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))),
nameof(TestRunnerManager.ReleaseTestRunner),
testRunnerField));
}

private void SetupScenarioInitializeMethod(TestClassGenerationContext generationContext)
Expand Down
Loading