Skip to content

Commit 2a5bbdd

Browse files
committed
Add unit tests for TaskSeq.updateAt
1 parent b641aa6 commit 2a5bbdd

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<Compile Include="TaskSeq.Take.Tests.fs" />
4646
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
4747
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
48+
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
4849
<Compile Include="TaskSeq.Zip.Tests.fs" />
4950
<Compile Include="TaskSeq.Tests.CE.fs" />
5051
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
module TaskSeq.Tests.UpdateAt
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
11+
//
12+
// TaskSeq.updateAt
13+
//
14+
15+
exception SideEffectPastEnd of string
16+
17+
module EmptySeq =
18+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
19+
let ``TaskSeq-updateAt(0) on empty input should throw ArgumentException`` variant =
20+
fun () ->
21+
Gen.getEmptyVariant variant
22+
|> TaskSeq.updateAt 0 42
23+
|> consumeTaskSeq
24+
25+
|> should throwAsyncExact typeof<ArgumentException>
26+
27+
[<Fact>]
28+
let ``TaskSeq-updateAt(-1) should throw ArgumentException on any input`` () =
29+
fun () ->
30+
TaskSeq.empty<int>
31+
|> TaskSeq.updateAt -1 42
32+
|> consumeTaskSeq
33+
34+
|> should throwAsyncExact typeof<ArgumentException>
35+
36+
fun () ->
37+
TaskSeq.init 10 id
38+
|> TaskSeq.updateAt -1 42
39+
|> consumeTaskSeq
40+
41+
|> should throwAsyncExact typeof<ArgumentException>
42+
43+
[<Fact>]
44+
let ``TaskSeq-updateAt(-1) should throw ArgumentException before awaiting`` () =
45+
fun () ->
46+
taskSeq {
47+
do! longDelay ()
48+
49+
if false then
50+
yield 0 // type inference
51+
}
52+
|> TaskSeq.updateAt -1 42
53+
|> ignore // throws even without running the async. Bad coding, don't ignore a task!
54+
55+
// test without awaiting the async
56+
|> should throw typeof<ArgumentException>
57+
58+
module Immutable =
59+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
60+
let ``TaskSeq-updateAt can update at end of sequence`` variant = task {
61+
do!
62+
Gen.getSeqImmutable variant
63+
|> TaskSeq.updateAt 9 99
64+
|> verifyDigitsAsString "ABCDEFGHI£"
65+
}
66+
67+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
68+
let ``TaskSeq-updateAt past end of sequence throws ArgumentException`` variant =
69+
fun () ->
70+
Gen.getSeqImmutable variant
71+
|> TaskSeq.updateAt 10 99
72+
|> consumeTaskSeq
73+
74+
|> should throwAsyncExact typeof<ArgumentException>
75+
76+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
77+
let ``TaskSeq-updateAt updates item immediately after the indexed position`` variant = task {
78+
79+
do!
80+
Gen.getSeqImmutable variant
81+
|> TaskSeq.updateAt 0 99
82+
|> verifyDigitsAsString "£BCDEFGHIJ"
83+
84+
do!
85+
Gen.getSeqImmutable variant
86+
|> TaskSeq.updateAt 1 99
87+
|> verifyDigitsAsString "A£CDEFGHIJ"
88+
89+
do!
90+
Gen.getSeqImmutable variant
91+
|> TaskSeq.updateAt 5 99
92+
|> verifyDigitsAsString "ABCDE£GHIJ"
93+
94+
do!
95+
Gen.getSeqImmutable variant
96+
|> TaskSeq.updateAt 9 99
97+
|> verifyDigitsAsString "ABCDEFGHI£"
98+
}
99+
100+
101+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
102+
let ``TaskSeq-updateAt can be repeated in a chain`` variant = task {
103+
104+
do!
105+
Gen.getSeqImmutable variant
106+
|> TaskSeq.updateAt 0 99
107+
|> TaskSeq.updateAt 1 99
108+
|> TaskSeq.updateAt 2 99
109+
|> TaskSeq.updateAt 3 99
110+
|> TaskSeq.updateAt 4 99
111+
|> verifyDigitsAsString "£££££FGHIJ"
112+
113+
do!
114+
Gen.getSeqImmutable variant
115+
|> TaskSeq.updateAt 9 99
116+
|> TaskSeq.updateAt 8 99
117+
|> TaskSeq.updateAt 6 99
118+
|> TaskSeq.updateAt 4 99
119+
|> TaskSeq.updateAt 2 99
120+
|> verifyDigitsAsString "AB£D£F£H££"
121+
}
122+
123+
124+
[<Fact>]
125+
let ``TaskSeq-updateAt can be applied to an infinite task sequence`` () =
126+
TaskSeq.initInfinite id
127+
|> TaskSeq.updateAt 1_000_000 12345
128+
|> TaskSeq.item 1_000_000
129+
|> Task.map (should equal 12345)
130+
131+
132+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
133+
let ``TaskSeq-updateAt throws when there are not enough elements`` variant =
134+
fun () ->
135+
TaskSeq.singleton 1
136+
// update after 1
137+
|> TaskSeq.updateAt 2 99
138+
|> consumeTaskSeq
139+
140+
|> should throwAsyncExact typeof<ArgumentException>
141+
142+
fun () ->
143+
Gen.getSeqImmutable variant
144+
|> TaskSeq.updateAt 10 99
145+
|> consumeTaskSeq
146+
147+
|> should throwAsyncExact typeof<ArgumentException>
148+
149+
fun () ->
150+
Gen.getSeqImmutable variant
151+
|> TaskSeq.updateAt 10_000_000 99
152+
|> consumeTaskSeq
153+
154+
|> should throwAsyncExact typeof<ArgumentException>
155+
156+
157+
module SideEffects =
158+
159+
// PoC test
160+
[<Fact>]
161+
let ``Seq-updateAt (poc-proof) will NOT execute side effect just after index`` () =
162+
// NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module
163+
// this shows that Seq.updateAt executes no extra side effects.
164+
165+
let mutable x = 42
166+
167+
let items = seq {
168+
yield x
169+
x <- x + 1 // we are proving this gets executed with updateAt(0)
170+
yield x * 2
171+
}
172+
173+
items
174+
|> Seq.updateAt 0 99
175+
|> Seq.item 0 // put enumerator to updated item
176+
|> ignore
177+
178+
x |> should equal 42 // one time side effect executed. QED
179+
180+
[<Fact>]
181+
let ``TaskSeq-updateAt(0) will execute side effects at start of sequence`` () =
182+
// NOTE: while not strictly necessary, this mirrors behavior of Seq.updateAt
183+
184+
let mutable x = 42 // for this test, the potential mutation should not actually occur
185+
186+
let items = taskSeq {
187+
x <- x + 1 // this is executed even with updateAt(0)
188+
yield x
189+
yield x * 2
190+
}
191+
192+
items
193+
|> TaskSeq.updateAt 0 99
194+
|> TaskSeq.item 0 // consume only the first item
195+
|> Task.map (should equal 99)
196+
|> Task.map (fun () -> x |> should equal 43) // the mutable was updated
197+
198+
[<Fact>]
199+
let ``TaskSeq-updateAt will NOT execute last side effect when inserting past end`` () =
200+
let mutable x = 42
201+
202+
let items = taskSeq {
203+
yield x
204+
yield x * 2
205+
yield x * 4
206+
x <- x + 1 // this is executed when inserting past last item
207+
}
208+
209+
items
210+
|> TaskSeq.updateAt 2 99
211+
|> TaskSeq.item 2
212+
|> Task.map (should equal 99)
213+
|> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS
214+
215+
216+
[<Fact>]
217+
let ``TaskSeq-updateAt will NOT execute side effect just before index`` () =
218+
let mutable x = 42
219+
220+
let items = taskSeq {
221+
yield x
222+
x <- x + 1 // this is executed, even though we insert after the first item
223+
yield x * 2
224+
yield x * 4
225+
}
226+
227+
items
228+
|> TaskSeq.updateAt 0 99
229+
|> TaskSeq.item 0
230+
|> Task.map (should equal 99)
231+
|> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS
232+
233+
[<Fact>]
234+
let ``TaskSeq-updateAt exception at update index is NOT thrown`` () =
235+
taskSeq {
236+
yield 1
237+
yield! [ 2; 3 ]
238+
do SideEffectPastEnd "at the end" |> raise // this is NOT raised
239+
yield 4
240+
}
241+
|> TaskSeq.updateAt 2 99
242+
|> TaskSeq.item 2
243+
|> Task.map (should equal 99)
244+
245+
[<Fact>]
246+
let ``TaskSeq-updateAt prove that an exception from the taskSeq is thrown instead of exception from function`` () =
247+
let items = taskSeq {
248+
yield 42
249+
yield! [ 1; 2 ]
250+
do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised
251+
}
252+
253+
fun () -> items |> TaskSeq.updateAt 4 99 |> consumeTaskSeq // this would raise ArgumentException normally, but not now
254+
|> should throwAsyncExact typeof<SideEffectPastEnd>

0 commit comments

Comments
 (0)