Skip to content

Commit

Permalink
Fix StackOverflowException when using [StepArgumentTransformation] wi…
Browse files Browse the repository at this point in the history
…th same input and output type (#136)
  • Loading branch information
obligaron authored May 21, 2024
1 parent e4e7eaa commit a8241b8
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 5 deletions.
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);
}
}

0 comments on commit a8241b8

Please sign in to comment.