From 69bddc6b81f48142ccb0ab51804817a481e2b26f Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 24 Oct 2022 11:30:14 +0200 Subject: [PATCH 1/5] Add unit tests --- .../Bindings/BindingInvokerTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs index d8a496857..94e270d5c 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs @@ -85,4 +85,53 @@ public async Task Sample_binding_invoker_test() var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve(); stepDefClass.CapturedValue.Should().Be("42"); } + + #region ValueTask related tests + + class StepDefClassWithValueTask + { + public bool WasInvokedAsyncValueTaskStepDef = false; + public bool WasInvokedAsyncValueTaskOfTStepDef = false; + + public async ValueTask AsyncValueTaskStepDef() + { + await Task.Delay(50); // we need to wait a bit otherwise the assertion passes even if the method is called sync + WasInvokedAsyncValueTaskStepDef = true; + } + + public async ValueTask AsyncValueTaskOfTStepDef() + { + await Task.Delay(50); // we need to wait a bit otherwise the assertion passes even if the method is called sync + WasInvokedAsyncValueTaskOfTStepDef = true; + return "foo"; + } + } + + [Fact] + public async Task Async_methods_of_ValueTask_return_type_can_be_invoked() + { + var sut = CreateSut(); + var contextManager = CreateContextManagerWith(); + + // call step definition methods + await InvokeBindingAsync(sut, contextManager, typeof(StepDefClassWithValueTask), nameof(StepDefClassWithValueTask.AsyncValueTaskStepDef)); + + // this is how to get THE instance of the step definition class + var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve(); + stepDefClass.WasInvokedAsyncValueTaskStepDef.Should().BeTrue(); + } + + [Fact] + public async Task Async_methods_of_ValueTaskOfT_return_type_can_be_invoked() + { + var sut = CreateSut(); + var contextManager = CreateContextManagerWith(); + + await InvokeBindingAsync(sut, contextManager, typeof(StepDefClassWithValueTask), nameof(StepDefClassWithValueTask.AsyncValueTaskOfTStepDef)); + + var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve(); + stepDefClass.WasInvokedAsyncValueTaskOfTStepDef.Should().BeTrue(); + } + + #endregion } \ No newline at end of file From 7cec1078068d8690855a3e55016fcae840f21738 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 24 Oct 2022 12:07:11 +0200 Subject: [PATCH 2/5] Add ValueTask support for .NET Core --- .../Bindings/AsyncMethodHelper.cs | 45 ++++++++++++++++++- .../Bindings/BindingDelegateInvoker.cs | 10 ++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs b/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs index dbddc9ed2..5ee3ee672 100644 --- a/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs +++ b/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs @@ -6,7 +6,10 @@ namespace TechTalk.SpecFlow.Bindings { internal static class AsyncMethodHelper { - private static readonly IBindingType TaskOfT = new RuntimeBindingType(typeof(Task<>)); + private static bool IsTask(Type type) + { + return typeof(Task).IsAssignableFrom(type); + } private static bool IsTaskOfT(Type type, out Type typeArg) { @@ -19,6 +22,15 @@ private static bool IsTaskOfT(Type type, out Type typeArg) return isTaskOfT; } + private static bool IsValueTask(Type type) + { +#if NETFRAMEWORK + return false; +#else + return typeof(ValueTask).IsAssignableFrom(type) || IsValueTaskOfT(type, out _); +#endif + } + private static bool IsValueTaskOfT(Type type, out Type typeArg) { typeArg = null; @@ -39,6 +51,37 @@ private static bool IsAwaitableOfT(Type type, out Type typeArg) return IsTaskOfT(type, out typeArg) || IsValueTaskOfT(type, out typeArg); } + public static bool IsAwaitable(Type type) + { + return IsTask(type) || IsValueTask(type); + } + + public static bool IsAwaitableAsTask(object obj, out Task task) + { + if (obj is Task taskObject) + { + task = taskObject; + return true; + } +#if !NETFRAMEWORK + if (obj is ValueTask valueTask) + { + task = valueTask.AsTask(); + return true; + } + if (obj != null && IsValueTaskOfT(obj.GetType(), out _)) + { + // unfortunately there is no base class/interface of the Value types, so we can only call the "AsTask" method via reflection + var asTaskMethod = obj.GetType().GetMethod(nameof(ValueTask.AsTask)); + task = (Task)asTaskMethod!.Invoke(obj, Array.Empty()); + return true; + } +#endif + + task = null; + return false; + } + public static IBindingType GetAwaitableReturnType(this IBindingMethod bindingMethod) { var returnType = bindingMethod.ReturnType; diff --git a/TechTalk.SpecFlow/Bindings/BindingDelegateInvoker.cs b/TechTalk.SpecFlow/Bindings/BindingDelegateInvoker.cs index cbd822d0a..611483769 100644 --- a/TechTalk.SpecFlow/Bindings/BindingDelegateInvoker.cs +++ b/TechTalk.SpecFlow/Bindings/BindingDelegateInvoker.cs @@ -7,7 +7,7 @@ public class BindingDelegateInvoker : IBindingDelegateInvoker { public virtual async Task InvokeDelegateAsync(Delegate bindingDelegate, object[] invokeArgs) { - if (typeof(Task).IsAssignableFrom(bindingDelegate.Method.ReturnType)) + if (AsyncMethodHelper.IsAwaitable(bindingDelegate.Method.ReturnType)) return await InvokeBindingDelegateAsync(bindingDelegate, invokeArgs); return InvokeBindingDelegateSync(bindingDelegate, invokeArgs); } @@ -19,10 +19,10 @@ protected virtual object InvokeBindingDelegateSync(Delegate bindingDelegate, obj protected virtual async Task InvokeBindingDelegateAsync(Delegate bindingDelegate, object[] invokeArgs) { - var r = bindingDelegate.DynamicInvoke(invokeArgs); - if (r is Task t) - await t; - return r; + var result = bindingDelegate.DynamicInvoke(invokeArgs); + if (AsyncMethodHelper.IsAwaitableAsTask(result, out var task)) + await task; + return result; } } } From cf0dcc92b8d22d4aa0dfb3e663efe7a2cc2f6935 Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 24 Oct 2022 12:13:22 +0200 Subject: [PATCH 3/5] Extend docs and changelog --- changelog.txt | 1 + docs/Bindings/Asynchronous-Bindings.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 8beb54442..f5bdf5c9d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -10,6 +10,7 @@ Features: + Support for using Cucumber Expressions for step definitions. + Support Rule tags (can be used for hook filters, scoping and access through 'ScenarioInfo.CombinedTags') + Support for async step argument transformations. Fixes #2230 ++ Support for ValueTask and ValueTask binding methods (step definitions, hooks, step argument transformations) 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/Asynchronous-Bindings.md b/docs/Bindings/Asynchronous-Bindings.md index 8addcd741..cb928a67f 100644 --- a/docs/Bindings/Asynchronous-Bindings.md +++ b/docs/Bindings/Asynchronous-Bindings.md @@ -22,3 +22,5 @@ public async Task HttpClientGet(string url) ``` From SpecFlow v4 you can also use asynchronous [step argument transformations](Step-Argument-Conversions.md). + +From SpecFlow v4 you can also use [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-6.0) and [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-6.0) return types (except for .NET Framework). \ No newline at end of file From a362c0004eaf92555f24d390afc6cc3d040d547c Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 24 Oct 2022 13:13:45 +0200 Subject: [PATCH 4/5] Fix unit tests for .NET 4 --- .../Bindings/BindingInvokerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs index 94e270d5c..56effdca1 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs @@ -107,6 +107,7 @@ public async ValueTask AsyncValueTaskOfTStepDef() } } + #if !NETFRAMEWORK [Fact] public async Task Async_methods_of_ValueTask_return_type_can_be_invoked() { @@ -132,6 +133,7 @@ public async Task Async_methods_of_ValueTaskOfT_return_type_can_be_invoked() var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve(); stepDefClass.WasInvokedAsyncValueTaskOfTStepDef.Should().BeTrue(); } + #endif #endregion } \ No newline at end of file From a93c9232970ea134bdfaa9ddfe361ae886cb9b6a Mon Sep 17 00:00:00 2001 From: Gaspar Nagy Date: Mon, 24 Oct 2022 14:29:57 +0200 Subject: [PATCH 5/5] Support for ValueTask binding methods in .NET 4 --- TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs | 10 ---------- TechTalk.SpecFlow/TechTalk.SpecFlow.csproj | 1 + .../Bindings/BindingInvokerTests.cs | 2 -- .../StepTransformationTests.cs | 2 -- docs/Bindings/Asynchronous-Bindings.md | 2 +- 5 files changed, 2 insertions(+), 15 deletions(-) diff --git a/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs b/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs index 5ee3ee672..5a50c29f5 100644 --- a/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs +++ b/TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs @@ -24,26 +24,18 @@ private static bool IsTaskOfT(Type type, out Type typeArg) private static bool IsValueTask(Type type) { -#if NETFRAMEWORK - return false; -#else return typeof(ValueTask).IsAssignableFrom(type) || IsValueTaskOfT(type, out _); -#endif } private static bool IsValueTaskOfT(Type type, out Type typeArg) { typeArg = null; -#if NETFRAMEWORK - return false; -#else var isTaskOfT = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>); if (isTaskOfT) { typeArg = type.GetGenericArguments()[0]; } return isTaskOfT; -#endif } private static bool IsAwaitableOfT(Type type, out Type typeArg) @@ -63,7 +55,6 @@ public static bool IsAwaitableAsTask(object obj, out Task task) task = taskObject; return true; } -#if !NETFRAMEWORK if (obj is ValueTask valueTask) { task = valueTask.AsTask(); @@ -76,7 +67,6 @@ public static bool IsAwaitableAsTask(object obj, out Task task) task = (Task)asTaskMethod!.Invoke(obj, Array.Empty()); return true; } -#endif task = null; return false; diff --git a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj index 50976c90c..1eb2caa8d 100644 --- a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj +++ b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj @@ -43,6 +43,7 @@ + diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs index 56effdca1..94e270d5c 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Bindings/BindingInvokerTests.cs @@ -107,7 +107,6 @@ public async ValueTask AsyncValueTaskOfTStepDef() } } - #if !NETFRAMEWORK [Fact] public async Task Async_methods_of_ValueTask_return_type_can_be_invoked() { @@ -133,7 +132,6 @@ public async Task Async_methods_of_ValueTaskOfT_return_type_can_be_invoked() var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve(); stepDefClass.WasInvokedAsyncValueTaskOfTStepDef.Should().BeTrue(); } - #endif #endregion } \ No newline at end of file diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs index c0d5ebe77..0db9266fc 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs @@ -202,7 +202,6 @@ public async Task StepArgumentTypeConverterShouldUseAsyncUserConverterForConvers result.Should().Be(resultUser); } -#if !NETFRAMEWORK [Fact] public async Task StepArgumentTypeConverterShouldUseAsyncValueTaskUserConverterForConversion() { @@ -220,7 +219,6 @@ public async Task StepArgumentTypeConverterShouldUseAsyncValueTaskUserConverterF var result = await stepArgumentTypeConverter.ConvertAsync("user xyz", typeof(User), new CultureInfo("en-US", false)); result.Should().Be(resultUser); } -#endif private StepArgumentTypeConverter CreateStepArgumentTypeConverter() { diff --git a/docs/Bindings/Asynchronous-Bindings.md b/docs/Bindings/Asynchronous-Bindings.md index cb928a67f..ced558c89 100644 --- a/docs/Bindings/Asynchronous-Bindings.md +++ b/docs/Bindings/Asynchronous-Bindings.md @@ -23,4 +23,4 @@ public async Task HttpClientGet(string url) From SpecFlow v4 you can also use asynchronous [step argument transformations](Step-Argument-Conversions.md). -From SpecFlow v4 you can also use [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-6.0) and [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-6.0) return types (except for .NET Framework). \ No newline at end of file +From SpecFlow v4 you can also use [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-6.0) and [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-6.0) return types.