Skip to content

Commit 27be66e

Browse files
committed
Add tests for skipWhile and skipWhileInclusive and async variants
1 parent 9a48e58 commit 27be66e

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-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
@@ -36,6 +36,7 @@
3636
<Compile Include="TaskSeq.Pick.Tests.fs" />
3737
<Compile Include="TaskSeq.Singleton.Tests.fs" />
3838
<Compile Include="TaskSeq.Skip.Tests.fs" />
39+
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
3940
<Compile Include="TaskSeq.Tail.Tests.fs" />
4041
<Compile Include="TaskSeq.Take.Tests.fs" />
4142
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
module TaskSeq.Tests.skipWhile
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.skipWhile
12+
// TaskSeq.skipWhileAsync
13+
// TaskSeq.skipWhileInclusive
14+
// TaskSeq.skipWhileInclusiveAsync
15+
//
16+
17+
[<AutoOpen>]
18+
module With =
19+
/// The only real difference in semantics between the base and the *Inclusive variant lies in whether the final item is returned.
20+
/// NOTE the semantics are very clear on only propagating a single failing item in the inclusive case.
21+
let getFunction inclusive isAsync =
22+
match inclusive, isAsync with
23+
| false, false -> TaskSeq.skipWhile
24+
| false, true -> fun pred -> TaskSeq.skipWhileAsync (pred >> Task.fromResult)
25+
| true, false -> TaskSeq.skipWhileInclusive
26+
| true, true -> fun pred -> TaskSeq.skipWhileInclusiveAsync (pred >> Task.fromResult)
27+
28+
/// This is the base condition as one would expect in actual code
29+
let inline cond x = x <> 6
30+
31+
/// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the
32+
/// first failing item in the known sequence (which is 1..10)
33+
let inline condWithGuard x =
34+
let res = cond x
35+
36+
if x > 6 then
37+
failwith "Test sequence should not be enumerated beyond the first item failing the predicate"
38+
39+
res
40+
41+
module EmptySeq =
42+
43+
// TaskSeq-skipWhile+A stands for:
44+
// skipWhile + skipWhileAsync etc.
45+
46+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
47+
let ``TaskSeq-skipWhile+A has no effect`` variant = task {
48+
do!
49+
Gen.getEmptyVariant variant
50+
|> TaskSeq.skipWhile ((=) 12)
51+
|> verifyEmpty
52+
53+
do!
54+
Gen.getEmptyVariant variant
55+
|> TaskSeq.skipWhileAsync ((=) 12 >> Task.fromResult)
56+
|> verifyEmpty
57+
}
58+
59+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
60+
let ``TaskSeq-skipWhileInclusive+A has no effect`` variant = task {
61+
do!
62+
Gen.getEmptyVariant variant
63+
|> TaskSeq.skipWhileInclusive ((=) 12)
64+
|> verifyEmpty
65+
66+
do!
67+
Gen.getEmptyVariant variant
68+
|> TaskSeq.skipWhileInclusiveAsync ((=) 12 >> Task.fromResult)
69+
|> verifyEmpty
70+
}
71+
72+
module Immutable =
73+
74+
// TaskSeq-skipWhile+A stands for:
75+
// skipWhile + skipWhileAsync etc.
76+
77+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
78+
let ``TaskSeq-skipWhile+A filters correctly`` variant = task {
79+
do!
80+
Gen.getSeqImmutable variant
81+
|> TaskSeq.skipWhile ((>) 5) // skip while less than 5
82+
|> verifyDigitsAsString "EFGHIJ"
83+
84+
do!
85+
Gen.getSeqImmutable variant
86+
|> TaskSeq.skipWhileAsync (fun x -> task { return x < 5 })
87+
|> verifyDigitsAsString "EFGHIJ"
88+
}
89+
90+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
91+
let ``TaskSeq-skipWhile+A does not skip first item when false`` variant = task {
92+
do!
93+
Gen.getSeqImmutable variant
94+
|> TaskSeq.skipWhile ((=) 0)
95+
|> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain!
96+
97+
do!
98+
Gen.getSeqImmutable variant
99+
|> TaskSeq.skipWhileAsync ((=) 0 >> Task.fromResult)
100+
|> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain!
101+
}
102+
103+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
104+
let ``TaskSeq-skipWhileInclusive+A filters correctly`` variant = task {
105+
do!
106+
Gen.getSeqImmutable variant
107+
|> TaskSeq.skipWhileInclusive ((>) 5)
108+
|> verifyDigitsAsString "GHIJ" // last 4
109+
110+
do!
111+
Gen.getSeqImmutable variant
112+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 })
113+
|> verifyDigitsAsString "GHIJ"
114+
}
115+
116+
117+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
118+
let ``TaskSeq-skipWhileInclusive+A returns the empty sequence if always true`` variant = task {
119+
do!
120+
Gen.getSeqImmutable variant
121+
|> TaskSeq.skipWhileInclusive ((<) -1)
122+
|> verifyEmpty
123+
124+
do!
125+
Gen.getSeqImmutable variant
126+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return true })
127+
|> verifyEmpty
128+
}
129+
130+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
131+
let ``TaskSeq-skipWhileInclusive+A always skips at least the first item`` variant = task {
132+
do!
133+
Gen.getSeqImmutable variant
134+
|> TaskSeq.skipWhileInclusive ((=) 0)
135+
|> verifyDigitsAsString "BCDEFGHIJ"
136+
137+
do!
138+
Gen.getSeqImmutable variant
139+
|> TaskSeq.skipWhileInclusiveAsync ((=) 0 >> Task.fromResult)
140+
|> verifyDigitsAsString "BCDEFGHIJ"
141+
}
142+
143+
module SideEffects =
144+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
145+
let ``TaskSeq-skipWhile filters correctly`` variant =
146+
Gen.getSeqWithSideEffect variant
147+
|> TaskSeq.skipWhile condWithGuard
148+
|> verifyDigitsAsString "ABCDE"
149+
150+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
151+
let ``TaskSeq-skipWhileAsync filters correctly`` variant =
152+
Gen.getSeqWithSideEffect variant
153+
|> TaskSeq.skipWhileAsync (fun x -> task { return condWithGuard x })
154+
|> verifyDigitsAsString "ABCDE"
155+
156+
[<Theory>]
157+
[<InlineData(false, false)>]
158+
[<InlineData(false, true)>]
159+
[<InlineData(true, false)>]
160+
[<InlineData(true, true)>]
161+
let ``TaskSeq-skipWhileXXX prove it does not read beyond the failing yield`` (inclusive, isAsync) = task {
162+
let mutable x = 42 // for this test, the potential mutation should not actually occur
163+
let functionToTest = getFunction inclusive isAsync ((=) 42)
164+
165+
let items = taskSeq {
166+
yield x // Always passes the test; always returned
167+
yield x * 2 // the failing item (which will also be yielded in the result when using *Inclusive)
168+
x <- x + 1 // we are proving we never get here
169+
}
170+
171+
let expected = if inclusive then [| 42; 84 |] else [| 42 |]
172+
173+
let! first = items |> functionToTest |> TaskSeq.toArrayAsync
174+
let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync
175+
176+
first |> should equal expected
177+
repeat |> should equal expected
178+
x |> should equal 42
179+
}
180+
181+
[<Theory>]
182+
[<InlineData(false, false)>]
183+
[<InlineData(false, true)>]
184+
[<InlineData(true, false)>]
185+
[<InlineData(true, true)>]
186+
let ``TaskSeq-skipWhileXXX prove side effects are executed`` (inclusive, isAsync) = task {
187+
let mutable x = 41
188+
let functionToTest = getFunction inclusive isAsync ((>) 50)
189+
190+
let items = taskSeq {
191+
x <- x + 1
192+
yield x
193+
x <- x + 2
194+
yield x * 2
195+
x <- x + 200 // as previously proven, we should not trigger this
196+
}
197+
198+
let expectedFirst = if inclusive then [| 42; 44 * 2 |] else [| 42 |]
199+
let expectedRepeat = if inclusive then [| 45; 47 * 2 |] else [| 45 |]
200+
201+
let! first = items |> functionToTest |> TaskSeq.toArrayAsync
202+
x |> should equal 44
203+
let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync
204+
x |> should equal 47
205+
206+
first |> should equal expectedFirst
207+
repeat |> should equal expectedRepeat
208+
}
209+
210+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
211+
let ``TaskSeq-skipWhile consumes the prefix of a longer sequence, with mutation`` variant = task {
212+
let ts = Gen.getSeqWithSideEffect variant
213+
214+
let! first =
215+
TaskSeq.skipWhile (fun x -> x < 5) ts
216+
|> TaskSeq.toArrayAsync
217+
218+
let expected = [| 1..4 |]
219+
first |> should equal expected
220+
221+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
222+
let! repeat =
223+
TaskSeq.skipWhile (fun x -> x < 5) ts
224+
|> TaskSeq.toArrayAsync
225+
226+
repeat |> should not' (equal expected)
227+
}
228+
229+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
230+
let ``TaskSeq-skipWhileInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task {
231+
let ts = Gen.getSeqWithSideEffect variant
232+
233+
let! first =
234+
TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts
235+
|> TaskSeq.toArrayAsync
236+
237+
let expected = [| 1..5 |]
238+
first |> should equal expected
239+
240+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
241+
let! repeat =
242+
TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts
243+
|> TaskSeq.toArrayAsync
244+
245+
repeat |> should not' (equal expected)
246+
}
247+
248+
module Other =
249+
[<Theory>]
250+
[<InlineData(false, false)>]
251+
[<InlineData(false, true)>]
252+
[<InlineData(true, false)>]
253+
[<InlineData(true, true)>]
254+
let ``TaskSeq-skipWhileXXX exclude all items after predicate fails`` (inclusive, isAsync) =
255+
let functionToTest = With.getFunction inclusive isAsync
256+
257+
[ 1; 2; 2; 3; 3; 2; 1 ]
258+
|> TaskSeq.ofSeq
259+
|> functionToTest (fun x -> x <= 2)
260+
|> verifyDigitsAsString (if inclusive then "ABBC" else "ABB")
261+
262+
[<Theory>]
263+
[<InlineData(false, false)>]
264+
[<InlineData(false, true)>]
265+
[<InlineData(true, false)>]
266+
[<InlineData(true, true)>]
267+
let ``TaskSeq-skipWhileXXX stops consuming after predicate fails`` (inclusive, isAsync) =
268+
let functionToTest = With.getFunction inclusive isAsync
269+
270+
seq {
271+
yield! [ 1; 2; 2; 3; 3 ]
272+
yield failwith "Too far"
273+
}
274+
|> TaskSeq.ofSeq
275+
|> functionToTest (fun x -> x <= 2)
276+
|> verifyDigitsAsString (if inclusive then "ABBC" else "ABB")

src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ module With =
3939
res
4040

4141
module EmptySeq =
42+
43+
// TaskSeq-takeWhile+A stands for:
44+
// takeWhile + takeWhileAsync etc.
45+
4246
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
4347
let ``TaskSeq-takeWhile+A has no effect`` variant = task {
4448
do!

0 commit comments

Comments
 (0)