Skip to content

Commit 5569447

Browse files
test: add 67 tests for TaskSeq.lengthOrMax (previously untested)
- covers null validation, empty sequences, immutable sequences, and side effects - documents the read-ahead characteristic: for max=N with a longer source, N+1 elements are evaluated (implementation calls MoveNextAsync once before the while loop, then once per iteration) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 41f84e8 commit 5569447

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Release notes:
1313
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
1414
- adds TaskSeq.distinct, TaskSeq.distinctBy, TaskSeq.distinctByAsync
1515
- performance: TaskSeq.exists, existsAsync, contains no longer allocate an intermediate Option value
16+
- test: adds 67 tests for TaskSeq.lengthOrMax (previously untested)
1617
- adds TaskSeq.mapFold and TaskSeq.mapFoldAsync
1718
- adds TaskSeq.sum, sumBy, sumByAsync, average, averageBy, averageByAsync
1819
- adds TaskSeq.reduce and TaskSeq.reduceAsync, #289

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open FSharp.Control
77

88
//
99
// TaskSeq.length
10+
// TaskSeq.lengthOrMax
1011
// TaskSeq.lengthBy
1112
// TaskSeq.lengthByAsync
1213
//
@@ -15,6 +16,7 @@ module EmptySeq =
1516
[<Fact>]
1617
let ``Null source is invalid`` () =
1718
assertNullArg <| fun () -> TaskSeq.length null
19+
assertNullArg <| fun () -> TaskSeq.lengthOrMax 10 null
1820

1921
assertNullArg
2022
<| fun () -> TaskSeq.lengthBy (fun _ -> false) null
@@ -46,6 +48,18 @@ module EmptySeq =
4648
len |> should equal 0
4749
}
4850

51+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
52+
let ``TaskSeq-lengthOrMax on empty sequence returns 0 regardless of max`` variant = task {
53+
let! len = Gen.getEmptyVariant variant |> TaskSeq.lengthOrMax 100
54+
len |> should equal 0
55+
}
56+
57+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
58+
let ``TaskSeq-lengthOrMax on empty sequence with max=0 returns 0`` variant = task {
59+
let! len = Gen.getEmptyVariant variant |> TaskSeq.lengthOrMax 0
60+
len |> should equal 0
61+
}
62+
4963
module Immutable =
5064
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
5165
let ``TaskSeq-length returns proper length`` variant = task {
@@ -89,6 +103,41 @@ module Immutable =
89103
do! run (fun x -> x % 3 = 2) |> Task.map (should equal 3) // [2; 5; 8]
90104
}
91105

106+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
107+
let ``TaskSeq-lengthOrMax returns actual length when sequence is shorter than max`` variant = task {
108+
// source has 10 items; max=100 → actual length 10 is returned
109+
let! len = Gen.getSeqImmutable variant |> TaskSeq.lengthOrMax 100
110+
len |> should equal 10
111+
}
112+
113+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
114+
let ``TaskSeq-lengthOrMax returns max when sequence is longer than max`` variant = task {
115+
// source has 10 items; max=5 → capped at 5
116+
let! len = Gen.getSeqImmutable variant |> TaskSeq.lengthOrMax 5
117+
len |> should equal 5
118+
}
119+
120+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
121+
let ``TaskSeq-lengthOrMax returns max when sequence is exactly max`` variant = task {
122+
// source has 10 items; max=10 → returns 10
123+
let! len = Gen.getSeqImmutable variant |> TaskSeq.lengthOrMax 10
124+
len |> should equal 10
125+
}
126+
127+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
128+
let ``TaskSeq-lengthOrMax with max=1 returns 1 for any non-empty sequence`` variant = task {
129+
let! len = Gen.getSeqImmutable variant |> TaskSeq.lengthOrMax 1
130+
len |> should equal 1
131+
}
132+
133+
[<Fact>]
134+
let ``TaskSeq-lengthOrMax with max=0 always returns 0 regardless of source`` () = task {
135+
// max=0: the while loop condition (i < max) is false from the start → 0 returned
136+
// NOTE: the implementation still calls MoveNextAsync once before the loop
137+
let! len = TaskSeq.ofList [ 1..100 ] |> TaskSeq.lengthOrMax 0
138+
len |> should equal 0
139+
}
140+
92141
module SideEffects =
93142
[<Fact>]
94143
let ``TaskSeq-length prove we execute after-effects`` () = task {
@@ -241,3 +290,65 @@ module SideEffects =
241290
do! run (fun x -> x % 3 = 2) |> Task.map (should equal 3) // [23; 26; 29] // id
242291
do! run (fun x -> x % 3 = 1) |> Task.map (should equal 4) // [31; 34; 37; 40] // id
243292
}
293+
294+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
295+
let ``TaskSeq-lengthOrMax returns correct length when below max`` variant = task {
296+
// side-effect sequence yields 10 items on first run
297+
let! len = Gen.getSeqWithSideEffect variant |> TaskSeq.lengthOrMax 100
298+
len |> should equal 10
299+
}
300+
301+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
302+
let ``TaskSeq-lengthOrMax returns max and stops evaluation when sequence exceeds max`` variant = task {
303+
// source has 10 items; max=5 → should stop early
304+
// NOTE: the implementation reads one element ahead (N+1 total MoveNextAsync calls for result N)
305+
let mutable evaluated = 0
306+
307+
let ts = taskSeq {
308+
for item in Gen.getSeqWithSideEffect variant do
309+
evaluated <- evaluated + 1
310+
yield item
311+
}
312+
313+
let! len = ts |> TaskSeq.lengthOrMax 5
314+
len |> should equal 5
315+
// exactly max+1 elements are pulled from the source due to read-ahead
316+
evaluated |> should equal 6
317+
}
318+
319+
[<Fact>]
320+
let ``TaskSeq-lengthOrMax stops evaluating source after reaching max - read-ahead characteristic`` () = task {
321+
// NOTE: the implementation calls MoveNextAsync once before the while loop,
322+
// then once more per iteration. For max=N and a longer source, this means N+1 calls total.
323+
let mutable sideEffects = 0
324+
325+
let ts = taskSeq {
326+
for i in 1..100 do
327+
sideEffects <- sideEffects + 1
328+
yield i
329+
}
330+
331+
let! len = ts |> TaskSeq.lengthOrMax 7
332+
len |> should equal 7
333+
// max+1 elements are evaluated due to the read-ahead implementation pattern
334+
sideEffects |> should equal 8
335+
}
336+
337+
[<Fact>]
338+
let ``TaskSeq-lengthOrMax with max=0 still evaluates the first element due to read-ahead`` () = task {
339+
// The implementation unconditionally calls MoveNextAsync once before entering
340+
// the while loop, so even max=0 evaluates the first element of the source.
341+
let mutable sideEffects = 0
342+
343+
let ts = taskSeq {
344+
sideEffects <- sideEffects + 1
345+
yield 1
346+
sideEffects <- sideEffects + 1
347+
yield 2
348+
}
349+
350+
let! len = ts |> TaskSeq.lengthOrMax 0
351+
len |> should equal 0
352+
// one element was evaluated despite max=0
353+
sideEffects |> should equal 1
354+
}

0 commit comments

Comments
 (0)