Skip to content

Commit

Permalink
Support for ValueTask and ValueTask<T> async binding methods. (#2661)
Browse files Browse the repository at this point in the history
  • Loading branch information
gasparnagy authored Oct 24, 2022
1 parent 4c3489e commit 52a3e39
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 12 deletions.
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.

0 comments on commit 52a3e39

Please sign in to comment.