Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Release notes:

Unreleased
- test: add SideEffects module to TaskSeq.ReplicateInfinite.Tests.fs and TaskSeq.ThreadState.Tests.fs, verifying computation/generator/folder call counts and re-enumeration semantics
- tests: add coverage for side-effect re-execution semantics when re-enumerating a `taskSeq` with independent `CancellationToken`s
- test: add TaskSeq.Issue452.Tests.fs, regression tests wrapping an externally-produced IAsyncEnumerable<'T> (TaskSeq.map and `taskSeq { for .. in .. do yield .. }`) while running on a custom, single-threaded TaskScheduler; investigates #452's reported duplicated-final-item bug, which could not be reproduced outside of Orleans, see #452
- adds TaskSeq.tryMax and TaskSeq.tryMin: safe variants of TaskSeq.max and TaskSeq.min that return None instead of raising ArgumentException when the input sequence is empty
Expand Down
68 changes: 68 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.ReplicateInfinite.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,71 @@ module ReplicateUntilNoneAsync =
arr[0] |> should equal 1
arr[count - 1] |> should equal count
}


module SideEffects =
[<Fact>]
let ``TaskSeq-replicateInfiniteAsync re-runs the computation on each fresh enumeration`` () = task {
let mutable calls = 0

let comp () = task {
calls <- calls + 1
return calls
}

let ts = TaskSeq.replicateInfiniteAsync comp

let! arr1 = ts |> TaskSeq.take 3 |> TaskSeq.toArrayAsync
arr1 |> should equal [| 1; 2; 3 |]
calls |> should equal 3

// a fresh enumeration starts the computation from scratch; side effects
// (here, the call counter) keep accumulating across enumerations
let! arr2 = ts |> TaskSeq.take 2 |> TaskSeq.toArrayAsync
arr2 |> should equal [| 4; 5 |]
calls |> should equal 5
}

[<Fact>]
let ``TaskSeq-replicateUntilNoneAsync re-runs the computation from its initial state on each fresh enumeration`` () = task {
let mutable totalCalls = 0

let comp () = task {
let mutable n = 0
totalCalls <- totalCalls + 1

if n <= 1 then
n <- n + 1
return Some n
else
return None
}

let ts = TaskSeq.replicateUntilNoneAsync comp

let! arr1 = ts |> TaskSeq.toArrayAsync
arr1 |> should equal [| 1 |]
totalCalls |> should equal 2

// re-enumerating re-invokes the generator function itself (state is local
// to each call), so side effects on shared state accumulate further
let! arr2 = ts |> TaskSeq.toArrayAsync
arr2 |> should equal [| 1 |]
totalCalls |> should equal 4
}

[<Fact>]
let ``TaskSeq-replicateInfinite abandoning enumeration early does not affect a later fresh enumeration`` () = task {
let ts = TaskSeq.replicateInfinite 3

// partially enumerate and abandon (dispose) without reaching a natural end
use enum1 = ts.GetAsyncEnumerator System.Threading.CancellationToken.None
let! hasNext = enum1.MoveNextAsync()
hasNext |> should be True
enum1.Current |> should equal 3
do! enum1.DisposeAsync()

// a fresh enumerator over the same taskSeq starts cleanly from the beginning
let! arr = ts |> TaskSeq.take 3 |> TaskSeq.toArrayAsync
arr |> should equal [| 3; 3; 3 |]
}
68 changes: 68 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.ThreadState.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,71 @@ module Functionality =

viaThread |> should equal viaScan
}


module SideEffects =
[<Fact>]
let ``TaskSeq-threadState folder is invoked exactly once per source item`` () = task {
let mutable folderCalls = 0

let source = taskSeq {
for i in 1..5 do
yield i
}

let folder state x =
folderCalls <- folderCalls + 1
x + state, state + 1

let! result = TaskSeq.threadState folder 0 source |> TaskSeq.toArrayAsync
result |> should haveLength 5
folderCalls |> should equal 5
}

[<Fact>]
let ``TaskSeq-threadState re-enumerating the result re-runs source side effects and folder calls`` () = task {
let mutable itemsProduced = 0
let mutable folderCalls = 0

let source = taskSeq {
for i in 1..3 do
itemsProduced <- itemsProduced + 1
yield i
}

let folder state x =
folderCalls <- folderCalls + 1
x + state, state + 1

let ts = TaskSeq.threadState folder 0 source

let! result1 = ts |> TaskSeq.toArrayAsync
result1 |> should equal [| 1; 3; 5 |]
itemsProduced |> should equal 3
folderCalls |> should equal 3

// threadState produces a fresh taskSeq that re-drives the source and the
// folder from scratch on each independent enumeration
let! result2 = ts |> TaskSeq.toArrayAsync
result2 |> should equal result1
itemsProduced |> should equal 6
folderCalls |> should equal 6
}

[<Fact>]
let ``TaskSeq-threadStateAsync folder is invoked exactly once per source item`` () = task {
let mutable folderCalls = 0
let source = taskSeq { yield! [ 1..4 ] }

let folder state x = task {
folderCalls <- folderCalls + 1
return x + state, state + 1
}

let! result =
TaskSeq.threadStateAsync folder 0 source
|> TaskSeq.toArrayAsync

result |> should haveLength 4
folderCalls |> should equal 4
}