Skip to content

Commit 95e2ca2

Browse files
committed
Added exception tests for AsyncVal
1 parent c24ec6e commit 95e2ca2

File tree

3 files changed

+148
-62
lines changed

3 files changed

+148
-62
lines changed

src/FSharp.Data.GraphQL.Shared/AsyncVal.fs

+89-62
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ namespace FSharp.Data.GraphQL
22

33
open System
44
open System.Collections.Generic
5+
open System.Linq
6+
open System.Threading.Tasks
57

68
#nowarn "25"
79

@@ -15,185 +17,210 @@ type AsyncVal<'T> =
1517
| Async of asynchronous : Async<'T>
1618
| Failure of exn : Exception
1719

18-
static member Zero = Value(Unchecked.defaultof<'T>)
20+
static member Zero = Value (Unchecked.defaultof<'T>)
1921
override x.ToString () =
2022
match x with
21-
| Value v -> "AsyncVal(" + v.ToString() + ")"
23+
| Value v -> "AsyncVal(" + v.ToString () + ")"
2224
| Async _ -> "AsyncVal(Async<>)"
2325
| Failure f -> "AsyncVal(Failure:" + f.Message + ")"
2426

2527
[<RequireQualifiedAccess>]
2628
module AsyncVal =
2729

2830
/// Returns true if AsyncVal wraps an Async computation, otherwise false.
29-
let inline isAsync (x: AsyncVal<'T>) = match x with | Async _ -> true | _ -> false
31+
let inline isAsync (x : AsyncVal<'T>) = match x with | Async _ -> true | _ -> false
3032

3133
/// Returns true if AsyncVal contains immediate result, otherwise false.
32-
let inline isSync (x: AsyncVal<'T>) = match x with | Value _ -> true | _ -> false
34+
let inline isSync (x : AsyncVal<'T>) = match x with | Value _ -> true | _ -> false
3335

3436
/// Returns true if the AsyncVal failed, otherwise false
35-
let inline isFailure (x: AsyncVal<'T>) = match x with | Failure _ -> true | _ -> false
37+
let inline isFailure (x : AsyncVal<'T>) = match x with | Failure _ -> true | _ -> false
3638

3739
/// Returns value wrapped by current AsyncVal. If it's part of Async computation,
3840
/// it's executed synchronously and then value is returned.
3941
/// If the asyncVal failed, then the exception that caused the failure is raised
40-
let get (x: AsyncVal<'T>) =
42+
let get (x : AsyncVal<'T>) =
4143
match x with
4244
| Value v -> v
4345
| Async a -> a |> Async.RunSynchronously
44-
| Failure f -> f.Reraise()
46+
| Failure f -> f.Reraise ()
4547

4648
/// Create new AsyncVal from Async computation.
47-
let inline ofAsync (a: Async<'T>) = Async(a)
49+
let inline ofAsync (a : Async<'T>) = Async (a)
4850

4951
/// Returns an AsyncVal wrapper around provided Async computation.
50-
let inline wrap (v: 'T) = Value(v)
52+
let inline wrap (v : 'T) = Value (v)
5153

5254
/// Converts AsyncVal to Async computation.
53-
let toAsync (x: AsyncVal<'T>) =
55+
let toAsync (x : AsyncVal<'T>) =
5456
match x with
5557
| Value v -> async.Return v
5658
| Async a -> a
57-
| Failure f -> async.Return (f.Reraise())
59+
| Failure f -> async.Return (f.Reraise ())
60+
61+
/// Converts AsyncVal to Async computation.
62+
let toTask (x : AsyncVal<'T>) =
63+
match x with
64+
| Value v -> Task.FromResult (v)
65+
| Async a -> Async.StartAsTask (a)
66+
| Failure f -> Task.FromException<'T> (f)
5867

5968
/// Returns an empty AsyncVal with immediatelly executed value.
6069
let inline empty<'T> : AsyncVal<'T> = AsyncVal<'T>.Zero
6170

6271
/// Maps content of AsyncVal using provided mapping function, returning new
6372
/// AsyncVal as the result.
64-
let map (fn: 'T -> 'U) (x: AsyncVal<'T>) =
73+
let map (fn : 'T -> 'U) (x : AsyncVal<'T>) =
6574
match x with
66-
| Value v -> Value(fn v)
75+
| Value v -> Value (fn v)
6776
| Async a ->
68-
Async(async {
77+
Async ( async {
6978
let! result = a
7079
return fn result
7180
})
72-
| Failure f -> Failure(f)
81+
| Failure f -> Failure (f)
7382

7483

7584
/// Applies rescue fn in case when contained Async value throws an exception.
76-
let rescue path (fn: FieldPath -> exn -> IGQLError list) (x: AsyncVal<'t>) =
85+
let rescue path (fn : FieldPath -> exn -> IGQLError list) (x : AsyncVal<'t>) =
7786
match x with
78-
| Value v -> Value(Ok v)
87+
| Value v -> Value (Ok v)
7988
| Async a ->
80-
Async(async {
89+
Async (async {
8190
try
8291
let! v = a
8392
return Ok v
84-
with e -> return fn path e |> Error
93+
with e ->
94+
return fn path e |> Error
8595
})
86-
| Failure f -> Value(fn path f |> Error)
96+
| Failure f -> Value (fn path f |> Error)
8797
|> map (Result.mapError (List.map (GQLProblemDetails.OfFieldExecutionError (path |> List.rev))))
8898

8999

90100
/// Folds content of AsyncVal over provided initial state zero using provided fn.
91101
/// Returns new AsyncVal as a result.
92-
let fold (fn: 'State -> 'T -> 'State) (zero: 'State) (x: AsyncVal<'T>) : AsyncVal<'State> =
102+
let fold (fn : 'State -> 'T -> 'State) (zero : 'State) (x : AsyncVal<'T>) : AsyncVal<'State> =
93103
match x with
94-
| Value v -> Value(fn zero v)
104+
| Value v -> Value (fn zero v)
95105
| Async a ->
96-
Async(async{
106+
Async (async {
97107
let! res = a
98108
return fn zero res
99109
})
100-
| Failure f -> Failure(f)
110+
| Failure f -> Failure (f)
101111

102112

103113
/// Binds AsyncVal using binder function to produce new AsyncVal.
104-
let bind (binder: 'T -> AsyncVal<'U>) (x: AsyncVal<'T>) : AsyncVal<'U> =
114+
let bind (binder : 'T -> AsyncVal<'U>) (x : AsyncVal<'T>) : AsyncVal<'U> =
105115
match x with
106116
| Value v -> binder v
107117
| Async a ->
108-
Async(async{
118+
Async (async {
109119
let! value = a
110120
let bound = binder value
111121
match bound with
112122
| Value v -> return v
113123
| Async a -> return! a
114-
| Failure f -> return f.Reraise()
124+
| Failure f -> return f.Reraise ()
115125
})
116-
| Failure f -> Failure(f)
126+
| Failure f -> Failure (f)
117127

118128
/// Converts array of AsyncVals into AsyncVal with array results.
119129
/// In case when are non-immediate values in provided array, they are
120130
/// executed asynchronously, one by one with regard to their order in array.
121131
/// Returned array maintain order of values.
122132
/// If the array contains a Failure, then the entire array will not resolve
123-
let collectSequential (values: AsyncVal<'T> []) : AsyncVal<'T []> =
133+
let collectSequential (values : AsyncVal<'T>[]) : AsyncVal<'T[]> =
124134
if values.Length = 0 then Value [||]
125135
elif values |> Array.exists isAsync then
126-
Async(async {
136+
Async (async {
127137
let results = Array.zeroCreate values.Length
138+
let exceptions = ResizeArray values.Length
128139
for i = 0 to values.Length - 1 do
129140
let v = values.[i]
130141
match v with
131142
| Value v -> results.[i] <- v
132143
| Async a ->
133144
let! r = a
134145
results.[i] <- r
135-
| Failure f ->
136-
results.[i] <- f.Reraise()
137-
return results })
138-
else Value (values |> Array.map (fun (Value v) -> v))
146+
| Failure f -> exceptions.Add f
147+
match exceptions.Count with
148+
| 0 -> return results
149+
| 1 -> return exceptions.First().Reraise ()
150+
| _ -> return AggregateException exceptions |> raise
151+
})
152+
else
153+
let exceptions =
154+
values
155+
|> Array.choose (function
156+
| Failure f -> Some f
157+
| _ -> None)
158+
match exceptions.Length with
159+
| 0 -> Value (values |> Array.map (fun (Value v) -> v))
160+
| 1 -> Failure (exceptions.First ())
161+
| _ -> Failure (AggregateException exceptions)
139162

140163
/// Converts array of AsyncVals into AsyncVal with array results.
141164
/// In case when are non-immediate values in provided array, they are
142165
/// executed all in parallel, in unordered fashion. Order of values
143166
/// inside returned array is maintained.
144167
/// If the array contains a Failure, then the entire array will not resolve
145-
let collectParallel (values: AsyncVal<'T> []) : AsyncVal<'T []> =
168+
let collectParallel (values : AsyncVal<'T>[]) : AsyncVal<'T[]> =
146169
if values.Length = 0 then Value [||]
147170
else
148-
let indexes = List<_>(0)
149-
let continuations = List<_>(0)
171+
let indexes = List<_> (0)
172+
let continuations = List<_> (0)
150173
let results = Array.zeroCreate values.Length
174+
let exceptions = ResizeArray values.Length
151175
for i = 0 to values.Length - 1 do
152176
let value = values.[i]
153177
match value with
154178
| Value v -> results.[i] <- v
155179
| Async a ->
156180
indexes.Add i
157181
continuations.Add a
158-
| Failure f ->
159-
results.[i] <- f.Reraise()
160-
if indexes.Count = 0
161-
then Value(results)
162-
else Async(async {
163-
let! vals = continuations |> Async.Parallel
164-
for i = 0 to indexes.Count - 1 do
165-
results.[indexes.[i]] <- vals.[i]
166-
return results })
182+
| Failure f -> exceptions.Add f
183+
match exceptions.Count with
184+
| 1 -> AsyncVal.Failure (exceptions.First ())
185+
| count when count > 1 -> AsyncVal.Failure (AggregateException exceptions)
186+
| _ ->
187+
if indexes.Count = 0 then Value (results)
188+
else Async (async {
189+
let! vals = continuations |> Async.Parallel
190+
for i = 0 to indexes.Count - 1 do
191+
results.[indexes.[i]] <- vals.[i]
192+
return results
193+
})
167194

168195
/// Converts array of AsyncVals of arrays into AsyncVal with array results
169196
/// by calling collectParallel and then appending the results.
170-
let appendParallel (values: AsyncVal<'T []> []) : AsyncVal<'T []> =
197+
let appendParallel (values : AsyncVal<'T[]>[]) : AsyncVal<'T[]> =
171198
values
172199
|> collectParallel
173200
|> map (Array.fold Array.append Array.empty)
174201

175202
/// Converts array of AsyncVals of arrays into AsyncVal with array results
176203
/// by calling collectSequential and then appending the results.
177-
let appendSequential (values: AsyncVal<'T []> []) : AsyncVal<'T []> =
204+
let appendSequential (values : AsyncVal<'T[]>[]) : AsyncVal<'T[]> =
178205
values
179206
|> collectSequential
180207
|> map (Array.fold Array.append Array.empty)
181208

182209
type AsyncValBuilder () =
183210
member _.Zero () = AsyncVal.empty
184211
member _.Return v = AsyncVal.wrap v
185-
member _.ReturnFrom (v: AsyncVal<_>) = v
186-
member _.ReturnFrom (a: Async<_>) = AsyncVal.ofAsync a
187-
member _.Bind (v: AsyncVal<'T>, binder: 'T -> AsyncVal<'U>) =
188-
AsyncVal.bind binder v
189-
member _.Bind (a: Async<'T>, binder: 'T -> AsyncVal<'U>) =
190-
Async(async {
212+
member _.ReturnFrom (v : AsyncVal<_>) = v
213+
member _.ReturnFrom (a : Async<_>) = AsyncVal.ofAsync a
214+
member _.Bind (v : AsyncVal<'T>, binder : 'T -> AsyncVal<'U>) = AsyncVal.bind binder v
215+
member _.Bind (a : Async<'T>, binder : 'T -> AsyncVal<'U>) =
216+
Async (async {
191217
let! value = a
192218
let bound = binder value
193219
match bound with
194220
| Value v -> return v
195221
| Async a -> return! a
196-
| Failure f -> return f.Reraise() })
222+
| Failure f -> return f.Reraise ()
223+
})
197224

198225

199226
[<AutoOpen>]
@@ -203,21 +230,21 @@ module AsyncExtensions =
203230
let asyncVal = AsyncValBuilder ()
204231

205232
/// Active pattern used for checking if AsyncVal contains immediate value.
206-
let (|Immediate|_|) (x: AsyncVal<'T>) = match x with | Value v -> Some v | _ -> None
233+
let (|Immediate|_|) (x : AsyncVal<'T>) = match x with | Value v -> Some v | _ -> None
207234

208235
/// Active patter used for checking if AsyncVal wraps an Async computation.
209-
let (|Async|_|) (x: AsyncVal<'T>) = match x with | Async a -> Some a | _ -> None
236+
let (|Async|_|) (x : AsyncVal<'T>) = match x with | Async a -> Some a | _ -> None
210237

211238
type Microsoft.FSharp.Control.AsyncBuilder with
212239

213-
member _.ReturnFrom (v: AsyncVal<'T>) =
240+
member _.ReturnFrom (v : AsyncVal<'T>) =
214241
match v with
215242
| Value v -> async.Return v
216243
| Async a -> async.ReturnFrom a
217244
| Failure f -> async.Return (raise f)
218245

219-
member _.Bind (v: AsyncVal<'T>, binder) =
246+
member _.Bind (v : AsyncVal<'T>, binder) =
220247
match v with
221-
| Value v -> async.Bind(async.Return v, binder)
222-
| Async a -> async.Bind(a, binder)
223-
| Failure f -> async.Bind(async.Return (raise f), binder)
248+
| Value v -> async.Bind (async.Return v, binder)
249+
| Async a -> async.Bind (a, binder)
250+
| Failure f -> async.Bind (async.Return (raise f), binder)

tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/AsyncValTests.fs

+56
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module FSharp.Data.GraphQL.Tests.AsyncValTests
55

66
open System
7+
open System.Threading.Tasks
78
open FSharp.Data.GraphQL
89
open Xunit
910

@@ -41,6 +42,17 @@ let ``AsyncVal computation allows to bind async computations`` () =
4142
AsyncVal.isSync v |> equals false
4243
v |> AsyncVal.get |> equals 1
4344

45+
[<Fact>]
46+
let ``AsyncVal computation allows to bind async computations preserving exception stack trace`` () : Task = task {
47+
let! ex = throwsAsyncVal<Exception>(
48+
asyncVal {
49+
let! value = async { return failwith "test" }
50+
return value
51+
}
52+
)
53+
ex.StackTrace |> String.IsNullOrEmpty |> Assert.False
54+
}
55+
4456
[<Fact>]
4557
let ``AsyncVal computation allows to bind another AsyncVal`` () =
4658
let v = asyncVal {
@@ -88,6 +100,28 @@ let ``AsyncVal sequential collection resolves all values in order of execution``
88100
v |> AsyncVal.get |> equals [| 1; 2; 3; 4 |]
89101
flag |> equals "b"
90102

103+
[<Fact>]
104+
let ``AsyncVal sequential collection preserves exception stack trace for a single exception`` () = task {
105+
let a = async { return failwith "test" }
106+
let array = [| AsyncVal.wrap 1; AsyncVal.ofAsync a; AsyncVal.wrap 3 |]
107+
let! ex = throwsAsyncVal<Exception>(array |> AsyncVal.collectSequential |> AsyncVal.map ignore)
108+
ex.StackTrace |> String.IsNullOrEmpty |> Assert.False
109+
}
110+
111+
[<Fact>]
112+
let ``AsyncVal sequential collection collects all exceptions into AggregareException`` () = task {
113+
let ex1 = Exception "test1"
114+
let ex2 = Exception "test2"
115+
let array = [| AsyncVal.wrap 1; AsyncVal.Failure ex1; AsyncVal.wrap 3; AsyncVal.Failure ex2 |]
116+
//let a = async { return failwith "test" }
117+
//let b = async { return failwith "test" }
118+
//let array = [| AsyncVal.wrap 1; AsyncVal.ofAsync a; AsyncVal.wrap 3; AsyncVal.ofAsync b |]
119+
let! ex = throwsAsyncVal<AggregateException>(array |> AsyncVal.collectSequential |> AsyncVal.map ignore)
120+
ex.InnerExceptions |> Seq.length |> equals 2
121+
ex.InnerExceptions[0] |> equals ex1
122+
ex.InnerExceptions[1] |> equals ex2
123+
}
124+
91125
[<Fact>]
92126
let ``AsyncVal parallel collection resolves all values with no order of execution`` () =
93127
let mutable flag = "none"
@@ -103,3 +137,25 @@ let ``AsyncVal parallel collection resolves all values with no order of executio
103137
let v = array |> AsyncVal.collectParallel
104138
v |> AsyncVal.get |> equals [| 1; 2; 3; 4 |]
105139
flag |> equals "a"
140+
141+
[<Fact>]
142+
let ``AsyncVal parallel collection preserves exception stack trace for a single exception`` () = task {
143+
let a = async { return failwith "test" }
144+
let array = [| AsyncVal.wrap 1; AsyncVal.ofAsync a; AsyncVal.wrap 3 |]
145+
let! ex = throwsAsyncVal<Exception>(array |> AsyncVal.collectParallel |> AsyncVal.map ignore)
146+
ex.StackTrace |> String.IsNullOrEmpty |> Assert.False
147+
}
148+
149+
[<Fact>]
150+
let ``AsyncVal parallel collection collects all exceptions into AggregareException`` () = task {
151+
let ex1 = Exception "test1"
152+
let ex2 = Exception "test2"
153+
let array = [| AsyncVal.wrap 1; AsyncVal.Failure ex1; AsyncVal.wrap 3; AsyncVal.Failure ex2 |]
154+
//let a = async { return failwith "test" }
155+
//let b = async { return failwith "test" }
156+
//let array = [| AsyncVal.wrap 1; AsyncVal.ofAsync a; AsyncVal.wrap 3; AsyncVal.ofAsync b |]
157+
let! ex = throwsAsyncVal<AggregateException>(array |> AsyncVal.collectParallel |> AsyncVal.map ignore)
158+
ex.InnerExceptions |> Seq.length |> equals 2
159+
ex.InnerExceptions[0] |> equals ex1
160+
ex.InnerExceptions[1] |> equals ex2
161+
}

0 commit comments

Comments
 (0)