From c3984153b2f1cf121169eaa78a8f4b6587ab56eb Mon Sep 17 00:00:00 2001 From: obligaron Date: Sat, 18 May 2024 00:08:47 +0200 Subject: [PATCH] Fix StackOverflowException when using [StepArgumentTransformation] with same input and output type --- CHANGELOG.md | 1 + .../Bindings/StepArgumentTypeConverter.cs | 15 ++-- .../CucumberExpressionIntegrationTests.cs | 80 +++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbbfa808b..21531040e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Reqnroll/Bindings/StepArgumentTypeConverter.cs b/Reqnroll/Bindings/StepArgumentTypeConverter.cs index 1d7a5cdc3..ea4d8a568 100644 --- a/Reqnroll/Bindings/StepArgumentTypeConverter.cs +++ b/Reqnroll/Bindings/StepArgumentTypeConverter.cs @@ -38,11 +38,16 @@ protected virtual IStepArgumentTransformationBinding GetMatchingStepTransformati } public async Task ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo) + { + return await ConvertAsync(value, typeToConvertTo, cultureInfo, null); + } + + private async Task 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)) @@ -55,16 +60,16 @@ private async Task 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 GetStepTransformationArgumentsFromRegexAsync(IStepArgumentTransformationBinding stepTransformation, string stepSnippet, CultureInfo cultureInfo) + private async Task GetStepTransformationArgumentsFromRegexAsync(IStepArgumentTransformationBinding stepTransformation, string stepSnippet, CultureInfo cultureInfo, IStepArgumentTransformationBinding lastBindingUsed) { var match = stepTransformation.Regex.Match(stepSnippet); var argumentStrings = match.Groups.Cast().Skip(1).Select(g => g.Value).ToList(); @@ -74,7 +79,7 @@ private async Task 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; diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 7e1095ee8..c81fb54aa 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -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 @@ -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 @@ -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); + } }