Skip to content

Commit 391f438

Browse files
committed
Add tests for TaskSeq.removeAt and removeManyAt
1 parent 42269e5 commit 391f438

File tree

3 files changed

+294
-1
lines changed

3 files changed

+294
-1
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
@@ -37,6 +37,7 @@
3737
<Compile Include="TaskSeq.MaxMin.Tests.fs" />
3838
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
3939
<Compile Include="TaskSeq.Pick.Tests.fs" />
40+
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
4041
<Compile Include="TaskSeq.Singleton.Tests.fs" />
4142
<Compile Include="TaskSeq.Skip.Tests.fs" />
4243
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module EmptySeq =
2323
|> verifySingleton 42
2424

2525
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
26-
let ``TaskSeq-insertAt(1) on empty input should throw InvalidOperation`` variant =
26+
let ``TaskSeq-insertAt(1) on empty input should throw ArgumentException`` variant =
2727
fun () ->
2828
Gen.getEmptyVariant variant
2929
|> TaskSeq.insertAt 1 42
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
module TaskSeq.Tests.RemoveAt
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
11+
//
12+
// TaskSeq.removeAt
13+
// TaskSeq.removeManyAt
14+
//
15+
16+
exception SideEffectPastEnd of string
17+
18+
module EmptySeq =
19+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
20+
let ``TaskSeq-removeAt(0) on empty input raises`` variant =
21+
fun () ->
22+
Gen.getEmptyVariant variant
23+
|> TaskSeq.removeAt 0
24+
|> consumeTaskSeq
25+
26+
|> should throwAsyncExact typeof<ArgumentException>
27+
28+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
29+
let ``TaskSeq-removeManyAt(0) on empty input raises`` variant =
30+
fun () ->
31+
Gen.getEmptyVariant variant
32+
|> TaskSeq.removeManyAt 0 0
33+
|> consumeTaskSeq
34+
35+
|> should throwAsyncExact typeof<ArgumentException>
36+
37+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
38+
let ``TaskSeq-removeAt(-1) on empty input should throw ArgumentException without consuming`` variant =
39+
fun () ->
40+
Gen.getEmptyVariant variant
41+
|> TaskSeq.removeAt -1
42+
|> consumeTaskSeq
43+
44+
|> should throwAsyncExact typeof<ArgumentException>
45+
46+
fun () -> Gen.getEmptyVariant variant |> TaskSeq.removeAt -1 |> ignore // task is not awaited
47+
48+
|> should throw typeof<ArgumentException>
49+
50+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
51+
let ``TaskSeq-removeManyAt(-1) on empty input should throw ArgumentException without consuming`` variant =
52+
fun () ->
53+
Gen.getEmptyVariant variant
54+
|> TaskSeq.removeManyAt -1 0
55+
|> consumeTaskSeq
56+
57+
|> should throwAsyncExact typeof<ArgumentException>
58+
59+
fun () ->
60+
Gen.getEmptyVariant variant
61+
|> TaskSeq.removeManyAt -1 0
62+
|> ignore
63+
64+
|> should throw typeof<ArgumentException>
65+
66+
module Immutable =
67+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
68+
let ``TaskSeq-removeAt can remove last item`` variant = task {
69+
do!
70+
Gen.getSeqImmutable variant
71+
|> TaskSeq.removeAt 9
72+
|> verifyDigitsAsString "ABCDEFGHI"
73+
}
74+
75+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
76+
let ``TaskSeq-removeAt removes the item at indexed positions`` variant = task {
77+
78+
do!
79+
Gen.getSeqImmutable variant
80+
|> TaskSeq.removeAt 0
81+
|> verifyDigitsAsString "BCDEFGHIJ"
82+
83+
do!
84+
Gen.getSeqImmutable variant
85+
|> TaskSeq.removeAt 1
86+
|> verifyDigitsAsString "ACDEFGHIJ"
87+
88+
do!
89+
Gen.getSeqImmutable variant
90+
|> TaskSeq.removeAt 5
91+
|> verifyDigitsAsString "ABCDEGHIJ"
92+
93+
}
94+
95+
96+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
97+
let ``TaskSeq-removeAt can be repeated in a chain`` variant = task {
98+
99+
do!
100+
Gen.getSeqImmutable variant
101+
|> TaskSeq.removeAt 0
102+
|> TaskSeq.removeAt 0
103+
|> TaskSeq.removeAt 0
104+
|> TaskSeq.removeAt 0
105+
|> TaskSeq.removeAt 0
106+
|> verifyDigitsAsString "FGHIJ"
107+
108+
do!
109+
Gen.getSeqImmutable variant
110+
|> TaskSeq.removeAt 9
111+
|> TaskSeq.removeAt 8
112+
|> TaskSeq.removeAt 7
113+
|> TaskSeq.removeAt 6
114+
|> TaskSeq.removeAt 5 // sequence gets shorter, pick last
115+
|> verifyDigitsAsString "ABCDE"
116+
}
117+
118+
[<Fact>]
119+
let ``TaskSeq-removeAt can be applied to an infinite task sequence`` () =
120+
TaskSeq.initInfinite id
121+
|> TaskSeq.removeAt 10_000
122+
|> TaskSeq.item 10_000
123+
|> Task.map (should equal 10_001)
124+
125+
126+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
127+
let ``TaskSeq-removeAt throws when there are not enough elements`` variant =
128+
fun () ->
129+
TaskSeq.singleton 1
130+
// remove after 1
131+
|> TaskSeq.removeAt 2
132+
|> consumeTaskSeq
133+
134+
|> should throwAsyncExact typeof<ArgumentException>
135+
136+
fun () ->
137+
Gen.getSeqImmutable variant
138+
|> TaskSeq.removeAt 10
139+
|> consumeTaskSeq
140+
141+
|> should throwAsyncExact typeof<ArgumentException>
142+
143+
fun () ->
144+
Gen.getSeqImmutable variant
145+
|> TaskSeq.removeAt 10_000_000
146+
|> consumeTaskSeq
147+
148+
|> should throwAsyncExact typeof<ArgumentException>
149+
150+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
151+
let ``TaskSeq-removeManyAt can remove last item`` variant = task {
152+
do!
153+
Gen.getSeqImmutable variant
154+
|> TaskSeq.removeManyAt 9 1
155+
|> verifyDigitsAsString "ABCDEFGHI"
156+
}
157+
158+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
159+
let ``TaskSeq-removeManyAt can remove multiple items`` variant = task {
160+
do!
161+
Gen.getSeqImmutable variant
162+
|> TaskSeq.removeManyAt 1 5
163+
|> verifyDigitsAsString "AGHIJ"
164+
}
165+
166+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
167+
let ``TaskSeq-removeManyAt can with a large count past the end of the sequence is fine`` variant = task {
168+
do!
169+
Gen.getSeqImmutable variant
170+
|> TaskSeq.removeManyAt 2 20_000 // try to remove too many is fine, like Seq.removeManyAt (regardless the docs at time of writing)
171+
|> verifyDigitsAsString "AB"
172+
}
173+
174+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
175+
let ``TaskSeq-removeManyAt does not remove any item when count is zero`` variant = task {
176+
do!
177+
Gen.getSeqImmutable variant
178+
|> TaskSeq.removeManyAt 9 0
179+
|> verifyDigitsAsString "ABCDEFGHIJ"
180+
}
181+
182+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
183+
let ``TaskSeq-removeManyAt does not remove any item when count is negative`` variant = task {
184+
do!
185+
Gen.getSeqImmutable variant
186+
|> TaskSeq.removeManyAt 1 -99
187+
|> verifyDigitsAsString "ABCDEFGHIJ"
188+
}
189+
190+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
191+
let ``TaskSeq-removeManyAt removes items at indexed positions`` variant = task {
192+
193+
do!
194+
Gen.getSeqImmutable variant
195+
|> TaskSeq.removeManyAt 0 5
196+
|> verifyDigitsAsString "FGHIJ"
197+
198+
do!
199+
Gen.getSeqImmutable variant
200+
|> TaskSeq.removeManyAt 1 3
201+
|> verifyDigitsAsString "AEFGHIJ"
202+
203+
do!
204+
Gen.getSeqImmutable variant
205+
|> TaskSeq.removeManyAt 5 5
206+
|> verifyDigitsAsString "ABCDE"
207+
208+
}
209+
210+
211+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
212+
let ``TaskSeq-removeManyAt can be repeated in a chain`` variant = task {
213+
214+
do!
215+
Gen.getSeqImmutable variant
216+
|> TaskSeq.removeManyAt 0 1
217+
|> TaskSeq.removeManyAt 0 2
218+
|> TaskSeq.removeManyAt 0 3
219+
|> verifyDigitsAsString "GHIJ"
220+
221+
do!
222+
Gen.getSeqImmutable variant
223+
|> TaskSeq.removeManyAt 9 1 // pick last, result ABCDEFGHIJ
224+
|> TaskSeq.removeManyAt 6 1 // then from 6th pos, result ABCDEFHI
225+
|> TaskSeq.removeManyAt 3 2 // from 3rd pos take 2, result ABCFHI
226+
|> TaskSeq.removeManyAt 0 2 // from start, take 2, result CFHI
227+
|> verifyDigitsAsString "CFHI"
228+
}
229+
230+
[<Fact>]
231+
let ``TaskSeq-removeManyAt can be applied to an infinite task sequence`` () =
232+
TaskSeq.initInfinite id
233+
|> TaskSeq.removeManyAt 10_000 5_000
234+
|> TaskSeq.item 12_000
235+
|> Task.map (should equal 17_000)
236+
237+
238+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
239+
let ``TaskSeq-removeManyAt throws when there are not enough elements for index`` variant =
240+
// NOTE: only raises if INDEX is out of bounds, not when COUNT is out of bounds!!!
241+
242+
fun () ->
243+
TaskSeq.singleton 1
244+
// remove after 1
245+
|> TaskSeq.removeManyAt 2 0 // regardless of count, it should raise
246+
|> consumeTaskSeq
247+
248+
|> should throwAsyncExact typeof<ArgumentException>
249+
250+
fun () ->
251+
Gen.getSeqImmutable variant
252+
|> TaskSeq.removeManyAt 10 5
253+
|> consumeTaskSeq
254+
255+
|> should throwAsyncExact typeof<ArgumentException>
256+
257+
fun () ->
258+
Gen.getSeqImmutable variant
259+
|> TaskSeq.removeManyAt 10_000_000 -5 // even with neg. count
260+
|> consumeTaskSeq
261+
262+
|> should throwAsyncExact typeof<ArgumentException>
263+
264+
265+
266+
module SideEffects =
267+
268+
// NOTES:
269+
//
270+
// no tests, it is not possible to create a meaningful side-effect test, as any consuming after
271+
// removing an item would logically require the side effect to be executed the normal way
272+
273+
// PoC test
274+
[<Fact>]
275+
let ``Seq-removeAt (poc-proof) will execute side effect before index`` () =
276+
// NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module
277+
// this shows that Seq.removeAt executes more side effects than necessary.
278+
279+
let mutable x = 42
280+
281+
let items = seq {
282+
yield x
283+
x <- x + 1 // we are proving this gets executed with removeAt(0), BUT this is the result of Seq.item
284+
yield x * 2
285+
}
286+
287+
items
288+
|> Seq.removeAt 0
289+
|> Seq.item 0 // consume anything (this is why there's nothing to test with Seq.removeAt, as this is always true after removing an item)
290+
|> ignore
291+
292+
x |> should equal 43 // one time side effect executed. QED

0 commit comments

Comments
 (0)