diff --git a/Examples/ByFeature/AsyncSteps/AsyncSteps.fsproj b/Examples/ByFeature/AsyncSteps/AsyncSteps.fsproj new file mode 100644 index 0000000..0cb62f2 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/AsyncSteps.fsproj @@ -0,0 +1,22 @@ + + + + + netcoreapp2.1;net452 + false + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/ByFeature/AsyncSteps/NunitWiring.fs b/Examples/ByFeature/AsyncSteps/NunitWiring.fs new file mode 100644 index 0000000..c9a74f4 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/NunitWiring.fs @@ -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 +[] +type FeatureFixture () = + /// Test method for all BDD tests in current assembly as NUnit unit tests + [] + [] + 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) \ No newline at end of file diff --git a/Examples/ByFeature/AsyncSteps/Time.feature b/Examples/ByFeature/AsyncSteps/Time.feature new file mode 100644 index 0000000..13c2048 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/Time.feature @@ -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 diff --git a/Examples/ByFeature/AsyncSteps/TimeSteps.fs b/Examples/ByFeature/AsyncSteps/TimeSteps.fs new file mode 100644 index 0000000..fbfa888 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/TimeSteps.fs @@ -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 [] ``having current time`` () = + Time DateTime.Now + +let [] ``I sleep for (\d*)ms using Async`` (duration: int) = + async { + do! Async.Sleep duration + } + +let [] ``I sleep for (\d*)ms using Tasks`` (duration: int) = + task { + do! Task.Delay duration + } + +let [] ``the current time is at least (\d*)ms higher than it was`` (duration: int) (Time previousCurrentTime) = + int (DateTime.Now - previousCurrentTime).TotalMilliseconds >= duration + |> Assert.True \ No newline at end of file diff --git a/Examples/ByFeature/AsyncSteps/Web.feature b/Examples/ByFeature/AsyncSteps/Web.feature new file mode 100644 index 0000000..ff24ff3 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/Web.feature @@ -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" \ No newline at end of file diff --git a/Examples/ByFeature/AsyncSteps/WebSteps.fs b/Examples/ByFeature/AsyncSteps/WebSteps.fs new file mode 100644 index 0000000..6e2a687 --- /dev/null +++ b/Examples/ByFeature/AsyncSteps/WebSteps.fs @@ -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 [] ``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 [] ``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 [] ``the downloaded page contains "(.*)"`` (text: string) (DownloadedPage page) = + page.Contains(text) + |> Assert.True + diff --git a/TickSpec.sln b/TickSpec.sln index 9472a7c..949158f 100644 --- a/TickSpec.sln +++ b/TickSpec.sln @@ -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 @@ -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 @@ -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} diff --git a/TickSpec/AsyncInvoker.fs b/TickSpec/AsyncInvoker.fs new file mode 100644 index 0000000..3c999f1 --- /dev/null +++ b/TickSpec/AsyncInvoker.fs @@ -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 \ No newline at end of file diff --git a/TickSpec/ScenarioGen.fs b/TickSpec/ScenarioGen.fs index e4f7e09..133ec87 100644 --- a/TickSpec/ScenarioGen.fs +++ b/TickSpec/ScenarioGen.fs @@ -433,8 +433,28 @@ let defineStepMethod else gen.Emit(OpCodes.Callvirt, mi) - if mi.ReturnType <> typeof 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.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.GetMethod("DoTaskCall", BindingFlags.Public ||| BindingFlags.Static) + gen.EmitCall(OpCodes.Call, callInfo, null) + typeof + | "Microsoft.FSharp.Control", "FSharpAsync`1" -> + let callInfo = + typeof.GetMethod("DoAsyncCall", BindingFlags.Public ||| BindingFlags.Static).MakeGenericMethod(v.GenericTypeArguments.[0]) + gen.EmitCall(OpCodes.Call, callInfo, null) + v.GenericTypeArguments.[0] + | _, _ -> v + + if typ <> typeof then + gen.Emit(OpCodes.Box,typ) let local0 = gen.DeclareLocal(typeof).LocalIndex gen.Emit(OpCodes.Stloc, local0) @@ -449,8 +469,8 @@ let defineStepMethod gen.Emit(OpCodes.Ldloc, l) gen.Emit(OpCodes.Callvirt, typeof.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).LocalIndex @@ -462,7 +482,7 @@ let defineStepMethod emitRegisterInstanceCall t local1 else - emitRegisterInstanceCall (mi.ReturnType) local0 + emitRegisterInstanceCall typ local0 // Emit return gen.Emit(OpCodes.Ret) diff --git a/TickSpec/ScenarioRun.fs b/TickSpec/ScenarioRun.fs index 99f7ce1..9b0f426 100644 --- a/TickSpec/ScenarioRun.fs +++ b/TickSpec/ScenarioRun.fs @@ -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) = @@ -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.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.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 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) + | "Microsoft.FSharp.Control", "FSharpAsync`1" -> + (AsyncHelper.Call(retP, v.GenericTypeArguments.[0]), v.GenericTypeArguments.[0]) + | _, _ -> (retP, v) + if typ <> typeof 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) = diff --git a/TickSpec/TickSpec.fsproj b/TickSpec/TickSpec.fsproj index 09c17fc..e42f889 100644 --- a/TickSpec/TickSpec.fsproj +++ b/TickSpec/TickSpec.fsproj @@ -25,6 +25,7 @@ + @@ -39,8 +40,7 @@ - - + all runtime; build; native; contentfiles; analyzers