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 Scenario-Level Parallelization for MsTest #119

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Fix for #81 in which Cucumber Expressions fail when two enums or two custom types with the same short name (differing namespaces) are used as parameters
* Fix: Adding @ignore to an Examples block generates invalid code for NUnit v3+ (#103)
* Fix: #111 @ignore attribute is not inherited to the scenarios from Rule
* Support Scenario-Level Parallelization for MsTest (`ExecutionScope.MethodLevel`)

# v1.0.1 - 2024-02-16

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 FEATURETESTRUNNER_FIELD = "featureTestRunner";
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
5 changes: 5 additions & 0 deletions Reqnroll.Generator/Generation/ScenarioPartHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ public CodeExpression GetTestRunnerExpression()
return new CodeVariableReferenceExpression(GeneratorConstants.TESTRUNNER_FIELD);
}

public CodeExpression GetTestFeatureRunnerExpression()
{
return new CodeVariableReferenceExpression(GeneratorConstants.FEATURETESTRUNNER_FIELD);
}

private CodeExpression GetStringArrayExpression(IEnumerable<string> items, ParameterSubstitution paramToIdentifier)
{
return new CodeArrayCreateExpression(typeof(string[]), items.Select(item => GetSubstitutedString(item, paramToIdentifier)).ToArray());
Expand Down
4 changes: 2 additions & 2 deletions Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio
_testGeneratorProvider.SetTestClassInitializeMethod(generationContext);

//testRunner = TestRunnerManager.GetTestRunnerForAssembly(null, [test_worker_id]);
var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();
var testRunnerField = generationContext.FeatureRunnerField == null ? _scenarioPartHelper.GetTestRunnerExpression() : _scenarioPartHelper.GetTestFeatureRunnerExpression();

var testRunnerParameters = new[]
{
Expand Down Expand Up @@ -229,7 +229,7 @@ private void SetupTestClassCleanupMethod(TestClassGenerationContext generationCo

_testGeneratorProvider.SetTestClassCleanupMethod(generationContext);

var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();
var testRunnerField = generationContext.FeatureRunnerField == null ? _scenarioPartHelper.GetTestRunnerExpression() : _scenarioPartHelper.GetTestFeatureRunnerExpression();

// await testRunner.OnFeatureEndAsync();
var expression = new CodeMethodInvokeExpression(
Expand Down
1 change: 1 addition & 0 deletions Reqnroll.Generator/TestClassGenerationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class TestClassGenerationContext
public CodeMemberMethod ScenarioCleanupMethod { get; private set; }
public CodeMemberMethod FeatureBackgroundMethod { get; private set; }
public CodeMemberField TestRunnerField { get; private set; }
public CodeMemberField FeatureRunnerField { get; internal set; }

public bool GenerateRowTests { get; private set; }

Expand Down
16 changes: 15 additions & 1 deletion Reqnroll.Generator/UnitTestProvider/MsTestGeneratorProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public virtual void SetTestClass(TestClassGenerationContext generationContext, s
new CodeThisReferenceExpression(), TESTCONTEXT_FIELD_NAME), new CodePropertySetValueReferenceExpression()));

generationContext.TestClass.Members.Add(testContextProperty);

// Add a feature Test Runner
var field = new CodeMemberField(CodeDomHelper.GetGlobalizedTypeName(typeof(ITestRunner)), Generation.GeneratorConstants.FEATURETESTRUNNER_FIELD);
field.Attributes |= MemberAttributes.Static;
generationContext.FeatureRunnerField = field;
generationContext.TestClass.Members.Add(field);
}

public virtual void SetTestClassCategories(TestClassGenerationContext generationContext, IEnumerable<string> featureCategories)
Expand Down Expand Up @@ -101,7 +107,6 @@ public virtual void SetTestClassNonParallelizable(TestClassGenerationContext gen
public virtual void SetTestClassInitializeMethod(TestClassGenerationContext generationContext)
{
generationContext.TestClassInitializeMethod.Attributes |= MemberAttributes.Static;
generationContext.TestRunnerField.Attributes |= MemberAttributes.Static;

generationContext.TestClassInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(
TESTCONTEXT_TYPE, "testContext"));
Expand All @@ -126,6 +131,15 @@ protected virtual void FixTestRunOrderingIssue(TestClassGenerationContext genera
{
//see https://github.com/reqnroll/Reqnroll/issues/96

var getTestRunnerExpression = new CodeMethodInvokeExpression(
new CodeVariableReferenceExpression(generationContext.FeatureRunnerField.Name),
nameof(ITestRunner.GetScenarioTestRunner));

generationContext.TestInitializeMethod.Statements.Add(
new CodeAssignStatement(
new CodeVariableReferenceExpression(generationContext.TestRunnerField.Name),
getTestRunnerExpression));

//if (testRunner.FeatureContext != null && testRunner.FeatureContext.FeatureInfo.Title != "<current_feature_title>")
// <TestClass>.<TestClassInitialize>(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private void AddUnitTestProviderSpecificConfig()
break;
case UnitTestProvider.MSTest when _parallelTestExecution:
_project.AddFile(
new ProjectFile("MsTestConfiguration.cs", "Compile", "using Microsoft.VisualStudio.TestTools.UnitTesting; [assembly: Parallelize(Workers = 4, Scope = ExecutionScope.ClassLevel)]"));
new ProjectFile("MsTestConfiguration.cs", "Compile", "using Microsoft.VisualStudio.TestTools.UnitTesting; [assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]"));
break;
case UnitTestProvider.MSTest when !_parallelTestExecution:
_project.AddFile(new ProjectFile("MsTestConfiguration.cs", "Compile", "using Microsoft.VisualStudio.TestTools.UnitTesting; [assembly: DoNotParallelize]"));
Expand Down
3 changes: 2 additions & 1 deletion Reqnroll/Bindings/IStepArgumentTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Globalization;
using System.Threading.Tasks;
using Reqnroll.Bindings.Reflection;
using Reqnroll.Infrastructure;

namespace Reqnroll.Bindings
{
public interface IStepArgumentTypeConverter
{
Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo);
Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, IContextManager contextManager, CultureInfo cultureInfo);
bool CanConvert(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo);
}
}
18 changes: 8 additions & 10 deletions Reqnroll/Bindings/StepArgumentTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ public class StepArgumentTypeConverter : IStepArgumentTypeConverter
{
private readonly ITestTracer testTracer;
private readonly IBindingRegistry bindingRegistry;
private readonly IContextManager contextManager;
private readonly IAsyncBindingInvoker bindingInvoker;

public StepArgumentTypeConverter(ITestTracer testTracer, IBindingRegistry bindingRegistry, IContextManager contextManager, IAsyncBindingInvoker bindingInvoker)
public StepArgumentTypeConverter(ITestTracer testTracer, IBindingRegistry bindingRegistry, IAsyncBindingInvoker bindingInvoker)
{
this.testTracer = testTracer;
this.bindingRegistry = bindingRegistry;
this.contextManager = contextManager;
this.bindingInvoker = bindingInvoker;
}

Expand All @@ -37,34 +35,34 @@ protected virtual IStepArgumentTransformationBinding GetMatchingStepTransformati
return stepTransformations.Length > 0 ? stepTransformations[0] : null;
}

public async Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo)
public async Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, IContextManager contextManager, CultureInfo cultureInfo)
{
if (value == null) throw new ArgumentNullException(nameof(value));

var stepTransformation = GetMatchingStepTransformation(value, typeToConvertTo, true);
if (stepTransformation != null)
return await DoTransformAsync(stepTransformation, value, cultureInfo);
return await DoTransformAsync(stepTransformation, value, contextManager, cultureInfo);

if (typeToConvertTo is RuntimeBindingType convertToType && convertToType.Type.IsInstanceOfType(value))
return value;

return ConvertSimple(typeToConvertTo, value, cultureInfo);
}

private async Task<object> DoTransformAsync(IStepArgumentTransformationBinding stepTransformation, object value, CultureInfo cultureInfo)
private async Task<object> DoTransformAsync(IStepArgumentTransformationBinding stepTransformation, object value, IContextManager contextManager, CultureInfo cultureInfo)
{
object[] arguments;
if (stepTransformation.Regex != null && value is string stringValue)
arguments = await GetStepTransformationArgumentsFromRegexAsync(stepTransformation, stringValue, cultureInfo);
arguments = await GetStepTransformationArgumentsFromRegexAsync(stepTransformation, stringValue, contextManager, cultureInfo);
else
arguments = new[] { await ConvertAsync(value, stepTransformation.Method.Parameters.ElementAtOrDefault(0)?.Type ?? new RuntimeBindingType(typeof(object)), cultureInfo)};
arguments = new[] { await ConvertAsync(value, stepTransformation.Method.Parameters.ElementAtOrDefault(0)?.Type ?? new RuntimeBindingType(typeof(object)), contextManager, cultureInfo)};

var result = await bindingInvoker.InvokeBindingAsync(stepTransformation, contextManager, arguments, testTracer, new DurationHolder());

return result;
}

private async Task<object[]> GetStepTransformationArgumentsFromRegexAsync(IStepArgumentTransformationBinding stepTransformation, string stepSnippet, CultureInfo cultureInfo)
private async Task<object[]> GetStepTransformationArgumentsFromRegexAsync(IStepArgumentTransformationBinding stepTransformation, string stepSnippet, IContextManager contextManager, CultureInfo cultureInfo)
{
var match = stepTransformation.Regex.Match(stepSnippet);
var argumentStrings = match.Groups.Cast<Group>().Skip(1).Select(g => g.Value).ToList();
Expand All @@ -74,7 +72,7 @@ private async Task<object[]> GetStepTransformationArgumentsFromRegexAsync(IStepA

for (int i = 0; i < argumentStrings.Count; i++)
{
result[i] = await ConvertAsync(argumentStrings[i], bindingParameters[i].Type, cultureInfo);
result[i] = await ConvertAsync(argumentStrings[i], bindingParameters[i].Type, contextManager, cultureInfo);
}

return result;
Expand Down
2 changes: 2 additions & 0 deletions Reqnroll/ITestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ public interface ITestRunner
Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null);

void Pending();

ITestRunner GetScenarioTestRunner();
}
}
49 changes: 49 additions & 0 deletions Reqnroll/Infrastructure/ContextManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public void Reset()
}
}

private readonly ITestTracer testTracer;
private readonly IObjectContainer testThreadContainer;
private readonly InternalContextManager<ScenarioContext> scenarioContextManager;
private readonly InternalContextManager<FeatureContext> featureContextManager;
Expand All @@ -132,6 +133,7 @@ public ContextManager(ITestTracer testTracer, IObjectContainer testThreadContain
this.featureContextManager = new InternalContextManager<FeatureContext>(testTracer);
this.scenarioContextManager = new InternalContextManager<ScenarioContext>(testTracer);
this.stepContextManager = new StackedInternalContextManager<ScenarioStepContext>(testTracer);
this.testTracer = testTracer;
this.testThreadContainer = testThreadContainer;
this.containerBuilder = containerBuilder;

Expand Down Expand Up @@ -183,6 +185,16 @@ public void InitializeScenarioContext(ScenarioInfo scenarioInfo)
{
var scenarioContainer = containerBuilder.CreateScenarioContainer(FeatureContext.FeatureContainer, scenarioInfo);
var newContext = scenarioContainer.Resolve<ScenarioContext>();

if (isSecnarioContextManager)
{
scenarioContainer.RegisterInstanceAs(this, typeof(IContextManager));
if (scenarioTestExecutionEngine != null)
scenarioContainer.RegisterInstanceAs(scenarioTestExecutionEngine, typeof(ITestExecutionEngine));
if (scenarioTestRunner != null)
scenarioContainer.RegisterInstanceAs(scenarioTestRunner, typeof(ITestRunner));
}

scenarioContextManager.Init(newContext, scenarioContainer);
#pragma warning disable 618
ScenarioContext.Current = newContext;
Expand All @@ -191,6 +203,23 @@ public void InitializeScenarioContext(ScenarioInfo scenarioInfo)
ResetCurrentStepStack();
}

bool isSecnarioContextManager;
ITestExecutionEngine scenarioTestExecutionEngine;
ITestRunner scenarioTestRunner;

public void InitScenarioExecutionEngine(ITestExecutionEngine testExecutionEngine)
{
scenarioTestExecutionEngine = testExecutionEngine;
if (scenarioContextManager.Instance?.ScenarioContainer != null)
scenarioContextManager.Instance.ScenarioContainer.RegisterInstanceAs(testExecutionEngine, typeof(ITestExecutionEngine));
}
public void InitScenarioRunner(ITestRunner testRunner)
{
scenarioTestRunner = testRunner;
if (scenarioContextManager.Instance?.ScenarioContainer != null)
scenarioContextManager.Instance.ScenarioContainer.RegisterInstanceAs(testRunner, typeof(ITestRunner));
}

private void ResetCurrentStepStack()
{
stepContextManager.Reset();
Expand Down Expand Up @@ -225,5 +254,25 @@ public void Dispose()
scenarioContextManager?.Dispose();
stepContextManager?.Dispose();
}

public ContextManager(ContextManager featureContextManager)
{
this.testTracer = featureContextManager.testTracer;
this.testThreadContainer = featureContextManager.testThreadContainer;
this.containerBuilder = featureContextManager.containerBuilder;
this.isSecnarioContextManager = true;

this.featureContextManager = featureContextManager.featureContextManager;
this.scenarioContextManager = new InternalContextManager<ScenarioContext>(testTracer);

this.stepContextManager = new StackedInternalContextManager<ScenarioStepContext>(testTracer);

InitializeTestThreadContext();
}

public IContextManager GetScenarioContextManager()
{
return new ContextManager(this);
}
}
}
5 changes: 5 additions & 0 deletions Reqnroll/Infrastructure/IContextManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public interface IContextManager
ScenarioStepContext StepContext { get; }
StepDefinitionType? CurrentTopLevelStepDefinitionType { get; }

void InitScenarioExecutionEngine(ITestExecutionEngine testExecutionEngine);
void InitScenarioRunner(ITestRunner testRunner);

void InitializeFeatureContext(FeatureInfo featureInfo);
void CleanupFeatureContext();

Expand All @@ -18,5 +21,7 @@ public interface IContextManager

void InitializeStepContext(StepInfo stepInfo);
void CleanupStepContext();

IContextManager GetScenarioContextManager();
}
}
3 changes: 3 additions & 0 deletions Reqnroll/Infrastructure/ITestExecutionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ public interface ITestExecutionEngine
Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg);

void Pending();

ITestExecutionEngine GetScenarioExecutionEngine();
void InitScenarioRunner(ITestRunner testRunner);
}
}
24 changes: 21 additions & 3 deletions Reqnroll/Infrastructure/TestExecutionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,24 @@ public virtual void Pending()
throw _errorProvider.GetPendingStepDefinitionError();
}

public virtual ITestExecutionEngine GetScenarioExecutionEngine()
{
var scenarioContextManager = _contextManager.GetScenarioContextManager();

var scenarioEngine = new TestExecutionEngine(_stepFormatter, _testTracer, _errorProvider, _stepArgumentTypeConverter, _reqnrollConfiguration, _bindingRegistry, _unitTestRuntimeProvider, scenarioContextManager,
_stepDefinitionMatchService, _bindingInvoker, _obsoleteStepHandler, _analyticsEventProvider, _analyticsTransmitter, _testRunnerManager,
_runtimePluginTestExecutionLifecycleEventEmitter, _testThreadExecutionEventPublisher, _testPendingMessageFactory, _testUndefinedMessageFactory, _testObjectResolver, _testRunContext);

scenarioContextManager.InitScenarioExecutionEngine(scenarioEngine);

return scenarioEngine;
}

public virtual void InitScenarioRunner(ITestRunner testRunner)
{
_contextManager.InitScenarioRunner(testRunner);
}

protected virtual async Task OnBlockStartAsync(ScenarioBlock block)
{
if (block == ScenarioBlock.None)
Expand Down Expand Up @@ -611,18 +629,18 @@ private async Task<object[]> GetExecuteArgumentsAsync(BindingMatch match)

for (var i = 0; i < match.Arguments.Length; i++)
{
arguments[i] = await ConvertArg(match.Arguments[i], bindingParameters[i].Type);
arguments[i] = await ConvertArg(match.Arguments[i], _contextManager, bindingParameters[i].Type);
}

return arguments;
}

private async Task<object> ConvertArg(object value, IBindingType typeToConvertTo)
private async Task<object> ConvertArg(object value, IContextManager contextManager, IBindingType typeToConvertTo)
{
Debug.Assert(value != null);
Debug.Assert(typeToConvertTo != null);

return await _stepArgumentTypeConverter.ConvertAsync(value, typeToConvertTo, FeatureContext.BindingCulture);
return await _stepArgumentTypeConverter.ConvertAsync(value, typeToConvertTo, _contextManager, FeatureContext.BindingCulture);
}

#endregion
Expand Down
7 changes: 7 additions & 0 deletions Reqnroll/TestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,12 @@ public void Pending()
{
_executionEngine.Pending();
}

public ITestRunner GetScenarioTestRunner()
{
var testRunner = new TestRunner(_executionEngine.GetScenarioExecutionEngine());
testRunner._executionEngine.InitScenarioRunner(testRunner);
return testRunner;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ public async Task Should_not_execute_step_argument_transformations_when_there_wa

await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "user bar", null, null);

_stepArgumentTypeConverterMock.Verify(i => i.ConvertAsync(It.IsAny<object>(), It.IsAny<IBindingType>(), It.IsAny<CultureInfo>()), Times.Never);
_stepArgumentTypeConverterMock.Verify(i => i.ConvertAsync(It.IsAny<object>(), It.IsAny<IBindingType>(), contextManagerStub.Object, It.IsAny<CultureInfo>()), Times.Never);
}

[Fact]
Expand Down
Loading