From 96e279c26c4b6289fd38d32334ec2cecd2045fcd Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 11:55:59 +0200 Subject: [PATCH 01/38] Add Cucumber.CucumberExpressions package dependency --- TechTalk.SpecFlow/SpecFlow.nuspec | 2 ++ TechTalk.SpecFlow/TechTalk.SpecFlow.csproj | 1 + 2 files changed, 3 insertions(+) diff --git a/TechTalk.SpecFlow/SpecFlow.nuspec b/TechTalk.SpecFlow/SpecFlow.nuspec index 13e77569e..e8e1cfa2f 100644 --- a/TechTalk.SpecFlow/SpecFlow.nuspec +++ b/TechTalk.SpecFlow/SpecFlow.nuspec @@ -19,6 +19,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj index d55af2704..b7099b15b 100644 --- a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj +++ b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj @@ -34,6 +34,7 @@ + From 9fc9408ed129792ccde2666bbd1629bde25d2a76 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 15:24:55 +0200 Subject: [PATCH 02/38] Refactor binding discovery to ise IStepDefinitionBindingBuilder --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 39 +++++++++++++++++++ .../Discovery/BindingSourceProcessor.cs | 15 ++++++- .../Discovery/IBindingSourceProcessor.cs | 2 + .../RuntimeBindingSourceProcessor.cs | 4 +- TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 32 ++------------- .../Bindings/IStepDefinitionBindingBuilder.cs | 9 +++++ .../MethodNameStepDefinitionBindingBuilder.cs | 26 +++++++++++++ TechTalk.SpecFlow/Bindings/RegexFactory.cs | 1 + .../RegexStepDefinitionBindingBuilder.cs | 37 ++++++++++++++++++ .../BindingSourceProcessorStub.cs | 5 --- .../Contexts/AccessingContexts.feature | 1 - .../Execution/CucumberExpressions.feature | 25 ++++++++++++ .../InAppDomainParallelExecution.feature | 1 - 13 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 TechTalk.SpecFlow/Bindings/BindingFactory.cs create mode 100644 TechTalk.SpecFlow/Bindings/IStepDefinitionBindingBuilder.cs create mode 100644 TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs create mode 100644 TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs create mode 100644 Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs new file mode 100644 index 000000000..51c673737 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -0,0 +1,39 @@ +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings; + +public class BindingFactory : IBindingFactory +{ + private readonly IStepDefinitionRegexCalculator stepDefinitionRegexCalculator; + + public BindingFactory(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator) + { + this.stepDefinitionRegexCalculator = stepDefinitionRegexCalculator; + } + + public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, + int hookOrder) + { + return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder); + } + + public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType type, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString) + { + return expressionString == null + ? new MethodNameStepDefinitionBindingBuilder(stepDefinitionRegexCalculator, type, bindingMethod, bindingScope) + : new RegexStepDefinitionBindingBuilder(type, bindingMethod, bindingScope, expressionString); + } + + public IStepDefinitionBinding CreateStepBinding(StepDefinitionType type, string regexString, + IBindingMethod bindingMethod, BindingScope bindingScope) + { + regexString ??= stepDefinitionRegexCalculator.CalculateRegexFromMethod(type, bindingMethod); + return new StepDefinitionBinding(type, regexString, bindingMethod, bindingScope); + } + + public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, + IBindingMethod bindingMethod) + { + return new StepArgumentTransformationBinding(regexString, bindingMethod); + } +} diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index bd06ba236..5eb195623 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -14,6 +14,7 @@ public abstract class BindingSourceProcessor : IBindingSourceProcessor private BindingSourceType currentBindingSourceType = null; private BindingScope[] typeScopes = null; + private List _stepDefinitionBindingBuilders = new(); protected BindingSourceProcessor(IBindingFactory bindingFactory) { @@ -61,6 +62,14 @@ public void ProcessTypeDone() typeScopes = null; } + public virtual void BuildingCompleted() + { + foreach (var stepDefinitionBinding in _stepDefinitionBindingBuilders.SelectMany(builder => builder.Build())) + { + ProcessStepDefinitionBinding(stepDefinitionBinding); + } + } + private IEnumerable GetScopes(IEnumerable attributes) { return attributes.Where(attr => attr.AttributeType.TypeEquals(typeof(ScopeAttribute))) @@ -229,8 +238,10 @@ private void ProcessStepDefinitionAttribute(BindingSourceMethod bindingSourceMet foreach (var stepDefinitionType in stepDefinitionTypes) { - var stepDefinitionBinding = bindingFactory.CreateStepBinding(stepDefinitionType, regex, bindingSourceMethod.BindingMethod, scope); - ProcessStepDefinitionBinding(stepDefinitionBinding); + var stepDefinitionBindingBuilder = bindingFactory.CreateStepDefinitionBindingBuilder(stepDefinitionType, bindingSourceMethod.BindingMethod, scope, regex); + _stepDefinitionBindingBuilders.Add(stepDefinitionBindingBuilder); + //var stepDefinitionBinding = bindingFactory.CreateStepBinding(stepDefinitionType, regex, bindingSourceMethod.BindingMethod, scope); + //ProcessStepDefinitionBinding(stepDefinitionBinding); } } diff --git a/TechTalk.SpecFlow/Bindings/Discovery/IBindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/IBindingSourceProcessor.cs index 75fbd0786..7689a9896 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/IBindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/IBindingSourceProcessor.cs @@ -12,5 +12,7 @@ public interface IBindingSourceProcessor bool ProcessType(BindingSourceType bindingSourceType); void ProcessMethod(BindingSourceMethod bindingSourceMethod); void ProcessTypeDone(); + + void BuildingCompleted(); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs index 410dca500..4daa08632 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs @@ -5,7 +5,6 @@ namespace TechTalk.SpecFlow.Bindings.Discovery { public interface IRuntimeBindingSourceProcessor : IBindingSourceProcessor { - void BuildingCompleted(); } public class RuntimeBindingSourceProcessor : BindingSourceProcessor, IRuntimeBindingSourceProcessor @@ -52,8 +51,9 @@ protected override bool ValidateHook(BindingSourceMethod bindingSourceMethod, Bi return base.ValidateHook(bindingSourceMethod, hookAttribute, hookType); } - public void BuildingCompleted() + public override void BuildingCompleted() { + base.BuildingCompleted(); bindingRegistry.Ready = true; } } diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index 9310f5216..fdb7ca07d 100644 --- a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs @@ -7,39 +7,13 @@ public interface IBindingFactory IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, int hookOrder); + IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType type, IBindingMethod bindingMethod, + BindingScope bindingScope, string expressionString); + IStepDefinitionBinding CreateStepBinding(StepDefinitionType type, string regexString, IBindingMethod bindingMethod, BindingScope bindingScope); IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, IBindingMethod bindingMethod); } - - public class BindingFactory : IBindingFactory - { - private readonly IStepDefinitionRegexCalculator stepDefinitionRegexCalculator; - - public BindingFactory(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator) - { - this.stepDefinitionRegexCalculator = stepDefinitionRegexCalculator; - } - - public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, - int hookOrder) - { - return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder); - } - - public IStepDefinitionBinding CreateStepBinding(StepDefinitionType type, string regexString, - IBindingMethod bindingMethod, BindingScope bindingScope) - { - regexString ??= stepDefinitionRegexCalculator.CalculateRegexFromMethod(type, bindingMethod); - return new StepDefinitionBinding(type, regexString, bindingMethod, bindingScope); - } - - public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, - IBindingMethod bindingMethod) - { - return new StepArgumentTransformationBinding(regexString, bindingMethod); - } - } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/IStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/IStepDefinitionBindingBuilder.cs new file mode 100644 index 000000000..da4c4a20f --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/IStepDefinitionBindingBuilder.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace TechTalk.SpecFlow.Bindings; + +public interface IStepDefinitionBindingBuilder +{ + public IEnumerable Build(); +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs new file mode 100644 index 000000000..fce3c6265 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs @@ -0,0 +1,26 @@ +using System; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings; + +public class MethodNameStepDefinitionBindingBuilder : RegexStepDefinitionBindingBuilder +{ + private readonly IStepDefinitionRegexCalculator _stepDefinitionRegexCalculator; + private readonly StepDefinitionType _stepDefinitionType; + private readonly IBindingMethod _bindingMethod; + private readonly BindingScope _bindingScope; + + public MethodNameStepDefinitionBindingBuilder(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator, StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope) + : base(stepDefinitionType, bindingMethod, bindingScope, bindingMethod.Name) + { + _stepDefinitionRegexCalculator = stepDefinitionRegexCalculator; + _stepDefinitionType = stepDefinitionType; + _bindingMethod = bindingMethod; + _bindingScope = bindingScope; + } + + protected override string GetRegexSource() + { + return _stepDefinitionRegexCalculator.CalculateRegexFromMethod(_stepDefinitionType, _bindingMethod); + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/RegexFactory.cs b/TechTalk.SpecFlow/Bindings/RegexFactory.cs index 47ea71aeb..557f93641 100644 --- a/TechTalk.SpecFlow/Bindings/RegexFactory.cs +++ b/TechTalk.SpecFlow/Bindings/RegexFactory.cs @@ -2,6 +2,7 @@ namespace TechTalk.SpecFlow.Bindings { + //TODO[cukeex]: remove internal static class RegexFactory { private static RegexOptions RegexOptions = RegexOptions.CultureInvariant; diff --git a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs new file mode 100644 index 000000000..36ebcdea6 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings; + +public class RegexStepDefinitionBindingBuilder : IStepDefinitionBindingBuilder +{ + private readonly StepDefinitionType _stepDefinitionType; + private readonly IBindingMethod _bindingMethod; + private readonly BindingScope _bindingScope; + private readonly string _sourceExpression; + + public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) + { + _stepDefinitionType = stepDefinitionType; + _bindingMethod = bindingMethod; + _bindingScope = bindingScope; + _sourceExpression = sourceExpression; + } + + protected virtual string GetRegexSource() + { + var regex = _sourceExpression; + if (!regex.StartsWith("^")) regex = "^" + regex; + if (!regex.EndsWith("$")) regex += "$"; + return regex; + } + + public IEnumerable Build() + { + //TODO[cukeex]: error handling + var regexSource = GetRegexSource(); + var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + yield return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope); + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs index a1917e029..beed81df7 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs @@ -29,10 +29,5 @@ protected override void ProcessStepArgumentTransformationBinding(IStepArgumentTr { StepArgumentTransformationBindings.Add(stepArgumentTransformationBinding); } - - public void BuildingCompleted() - { - //nop - } } } \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Contexts/AccessingContexts.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Contexts/AccessingContexts.feature index 023d0a21e..11e99e72a 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Contexts/AccessingContexts.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Contexts/AccessingContexts.feature @@ -1,6 +1,5 @@ Feature: Accessing Contexts -@focus Scenario: The obsolete ScenarioContext.Current can be accessed from a non-parallel execution Given the following step definition """ diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature new file mode 100644 index 000000000..fe60e8630 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -0,0 +1,25 @@ +@focus +Feature: Cucumber expressions + +Rule: Shoud support Cucumber expressions + +Scenario: Simple Cucumber expresions are used for Step Definitions + Given a scenario 'Simple Scenario' as + """ + When I have 42 cucumbers in my belly + """ + And the following step definition + """ + [When(@"I have {int} cucumbers in my belly")] + public void WhenIHaveCucumbersInMyBelly(int count) + { + global::Log.LogStep(); + } + """ + When I execute the tests + Then the binding method 'WhenIHaveCucumbersInMyBelly' is executed + + +Rule: Regular expressions and Cucumber expressions can be mixed + +Rule: Custom parameter types can be used in Cucumber expressions \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/InAppDomainParallelExecution.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/InAppDomainParallelExecution.feature index b57db7c74..6f4721cde 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/InAppDomainParallelExecution.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/InAppDomainParallelExecution.feature @@ -1,5 +1,4 @@ @xUnit @NUnit3 @MSTest -@focus Feature: In-AppDomain Parallel Execution Background: From 35ff2535184ec0706bdad503b168e65d114ce383 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 16:19:06 +0200 Subject: [PATCH 03/38] Refactor StepDefinitionBinding to contain source expression, expression type and validity --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 7 -- .../Discovery/BindingSourceProcessor.cs | 2 - .../RuntimeBindingSourceProcessor.cs | 104 +++++++++--------- TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 3 - .../Bindings/IStepDefinitionBinding.cs | 15 ++- .../MethodNameStepDefinitionBindingBuilder.cs | 9 +- .../RegexStepDefinitionBindingBuilder.cs | 37 +++++-- .../Bindings/StepDefinitionBinding.cs | 62 +++++++---- .../Bindings/StepDefinitionExpressionTypes.cs | 9 ++ .../Bindings/BindingRegistryTests.cs | 9 +- .../StepDefinitionMatchServiceTest.cs | 96 ++++++---------- .../StepDefinitionHelper.cs | 19 ++++ .../Features/Execution/BasicExecution.feature | 3 +- 13 files changed, 198 insertions(+), 177 deletions(-) create mode 100644 TechTalk.SpecFlow/Bindings/StepDefinitionExpressionTypes.cs create mode 100644 Tests/TechTalk.SpecFlow.RuntimeTests/StepDefinitionHelper.cs diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index 51c673737..b26c9e7ba 100644 --- a/TechTalk.SpecFlow/Bindings/BindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -24,13 +24,6 @@ public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefi : new RegexStepDefinitionBindingBuilder(type, bindingMethod, bindingScope, expressionString); } - public IStepDefinitionBinding CreateStepBinding(StepDefinitionType type, string regexString, - IBindingMethod bindingMethod, BindingScope bindingScope) - { - regexString ??= stepDefinitionRegexCalculator.CalculateRegexFromMethod(type, bindingMethod); - return new StepDefinitionBinding(type, regexString, bindingMethod, bindingScope); - } - public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, IBindingMethod bindingMethod) { diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index 5eb195623..e8065a9dc 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -240,8 +240,6 @@ private void ProcessStepDefinitionAttribute(BindingSourceMethod bindingSourceMet { var stepDefinitionBindingBuilder = bindingFactory.CreateStepDefinitionBindingBuilder(stepDefinitionType, bindingSourceMethod.BindingMethod, scope, regex); _stepDefinitionBindingBuilders.Add(stepDefinitionBindingBuilder); - //var stepDefinitionBinding = bindingFactory.CreateStepBinding(stepDefinitionType, regex, bindingSourceMethod.BindingMethod, scope); - //ProcessStepDefinitionBinding(stepDefinitionBinding); } } diff --git a/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs index 4daa08632..047c0ba22 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/RuntimeBindingSourceProcessor.cs @@ -1,54 +1,54 @@ -using TechTalk.SpecFlow.ErrorHandling; -using TechTalk.SpecFlow.Tracing; - -namespace TechTalk.SpecFlow.Bindings.Discovery -{ - public interface IRuntimeBindingSourceProcessor : IBindingSourceProcessor +using TechTalk.SpecFlow.ErrorHandling; +using TechTalk.SpecFlow.Tracing; + +namespace TechTalk.SpecFlow.Bindings.Discovery +{ + public interface IRuntimeBindingSourceProcessor : IBindingSourceProcessor + { + } + + public class RuntimeBindingSourceProcessor : BindingSourceProcessor, IRuntimeBindingSourceProcessor { - } - - public class RuntimeBindingSourceProcessor : BindingSourceProcessor, IRuntimeBindingSourceProcessor - { - private readonly IBindingRegistry bindingRegistry; - private readonly IErrorProvider errorProvider; - private readonly ITestTracer testTracer; - - public RuntimeBindingSourceProcessor(IBindingFactory bindingFactory, IBindingRegistry bindingRegistry, IErrorProvider errorProvider, ITestTracer testTracer) : base(bindingFactory) - { - this.bindingRegistry = bindingRegistry; - this.errorProvider = errorProvider; - this.testTracer = testTracer; - } - - protected override void ProcessStepDefinitionBinding(IStepDefinitionBinding stepDefinitionBinding) - { - bindingRegistry.RegisterStepDefinitionBinding(stepDefinitionBinding); - } - - protected override void ProcessHookBinding(IHookBinding hookBinding) - { - bindingRegistry.RegisterHookBinding(hookBinding); - } - - protected override void ProcessStepArgumentTransformationBinding(IStepArgumentTransformationBinding stepArgumentTransformationBinding) - { - bindingRegistry.RegisterStepArgumentTransformationBinding(stepArgumentTransformationBinding); - } - - protected override bool OnValidationError(string messageFormat, params object[] arguments) - { - testTracer.TraceWarning("Invalid binding: " + string.Format(messageFormat, arguments)); - return false; //TODO: currently this is a warning only (hence return false), in v2 this will be changed - } - - protected override bool ValidateHook(BindingSourceMethod bindingSourceMethod, BindingSourceAttribute hookAttribute, HookType hookType) - { - //TODO: this call will be refactored when binding error detecttion will be improved in v2 - currently implemented here for backwards compatibility - if (!IsScenarioSpecificHook(hookType) && - !bindingSourceMethod.IsStatic) - throw errorProvider.GetNonStaticEventError(bindingSourceMethod.BindingMethod); - - return base.ValidateHook(bindingSourceMethod, hookAttribute, hookType); + private readonly IBindingRegistry bindingRegistry; + private readonly IErrorProvider errorProvider; + private readonly ITestTracer testTracer; + + public RuntimeBindingSourceProcessor(IBindingFactory bindingFactory, IBindingRegistry bindingRegistry, IErrorProvider errorProvider, ITestTracer testTracer) : base(bindingFactory) + { + this.bindingRegistry = bindingRegistry; + this.errorProvider = errorProvider; + this.testTracer = testTracer; + } + + protected override void ProcessStepDefinitionBinding(IStepDefinitionBinding stepDefinitionBinding) + { + bindingRegistry.RegisterStepDefinitionBinding(stepDefinitionBinding); + } + + protected override void ProcessHookBinding(IHookBinding hookBinding) + { + bindingRegistry.RegisterHookBinding(hookBinding); + } + + protected override void ProcessStepArgumentTransformationBinding(IStepArgumentTransformationBinding stepArgumentTransformationBinding) + { + bindingRegistry.RegisterStepArgumentTransformationBinding(stepArgumentTransformationBinding); + } + + protected override bool OnValidationError(string messageFormat, params object[] arguments) + { + testTracer.TraceWarning("Invalid binding: " + string.Format(messageFormat, arguments)); + return false; //TODO: currently this is a warning only (hence return false), in v2 this will be changed + } + + protected override bool ValidateHook(BindingSourceMethod bindingSourceMethod, BindingSourceAttribute hookAttribute, HookType hookType) + { + //TODO: this call will be refactored when binding error detecttion will be improved in v2 - currently implemented here for backwards compatibility + if (!IsScenarioSpecificHook(hookType) && + !bindingSourceMethod.IsStatic) + throw errorProvider.GetNonStaticEventError(bindingSourceMethod.BindingMethod); + + return base.ValidateHook(bindingSourceMethod, hookAttribute, hookType); } public override void BuildingCompleted() @@ -56,5 +56,5 @@ public override void BuildingCompleted() base.BuildingCompleted(); bindingRegistry.Ready = true; } - } -} + } +} diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index fdb7ca07d..1188430b5 100644 --- a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs @@ -10,9 +10,6 @@ IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType type, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString); - IStepDefinitionBinding CreateStepBinding(StepDefinitionType type, string regexString, - IBindingMethod bindingMethod, BindingScope bindingScope); - IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, IBindingMethod bindingMethod); } diff --git a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs index e767dc55d..939d512a6 100644 --- a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs @@ -1,10 +1,13 @@ using System.Text.RegularExpressions; -namespace TechTalk.SpecFlow.Bindings +namespace TechTalk.SpecFlow.Bindings; + +public interface IStepDefinitionBinding : IScopedBinding, IBinding { - public interface IStepDefinitionBinding : IScopedBinding, IBinding - { - StepDefinitionType StepDefinitionType { get; } - Regex Regex { get; } - } + StepDefinitionType StepDefinitionType { get; } + string SourceExpression { get; } + string ExpressionType { get; } + bool IsValid { get; } + string ValidationErrorMessage { get; } + Regex Regex { get; } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs index fce3c6265..eeba9c36a 100644 --- a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs @@ -6,21 +6,16 @@ namespace TechTalk.SpecFlow.Bindings; public class MethodNameStepDefinitionBindingBuilder : RegexStepDefinitionBindingBuilder { private readonly IStepDefinitionRegexCalculator _stepDefinitionRegexCalculator; - private readonly StepDefinitionType _stepDefinitionType; - private readonly IBindingMethod _bindingMethod; - private readonly BindingScope _bindingScope; public MethodNameStepDefinitionBindingBuilder(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator, StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope) : base(stepDefinitionType, bindingMethod, bindingScope, bindingMethod.Name) { _stepDefinitionRegexCalculator = stepDefinitionRegexCalculator; - _stepDefinitionType = stepDefinitionType; - _bindingMethod = bindingMethod; - _bindingScope = bindingScope; } - protected override string GetRegexSource() + protected override string GetRegexSource(out string expressionType) { + expressionType = StepDefinitionExpressionTypes.MethodName; return _stepDefinitionRegexCalculator.CalculateRegexFromMethod(_stepDefinitionType, _bindingMethod); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs index 36ebcdea6..f68a1e65a 100644 --- a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using TechTalk.SpecFlow.Bindings.Reflection; @@ -6,10 +7,10 @@ namespace TechTalk.SpecFlow.Bindings; public class RegexStepDefinitionBindingBuilder : IStepDefinitionBindingBuilder { - private readonly StepDefinitionType _stepDefinitionType; - private readonly IBindingMethod _bindingMethod; - private readonly BindingScope _bindingScope; - private readonly string _sourceExpression; + protected readonly StepDefinitionType _stepDefinitionType; + protected readonly IBindingMethod _bindingMethod; + protected readonly BindingScope _bindingScope; + protected readonly string _sourceExpression; public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) { @@ -19,19 +20,33 @@ public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, _sourceExpression = sourceExpression; } - protected virtual string GetRegexSource() + protected virtual string GetRegexSource(out string expressionType) { + expressionType = StepDefinitionExpressionTypes.RegularExpression; var regex = _sourceExpression; if (!regex.StartsWith("^")) regex = "^" + regex; if (!regex.EndsWith("$")) regex += "$"; return regex; } - public IEnumerable Build() + public virtual IEnumerable Build() { - //TODO[cukeex]: error handling - var regexSource = GetRegexSource(); - var regex = new Regex(regexSource, RegexOptions.CultureInvariant); - yield return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope); + yield return BuildSingle(); + } + + public virtual IStepDefinitionBinding BuildSingle() + { + string expressionType = StepDefinitionExpressionTypes.Unknown; + try + { + var regexSource = GetRegexSource(out expressionType); + var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope, expressionType, _sourceExpression); + } + catch (Exception ex) + { + var errorMessage = ex.Message; + return StepDefinitionBinding.CreateInvalid(_stepDefinitionType, _bindingMethod, _bindingScope, expressionType, _sourceExpression, errorMessage); + } } } diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs index 0b195c187..0552329a7 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs @@ -1,27 +1,49 @@ +using System; using System.Text.RegularExpressions; using TechTalk.SpecFlow.Bindings.Reflection; -namespace TechTalk.SpecFlow.Bindings +namespace TechTalk.SpecFlow.Bindings; + +public class StepDefinitionBinding : MethodBinding, IStepDefinitionBinding { - public class StepDefinitionBinding : MethodBinding, IStepDefinitionBinding + public StepDefinitionType StepDefinitionType { get; } + + public string SourceExpression { get; } + + public string ExpressionType { get; } + + public bool IsValid => ValidationErrorMessage == null; + + public string ValidationErrorMessage { get; } + + public Regex Regex { get; } + + public BindingScope BindingScope { get; } + public bool IsScoped => BindingScope != null; + + public StepDefinitionBinding(StepDefinitionType stepDefinitionType, Regex regex, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionType, string sourceExpression) + : base(bindingMethod) + { + StepDefinitionType = stepDefinitionType; + Regex = regex ?? throw new ArgumentNullException(nameof(regex)); + BindingScope = bindingScope; + SourceExpression = sourceExpression; + ExpressionType = expressionType; + ValidationErrorMessage = null; + } + + private StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionType, string sourceExpression, string errorMessage) + : base(bindingMethod) { - public StepDefinitionType StepDefinitionType { get; private set; } - public Regex Regex { get; private set; } - - public BindingScope BindingScope { get; private set; } - public bool IsScoped { get { return BindingScope != null; } } - - public StepDefinitionBinding(StepDefinitionType stepDefinitionType, Regex regex, IBindingMethod bindingMethod, BindingScope bindingScope) - : base(bindingMethod) - { - StepDefinitionType = stepDefinitionType; - Regex = regex; - BindingScope = bindingScope; - } - - public StepDefinitionBinding(StepDefinitionType stepDefinitionType, string regexString, IBindingMethod bindingMethod, BindingScope bindingScope) - : this(stepDefinitionType, RegexFactory.Create(regexString), bindingMethod, bindingScope) - { - } + StepDefinitionType = stepDefinitionType; + Regex = null; + BindingScope = bindingScope; + SourceExpression = sourceExpression; + ExpressionType = expressionType; + ValidationErrorMessage = errorMessage; } + + public static StepDefinitionBinding CreateInvalid(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, + BindingScope bindingScope, string expressionType, string sourceExpression, string errorMessage) + => new(stepDefinitionType, bindingMethod, bindingScope, expressionType, sourceExpression, errorMessage); } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionExpressionTypes.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionExpressionTypes.cs new file mode 100644 index 000000000..7a27b110e --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionExpressionTypes.cs @@ -0,0 +1,9 @@ +namespace TechTalk.SpecFlow.Bindings; + +public static class StepDefinitionExpressionTypes +{ + public const string RegularExpression = nameof(RegularExpression); + public const string CucumberExpression = nameof(CucumberExpression); + public const string MethodName = nameof(MethodName); + public const string Unknown = nameof(Unknown); +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs index b8533859f..5c28425fc 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using FluentAssertions; using Moq; using Xunit; @@ -19,14 +16,14 @@ public void GetStepDefinitions_should_return_all_step_definitions() { var sut = new BindingRegistry(); - var stepDefinitionBinding1 = new StepDefinitionBinding(StepDefinitionType.Given, @"foo.*", new Mock().Object, null); - var stepDefinitionBinding2 = new StepDefinitionBinding(StepDefinitionType.When, @"bar.*", new Mock().Object, null); + var stepDefinitionBinding1 = StepDefinitionHelper.CreateRegex(StepDefinitionType.Given, @"foo.*"); + var stepDefinitionBinding2 = StepDefinitionHelper.CreateRegex(StepDefinitionType.When, @"bar.*"); sut.RegisterStepDefinitionBinding(stepDefinitionBinding1); sut.RegisterStepDefinitionBinding(stepDefinitionBinding2); var result = sut.GetStepDefinitions(); - result.Should().BeEquivalentTo(new List { stepDefinitionBinding1, stepDefinitionBinding2 }); + result.Should().BeEquivalentTo(new List { stepDefinitionBinding1, stepDefinitionBinding2 }); } [Fact] diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs index 4fca6742f..d785856b5 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; using Moq; using Xunit; using TechTalk.SpecFlow.Bindings; @@ -65,13 +63,11 @@ private static BindingMethod CreateBindingMethodWithObjectParam(string name = "d [Fact] public void Should_GetBestMatch_succeed_when_proper_match() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -79,14 +75,12 @@ public void Should_GetBestMatch_succeed_when_proper_match() [Fact] public void Should_GetBestMatch_succeed_when_proper_match_and_non_matching_scopes() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("m1"), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("m2"), new BindingScope("non-matching-tag", null, null))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m1"))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m2"), new BindingScope("non-matching-tag", null, null))); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -94,13 +88,11 @@ public void Should_GetBestMatch_succeed_when_proper_match_and_non_matching_scope [Fact] public void Should_GetBestMatch_succeed_when_proper_match_with_parameters() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -108,14 +100,12 @@ public void Should_GetBestMatch_succeed_when_proper_match_with_parameters() [Fact] public void Should_GetBestMatch_succeed_when_proper_match_with_parameters_even_if_there_is_a_DataTable_overload() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam(), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -123,13 +113,11 @@ public void Should_GetBestMatch_succeed_when_proper_match_with_parameters_even_i [Fact] public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -138,14 +126,12 @@ public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters [Fact] public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters_even_if_there_is_a_DataTable_overload() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam(), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -153,13 +139,11 @@ public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters [Fact] public void Should_GetBestMatch_fail_when_scope_errors_with_single_match() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod(), new BindingScope("non-matching-tag", null, null))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod(), new BindingScope("non-matching-tag", null, null))); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); result.Success.Should().BeFalse(); ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); @@ -168,29 +152,25 @@ public void Should_GetBestMatch_fail_when_scope_errors_with_single_match() [Fact] public void Should_GetBestMatch_fail_when_scope_errors_with_multiple_matches() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"), new BindingScope("non-matching-tag", null, null))); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"), new BindingScope("other-non-matching-tag", null, null))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"), new BindingScope("non-matching-tag", null, null))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"), new BindingScope("other-non-matching-tag", null, null))); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); result.Success.Should().BeFalse(); ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); } [Fact] // in case of single parameter error, we pretend success - the error will be displayed runtime - public void Should_GetBestMatch_succeed_when_paramter_errors_with_single_match() + public void Should_GetBestMatch_succeed_when_parameter_errors_with_single_match() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethod(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -198,14 +178,12 @@ public void Should_GetBestMatch_succeed_when_paramter_errors_with_single_match() [Fact] public void Should_GetBestMatch_fail_when_parameter_errors_with_multiple_matches() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy1"), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy2"), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy1"))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy2"))); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); result.Success.Should().BeFalse(); ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.ParameterErrors); @@ -214,14 +192,12 @@ public void Should_GetBestMatch_fail_when_parameter_errors_with_multiple_matches [Fact] public void Should_GetBestMatch_fail_when_multiple_matches() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"))); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"))); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); result.Success.Should().BeFalse(); ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousSteps); @@ -230,14 +206,12 @@ public void Should_GetBestMatch_fail_when_multiple_matches() [Fact] public void Should_GetBestMatch_succeed_when_multiple_matches_are_on_the_same_method() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy"), null)); - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, ".*", CreateBindingMethod("dummy"), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); result.Success.Should().BeTrue(); } @@ -245,13 +219,11 @@ public void Should_GetBestMatch_succeed_when_multiple_matches_are_on_the_same_me [Fact] public void Should_GetBestMatch_succeed_when_no_matching_step_definitions() { - whenStepDefinitions.Add(new StepDefinitionBinding(StepDefinitionType.When, "non-maching-regex", CreateBindingMethod(), null)); + whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "non-maching-regex", CreateBindingMethod())); var sut = CreateSUT(); - StepDefinitionAmbiguityReason ambiguityReason; - List candidatingMatches; - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out ambiguityReason, out candidatingMatches); + var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); result.Success.Should().BeFalse(); ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.None); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepDefinitionHelper.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepDefinitionHelper.cs new file mode 100644 index 000000000..79fd01a20 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepDefinitionHelper.cs @@ -0,0 +1,19 @@ +using System; +using FluentAssertions; +using Moq; +using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.RuntimeTests; + +internal static class StepDefinitionHelper +{ + public static IStepDefinitionBinding CreateRegex(StepDefinitionType stepDefinitionType, string regex, IBindingMethod bindingMethod = null, BindingScope bindingScope = null) + { + bindingMethod ??= new Mock().Object; + var builder = new RegexStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, regex); + var stepDefinitionBinding = builder.BuildSingle(); + stepDefinitionBinding.IsValid.Should().BeTrue($"the {nameof(CreateRegex)} method should create valid step definitions"); + return stepDefinitionBinding; + } +} \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature index 880d2b529..c3d14d114 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature @@ -1,4 +1,5 @@ -Feature: Basic scenario execution +@focus +Feature: Basic scenario execution Background: Given there is a feature file in the project as From b939028e556228aca9e99411039602d28d761539 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 16:36:24 +0200 Subject: [PATCH 04/38] Fix unit tests --- .../Discovery/BindingSourceProcessorTests.cs | 2 +- .../RuntimeBindingRegistryBuilderTests.cs | 41 ++++++++++++------- .../StepExecutionTestsBase.cs | 1 + 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs index 9b447480c..53404ae18 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs @@ -22,7 +22,6 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() BindingSourceType bindingSourceType = new BindingSourceType { - Attributes = new[] { CreateBindingSourceAttribute("BindingAttribute", "TechTalk.SpecFlow.BindingAttribute") @@ -39,6 +38,7 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() //ACT sut.ProcessType(bindingSourceType); sut.ProcessMethod(bindingSourceMethod); + sut.BuildingCompleted(); //ASSERT var binding = sut.StepDefinitionBindings.Should().ContainSingle().Subject; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 6f8a49da8..229dda0e7 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Reflection; using Xunit; @@ -213,7 +214,7 @@ public void ShouldFindBinding_WithDefaultOrder() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromType(typeof (ScopedHookExample)); + BuildCompleteBindingFromType(builder, typeof (ScopedHookExample)); Assert.Equal(4, bindingSourceProcessorStub.HookBindings.Count(s => s.HookOrder == 10000)); } @@ -223,7 +224,7 @@ public void ShouldFindBinding_WithSpecifiedPriorities() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromType(typeof (PrioritizedHookExample)); + BuildCompleteBindingFromType(builder, typeof (PrioritizedHookExample)); Assert.Equal(1, bindingSourceProcessorStub.HookBindings.Count( @@ -293,7 +294,7 @@ public void ShouldFindExampleConverter() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + BuildCompleteBindingFromAssembly(builder); Assert.Equal(1, bindingSourceProcessorStub.StepArgumentTransformationBindings.Count( s => @@ -301,18 +302,30 @@ public void ShouldFindExampleConverter() s.Regex.Match("").Success == false)); } + private static void BuildCompleteBindingFromAssembly(RuntimeBindingRegistryBuilder builder) + { + builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + builder.BuildingCompleted(); + } + + private static void BuildCompleteBindingFromType(RuntimeBindingRegistryBuilder builder, Type type) + { + builder.BuildBindingsFromType(type); + builder.BuildingCompleted(); + } + [Fact] public void ShouldFindScopedExampleConverter() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + BuildCompleteBindingFromAssembly(builder); Assert.Equal(2, - bindingSourceProcessorStub.StepDefinitionBindings.Count( - s => - s.StepDefinitionType == StepDefinitionType.Then && - s.Regex.Match("SpecificBindingRegistryTests").Success && s.IsScoped)); + bindingSourceProcessorStub.StepDefinitionBindings.Count( + s => + s.StepDefinitionType == StepDefinitionType.Then && + s.Regex.Match("SpecificBindingRegistryTests").Success && s.IsScoped)); Assert.Equal(0, bindingSourceProcessorStub.StepDefinitionBindings.Count( s => @@ -325,7 +338,7 @@ public void ShouldFindScopedHook_WithCtorArg() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + BuildCompleteBindingFromAssembly(builder); Assert.Equal(1, bindingSourceProcessorStub.HookBindings.Count(s => s.Method.Name == "Tag2BeforeScenario" && s.IsScoped)); @@ -336,7 +349,7 @@ public void ShouldFindScopedHook_WithMultipleCtorArg() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + BuildCompleteBindingFromAssembly(builder); Assert.Equal(2, bindingSourceProcessorStub.HookBindings.Count(s => s.Method.Name == "Tag34BeforeScenario" && s.IsScoped)); @@ -347,7 +360,7 @@ public void ShouldFindScopedHook_WithScopeAttribute() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromAssembly(Assembly.GetExecutingAssembly()); + BuildCompleteBindingFromAssembly(builder); Assert.Equal(1, bindingSourceProcessorStub.HookBindings.Count(s => s.Method.Name == "Tag1BeforeScenario" && s.IsScoped)); @@ -358,7 +371,7 @@ public void ShouldFindStepDefinitionsWithStepDefinitionAttributes() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromType(typeof(BindingClassWithStepDefinitionAttributes)); + BuildCompleteBindingFromType(builder, typeof(BindingClassWithStepDefinitionAttributes)); Assert.Equal(3, bindingSourceProcessorStub.StepDefinitionBindings.Count); Assert.Equal(1, bindingSourceProcessorStub.StepDefinitionBindings.Count(b => b.StepDefinitionType == StepDefinitionType.Given)); @@ -371,7 +384,7 @@ public void ShouldFindStepDefinitionsWithTranslatedAttributes() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromType(typeof(BindingClassWithTranslatedStepDefinitionAttributes)); + BuildCompleteBindingFromType(builder, typeof(BindingClassWithTranslatedStepDefinitionAttributes)); Assert.Equal(3, bindingSourceProcessorStub.StepDefinitionBindings.Count); Assert.Equal(1, bindingSourceProcessorStub.StepDefinitionBindings.Count(b => b.StepDefinitionType == StepDefinitionType.Given)); @@ -384,7 +397,7 @@ public void ShouldFindStepDefinitionsWithCustomAttribute() { var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); - builder.BuildBindingsFromType(typeof(BindingClassWithCustomStepDefinitionAttribute)); + BuildCompleteBindingFromType(builder, typeof(BindingClassWithCustomStepDefinitionAttribute)); Assert.Equal(2, bindingSourceProcessorStub.StepDefinitionBindings.Count); Assert.Equal(1, bindingSourceProcessorStub.StepDefinitionBindings.Count(b => b.StepDefinitionType == StepDefinitionType.Given)); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTestsBase.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTestsBase.cs index 4ed86b201..360b8f8f0 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTestsBase.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTestsBase.cs @@ -140,6 +140,7 @@ protected TestRunner GetTestRunnerFor(Action registerMocks, pa var builder = (RuntimeBindingRegistryBuilder)container.Resolve(); foreach (var bindingType in bindingTypes) builder.BuildBindingsFromType(bindingType); + builder.BuildingCompleted(); registerMocks?.Invoke(container); }); From 4ba1d1d87c84d8b9bbe7a533014b8671257e1e17 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 17:11:18 +0200 Subject: [PATCH 05/38] Introduce CucumberExpressionStepDefinitionBindingBuilder --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 8 ++-- ...rExpressionStepDefinitionBindingBuilder.cs | 38 ++++++++++++++++ TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 2 +- .../MethodNameStepDefinitionBindingBuilder.cs | 2 +- .../RegexStepDefinitionBindingBuilder.cs | 40 ++--------------- .../StepDefinitionBindingBuilderBase.cs | 45 +++++++++++++++++++ 6 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs create mode 100644 TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index b26c9e7ba..a5844cca7 100644 --- a/TechTalk.SpecFlow/Bindings/BindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -17,11 +17,13 @@ public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hoo return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder); } - public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType type, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString) + public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString) { return expressionString == null - ? new MethodNameStepDefinitionBindingBuilder(stepDefinitionRegexCalculator, type, bindingMethod, bindingScope) - : new RegexStepDefinitionBindingBuilder(type, bindingMethod, bindingScope, expressionString); + ? new MethodNameStepDefinitionBindingBuilder(stepDefinitionRegexCalculator, stepDefinitionType, bindingMethod, bindingScope) + : CucumberExpressionStepDefinitionBindingBuilder.IsCucumberExpression(expressionString) + ? new CucumberExpressionStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, expressionString) + : new RegexStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, expressionString); } public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs new file mode 100644 index 000000000..f5f1f8ea6 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs @@ -0,0 +1,38 @@ +using System; +using System.Text.RegularExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings; + +public class CucumberExpressionStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase +{ + private static readonly Regex ParameterPlaceholder = new(@"{\w*}"); + private static readonly Regex CommonRegexStepDefPatterns = new(@"(\([^\)]+[\*\+]\)|\.\*)"); + + public CucumberExpressionStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) : base(stepDefinitionType, bindingMethod, bindingScope, sourceExpression) + { + } + + public static bool IsCucumberExpression(string cucumberExpressionCandidate) + { + if (cucumberExpressionCandidate.StartsWith("^") || cucumberExpressionCandidate.EndsWith("$")) + return false; + + if (ParameterPlaceholder.IsMatch(cucumberExpressionCandidate)) + return true; + + if (CommonRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) + return false; + + return true; + } + + protected override string GetRegexSource(out string expressionType) + { + expressionType = StepDefinitionExpressionTypes.CucumberExpression; + if (!ParameterPlaceholder.IsMatch(_sourceExpression)) + return "^" + _sourceExpression + "$"; + + throw new NotImplementedException(); + } +} diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index 1188430b5..ce0c4c388 100644 --- a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs @@ -7,7 +7,7 @@ public interface IBindingFactory IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, int hookOrder); - IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType type, IBindingMethod bindingMethod, + IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionString); IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, diff --git a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs index eeba9c36a..c6072a15a 100644 --- a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs @@ -3,7 +3,7 @@ namespace TechTalk.SpecFlow.Bindings; -public class MethodNameStepDefinitionBindingBuilder : RegexStepDefinitionBindingBuilder +public class MethodNameStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase { private readonly IStepDefinitionRegexCalculator _stepDefinitionRegexCalculator; diff --git a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs index f68a1e65a..84906e198 100644 --- a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs @@ -1,26 +1,15 @@ using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; using TechTalk.SpecFlow.Bindings.Reflection; namespace TechTalk.SpecFlow.Bindings; -public class RegexStepDefinitionBindingBuilder : IStepDefinitionBindingBuilder +public class RegexStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase { - protected readonly StepDefinitionType _stepDefinitionType; - protected readonly IBindingMethod _bindingMethod; - protected readonly BindingScope _bindingScope; - protected readonly string _sourceExpression; - - public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) + public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) : base(stepDefinitionType, bindingMethod, bindingScope, sourceExpression) { - _stepDefinitionType = stepDefinitionType; - _bindingMethod = bindingMethod; - _bindingScope = bindingScope; - _sourceExpression = sourceExpression; } - protected virtual string GetRegexSource(out string expressionType) + protected override string GetRegexSource(out string expressionType) { expressionType = StepDefinitionExpressionTypes.RegularExpression; var regex = _sourceExpression; @@ -28,25 +17,4 @@ protected virtual string GetRegexSource(out string expressionType) if (!regex.EndsWith("$")) regex += "$"; return regex; } - - public virtual IEnumerable Build() - { - yield return BuildSingle(); - } - - public virtual IStepDefinitionBinding BuildSingle() - { - string expressionType = StepDefinitionExpressionTypes.Unknown; - try - { - var regexSource = GetRegexSource(out expressionType); - var regex = new Regex(regexSource, RegexOptions.CultureInvariant); - return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope, expressionType, _sourceExpression); - } - catch (Exception ex) - { - var errorMessage = ex.Message; - return StepDefinitionBinding.CreateInvalid(_stepDefinitionType, _bindingMethod, _bindingScope, expressionType, _sourceExpression, errorMessage); - } - } -} +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs new file mode 100644 index 000000000..0aaaebeb2 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings; + +public abstract class StepDefinitionBindingBuilderBase : IStepDefinitionBindingBuilder +{ + protected StepDefinitionType _stepDefinitionType; + protected IBindingMethod _bindingMethod; + protected BindingScope _bindingScope; + protected string _sourceExpression; + + protected StepDefinitionBindingBuilderBase(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) + { + _stepDefinitionType = stepDefinitionType; + _bindingMethod = bindingMethod; + _bindingScope = bindingScope; + _sourceExpression = sourceExpression; + } + + protected abstract string GetRegexSource(out string expressionType); + + public virtual IEnumerable Build() + { + yield return BuildSingle(); + } + + public virtual IStepDefinitionBinding BuildSingle() + { + string expressionType = StepDefinitionExpressionTypes.Unknown; + try + { + var regexSource = GetRegexSource(out expressionType); + var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope, expressionType, _sourceExpression); + } + catch (Exception ex) + { + var errorMessage = ex.Message; + return StepDefinitionBinding.CreateInvalid(_stepDefinitionType, _bindingMethod, _bindingScope, expressionType, _sourceExpression, errorMessage); + } + } +} From a785bc2a6013d345a44fcbe56cdd1f1e470b826d Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 17:21:51 +0200 Subject: [PATCH 06/38] Use IExpression on IStepDefinitionBinding --- .../Bindings/IStepDefinitionBinding.cs | 2 ++ .../MethodNameStepDefinitionBindingBuilder.cs | 2 +- .../Bindings/StepDefinitionBinding.cs | 11 +++++++---- .../Bindings/StepDefinitionBindingBuilderBase.cs | 14 +++++++++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs index 939d512a6..47f0bf1e6 100644 --- a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using CucumberExpressions; namespace TechTalk.SpecFlow.Bindings; @@ -10,4 +11,5 @@ public interface IStepDefinitionBinding : IScopedBinding, IBinding bool IsValid { get; } string ValidationErrorMessage { get; } Regex Regex { get; } + IExpression Expression { get; } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs index c6072a15a..eeba9c36a 100644 --- a/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/MethodNameStepDefinitionBindingBuilder.cs @@ -3,7 +3,7 @@ namespace TechTalk.SpecFlow.Bindings; -public class MethodNameStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase +public class MethodNameStepDefinitionBindingBuilder : RegexStepDefinitionBindingBuilder { private readonly IStepDefinitionRegexCalculator _stepDefinitionRegexCalculator; diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs index 0552329a7..2673ae155 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs @@ -1,5 +1,6 @@ using System; using System.Text.RegularExpressions; +using CucumberExpressions; using TechTalk.SpecFlow.Bindings.Reflection; namespace TechTalk.SpecFlow.Bindings; @@ -18,17 +19,19 @@ public class StepDefinitionBinding : MethodBinding, IStepDefinitionBinding public Regex Regex { get; } + public IExpression Expression { get; } + public BindingScope BindingScope { get; } public bool IsScoped => BindingScope != null; - public StepDefinitionBinding(StepDefinitionType stepDefinitionType, Regex regex, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionType, string sourceExpression) + public StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionType, string sourceExpression, IExpression expression) : base(bindingMethod) { StepDefinitionType = stepDefinitionType; - Regex = regex ?? throw new ArgumentNullException(nameof(regex)); BindingScope = bindingScope; - SourceExpression = sourceExpression; - ExpressionType = expressionType; + ExpressionType = expressionType ?? throw new ArgumentNullException(nameof(expressionType)); + SourceExpression = sourceExpression ?? throw new ArgumentNullException(nameof(sourceExpression)); + Expression = expression ?? throw new ArgumentNullException(nameof(expression)); ValidationErrorMessage = null; } diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs index 0aaaebeb2..362372747 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using CucumberExpressions; using TechTalk.SpecFlow.Bindings.Reflection; namespace TechTalk.SpecFlow.Bindings; @@ -27,14 +28,21 @@ public virtual IEnumerable Build() yield return BuildSingle(); } + protected virtual RegularExpression CreateExpression(out string expressionType) + { + var regexSource = GetRegexSource(out expressionType); + var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + var expression = new RegularExpression(regex); + return expression; + } + public virtual IStepDefinitionBinding BuildSingle() { string expressionType = StepDefinitionExpressionTypes.Unknown; try { - var regexSource = GetRegexSource(out expressionType); - var regex = new Regex(regexSource, RegexOptions.CultureInvariant); - return new StepDefinitionBinding(_stepDefinitionType, regex, _bindingMethod, _bindingScope, expressionType, _sourceExpression); + var expression = CreateExpression(out expressionType); + return new StepDefinitionBinding(_stepDefinitionType, _bindingMethod, _bindingScope, expressionType, _sourceExpression, expression); } catch (Exception ex) { From d3d049b6430b9f3c022f25d1ea44d32354b87eb0 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 5 May 2022 19:56:21 +0200 Subject: [PATCH 07/38] small fix & ignore cukeex scenario --- TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs | 4 ++-- .../Features/Execution/CucumberExpressions.feature | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs index 2673ae155..24dc88c7b 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs @@ -17,7 +17,7 @@ public class StepDefinitionBinding : MethodBinding, IStepDefinitionBinding public string ValidationErrorMessage { get; } - public Regex Regex { get; } + public Regex Regex => Expression?.Regex; public IExpression Expression { get; } @@ -39,7 +39,7 @@ private StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMet : base(bindingMethod) { StepDefinitionType = stepDefinitionType; - Regex = null; + Expression = null; BindingScope = bindingScope; SourceExpression = sourceExpression; ExpressionType = expressionType; diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature index fe60e8630..e70a05801 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -3,6 +3,7 @@ Feature: Cucumber expressions Rule: Shoud support Cucumber expressions +@ignore Scenario: Simple Cucumber expresions are used for Step Definitions Given a scenario 'Simple Scenario' as """ From 80580e43603196482bef651307f8cf0b070db7ef Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 6 May 2022 15:14:29 +0200 Subject: [PATCH 08/38] Fix old project format tests --- ExternalRepositories/SpecFlow.TestProjectGenerator | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExternalRepositories/SpecFlow.TestProjectGenerator b/ExternalRepositories/SpecFlow.TestProjectGenerator index bca83adcf..46d41a930 160000 --- a/ExternalRepositories/SpecFlow.TestProjectGenerator +++ b/ExternalRepositories/SpecFlow.TestProjectGenerator @@ -1 +1 @@ -Subproject commit bca83adcfbbe863c415fc334cb9bb393800eccba +Subproject commit 46d41a9307beb7a73fe4804448c6481848d46988 From 19e0e2fe9fac5be00d58d87ff4d12c99754832fb Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 6 May 2022 15:15:49 +0200 Subject: [PATCH 09/38] Introduce step definition error handling --- .../Bindings/IStepDefinitionBinding.cs | 2 +- .../Bindings/StepDefinitionBinding.cs | 8 ++--- .../ErrorHandling/ErrorProvider.cs | 10 +++++-- .../StepDefinitionMatchService.cs | 29 ++++++++++++------- .../PendingStepExceptionTests.cs | 6 +--- .../ErrorHandling/StubErrorProvider.cs | 26 +++++++++++++++++ .../StepDefinitionMatchServiceTest.cs | 3 +- 7 files changed, 61 insertions(+), 23 deletions(-) rename Tests/TechTalk.SpecFlow.RuntimeTests/{Exceptions => ErrorHandling}/PendingStepExceptionTests.cs (80%) create mode 100644 Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs diff --git a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs index 47f0bf1e6..b4da03f86 100644 --- a/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/IStepDefinitionBinding.cs @@ -9,7 +9,7 @@ public interface IStepDefinitionBinding : IScopedBinding, IBinding string SourceExpression { get; } string ExpressionType { get; } bool IsValid { get; } - string ValidationErrorMessage { get; } + string ErrorMessage { get; } Regex Regex { get; } IExpression Expression { get; } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs index 24dc88c7b..1f3ffba7a 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBinding.cs @@ -13,9 +13,9 @@ public class StepDefinitionBinding : MethodBinding, IStepDefinitionBinding public string ExpressionType { get; } - public bool IsValid => ValidationErrorMessage == null; + public bool IsValid => ErrorMessage == null; - public string ValidationErrorMessage { get; } + public string ErrorMessage { get; } public Regex Regex => Expression?.Regex; @@ -32,7 +32,7 @@ public StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMeth ExpressionType = expressionType ?? throw new ArgumentNullException(nameof(expressionType)); SourceExpression = sourceExpression ?? throw new ArgumentNullException(nameof(sourceExpression)); Expression = expression ?? throw new ArgumentNullException(nameof(expression)); - ValidationErrorMessage = null; + ErrorMessage = null; } private StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string expressionType, string sourceExpression, string errorMessage) @@ -43,7 +43,7 @@ private StepDefinitionBinding(StepDefinitionType stepDefinitionType, IBindingMet BindingScope = bindingScope; SourceExpression = sourceExpression; ExpressionType = expressionType; - ValidationErrorMessage = errorMessage; + ErrorMessage = errorMessage; } public static StepDefinitionBinding CreateInvalid(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, diff --git a/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs b/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs index 296ae9edf..f7881a1bd 100644 --- a/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs +++ b/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs @@ -23,15 +23,16 @@ public interface IErrorProvider Exception GetTooManyBindingParamError(int maxParam); Exception GetNonStaticEventError(IBindingMethod method); Exception GetObsoleteStepError(BindingObsoletion bindingObsoletion); + Exception GetInvalidStepDefinitionError(IStepDefinitionBinding stepDefinitionBinding); } internal class ErrorProvider : IErrorProvider { private readonly IStepFormatter stepFormatter; private readonly IUnitTestRuntimeProvider unitTestRuntimeProvider; - private readonly Configuration.SpecFlowConfiguration specFlowConfiguration; + private readonly SpecFlowConfiguration specFlowConfiguration; - public ErrorProvider(IStepFormatter stepFormatter, Configuration.SpecFlowConfiguration specFlowConfiguration, IUnitTestRuntimeProvider unitTestRuntimeProvider) + public ErrorProvider(IStepFormatter stepFormatter, SpecFlowConfiguration specFlowConfiguration, IUnitTestRuntimeProvider unitTestRuntimeProvider) { this.stepFormatter = stepFormatter; this.unitTestRuntimeProvider = unitTestRuntimeProvider; @@ -124,5 +125,10 @@ public Exception GetObsoleteStepError(BindingObsoletion bindingObsoletion) { throw new BindingException(bindingObsoletion.Message); } + + public Exception GetInvalidStepDefinitionError(IStepDefinitionBinding stepDefinitionBinding) + { + return new BindingException($"Invalid step definition! The step definition method '{GetMethodText(stepDefinitionBinding.Method)}' is invalid: {stepDefinitionBinding.ErrorMessage}."); + } } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs b/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs index ec51bafed..e1f58dd1e 100644 --- a/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs +++ b/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Bindings.Reflection; +using TechTalk.SpecFlow.ErrorHandling; namespace TechTalk.SpecFlow.Infrastructure { @@ -26,13 +27,15 @@ public interface IStepDefinitionMatchService public class StepDefinitionMatchService : IStepDefinitionMatchService { - private readonly IBindingRegistry bindingRegistry; - private readonly IStepArgumentTypeConverter stepArgumentTypeConverter; + private readonly IBindingRegistry _bindingRegistry; + private readonly IStepArgumentTypeConverter _stepArgumentTypeConverter; + private readonly IErrorProvider _errorProvider; - public StepDefinitionMatchService(IBindingRegistry bindingRegistry, IStepArgumentTypeConverter stepArgumentTypeConverter) + public StepDefinitionMatchService(IBindingRegistry bindingRegistry, IStepArgumentTypeConverter stepArgumentTypeConverter, IErrorProvider errorProvider) { - this.bindingRegistry = bindingRegistry; - this.stepArgumentTypeConverter = stepArgumentTypeConverter; + _bindingRegistry = bindingRegistry; + _stepArgumentTypeConverter = stepArgumentTypeConverter; + _errorProvider = errorProvider; } private object[] CalculateArguments(Match match, StepInstance stepInstance) @@ -52,12 +55,12 @@ private bool CanConvertArg(object value, IBindingType typeToConvertTo, CultureIn if (value.GetType().IsAssignableTo(typeToConvertTo)) return true; - return stepArgumentTypeConverter.CanConvert(value, typeToConvertTo, bindingCulture); + return _stepArgumentTypeConverter.CanConvert(value, typeToConvertTo, bindingCulture); } public bool Ready { - get { return bindingRegistry.Ready; } + get { return _bindingRegistry.Ready; } } public BindingMatch Match(IStepDefinitionBinding stepDefinitionBinding, StepInstance stepInstance, CultureInfo bindingCulture, bool useRegexMatching = true, bool useParamMatching = true, bool useScopeMatching = true) @@ -69,8 +72,14 @@ public BindingMatch Match(IStepDefinitionBinding stepDefinitionBinding, StepInst return BindingMatch.NonMatching; Match match = null; - if (useRegexMatching && stepDefinitionBinding.Regex != null && !(match = stepDefinitionBinding.Regex.Match(stepInstance.Text)).Success) - return BindingMatch.NonMatching; + if (useRegexMatching) + { + if (!stepDefinitionBinding.IsValid) + throw _errorProvider.GetInvalidStepDefinitionError(stepDefinitionBinding); + match = stepDefinitionBinding.Regex.Match(stepInstance.Text); + if (!match.Success) + return BindingMatch.NonMatching; + } int scopeMatches = 0; if (useScopeMatching && stepDefinitionBinding.IsScoped && stepInstance.StepContext != null && !stepDefinitionBinding.BindingScope.Match(stepInstance.StepContext, out scopeMatches)) @@ -164,7 +173,7 @@ protected virtual StepDefinitionAmbiguityReason OnNoMatch(StepInstance stepInsta protected IEnumerable GetCandidatingBindings(StepInstance stepInstance, CultureInfo bindingCulture, bool useRegexMatching = true, bool useParamMatching = true, bool useScopeMatching = true) { - var matches = bindingRegistry.GetConsideredStepDefinitions(stepInstance.StepDefinitionType, stepInstance.Text).Select(b => Match(b, stepInstance, bindingCulture, useRegexMatching, useParamMatching, useScopeMatching)).Where(b => b.Success); + var matches = _bindingRegistry.GetConsideredStepDefinitions(stepInstance.StepDefinitionType, stepInstance.Text).Select(b => Match(b, stepInstance, bindingCulture, useRegexMatching, useParamMatching, useScopeMatching)).Where(b => b.Success); // we remove duplicate maches for the same method (take the highest scope matches from each) matches = matches.GroupBy(m => m.StepBinding.Method, (_, methodMatches) => methodMatches.OrderByDescending(m => m.ScopeMatches).First(), BindingMethodComparer.Instance); return matches; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Exceptions/PendingStepExceptionTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/PendingStepExceptionTests.cs similarity index 80% rename from Tests/TechTalk.SpecFlow.RuntimeTests/Exceptions/PendingStepExceptionTests.cs rename to Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/PendingStepExceptionTests.cs index 35fc7a4a2..6e0f6c222 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Exceptions/PendingStepExceptionTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/PendingStepExceptionTests.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using FluentAssertions; using Xunit; -namespace TechTalk.SpecFlow.RuntimeTests.Exceptions +namespace TechTalk.SpecFlow.RuntimeTests.ErrorHandling { public class PendingStepExceptionTests { diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs new file mode 100644 index 000000000..5acf65e4b --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs @@ -0,0 +1,26 @@ +using System; +using Moq; +using TechTalk.SpecFlow.Configuration; +using TechTalk.SpecFlow.ErrorHandling; +using TechTalk.SpecFlow.Tracing; +using TechTalk.SpecFlow.UnitTestProvider; + +namespace TechTalk.SpecFlow.RuntimeTests.ErrorHandling +{ + internal class StubErrorProvider : ErrorProvider + { + public StubErrorProvider() : + base(new StepFormatter(), ConfigurationLoader.GetDefault(), GetStubUnitTestProvider()) + { + } + + private static IUnitTestRuntimeProvider GetStubUnitTestProvider() + { + var mock = new Mock(); + mock.Setup(utp => utp.TestIgnore(It.IsAny())).Throws(); + mock.Setup(utp => utp.TestInconclusive(It.IsAny())).Throws(); + mock.Setup(utp => utp.TestPending(It.IsAny())).Throws(); + return mock.Object; + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs index d785856b5..67193e452 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs @@ -7,6 +7,7 @@ using TechTalk.SpecFlow.Bindings.Reflection; using TechTalk.SpecFlow.Infrastructure; using FluentAssertions; +using TechTalk.SpecFlow.RuntimeTests.ErrorHandling; namespace TechTalk.SpecFlow.RuntimeTests.Infrastructure { @@ -30,7 +31,7 @@ public StepDefinitionMatchServiceTest() private StepDefinitionMatchService CreateSUT() { - return new StepDefinitionMatchService(bindingRegistryMock.Object, stepArgumentTypeConverterMock.Object); + return new StepDefinitionMatchService(bindingRegistryMock.Object, stepArgumentTypeConverterMock.Object, new StubErrorProvider()); } private static BindingMethod CreateBindingMethod(string name = "dummy") From 25d54ca3bc67751fddeb43b96ab4df218c71c8f2 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 6 May 2022 17:36:18 +0200 Subject: [PATCH 10/38] Integrate Cucumber Expressions --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 9 +- ...rExpressionStepDefinitionBindingBuilder.cs | 38 ------- ...erExpressionParameterTypeTransformation.cs | 21 ++++ .../CucumberExpressionParameterType.cs | 35 ++++++ .../CucumberExpressionParameterTypeBinding.cs | 13 +++ ...CucumberExpressionParameterTypeRegistry.cs | 104 ++++++++++++++++++ ...rExpressionStepDefinitionBindingBuilder.cs | 49 +++++++++ ...sionStepDefinitionBindingBuilderFactory.cs | 25 +++++ ...erExpressionParameterTypeTransformation.cs | 12 ++ ...SpecFlowCucumberExpressionParameterType.cs | 11 ++ ...erExpressionParameterTypeTransformation.cs | 59 ++++++++++ .../RegexStepDefinitionBindingBuilder.cs | 12 +- .../StepDefinitionBindingBuilderBase.cs | 10 +- .../DefaultDependencyProvider.cs | 2 + .../BindingSourceProcessorStub.cs | 3 +- .../StepExecutionTests.cs | 6 +- .../Execution/CucumberExpressions.feature | 1 - .../StepDefinitions/BindingSteps.cs | 8 +- 18 files changed, 358 insertions(+), 60 deletions(-) delete mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/BuiltInCucumberExpressionParameterTypeTransformation.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/ICucumberExpressionParameterTypeTransformation.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/ISpecFlowCucumberExpressionParameterType.cs create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index a5844cca7..7a6176719 100644 --- a/TechTalk.SpecFlow/Bindings/BindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -1,14 +1,17 @@ -using TechTalk.SpecFlow.Bindings.Reflection; +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) + public BindingFactory(IStepDefinitionRegexCalculator stepDefinitionRegexCalculator, ICucumberExpressionStepDefinitionBindingBuilderFactory cucumberExpressionStepDefinitionBindingBuilderFactory) { this.stepDefinitionRegexCalculator = stepDefinitionRegexCalculator; + _cucumberExpressionStepDefinitionBindingBuilderFactory = cucumberExpressionStepDefinitionBindingBuilderFactory; } public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, @@ -22,7 +25,7 @@ public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefi return expressionString == null ? new MethodNameStepDefinitionBindingBuilder(stepDefinitionRegexCalculator, stepDefinitionType, bindingMethod, bindingScope) : CucumberExpressionStepDefinitionBindingBuilder.IsCucumberExpression(expressionString) - ? new CucumberExpressionStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, expressionString) + ? _cucumberExpressionStepDefinitionBindingBuilderFactory.Create(stepDefinitionType, bindingMethod, bindingScope, expressionString) : new RegexStepDefinitionBindingBuilder(stepDefinitionType, bindingMethod, bindingScope, expressionString); } diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs deleted file mode 100644 index f5f1f8ea6..000000000 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressionStepDefinitionBindingBuilder.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using TechTalk.SpecFlow.Bindings.Reflection; - -namespace TechTalk.SpecFlow.Bindings; - -public class CucumberExpressionStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase -{ - private static readonly Regex ParameterPlaceholder = new(@"{\w*}"); - private static readonly Regex CommonRegexStepDefPatterns = new(@"(\([^\)]+[\*\+]\)|\.\*)"); - - public CucumberExpressionStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) : base(stepDefinitionType, bindingMethod, bindingScope, sourceExpression) - { - } - - public static bool IsCucumberExpression(string cucumberExpressionCandidate) - { - if (cucumberExpressionCandidate.StartsWith("^") || cucumberExpressionCandidate.EndsWith("$")) - return false; - - if (ParameterPlaceholder.IsMatch(cucumberExpressionCandidate)) - return true; - - if (CommonRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) - return false; - - return true; - } - - protected override string GetRegexSource(out string expressionType) - { - expressionType = StepDefinitionExpressionTypes.CucumberExpression; - if (!ParameterPlaceholder.IsMatch(_sourceExpression)) - return "^" + _sourceExpression + "$"; - - throw new NotImplementedException(); - } -} diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/BuiltInCucumberExpressionParameterTypeTransformation.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/BuiltInCucumberExpressionParameterTypeTransformation.cs new file mode 100644 index 000000000..eae601341 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/BuiltInCucumberExpressionParameterTypeTransformation.cs @@ -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; + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs new file mode 100644 index 000000000..cd390dee0 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs new file mode 100644 index 000000000..e13dcc7d4 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs @@ -0,0 +1,13 @@ +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public class CucumberExpressionParameterTypeBinding : StepArgumentTransformationBinding +{ + public string Name { get; } + + public CucumberExpressionParameterTypeBinding(string regexString, IBindingMethod bindingMethod, string name) : base(regexString, bindingMethod) + { + Name = name; + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs new file mode 100644 index 000000000..d347f7525 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CucumberExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public class CucumberExpressionParameterTypeRegistry : IParameterTypeRegistry +{ + private readonly IBindingRegistry _bindingRegistry; + private readonly Lazy> _parameterTypesByName; + + public CucumberExpressionParameterTypeRegistry(IBindingRegistry bindingRegistry) + { + _bindingRegistry = bindingRegistry; + _parameterTypesByName = new Lazy>(InitializeRegistry, true); + } + + public static string ConvertQuotedString(string message) + { + return message; + } + + private Dictionary InitializeRegistry() + { + var boolBindingType = new RuntimeBindingType(typeof(bool)); + var byteBindingType = new RuntimeBindingType(typeof(byte)); + var charBindingType = new RuntimeBindingType(typeof(char)); + var dateTimeBindingType = new RuntimeBindingType(typeof(DateTime)); + var decimalBindingType = new RuntimeBindingType(typeof(decimal)); + var doubleBindingType = new RuntimeBindingType(typeof(double)); + var floatBindingType = new RuntimeBindingType(typeof(float)); + var shortBindingType = new RuntimeBindingType(typeof(short)); + var intBindingType = new RuntimeBindingType(typeof(int)); + var longBindingType = new RuntimeBindingType(typeof(long)); + var objectBindingType = new RuntimeBindingType(typeof(object)); + var stringBindingType = new RuntimeBindingType(typeof(string)); + var guidBindingType = new RuntimeBindingType(typeof(Guid)); + + // exposing built-in transformations + + var builtInTransformations = new[] + { + // official cucumber expression types + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, objectBindingType, name: string.Empty, useForSnippets: false), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, intBindingType, ParameterTypeConstants.IntParameterName, weight: 1000), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, doubleBindingType, ParameterTypeConstants.FloatParameterName), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), + + // other types supported by SpecFlow by default: Make them accessible with type name (e.g. Int32) + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, boolBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, byteBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, charBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, dateTimeBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, decimalBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, floatBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, shortBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, intBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, longBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, guidBindingType), + }; + + var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); + _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + + var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); + + var parameterTypes = builtInTransformations.Cast() + .Concat(userTransformations) + .GroupBy(t => new Tuple(t.TargetType, t.Name)) + .Select(g => new CucumberExpressionParameterType(g.Key.Item2 ?? g.Key.Item1.Name, g.Key.Item1, g)) + .ToDictionary(pt => pt.Name, pt => (ISpecFlowCucumberExpressionParameterType)pt); + + DumpParameterTypes(parameterTypes); + + return parameterTypes; + } + + [Conditional("DEBUG")] + private static void DumpParameterTypes(Dictionary parameterTypes) + { + foreach (var parameterType in parameterTypes) + { + Console.WriteLine( + $"PT: {parameterType.Key}, transformations: {string.Join(",", parameterType.Value.Transformations.Select(t => t.Regex))}, Regexps: {string.Join(",", parameterType.Value.RegexStrings)}"); + } + } + + public IParameterType LookupByTypeName(string name) + { + if (_parameterTypesByName.Value.TryGetValue(name, out var parameterType)) + return parameterType; + return null; + } + + public IEnumerable GetParameterTypes() + { + return _parameterTypesByName.Value.Values; + } +} diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs new file mode 100644 index 000000000..f2e11bdf9 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs @@ -0,0 +1,49 @@ +using System; +using System.Text.RegularExpressions; +using CucumberExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public class CucumberExpressionStepDefinitionBindingBuilder : StepDefinitionBindingBuilderBase +{ + private static readonly Regex ParameterPlaceholder = new(@"{\w*}"); + private static readonly Regex CommonRegexStepDefPatterns = new(@"(\([^\)]+[\*\+]\)|\.\*)"); + private static readonly Regex ExtendedRegexStepDefPatterns = new(@"(\\\.|\\d\+)"); // \. \d+ + + private readonly CucumberExpressionParameterTypeRegistry _cucumberExpressionParameterTypeRegistry; + + public CucumberExpressionStepDefinitionBindingBuilder( + CucumberExpressionParameterTypeRegistry cucumberExpressionParameterTypeRegistry, + StepDefinitionType stepDefinitionType, + IBindingMethod bindingMethod, + BindingScope bindingScope, + string sourceExpression) : base(stepDefinitionType, bindingMethod, bindingScope, sourceExpression) + { + _cucumberExpressionParameterTypeRegistry = cucumberExpressionParameterTypeRegistry; + } + + public static bool IsCucumberExpression(string cucumberExpressionCandidate) + { + if (cucumberExpressionCandidate.StartsWith("^") || cucumberExpressionCandidate.EndsWith("$")) + return false; + + if (ParameterPlaceholder.IsMatch(cucumberExpressionCandidate)) + return true; + + if (CommonRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) + return false; + + //TODO[cukeex]: consider this once more + if (ExtendedRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) + return false; + + return true; + } + + protected override IExpression CreateExpression(out string expressionType) + { + expressionType = StepDefinitionExpressionTypes.CucumberExpression; + return new CucumberExpression(_sourceExpression, _cucumberExpressionParameterTypeRegistry); + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs new file mode 100644 index 000000000..89af37cd6 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs @@ -0,0 +1,25 @@ +using System; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public interface ICucumberExpressionStepDefinitionBindingBuilderFactory +{ + CucumberExpressionStepDefinitionBindingBuilder Create(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression); +} + +public class CucumberExpressionStepDefinitionBindingBuilderFactory + : ICucumberExpressionStepDefinitionBindingBuilderFactory +{ + private readonly CucumberExpressionParameterTypeRegistry _cucumberExpressionParameterTypeRegistry; + + public CucumberExpressionStepDefinitionBindingBuilderFactory(CucumberExpressionParameterTypeRegistry cucumberExpressionParameterTypeRegistry) + { + _cucumberExpressionParameterTypeRegistry = cucumberExpressionParameterTypeRegistry; + } + + public CucumberExpressionStepDefinitionBindingBuilder Create(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) + { + return new CucumberExpressionStepDefinitionBindingBuilder(_cucumberExpressionParameterTypeRegistry, stepDefinitionType, bindingMethod, bindingScope, sourceExpression); + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/ICucumberExpressionParameterTypeTransformation.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/ICucumberExpressionParameterTypeTransformation.cs new file mode 100644 index 000000000..a67448d7a --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/ICucumberExpressionParameterTypeTransformation.cs @@ -0,0 +1,12 @@ +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public interface ICucumberExpressionParameterTypeTransformation +{ + string Name { get; } + string Regex { get; } + IBindingType TargetType { get; } + bool UseForSnippets { get; } + int Weight { get; } +} diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/ISpecFlowCucumberExpressionParameterType.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/ISpecFlowCucumberExpressionParameterType.cs new file mode 100644 index 000000000..661e412ef --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/ISpecFlowCucumberExpressionParameterType.cs @@ -0,0 +1,11 @@ +using System; +using CucumberExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public interface ISpecFlowCucumberExpressionParameterType : IParameterType +{ + IBindingType TargetType { get; } + ICucumberExpressionParameterTypeTransformation[] Transformations { get; } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs new file mode 100644 index 000000000..ad1444f1c --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs @@ -0,0 +1,59 @@ +using System; +using TechTalk.SpecFlow.Bindings.Reflection; +using RegexClass = System.Text.RegularExpressions.Regex; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public class UserDefinedCucumberExpressionParameterTypeTransformation : ICucumberExpressionParameterTypeTransformation +{ + private readonly IStepArgumentTransformationBinding _stepArgumentTransformationBinding; + + public string Name => GetProvidedTransformationTypeName(_stepArgumentTransformationBinding); + public string Regex { get; } + public IBindingType TargetType => _stepArgumentTransformationBinding.Method.ReturnType; + public bool UseForSnippets => true; + public int Weight => 0; + + public UserDefinedCucumberExpressionParameterTypeTransformation(IStepArgumentTransformationBinding stepArgumentTransformationBinding) + { + _stepArgumentTransformationBinding = stepArgumentTransformationBinding; + Regex = GetCucumberExpressionRegex(stepArgumentTransformationBinding); + } + + private static string GetProvidedTransformationTypeName(IStepArgumentTransformationBinding transformationBinding) + { + // later IStepArgumentTransformationBinding could capture name + return transformationBinding is CucumberExpressionParameterTypeBinding parameterTypeBinding ? parameterTypeBinding.Name : null; + } + + private string GetCucumberExpressionRegex(IStepArgumentTransformationBinding stepArgumentTransformationBinding) + { + var regexString = stepArgumentTransformationBinding.Regex?.ToString().TrimStart('^').TrimEnd('$'); + if (regexString == null) + return CucumberExpressionParameterType.MatchAllRegex; + + if (RegexClass.IsMatch(regexString, @"\(\?|\\\(")) + return CucumberExpressionParameterType.MatchAllRegex; // too complex regex + + var regexRemovedCaptureGroups = regexString.Replace("(", "(?:"); + var cucumberExpressionTypeRegex = $"({regexRemovedCaptureGroups})"; + + if (!IsValidRegex(cucumberExpressionTypeRegex)) + return CucumberExpressionParameterType.MatchAllRegex; // too complex regex + + return cucumberExpressionTypeRegex; + } + + private bool IsValidRegex(string regexString) + { + try + { + var _ = new RegexClass(regexString); + return true; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs index 84906e198..3dbd6d0fe 100644 --- a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs @@ -1,4 +1,6 @@ using System; +using System.Text.RegularExpressions; +using CucumberExpressions; using TechTalk.SpecFlow.Bindings.Reflection; namespace TechTalk.SpecFlow.Bindings; @@ -9,7 +11,15 @@ public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, { } - protected override string GetRegexSource(out string expressionType) + protected override IExpression CreateExpression(out string expressionType) + { + var regexSource = GetRegexSource(out expressionType); + var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + var expression = new RegularExpression(regex); + return expression; + } + + protected virtual string GetRegexSource(out string expressionType) { expressionType = StepDefinitionExpressionTypes.RegularExpression; var regex = _sourceExpression; diff --git a/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs index 362372747..ddc7c5853 100644 --- a/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs +++ b/TechTalk.SpecFlow/Bindings/StepDefinitionBindingBuilderBase.cs @@ -21,20 +21,12 @@ protected StepDefinitionBindingBuilderBase(StepDefinitionType stepDefinitionType _sourceExpression = sourceExpression; } - protected abstract string GetRegexSource(out string expressionType); - public virtual IEnumerable Build() { yield return BuildSingle(); } - protected virtual RegularExpression CreateExpression(out string expressionType) - { - var regexSource = GetRegexSource(out expressionType); - var regex = new Regex(regexSource, RegexOptions.CultureInvariant); - var expression = new RegularExpression(regex); - return expression; - } + protected abstract IExpression CreateExpression(out string expressionType); public virtual IStepDefinitionBinding BuildSingle() { diff --git a/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs b/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs index 4ef51dff2..c63e31829 100644 --- a/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs +++ b/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs @@ -4,6 +4,7 @@ using TechTalk.SpecFlow.Analytics.UserId; using TechTalk.SpecFlow.BindingSkeletons; using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.CucumberExpressions; using TechTalk.SpecFlow.Bindings.Discovery; using TechTalk.SpecFlow.Configuration; using TechTalk.SpecFlow.EnvironmentAccess; @@ -39,6 +40,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); #pragma warning disable CS0618 container.RegisterTypeAs(); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs index beed81df7..151a2065a 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSourceProcessorStub.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.CucumberExpressions; using TechTalk.SpecFlow.Bindings.Discovery; using TechTalk.SpecFlow.Configuration; @@ -11,7 +12,7 @@ public class BindingSourceProcessorStub : BindingSourceProcessor, IRuntimeBindin public readonly List HookBindings = new List(); public readonly List StepArgumentTransformationBindings = new List(); - public BindingSourceProcessorStub() : base(new BindingFactory(new StepDefinitionRegexCalculator(ConfigurationLoader.GetDefault()))) + public BindingSourceProcessorStub() : base(new BindingFactory(new StepDefinitionRegexCalculator(ConfigurationLoader.GetDefault()), new CucumberExpressionStepDefinitionBindingBuilderFactory(new CucumberExpressionParameterTypeRegistry(new BindingRegistry())))) { } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTests.cs index 39ef49223..21fb50743 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepExecutionTests.cs @@ -17,13 +17,13 @@ public virtual void BindingWithoutParam() } - [Given("sample step with (single) param")] + [Given("^sample step with (single) param$")] public virtual void BindingWithSingleParam(string param) { } - [Given("sample step with (multi)(ple) param")] + [Given("^sample step with (multi)(ple) param$")] public virtual void BindingWithMultipleParam(string param1, string param2) { @@ -47,7 +47,7 @@ public virtual void BindingWithTableAndMlStringParam(string multiLineString, Tab } - [Given("sample step with (mixed) params")] + [Given("^sample step with (mixed) params$")] public virtual void BindingWithMixedParams(string param1, string multiLineString, Table table) { diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature index e70a05801..fe60e8630 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -3,7 +3,6 @@ Feature: Cucumber expressions Rule: Shoud support Cucumber expressions -@ignore Scenario: Simple Cucumber expresions are used for Step Definitions Given a scenario 'Simple Scenario' as """ diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/BindingSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/BindingSteps.cs index 13b5100aa..8cf40519c 100644 --- a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/BindingSteps.cs +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/BindingSteps.cs @@ -61,10 +61,10 @@ public void GivenTheFollowingStepDefinitionInTheProject(string projectName, stri _projectsDriver.AddStepBinding(projectName, stepDefinition); } - [Given(@"the following hooks?")] - [Given(@"the following step definitions?")] - [Given(@"the following step argument transformations?")] - [Given(@"the following binding methods?")] + [Given(@"the following hook(s)")] + [Given(@"the following step definition(s)")] + [Given(@"the following step argument transformation(s)")] + [Given(@"the following binding method(s)")] public void GivenTheFollowingBindings(string bindingCode) { _projectsDriver.AddStepBinding(bindingCode); From 3c969919514e27439f7a7900e1428ad43623cc51 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Tue, 10 May 2022 08:11:34 +0200 Subject: [PATCH 11/38] Force regex for Step Def --- .../TechTalk.SpecFlow.Specs/StepDefinitions/ExecutionSteps.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/ExecutionSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/ExecutionSteps.cs index 822b34378..846f9ef48 100644 --- a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/ExecutionSteps.cs +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/ExecutionSteps.cs @@ -39,8 +39,8 @@ public void GivenIsUsedForCompiling(BuildTool buildTool) _compilationDriver.SetBuildTool(buildTool); } - [When(@"I build the solution using '(dotnet msbuild|dotnet build|MSBuild)'")] - [When(@"I compile the solution using '(dotnet msbuild|dotnet build|MSBuild)'")] + [When(@"^I build the solution using '(dotnet msbuild|dotnet build|MSBuild)'$")] + [When(@"^I compile the solution using '(dotnet msbuild|dotnet build|MSBuild)'$")] public void WhenIBuildTheSolutionUsing(BuildTool buildTool) { _compilationDriver.CompileSolution(buildTool); From 51a5cf2e2ca16c06f5ad32a1576638faadcbd875 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Tue, 10 May 2022 17:43:52 +0200 Subject: [PATCH 12/38] Force regex for Step Def once more --- .../StepDefinitions/MultipleSpecsProjectsSteps.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/MultipleSpecsProjectsSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/MultipleSpecsProjectsSteps.cs index c74fa1fd0..7f04ec8f0 100644 --- a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/MultipleSpecsProjectsSteps.cs +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/MultipleSpecsProjectsSteps.cs @@ -16,14 +16,14 @@ public MultipleSpecsProjectsSteps(ProjectsDriver projectsDriver, CompilationDriv _compilationResultDriver = compilationResultDriver; } - [Given("I have (Specs.Project.[A-Z]) and (Specs.Project.[A-Z]) using the same unit test provider")] + [Given("^I have (Specs.Project.[A-Z]) and (Specs.Project.[A-Z]) using the same unit test provider")] public void GivenIHaveTwoSpecsProjectsWithTheSameUnitTestProvider(string projectName1, string projectName2) { _projectsDriver.CreateProject(projectName1, "C#"); _projectsDriver.CreateProject(projectName2, "C#"); } - [Given(@"(Specs.Project.[A-Z]) references (Specs.Project.[A-Z])")] + [Given(@"^(Specs.Project.[A-Z]) references (Specs.Project.[A-Z])")] public void GivenSpecsProjectOneReferencesSpecsProjectTwo(string targetProjectName, string referencedProjectName) { _projectsDriver.AddProjectReference(referencedProjectName, targetProjectName); From 1eeb6b22b892e5c1065c32abf6c8a4e0a1cdc43d Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 20 May 2022 13:49:43 +0200 Subject: [PATCH 13/38] add tests --- ...CucumberExpressionParameterTypeRegistry.cs | 5 +- .../CucumberExpressionIntegrationTests.cs | 226 ++++++++++++++++++ ...berExpressionParameterTypeRegistryTests.cs | 27 +++ ...essionStepDefinitionBindingBuilderTests.cs | 59 +++++ 4 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs create mode 100644 Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs create mode 100644 Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index d347f7525..f8e059281 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -47,7 +47,9 @@ private Dictionary InitializeR new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, objectBindingType, name: string.Empty, useForSnippets: false), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, intBindingType, ParameterTypeConstants.IntParameterName, weight: 1000), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, doubleBindingType, ParameterTypeConstants.FloatParameterName), - new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), + //TODO[cukeex]: fix constant in cuke ex + //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, "word", useForSnippets: false), // other types supported by SpecFlow by default: Make them accessible with type name (e.g. Int32) new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, boolBindingType), @@ -63,6 +65,7 @@ private Dictionary InitializeR new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, guidBindingType), }; + //TODO[cukeex]: support strings without custom converters var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs new file mode 100644 index 000000000..3e7ac6c15 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using BoDi; +using FluentAssertions; +using Moq; +using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.Discovery; +using TechTalk.SpecFlow.Bindings.Reflection; +using TechTalk.SpecFlow.Infrastructure; +using TechTalk.SpecFlow.UnitTestProvider; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; + +public class CucumberExpressionIntegrationTests +{ + public class SampleBindings + { + public bool WasStepDefWithNoParamExecuted; + public List<(object ParamValue, Type ParamType)> ExecutedParams = new(); + + public void StepDefWithNoParam() + { + WasStepDefWithNoParamExecuted = true; + } + + public void StepDefWithStringParam(string stringParam) + { + ExecutedParams.Add((stringParam, typeof(string))); + } + + public void StepDefWithIntParam(int intParam) + { + ExecutedParams.Add((intParam, typeof(int))); + } + + public void StepDefWithDoubleParam(double doubleParam) + { + ExecutedParams.Add((doubleParam, typeof(double))); + } + + } + + public class TestDependencyProvider : DefaultDependencyProvider + { + public override void RegisterGlobalContainerDefaults(ObjectContainer container) + { + base.RegisterGlobalContainerDefaults(container); + var stubUintTestProvider = new Mock(); + container.RegisterInstanceAs(stubUintTestProvider.Object, "nunit"); + } + } + + private async Task PerformStepExecution(string methodName, string expression, string stepText) + { + var containerBuilder = new ContainerBuilder(new TestDependencyProvider()); + var globalContainer = containerBuilder.CreateGlobalContainer(GetType().Assembly); + var testThreadContainer = containerBuilder.CreateTestThreadContainer(globalContainer); + var engine = testThreadContainer.Resolve(); + + var bindingSourceProcessor = globalContainer.Resolve(); + var bindingSourceMethod = new BindingSourceMethod + { + BindingMethod = new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(methodName)), + IsPublic = true, + Attributes = new[] + { + new BindingSourceAttribute + { + AttributeType = new RuntimeBindingType(typeof(GivenAttribute)), + AttributeValues = new IBindingSourceAttributeValueProvider[] + { + new BindingSourceAttributeValueProvider(expression) + } + } + } + }; + bindingSourceProcessor.ProcessType( + new BindingSourceType + { + BindingType = new RuntimeBindingType(typeof(SampleBindings)), + Attributes = new[] + { + new BindingSourceAttribute + { + AttributeType = new RuntimeBindingType(typeof(BindingAttribute)) + } + }, + IsPublic = true, + IsClass = true + }); + bindingSourceProcessor.ProcessMethod(bindingSourceMethod); + bindingSourceProcessor.BuildingCompleted(); + + await engine.OnTestRunStartAsync(); + await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo("en-US"), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); + await engine.OnScenarioStartAsync(); + engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null)); + await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); + + var contextManager = testThreadContainer.Resolve(); + contextManager.ScenarioContext.ScenarioExecutionStatus.Should().Be(ScenarioExecutionStatus.OK, $"should not fail with '{contextManager.ScenarioContext.TestError?.Message}'"); + + return contextManager.ScenarioContext.ScenarioContainer.Resolve(); + } + + [Fact] + public async void Should_match_step_with_simple_cucumber_expression() + { + var expression = "there is something"; + var stepText = "there is something"; + var methodName = nameof(SampleBindings.StepDefWithNoParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.WasStepDefWithNoParamExecuted.Should().BeTrue(); + } + + [Fact] + public async void Should_match_step_with_parameterless_cucumber_expression() + { + var expression = "there is/are something(s) here \\/ now"; + var stepText = "there are something here / now"; + var methodName = nameof(SampleBindings.StepDefWithNoParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.WasStepDefWithNoParamExecuted.Should().BeTrue(); + } + + [Fact] + public async void Should_match_step_with_string_parameter_using_apostrophe() + { + 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); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_string_parameter_using_quotes() + { + 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); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_word_parameter() + { + var expression = "there is a user {word} registered"; + var stepText = "there is a user Marvin registered"; + var expectedParam = ("Marvin", typeof(string)); + var methodName = nameof(SampleBindings.StepDefWithStringParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_int_parameter() + { + var expression = "I have {int} cucumbers in my belly"; + var stepText = "I have 42 cucumbers in my belly"; + var expectedParam = (42, typeof(int)); + var methodName = nameof(SampleBindings.StepDefWithIntParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_float_parameter() + { + var expression = "I have {float} cucumbers in my belly"; + var stepText = "I have 42 cucumbers in my belly"; + var expectedParam = (42.0, typeof(double)); + var methodName = nameof(SampleBindings.StepDefWithDoubleParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_joker_parameter() + { + var expression = "there is a user {} registered"; + var stepText = "there is a user Marvin registered"; + var expectedParam = ("Marvin", typeof(string)); + var methodName = nameof(SampleBindings.StepDefWithStringParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + // build-in types supported by SpecFlow + [Fact] + public async void Should_match_step_with_Int32_parameter() + { + var expression = "I have {Int32} cucumbers in my belly"; + var stepText = "I have 42 cucumbers in my belly"; + var expectedParam = (42, typeof(int)); + var methodName = nameof(SampleBindings.StepDefWithIntParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + //TODO[cukeex]: enum +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs new file mode 100644 index 000000000..a4db9071b --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs @@ -0,0 +1,27 @@ +using System; +using FluentAssertions; +using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.CucumberExpressions; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; + +public class CucumberExpressionParameterTypeRegistryTests +{ + private BindingRegistry _bindingRegistry; + private CucumberExpressionParameterTypeRegistry CreateSut() + { + _bindingRegistry = new BindingRegistry(); + return new CucumberExpressionParameterTypeRegistry(_bindingRegistry); + } + + [Fact] + public void Should_provide_string_type() + { + var sut = CreateSut(); + var paramType = sut.LookupByTypeName("string"); + + paramType.Should().NotBeNull(); + paramType.RegexStrings.Should().HaveCount(1);//TODO[cukeex]: is this really what we want? + } +} \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs new file mode 100644 index 000000000..fade2f896 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.CucumberExpressions; +using TechTalk.SpecFlow.Bindings.Reflection; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; + +public class CucumberExpressionStepDefinitionBindingBuilderTests +{ + private void SampleBindingMethod() + { + //nop + } + private CucumberExpressionStepDefinitionBindingBuilder CreateSut(string sourceExpression, StepDefinitionType stepDefinitionType = StepDefinitionType.Given, IBindingMethod bindingMethod = null, BindingScope bindingScope = null) + { + bindingMethod ??= new RuntimeBindingMethod(GetType().GetMethod(nameof(SampleBindingMethod))); + return new CucumberExpressionStepDefinitionBindingBuilder( + new CucumberExpressionParameterTypeRegistry(new BindingRegistry()), + stepDefinitionType, + bindingMethod, + bindingScope, + sourceExpression); + } + + [Fact] + public void Should_build_from_simple_expression() + { + var sut = CreateSut("simple expression"); + + var result = sut.BuildSingle(); + + result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + result.Regex?.ToString().Should().Be("^simple expression$"); + } + + [Fact] + public void Should_build_from_expression_with_int_param() + { + var sut = CreateSut("I have {int} cucumbers in my belly"); + + var result = sut.BuildSingle(); + + result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + result.Regex?.ToString().Should().Be(@"^I have (-?\d+) cucumbers in my belly$"); + } + + //TODO[cukeex]: figure out what to expect + //[Fact] + //public void Should_build_from_expression_with_string_param() + //{ + // var sut = CreateSut("there is a user {string} registered"); + + // var result = sut.BuildSingle(); + + // result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + // result.Regex?.ToString().Should().Be(@"^there is a user () registered$"); + //} +} \ No newline at end of file From 61f0c7e88b6719716576ab245c0c065312027b6c Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 20 May 2022 15:47:00 +0200 Subject: [PATCH 14/38] enum support --- ...CucumberExpressionParameterTypeRegistry.cs | 27 +++++++++++ ...sionStepDefinitionBindingBuilderFactory.cs | 1 + .../CucumberExpressionIntegrationTests.cs | 48 +++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index f8e059281..fcacbc98c 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -11,6 +11,7 @@ public class CucumberExpressionParameterTypeRegistry : IParameterTypeRegistry { private readonly IBindingRegistry _bindingRegistry; private readonly Lazy> _parameterTypesByName; + private readonly HashSet _consideredParameterTypes = new(); public CucumberExpressionParameterTypeRegistry(IBindingRegistry bindingRegistry) { @@ -65,6 +66,8 @@ private Dictionary InitializeR new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, guidBindingType), }; + var enumTypes = GetEnumTypesUsedInParameters(); + //TODO[cukeex]: support strings without custom converters var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); @@ -73,6 +76,7 @@ private Dictionary InitializeR var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); var parameterTypes = builtInTransformations.Cast() + .Concat(enumTypes) .Concat(userTransformations) .GroupBy(t => new Tuple(t.TargetType, t.Name)) .Select(g => new CucumberExpressionParameterType(g.Key.Item2 ?? g.Key.Item1.Name, g.Key.Item1, g)) @@ -83,6 +87,19 @@ private Dictionary InitializeR return parameterTypes; } + private IEnumerable GetEnumTypesUsedInParameters() + { + var enumParameterTypes = _consideredParameterTypes + .OfType() + .Where(t => t.Type.IsEnum); + foreach (var enumParameterType in enumParameterTypes) + { + yield return new BuiltInCucumberExpressionParameterTypeTransformation( + CucumberExpressionParameterType.MatchAllRegex, + enumParameterType); + } + } + [Conditional("DEBUG")] private static void DumpParameterTypes(Dictionary parameterTypes) { @@ -104,4 +121,14 @@ public IEnumerable GetParameterTypes() { return _parameterTypesByName.Value.Values; } + + public void OnBindingMethodProcessed(IBindingMethod bindingMethod) + { + var parameterTypes = bindingMethod.Parameters + .Select(p => p.Type); + foreach (var parameterType in parameterTypes) + { + _consideredParameterTypes.Add(parameterType); + } + } } diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs index 89af37cd6..da8ff3d3e 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderFactory.cs @@ -20,6 +20,7 @@ public CucumberExpressionStepDefinitionBindingBuilderFactory(CucumberExpressionP public CucumberExpressionStepDefinitionBindingBuilder Create(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope, string sourceExpression) { + _cucumberExpressionParameterTypeRegistry.OnBindingMethodProcessed(bindingMethod); return new CucumberExpressionStepDefinitionBindingBuilder(_cucumberExpressionParameterTypeRegistry, stepDefinitionType, bindingMethod, bindingScope, sourceExpression); } } \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 3e7ac6c15..3d58e2c8d 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -14,6 +14,12 @@ namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; +public enum SampleColorEnum +{ + Yellow, + Brown +} + public class CucumberExpressionIntegrationTests { public class SampleBindings @@ -40,7 +46,14 @@ public void StepDefWithDoubleParam(double doubleParam) { ExecutedParams.Add((doubleParam, typeof(double))); } - + public void StepDefWithDecimalParam(decimal decimalParam) + { + ExecutedParams.Add((decimalParam, typeof(decimal))); + } + public void StepDefWithEnumParam(SampleColorEnum enumParam) + { + ExecutedParams.Add((enumParam, typeof(SampleColorEnum))); + } } public class TestDependencyProvider : DefaultDependencyProvider @@ -183,11 +196,11 @@ public async void Should_match_step_with_int_parameter() } [Fact] - public async void Should_match_step_with_float_parameter() + public async void Should_match_step_with_float_parameter_to_double() { var expression = "I have {float} cucumbers in my belly"; - var stepText = "I have 42 cucumbers in my belly"; - var expectedParam = (42.0, typeof(double)); + var stepText = "I have 42.1 cucumbers in my belly"; + var expectedParam = (42.1, typeof(double)); var methodName = nameof(SampleBindings.StepDefWithDoubleParam); var sampleBindings = await PerformStepExecution(methodName, expression, stepText); @@ -195,6 +208,19 @@ public async void Should_match_step_with_float_parameter() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + [Fact] + public async void Should_match_step_with_float_parameter_to_decimal() + { + var expression = "I have {float} cucumbers in my belly"; + var stepText = "I have 42.1 cucumbers in my belly"; + var expectedParam = (42.1m, typeof(decimal)); + var methodName = nameof(SampleBindings.StepDefWithDecimalParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + [Fact] public async void Should_match_step_with_joker_parameter() { @@ -222,5 +248,17 @@ public async void Should_match_step_with_Int32_parameter() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } - //TODO[cukeex]: enum + // enum support + [Fact] + public async void Should_match_step_with_enum_parameter() + { + var expression = "I have {SampleColorEnum} cucumbers in my belly"; + var stepText = "I have Yellow cucumbers in my belly"; + var expectedParam = (SampleColorEnum.Yellow, typeof(SampleColorEnum)); + var methodName = nameof(SampleBindings.StepDefWithEnumParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } } From 5f475ab737274729643f01b5fb7822c9912b8be8 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 20 May 2022 16:04:28 +0200 Subject: [PATCH 15/38] tests for custom type --- .../CucumberExpressionIntegrationTests.cs | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 3d58e2c8d..aaa288981 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Threading.Tasks; using BoDi; using FluentAssertions; @@ -20,6 +21,42 @@ public enum SampleColorEnum Brown } +public class SampleUser +{ + public string UserName { get; } + + public SampleUser(string userName) + { + UserName = userName; + } + + public static SampleUser Create(string userName) => new(userName); + + protected bool Equals(SampleUser other) => UserName == other.UserName; + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((SampleUser)obj); + } + + public override int GetHashCode() => UserName.GetHashCode(); +} + public class CucumberExpressionIntegrationTests { public class SampleBindings @@ -54,6 +91,10 @@ public void StepDefWithEnumParam(SampleColorEnum enumParam) { ExecutedParams.Add((enumParam, typeof(SampleColorEnum))); } + public void StepDefWithCustomClassParam(SampleUser userParam) + { + ExecutedParams.Add((userParam, typeof(SampleUser))); + } } public class TestDependencyProvider : DefaultDependencyProvider @@ -66,7 +107,7 @@ public override void RegisterGlobalContainerDefaults(ObjectContainer container) } } - private async Task PerformStepExecution(string methodName, string expression, string stepText) + private async Task PerformStepExecution(string methodName, string expression, string stepText, IStepArgumentTransformationBinding[] transformations = null, Action onBindingRegistryPreparation = null) { var containerBuilder = new ContainerBuilder(new TestDependencyProvider()); var globalContainer = containerBuilder.CreateGlobalContainer(GetType().Assembly); @@ -74,6 +115,11 @@ private async Task PerformStepExecution(string methodName, strin var engine = testThreadContainer.Resolve(); var bindingSourceProcessor = globalContainer.Resolve(); + + var bindingRegistry = globalContainer.Resolve(); + transformations?.ToList().ForEach(binding => bindingRegistry.RegisterStepArgumentTransformationBinding(binding)); + onBindingRegistryPreparation?.Invoke(bindingRegistry); + var bindingSourceMethod = new BindingSourceMethod { BindingMethod = new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(methodName)), @@ -261,4 +307,23 @@ public async void Should_match_step_with_enum_parameter() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + + // custom type conversion support + + [Fact] + public async void Should_match_step_with_custom_parameter_with_type_name() + { + var expression = "there is a {SampleUser} registered"; + var stepText = "there is a user Marvin registered"; + var expectedParam = (new SampleUser("Marvin"), typeof(SampleUser)); + var methodName = nameof(SampleBindings.StepDefWithCustomClassParam); + IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding( + "user ([A-Z][a-z]+)", + new RuntimeBindingMethod(typeof(SampleUser).GetMethod(nameof(SampleUser.Create)))); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformation}); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + } From 3e822a2550094252316ad26cde885a1444392d65 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 20 May 2022 16:28:53 +0200 Subject: [PATCH 16/38] support for custom parameter names for StepArgumentTransformation --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 4 ++-- .../CucumberExpressionParameterTypeBinding.cs | 13 ------------- .../CucumberExpressionParameterTypeRegistry.cs | 4 ++-- ...berExpressionParameterTypeTransformation.cs | 8 +------- .../Discovery/BindingSourceProcessor.cs | 5 +++-- TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 2 +- .../IStepArgumentTransformationBinding.cs | 11 +++++++++++ .../StepArgumentTransformationBinding.cs | 11 +++++++---- .../StepArgumentTransformationAttribute.cs | 10 ++++++++++ .../CucumberExpressionIntegrationTests.cs | 18 ++++++++++++++++++ 10 files changed, 55 insertions(+), 31 deletions(-) delete mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index 7a6176719..9d546534f 100644 --- a/TechTalk.SpecFlow/Bindings/BindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -30,8 +30,8 @@ public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefi } public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, - IBindingMethod bindingMethod) + IBindingMethod bindingMethod, string parameterTypeName = null) { - return new StepArgumentTransformationBinding(regexString, bindingMethod); + return new StepArgumentTransformationBinding(regexString, bindingMethod, parameterTypeName); } } diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs deleted file mode 100644 index e13dcc7d4..000000000 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeBinding.cs +++ /dev/null @@ -1,13 +0,0 @@ -using TechTalk.SpecFlow.Bindings.Reflection; - -namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; - -public class CucumberExpressionParameterTypeBinding : StepArgumentTransformationBinding -{ - public string Name { get; } - - public CucumberExpressionParameterTypeBinding(string regexString, IBindingMethod bindingMethod, string name) : base(regexString, bindingMethod) - { - Name = name; - } -} \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index fcacbc98c..8f18becfc 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -70,8 +70,8 @@ private Dictionary InitializeR //TODO[cukeex]: support strings without custom converters var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); - _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); - _bindingRegistry.RegisterStepArgumentTransformationBinding(new CucumberExpressionParameterTypeBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + _bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + _bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs index ad1444f1c..a6ff20eb7 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs @@ -8,7 +8,7 @@ public class UserDefinedCucumberExpressionParameterTypeTransformation : ICucumbe { private readonly IStepArgumentTransformationBinding _stepArgumentTransformationBinding; - public string Name => GetProvidedTransformationTypeName(_stepArgumentTransformationBinding); + public string Name => _stepArgumentTransformationBinding.Name; public string Regex { get; } public IBindingType TargetType => _stepArgumentTransformationBinding.Method.ReturnType; public bool UseForSnippets => true; @@ -20,12 +20,6 @@ public UserDefinedCucumberExpressionParameterTypeTransformation(IStepArgumentTra Regex = GetCucumberExpressionRegex(stepArgumentTransformationBinding); } - private static string GetProvidedTransformationTypeName(IStepArgumentTransformationBinding transformationBinding) - { - // later IStepArgumentTransformationBinding could capture name - return transformationBinding is CucumberExpressionParameterTypeBinding parameterTypeBinding ? parameterTypeBinding.Name : null; - } - private string GetCucumberExpressionRegex(IStepArgumentTransformationBinding stepArgumentTransformationBinding) { var regexString = stepArgumentTransformationBinding.Regex?.ToString().TrimStart('^').TrimEnd('$'); diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index e8065a9dc..3dfb33ae9 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -189,12 +189,13 @@ private int GetHookOrder(BindingSourceAttribute hookAttribute) private void ProcessStepArgumentTransformationAttribute(BindingSourceMethod bindingSourceMethod, BindingSourceAttribute stepArgumentTransformationAttribute) { - string regex = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue("Regex"); + string regex = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Regex)); + string name = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Name)); if (!ValidateStepArgumentTransformation(bindingSourceMethod, stepArgumentTransformationAttribute)) return; - var stepArgumentTransformationBinding = bindingFactory.CreateStepArgumentTransformation(regex, bindingSourceMethod.BindingMethod); + var stepArgumentTransformationBinding = bindingFactory.CreateStepArgumentTransformation(regex, bindingSourceMethod.BindingMethod, name); ProcessStepArgumentTransformationBinding(stepArgumentTransformationBinding); } diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index ce0c4c388..fc5919b9b 100644 --- a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs @@ -11,6 +11,6 @@ IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionT BindingScope bindingScope, string expressionString); IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, - IBindingMethod bindingMethod); + IBindingMethod bindingMethod, string parameterTypeName = null); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs index 9c4d7f3e6..87424e706 100644 --- a/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs @@ -2,8 +2,19 @@ namespace TechTalk.SpecFlow.Bindings { + /// + /// Represents a custom step definition parameter binding + /// public interface IStepArgumentTransformationBinding : IBinding { + /// + /// The optional name of the custom parameter. The name can be used in cucumber expressions. + /// + string Name { get; } + + /// + /// The regular expression matches the step argument. Optional, if null, the transformation receives the entire argument. + /// Regex Regex { get; } } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs index 6081b314c..688e980b4 100644 --- a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs @@ -5,16 +5,19 @@ namespace TechTalk.SpecFlow.Bindings { public class StepArgumentTransformationBinding : MethodBinding, IStepArgumentTransformationBinding { - public Regex Regex { get; private set; } + public string Name { get; } - public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod) + public Regex Regex { get; } + + public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod, string name = null) : base(bindingMethod) { Regex = regex; + Name = name; } - public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod) - : this(RegexFactory.Create(regexString), bindingMethod) + public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null) + : this(RegexFactory.Create(regexString), bindingMethod, name) { } } diff --git a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs index 8226d5b33..08c946d3c 100644 --- a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs +++ b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs @@ -2,10 +2,20 @@ namespace TechTalk.SpecFlow { + /// + /// Specifies the method to be used as a custom step definition parameter conversion. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StepArgumentTransformationAttribute : Attribute { + /// + /// The regular expression that have to match the step argument. The entire argument is passed to the method if omitted. + /// public string Regex { get; set; } + /// + /// The custom parameter type name to be used in Cucumber Expressions + /// + public string Name { get; set; } public StepArgumentTransformationAttribute(string regex) { diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index aaa288981..cea9ab116 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -326,4 +326,22 @@ public async void Should_match_step_with_custom_parameter_with_type_name() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + + [Fact] + public async void Should_match_step_with_custom_parameter_with_custom_name() + { + var expression = "there is a {user} registered"; + var stepText = "there is a user Marvin registered"; + var expectedParam = (new SampleUser("Marvin"), typeof(SampleUser)); + var methodName = nameof(SampleBindings.StepDefWithCustomClassParam); + IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding( + "user ([A-Z][a-z]+)", + new RuntimeBindingMethod(typeof(SampleUser).GetMethod(nameof(SampleUser.Create))), + "user"); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformation}); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + } From de90ff38f67c418a17af977a5b9695efdf1426ee Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 20 May 2022 16:49:59 +0200 Subject: [PATCH 17/38] try cleaning up string handling (not working) --- ...CucumberExpressionParameterTypeRegistry.cs | 10 +++++++--- ...rExpressionStepDefinitionBindingBuilder.cs | 2 +- .../SpecFlowCucumberExpression.cs | 19 +++++++++++++++++++ ...essionStepDefinitionBindingBuilderTests.cs | 16 ++++++++-------- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index 8f18becfc..028b2c8cd 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -52,6 +52,10 @@ private Dictionary InitializeR //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, "word", useForSnippets: false), + //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.StringParameterRegexDoubleQuote, doubleBindingType, ParameterTypeConstants.StringParameterName), + //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.StringParameterRegexApostrophe, doubleBindingType, ParameterTypeConstants.StringParameterName), + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType, ParameterTypeConstants.StringParameterName), + // other types supported by SpecFlow by default: Make them accessible with type name (e.g. Int32) new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, boolBindingType), new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, byteBindingType), @@ -69,9 +73,9 @@ private Dictionary InitializeR var enumTypes = GetEnumTypesUsedInParameters(); //TODO[cukeex]: support strings without custom converters - var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); - _bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); - _bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + //var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); + //_bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); + //_bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs index f2e11bdf9..308c892af 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs @@ -44,6 +44,6 @@ public static bool IsCucumberExpression(string cucumberExpressionCandidate) protected override IExpression CreateExpression(out string expressionType) { expressionType = StepDefinitionExpressionTypes.CucumberExpression; - return new CucumberExpression(_sourceExpression, _cucumberExpressionParameterTypeRegistry); + return new SpecFlowCucumberExpression(_sourceExpression, _cucumberExpressionParameterTypeRegistry); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs new file mode 100644 index 000000000..47b5c8cc8 --- /dev/null +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs @@ -0,0 +1,19 @@ +using System; +using CucumberExpressions; + +namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; + +public class SpecFlowCucumberExpression : CucumberExpression +{ + public SpecFlowCucumberExpression(string expression, IParameterTypeRegistry parameterTypeRegistry) : base(expression, parameterTypeRegistry) + { + } + + protected override bool HandleStringType(string name, IParameterType parameterType, out string[] regexps, out bool shouldWrapWithCaptureGroup) + { + regexps = ParameterTypeConstants.StringParameterRegexps; + //regexps = new[] { $"(?:{ParameterTypeConstants.StringParameterRegexDoubleQuote}|{ParameterTypeConstants.StringParameterRegexApostrophe})" }; + shouldWrapWithCaptureGroup = false; + return true; + } +} \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs index fade2f896..9d6f305cd 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs @@ -46,14 +46,14 @@ public void Should_build_from_expression_with_int_param() } //TODO[cukeex]: figure out what to expect - //[Fact] - //public void Should_build_from_expression_with_string_param() - //{ - // var sut = CreateSut("there is a user {string} registered"); + [Fact] + public void Should_build_from_expression_with_string_param() + { + var sut = CreateSut("there is a user {string} registered"); - // var result = sut.BuildSingle(); + var result = sut.BuildSingle(); - // result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); - // result.Regex?.ToString().Should().Be(@"^there is a user () registered$"); - //} + result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + result.Regex?.ToString().Should().Be(@"^there is a user () registered$"); + } } \ No newline at end of file From 26aac4c0409c49102cf0be1f2474a98ae91f4a3d Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 23 May 2022 14:43:29 +0200 Subject: [PATCH 18/38] fix string handling? --- .../CucumberExpressionParameterTypeRegistry.cs | 8 +------- .../CucumberExpressions/SpecFlowCucumberExpression.cs | 1 - .../Infrastructure/StepDefinitionMatchService.cs | 2 +- ...CucumberExpressionStepDefinitionBindingBuilderTests.cs | 2 +- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index 028b2c8cd..d07824448 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -52,8 +52,7 @@ private Dictionary InitializeR //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, "word", useForSnippets: false), - //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.StringParameterRegexDoubleQuote, doubleBindingType, ParameterTypeConstants.StringParameterName), - //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.StringParameterRegexApostrophe, doubleBindingType, ParameterTypeConstants.StringParameterName), + //the regex specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType, ParameterTypeConstants.StringParameterName), // other types supported by SpecFlow by default: Make them accessible with type name (e.g. Int32) @@ -72,11 +71,6 @@ private Dictionary InitializeR var enumTypes = GetEnumTypesUsedInParameters(); - //TODO[cukeex]: support strings without custom converters - //var convertQuotedStringMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(ConvertQuotedString))); - //_bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexDoubleQuote, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); - //_bindingRegistry.RegisterStepArgumentTransformationBinding(new StepArgumentTransformationBinding(ParameterTypeConstants.StringParameterRegexApostrophe, convertQuotedStringMethod, ParameterTypeConstants.StringParameterName)); - var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); var parameterTypes = builtInTransformations.Cast() diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs index 47b5c8cc8..35e59d3c8 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs @@ -12,7 +12,6 @@ public SpecFlowCucumberExpression(string expression, IParameterTypeRegistry para protected override bool HandleStringType(string name, IParameterType parameterType, out string[] regexps, out bool shouldWrapWithCaptureGroup) { regexps = ParameterTypeConstants.StringParameterRegexps; - //regexps = new[] { $"(?:{ParameterTypeConstants.StringParameterRegexDoubleQuote}|{ParameterTypeConstants.StringParameterRegexApostrophe})" }; shouldWrapWithCaptureGroup = false; return true; } diff --git a/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs b/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs index e1f58dd1e..35ebf4e9f 100644 --- a/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs +++ b/TechTalk.SpecFlow/Infrastructure/StepDefinitionMatchService.cs @@ -40,7 +40,7 @@ public StepDefinitionMatchService(IBindingRegistry bindingRegistry, IStepArgumen private object[] CalculateArguments(Match match, StepInstance stepInstance) { - var regexArgs = match.Groups.Cast().Skip(1).Select(g => g.Value); + var regexArgs = match.Groups.Cast().Skip(1).Where(g => g.Success).Select(g => g.Value); var arguments = regexArgs.Cast().ToList(); if (stepInstance.MultilineTextArgument != null) arguments.Add(stepInstance.MultilineTextArgument); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs index 9d6f305cd..4e3e7a601 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs @@ -54,6 +54,6 @@ public void Should_build_from_expression_with_string_param() var result = sut.BuildSingle(); result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); - result.Regex?.ToString().Should().Be(@"^there is a user () registered$"); + result.Regex?.ToString().Should().Be(@"^there is a user (?:(?:""([^""\\]*(?:\\.[^""\\]*)*)"")|(?:'([^'\\]*(?:\\.[^'\\]*)*)')) registered$"); } } \ No newline at end of file From b86ca05db4d70fa8cf5a13d01c8679a2f68b1ffe Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 23 May 2022 16:49:57 +0200 Subject: [PATCH 19/38] Add scenarios, fix [StepArgTrafo] --- ...CucumberExpressionParameterTypeRegistry.cs | 6 +- .../Discovery/BindingSourceProcessor.cs | 2 +- .../Execution/CucumberExpressions.feature | 111 +++++++++++++++++- 3 files changed, 110 insertions(+), 9 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index d07824448..33d8410d5 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -73,11 +73,11 @@ private Dictionary InitializeR var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); - var parameterTypes = builtInTransformations.Cast() + var parameterTypes = builtInTransformations .Concat(enumTypes) .Concat(userTransformations) - .GroupBy(t => new Tuple(t.TargetType, t.Name)) - .Select(g => new CucumberExpressionParameterType(g.Key.Item2 ?? g.Key.Item1.Name, g.Key.Item1, g)) + .GroupBy(t => (t.TargetType, t.Name)) + .Select(g => new CucumberExpressionParameterType(g.Key.Name ?? g.Key.TargetType.Name, g.Key.TargetType, g)) .ToDictionary(pt => pt.Name, pt => (ISpecFlowCucumberExpressionParameterType)pt); DumpParameterTypes(parameterTypes); diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index 3dfb33ae9..94635d388 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -190,7 +190,7 @@ private int GetHookOrder(BindingSourceAttribute hookAttribute) private void ProcessStepArgumentTransformationAttribute(BindingSourceMethod bindingSourceMethod, BindingSourceAttribute stepArgumentTransformationAttribute) { string regex = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Regex)); - string name = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Name)); + string name = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Name)); if (!ValidateStepArgumentTransformation(bindingSourceMethod, stepArgumentTransformationAttribute)) return; diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature index fe60e8630..a7603bc51 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -3,23 +3,124 @@ Feature: Cucumber expressions Rule: Shoud support Cucumber expressions -Scenario: Simple Cucumber expresions are used for Step Definitions +Scenario: Cucumber expresions are used for Step Definitions Given a scenario 'Simple Scenario' as """ When I have 42 cucumbers in my belly + And there is a user 'Marvin' registered """ - And the following step definition + And the following step definitions """ - [When(@"I have {int} cucumbers in my belly")] + [When("I have {int} cucumber(s) in my belly/tummy")] public void WhenIHaveCucumbersInMyBelly(int count) { - global::Log.LogStep(); + if (count == 42) + global::Log.LogStep(); + } + + [When("there is a user {string} registered")] + public void ThereIsAUserRegistered(string userName) + { + if (userName == "Marvin") + global::Log.LogStep(); } """ When I execute the tests Then the binding method 'WhenIHaveCucumbersInMyBelly' is executed + And the binding method 'ThereIsAUserRegistered' is executed Rule: Regular expressions and Cucumber expressions can be mixed -Rule: Custom parameter types can be used in Cucumber expressions \ No newline at end of file +The expression is considered to be a regular expression if any of the statements is true + +* Starts with `^` +* Ends with `$` +* Contains `.*` +* Contains `(*)` +* Contains `(+)` +* Contains `\d+` +* Contains `\.` + +Except for `^` and `$` all other conditions are ignored if the expression contains a cucumber expression parameter placeholder (`{}`). + + +Scenario: Cucumber expresions and Regular expressions are mixed in the same project + Given a scenario 'Simple Scenario' as + """ + When I have 42 cucumbers in my belly + And there is a user 'Marvin' registered + """ + And the following step definitions + """ + [When("I have {int} cucumbers in my belly")] + public void WhenIHaveCucumbersInMyBelly(int count) + { + if (count == 42) + global::Log.LogStep(); + } + + [When(@"there is a user '(.*)' registered")] + public void ThereIsAUserRegistered(string userName) + { + if (userName == "Marvin") + global::Log.LogStep(); + } + """ + When I execute the tests + Then the binding method 'WhenIHaveCucumbersInMyBelly' is executed + And the binding method 'ThereIsAUserRegistered' is executed + + + +Rule: Custom parameter types can be used in Cucumber expressions + +Scenario: Custom parameter types definied with [StepArgumentTransformation] can be used with type name + Given a scenario 'Simple Scenario' as + """ + When I download the release v1.2.3 of the application + """ + And the following step argument transformation + """ + [StepArgumentTransformation("v(.*)")] + public Version ConvertVersion(string versionString) + { + return new Version(versionString); + } + """ + And the following step definition + """ + [When("I download the release {Version} of the application")] + public void WhenIDownloadTheReleaseOfTheApplication(Version version) + { + if (version.ToString() == "1.2.3") + global::Log.LogStep(); + } + """ + When I execute the tests + Then the binding method 'WhenIDownloadTheReleaseOfTheApplication' is executed + +Scenario: Custom parameter types definied with [StepArgumentTransformation] can be used with specified name + Given a scenario 'Simple Scenario' as + """ + When I download the release v1.2.3 of the application + """ + And the following step argument transformation + """ + [StepArgumentTransformation("v(.*)", Name = "my_version")] + public Version ConvertVersion(string versionString) + { + return new Version(versionString); + } + """ + And the following step definition + """ + [When("I download the release {my_version} of the application")] + public void WhenIDownloadTheReleaseOfTheApplication(Version version) + { + if (version.ToString() == "1.2.3") + global::Log.LogStep(); + } + """ + When I execute the tests + Then the binding method 'WhenIDownloadTheReleaseOfTheApplication' is executed From dd8722937404fa0618d1bdd4963d415a4dae057d Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 23 May 2022 17:00:11 +0200 Subject: [PATCH 20/38] fix unit test --- .../Discovery/BindingSourceProcessorTests.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs index 53404ae18..2397d8dea 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using TechTalk.SpecFlow.Bindings; @@ -20,7 +21,7 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() //ARRANGE var sut = CreateBindingSourceProcessor(); - BindingSourceType bindingSourceType = new BindingSourceType + var bindingSourceType = new BindingSourceType { Attributes = new[] { @@ -28,11 +29,14 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() }, }; - BindingSourceMethod bindingSourceMethod = new BindingSourceMethod(); - bindingSourceMethod.Attributes = new[] + var bindingSourceMethod = new BindingSourceMethod { - CreateBindingSourceAttribute("GivenAttribute", "TechTalk.SpecFlow.GivenAttribute") - .WithValue("an authenticated user"), + BindingMethod = CreateBindingMethodStub(), + Attributes = new[] + { + CreateBindingSourceAttribute("GivenAttribute", "TechTalk.SpecFlow.GivenAttribute") + .WithValue("an authenticated user"), + } }; //ACT @@ -47,10 +51,10 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() binding.Regex.IsMatch("an authenticated user").Should().BeTrue(); } - private static BindingSourceAttribute CreateBindingSourceAttribute(string name, string fullName) => new BindingSourceAttribute + private static BindingSourceAttribute CreateBindingSourceAttribute(string name, string fullName) => new() { AttributeType = new BindingType(name, fullName, "default"), - AttributeValues = new IBindingSourceAttributeValueProvider[0], + AttributeValues = Array.Empty(), NamedAttributeValues = new Dictionary() }; @@ -59,6 +63,12 @@ private BindingSourceProcessorStub CreateBindingSourceProcessor() //NOTE: BindingSourceProcessor is abstract, to test its base functionality we need to instantiate a subclass return new BindingSourceProcessorStub(); } + + private BindingMethod CreateBindingMethodStub() + { + var bindingType = new BindingType("MyType", "MyProject.MyType", "MyProject"); + return new BindingMethod(bindingType, "MyMethod", Array.Empty(), RuntimeBindingType.Void); + } } public static class BindingSourceTestExtensions From 8f6b41515d42ca9cc863ba4f2441a0f5be785b75 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 23 May 2022 17:15:19 +0200 Subject: [PATCH 21/38] refactor RegexFactory --- TechTalk.SpecFlow/Bindings/RegexFactory.cs | 21 ++++++++++++++++--- .../RegexStepDefinitionBindingBuilder.cs | 7 ++----- .../StepArgumentTransformationBinding.cs | 9 +++++++- .../StepDefinitionRegexCalculatorTests.cs | 2 +- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/RegexFactory.cs b/TechTalk.SpecFlow/Bindings/RegexFactory.cs index 557f93641..a925eb593 100644 --- a/TechTalk.SpecFlow/Bindings/RegexFactory.cs +++ b/TechTalk.SpecFlow/Bindings/RegexFactory.cs @@ -1,15 +1,30 @@ +using System; using System.Text.RegularExpressions; namespace TechTalk.SpecFlow.Bindings { - //TODO[cukeex]: remove internal static class RegexFactory { private static RegexOptions RegexOptions = RegexOptions.CultureInvariant; - public static Regex Create(string regexString) + public static Regex CreateWholeTextRegexForBindings(string regexString) => CreateRegexForBindings(GetWholeTextMatchRegexSource(regexString)); + + public static string GetWholeTextMatchRegexSource(string regexString) + { + if (regexString == null) + throw new ArgumentNullException(nameof(regexString)); + + if (!regexString.StartsWith("^")) regexString = "^" + regexString; + if (!regexString.EndsWith("$")) regexString += "$"; + return regexString; + } + + public static Regex CreateRegexForBindings(string regexString) { - return regexString == null ? null : new Regex("^" + regexString + "$", RegexOptions); + if (regexString == null) + throw new ArgumentNullException(nameof(regexString)); + + return new Regex(regexString, RegexOptions); } } } diff --git a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs index 3dbd6d0fe..20afe3e2c 100644 --- a/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/RegexStepDefinitionBindingBuilder.cs @@ -14,7 +14,7 @@ public RegexStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, protected override IExpression CreateExpression(out string expressionType) { var regexSource = GetRegexSource(out expressionType); - var regex = new Regex(regexSource, RegexOptions.CultureInvariant); + var regex = RegexFactory.CreateRegexForBindings(regexSource); var expression = new RegularExpression(regex); return expression; } @@ -22,9 +22,6 @@ protected override IExpression CreateExpression(out string expressionType) protected virtual string GetRegexSource(out string expressionType) { expressionType = StepDefinitionExpressionTypes.RegularExpression; - var regex = _sourceExpression; - if (!regex.StartsWith("^")) regex = "^" + regex; - if (!regex.EndsWith("$")) regex += "$"; - return regex; + return RegexFactory.GetWholeTextMatchRegexSource(_sourceExpression); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs index 688e980b4..cbba326bf 100644 --- a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs @@ -17,8 +17,15 @@ public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMeth } public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null) - : this(RegexFactory.Create(regexString), bindingMethod, name) + : this(CreateRegexOrNull(regexString), bindingMethod, name) { } + + private static Regex CreateRegexOrNull(string regexString) + { + if (regexString == null) + return null; + return RegexFactory.CreateWholeTextRegexForBindings(regexString); + } } } \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/StepDefinitionRegexCalculatorTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/StepDefinitionRegexCalculatorTests.cs index 4d1e51529..4ed2f3cd4 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/StepDefinitionRegexCalculatorTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/StepDefinitionRegexCalculatorTests.cs @@ -41,7 +41,7 @@ private IBindingMethod CreateBindingMethod(string name, params string[] paramete private Regex AssertRegex(string regexText) { regexText.Should().NotBeNullOrWhiteSpace("null, empty or whitespace-only regex is not valid for step definitions"); - return RegexFactory.Create(regexText); // uses the same regex creation as real step definitions + return RegexFactory.CreateWholeTextRegexForBindings(regexText); // uses the same regex creation as real step definitions } private Regex CallCalculateRegexFromMethodAndAssertRegex(StepDefinitionRegexCalculator sut, StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod) From a7f0eaafbd8453ffd246038750f9c8fc899489eb Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Tue, 24 May 2022 11:17:24 +0200 Subject: [PATCH 22/38] provide cucumber expression step definition skeletons --- .../DefaultSkeletonTemplates.sftemplate | 14 ++--- .../FileBasedSkeletonTemplateProvider.cs | 4 +- .../ISkeletonTemplateProvider.cs | 2 +- .../StepDefinitionSkeletonProvider.cs | 43 +++++++++++++-- .../StepDefinitionSkeletonStyle.cs | 4 +- .../BindingSkeletons/StepTextAnalyzer.cs | 35 ++++++------ .../Configuration/ConfigDefaults.cs | 2 +- .../FileBasedSkeletonTemplateProviderTests.cs | 4 +- .../StepDefinitionSkeletonProviderTests.cs | 54 +++++++++++++------ .../BindingSkeletons/StepTextAnalyzerTests.cs | 48 ++++++++++------- 10 files changed, 143 insertions(+), 67 deletions(-) diff --git a/TechTalk.SpecFlow/BindingSkeletons/DefaultSkeletonTemplates.sftemplate b/TechTalk.SpecFlow/BindingSkeletons/DefaultSkeletonTemplates.sftemplate index 8cd5a10e0..ceab6c8bb 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/DefaultSkeletonTemplates.sftemplate +++ b/TechTalk.SpecFlow/BindingSkeletons/DefaultSkeletonTemplates.sftemplate @@ -16,8 +16,8 @@ namespace {namespace} {bindings} } } ->>>CSharp/StepDefinitionRegex -[{attribute}(@"{regex}")] +>>>CSharp/StepDefinitionExpression +[{attribute}({expression})] public void {methodName}({parameters}) { _scenarioContext.Pending(); @@ -26,7 +26,7 @@ public void {methodName}({parameters}) [{attribute}] public void {methodName}({parameters}) { - _scenarioContext .Pending(); + _scenarioContext.Pending(); } >>>VB/StepDefinitionClass Imports System @@ -42,8 +42,8 @@ Namespace {namespace} End Class End Namespace ->>>VB/StepDefinitionRegex - _ +>>>VB/StepDefinitionExpression + _ Public Sub {methodName}({parameters}) ScenarioContext.Current.Pending() End Sub @@ -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}) = () diff --git a/TechTalk.SpecFlow/BindingSkeletons/FileBasedSkeletonTemplateProvider.cs b/TechTalk.SpecFlow/BindingSkeletons/FileBasedSkeletonTemplateProvider.cs index ee2aef74a..115b8039e 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/FileBasedSkeletonTemplateProvider.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/FileBasedSkeletonTemplateProvider.cs @@ -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); diff --git a/TechTalk.SpecFlow/BindingSkeletons/ISkeletonTemplateProvider.cs b/TechTalk.SpecFlow/BindingSkeletons/ISkeletonTemplateProvider.cs index 8e8d5d6d3..fa7714d0d 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/ISkeletonTemplateProvider.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/ISkeletonTemplateProvider.cs @@ -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); } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs index aa2230553..d741e7793 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs @@ -46,19 +46,52 @@ private static IEnumerable 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(); + + throw new NotImplementedException(); + } + private AnalyzedStepText Analyze(StepInstance stepInstance, CultureInfo bindingCulture) { var result = stepTextAnalyzer.Analyze(stepInstance.Text, bindingCulture); @@ -81,6 +114,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}"); @@ -137,7 +171,8 @@ private string GetRegex(AnalyzedStepText stepText) 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])); } diff --git a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonStyle.cs b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonStyle.cs index 0cf5ea621..50423570e 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonStyle.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonStyle.cs @@ -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, } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs b/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs index 03659bed3..066e1565b 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs @@ -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; } } @@ -59,47 +63,48 @@ public AnalyzedStepText Analyze(string stepText, CultureInfo bindingCulture) 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; + regexPattern = doubleQuoteRegexPattern; //TODO[cukeex]: this code was not working and provided .* regex, shall we keep that? 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(@"""+(?.*?)""+|'+(?.*?)'+|(?\<.*?\>)"); + private static readonly Regex quotesRe = new Regex(@"(?"".*?"")|(?'.*?')|(?\<.*?\>)"); private IEnumerable RecognizeQuotedTexts(string stepText) { return quotesRe.Matches(stepText) diff --git a/TechTalk.SpecFlow/Configuration/ConfigDefaults.cs b/TechTalk.SpecFlow/Configuration/ConfigDefaults.cs index 547b1f826..eeb1a9f7f 100644 --- a/TechTalk.SpecFlow/Configuration/ConfigDefaults.cs +++ b/TechTalk.SpecFlow/Configuration/ConfigDefaults.cs @@ -18,7 +18,7 @@ public static class ConfigDefaults public const bool TraceSuccessfulSteps = true; public const bool TraceTimings = false; public const string MinTracedDuration = "0:0:0.1"; - public const StepDefinitionSkeletonStyle StepDefinitionSkeletonStyle = TechTalk.SpecFlow.BindingSkeletons.StepDefinitionSkeletonStyle.RegexAttribute; + public const StepDefinitionSkeletonStyle StepDefinitionSkeletonStyle = TechTalk.SpecFlow.BindingSkeletons.StepDefinitionSkeletonStyle.CucumberExpressionAttribute; public const ObsoleteBehavior ObsoleteBehavior = Configuration.ObsoleteBehavior.Warn; public const bool AllowDebugGeneratedFiles = false; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/FileBasedSkeletonTemplateProviderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/FileBasedSkeletonTemplateProviderTests.cs index 17dfee9c1..3d47fe15b 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/FileBasedSkeletonTemplateProviderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/FileBasedSkeletonTemplateProviderTests.cs @@ -46,9 +46,9 @@ public void Should_parse_step_definition_class_template() } [Fact] - public void Should_parse_step_definition_regex_template() + public void Should_parse_step_definition_expression_template() { - var sut = new FileBasedSkeletonTemplateProviderStub(@">>>CSharp/StepDefinitionRegex + var sut = new FileBasedSkeletonTemplateProviderStub(@">>>CSharp/StepDefinitionExpression mytemplate >>>other"); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs index 0ee58f326..0bf0f2441 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs @@ -23,7 +23,7 @@ public StepDefinitionSkeletonProviderTests() templateProviderMock.Setup(tp => tp.GetStepDefinitionClassTemplate(It.IsAny())) .Returns("{namespace}/{className}/{bindings}"); templateProviderMock.Setup(tp => tp.GetStepDefinitionTemplate(It.IsAny(), true)) - .Returns("{attribute}/{regex}/{methodName}/{parameters}"); + .Returns("{attribute}/{expression}/{methodName}/{parameters}"); templateProviderMock.Setup(tp => tp.GetStepDefinitionTemplate(It.IsAny(), false)) .Returns("{attribute}/{methodName}/{parameters}"); @@ -161,7 +161,7 @@ public void Should_GetStepDefinitionSkeleton_generate_simple_regex_style() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, CreateSimpleWhen(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I do something/WhenIDoSomething/"); + result.Should().Be("When/@\"I do something\"/WhenIDoSomething/"); } [Fact] @@ -171,7 +171,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I do something/WhenIDoSomething/string multilineText, Table table"); + result.Should().Be("When/@\"I do something\"/WhenIDoSomething/string multilineText, Table table"); } [Fact] @@ -181,7 +181,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I do something/WhenIDoSomething/multilineText : string, table : Table"); + result.Should().Be("When/@\"I do something\"/WhenIDoSomething/multilineText : string, table : Table"); } [Fact] @@ -191,7 +191,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I do something/WhenIDoSomething/ByVal multilineText As String, ByVal table As Table"); + result.Should().Be("When/\"I do something\"/WhenIDoSomething/ByVal multilineText As String, ByVal table As Table"); } [Fact] @@ -203,7 +203,7 @@ public void Should_GetStepDefinitionSkeleton_exclude_parameter_from_method_name_ var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/string p0"); + result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string p0"); } [Fact] @@ -215,7 +215,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_csharp_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/string @do"); + result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string @do"); } [Fact] @@ -227,7 +227,7 @@ public void Should_GetStepDefinitionSkeleton_should_not_escape_csharp_keywords_w var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/string fLoaT"); + result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string fLoaT"); } [Fact] @@ -239,7 +239,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_vb_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/ByVal [do] As String"); + result.Should().Be("When/\"I '(.*)' something\"/WhenISomething/ByVal [do] As String"); } [Fact] @@ -251,7 +251,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_vb_keywords_regardles var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/ByVal [Do] As String"); + result.Should().Be("When/\"I '(.*)' something\"/WhenISomething/ByVal [Do] As String"); } [Fact] @@ -263,7 +263,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_fsharp_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/``do`` : string"); + result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/``do`` : string"); } [Fact] @@ -275,16 +275,16 @@ public void Should_GetStepDefinitionSkeleton_should_not_escape_fsharp_keywords_w var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/I '(.*)' something/WhenISomething/finaLLY : string"); + result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/finaLLY : string"); } - private StepInstance CreateWhenWithSingleParam() + private StepInstance CreateWhenWithSingleParam(bool wrapQuotes = true) { var stepInstance = CreateSimpleWhen("I 'do' something"); analizeResult.TextParts.Clear(); - analizeResult.TextParts.Add("I '"); - analizeResult.TextParts.Add("' something"); - analizeResult.Parameters.Add(new AnalyzedStepParameter("String", "p0", ".*")); + analizeResult.TextParts.Add("I " + (wrapQuotes ? "'" : "")); + analizeResult.TextParts.Add((wrapQuotes ? "'" : "") + " something"); + analizeResult.Parameters.Add(new AnalyzedStepParameter("String", "p0", ".*", "string")); return stepInstance; } @@ -420,5 +420,27 @@ public void Should_GetStepDefinitionSkeleton_generate_simple_regex_method_name_s result.Should().Be("When/``I do something``/"); } + + [Fact] + public void Should_GetStepDefinitionSkeleton_generate_cucumber_expression() + { + var sut = CreateSut(); + + var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, CreateSimpleWhen(), StepDefinitionSkeletonStyle.CucumberExpressionAttribute, bindingCulture); + + result.Should().Be("When/\"I do something\"/WhenIDoSomething/"); + } + + [Fact] + public void Should_GetStepDefinitionSkeleton_generate_cucumber_expression_with_parameter() + { + var sut = CreateSut(); + + var stepInstance = CreateWhenWithSingleParam(wrapQuotes: false); + + var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.CucumberExpressionAttribute, bindingCulture); + + result.Should().Be("When/\"I {string} something\"/WhenISomething/string p0"); + } } } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepTextAnalyzerTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepTextAnalyzerTests.cs index 9aa9cf426..cc67ebabe 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepTextAnalyzerTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepTextAnalyzerTests.cs @@ -33,9 +33,11 @@ public void Should_recognize_quoted_strings() var result = sut.Analyze("I \"did\" something", bindingCulture); result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("did"); + result.Parameters[0].WrapText.Should().Be("\""); + result.Parameters[0].CucumberExpressionTypeName.Should().Be("string"); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I \""); - result.TextParts[1].Should().Be("\" something"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something"); } [Fact] @@ -46,9 +48,11 @@ public void Should_recognize_apostrophed_strings() var result = sut.Analyze("I 'did' something", bindingCulture); result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("did"); + result.Parameters[0].WrapText.Should().Be("'"); + result.Parameters[0].CucumberExpressionTypeName.Should().Be("string"); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I '"); - result.TextParts[1].Should().Be("' something"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something"); } [Fact] @@ -72,9 +76,10 @@ public void Should_handle_quote_overlaps() var result = sut.Analyze("I 'do \" something' really \" strange", bindingCulture); result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("p0"); + result.Parameters[0].WrapText.Should().Be("'"); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I '"); - result.TextParts[1].Should().Be("' really \" strange"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" really \" strange"); } [Fact] @@ -84,9 +89,10 @@ public void Should_handle_overlaps_with_numbers() var result = sut.Analyze("I 'do 42 something' foo", bindingCulture); result.Parameters.Count.Should().Be(1); + result.Parameters[0].WrapText.Should().Be("'"); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I '"); - result.TextParts[1].Should().Be("' foo"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" foo"); } [Fact] @@ -101,6 +107,7 @@ public void Should_recognize_integers() result.TextParts[0].Should().Be("I have "); result.TextParts[1].Should().Be(" bars"); result.Parameters[0].Type.Should().Be("Int32"); + result.Parameters[0].CucumberExpressionTypeName.Should().Be("int"); } [Theory] @@ -118,6 +125,7 @@ public void Should_recognize_dates(string dateString, string cultureCode) result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("p0"); result.Parameters[0].Type.Should().Be("DateTime"); + result.Parameters[0].CucumberExpressionTypeName.Should().Be("DateTime"); result.TextParts.Count.Should().Be(2); result.TextParts[0].Should().Be("Zombie apocalypse is expected at "); result.TextParts[1].Should().Be(""); @@ -135,6 +143,7 @@ public void Should_recognize_decimals() result.TextParts[0].Should().Be("I have "); result.TextParts[1].Should().Be(" bars"); result.Parameters[0].Type.Should().Be("Decimal"); + result.Parameters[0].CucumberExpressionTypeName.Should().Be("float"); } [Fact] @@ -145,11 +154,13 @@ public void Should_recognize_quoted_strings_with_multiple_parameters() var result = sut.Analyze("I \"did\" something with \"multiple\" parameters", bindingCulture); result.Parameters.Count.Should().Be(2); result.Parameters[0].Name.Should().Be("did"); + result.Parameters[0].WrapText.Should().Be("\""); result.Parameters[1].Name.Should().Be("multiple"); + result.Parameters[1].WrapText.Should().Be("\""); result.TextParts.Count.Should().Be(3); - result.TextParts[0].Should().Be("I \""); - result.TextParts[1].Should().Be("\" something with \""); - result.TextParts[2].Should().Be("\" parameters"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something with "); + result.TextParts[2].Should().Be(" parameters"); } [Fact] @@ -215,9 +226,9 @@ public void Should_correctly_case_parameter_names() result.Parameters[0].Name.Should().Be("did"); result.Parameters[1].Name.Should().Be("mUlTiPLe"); result.TextParts.Count.Should().Be(3); - result.TextParts[0].Should().Be("I \""); - result.TextParts[1].Should().Be("\" something with \""); - result.TextParts[2].Should().Be("\" parameters"); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something with "); + result.TextParts[2].Should().Be(" parameters"); } [Fact] @@ -229,8 +240,8 @@ public void Should_support_accented_characters() result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("dö"); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I \""); - result.TextParts[1].Should().Be("\" something "); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something "); } [Fact] @@ -241,9 +252,10 @@ public void Should_support_empty_strings() var result = sut.Analyze("I \"\" something ", bindingCulture); result.Parameters.Count.Should().Be(1); result.Parameters[0].Name.Should().Be("p0"); + result.Parameters[0].WrapText.Should().Be("\""); result.TextParts.Count.Should().Be(2); - result.TextParts[0].Should().Be("I \""); - result.TextParts[1].Should().Be("\" something "); + result.TextParts[0].Should().Be("I "); + result.TextParts[1].Should().Be(" something "); } } } From 1b573e8f3caf396a7a62ccc94af5c845d6ce6b45 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Tue, 24 May 2022 11:19:09 +0200 Subject: [PATCH 23/38] remove parameter type dumping --- .../CucumberExpressionParameterTypeRegistry.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index 33d8410d5..bd2ad766e 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -80,8 +80,6 @@ private Dictionary InitializeR .Select(g => new CucumberExpressionParameterType(g.Key.Name ?? g.Key.TargetType.Name, g.Key.TargetType, g)) .ToDictionary(pt => pt.Name, pt => (ISpecFlowCucumberExpressionParameterType)pt); - DumpParameterTypes(parameterTypes); - return parameterTypes; } @@ -98,16 +96,6 @@ private IEnumerable GetEnumTypes } } - [Conditional("DEBUG")] - private static void DumpParameterTypes(Dictionary parameterTypes) - { - foreach (var parameterType in parameterTypes) - { - Console.WriteLine( - $"PT: {parameterType.Key}, transformations: {string.Join(",", parameterType.Value.Transformations.Select(t => t.Regex))}, Regexps: {string.Join(",", parameterType.Value.RegexStrings)}"); - } - } - public IParameterType LookupByTypeName(string name) { if (_parameterTypesByName.Value.TryGetValue(name, out var parameterType)) From 17d29f6d70ebb82df5439f2eea688ebc88942f39 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 11:27:02 +0200 Subject: [PATCH 24/38] Generate regex-based snippets in ^xxx$ style, cleanup --- .../StepDefinitionSkeletonProvider.cs | 4 ++-- .../BindingSkeletons/StepTextAnalyzer.cs | 6 ++--- ...rExpressionStepDefinitionBindingBuilder.cs | 5 +++- .../StepDefinitionSkeletonProviderTests.cs | 24 +++++++++---------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs index d741e7793..993e7f380 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/StepDefinitionSkeletonProvider.cs @@ -88,8 +88,6 @@ private string GetCucumberExpression(AnalyzedStepText stepText) } return result.ToString(); - - throw new NotImplementedException(); } private AnalyzedStepText Analyze(StepInstance stepInstance, CultureInfo bindingCulture) @@ -168,6 +166,7 @@ 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++) { @@ -175,6 +174,7 @@ private string GetRegex(AnalyzedStepText stepText) result.Append($"{parameter.WrapText}({parameter.RegexPattern}){parameter.WrapText}"); result.Append(EscapeRegex(stepText.TextParts[i])); } + result.Append("$"); return result.ToString(); } diff --git a/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs b/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs index 066e1565b..212f6ed37 100644 --- a/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs +++ b/TechTalk.SpecFlow/BindingSkeletons/StepTextAnalyzer.cs @@ -56,8 +56,8 @@ 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; @@ -68,7 +68,7 @@ public AnalyzedStepText Analyze(string stepText, CultureInfo bindingCulture) switch (value.Substring(0, Math.Min(value.Length, 1))) { case "\"": - regexPattern = doubleQuoteRegexPattern; //TODO[cukeex]: this code was not working and provided .* regex, shall we keep that? + regexPattern = doubleQuoteRegexPattern; value = value.Substring(1, value.Length - 2); wrapText = "\""; break; diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs index 308c892af..a8c3bda83 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilder.cs @@ -34,7 +34,10 @@ public static bool IsCucumberExpression(string cucumberExpressionCandidate) if (CommonRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) return false; - //TODO[cukeex]: consider this once more + // These are special constructs that usually happen in regex, but not valid + // in Cucumber Expressions => If they exist, we treat the expression as regex. + // - \d+ + // - \. if (ExtendedRegexStepDefPatterns.IsMatch(cucumberExpressionCandidate)) return false; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs index 0bf0f2441..7e3b729f0 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/BindingSkeletons/StepDefinitionSkeletonProviderTests.cs @@ -161,7 +161,7 @@ public void Should_GetStepDefinitionSkeleton_generate_simple_regex_style() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, CreateSimpleWhen(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I do something\"/WhenIDoSomething/"); + result.Should().Be("When/@\"^I do something$\"/WhenIDoSomething/"); } [Fact] @@ -171,7 +171,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I do something\"/WhenIDoSomething/string multilineText, Table table"); + result.Should().Be("When/@\"^I do something$\"/WhenIDoSomething/string multilineText, Table table"); } [Fact] @@ -181,7 +181,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I do something\"/WhenIDoSomething/multilineText : string, table : Table"); + result.Should().Be("When/@\"^I do something$\"/WhenIDoSomething/multilineText : string, table : Table"); } [Fact] @@ -191,7 +191,7 @@ public void Should_GetStepDefinitionSkeleton_generate_regex_style_with_extra_arg var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, CreateWhenWithExtraArgs(), StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/\"I do something\"/WhenIDoSomething/ByVal multilineText As String, ByVal table As Table"); + result.Should().Be("When/\"^I do something$\"/WhenIDoSomething/ByVal multilineText As String, ByVal table As Table"); } [Fact] @@ -203,7 +203,7 @@ public void Should_GetStepDefinitionSkeleton_exclude_parameter_from_method_name_ var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string p0"); + result.Should().Be("When/@\"^I '(.*)' something$\"/WhenISomething/string p0"); } [Fact] @@ -215,7 +215,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_csharp_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string @do"); + result.Should().Be("When/@\"^I '(.*)' something$\"/WhenISomething/string @do"); } [Fact] @@ -227,7 +227,7 @@ public void Should_GetStepDefinitionSkeleton_should_not_escape_csharp_keywords_w var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.CSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/string fLoaT"); + result.Should().Be("When/@\"^I '(.*)' something$\"/WhenISomething/string fLoaT"); } [Fact] @@ -239,7 +239,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_vb_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/\"I '(.*)' something\"/WhenISomething/ByVal [do] As String"); + result.Should().Be("When/\"^I '(.*)' something$\"/WhenISomething/ByVal [do] As String"); } [Fact] @@ -251,7 +251,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_vb_keywords_regardles var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.VB, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/\"I '(.*)' something\"/WhenISomething/ByVal [Do] As String"); + result.Should().Be("When/\"^I '(.*)' something$\"/WhenISomething/ByVal [Do] As String"); } [Fact] @@ -263,7 +263,7 @@ public void Should_GetStepDefinitionSkeleton_should_escape_fsharp_keywords() var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/``do`` : string"); + result.Should().Be("When/@\"^I '(.*)' something$\"/WhenISomething/``do`` : string"); } [Fact] @@ -275,7 +275,7 @@ public void Should_GetStepDefinitionSkeleton_should_not_escape_fsharp_keywords_w var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, stepInstance, StepDefinitionSkeletonStyle.RegexAttribute, bindingCulture); - result.Should().Be("When/@\"I '(.*)' something\"/WhenISomething/finaLLY : string"); + result.Should().Be("When/@\"^I '(.*)' something$\"/WhenISomething/finaLLY : string"); } private StepInstance CreateWhenWithSingleParam(bool wrapQuotes = true) @@ -418,7 +418,7 @@ public void Should_GetStepDefinitionSkeleton_generate_simple_regex_method_name_s var result = sut.GetStepDefinitionSkeleton(ProgrammingLanguage.FSharp, CreateSimpleWhen(), StepDefinitionSkeletonStyle.MethodNameRegex, bindingCulture); - result.Should().Be("When/``I do something``/"); + result.Should().Be("When/``^I do something$``/"); } [Fact] From 8828ccfb4e356e260a0a4d031de92320513dadcf Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 11:32:29 +0200 Subject: [PATCH 25/38] Upgrade Cucumber.CucumberExpressions to v16.0.0 --- ExternalRepositories/SpecFlow.TestProjectGenerator | 2 +- .../CucumberExpressionParameterTypeRegistry.cs | 4 +--- TechTalk.SpecFlow/SpecFlow.nuspec | 4 ++-- TechTalk.SpecFlow/TechTalk.SpecFlow.csproj | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ExternalRepositories/SpecFlow.TestProjectGenerator b/ExternalRepositories/SpecFlow.TestProjectGenerator index 46d41a930..a0a50c838 160000 --- a/ExternalRepositories/SpecFlow.TestProjectGenerator +++ b/ExternalRepositories/SpecFlow.TestProjectGenerator @@ -1 +1 @@ -Subproject commit 46d41a9307beb7a73fe4804448c6481848d46988 +Subproject commit a0a50c838a19678e3019d9d0b2e329e098d80418 diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index bd2ad766e..5488759d4 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -48,9 +48,7 @@ private Dictionary InitializeR new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, objectBindingType, name: string.Empty, useForSnippets: false), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, intBindingType, ParameterTypeConstants.IntParameterName, weight: 1000), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, doubleBindingType, ParameterTypeConstants.FloatParameterName), - //TODO[cukeex]: fix constant in cuke ex - //new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), - new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, "word", useForSnippets: false), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), //the regex specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType, ParameterTypeConstants.StringParameterName), diff --git a/TechTalk.SpecFlow/SpecFlow.nuspec b/TechTalk.SpecFlow/SpecFlow.nuspec index e8e1cfa2f..122d2ab7a 100644 --- a/TechTalk.SpecFlow/SpecFlow.nuspec +++ b/TechTalk.SpecFlow/SpecFlow.nuspec @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj index b7099b15b..214350bea 100644 --- a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj +++ b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj @@ -34,7 +34,7 @@ - + From f87584b46a707a31e210ca2530fc9b454c4ead32 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 12:13:11 +0200 Subject: [PATCH 26/38] allow overriding {int}, define: {double}, {decimal}, cleanup --- ...CucumberExpressionParameterTypeRegistry.cs | 58 ++++++++++---- ...erExpressionParameterTypeTransformation.cs | 6 +- .../CucumberExpressionIntegrationTests.cs | 79 +++++++++++++++++++ 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index 5488759d4..f3fe9c982 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -44,35 +44,52 @@ private Dictionary InitializeR var builtInTransformations = new[] { - // official cucumber expression types + // official cucumber expression special expression parameter types ({}, {word}) new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, objectBindingType, name: string.Empty, useForSnippets: false), - new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, intBindingType, ParameterTypeConstants.IntParameterName, weight: 1000), - new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, doubleBindingType, ParameterTypeConstants.FloatParameterName), new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), - //the regex specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType, ParameterTypeConstants.StringParameterName), - - // other types supported by SpecFlow by default: Make them accessible with type name (e.g. Int32) + // official cucumber expression string expression parameter type ({string}) + // The regex '.*' specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression + new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, stringBindingType, ParameterTypeConstants.StringParameterName), + + // other types supported by SpecFlow by default: Make them accessible with type name (e.g. {Guid}) + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, byteBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, shortBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, intBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.IntParameterRegex, longBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, doubleBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, floatBindingType), + new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.FloatParameterRegex, decimalBindingType), new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, boolBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, byteBindingType), new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, charBindingType), new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, dateTimeBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, decimalBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, doubleBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, floatBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, shortBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, intBindingType), - new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, longBindingType), new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, guidBindingType), }; + // cucumber expression aliases for supported types ({int}, {float}, {double}, {decimal}) + var aliases = new Dictionary + { + { intBindingType, "int" }, // official + { floatBindingType, "float" }, // official + { doubleBindingType, "double" }, // custom for .NET + { decimalBindingType, "decimal" } // custom for .NET + }; + + var aliasTransformations = builtInTransformations.Select(t => aliases.TryGetValue(t.TargetType, out var alias) ? new { Transformation = t, Alias = alias } : null) + .Where(t => t != null) + .Select(t => new BuiltInCucumberExpressionParameterTypeTransformation(t.Transformation.Regex, t.Transformation.TargetType, t.Alias)) + .ToArray(); + + // cucumber expression parameter type support for enums with type name, using the built-in enum transformation (e.g. {Color}) var enumTypes = GetEnumTypesUsedInParameters(); - var userTransformations = _bindingRegistry.GetStepTransformations().Select(t => new UserDefinedCucumberExpressionParameterTypeTransformation(t)); + // get custom user transformations (both for built-in types and for custom types) + var userTransformations = _bindingRegistry.GetStepTransformations() + .SelectMany(t => GetUserTransformations(t, aliases)); var parameterTypes = builtInTransformations .Concat(enumTypes) + .Concat(aliasTransformations) .Concat(userTransformations) .GroupBy(t => (t.TargetType, t.Name)) .Select(g => new CucumberExpressionParameterType(g.Key.Name ?? g.Key.TargetType.Name, g.Key.TargetType, g)) @@ -81,8 +98,19 @@ private Dictionary InitializeR return parameterTypes; } + private IEnumerable GetUserTransformations(IStepArgumentTransformationBinding t, Dictionary aliases) + { + yield return new UserDefinedCucumberExpressionParameterTypeTransformation(t); + + // If the custom user transformations is for a built-in type, we also expose it with the + // short name (e.g {int}) and not only with the type name (e.g. {Int32}). + if (aliases.TryGetValue(t.Method.ReturnType, out var alias)) + yield return new UserDefinedCucumberExpressionParameterTypeTransformation(t, alias); + } + private IEnumerable GetEnumTypesUsedInParameters() { + // As we don't have a full list of possible enums, we collect all enums that are used as parameters of the step definitions. var enumParameterTypes = _consideredParameterTypes .OfType() .Where(t => t.Type.IsEnum); diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs index a6ff20eb7..3bd0091af 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/UserDefinedCucumberExpressionParameterTypeTransformation.cs @@ -7,16 +7,18 @@ namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; public class UserDefinedCucumberExpressionParameterTypeTransformation : ICucumberExpressionParameterTypeTransformation { private readonly IStepArgumentTransformationBinding _stepArgumentTransformationBinding; + private readonly string _customName; - public string Name => _stepArgumentTransformationBinding.Name; + public string Name => _customName ?? _stepArgumentTransformationBinding.Name; public string Regex { get; } public IBindingType TargetType => _stepArgumentTransformationBinding.Method.ReturnType; public bool UseForSnippets => true; public int Weight => 0; - public UserDefinedCucumberExpressionParameterTypeTransformation(IStepArgumentTransformationBinding stepArgumentTransformationBinding) + public UserDefinedCucumberExpressionParameterTypeTransformation(IStepArgumentTransformationBinding stepArgumentTransformationBinding, string customName = null) { _stepArgumentTransformationBinding = stepArgumentTransformationBinding; + _customName = customName; Regex = GetCucumberExpressionRegex(stepArgumentTransformationBinding); } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index cea9ab116..e32251533 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -79,6 +79,10 @@ public void StepDefWithIntParam(int intParam) ExecutedParams.Add((intParam, typeof(int))); } + public void StepDefWithFloatParam(float doubleParam) + { + ExecutedParams.Add((doubleParam, typeof(float))); + } public void StepDefWithDoubleParam(double doubleParam) { ExecutedParams.Add((doubleParam, typeof(double))); @@ -95,6 +99,11 @@ public void StepDefWithCustomClassParam(SampleUser userParam) { ExecutedParams.Add((userParam, typeof(SampleUser))); } + + public int ConvertFortyTwo() + { + return 42; + } } public class TestDependencyProvider : DefaultDependencyProvider @@ -241,6 +250,19 @@ public async void Should_match_step_with_int_parameter() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + [Fact] + public async void Should_match_step_with_float_parameter() + { + var expression = "I have {float} cucumbers in my belly"; + var stepText = "I have 42.1 cucumbers in my belly"; + var expectedParam = ((float)42.1, typeof(float)); + var methodName = nameof(SampleBindings.StepDefWithFloatParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + [Fact] public async void Should_match_step_with_float_parameter_to_double() { @@ -254,6 +276,32 @@ public async void Should_match_step_with_float_parameter_to_double() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + [Fact] + public async void Should_match_step_with_double_parameter() + { + var expression = "I have {double} cucumbers in my belly"; + var stepText = "I have 42.1 cucumbers in my belly"; + var expectedParam = (42.1, typeof(double)); + var methodName = nameof(SampleBindings.StepDefWithDoubleParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + + [Fact] + public async void Should_match_step_with_decimal_parameter() + { + var expression = "I have {decimal} cucumbers in my belly"; + var stepText = "I have 42.1 cucumbers in my belly"; + var expectedParam = (42.1m, typeof(decimal)); + var methodName = nameof(SampleBindings.StepDefWithDecimalParam); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } + [Fact] public async void Should_match_step_with_float_parameter_to_decimal() { @@ -344,4 +392,35 @@ public async void Should_match_step_with_custom_parameter_with_custom_name() sampleBindings.ExecutedParams.Should().Contain(expectedParam); } + [Fact] + public async void Should_match_step_with_customized_built_in_parameter_with_type_name() + { + var expression = "I have {Int32} cucumbers in my belly"; + var stepText = "I have forty two cucumbers in my belly"; + var expectedParam = (42, typeof(int)); + var methodName = nameof(SampleBindings.StepDefWithIntParam); + IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding( + "forty two", + new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertFortyTwo)))); + + 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_with_simple_name() + { + var expression = "I have {int} cucumbers in my belly"; + var stepText = "I have forty two cucumbers in my belly"; + var expectedParam = (42, typeof(int)); + var methodName = nameof(SampleBindings.StepDefWithIntParam); + IStepArgumentTransformationBinding transformation = new StepArgumentTransformationBinding( + "forty two", + new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(nameof(SampleBindings.ConvertFortyTwo)))); + + var sampleBindings = await PerformStepExecution(methodName, expression, stepText, new[] { transformation }); + + sampleBindings.ExecutedParams.Should().Contain(expectedParam); + } } From c64b42ff7167c2037783976e0f1dec9f09039bae Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 12:37:40 +0200 Subject: [PATCH 27/38] Cleanup {string} handling --- .../CucumberExpressionParameterType.cs | 2 +- .../CucumberExpressionParameterTypeRegistry.cs | 4 ++-- .../SpecFlowCucumberExpression.cs | 5 +++++ ...cumberExpressionParameterTypeRegistryTests.cs | 9 ++++++++- ...xpressionStepDefinitionBindingBuilderTests.cs | 16 +++++++++++++++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs index cd390dee0..ccd18da73 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterType.cs @@ -7,7 +7,7 @@ namespace TechTalk.SpecFlow.Bindings.CucumberExpressions; public class CucumberExpressionParameterType : ISpecFlowCucumberExpressionParameterType { - internal const string MatchAllRegex = @"(.*)"; + internal const string MatchAllRegex = @".*"; public string Name { get; } public IBindingType TargetType { get; } diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index f3fe9c982..6fe1a8980 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using CucumberExpressions; using TechTalk.SpecFlow.Bindings.Reflection; @@ -49,7 +48,8 @@ private Dictionary InitializeR new BuiltInCucumberExpressionParameterTypeTransformation(ParameterTypeConstants.WordParameterRegex, stringBindingType, ParameterTypeConstants.WordParameterName, useForSnippets: false), // official cucumber expression string expression parameter type ({string}) - // The regex '.*' specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression + // The regex '.*' specified here will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression. + // See SpecFlowCucumberExpression.HandleStringType for detailed explanation. new BuiltInCucumberExpressionParameterTypeTransformation(CucumberExpressionParameterType.MatchAllRegex, stringBindingType, ParameterTypeConstants.StringParameterName), // other types supported by SpecFlow by default: Make them accessible with type name (e.g. {Guid}) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs index 35e59d3c8..aac75e08a 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/SpecFlowCucumberExpression.cs @@ -11,6 +11,11 @@ public SpecFlowCucumberExpression(string expression, IParameterTypeRegistry para protected override bool HandleStringType(string name, IParameterType parameterType, out string[] regexps, out bool shouldWrapWithCaptureGroup) { + // The {string} type provided by the CucumberExpressionParameterTypeRegistry has a fake regex ('.*'), + // because to be able to support both double-quote (") and apostrophe (') wrapping, there has to be + // two capture groups that is not supported inside a parameter group in SpecFlow. + // The solution is to override the {string} handling here and provide the two capture groups without + // wrapping it to a general parameter group. regexps = ParameterTypeConstants.StringParameterRegexps; shouldWrapWithCaptureGroup = false; return true; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs index a4db9071b..ed06af370 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs @@ -8,6 +8,9 @@ namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; public class CucumberExpressionParameterTypeRegistryTests { + // Most of the logic in CucumberExpressionParameterTypeRegistry can only be tested in integration of a cucumber expression match, + // so those are tested in the CucumberExpressionIntegrationTests class. + private BindingRegistry _bindingRegistry; private CucumberExpressionParameterTypeRegistry CreateSut() { @@ -21,7 +24,11 @@ public void Should_provide_string_type() var sut = CreateSut(); var paramType = sut.LookupByTypeName("string"); + // The regex '.*' provided by the CucumberExpressionParameterTypeRegistry is fake and + // will be ignored because of the special string type handling implemented in SpecFlowCucumberExpression. + // See SpecFlowCucumberExpression.HandleStringType for detailed explanation. paramType.Should().NotBeNull(); - paramType.RegexStrings.Should().HaveCount(1);//TODO[cukeex]: is this really what we want? + paramType.RegexStrings.Should().HaveCount(1); + paramType.RegexStrings[0].Should().Be(".*"); } } \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs index 4e3e7a601..0b0ceac3b 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs @@ -8,10 +8,14 @@ namespace TechTalk.SpecFlow.RuntimeTests.Bindings.CucumberExpressions; public class CucumberExpressionStepDefinitionBindingBuilderTests { + // The different parameter types are tested in integration of a cucumber expression match, + // see CucumberExpressionIntegrationTests class. + private void SampleBindingMethod() { //nop } + private CucumberExpressionStepDefinitionBindingBuilder CreateSut(string sourceExpression, StepDefinitionType stepDefinitionType = StepDefinitionType.Given, IBindingMethod bindingMethod = null, BindingScope bindingScope = null) { bindingMethod ??= new RuntimeBindingMethod(GetType().GetMethod(nameof(SampleBindingMethod))); @@ -45,7 +49,17 @@ public void Should_build_from_expression_with_int_param() result.Regex?.ToString().Should().Be(@"^I have (-?\d+) cucumbers in my belly$"); } - //TODO[cukeex]: figure out what to expect + [Fact] + public void Should_build_from_expression_with_DateTime_param() + { + var sut = CreateSut("I have eaten cucumbers on {DateTime}"); + + var result = sut.BuildSingle(); + + result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + result.Regex?.ToString().Should().Be(@"^I have eaten cucumbers on (.*)$"); + } + [Fact] public void Should_build_from_expression_with_string_param() { From 7f2c8b0dbebfdabc9b8bf2b08fcb769f253597c1 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 12:55:43 +0200 Subject: [PATCH 28/38] fix build? --- .../ErrorHandling/StubErrorProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs index 5acf65e4b..1aa2798cb 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/ErrorHandling/StubErrorProvider.cs @@ -10,7 +10,7 @@ namespace TechTalk.SpecFlow.RuntimeTests.ErrorHandling internal class StubErrorProvider : ErrorProvider { public StubErrorProvider() : - base(new StepFormatter(), ConfigurationLoader.GetDefault(), GetStubUnitTestProvider()) + base(new StepFormatter(new ColorOutputHelper(ConfigurationLoader.GetDefault()), new ColorOutputTheme()), ConfigurationLoader.GetDefault(), GetStubUnitTestProvider()) { } From 7428335cfbabad91a095c0c8eb7c951680d3a3e3 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:29:18 +0200 Subject: [PATCH 29/38] Add specflow-config.json --- TechTalk.SpecFlow.sln | 1 + specflow-config.json | 278 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 specflow-config.json diff --git a/TechTalk.SpecFlow.sln b/TechTalk.SpecFlow.sln index 2c3e54569..b89f8ca0c 100644 --- a/TechTalk.SpecFlow.sln +++ b/TechTalk.SpecFlow.sln @@ -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 diff --git a/specflow-config.json b/specflow-config.json new file mode 100644 index 000000000..7f98e0c40 --- /dev/null +++ b/specflow-config.json @@ -0,0 +1,278 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + /* + + Changelog: + + 1.0 + - Initial SpecFlow configuration schema containing SpecFlow v3.9 settings and IDE settings + + */ + + "type": "object", + "additionalProperties": true, + "properties": { + "language": { + "description": "Use this section to define the default language for feature files and other language-related settings. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#language", + "type": "object", + "additionalProperties": true, + "properties": { + "feature": { + "description": "The default language of feature files added to the project. We recommend using specific culture names (e.g.: \"en-US\") rather than generic (neutral) cultures (e.g.: \"en\").", + "type": "string", + "default": "en-US" + } + } + }, + "bindingCulture": { + "description": "Use this section to define the culture for executing binding methods and converting step arguments. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#bindingculture", + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "description": "Specifies the culture to be used to execute binding methods and convert step arguments. If not specified, the feature language is used. We recommend using specific culture names (e.g.: \"en-US\") rather than generic (neutral) cultures (e.g.: \"en\").", + "type": "string", + "default": "not specified" + } + } + }, + "generator": { + "description": "Use this section to define unit test generation options. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#generator", + "type": "object", + "additionalProperties": true, + "properties": { + "allowDebugGeneratedFiles": { + "description": "By default, the debugger is configured to step through the generated code. This helps you debug your feature files and bindings. Disable this option by setting this attribute to \"true\".", + "type": "boolean", + "default": false + }, + "allowRowTests": { + "description": "Determines whether \"row tests\" should be generated for scenario outlines. This setting is ignored if the unit test framework does not support row based testing.", + "type": "boolean", + "default": false + }, + "addNonParallelizableMarkerForTags": { + "description": "Defines a set of tags, any of which specify that a feature should be excluded from running in parallel with any other feature. For mor details see https://docs.specflow.org/projects/specflow/en/latest/Execution/Parallel-Execution.html", + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + } + }, + "runtime": { + "description": "Use this section to specify various test execution options. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#runtime", + "type": "object", + "additionalProperties": true, + "properties": { + "missingOrPendingStepsOutcome": { + "description": "Determines how SpecFlow behaves if a step binding is not implemented or pending.", + "type": "string", + "default": "Pending", + "enum": [ "Pending", "Inconclusive", "Ignore", "Error" ] + }, + "obsoleteBehavior": { + "description": "Determines how SpecFlow behaves if a step binding is marked with [Obsolete] attribute.", + "type": "string", + "default": "Warn", + "enum": [ "None", "Warn", "Pending", "Error" ] + }, + "stopAtFirstError": { + "description": "Determines whether the execution should stop when encountering the first error, or whether it should attempt to try and match subsequent steps (in order to detect missing steps).", + "type": "boolean", + "default": false + } + } + }, + "trace": { + "description": "Use this section to determine the SpecFlow trace output. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#trace", + "type": "object", + "additionalProperties": true, + "properties": { + "traceSuccessfulSteps": { + "description": "Determines whether SpecFlow should trace successful step binding executions.", + "type": "boolean", + "default": true + }, + "traceTimings": { + "description": "Determines whether SpecFlow should trace execution time of the binding methods (only if the execution time is longer than the minTracedDuration value).", + "type": "boolean", + "default": false + }, + "minTracedDuration": { + "description": "Specifies a threshold for tracing the binding execution times. Example: 0:0:0.1 (100 ms)", + "type": "string", + "default": "0:0:0.1" + }, + "stepDefinitionSkeletonStyle": { + "description": "Specifies the default step definition style.", + "type": "string", + "default": "RegexAttribute", + "enum": [ "RegexAttribute", "MethodNameUnderscores", "MethodNamePascalCase", "MethodNameRegex" ] + } + } + }, + "stepAssemblies": { + "description": "This section can be used to configure additional assemblies that contain external binding assemblies. The assembly of the SpecFlow project (the project containing the feature files) is automatically included. The binding assemblies must be placed in the output folder (e.g. bin/Debug) of the SpecFlow project, for example by adding a reference to the assembly from the project. For more details see https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#stepassemblies", + "type": "array", + "default": [], + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "assembly": { + "description": "The name of the assembly containing bindings.", + "type": "string" + } + } + } + }, + "ide": { + "description": "This section contains all the extension configurations related to the IDE.", + "type": "object", + "additionalProperties": true, + "properties": { + "specFlow": { + "$ref": "#/definitions/SpecFlow" + }, + "traceability": { + "$ref": "#/definitions/Traceability" + }, + "editor": { + "$ref": "#/definitions/Editor" + } + } + }, + "$schema": { + "type": "string" + } + }, + "definitions": { + "SpecFlow": { + "description": "Settings related to SpecFlow projects", + "type": "object", + "additionalProperties": false, + "properties": { + "isSpecFlowProject": { + "description": "Enables the project to be handled as SpecFlow project. (Default: [detect automatically])", + "type": "boolean" + }, + "generatorFolder": { + "description": "The path of the SpecFlow generator folder used by the project, that is usually the 'tools' folder of the SpecFlow NuGet package, e.g. '..\\MyDependencies\\SpecFlow.2.3.0\\tools'. (Default: [detect from the installed SpecFlow NuGet package])", + "type": "string" + }, + "configFilePath": { + "description": "The path of the SpecFlow configuration file (App.config or specflow.json) used by the project, e.g. 'specflow.json'. (Default: [detect config file automatically])", + "type": "string" + }, + "version": { + "description": "The SpecFlow version used by the project, e.g. '2.3.1'. (Default: [detect version automatically])", + "type": "string", + "pattern": "^(?:\\.?[0-9]+){2,}(?:\\-[\\-a-z0-9]*)?$" + }, + "traits": { + "description": "The list of the SpecFlow-related project traits. The possible traits are: 'MsBuildGeneration', 'XUnitAdapter', 'DesignTimeFeatureFileGeneration', e.g. '[\"XUnitAdapter\"]'. (Default: [detect from the installed NuGet packages])", + "type": "array", + "items": { + "enum": [ "None", "MsBuildGeneration", "XUnitAdapter", "DesignTimeFeatureFileGeneration" ] + } + } + } + }, + + "Traceability": { + "description": "Settings for traceability of scenarios, see http://speclink.me/deveroomtraceability for details.", + "type": "object", + "additionalProperties": false, + "properties": { + "tagLinks": { + "description": "List of tags patterns that should be converted to an external URL", + "type": "array", + "items": { + "type": "object", + "description": "Tag pattern configuration", + "additionalProperties": false, + "properties": { + "tagPattern": { + "description": "A regular expression that matches to the tag name (without leading '@' sign). The identifier of the artifacts should be matched with a named regex group. E.g. 'issue\\\\:(?\\\\d+)'. ", + "type": "string" + }, + "urlTemplate": { + "description": "An URL template to generate the external link. It can contain parameters in an '{paramName}' format, where 'paramName' refers to the regular expression group name in the 'tagPattern'. E.g. 'https://github.com/me/my-project/issues/{id}'", + "type": "string" + } + }, + "required": [ "tagPattern", "urlTemplate" ] + } + } + } + }, + + "Editor": { + "description": "This section contains all the Editor related configurations.", + "type": "object", + "additionalProperties": true, + "properties": { + "showStepCompletionAfterStepKeywords": { + "description": "Determines whether the extension should suggest step completions.", + "type": "boolean", + "default": true + }, + "gherkinFormat": { + "description": "Provides settings for formatting Gherkin feature files", + "type": "object", + "additionalItems": true, + "properties": { + "indentFeatureChildren": { + "description": "Specifies whether child elements of Feature (Background, Rule, Scenario, Scenario Outline) should be indented.", + "type": "boolean", + "default": false + }, + "indentRuleChildren": { + "description": "Specifies whether child elements fo Rule (Background, Scenario, Scenario Outline) should be indented.", + "type": "boolean", + "default": false + }, + "indentSteps": { + "description": "Specifies whether steps of scenarios should be indented.", + "type": "boolean", + "default": true + }, + "indentAndSteps": { + "description": "Specifies whether the 'And' and 'But' steps of the scenarios should have an additional indentation.", + "type": "boolean", + "default": false + }, + "indentDataTable": { + "description": "Specifies whether DataTable arguments should be indented within the step.", + "type": "boolean", + "default": true + }, + "indentDocString": { + "description": "Specifies whether DocString arguments should be indented within the step.", + "type": "boolean", + "default": true + }, + "indentExamples": { + "description": "Specifies whether the Examples block should be indented within the Scenario Outline.", + "type": "boolean", + "default": false + }, + "indentExamplesTable": { + "description": "Specifies whether the Examples table should be indented within the Examples block.", + "type": "boolean", + "default": true + }, + "tableCellPaddingSize": { + "description": "The number of space characters to be used on each sides as table cell padding.", + "type": "integer", + "default": 1 + } + } + } + } + } + } +} From 9cd71815c22eab2edad3eb063000cd03faef5651 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:29:44 +0200 Subject: [PATCH 30/38] update spec specflow-config.json wit cucumber expressions --- specflow-config.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specflow-config.json b/specflow-config.json index 7f98e0c40..577e9299c 100644 --- a/specflow-config.json +++ b/specflow-config.json @@ -5,6 +5,9 @@ Changelog: + 1.1 + - Added "CucumberExpressionAttribute" to trace/stepDefinitionSkeletonStyle and set it is as default. + 1.0 - Initial SpecFlow configuration schema containing SpecFlow v3.9 settings and IDE settings @@ -109,8 +112,8 @@ "stepDefinitionSkeletonStyle": { "description": "Specifies the default step definition style.", "type": "string", - "default": "RegexAttribute", - "enum": [ "RegexAttribute", "MethodNameUnderscores", "MethodNamePascalCase", "MethodNameRegex" ] + "default": "CucumberExpressionAttribute", + "enum": [ "CucumberExpressionAttribute", "RegexAttribute", "MethodNameUnderscores", "MethodNamePascalCase", "MethodNameRegex" ] } } }, From 523c3ae1eb85ced3b3236d454cffbea8347b6e4b Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:33:31 +0200 Subject: [PATCH 31/38] Add {byte}, {long} --- .../CucumberExpressionParameterTypeRegistry.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs index 6fe1a8980..f85bd2623 100644 --- a/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistry.cs @@ -71,7 +71,9 @@ private Dictionary InitializeR { { intBindingType, "int" }, // official { floatBindingType, "float" }, // official - { doubleBindingType, "double" }, // custom for .NET + { doubleBindingType, "double" }, // official + { byteBindingType, "byte" }, // official + { longBindingType, "long" }, // official { decimalBindingType, "decimal" } // custom for .NET }; From de9df248e7db7073ff2f012c9dba80815ef16596 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:34:30 +0200 Subject: [PATCH 32/38] Check is [Obsolete] works --- .../Reflection/RuntimeBindingMethod.cs | 3 +- ...essionStepDefinitionBindingBuilderTests.cs | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/Reflection/RuntimeBindingMethod.cs b/TechTalk.SpecFlow/Bindings/Reflection/RuntimeBindingMethod.cs index 4401525c0..ab80d692b 100644 --- a/TechTalk.SpecFlow/Bindings/Reflection/RuntimeBindingMethod.cs +++ b/TechTalk.SpecFlow/Bindings/Reflection/RuntimeBindingMethod.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -30,7 +31,7 @@ public IEnumerable Parameters public RuntimeBindingMethod(MethodInfo methodInfo) { - this.MethodInfo = methodInfo; + this.MethodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo)); } public override string ToString() diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs index 0b0ceac3b..80996bf1c 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionStepDefinitionBindingBuilderTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using System; +using System.Reflection; +using FluentAssertions; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Bindings.CucumberExpressions; using TechTalk.SpecFlow.Bindings.Reflection; @@ -18,7 +20,7 @@ private void SampleBindingMethod() private CucumberExpressionStepDefinitionBindingBuilder CreateSut(string sourceExpression, StepDefinitionType stepDefinitionType = StepDefinitionType.Given, IBindingMethod bindingMethod = null, BindingScope bindingScope = null) { - bindingMethod ??= new RuntimeBindingMethod(GetType().GetMethod(nameof(SampleBindingMethod))); + bindingMethod ??= new RuntimeBindingMethod(GetType().GetMethod(nameof(SampleBindingMethod), BindingFlags.NonPublic | BindingFlags.Instance)); return new CucumberExpressionStepDefinitionBindingBuilder( new CucumberExpressionParameterTypeRegistry(new BindingRegistry()), stepDefinitionType, @@ -70,4 +72,27 @@ public void Should_build_from_expression_with_string_param() result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); result.Regex?.ToString().Should().Be(@"^there is a user (?:(?:""([^""\\]*(?:\\.[^""\\]*)*)"")|(?:'([^'\\]*(?:\\.[^'\\]*)*)')) registered$"); } + + [Obsolete("this is deprecated", false)] + // ReSharper disable once UnusedMember.Local + private void SampleBindingMethod_Obsolete() + { + //nop + } + + [Fact] + public void Should_create_binding_that_can_be_detected_as_obsolete() + { + var runtimeBindingMethod = new RuntimeBindingMethod(GetType().GetMethod(nameof(SampleBindingMethod) + "_Obsolete", BindingFlags.NonPublic | BindingFlags.Instance)); + var sut = CreateSut("simple expression", bindingMethod: + runtimeBindingMethod); + + var result = sut.BuildSingle(); + + result.ExpressionType.Should().Be(StepDefinitionExpressionTypes.CucumberExpression); + result.Regex?.ToString().Should().Be("^simple expression$"); + + var obsoletion = new BindingObsoletion(result); + obsoletion.IsObsolete.Should().BeTrue(); + } } \ No newline at end of file From fe661d06e65b37b99f83443c3a2d282c6d07ebe7 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:34:51 +0200 Subject: [PATCH 33/38] Extend docs with Cucumber-Expressions --- docs/Bindings/Cucumber-Expressions.md | 68 ++++++++++++++ docs/Bindings/Step-Definitions.md | 59 +++++++++--- docs/Guides/UpgradeSpecFlow3To4.md | 92 +++++++++++++++++++ .../Breaking-Changes-with-SpecFlow-4.0.md | 15 +++ docs/Installation/Configuration.md | 4 +- docs/index.rst | 3 + 6 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 docs/Bindings/Cucumber-Expressions.md create mode 100644 docs/Guides/UpgradeSpecFlow3To4.md create mode 100644 docs/Installation/Breaking-Changes-with-SpecFlow-4.0.md diff --git a/docs/Bindings/Cucumber-Expressions.md b/docs/Bindings/Cucumber-Expressions.md new file mode 100644 index 000000000..e4da3299a --- /dev/null +++ b/docs/Bindings/Cucumber-Expressions.md @@ -0,0 +1,68 @@ +# Cucumber Expression support in SpecFlow + +Cucumber Expression is an expression type to specify [step definitions](Step-Definitions.md). Cucumber Expressions is an alternative to [Regular Expressions](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions) with a more intuitive syntax. + +You can find a detailed description about cucumber expressions [on GitHub](https://github.com/cucumber/cucumber-expressions#readme). In this page we only provide a short summary and the special handling in .NET / SpecFlow. + +Cucumber Expressions are supported natively by SpecFlow from v4. In earlier versions you need to add a reference to an additional NuGet package, e.g. for SpecFlow v3.9, you need to use the [CucumberExpressions.SpecFlow.3-9](https://www.nuget.org/packages/CucumberExpressions.SpecFlow.3-9) package. + +The following step definition that uses cucumber expression matches to the step `When I have 42 cucumbers in my belly` + +``` +[When("I have {int} cucumbers in my belly")] +public void WhenIHaveCucumbersInMyBelly(int count) { ... } +``` + +## Cucumber Expression basics + +### Simple text + +To match for a simple text, just use the text as cucumber expression. + +`[When("I do something")]` matches to `When I do something` + +### Parameters + +Parameters can be defined using the `{parameter-type}` syntax. Where `parameter-type` can be any of the following: + +* A short name for some simple built-in types: `{int}`, `{long}`, `{byte}`, `{float}`, `{double}`, `{decimal}` +* `string` (`{string}`) that matches to quoted text wrapped with either `"` or `'`. E.g., `[Given("a user {string}")]` matches to `Given a user "Marvin"` or `Given a user 'Zaphod Beeblebrox'`. +* `word` (`{word}`) that matches to a single word without quotes. E.g., `[Given("a user {word}")]` matches to `Given a user Marvin`. +* Empty (`{}`) that matches to anything (like `(.*)` with regex). +* A type name without namespace that is supported by SpecFlow as a parameter type (types with built-in support, enum types and types with [custom argument conversions](Step-Argument-Conversions.md)). E.g. `[When("I have {CustomColor} cucumbers in my belly")]` matches to `When I have green cucumbers in my belly` if `CustomColor` is an enum with `Green` as a value. +* A custom type name you have specified int the `[StepArgumentTransformation]` attribute. E.g., With `[StepArgumentTransformation("v(.*)", Name = "my_version")]`, you can define a step as `[When("I download the release {my_version} of the application")]` that matches to `When I download the release v1.2.3 of the application`. + +### Optionals, alternatives + +Cucumber expressions use the parentheses (`(...)`) for optionals and the `/` character to define alternatives. The step definition + +``` +[When("I have {int} cucumber(s) in my belly/tummy")] +public void WhenIHaveCucumbersInMyBelly(int count) +``` + +will match to all of the following steps + +``` +When I have 42 cucumbers in my belly +When I have 1 cucumber in my belly +When I have 8 cucumbers in my tummy +``` + +## Using Cucumber Expressions with SpecFlow + +When you use SpecFlow v4 or have installed the package for the earlier versions, you can use both cucumber expressions and regular expressions in your project. SpecFlow has uses some heuristics to decide if your expression is a cucumber expression or a regular expression. + +In case your regular expression is wrongly detected as cucumber expression, you can always force to use regular expression by specifying the regex start/end markers (`^`/`$`). + +``` +[When(@"^this expression is treated as a regex$")] +``` + +For potential migration problems of your existing SpecFlow codebase with regular expressions, please check the [Upgrade from SpecFlow 3.* to 4.*](../Guides/UpgradeSpecFlow3To4.md) guide. + +### IDE Support for Cucumber Expressions + +Recognition of step definitions using cucumber expressions (including "Go To Definition" navigation) is only supported in Visual Studio 2022, or in Visual Studio 2017/20017 using the "Deveroom" Visual Studio extension. + +In older versions you will see these steps as "undefined step", but the test execution is not affected by this problem. diff --git a/docs/Bindings/Step-Definitions.md b/docs/Bindings/Step-Definitions.md index 2a21403b6..0d5d613bf 100644 --- a/docs/Bindings/Step-Definitions.md +++ b/docs/Bindings/Step-Definitions.md @@ -17,7 +17,7 @@ For better reusability, the step definitions can include parameters. This means The following example shows a simple step definition that matches to the step `When I perform a simple search on 'Domain'`: ``` csharp -[When(@"I perform a simple search on '(.*)'")] +[When(@"^I perform a simple search on '(.*)'$")] public void WhenIPerformASimpleSearchOn(string searchTerm) { var controller = new CatalogController(); @@ -27,18 +27,31 @@ public void WhenIPerformASimpleSearchOn(string searchTerm) Here the method is annotated with the `[When]` attribute, and includes the regular expression used to match the step's text. This [regular expression](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions) uses (`(.*)`) to define parameters for the method. +With SpecFlow v4 you can also use [Cucumber Expressions](Cucumber-Expressions.md) to specify step definitions. The step definition above can now be written as: + +``` csharp +[When("I perform a simple search on {string}")] +public void WhenIPerformASimpleSearchOn(string searchTerm) +{ + var controller = new CatalogController(); + actionResult = controller.Search(searchTerm); +} +``` + ## Supported Step Definition Attributes -* `[Given(regex)]` or `[Given]` - `TechTalk.SpecFlow.GivenAttribute` -* `[When(regex)]` or `[When]` - `TechTalk.SpecFlow.WhenAttribute` -* `[Then(regex)]` or `[Then]` - `TechTalk.SpecFlow.ThenAttribute` -* `[StepDefinition(regex)]` or `[StepDefinition]` - `TechTalk.SpecFlow.StepDefinitionAttribute`, matches for given, when or then attributes +* `[Given(expression)]` or `[Given]` - `TechTalk.SpecFlow.GivenAttribute` +* `[When(expression)]` or `[When]` - `TechTalk.SpecFlow.WhenAttribute` +* `[Then(expression)]` or `[Then]` - `TechTalk.SpecFlow.ThenAttribute` +* `[StepDefinition(expression)]` or `[StepDefinition]` - `TechTalk.SpecFlow.StepDefinitionAttribute`, matches for given, when or then attributes + +The `expression` can be either a Cucumber Expression (from SpecFlow v4) or a Regular Expression. You can annotate a single method with multiple attributes in order to support different phrasings in the feature file for the same automation logic. ```c# -[When(@"I perform a simple search on '(.*)'")] -[When(@"I search for '(.*)'")] +[When("I perform a simple search on {string}")] +[When("I search for {string}")] public void WhenIPerformASimpleSearchOn(string searchTerm) { ... @@ -50,13 +63,12 @@ public void WhenIPerformASimpleSearchOn(string searchTerm) The `[Obsolete]` attribute from the system namespace is also supported, check [here](https://docs.specflow.org/projects/specflow/en/latest/Installation/Configuration.html#runtime) for more details. ```c# -[Given(@"Stuff is done")] - [Obsolete] - public void GivenStuffIsDone() - { - var x = 2+3; - } - +[Given("Stuff is done")] +[Obsolete] +public void GivenStuffIsDone() +{ + var x = 2+3; +} ``` @@ -72,6 +84,20 @@ The `[Obsolete]` attribute from the system namespace is also supported, check [h The rules depend on the step definition style you use. +### Cucumber expressions in attributes + +This is the new default style from SpecFlow v4. The step definition method has to be annotated with one or more step definition attributes with [cucumber expressions](Cucumber-Expressions.md). + +```c# +[Given("I have entered {int} into the calculator")] +public void GivenIHaveEnteredNumberIntoTheCalculator(int number) +{ + ... +} +``` + +For cucumber expression matching rules please check the [cucumber expressions](Cucumber-Expressions.md) page. + ### Regular expressions in attributes This is the classic and most used way of specifying the step definitions. The step definition method has to be annotated with one or more step definition attributes with regular expressions. @@ -143,6 +169,9 @@ let [] ``I have entered (.*) into the calculator``(number:int) = * Step definitions can specify parameters. These will match to the parameters of the step definition method. * The method parameter type can be `string` or other .NET type. In the later case a [configurable conversion](Step-Argument-Conversions.md) is applied. +* With cucumber expressions + * The parameter placeholders (`{parameter-type}`) define the arguments for the method based on the order (the match result of the first group becomes the first argument, etc.). + * For the exact parameter rules please check the [cucumber expressions](Cucumber-Expressions.md) page. * With regular expressions * The match groups (`(…)`) of the regular expression define the arguments for the method based on the order (the match result of the first group becomes the first argument, etc.). * You can use non-capturing groups `(?:regex)` in order to use groups without a method argument. @@ -161,7 +190,7 @@ Given the following books ``` ``` csharp -[Given(@"the following books")] +[Given("the following books")] public void GivenTheFollowingBooks(Table table) { ... diff --git a/docs/Guides/UpgradeSpecFlow3To4.md b/docs/Guides/UpgradeSpecFlow3To4.md new file mode 100644 index 000000000..516dcd8af --- /dev/null +++ b/docs/Guides/UpgradeSpecFlow3To4.md @@ -0,0 +1,92 @@ +# Upgrade from SpecFlow 3.* to 4.* + +This guide explains how to update your SpecFlow 3.* project to the latest SpecFlow 4.* version + +## Make a Backup! + +Before upgrading to the latest version, ensure you have a backup of your project (either locally or in a source control system). + +## Cucumber Expressions support, compatibility of existing expressions + +SpecFlow v4 supports [Cucumber Expressions](../Bindings/Cucumber-Expressions.md) natively for [step definitions](../Bindings/Step-Definitions.md). This means that whenever you define a step using the `[Given]`, `[When]` or `[Then]` attribute you can either provide a regular expression for it as a parameter or a cucumber expression. + +Based on the expression you have provided, SpecFlow tries to decide whether it is a regular expression or a cucumber expression. + +Most of your existing regex step definitions will be compatible, because they are either properly recognized as regex or the expression works the same way with both expression types (e.g. simple text without parameters). + +### Invalid expressions after upgrade + +In some cases you may see an error after upgrading to the new SpecFlow version. For example if you had a step definition with an attribute like: + +``` +[When(@"I \$ something")] +``` + +``` +This Cucumber Expression has a problem ... +``` + +In this case the problem is that SpecFlow wrongly identified your expression as a cucumber expression. + +**Solution 1:** Force the expression to be a regular expression by specifying the regex start/end markers (`^`/`$`): + +``` +[When(@"^I \$ something$")] +``` + +**Solution 2:** Change the expression to be a valid cucumber expression. For the example above, you need to remove the masking character (`\`), because the `$` sign does not have to be masked in cucumber expressions: + +``` +[When("I $ something")] +``` + +### Expression matching problems during test execution + +In some very special cases it can happen that the expression is wrongly identified as cucumber expression but you only get the step binding error during test execution (usually "No matching step definition found" error), because the expression is valid as regex and as cucumber expression as well, but with different meaning. + +For example if you had a step definition that matches the step `When I a/b something`, that will be considered as a cucumber expression, but in cucumber expressions, the `/` is used for alternation (so it matches to either `When I a something` or `When I b something`). + +``` +[When(@"I a/b something")] +``` + +**Solutions:** You can apply the same solutions as above: either force it to be a regular expression by specifying the regex start/end markers or make it a valid cucumber expression. + +For the latter case, you would need to mask the `/` character: + +``` +[When(@"I a\/b something")] +``` + +### Cucumber Expression step definition skeletons + +From v4 SpecFlow will by default generate step definition skeletons (snippets) for the new steps. So in case you write a new step as + +``` +When I have 42 cucumbers in my belly +``` + +SpecFlow will suggest the step definition to be: + +``` +[When("I have {int} cucumbers in my belly")] +public void WhenIHaveCucumbersInMyBelly(int p0) +... +``` + +If you would like to use only regular expressions in your project you either have to manually fix the expression, or you can configure SpecFlow to generate skeletons with regular expressions. This you can achieve with the following setting in the `specflow.json` file: + +``` +{ + "$schema": "https://specflow.org/specflow-config.json", + "trace": { + "stepDefinitionSkeletonStyle": "RegexAttribute" + } +} +``` + +### IDE Support for Cucumber Expressions + +Recognition of step definitions using cucumber expressions (including "Go To Definition" navigation) is only supported in Visual Studio 2022, or in Visual Studio 2017/20017 using the "Deveroom" Visual Studio extension. + +In older versions you will see these steps as "undefined step", but the test execution is not affected by this problem. diff --git a/docs/Installation/Breaking-Changes-with-SpecFlow-4.0.md b/docs/Installation/Breaking-Changes-with-SpecFlow-4.0.md new file mode 100644 index 000000000..33fa35298 --- /dev/null +++ b/docs/Installation/Breaking-Changes-with-SpecFlow-4.0.md @@ -0,0 +1,15 @@ +# Breaking changes with SpecFlow 4.0 + +## Cucumber Expressions support, compatibility of existing expressions + +SpecFlow v4 supports [Cucumber Expressions](../Bindings/Cucumber-Expressions.md) natively for [step definitions](../Bindings/Step-Definitions.md). This means that whenever you define a step using the `[Given]`, `[When]` or `[Then]` attribute you can either provide a regular expression for it as a parameter or a cucumber expression. + +Most of your existing regex step definitions will be compatible, because they are either properly recognized as regex or the expression works the same way with both expression types (e.g. simple text without parameters). + +In case your regular expression is wrongly detected as cucumber expression, you can always force to use regular expression by specifying the regex start/end markers (`^`/`$`). + +``` +[When(@"^this expression is treated as a regex$")] +``` + +For potential migration problems of your existing SpecFlow codebase with regular expressions, please check the [Upgrade from SpecFlow 3.* to 4.*](../Guides/UpgradeSpecFlow3To4.md) guide. diff --git a/docs/Installation/Configuration.md b/docs/Installation/Configuration.md index 8ed5851f3..5f57b619e 100644 --- a/docs/Installation/Configuration.md +++ b/docs/Installation/Configuration.md @@ -31,6 +31,7 @@ The following 2 examples show the same option defined in the `specflow.json` and ```json { + "$schema": "https://specflow.org/specflow-config.json", "language": { "feature": "de-AT" } @@ -130,7 +131,7 @@ Use this section to determine the SpecFlow trace output. | traceSuccessfulSteps | true/false | Determines whether SpecFlow should trace successful step binding executions.
**Default:** true | | traceTimings | true/false | Determines whether SpecFlow should trace execution time of the binding methods (only if the execution time is longer than the minTracedDuration value).
**Default:** false | | minTracedDuration | TimeSpan (0:0:0.1) | Specifies a threshold for tracing the binding execution times.
**Default:** 0:0:0.1 (100 ms) | -| stepDefinitionSkeletonStyle | RegexAttribute/MethodNameUnderscores/MethodNamePascalCase/MethodNameRegex | Specifies the default [step definition style](../Bindings/Step-Definitions.html#step-matching-styles-rules).
**Default:** RegexAttribute | +| stepDefinitionSkeletonStyle | CucumberExpressionAttribute/RegexAttribute/MethodNameUnderscores/MethodNamePascalCase/MethodNameRegex | Specifies the default [step definition style](../Bindings/Step-Definitions.html#step-matching-styles-rules).
**Default:** CucumberExpressionAttribute (from v4), RegexAttribute (in v3 or earlier) | | coloredOutput | true/false | Determine whether SpecFlow should color the test result output. See [Color Test Result Output](../Execution/Color-Output.md) for more details
**Default:** false
When this setting is enable you can disable color, for example to run it on a build server that does not support colors, with the environment variable `NO_COLOR=1` | ### `stepAssemblies` @@ -143,6 +144,7 @@ The following example registers an additional binding assembly (MySharedBindings ```json { + "$schema": "https://specflow.org/specflow-config.json", "stepAssemblies": [ { "assembly": "MySharedBindings" diff --git a/docs/index.rst b/docs/index.rst index 6863f0a3b..022251ae5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,6 +60,8 @@ You can find a number of step- by- step guides to start with SpecFlow `here Date: Thu, 7 Jul 2022 16:39:58 +0200 Subject: [PATCH 34/38] Show upgrade guide link in binding error message. --- TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs b/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs index f7881a1bd..0abbe2601 100644 --- a/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs +++ b/TechTalk.SpecFlow/ErrorHandling/ErrorProvider.cs @@ -128,7 +128,11 @@ public Exception GetObsoleteStepError(BindingObsoletion bindingObsoletion) public Exception GetInvalidStepDefinitionError(IStepDefinitionBinding stepDefinitionBinding) { - return new BindingException($"Invalid step definition! The step definition method '{GetMethodText(stepDefinitionBinding.Method)}' is invalid: {stepDefinitionBinding.ErrorMessage}."); + var upgradeMessage = + stepDefinitionBinding.ExpressionType == StepDefinitionExpressionTypes.CucumberExpression ? + $"{Environment.NewLine}If this error comes after upgrading to SpecFlow v4, check the upgrade guide: https://docs.specflow.org/projects/specflow/en/latest/Guides/UpgradeSpecFlow3To4.html" : + ""; + return new BindingException($"Invalid step definition! The step definition method '{GetMethodText(stepDefinitionBinding.Method)}' is invalid: {stepDefinitionBinding.ErrorMessage}.{upgradeMessage}"); } } } \ No newline at end of file From a5102b4d9ca861bfb9a54440a6a867aed760277d Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Thu, 7 Jul 2022 16:50:45 +0200 Subject: [PATCH 35/38] Remove @focus from scenarios --- .../Features/Execution/BasicExecution.feature | 2 +- .../Features/Execution/CucumberExpressions.feature | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature index c3d14d114..4b94582af 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/BasicExecution.feature @@ -1,4 +1,4 @@ -@focus +@basicExecution Feature: Basic scenario execution Background: diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature index a7603bc51..4fab686c8 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -1,4 +1,4 @@ -@focus +@cucumberExpressions Feature: Cucumber expressions Rule: Shoud support Cucumber expressions From b564718600ffebca4ba4110a09ed81fb50d6dfea Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 8 Jul 2022 11:17:04 +0200 Subject: [PATCH 36/38] Update changelog.txt --- changelog.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b519f733a..e6e452db5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,12 @@ -3.11 +4.0 Features: + Add an option to colorize test result output ++ Support for using Cucumber Expressions for step definitions. + +Changes: ++ Existing step definition expressions detected to be either regular or cucumber expression. Check https://docs.specflow.org/projects/specflow/en/latest/Guides/UpgradeSpecFlow3To4.html for potential upgrade issues. ++ Default step definition skeletons are generating cucumber expressions. 3.10 From d11d6500d9802b532851d6ac94a061412a03f4ea Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 8 Jul 2022 11:21:30 +0200 Subject: [PATCH 37/38] fix typo --- .../Features/Execution/CucumberExpressions.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature index 4fab686c8..4af200d5e 100644 --- a/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature +++ b/Tests/TechTalk.SpecFlow.Specs/Features/Execution/CucumberExpressions.feature @@ -1,7 +1,7 @@ @cucumberExpressions Feature: Cucumber expressions -Rule: Shoud support Cucumber expressions +Rule: Should support Cucumber expressions Scenario: Cucumber expresions are used for Step Definitions Given a scenario 'Simple Scenario' as From 083237cc811255afc1544a6c8bcb79898e4eb4aa Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Fri, 8 Jul 2022 14:03:34 +0200 Subject: [PATCH 38/38] fix cucumber expression dependency --- ExternalRepositories/SpecFlow.TestProjectGenerator | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExternalRepositories/SpecFlow.TestProjectGenerator b/ExternalRepositories/SpecFlow.TestProjectGenerator index 1faab9c67..588b2a96d 160000 --- a/ExternalRepositories/SpecFlow.TestProjectGenerator +++ b/ExternalRepositories/SpecFlow.TestProjectGenerator @@ -1 +1 @@ -Subproject commit 1faab9c67a83a337a8bc6fc2ef15294bb3d60fec +Subproject commit 588b2a96d750915e80db3045d1e433238c5c9bc5