From 933eae44b2441fde646d8c81b1eb13c8676eee01 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bychkov Date: Sat, 15 Jun 2024 13:52:58 +0800 Subject: [PATCH 1/4] Added order parameter to stepArgumentTransformation to prioritize execution --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 4 +- TechTalk.SpecFlow/Bindings/BindingRegistry.cs | 34 ++++++------ .../Discovery/BindingSourceProcessor.cs | 5 +- TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 4 +- .../IStepArgumentTransformationBinding.cs | 7 ++- .../StepArgumentTransformationBinding.cs | 11 ++-- .../Bindings/StepArgumentTypeConverter.cs | 7 ++- .../StepArgumentTransformationAttribute.cs | 12 ++++- .../Bindings/BindingRegistryTests.cs | 20 +++++++ .../RuntimeBindingRegistryBuilderTests.cs | 52 +++++++++++++++++-- ....cs => StepArgumentTransformationTests.cs} | 6 +-- .../StepArgumentTypeConverterTest.cs | 50 ++++++++++++++---- changelog.txt | 3 +- docs/Bindings/Step-Argument-Conversions.md | 27 +++++++++- 14 files changed, 192 insertions(+), 50 deletions(-) rename Tests/TechTalk.SpecFlow.RuntimeTests/{StepTransformationTests.cs => StepArgumentTransformationTests.cs} (99%) diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index 9d546534f..dfb4a00bc 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, string parameterTypeName = null) + IBindingMethod bindingMethod, string parameterTypeName = null, int order = default) { - return new StepArgumentTransformationBinding(regexString, bindingMethod, parameterTypeName); + return new StepArgumentTransformationBinding(regexString, bindingMethod, parameterTypeName, order); } } diff --git a/TechTalk.SpecFlow/Bindings/BindingRegistry.cs b/TechTalk.SpecFlow/Bindings/BindingRegistry.cs index 97aca5f5e..ba6c9d74f 100644 --- a/TechTalk.SpecFlow/Bindings/BindingRegistry.cs +++ b/TechTalk.SpecFlow/Bindings/BindingRegistry.cs @@ -24,8 +24,8 @@ public IEnumerable GetConsideredStepDefinitions(StepDefi { //TODO: later optimize to return step definitions that has a chance to match to stepText return _stepDefinitions.Where(sd => sd.StepDefinitionType == stepDefinitionType); - } - + } + public virtual IEnumerable GetHooks() { return _hooks.Values.SelectMany(hookList => hookList); @@ -34,19 +34,19 @@ public virtual IEnumerable GetHooks() public virtual IEnumerable GetHooks(HookType bindingEvent) { return GetHookList(bindingEvent); - } - - private IEnumerable GetHookList(HookType bindingEvent) + } + + private IEnumerable GetHookList(HookType bindingEvent) { - if (_hooks.TryGetValue(bindingEvent, out var list)) - return list; - - return Enumerable.Empty(); - } - + if (_hooks.TryGetValue(bindingEvent, out var list)) + return list; + + return Enumerable.Empty(); + } + public virtual IEnumerable GetStepTransformations() { - return _stepArgumentTransformations; + return _stepArgumentTransformations.OrderBy(s => s.Order); } public IEnumerable GetErrorMessages() @@ -63,8 +63,8 @@ public IEnumerable GetErrorMessages() public virtual void RegisterStepDefinitionBinding(IStepDefinitionBinding stepDefinitionBinding) { _stepDefinitions.Add(stepDefinitionBinding); - } - + } + private List GetHookListForRegister(HookType bindingEvent) { if (!_hooks.TryGetValue(bindingEvent, out var list)) @@ -74,8 +74,8 @@ private List GetHookListForRegister(HookType bindingEvent) } return list; - } - + } + public virtual void RegisterHookBinding(IHookBinding hookBinding) { List hookRegistry = GetHookListForRegister(hookBinding.HookType); @@ -94,4 +94,4 @@ public void RegisterGenericBindingError(BindingError error) _genericBindingErrors.Add(error); } } -} \ No newline at end of file +} diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index 6a4f4866b..7d08c4436 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -199,6 +199,7 @@ private void ProcessStepArgumentTransformationAttribute(BindingSourceMethod bind { string regex = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Regex)); string name = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Name)); + int order = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Order)); var validationResult = ValidateStepArgumentTransformation(bindingSourceMethod, stepArgumentTransformationAttribute); if (!validationResult.IsValid) @@ -207,7 +208,7 @@ private void ProcessStepArgumentTransformationAttribute(BindingSourceMethod bind return; } - var stepArgumentTransformationBinding = _bindingFactory.CreateStepArgumentTransformation(regex, bindingSourceMethod.BindingMethod, name); + var stepArgumentTransformationBinding = _bindingFactory.CreateStepArgumentTransformation(regex, bindingSourceMethod.BindingMethod, name, order); ProcessStepArgumentTransformationBinding(stepArgumentTransformationBinding); } @@ -394,4 +395,4 @@ private void ApplyForScope(BindingScope[] scopes, Action action) } } } -} \ No newline at end of file +} diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index fc5919b9b..3b6c8bfb6 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, string parameterTypeName = null); + IBindingMethod bindingMethod, string parameterTypeName = null, int order = default); } -} \ No newline at end of file +} diff --git a/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs index 87424e706..887042517 100644 --- a/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/IStepArgumentTransformationBinding.cs @@ -16,5 +16,10 @@ public interface IStepArgumentTransformationBinding : IBinding /// The regular expression matches the step argument. Optional, if null, the transformation receives the entire argument. /// Regex Regex { get; } + + /// + /// The deterministic order for step argument transformation + /// + int Order { get; } } -} \ No newline at end of file +} diff --git a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs index cbba326bf..a4c0da000 100644 --- a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs @@ -8,16 +8,19 @@ public class StepArgumentTransformationBinding : MethodBinding, IStepArgumentTra public string Name { get; } public Regex Regex { get; } + + public int Order { get; } - public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod, string name = null) + public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod, string name = null, int order = default) : base(bindingMethod) { Regex = regex; Name = name; + Order = order; } - public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null) - : this(CreateRegexOrNull(regexString), bindingMethod, name) + public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null, int order = default) + : this(CreateRegexOrNull(regexString), bindingMethod, name, order) { } @@ -28,4 +31,4 @@ private static Regex CreateRegexOrNull(string regexString) return RegexFactory.CreateWholeTextRegexForBindings(regexString); } } -} \ No newline at end of file +} diff --git a/TechTalk.SpecFlow/Bindings/StepArgumentTypeConverter.cs b/TechTalk.SpecFlow/Bindings/StepArgumentTypeConverter.cs index 823a615b5..7f1d8b496 100644 --- a/TechTalk.SpecFlow/Bindings/StepArgumentTypeConverter.cs +++ b/TechTalk.SpecFlow/Bindings/StepArgumentTypeConverter.cs @@ -29,7 +29,7 @@ public StepArgumentTypeConverter(ITestTracer testTracer, IBindingRegistry bindin protected virtual IStepArgumentTransformationBinding GetMatchingStepTransformation(object value, IBindingType typeToConvertTo, bool traceWarning) { var stepTransformations = bindingRegistry.GetStepTransformations().Where(t => CanConvert(t, value, typeToConvertTo)).ToArray(); - if (stepTransformations.Length > 1 && traceWarning) + if (traceWarning && HasMultipleTransformationsWithSameOrder(stepTransformations)) { testTracer.TraceWarning($"Multiple step transformation matches to the input ({value}, target type: {typeToConvertTo}). We use the first."); } @@ -37,6 +37,11 @@ protected virtual IStepArgumentTransformationBinding GetMatchingStepTransformati return stepTransformations.Length > 0 ? stepTransformations[0] : null; } + private bool HasMultipleTransformationsWithSameOrder(IStepArgumentTransformationBinding[] transformations) => + transformations + .GroupBy(t => t.Order) + .Any(group => group.Count() > 1); + public async Task ConvertAsync(object value, IBindingType typeToConvertTo, CultureInfo cultureInfo) { if (value == null) throw new ArgumentNullException(nameof(value)); diff --git a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs index 08c946d3c..8ed1357b2 100644 --- a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs +++ b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs @@ -17,9 +17,17 @@ public class StepArgumentTransformationAttribute : Attribute /// public string Name { get; set; } - public StepArgumentTransformationAttribute(string regex) + /// + /// Specifies the deterministic order for step argument transformations. Lower numbers have higher priority. + /// Before .NET 7, step argument transformations with the same priority will execute in a non-deterministic order. + /// Default value is 0. + /// + public int Order { get; set; } + + public StepArgumentTransformationAttribute(string regex, int order = default) { Regex = regex; + Order = order; } public StepArgumentTransformationAttribute() @@ -27,4 +35,4 @@ public StepArgumentTransformationAttribute() Regex = null; } } -} \ No newline at end of file +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs index 5c28425fc..5bc4a799a 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingRegistryTests.cs @@ -40,5 +40,25 @@ public void GetHooks_should_return_all_hooks() result.Should().BeEquivalentTo(new List { hook1, hook2 }); } + + [Fact] + public void GetStepTransformations_should_return_all_step_transformers_in_correct_order() + { + var sut = new BindingRegistry(); + + var sat1 = new StepArgumentTransformationBinding(string.Empty, new Mock().Object); + var sat2 = new StepArgumentTransformationBinding(string.Empty, new Mock().Object); + var sat3 = new StepArgumentTransformationBinding(string.Empty, new Mock().Object, order: 1); + var sat4 = new StepArgumentTransformationBinding(string.Empty, new Mock().Object, null, 2); + + sut.RegisterStepArgumentTransformationBinding(sat4); + sut.RegisterStepArgumentTransformationBinding(sat1); + sut.RegisterStepArgumentTransformationBinding(sat3); + sut.RegisterStepArgumentTransformationBinding(sat2); + + var result = sut.GetStepTransformations(); + + result.Should().BeEquivalentTo(new List { sat1, sat2, sat3, sat4 }); + } } } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 229dda0e7..0b4a1329a 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -18,11 +18,29 @@ public RuntimeBindingRegistryBuilderTests() [Binding] public class StepTransformationExample { - [StepArgumentTransformation("BindingRegistryTests")] - public int Transform(string val) + [StepArgumentTransformation("prop1 .*")] + public int TransformProperty1(string val) { return 42; } + + [StepArgumentTransformation(Regex="prop2 .*")] + public int TransformProperty2(string val) + { + return 43; + } + + [StepArgumentTransformation(Regex="prop3 .*", Order = 5)] + public int TransformProperty3(string val) + { + return 44; + } + + [StepArgumentTransformation(Order = 10)] + public int TransformGlobal(string val) + { + return 45; + } } private BindingSourceProcessorStub bindingSourceProcessorStub; @@ -288,6 +306,34 @@ public void ShouldFindBinding_WithSpecifiedPriorities() s.HookType == HookType.AfterTestRun && s.Method.Name == "AfterOrderTenThousandAnd4" && s.HookOrder == 10004)); } + + [Fact] + public void ShouldFindStepArgumentTransformations_WithSpecifiedOrder() + { + var builder = new RuntimeBindingRegistryBuilder(bindingSourceProcessorStub, new SpecFlowAttributesFilter()); + + BuildCompleteBindingFromType(builder, typeof (StepTransformationExample)); + + Assert.Single( + bindingSourceProcessorStub.StepArgumentTransformationBindings, + sat => + sat.Method.Name == nameof(StepTransformationExample.TransformProperty1) && sat.Order == default); + + Assert.Single( + bindingSourceProcessorStub.StepArgumentTransformationBindings, + sat => + sat.Method.Name == nameof(StepTransformationExample.TransformProperty2) && sat.Order == default); + + Assert.Single( + bindingSourceProcessorStub.StepArgumentTransformationBindings, + sat => + sat.Method.Name == nameof(StepTransformationExample.TransformProperty3) && sat.Order == 5); + + Assert.Single( + bindingSourceProcessorStub.StepArgumentTransformationBindings, + sat => + sat.Method.Name == nameof(StepTransformationExample.TransformGlobal) && sat.Order == 10); + } [Fact] public void ShouldFindExampleConverter() @@ -405,4 +451,4 @@ public void ShouldFindStepDefinitionsWithCustomAttribute() Assert.Equal(0, bindingSourceProcessorStub.StepDefinitionBindings.Count(b => b.StepDefinitionType == StepDefinitionType.Then)); } } -} \ No newline at end of file +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTransformationTests.cs similarity index 99% rename from Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs rename to Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTransformationTests.cs index 0db9266fc..b87f10e3f 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTransformationTests.cs @@ -79,14 +79,14 @@ public Table TableToTableConvert(Table table) } } - public class StepTransformationTests + public class StepArgumentTransformationTests { private readonly Mock bindingRegistryStub = new Mock(); private readonly Mock contextManagerStub = new Mock(); private readonly Mock methodBindingInvokerStub = new Mock(); private readonly List stepTransformations = new List(); - public StepTransformationTests() + public StepArgumentTransformationTests() { // ScenarioContext is needed, because the [Binding]-instances live there var scenarioContext = new ScenarioContext(new ObjectContainer(), null, new TestObjectResolver()); @@ -250,4 +250,4 @@ public async Task ShouldUseStepArgumentTransformationToConvertTable() } } -} \ No newline at end of file +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTypeConverterTest.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTypeConverterTest.cs index 76ab65ae1..25c59ee05 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTypeConverterTest.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepArgumentTypeConverterTest.cs @@ -1,32 +1,34 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; using FluentAssertions; using Moq; using Xunit; using TechTalk.SpecFlow.Bindings; +using TechTalk.SpecFlow.Bindings.Reflection; using TechTalk.SpecFlow.Infrastructure; using TechTalk.SpecFlow.Tracing; namespace TechTalk.SpecFlow.RuntimeTests { - public class StepArgumentTypeConverterTests { - private IStepArgumentTypeConverter _stepArgumentTypeConverter; - private readonly Mock methodBindingInvokerStub = new Mock(); - private CultureInfo _enUSCulture; + private readonly Mock _testTracer; + private readonly List _stepTransformations; + private readonly IStepArgumentTypeConverter _stepArgumentTypeConverter; + private readonly Mock methodBindingInvokerStub = new(); + private readonly CultureInfo _enUSCulture; public StepArgumentTypeConverterTests() { Mock bindingRegistryStub = new Mock(); - List stepTransformations = new List(); - bindingRegistryStub.Setup(br => br.GetStepTransformations()).Returns(stepTransformations); + _stepTransformations = new List(); + bindingRegistryStub.Setup(br => br.GetStepTransformations()).Returns(_stepTransformations); + _testTracer = new Mock(); - _stepArgumentTypeConverter = new StepArgumentTypeConverter(new Mock().Object, bindingRegistryStub.Object, new Mock().Object, methodBindingInvokerStub.Object); + _stepArgumentTypeConverter = new StepArgumentTypeConverter(_testTracer.Object, bindingRegistryStub.Object, new Mock().Object, methodBindingInvokerStub.Object); _enUSCulture = new CultureInfo("en-US", false); } @@ -121,6 +123,31 @@ public async Task ShouldUseATypeConverterWhenAvailable() result.As().Time.Should().Be(originalValue); } + [Fact] + public async Task ShouldTraceWarningIfMultipleTransformationsFound() + { + var method = typeof(TestClass).GetMethod(nameof(TestClass.StringToIntConverter)); + _stepTransformations.Add(new StepArgumentTransformationBinding(@"\d+", new RuntimeBindingMethod(method))); + _stepTransformations.Add(new StepArgumentTransformationBinding(@".*", new RuntimeBindingMethod(method))); + + await _stepArgumentTypeConverter.ConvertAsync("1", typeof(int), _enUSCulture); + + _testTracer.Verify(c => c.TraceWarning(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ShouldNotTraceWarningIfTransformationsHaveDifferentOrder() + { + var method = typeof(TestClass).GetMethod(nameof(TestClass.StringToIntConverter)); + + _stepTransformations.Add(new StepArgumentTransformationBinding(@"\d+", new RuntimeBindingMethod(method))); + _stepTransformations.Add(new StepArgumentTransformationBinding(@".*", new RuntimeBindingMethod(method), order: 10)); + + await _stepArgumentTypeConverter.ConvertAsync("1", typeof(int), _enUSCulture); + + _testTracer.Verify(c => c.TraceWarning(It.IsAny()), Times.Never); + } + [TypeConverter(typeof(TessClassTypeConverter))] class TestClass { @@ -138,8 +165,9 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c return new TestClass { Time = (DateTimeOffset) value }; } } - } - + [StepArgumentTransformation] + public int StringToIntConverter(string value) => int.Parse(value); + } } -} \ No newline at end of file +} diff --git a/changelog.txt b/changelog.txt index 716b4584a..bc523e2d0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,7 +4,7 @@ Breaking Changes: + Removed the ability to call steps from steps via string - https://github.com/SpecFlowOSS/SpecFlow/issues/1733 + Removed .NET Core 2.1 support (min .NET Core version: 3.1) + Removed .NET Framework 4.6.1 support (min .NET Framework version: 4.6.2) -+ Bindigns declared as 'async void' are not allowed. Use 'async Task' instead. ++ Bindings declared as 'async void' are not allowed. Use 'async Task' instead. Features: + Add an option to colorize test result output @@ -14,6 +14,7 @@ Features: + Support for ValueTask and ValueTask binding methods (step definitions, hooks, step argument transformations) + Rules now support Background blocks + Collect binding errors (type load, binding, step definition) and report them as exception when any of the tests are executed. ++ Support 'Order' parameter for `StepArgumentTransformationAttribute` to prioritize execution 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. diff --git a/docs/Bindings/Step-Argument-Conversions.md b/docs/Bindings/Step-Argument-Conversions.md index f78f29b28..3e68afb47 100644 --- a/docs/Bindings/Step-Argument-Conversions.md +++ b/docs/Bindings/Step-Argument-Conversions.md @@ -17,7 +17,7 @@ A step argument transformation is used to convert an argument if: * The return type of the transformation is the same as the parameter type * The regular expression (if specified) matches the original (string) argument -**Note:** If multiple matching transformation are available, a warning is output in the trace and the first transformation is used. +**Note:** If multiple matching transformations are available and no order is specified, a warning is output in the trace and the first transformation is used. If the order is specified, the transformation with the lowest order value is used. The following example transforms a relative period of time (`in 3 days`) into a `DateTime` structure. @@ -76,6 +76,31 @@ public class Transforms } ``` +The following example transforms a string argument to a Rating model. If regex matches the expression, the given rating score will be parsed. Otherwise, the default rating will be used. + +```c# +[Binding] +public class Transforms +{ + [StepArgumentTransformation(@"with (\d+) score")] + public Rating RatingTransformation(int score) + { + return new Rating(score); + } + + [StepArgumentTransformation(Order = 10)] + public Rating GlobalRatingTransformation(string input) + { + return Rating.DefaultRating; + } +} + +public record Rating(int Value) +{ + public static Rating DefaultRating => new Rating(50); +} +``` + ## Standard Conversion A standard conversion is performed by SpecFlow in the following cases: From b1e2316c25f0a6c87896c8657f8674f6df63d732 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bychkov Date: Sat, 22 Jun 2024 10:06:06 +0800 Subject: [PATCH 2/4] Fixed broken unit tests due to the changed StepArgumentTransformation regex --- .../RuntimeBindingRegistryBuilderTests.cs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 0b4a1329a..995a006aa 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -18,29 +18,23 @@ public RuntimeBindingRegistryBuilderTests() [Binding] public class StepTransformationExample { - [StepArgumentTransformation("prop1 .*")] - public int TransformProperty1(string val) + [StepArgumentTransformation("BindingRegistryTests")] + public int Transform(string val) { return 42; } - [StepArgumentTransformation(Regex="prop2 .*")] - public int TransformProperty2(string val) + [StepArgumentTransformation(Regex="BindingRegistryTests2", Order = 5)] + public int TransformWithRegexAndOrder(string val) { return 43; } - [StepArgumentTransformation(Regex="prop3 .*", Order = 5)] - public int TransformProperty3(string val) - { - return 44; - } - [StepArgumentTransformation(Order = 10)] - public int TransformGlobal(string val) + public int TransformWithOrderAndWithoutRegex(string val) { - return 45; - } + return 44; + } } private BindingSourceProcessorStub bindingSourceProcessorStub; @@ -317,22 +311,17 @@ public void ShouldFindStepArgumentTransformations_WithSpecifiedOrder() Assert.Single( bindingSourceProcessorStub.StepArgumentTransformationBindings, sat => - sat.Method.Name == nameof(StepTransformationExample.TransformProperty1) && sat.Order == default); - - Assert.Single( - bindingSourceProcessorStub.StepArgumentTransformationBindings, - sat => - sat.Method.Name == nameof(StepTransformationExample.TransformProperty2) && sat.Order == default); + sat.Method.Name == nameof(StepTransformationExample.Transform) && sat.Order == default); Assert.Single( bindingSourceProcessorStub.StepArgumentTransformationBindings, sat => - sat.Method.Name == nameof(StepTransformationExample.TransformProperty3) && sat.Order == 5); + sat.Method.Name == nameof(StepTransformationExample.TransformWithRegexAndOrder) && sat.Order == 5); Assert.Single( bindingSourceProcessorStub.StepArgumentTransformationBindings, sat => - sat.Method.Name == nameof(StepTransformationExample.TransformGlobal) && sat.Order == 10); + sat.Method.Name == nameof(StepTransformationExample.TransformWithOrderAndWithoutRegex) && sat.Order == 10); } [Fact] From c033788375318054ed988fc33851d705284f7632 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bychkov Date: Sat, 6 Jul 2024 11:09:33 +0800 Subject: [PATCH 3/4] Added the constant for the default step argument transformer order --- TechTalk.SpecFlow/Bindings/BindingFactory.cs | 3 ++- .../Bindings/Discovery/BindingSourceProcessor.cs | 2 +- TechTalk.SpecFlow/Bindings/IBindingFactory.cs | 3 ++- .../Bindings/StepArgumentTransformationBinding.cs | 6 ++++-- TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs | 7 ++++--- .../RuntimeBindingRegistryBuilderTests.cs | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/BindingFactory.cs b/TechTalk.SpecFlow/Bindings/BindingFactory.cs index dfb4a00bc..b2f1c9179 100644 --- a/TechTalk.SpecFlow/Bindings/BindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/BindingFactory.cs @@ -30,7 +30,8 @@ public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefi } public IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, - IBindingMethod bindingMethod, string parameterTypeName = null, int order = default) + IBindingMethod bindingMethod, string parameterTypeName = null, + int order = StepArgumentTransformationAttribute.DefaultOrder) { return new StepArgumentTransformationBinding(regexString, bindingMethod, parameterTypeName, order); } diff --git a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs index 7d08c4436..663453901 100644 --- a/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs +++ b/TechTalk.SpecFlow/Bindings/Discovery/BindingSourceProcessor.cs @@ -199,7 +199,7 @@ private void ProcessStepArgumentTransformationAttribute(BindingSourceMethod bind { string regex = stepArgumentTransformationAttribute.TryGetAttributeValue(0) ?? stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Regex)); string name = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Name)); - int order = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Order)); + int order = stepArgumentTransformationAttribute.TryGetAttributeValue(nameof(StepArgumentTransformationAttribute.Order), StepArgumentTransformationAttribute.DefaultOrder); var validationResult = ValidateStepArgumentTransformation(bindingSourceMethod, stepArgumentTransformationAttribute); if (!validationResult.IsValid) diff --git a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs index 3b6c8bfb6..e2dbbd29f 100644 --- a/TechTalk.SpecFlow/Bindings/IBindingFactory.cs +++ b/TechTalk.SpecFlow/Bindings/IBindingFactory.cs @@ -11,6 +11,7 @@ IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionT BindingScope bindingScope, string expressionString); IStepArgumentTransformationBinding CreateStepArgumentTransformation(string regexString, - IBindingMethod bindingMethod, string parameterTypeName = null, int order = default); + IBindingMethod bindingMethod, string parameterTypeName = null, + int order = StepArgumentTransformationAttribute.DefaultOrder); } } diff --git a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs index a4c0da000..a3a0c6a74 100644 --- a/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs +++ b/TechTalk.SpecFlow/Bindings/StepArgumentTransformationBinding.cs @@ -11,7 +11,8 @@ public class StepArgumentTransformationBinding : MethodBinding, IStepArgumentTra public int Order { get; } - public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod, string name = null, int order = default) + public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMethod, string name = null, + int order = StepArgumentTransformationAttribute.DefaultOrder) : base(bindingMethod) { Regex = regex; @@ -19,7 +20,8 @@ public StepArgumentTransformationBinding(Regex regex, IBindingMethod bindingMeth Order = order; } - public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null, int order = default) + public StepArgumentTransformationBinding(string regexString, IBindingMethod bindingMethod, string name = null, + int order = StepArgumentTransformationAttribute.DefaultOrder) : this(CreateRegexOrNull(regexString), bindingMethod, name, order) { } diff --git a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs index 8ed1357b2..fdfc83738 100644 --- a/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs +++ b/TechTalk.SpecFlow/StepArgumentTransformationAttribute.cs @@ -19,12 +19,13 @@ public class StepArgumentTransformationAttribute : Attribute /// /// Specifies the deterministic order for step argument transformations. Lower numbers have higher priority. - /// Before .NET 7, step argument transformations with the same priority will execute in a non-deterministic order. - /// Default value is 0. + /// Default value is 10000. /// public int Order { get; set; } + + public const int DefaultOrder = 10000; - public StepArgumentTransformationAttribute(string regex, int order = default) + public StepArgumentTransformationAttribute(string regex, int order = DefaultOrder) { Regex = regex; Order = order; diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 995a006aa..970ac369e 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -311,7 +311,7 @@ public void ShouldFindStepArgumentTransformations_WithSpecifiedOrder() Assert.Single( bindingSourceProcessorStub.StepArgumentTransformationBindings, sat => - sat.Method.Name == nameof(StepTransformationExample.Transform) && sat.Order == default); + sat.Method.Name == nameof(StepTransformationExample.Transform) && sat.Order == StepArgumentTransformationAttribute.DefaultOrder); Assert.Single( bindingSourceProcessorStub.StepArgumentTransformationBindings, From f139dcf881c738df1b200cdd06ef5c990536e95c Mon Sep 17 00:00:00 2001 From: Sviatoslav Bychkov Date: Sat, 6 Jul 2024 11:13:03 +0800 Subject: [PATCH 4/4] Described the default order in the step argument conversion doc page --- docs/Bindings/Step-Argument-Conversions.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/Bindings/Step-Argument-Conversions.md b/docs/Bindings/Step-Argument-Conversions.md index 3e68afb47..483269cff 100644 --- a/docs/Bindings/Step-Argument-Conversions.md +++ b/docs/Bindings/Step-Argument-Conversions.md @@ -17,7 +17,7 @@ A step argument transformation is used to convert an argument if: * The return type of the transformation is the same as the parameter type * The regular expression (if specified) matches the original (string) argument -**Note:** If multiple matching transformations are available and no order is specified, a warning is output in the trace and the first transformation is used. If the order is specified, the transformation with the lowest order value is used. +**Note:** If multiple matching transformations are available, a warning is output in the trace and the first transformation is used. The following example transforms a relative period of time (`in 3 days`) into a `DateTime` structure. @@ -76,19 +76,23 @@ public class Transforms } ``` +By default, selection among matching step argument transformations is undeterministic. +To specify selection order, use the `Order` property in the `StepArgumentTransformation` attribute, where the transformation with lower numbers takes precedence. +If no order is specified, the default value is 10000. + The following example transforms a string argument to a Rating model. If regex matches the expression, the given rating score will be parsed. Otherwise, the default rating will be used. ```c# [Binding] public class Transforms { - [StepArgumentTransformation(@"with (\d+) score")] + [StepArgumentTransformation(@"with (\d+) score", Order = 1)] public Rating RatingTransformation(int score) { return new Rating(score); } - [StepArgumentTransformation(Order = 10)] + [StepArgumentTransformation] public Rating GlobalRatingTransformation(string input) { return Rating.DefaultRating;