@@ -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+
4963module 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+
92141module 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