Skip to content

Commit b88b0b9

Browse files
Support Async<'Testable> & Task<'Testable> (#694)
Support async & task as first-class testables
1 parent dcc9ec2 commit b88b0b9

File tree

9 files changed

+370
-48
lines changed

9 files changed

+370
-48
lines changed

examples/FsCheck.Examples/Examples.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Check.One(bigSize,fun (s:Simple) -> match s with Leaf2 _ -> false | Void3 -> fal
194194

195195
Check.One(bigSize,fun i -> (-10 < i && i < 0) || (0 < i) && (i < 10 ))
196196
Check.Quick (fun opt -> match opt with None -> false | Some b -> b )
197-
Check.Quick (fun opt -> match opt with Some n when n<0 -> false | Some n when n >= 0 -> true | _ -> true )
197+
Check.Quick (fun opt -> match opt with Some n when n < 0 -> false | Some n when n >= 0 -> true | _ -> true )
198198

199199
let prop_RevId' (xs:list<int>) (x:int) = if (xs.Length > 2) && (x >10) then false else true
200200
Check.Quick prop_RevId'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Threading.Tasks;
2+
using FsCheck.Fluent;
3+
using NUnit.Framework;
4+
5+
namespace FsCheck.NUnit.CSharpExamples;
6+
7+
public class SyncVersusAsyncExamples
8+
{
9+
[Property]
10+
public Property Property_ShouldPass(bool b)
11+
{
12+
return (b ^ !b).Label("b ^ !b");
13+
}
14+
15+
[Property]
16+
public Property Property_ShouldFail(bool b)
17+
{
18+
return (b && !b).Label("b && !b");
19+
}
20+
21+
[Property]
22+
public async Task Task_ShouldPass(bool b)
23+
{
24+
await DoSomethingAsync();
25+
Assert.That(b ^ !b);
26+
}
27+
28+
[Property]
29+
public async Task Task_Exception_ShouldFail(bool b)
30+
{
31+
await DoSomethingAsync();
32+
Assert.That(b && !b);
33+
}
34+
35+
[Property]
36+
public async Task Task_Cancelled_ShouldFail(bool b)
37+
{
38+
await Task.Run(() => Assert.That(b ^ !b), new System.Threading.CancellationToken(canceled: true));
39+
}
40+
41+
[Property]
42+
public async Task<Property> TaskProperty_ShouldPass(bool b)
43+
{
44+
await DoSomethingAsync();
45+
return (b ^ !b).Label("b ^ !b");
46+
}
47+
48+
[Property]
49+
public async Task<Property> TaskProperty_ShouldFail(bool b)
50+
{
51+
await DoSomethingAsync();
52+
return (b && !b).Label("b && !b");
53+
}
54+
55+
private static async Task DoSomethingAsync() => await Task.Yield();
56+
}

examples/FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<Content Include="App.config" />
1111
<Compile Include="PropertyExamples.fs" />
12+
<Compile Include="SyncVersusAsyncExamples.fs" />
1213
</ItemGroup>
1314
<ItemGroup>
1415
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace FsCheck.NUnit.Examples
2+
3+
open FsCheck.FSharp
4+
open FsCheck.NUnit
5+
6+
module SyncVersusAsyncExamples =
7+
let private doSomethingAsync () = async { return () }
8+
9+
[<Property>]
10+
let ``Sync - should pass`` b =
11+
b = b |> Prop.label "b = b"
12+
13+
[<Property>]
14+
let ``Sync - should fail`` b =
15+
b = not b |> Prop.label "b = not b"
16+
17+
[<Property>]
18+
let ``Async - should pass`` b =
19+
async {
20+
do! doSomethingAsync ()
21+
return b = b |> Prop.label "b = b"
22+
}
23+
24+
[<Property>]
25+
let ``Async - should fail`` b =
26+
async {
27+
do! doSomethingAsync ()
28+
return b = not b |> Prop.label "b = not b"
29+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Threading.Tasks;
2+
using FsCheck.Fluent;
3+
using FsCheck.Xunit;
4+
using Xunit;
5+
6+
namespace FsCheck.XUnit.CSharpExamples;
7+
8+
public class SyncVersusAsyncExamples
9+
{
10+
[Property]
11+
public Property Property_ShouldPass(bool b)
12+
{
13+
return (b ^ !b).Label("b ^ !b");
14+
}
15+
16+
[Property]
17+
public Property Property_ShouldFail(bool b)
18+
{
19+
return (b && !b).Label("b && !b");
20+
}
21+
22+
[Property]
23+
public async Task Task_ShouldPass(bool b)
24+
{
25+
await DoSomethingAsync();
26+
Assert.True(b ^ !b);
27+
}
28+
29+
[Property]
30+
public async Task Task_Exception_ShouldFail(bool b)
31+
{
32+
await DoSomethingAsync();
33+
Assert.True(b && !b);
34+
}
35+
36+
[Property]
37+
public async Task Task_Cancelled_ShouldFail(bool b)
38+
{
39+
await Task.Run(() => Assert.True(b ^ !b), new System.Threading.CancellationToken(canceled: true));
40+
}
41+
42+
[Property]
43+
public async Task<Property> TaskProperty_ShouldPass(bool b)
44+
{
45+
await DoSomethingAsync();
46+
return (b ^ !b).Label("b ^ !b");
47+
}
48+
49+
[Property]
50+
public async Task<Property> TaskProperty_ShouldFail(bool b)
51+
{
52+
await DoSomethingAsync();
53+
return (b && !b).Label("b && !b");
54+
}
55+
56+
private static async Task DoSomethingAsync() => await Task.Yield();
57+
}

src/FsCheck/FSharp.Prop.fs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,8 @@ module Prop =
6767
{ r with Labels = Set.add l r.Labels }) |> Future
6868
Prop.mapResult add
6969

70-
/// Turns a testable type into a property. Testables are unit, boolean, Lazy testables, Gen testables, functions
71-
/// from a type for which a generator is know to a testable, tuples up to 6 tuple containing testables, and lists
72-
/// containing testables.
70+
/// Turns a testable type into a property. Testables are unit, Boolean, Lazy testables, Gen testables,
71+
/// Async testables, Task testables, and functions from a type for which a generator is known to a testable.
7372
[<CompiledName("OfTestable")>]
7473
let ofTestable (testable:'Testable) =
7574
property testable

src/FsCheck/Testable.fs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type ResultContainer =
4646
match (l,r) with
4747
| (Value vl,Value vr) -> f (vl, vr) |> Value
4848
| (Future tl,Value vr) -> tl.ContinueWith (fun (x :Task<Result>) -> f (x.Result, vr)) |> Future
49-
| (Value vl,Future tr) -> tr.ContinueWith (fun (x :Task<Result>) -> f (x.Result, vl)) |> Future
49+
| (Value vl,Future tr) -> tr.ContinueWith (fun (x :Task<Result>) -> f (vl, x.Result)) |> Future
5050
| (Future tl,Future tr) -> tl.ContinueWith (fun (x :Task<Result>) ->
5151
tr.ContinueWith (fun (y :Task<Result>) -> f (x.Result, y.Result))) |> TaskExtensions.Unwrap |> Future
5252
static member (&&&) (l,r) = ResultContainer.MapResult2(Result.ResAnd, l, r)
@@ -113,15 +113,6 @@ module private Testable =
113113
|> Value
114114
|> ofResult
115115

116-
let ofTaskBool (b:Task<bool>) :Property =
117-
b.ContinueWith (fun (x:Task<bool>) ->
118-
match (x.IsCanceled, x.IsFaulted) with
119-
| (false,false) -> Res.ofBool x.Result
120-
| (_,true) -> Res.failedException x.Exception
121-
| (true,_) -> Res.failedCancelled)
122-
|> Future
123-
|> ofResult
124-
125116
let ofTask (b:Task) :Property =
126117
b.ContinueWith (fun (x:Task) ->
127118
match (x.IsCanceled, x.IsFaulted) with
@@ -131,6 +122,26 @@ module private Testable =
131122
|> Future
132123
|> ofResult
133124

125+
let ofTaskGeneric (t : Task<'T>) : Property =
126+
Property (fun arbMap ->
127+
Gen.promote (fun runner ->
128+
Shrink.ofValue (Future (
129+
t.ContinueWith (fun (t : Task<'T>) ->
130+
match t.IsCanceled, t.IsFaulted with
131+
| _, true -> Task.FromResult (Res.failedException t.Exception)
132+
| true, _ -> Task.FromResult Res.failedCancelled
133+
| false, false ->
134+
let prop = property t.Result
135+
let gen = Property.GetGen arbMap prop
136+
let shrink = runner gen
137+
138+
let value, shrinks = Shrink.getValue shrink
139+
assert Seq.isEmpty shrinks
140+
match value with
141+
| Value result -> Task.FromResult result
142+
| Future resultTask -> resultTask)
143+
|> _.Unwrap()))))
144+
134145
let mapShrinkResult (f:Shrink<ResultContainer> -> _) a =
135146
fun arbMap ->
136147
property a
@@ -194,21 +205,15 @@ module private Testable =
194205
static member Bool() =
195206
{ new ITestable<bool> with
196207
member __.Property b = Prop.ofBool b }
197-
static member TaskBool() =
198-
{ new ITestable<Task<bool>> with
199-
member __.Property b = Prop.ofTaskBool b }
200208
static member Task() =
201209
{ new ITestable<Task> with
202210
member __.Property b = Prop.ofTask b }
203211
static member TaskGeneric() =
204212
{ new ITestable<Task<'T>> with
205-
member __.Property b = Prop.ofTask (b :> Task) }
206-
static member AsyncBool() =
207-
{ new ITestable<Async<bool>> with
208-
member __.Property b = Prop.ofTaskBool <| Async.StartAsTask b }
209-
static member Async() =
210-
{ new ITestable<Async<unit>> with
211-
member __.Property b = Prop.ofTask <| Async.StartAsTask b }
213+
member __.Property t = Prop.ofTaskGeneric t }
214+
static member AsyncGeneric() =
215+
{ new ITestable<Async<'T>> with
216+
member __.Property a = Prop.ofTaskGeneric <| Async.StartAsTask a }
212217
static member Lazy() =
213218
{ new ITestable<Lazy<'T>> with
214219
member __.Property b =

tests/FsCheck.Test/Arbitrary.fs

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -826,15 +826,117 @@ module Arbitrary =
826826
assert (ImmutableDictionary.CreateRange(values) |> shrink |> Seq.forall checkShrink)
827827
assert (ImmutableSortedDictionary.CreateRange(values) |> shrink |> Seq.forall checkShrink)
828828

829-
[<Property>]
830-
let ``should execute generic-task-valued property`` (value: int) =
831-
// Since this doesn't throw, the test should pass and ignore the integer value
832-
System.Threading.Tasks.Task.FromResult value
833-
834829
[<Fact>]
835830
let ``Zip should shrink both values independently``() =
836831
let shrinkable = Arb.fromGenShrink(Gen.choose(0, 10), fun x -> [| x-1 |] |> Seq.where(fun x -> x >= 0))
837832
let notShrinkable = Gen.choose(0, 10) |> Arb.fromGen
838833
let zipped = Fluent.Arb.Zip(shrinkable, notShrinkable)
839834
let shrinks = zipped.Shrinker(struct (10, 10)) |> Seq.toArray
840835
test <@ shrinks = [| struct (9, 10) |] @>
836+
837+
module Truthy =
838+
let private shouldBeTruthy description testable =
839+
try Check.One (Config.QuickThrowOnFailure, testable) with
840+
| exn -> failwith $"'%s{description}' should be truthy. Got: '{exn}'."
841+
842+
[<Fact>]
843+
let ``()`` () = shouldBeTruthy "()" ()
844+
845+
[<Fact>]
846+
let ``true`` () = shouldBeTruthy "true" true
847+
848+
[<Fact>]
849+
let ``Prop.ofTestable ()`` () = shouldBeTruthy "Prop.ofTestable ()" (Prop.ofTestable ())
850+
851+
[<Fact>]
852+
let ``lazy ()`` () = shouldBeTruthy "lazy ()" (lazy ())
853+
854+
[<Fact>]
855+
let ``lazy true`` () = shouldBeTruthy "lazy true" (lazy true)
856+
857+
[<Fact>]
858+
let ``lazy Prop.ofTestable ()`` () = shouldBeTruthy "lazy Prop.ofTestable ()" (lazy Prop.ofTestable ())
859+
860+
[<Fact>]
861+
let ``gen { return () }`` () = shouldBeTruthy "gen { return () }" (gen { return () })
862+
863+
[<Fact>]
864+
let ``gen { return true }`` () = shouldBeTruthy "gen { return true }" (gen { return true })
865+
866+
[<Fact>]
867+
let ``gen { return Prop.ofTestable () }`` () = shouldBeTruthy "gen { return Prop.ofTestable () }" (gen { return Prop.ofTestable () })
868+
869+
[<Fact>]
870+
let ``async { return () }`` () = shouldBeTruthy "async { return () }" (async { return () })
871+
872+
[<Fact>]
873+
let ``async { return true }`` () = shouldBeTruthy "async { return true }" (async { return true })
874+
875+
[<Fact>]
876+
let ``async { return Prop.ofTestable () }`` () = shouldBeTruthy "async { return Prop.ofTestable () }" (async { return Prop.ofTestable () })
877+
878+
[<Fact>]
879+
let ``task { return true }`` () = shouldBeTruthy "task { return true }" (System.Threading.Tasks.Task.FromResult true)
880+
881+
[<Fact>]
882+
let ``task { return () }`` () = shouldBeTruthy "task { return () }" (System.Threading.Tasks.Task.FromResult ())
883+
884+
[<Fact>]
885+
let ``task { return Prop.ofTestable () }`` () = shouldBeTruthy "task { return Prop.ofTestable () }" (System.Threading.Tasks.Task.FromResult (Prop.ofTestable ()))
886+
887+
[<Fact>]
888+
let ``task { return fun b -> b ==> b }`` () = shouldBeTruthy "task { return fun b -> b ==> b }" (System.Threading.Tasks.Task.FromResult (fun b -> b ==> b))
889+
890+
[<Fact>]
891+
let ``task { return task { return true } }`` () = shouldBeTruthy "task { return task { return true } }" (System.Threading.Tasks.Task.FromResult (Prop.ofTestable (System.Threading.Tasks.Task.FromResult true)))
892+
893+
module Falsy =
894+
let private shouldBeFalsy description testable =
895+
let exn =
896+
try Check.One (Config.QuickThrowOnFailure, testable); None with
897+
| exn -> Some exn
898+
899+
match exn with
900+
| None -> failwith $"'%s{description}' should be falsy."
901+
| Some exn ->
902+
if not (exn.Message.StartsWith "Falsifiable") then
903+
failwith $"Unexpected exception: '{exn}'."
904+
905+
[<Fact>]
906+
let ``false`` () = shouldBeFalsy "false" false
907+
908+
[<Fact>]
909+
let ``Prop.ofTestable false`` () = shouldBeFalsy "Prop.ofTestable false" (Prop.ofTestable false)
910+
911+
[<Fact>]
912+
let ``lazy false`` () = shouldBeFalsy "lazy false" (lazy false)
913+
914+
[<Fact>]
915+
let ``lazy Prop.ofTestable false`` () = shouldBeFalsy "lazy Prop.ofTestable false" (lazy Prop.ofTestable false)
916+
917+
[<Fact>]
918+
let ``gen { return false }`` () = shouldBeFalsy "gen { return false }" (gen { return false })
919+
920+
[<Fact>]
921+
let ``gen { return Prop.ofTestable false }`` () = shouldBeFalsy "gen { return Prop.ofTestable false }" (gen { return Prop.ofTestable false })
922+
923+
[<Fact>]
924+
let ``async { return false }`` () = shouldBeFalsy "async { return false }" (async { return false })
925+
926+
[<Fact>]
927+
let ``async { return Prop.ofTestable false }`` () = shouldBeFalsy "async { return Prop.ofTestable false }" (async { return Prop.ofTestable false })
928+
929+
[<Fact>]
930+
let ``task { return false }`` () = shouldBeFalsy "task { return false }" (System.Threading.Tasks.Task.FromResult false)
931+
932+
[<Fact>]
933+
let ``task { return Prop.ofTestable false }`` () = shouldBeFalsy "task { return Prop.ofTestable false }" (System.Threading.Tasks.Task.FromResult (Prop.ofTestable false))
934+
935+
[<Fact>]
936+
let ``task { return fun b -> b ==> not b }`` () = shouldBeFalsy "task { return fun b -> b ==> not b }" (System.Threading.Tasks.Task.FromResult (fun b -> b ==> not b))
937+
938+
[<Fact>]
939+
let ``task { return task { return false } }`` () = shouldBeFalsy "task { return task { return false } }" (System.Threading.Tasks.Task.FromResult (Prop.ofTestable (System.Threading.Tasks.Task.FromResult false)))
940+
941+
[<Fact>]
942+
let ``task { return lazy false }`` () = shouldBeFalsy "task { return lazy false }" (System.Threading.Tasks.Task.FromResult (lazy false))

0 commit comments

Comments
 (0)