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

Cucumber expressions support #2595

Merged
merged 40 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
96e279c
Add Cucumber.CucumberExpressions package dependency
gasparnagy May 5, 2022
9fc9408
Refactor binding discovery to ise IStepDefinitionBindingBuilder
gasparnagy May 5, 2022
35ff253
Refactor StepDefinitionBinding to contain source expression, expressi…
gasparnagy May 5, 2022
b939028
Fix unit tests
gasparnagy May 5, 2022
4ba1d1d
Introduce CucumberExpressionStepDefinitionBindingBuilder
gasparnagy May 5, 2022
a785bc2
Use IExpression on IStepDefinitionBinding
gasparnagy May 5, 2022
d3d049b
small fix & ignore cukeex scenario
gasparnagy May 5, 2022
80580e4
Fix old project format tests
gasparnagy May 6, 2022
19e0e2f
Introduce step definition error handling
gasparnagy May 6, 2022
25d54ca
Integrate Cucumber Expressions
gasparnagy May 6, 2022
3c96991
Force regex for Step Def
gasparnagy May 10, 2022
51a5cf2
Force regex for Step Def once more
gasparnagy May 10, 2022
1eeb6b2
add tests
gasparnagy May 20, 2022
61f0c7e
enum support
gasparnagy May 20, 2022
5f475ab
tests for custom type
gasparnagy May 20, 2022
3e822a2
support for custom parameter names for StepArgumentTransformation
gasparnagy May 20, 2022
de90ff3
try cleaning up string handling (not working)
gasparnagy May 20, 2022
26aac4c
fix string handling?
gasparnagy May 23, 2022
b86ca05
Add scenarios, fix [StepArgTrafo]
gasparnagy May 23, 2022
dd87229
fix unit test
gasparnagy May 23, 2022
8f6b415
refactor RegexFactory
gasparnagy May 23, 2022
a7f0eaa
provide cucumber expression step definition skeletons
gasparnagy May 24, 2022
1b573e8
remove parameter type dumping
gasparnagy May 24, 2022
17d29f6
Generate regex-based snippets in ^xxx$ style, cleanup
gasparnagy Jul 7, 2022
8828ccf
Upgrade Cucumber.CucumberExpressions to v16.0.0
gasparnagy Jul 7, 2022
f87584b
allow overriding {int}, define: {double}, {decimal}, cleanup
gasparnagy Jul 7, 2022
c64b42f
Cleanup {string} handling
gasparnagy Jul 7, 2022
f6ce796
Merge remote-tracking branch 'origin/master' into cucumber_expressions
gasparnagy Jul 7, 2022
7f2c8b0
fix build?
gasparnagy Jul 7, 2022
7428335
Add specflow-config.json
gasparnagy Jul 7, 2022
9cd7181
update spec specflow-config.json wit cucumber expressions
gasparnagy Jul 7, 2022
523c3ae
Add {byte}, {long}
gasparnagy Jul 7, 2022
de9df24
Check is [Obsolete] works
gasparnagy Jul 7, 2022
fe661d0
Extend docs with Cucumber-Expressions
gasparnagy Jul 7, 2022
9c121df
Show upgrade guide link in binding error message.
gasparnagy Jul 7, 2022
a5102b4
Remove @focus from scenarios
gasparnagy Jul 7, 2022
c13f3cf
Merge remote-tracking branch 'origin/master' into cucumber_expressions
gasparnagy Jul 8, 2022
b564718
Update changelog.txt
gasparnagy Jul 8, 2022
d11d650
fix typo
gasparnagy Jul 8, 2022
083237c
fix cucumber expression dependency
gasparnagy Jul 8, 2022
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 TechTalk.SpecFlow.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
LICENSE.txt = LICENSE.txt
nuget.config = nuget.config
README.md = README.md
specflow-config.json = specflow-config.json
TechTalk.SpecFlow.sln.DotSettings = TechTalk.SpecFlow.sln.DotSettings
version.json = version.json
EndProjectSection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ namespace {namespace}
{bindings}
}
}
>>>CSharp/StepDefinitionRegex
[{attribute}(@"{regex}")]
>>>CSharp/StepDefinitionExpression
[{attribute}({expression})]
public void {methodName}({parameters})
{
_scenarioContext.Pending();
Expand All @@ -26,7 +26,7 @@ public void {methodName}({parameters})
[{attribute}]
public void {methodName}({parameters})
{
_scenarioContext .Pending();
_scenarioContext.Pending();
}
>>>VB/StepDefinitionClass
Imports System
Expand All @@ -42,8 +42,8 @@ Namespace {namespace}
End Class

End Namespace
>>>VB/StepDefinitionRegex
<TechTalk.SpecFlow.{attribute}("{regex}")> _
>>>VB/StepDefinitionExpression
<TechTalk.SpecFlow.{attribute}({expression})> _
Public Sub {methodName}({parameters})
ScenarioContext.Current.Pending()
End Sub
Expand All @@ -59,7 +59,7 @@ module {className}
open TechTalk.SpecFlow

{bindings}
>>>FSharp/StepDefinitionRegex
let [<{attribute}(@"{regex}")>] {methodName}({parameters}) = ()
>>>FSharp/StepDefinitionExpression
let [<{attribute}({expression}>] {methodName}({parameters}) = ()
>>>FSharp/StepDefinition
let [<{attribute}>] {methodName}({parameters}) = ()
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ protected internal virtual string GetTemplate(string key)
return template;
}

public string GetStepDefinitionTemplate(ProgrammingLanguage language, bool withRegex)
public string GetStepDefinitionTemplate(ProgrammingLanguage language, bool withExpression)
{
string key = $"{language}/StepDefinition{(withRegex ? "Regex" : "")}";
string key = $"{language}/StepDefinition{(withExpression ? "Expression" : "")}";
string template = GetTemplate(key);
if (template == null)
return MissingTemplate(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace TechTalk.SpecFlow.BindingSkeletons
{
public interface ISkeletonTemplateProvider
{
string GetStepDefinitionTemplate(ProgrammingLanguage language, bool withRegex);
string GetStepDefinitionTemplate(ProgrammingLanguage language, bool withExpression);
string GetStepDefinitionClassTemplate(ProgrammingLanguage language);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,50 @@ private static IEnumerable<StepInstance> GetOrderedSteps(StepInstance[] stepInst

public virtual string GetStepDefinitionSkeleton(ProgrammingLanguage language, StepInstance stepInstance, StepDefinitionSkeletonStyle style, CultureInfo bindingCulture)
{
var withRegex = style == StepDefinitionSkeletonStyle.RegexAttribute;
var template = templateProvider.GetStepDefinitionTemplate(language, withRegex);
var withExpression = style == StepDefinitionSkeletonStyle.RegexAttribute || style == StepDefinitionSkeletonStyle.CucumberExpressionAttribute;
var template = templateProvider.GetStepDefinitionTemplate(language, withExpression);
var analyzedStepText = Analyze(stepInstance, bindingCulture);
//{attribute}/{regex}/{methodName}/{parameters}
return ApplyTemplate(template, new
{
attribute = stepInstance.StepDefinitionType,
regex = withRegex ? GetRegex(analyzedStepText) : "",
expression = withExpression ? GetExpression(analyzedStepText, style, language) : "",
methodName = GetMethodName(stepInstance, analyzedStepText, style, language),
parameters = string.Join(", ", analyzedStepText.Parameters.Select(p => ToDeclaration(language, p)).ToArray())
});
}

private string GetExpression(AnalyzedStepText analyzedStepText, StepDefinitionSkeletonStyle style, ProgrammingLanguage programmingLanguage)
{
switch (style)
{
case StepDefinitionSkeletonStyle.RegexAttribute:
var regex = GetRegex(analyzedStepText);
var stringPrefix = programmingLanguage == ProgrammingLanguage.VB ? "" : "@";
return $"{stringPrefix}\"{regex}\"";
case StepDefinitionSkeletonStyle.CucumberExpressionAttribute:
var cucumberExpression = GetCucumberExpression(analyzedStepText);
return $"\"{cucumberExpression}\"";
default:
return "";
}
}

private string GetCucumberExpression(AnalyzedStepText stepText)
{
StringBuilder result = new StringBuilder();

result.Append(EscapeRegex(stepText.TextParts[0]));
for (int i = 1; i < stepText.TextParts.Count; i++)
{
var parameter = stepText.Parameters[i - 1];
result.AppendFormat("{{{0}}}", parameter.CucumberExpressionTypeName ?? parameter.Type);
result.Append(EscapeRegex(stepText.TextParts[i]));
}

return result.ToString();
}

private AnalyzedStepText Analyze(StepInstance stepInstance, CultureInfo bindingCulture)
{
var result = stepTextAnalyzer.Analyze(stepInstance.Text, bindingCulture);
Expand All @@ -81,6 +112,7 @@ private string GetMethodName(StepInstance stepInstance, AnalyzedStepText analyze
switch (style)
{
case StepDefinitionSkeletonStyle.RegexAttribute:
case StepDefinitionSkeletonStyle.CucumberExpressionAttribute:
return keyword.ToIdentifier() + string.Concat(analyzedStepText.TextParts.ToArray()).ToIdentifier();
case StepDefinitionSkeletonStyle.MethodNameUnderscores:
return GetMatchingMethodName(keyword, analyzedStepText, stepInstance.StepContext.Language, AppendWordsUnderscored, "_{0}");
Expand Down Expand Up @@ -134,12 +166,15 @@ private string GetRegex(AnalyzedStepText stepText)
{
StringBuilder result = new StringBuilder();

result.Append("^");
result.Append(EscapeRegex(stepText.TextParts[0]));
for (int i = 1; i < stepText.TextParts.Count; i++)
{
result.AppendFormat("({0})", stepText.Parameters[i-1].RegexPattern);
var parameter = stepText.Parameters[i - 1];
result.Append($"{parameter.WrapText}({parameter.RegexPattern}){parameter.WrapText}");
result.Append(EscapeRegex(stepText.TextParts[i]));
}
result.Append("$");

return result.ToString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public enum StepDefinitionSkeletonStyle
[Description("Method name - pascal case")]
MethodNamePascalCase = 2,
[Description("Method name as regulare expression (F#)")]
MethodNameRegex = 3
MethodNameRegex = 3,
[Description("Cucumber expressions in attributes")]
CucumberExpressionAttribute = 4,
}
}
37 changes: 21 additions & 16 deletions TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ public class AnalyzedStepParameter
public readonly string Type;
public readonly string Name;
public readonly string RegexPattern;
public readonly string WrapText;
public readonly string CucumberExpressionTypeName;

public AnalyzedStepParameter(string type, string name, string regexPattern = null)
public AnalyzedStepParameter(string type, string name, string regexPattern = null, string cucumberExpressionTypeName = null, string wrapText = "")
{
this.Type = type;
this.Name = name;
this.RegexPattern = regexPattern;
Type = type;
Name = name;
RegexPattern = regexPattern;
CucumberExpressionTypeName = cucumberExpressionTypeName;
WrapText = wrapText;
}
}

Expand Down Expand Up @@ -52,54 +56,55 @@ public AnalyzedStepText Analyze(string stepText, CultureInfo bindingCulture)
if (paramMatch.Capture.Index < textIndex)
continue;

const string singleQuoteRegexPattern = "[^']*";
const string doubleQuoteRegexPattern = "[^\"\"]*";
const string singleQuoteRegexPattern = ".*"; // earlier it was "[^']*"
const string doubleQuoteRegexPattern = ".*"; // earlier it was "[^\"\"]*"
const string defaultRegexPattern = ".*";

string regexPattern = defaultRegexPattern;
string value = paramMatch.Capture.Value;
int index = paramMatch.Capture.Index;
string wrapText = "";

switch (value.Substring(0, Math.Min(value.Length, 1)))
{
case "\"":
regexPattern = doubleQuoteRegexPattern;
value = value.Substring(1, value.Length - 2);
index++;
wrapText = "\"";
break;
case "'":
regexPattern = singleQuoteRegexPattern;
value = value.Substring(1, value.Length - 2);
index++;
wrapText = "'";
break;
}

result.TextParts.Add(stepText.Substring(textIndex, index - textIndex));
result.Parameters.Add(AnalyzeParameter(value, bindingCulture, result.Parameters.Count, regexPattern, paramMatch.ParameterType));
textIndex = index + value.Length;
result.Parameters.Add(AnalyzeParameter(value, bindingCulture, result.Parameters.Count, regexPattern, paramMatch.ParameterType, wrapText));
textIndex = index + paramMatch.Capture.Length;
}

result.TextParts.Add(stepText.Substring(textIndex));
return result;
}

private AnalyzedStepParameter AnalyzeParameter(string value, CultureInfo bindingCulture, int paramIndex, string regexPattern, ParameterType parameterType)
private AnalyzedStepParameter AnalyzeParameter(string value, CultureInfo bindingCulture, int paramIndex, string regexPattern, ParameterType parameterType, string wrapText)
{
string paramName = StepParameterNameGenerator.GenerateParameterName(value, paramIndex, usedParameterNames);

if (parameterType == ParameterType.Int && int.TryParse(value, NumberStyles.Integer, bindingCulture, out _))
return new AnalyzedStepParameter("Int32", paramName, regexPattern);
return new AnalyzedStepParameter("Int32", paramName, regexPattern, "int", wrapText);

if (parameterType == ParameterType.Decimal && decimal.TryParse(value, NumberStyles.Number, bindingCulture, out _))
return new AnalyzedStepParameter("Decimal", paramName, regexPattern);
return new AnalyzedStepParameter("Decimal", paramName, regexPattern, "float", wrapText);

if (parameterType == ParameterType.Date && DateTime.TryParse(value, bindingCulture, DateTimeStyles.AllowWhiteSpaces, out _))
return new AnalyzedStepParameter("DateTime", paramName, regexPattern);
return new AnalyzedStepParameter("DateTime", paramName, regexPattern, "DateTime", wrapText);

return new AnalyzedStepParameter("String", paramName, regexPattern);
return new AnalyzedStepParameter("String", paramName, regexPattern, "string", wrapText);
}

private static readonly Regex quotesRe = new Regex(@"""+(?<param>.*?)""+|'+(?<param>.*?)'+|(?<param>\<.*?\>)");
private static readonly Regex quotesRe = new Regex(@"(?<param>"".*?"")|(?<param>'.*?')|(?<param>\<.*?\>)");
private IEnumerable<CaptureWithContext> RecognizeQuotedTexts(string stepText)
{
return quotesRe.Matches(stepText)
Expand Down
37 changes: 37 additions & 0 deletions TechTalk.SpecFlow/Bindings/BindingFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using TechTalk.SpecFlow.Bindings.CucumberExpressions;
using TechTalk.SpecFlow.Bindings.Reflection;

namespace TechTalk.SpecFlow.Bindings;

public class BindingFactory : IBindingFactory
{
private readonly IStepDefinitionRegexCalculator stepDefinitionRegexCalculator;
private readonly ICucumberExpressionStepDefinitionBindingBuilderFactory _cucumberExpressionStepDefinitionBindingBuilderFactory;

public BindingFactory(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator, ICucumberExpressionStepDefinitionBindingBuilderFactory cucumberExpressionStepDefinitionBindingBuilderFactory)
{
this.stepDefinitionRegexCalculator = stepDefinitionRegexCalculator;
_cucumberExpressionStepDefinitionBindingBuilderFactory = cucumberExpressionStepDefinitionBindingBuilderFactory;
}

public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope,
int hookOrder)
{
return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder);
}

public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString)
{
return expressionString == null
? new MethodNameStepDefinitionBindingBuilder(stepDefinitionRegexCalculator, stepDefinitionType, bindingMethod, bindingScope)
: CucumberExpressionStepDefinitionBindingBuilder.IsCucumberExpression(expressionString)
? _cucumberExpressionStepDefinitionBindingBuilderFactory.Create(stepDefinitionType, bindingMethod, bindingScope, expressionString)
: new RegexStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, expressionString);
}

public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString,
IBindingMethod bindingMethod, string parameterTypeName = null)
{
return new StepArgumentTransformationBinding(regexString, bindingMethod, parameterTypeName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using TechTalk.SpecFlow.Bindings.Reflection;

namespace TechTalk.SpecFlow.Bindings.CucumberExpressions;

public class BuiltInCucumberExpressionParameterTypeTransformation : ICucumberExpressionParameterTypeTransformation
{
public string Name { get; }
public string Regex { get; }
public IBindingType TargetType { get; }
public bool UseForSnippets { get; }
public int Weight { get; }

public BuiltInCucumberExpressionParameterTypeTransformation(string regex, IBindingType targetType, string name = null, bool useForSnippets = true, int weight = 0)
{
Regex = regex;
TargetType = targetType;
Name = name;
UseForSnippets = useForSnippets;
Weight = weight;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TechTalk.SpecFlow.Bindings.Reflection;

namespace TechTalk.SpecFlow.Bindings.CucumberExpressions;

public class CucumberExpressionParameterType : ISpecFlowCucumberExpressionParameterType
{
internal const string MatchAllRegex = @".*";

public string Name { get; }
public IBindingType TargetType { get; }
public ICucumberExpressionParameterTypeTransformation[] Transformations { get; }
public string[] RegexStrings { get; }

public bool UseForSnippets { get; }
public int Weight { get; }

public Type ParameterType => ((RuntimeBindingType)TargetType).Type;

public CucumberExpressionParameterType(string name, IBindingType targetType, IEnumerable<ICucumberExpressionParameterTypeTransformation> transformations)
{
Name = name;
TargetType = targetType;
Transformations = transformations.ToArray();
UseForSnippets = Transformations.Any(t => t.UseForSnippets);
Weight = Transformations.Max(t => t.Weight);

var regexStrings = Transformations.Select(tr => tr.Regex).Distinct().ToArray();
if (regexStrings.Length > 1 && regexStrings.Contains(MatchAllRegex))
regexStrings = new[] {MatchAllRegex};
RegexStrings = regexStrings;
}
}
Loading