Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ValueTask and ValueTask<T> async binding methods. #2661

Merged
merged 5 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -19,26 +22,56 @@ private static bool IsTaskOfT(Type type, out Type typeArg)
return isTaskOfT;
}

private static bool IsValueTask(Type type)
{
return typeof(ValueTask).IsAssignableFrom(type) || IsValueTaskOfT(type, out _);
}

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)
{
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 (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<T> types, so we can only call the "AsTask" method via reflection
var asTaskMethod = obj.GetType().GetMethod(nameof(ValueTask<object>.AsTask));
task = (Task)asTaskMethod!.Invoke(obj, Array.Empty<object>());
return true;
}

task = null;
return false;
}

public static IBindingType GetAwaitableReturnType(this IBindingMethod bindingMethod)
{
var returnType = bindingMethod.ReturnType;
Expand Down
10 changes: 5 additions & 5 deletions TechTalk.SpecFlow/Bindings/BindingDelegateInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class BindingDelegateInvoker : IBindingDelegateInvoker
{
public virtual async Task<object> 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);
}
Expand All @@ -19,10 +19,10 @@ protected virtual object InvokeBindingDelegateSync(Delegate bindingDelegate, obj

protected virtual async Task<object> 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;
}
}
}
1 change: 1 addition & 0 deletions TechTalk.SpecFlow/TechTalk.SpecFlow.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<Reference Include="System.Net.Http" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == '$(SpecFlow_Core_Runtime_TFM)' Or '$(TargetFramework)' == '$(SpecFlow_Net6_TFM)'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,53 @@ public async Task Sample_binding_invoker_test()
var stepDefClass = contextManager.ScenarioContext.ScenarioContainer.Resolve<SampleStepDefClass>();
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<string> 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<StepDefClassWithValueTask>();
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<StepDefClassWithValueTask>();
stepDefClass.WasInvokedAsyncValueTaskOfTStepDef.Should().BeTrue();
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ public async Task StepArgumentTypeConverterShouldUseAsyncUserConverterForConvers
result.Should().Be(resultUser);
}

#if !NETFRAMEWORK
[Fact]
public async Task StepArgumentTypeConverterShouldUseAsyncValueTaskUserConverterForConversion()
{
Expand All @@ -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()
{
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/Bindings/Asynchronous-Bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-6.0) return types.