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

Fix StackOverflowException when using [StepArgumentTransformation] with same input and output type #136

Merged
Merged
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 @@ -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: #71 StackOverflowException when using [StepArgumentTransformation] with same input and output type (for example string)

# v1.0.1 - 2024-02-16

Expand Down
15 changes: 10 additions & 5 deletions Reqnroll/Bindings/StepArgumentTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ protected virtual IStepArgumentTransformationBinding GetMatchingStepTransformati
}

public async Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo)
{
return await ConvertAsync(value, typeToConvertTo, cultureInfo, null);
}

private async Task<object> ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo, IStepArgumentTransformationBinding lastBindingUsed)
{
if (value == null) throw new ArgumentNullException(nameof(value));

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

if (typeToConvertTo is RuntimeBindingType convertToType && convertToType.Type.IsInstanceOfType(value))
Expand All @@ -55,16 +60,16 @@ private async Task<object> DoTransformAsync(IStepArgumentTransformationBinding s
{
object[] arguments;
if (stepTransformation.Regex != null && value is string stringValue)
arguments = await GetStepTransformationArgumentsFromRegexAsync(stepTransformation, stringValue, cultureInfo);
arguments = await GetStepTransformationArgumentsFromRegexAsync(stepTransformation, stringValue, cultureInfo, stepTransformation);
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)), cultureInfo, stepTransformation) };

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, CultureInfo cultureInfo, IStepArgumentTransformationBinding lastBindingUsed)
{
var match = stepTransformation.Regex.Match(stepSnippet);
var argumentStrings = match.Groups.Cast<Group>().Skip(1).Select(g => g.Value).ToList();
Expand All @@ -74,7 +79,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, cultureInfo, lastBindingUsed);
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public override bool Equals(object obj)
public override int GetHashCode() => UserName.GetHashCode();
}

public record SampleComplexUser(string Firstname, string Surname, int Age, int Height)
{
public static SampleComplexUser Create(string firstname, string surname, int age, int height) => new(firstname, surname, age, height);
}

public class CucumberExpressionIntegrationTests
{
public class SampleBindings
Expand Down Expand Up @@ -100,10 +105,25 @@ public void StepDefWithCustomClassParam(SampleUser userParam)
ExecutedParams.Add((userParam, typeof(SampleUser)));
}

public void StepDefWithCustomComplexClassParam(SampleComplexUser userParam)
{
ExecutedParams.Add((userParam, typeof(SampleComplexUser)));
}

public int ConvertFortyTwo()
{
return 42;
}

public string ConvertToStringLowercase(string str)
{
return str.ToLower();
}

public int ConvertToIntPlus100(int nr)
{
return nr + 100;
}
}

public class TestDependencyProvider : DefaultDependencyProvider
Expand Down Expand Up @@ -423,4 +443,64 @@ public async void Should_match_step_with_customized_built_in_parameter_with_simp

sampleBindings.ExecutedParams.Should().Contain(expectedParam);
}

[Fact]
public async void Should_match_step_with_customized_built_in_parameter_without_recursion_string()
{
var expression = "there is a user {string} registered";
var stepText = "there is a user 'Marvin' registered";
var expectedParam = ("marvin", typeof(string));
var methodName = nameof(SampleBindings.StepDefWithStringParam);

IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding(
(string)null,
new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertToStringLowercase))));

var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformation });

sampleBindings.ExecutedParams.Should().Contain(expectedParam);
}

[Fact]
public async void Should_match_step_with_customized_built_in_parameter_without_recursion_int32()
{
var expression = "I have {int} cucumbers in my belly";
var stepText = "I have 43 cucumbers in my belly";
var expectedParam = (143, typeof(int));
var methodName = nameof(SampleBindings.StepDefWithIntParam);

IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding(
(string)null,
new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertToIntPlus100))));

var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformation });

sampleBindings.ExecutedParams.Should().Contain(expectedParam);
}

[Fact]
public async void Should_match_step_with_custom_parameter_with_additional_step_arguments()
{
var expression = "there is a {user} registered";
var stepText = "there is a user Marvin Smith he is 27 years old and 175 height registered";
var expectedParam = (new SampleComplexUser("marvin", "smith", 127, 275), typeof(SampleComplexUser));
var methodName = nameof(SampleBindings.StepDefWithCustomComplexClassParam);

IStepArgumentTransformationBinding transformationStringWithouthBlanks = new StepArgumentTransformationBinding(
(string)null,
new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertToStringLowercase))));

IStepArgumentTransformationBinding transformationIntPlus100 = new StepArgumentTransformationBinding(
(string)null,
new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertToIntPlus100))));

IStepArgumentTransformationBinding transformationSampleComplexUser = new StepArgumentTransformationBinding(
"user ([A-Za-z]+) ([A-Za-z]+) he is ([0-9]+) years old and ([0-9]+) height",
new RuntimeBindingMethod(typeof(SampleComplexUser).GetMethod(nameof(SampleComplexUser.Create))),
"user");

var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformationStringWithouthBlanks, transformationIntPlus100, transformationSampleComplexUser });

sampleBindings.ExecutedParams.Should().Contain(expectedParam);
}
}