SpecFlow icon indicating copy to clipboard operation
SpecFlow copied to clipboard

StepArgumentTransformation stack overflow exception

Open jrod567 opened this issue 2 years ago • 8 comments

SpecFlow Version

VS2022

Which test runner are you using?

NUnit

Test Runner Version Number

3.13.2

.NET Implementation

.NET 5.0

Project Format of the SpecFlow project

Classic project format using <PackageReference> tags

.feature.cs files are generated using

SpecFlow.Tools.MsBuild.Generation NuGet package

Test Execution Method

Visual Studio Test Explorer

SpecFlow Section in app.config or content of specflow.json

No response

Issue Description

StepArgumentTransformation throws stack overflow exception whenever one tries to filter parameter data. Further details provided on SpecFlow forum here:

https://support.specflow.org/hc/en-us/community/posts/4423434386577-StepArgumentTransformation-stack-overflow-exception?page=1#community_comment_4436919840401

Exception:

The active test run was aborted. Reason: Test host process crashed : Stack overflow.
   at System.Text.RegularExpressions.Match.get_Groups()
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.GetStepTransformationArgumentsFromRegex(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.String, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.DoTransform(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.Object, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.Convert(System.Object, TechTalk.SpecFlow.Bindings.Reflection.IBindingType, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter+<>c__DisplayClass8_0.<GetStepTransformationArgumentsFromRegex>b__1(System.String, Int32)
   at System.Linq.Enumerable+<SelectIterator>d__174`2[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].AddRange(System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Collections.Generic.EnumerableHelpers.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Linq.Enumerable.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.GetStepTransformationArgumentsFromRegex(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.String, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.DoTransform(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.Object, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.Convert(System.Object, TechTalk.SpecFlow.Bindings.Reflection.IBindingType, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter+<>c__DisplayClass8_0.<GetStepTransformationArgumentsFromRegex>b__1(System.String, Int32)
   at System.Linq.Enumerable+<SelectIterator>d__174`2[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].AddRange(System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Collections.Generic.EnumerableHelpers.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Linq.Enumerable.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.GetStepTransformationArgumentsFromRegex(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.String, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.DoTransform(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.Object, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.Convert(System.Object, TechTalk.SpecFlow.Bindings.Reflection.IBindingType, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter+<>c__DisplayClass8_0.<GetStepTransformationArgumentsFromRegex>b__1(System.String, Int32)
   at System.Linq.Enumerable+<SelectIterator>d__174`2[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].AddRange(System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Collections.Generic.EnumerableHelpers.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Linq.Enumerable.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.GetStepTransformationArgumentsFromRegex(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.String, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.DoTransform(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.Object, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.Convert(System.Object, TechTalk.SpecFlow.Bindings.Reflection.IBindingType, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter+<>c__DisplayClass8_0.<GetStepTransformationArgumentsFromRegex>b__1(System.String, Int32)
   at System.Linq.Enumerable+<SelectIterator>d__174`2[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].AddRange(System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Collections.Generic.EnumerableHelpers.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Linq.Enumerable.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.GetStepTransformationArgumentsFromRegex(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.String, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.DoTransform(TechTalk.SpecFlow.Bindings.IStepArgumentTransformationBinding, System.Object, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter.Convert(System.Object, TechTalk.SpecFlow.Bindings.Reflection.IBindingType, System.Globalization.CultureInfo)
   at TechTalk.SpecFlow.Bindings.StepArgumentTypeConverter+<>c__DisplayClass8_0.<GetStepTransformationArgumentsFromRegex>b__1(System.String, Int32)
   at System.Linq.Enumerable+<SelectIterator>d__174`2[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].AddRange(System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Collections.Generic.EnumerableHelpers.ToArray[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at System.Linq.Enumerable

Steps to Reproduce

Apply StepArgumentTransformation binding and apply a regular expression such as:

 **[StepArgumentTransformation(@"(\d+)")]        
        public string Transform(string val)
        {
            Console.WriteLine($"Transform: {val}");
            return val;
        }**

 [Given(@"I have a value '([^']*)'")]
        public void GivenIHaveAValue(string val)
        {
            Console.WriteLine($"Given: {val}");
        }

        [When(@"I have another value '([^']*)'")]
        public void WhenIHaveAnotherValue(string val)
        {
            Console.WriteLine($"When: {val}");
        }

        [Then(@"I have something else '([^']*)'")]
        public void ThenIHaveSomethingElse(string val)
        {
            Console.WriteLine($"Then: {val}");
        }

When this goes to run a stack overflow will be thrown; remove the filter regular expression and it works fine and iterates through all parameter data such as:

Standard Output:  Given I have a value 'abc' Transform: abc Given: abc -> done: StepArgExStepDefinitions.GivenIHaveAValue("abc") (0.0s) When I have another value 'xyz' Transform: xyz When: xyz -> done: StepArgExStepDefinitions.WhenIHaveAnotherValue("xyz") (0.0s) Then I have something else '123' Transform: 123 Then: 123 -> done: StepArgExStepDefinitions.ThenIHaveSomethingElse("123") (0.0s)

Link to Repro Project

No response

jrod567 avatar Feb 18 '22 14:02 jrod567

As the step argument transformation is limited to numbers, I changed the parameter type to int and it is working:

[StepArgumentTransformation(@"(\d+)")]
public string Transform(int val)
{
    Console.WriteLine($"Transform: {val}");
    return val.ToString();
}

SabotageAndi avatar Mar 01 '22 09:03 SabotageAndi

@gasparnagy Is this a bug?

SabotageAndi avatar Mar 01 '22 09:03 SabotageAndi

@SabotageAndi @jrod567 The string -> string transformations are not supported with the step argument transformations. (It is not limited to numbers, but this special case is not supported.) For such cases it is recommended to create a custom type (that wraps a string) and use that instead:

public class ValueString
{
  public ValueString(string value) { ... }
}

[StepArgumentTransformation(@"(\d+)")]        
public ValueString Transform(string val)
{
  Console.WriteLine($"Transform: {val}");
  return new ValueString(val);
}

[Given(@"I have a value '([^']*)'")]
public void GivenIHaveAValue(ValueString val)
{
  Console.WriteLine($"Given: {val}");
}

gasparnagy avatar Mar 01 '22 12:03 gasparnagy

Thanks @gasparnagy for clarifying.

SabotageAndi avatar Mar 01 '22 13:03 SabotageAndi

Hi Andreas and Gaspar - Thank you for the feedback! For our needs, we were looking at using this to convert string based dynamic info (i.e. dates, unique placeholders, etc.) back into another string so string->string transformation would be a nice feature for our team. Maybe something to put in the backlog?

That said, string to string transformation appears to work without using the StepArgumentTransformation filter. Do you see issues using it in this capacity? Wasn't sure if there were any gotchas in doing so that aren't immediately evident.
Thanks!!

jrod567 avatar Mar 02 '22 22:03 jrod567

@jrod567 I did a bit more investigation and found, that indeed, we have improved the conversion logic for string -> string conversions, but only for cases where there is no regex provided for the [StepArgumentTransformation] attribute. This means that you could also implement the transformation in a way, that you just take the entire string, do the Regex matching yourself and return the parameter value if they don't match.

[StepArgumentTransformation]        
public string Transform(string val)
{
  if (!Regex,IsMatch(val, @"(\d+)")
    return val; // no change
  // do transformation
}

I checked the code. The conversion is done by the DoTransform method of the StepArgumentTypeConverter class here.

The step argument transformations are handled reclusively by design. I.e. if you have a conversion from string to int (e.g. roman numbers) and another conversion from int to DateTime (gets the date in X days) then you will be able use a step that provides a date using the "in X days" conversion by specifying a roman number for X.

Because of this reclusive nature, the x -> x conversions are problematic, because when converting the parameter of the conversion, the conversion will be invoked again. This is why you see the stack overflow.

As you can see the code, the string -> string conversion was fixed by cutting out the recursion. But we could only do it in cases where there is no regex here. Generalizing this special handling is not trivial, at he moment I don't have an idea to solve it in a simple way.

If you have any suggestion, feel free to provide it in a pull request. You can also submit it as a feature request, but knowing the capacity of the team and the specific nature of the issue, I don't think that it will get much attention, unless you can work on it (it is open-source!).

gasparnagy avatar Mar 03 '22 08:03 gasparnagy

Yeah, I also don't see a possibility to put this on the backlog of the team in the future. Especially as there is a workaround.

SabotageAndi avatar Mar 03 '22 09:03 SabotageAndi

Thanks again for looking into this! I see what you mean now. That said, the workaround above was similar to what I was considering as well so it sounds like we'd be fine going that route.

jrod567 avatar Mar 03 '22 14:03 jrod567