Skip to content

Commit d28f5f5

Browse files
committed
Add tests for TaskSeq.take and truncate
1 parent 3f12fd9 commit d28f5f5

File tree

5 files changed

+261
-3
lines changed

5 files changed

+261
-3
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -35,8 +35,9 @@
3535
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
3636
<Compile Include="TaskSeq.Pick.Tests.fs" />
3737
<Compile Include="TaskSeq.Singleton.Tests.fs" />
38-
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
3938
<Compile Include="TaskSeq.Tail.Tests.fs" />
39+
<Compile Include="TaskSeq.Take.fs" />
40+
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
4041
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
4142
<Compile Include="TaskSeq.Zip.Tests.fs" />
4243
<Compile Include="TaskSeq.Tests.CE.fs" />

src/FSharp.Control.TaskSeq.Test/Nunit.Extensions.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,14 @@ module ExtraCustomMatchers =
138138
)
139139

140140
/// <summary>
141+
/// This makes a test BLOCKING!!! (TODO: get a better test framework?)
142+
///
141143
/// Asserts any exception that exactly matches the given exception <see cref="Type" />.
142144
/// Async exceptions are almost always nested in an <see cref="AggregateException" />, however, in an
143145
/// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and
144146
/// in cases where we just call <see cref="Task.Wait" />, and <see cref="AggregateException" /> will be raised regardless.
145147
/// This assertion will go over all nested exceptions and 'self', to find a matching exception.
148+
///
146149
/// Function to evaluate MUST return a <see cref="System.Threading.Tasks.Task" />, not a generic
147150
/// <see cref="Task&lt;'T>" />.
148151
/// Calls <see cref="Assert.ThrowsAnyAsync&lt;Exception>" /> of xUnit to ensure proper evaluation of async.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
module TaskSeq.Tests.Take
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.take
12+
// TaskSeq.truncate
13+
//
14+
15+
exception SideEffectPastEnd of string
16+
17+
[<AutoOpen>]
18+
module With =
19+
/// Turns a sequence of numbers into a string, starting with A for '1'
20+
let verifyAsString expected =
21+
TaskSeq.map char
22+
>> TaskSeq.map ((+) '@')
23+
>> TaskSeq.toArrayAsync
24+
>> Task.map (String >> should equal expected)
25+
26+
module EmptySeq =
27+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
28+
let ``TaskSeq-take(0) has no effect on empty input`` variant =
29+
// no `task` block needed
30+
Gen.getEmptyVariant variant |> TaskSeq.take 0 |> verifyEmpty
31+
32+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
33+
let ``TaskSeq-take(1) on empty input should throw InvalidOperation`` variant =
34+
fun () ->
35+
Gen.getEmptyVariant variant
36+
|> TaskSeq.take 1
37+
|> consumeTaskSeq
38+
39+
|> should throwAsyncExact typeof<ArgumentException>
40+
41+
[<Fact>]
42+
let ``TaskSeq-take(-1) should throw ArgumentException on any input`` () =
43+
fun () -> TaskSeq.empty<int> |> TaskSeq.take -1 |> consumeTaskSeq
44+
|> should throwAsyncExact typeof<ArgumentException>
45+
46+
fun () -> TaskSeq.init 10 id |> TaskSeq.take -1 |> consumeTaskSeq
47+
|> should throwAsyncExact typeof<ArgumentException>
48+
49+
[<Fact>]
50+
let ``TaskSeq-take(-1) should throw ArgumentException before awaiting`` () =
51+
fun () ->
52+
taskSeq {
53+
do! longDelay ()
54+
55+
if false then
56+
yield 0 // type inference
57+
}
58+
|> TaskSeq.take -1
59+
|> ignore // throws even without running the async. Bad coding, don't ignore a task!
60+
61+
|> should throw typeof<ArgumentException>
62+
63+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
64+
let ``TaskSeq-truncate(0) has no effect on empty input`` variant =
65+
Gen.getEmptyVariant variant
66+
|> TaskSeq.truncate 0
67+
|> verifyEmpty
68+
69+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
70+
let ``TaskSeq-truncate(99) does not throw on empty input`` variant =
71+
Gen.getEmptyVariant variant
72+
|> TaskSeq.truncate 99
73+
|> verifyEmpty
74+
75+
76+
[<Fact>]
77+
let ``TaskSeq-truncate(-1) should throw ArgumentException on any input`` () =
78+
fun () -> TaskSeq.empty<int> |> TaskSeq.truncate -1 |> consumeTaskSeq
79+
|> should throwAsyncExact typeof<ArgumentException>
80+
81+
fun () -> TaskSeq.init 10 id |> TaskSeq.truncate -1 |> consumeTaskSeq
82+
|> should throwAsyncExact typeof<ArgumentException>
83+
84+
[<Fact>]
85+
let ``TaskSeq-truncate(-1) should throw ArgumentException before awaiting`` () =
86+
fun () ->
87+
taskSeq {
88+
do! longDelay ()
89+
90+
if false then
91+
yield 0 // type inference
92+
}
93+
|> TaskSeq.truncate -1
94+
|> ignore // throws even without running the async. Bad coding, don't ignore a task!
95+
96+
|> should throw typeof<ArgumentException>
97+
98+
module Immutable =
99+
100+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
101+
let ``TaskSeq-take returns exactly 'count' items`` variant = task {
102+
103+
do! Gen.getSeqImmutable variant |> TaskSeq.take 0 |> verifyEmpty
104+
105+
do!
106+
Gen.getSeqImmutable variant
107+
|> TaskSeq.take 1
108+
|> verifyAsString "A"
109+
110+
do!
111+
Gen.getSeqImmutable variant
112+
|> TaskSeq.take 5
113+
|> verifyAsString "ABCDE"
114+
115+
do!
116+
Gen.getSeqImmutable variant
117+
|> TaskSeq.take 10
118+
|> verifyAsString "ABCDEFGHIJ"
119+
}
120+
121+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
122+
let ``TaskSeq-take throws when there are not enough elements`` variant =
123+
fun () -> TaskSeq.init 1 id |> TaskSeq.take 2 |> consumeTaskSeq
124+
125+
|> should throwAsyncExact typeof<ArgumentException>
126+
127+
fun () ->
128+
Gen.getSeqImmutable variant
129+
|> TaskSeq.take 11
130+
|> consumeTaskSeq
131+
132+
|> should throwAsyncExact typeof<ArgumentException>
133+
134+
fun () ->
135+
Gen.getSeqImmutable variant
136+
|> TaskSeq.take 10_000_000
137+
|> consumeTaskSeq
138+
139+
|> should throwAsyncExact typeof<ArgumentException>
140+
141+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
142+
let ``TaskSeq-truncate returns at least 'count' items`` variant = task {
143+
do!
144+
Gen.getSeqImmutable variant
145+
|> TaskSeq.truncate 0
146+
|> verifyEmpty
147+
148+
do!
149+
Gen.getSeqImmutable variant
150+
|> TaskSeq.truncate 1
151+
|> verifyAsString "A"
152+
153+
do!
154+
Gen.getSeqImmutable variant
155+
|> TaskSeq.truncate 5
156+
|> verifyAsString "ABCDE"
157+
158+
do!
159+
Gen.getSeqImmutable variant
160+
|> TaskSeq.truncate 10
161+
|> verifyAsString "ABCDEFGHIJ"
162+
163+
do!
164+
Gen.getSeqImmutable variant
165+
|> TaskSeq.truncate 11
166+
|> verifyAsString "ABCDEFGHIJ"
167+
168+
do!
169+
Gen.getSeqImmutable variant
170+
|> TaskSeq.truncate 10_000_000
171+
|> verifyAsString "ABCDEFGHIJ"
172+
}
173+
174+
module SideEffects =
175+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
176+
let ``TaskSeq-take gets enough items`` variant =
177+
Gen.getSeqWithSideEffect variant
178+
|> TaskSeq.take 5
179+
|> verifyAsString "ABCDE"
180+
181+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
182+
let ``TaskSeq-truncate gets enough items`` variant =
183+
Gen.getSeqWithSideEffect variant
184+
|> TaskSeq.truncate 5
185+
|> verifyAsString "ABCDE"
186+
187+
[<Fact>]
188+
let ``TaskSeq-take prove it does not read beyond the last yield`` () = task {
189+
let mutable x = 42 // for this test, the potential mutation should not actually occur
190+
191+
let items = taskSeq {
192+
yield x
193+
yield x * 2
194+
x <- x + 1 // we are proving we never get here
195+
}
196+
197+
let expected = [| 42; 84 |]
198+
199+
let! first = items |> TaskSeq.take 2 |> TaskSeq.toArrayAsync
200+
let! repeat = items |> TaskSeq.take 2 |> TaskSeq.toArrayAsync
201+
202+
first |> should equal expected
203+
repeat |> should equal expected // if we read too far, this is now [|43, 86|]
204+
x |> should equal 42 // expect: side-effect at end of taskseq not executed
205+
}
206+
207+
[<Fact>]
208+
let ``TaskSeq-take prove that an exception that is not consumed, is not raised`` () =
209+
let items = taskSeq {
210+
yield 1
211+
yield! [ 2; 3 ]
212+
do SideEffectPastEnd "at the end" |> raise // we SHOULD NOT get here
213+
}
214+
215+
items |> TaskSeq.take 3 |> verifyAsString "ABC"
216+
217+
218+
[<Fact>]
219+
let ``TaskSeq-take prove that an exception from the taskseq is thrown instead of exception from function`` () =
220+
let items = taskSeq {
221+
yield 42
222+
yield! [ 1; 2 ]
223+
do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised
224+
}
225+
226+
fun () -> items |> TaskSeq.take 4 |> consumeTaskSeq // this would raise ArgumentException normally
227+
|> should throwAsyncExact typeof<SideEffectPastEnd>
228+
229+
230+
[<Fact>]
231+
let ``TaskSeq-truncate prove it does not read beyond the last yield`` () = task {
232+
let mutable x = 42 // for this test, the potential mutation should not actually occur
233+
234+
let items = taskSeq {
235+
yield x
236+
yield x * 2
237+
x <- x + 1 // we are proving we never get here
238+
}
239+
240+
let expected = [| 42; 84 |]
241+
242+
let! first = items |> TaskSeq.truncate 2 |> TaskSeq.toArrayAsync
243+
let! repeat = items |> TaskSeq.truncate 2 |> TaskSeq.toArrayAsync
244+
245+
first |> should equal expected
246+
repeat |> should equal expected // if we read too far, this is now [|43, 86|]
247+
x |> should equal 42 // expect: side-effect at end of taskseq not executed
248+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module With =
2525
| true, false -> TaskSeq.takeWhileInclusive
2626
| true, true -> fun pred -> TaskSeq.takeWhileInclusiveAsync (pred >> Task.fromResult)
2727

28-
/// adds '@' to each number and concatenates the chars before calling 'should equal'
28+
/// Turns a sequence of numbers into a string, starting with A for '1'
2929
let verifyAsString expected =
3030
TaskSeq.map char
3131
>> TaskSeq.map ((+) '@')
@@ -74,6 +74,9 @@ module EmptySeq =
7474

7575
module Immutable =
7676

77+
// TaskSeq-takeWhile+A stands for:
78+
// takeWhile + takeWhileAsync etc.
79+
7780
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
7881
let ``TaskSeq-takeWhile+A filters correctly`` variant = task {
7982
do!

src/FSharp.Control.TaskSeq.Test/TestUtils.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ module TestUtils =
147147
/// Spin-waits, occasionally normal delay, between 50µs - 18,000µs
148148
let microDelay () = task { do! DelayHelper.delayTask 50L<µs> 18_000L<µs> (fun _ -> ()) }
149149

150+
/// Consumes and returns a Task (not a Task<unit>!!!)
151+
let consumeTaskSeq ts = TaskSeq.iter ignore ts |> Task.ignore
152+
150153
module Assert =
151154
/// Call MoveNextAsync() and check if return value is the expected value
152155
let moveNextAndCheck expected (enumerator: IAsyncEnumerator<_>) = task {

0 commit comments

Comments
 (0)