Skip to content
Closed
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
22 changes: 22 additions & 0 deletions Examples/ByFeature/AsyncSteps/AsyncSteps.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\netfx.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net452</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="NunitWiring.fs" />
<EmbeddedResource Include="Web.feature" />
<Compile Include="WebSteps.fs" />
<EmbeddedResource Include="Time.feature" />
<Compile Include="TimeSteps.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\TickSpec\TickSpec.fsproj" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="TaskBuilder.fs" Version="2.1.0" />
</ItemGroup>
</Project>
46 changes: 46 additions & 0 deletions Examples/ByFeature/AsyncSteps/NunitWiring.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module NUnit.TickSpec

open TickSpec
open NUnit.Framework

open System.Reflection
open System.Runtime.ExceptionServices

/// Class containing all BDD tests in current assembly as NUnit unit tests
[<TestFixture>]
type FeatureFixture () =
/// Test method for all BDD tests in current assembly as NUnit unit tests
[<Test>]
[<TestCaseSource("Scenarios")>]
member __.Bdd (scenario:Scenario) =
if scenario.Tags |> Seq.exists ((=) "ignore") then
raise (new IgnoreException("Ignored: " + scenario.ToString()))
try
scenario.Action.Invoke()
with
| :? TargetInvocationException as ex -> ExceptionDispatchInfo.Capture(ex.InnerException).Throw()

/// All test scenarios from feature files in current assembly
static member Scenarios =
let createFeatureData (feature:Feature) =
let createTestCaseData (feature:Feature) (scenario:Scenario) =
let enhanceScenarioName parameters scenarioName =
let replaceParameterInScenarioName (scenarioName:string) parameter =
scenarioName.Replace("<" + fst parameter + ">", snd parameter)
parameters
|> Seq.fold replaceParameterInScenarioName scenarioName
(new TestCaseData(scenario))
.SetName(enhanceScenarioName scenario.Parameters scenario.Name)
.SetProperty("Feature", feature.Name)
|> Seq.foldBack (fun (tag:string) data -> data.SetProperty("Tag", tag)) scenario.Tags
feature.Scenarios
|> Seq.map (createTestCaseData feature)

let assembly = Assembly.GetExecutingAssembly()
let definitions = new StepDefinitions(assembly.GetTypes())

assembly.GetManifestResourceNames()
|> Seq.filter (fun (n:string) -> n.EndsWith(".feature") )
|> Seq.collect (fun n ->
definitions.GenerateFeature(n, assembly.GetManifestResourceStream(n))
|> createFeatureData)
7 changes: 7 additions & 0 deletions Examples/ByFeature/AsyncSteps/Time.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Feature: Time

Scenario: Time
Given having current time
When I sleep for 20ms using Async
And I sleep for 20ms using Tasks
Then the current time is at least 40ms higher than it was
27 changes: 27 additions & 0 deletions Examples/ByFeature/AsyncSteps/TimeSteps.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module TimeSteps

open NUnit.Framework
open TickSpec
open System
open FSharp.Control.Tasks.V2.ContextInsensitive
open System.Threading.Tasks

type Time =
| Time of DateTime

let [<Given>] ``having current time`` () =
Time DateTime.Now

let [<When>] ``I sleep for (\d*)ms using Async`` (duration: int) =
async {
do! Async.Sleep duration
}

let [<When>] ``I sleep for (\d*)ms using Tasks`` (duration: int) =
task {
do! Task.Delay duration
}

let [<Then>] ``the current time is at least (\d*)ms higher than it was`` (duration: int) (Time previousCurrentTime) =
int (DateTime.Now - previousCurrentTime).TotalMilliseconds >= duration
|> Assert.True
9 changes: 9 additions & 0 deletions Examples/ByFeature/AsyncSteps/Web.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Feature: Web requests

Scenario: Google contains Google
When I download https://www.google.com/ web page using Async
Then the downloaded page contains "Google"

Scenario: Bing contains Bing
When I download https://www.bing.com/ web page using Tasks
Then the downloaded page contains "Bing"
33 changes: 33 additions & 0 deletions Examples/ByFeature/AsyncSteps/WebSteps.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module WebSteps

open NUnit.Framework
open TickSpec
open System.Net
open System
open FSharp.Control.Tasks.V2.ContextInsensitive

type DownloadedPage =
| DownloadedPage of string

let [<When>] ``I download (.*) web page using Async`` address =
async {
let req = WebRequest.Create(Uri address)
use! resp = req.AsyncGetResponse()
use stream = resp.GetResponseStream()
use reader = new IO.StreamReader(stream)
return reader.ReadToEnd() |> DownloadedPage
}

let [<When>] ``I download (.*) web page using Tasks`` address =
task {
let req = WebRequest.Create(Uri address)
use! resp = req.GetResponseAsync()
use stream = resp.GetResponseStream()
use reader = new IO.StreamReader(stream)
return reader.ReadToEnd() |> DownloadedPage
}

let [<Then>] ``the downloaded page contains "(.*)"`` (text: string) (DownloadedPage page) =
page.Contains(text)
|> Assert.True

7 changes: 7 additions & 0 deletions TickSpec.sln
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TickSpec.Tests", "TickSpec.
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TaggedExamples", "Examples\ByFeature\TaggedExamples\TaggedExamples.fsproj", "{39C63F8E-F3A5-48D8-851C-62BEB9C701C2}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "AsyncSteps", "Examples\ByFeature\AsyncSteps\AsyncSteps.fsproj", "{B1CD5BF8-4A92-4005-909F-BCBFDE35D150}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -124,6 +126,10 @@ Global
{39C63F8E-F3A5-48D8-851C-62BEB9C701C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39C63F8E-F3A5-48D8-851C-62BEB9C701C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39C63F8E-F3A5-48D8-851C-62BEB9C701C2}.Release|Any CPU.Build.0 = Release|Any CPU
{B1CD5BF8-4A92-4005-909F-BCBFDE35D150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1CD5BF8-4A92-4005-909F-BCBFDE35D150}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1CD5BF8-4A92-4005-909F-BCBFDE35D150}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1CD5BF8-4A92-4005-909F-BCBFDE35D150}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -148,6 +154,7 @@ Global
{6DD7F109-6798-4CAF-BD13-7BA4689BD892} = {EB1F6262-0913-4464-A034-4F4D01268E83}
{1CE6B475-C94F-438E-97D4-5E99FD0F04D4} = {EB1F6262-0913-4464-A034-4F4D01268E83}
{39C63F8E-F3A5-48D8-851C-62BEB9C701C2} = {EB1F6262-0913-4464-A034-4F4D01268E83}
{B1CD5BF8-4A92-4005-909F-BCBFDE35D150} = {EB1F6262-0913-4464-A034-4F4D01268E83}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B381617-F1EE-4BFC-8EAE-CAB4DBB4B9A2}
Expand Down
18 changes: 18 additions & 0 deletions TickSpec/AsyncInvoker.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace TickSpec
open System.Threading.Tasks

type AsyncInvoker() =
static member DoTaskCall (task: Task) =
async {
do! task |> Async.AwaitTask
} |> Async.RunSynchronously

static member DoCallAsync<'T> (input: Task<'T>) =
async {
return! input |> Async.AwaitTask
} |> Async.RunSynchronously

static member DoAsyncCall<'T> (input: Async<'T>) =
async {
return! input
} |> Async.RunSynchronously
30 changes: 25 additions & 5 deletions TickSpec/ScenarioGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,28 @@ let defineStepMethod
else
gen.Emit(OpCodes.Callvirt, mi)

if mi.ReturnType <> typeof<System.Void> then
gen.Emit(OpCodes.Box,mi.ReturnType)
let v = mi.ReturnType
let typ =
match v.Namespace, v.Name with
| "System.Threading.Tasks", "Task`1" ->
let callInfo =
typeof<AsyncInvoker>.GetMethod("DoCallAsync", BindingFlags.Public ||| BindingFlags.Static).MakeGenericMethod(v.GenericTypeArguments.[0])
gen.EmitCall(OpCodes.Call, callInfo, null)
v.GenericTypeArguments.[0]
| "System.Threading.Tasks", "Task" ->
let callInfo =
typeof<AsyncInvoker>.GetMethod("DoTaskCall", BindingFlags.Public ||| BindingFlags.Static)
gen.EmitCall(OpCodes.Call, callInfo, null)
typeof<System.Void>
| "Microsoft.FSharp.Control", "FSharpAsync`1" ->
let callInfo =
typeof<AsyncInvoker>.GetMethod("DoAsyncCall", BindingFlags.Public ||| BindingFlags.Static).MakeGenericMethod(v.GenericTypeArguments.[0])
gen.EmitCall(OpCodes.Call, callInfo, null)
v.GenericTypeArguments.[0]
| _, _ -> v

if typ <> typeof<System.Void> then
gen.Emit(OpCodes.Box,typ)
let local0 = gen.DeclareLocal(typeof<Object>).LocalIndex
gen.Emit(OpCodes.Stloc, local0)

Expand All @@ -449,8 +469,8 @@ let defineStepMethod
gen.Emit(OpCodes.Ldloc, l)
gen.Emit(OpCodes.Callvirt, typeof<IInstanceProvider>.GetMethod("RegisterInstance"))

if FSharpType.IsTuple mi.ReturnType then
let types = FSharpType.GetTupleElements mi.ReturnType
if FSharpType.IsTuple typ then
let types = FSharpType.GetTupleElements typ
for i = 0 to (types.Length - 1) do
let t = types.[i]
let local1 = gen.DeclareLocal(typeof<Object>).LocalIndex
Expand All @@ -462,7 +482,7 @@ let defineStepMethod

emitRegisterInstanceCall t local1
else
emitRegisterInstanceCall (mi.ReturnType) local0
emitRegisterInstanceCall typ local0

// Emit return
gen.Emit(OpCodes.Ret)
Expand Down
46 changes: 41 additions & 5 deletions TickSpec/ScenarioRun.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open System.Collections.Generic
open System.Reflection
open Microsoft.FSharp.Reflection
open System.Threading.Tasks

/// Splits CSV
let split (s:string) =
Expand All @@ -16,18 +17,53 @@ let getInstance (provider:IInstanceProvider) (m:MethodInfo) =
if m.IsStatic then null
else provider.GetService m.DeclaringType

type AsyncHelper private () =
static let CallMethodInfo =
let flags = System.Reflection.BindingFlags.NonPublic ||| System.Reflection.BindingFlags.Static
typeof<AsyncHelper>.GetMethod("DoAsyncCall", flags).GetGenericMethodDefinition()

static member private DoAsyncCall<'T> (input: obj) =
let typedInput: Async<'T> = unbox input
AsyncInvoker.DoAsyncCall typedInput

static member Call (input: obj, typeOfValue: System.Type) =
CallMethodInfo.MakeGenericMethod(typeOfValue).Invoke(null, [|input|]) :?> _

type TaskHelper private () =
static let CallMethodInfo =
let flags = System.Reflection.BindingFlags.NonPublic ||| System.Reflection.BindingFlags.Static
typeof<TaskHelper>.GetMethod("DoCallAsync", flags).GetGenericMethodDefinition()

static member private DoCallAsync<'T> (input: obj) =
let typedInput: Task<'T> = unbox input
AsyncInvoker.DoCallAsync typedInput

static member Call (input: obj, typeOfValue: System.Type) =
CallMethodInfo.MakeGenericMethod(typeOfValue).Invoke(null, [|input|]) :?> _

/// Invokes specified method with specified parameters
let invoke (provider:IInstanceProvider) (m:MethodInfo) ps =
let instance = getInstance provider m
let ret = m.Invoke(instance,ps)
if m.ReturnType <> typeof<System.Void> then
if FSharpType.IsTuple m.ReturnType then
let types = FSharpType.GetTupleElements m.ReturnType
let v = m.ReturnType
let retP = m.Invoke(instance,ps)
let ret, typ =
match v.Namespace, v.Name with
| "System.Threading.Tasks", "Task`1" ->
(TaskHelper.Call(retP, v.GenericTypeArguments.[0]), v.GenericTypeArguments.[0])
| "System.Threading.Tasks", "Task" ->
AsyncInvoker.DoTaskCall (retP :?> Task)
(() :> obj, typeof<System.Void>)
| "Microsoft.FSharp.Control", "FSharpAsync`1" ->
(AsyncHelper.Call(retP, v.GenericTypeArguments.[0]), v.GenericTypeArguments.[0])
| _, _ -> (retP, v)
if typ <> typeof<System.Void> then
if FSharpType.IsTuple typ then
let types = FSharpType.GetTupleElements typ
let values = FSharpValue.GetTupleFields ret
Seq.map2 (fun t v -> t,v) types values
|> Seq.iter (fun (t,v) -> provider.RegisterInstance(t, v))
else
provider.RegisterInstance(m.ReturnType, ret)
provider.RegisterInstance(typ, ret)

/// Converts generic methods
let toConcreteMethod (m:MethodInfo) =
Expand Down
4 changes: 2 additions & 2 deletions TickSpec/TickSpec.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<Compile Include="Extensions.fs" />
<Compile Include="ServiceProvider.fs" />
<Compile Include="FeatureSource.fs" />
<Compile Include="AsyncInvoker.fs" />
<Compile Include="ScenarioRun.fs" />
<Compile Include="ScenarioGen.fs" />
<Compile Include="FeatureGen.fs" />
Expand All @@ -39,8 +40,7 @@
<Compile Include="TickSpec.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Core" Version="3.1.2.5" Condition=" '$(TargetFramework)' == 'net452' " />
<PackageReference Include="FSharp.Core" Version="4.3.4" Condition=" '$(TargetFramework)' == 'netstandard2.0' " />
<PackageReference Include="FSharp.Core" Version="4.3.4" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-63127-02">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
Expand Down