Skip to content
Merged
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
- 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
- test: rename `SideEffect` module to `SideEffects` in TaskSeq.Concat.Tests.fs, TaskSeq.Delay.Tests.fs, and TaskSeq.Item.Tests.fs for consistency with the rest of the test suite (50+ files already use the plural form)
Expand Down
80 changes: 80 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.CancellationToken.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module TaskSeq.Tests.CancellationToken

open System
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks

Expand Down Expand Up @@ -152,3 +153,82 @@ module Cancellation =
hasNext |> should be True
enum2.Current |> should equal 1
}

module SideEffects =

[<Fact>]
let ``Cancelling one enumerator does not affect side effects of a fresh enumerator over the same taskSeq`` () = task {
let mutable itemsProduced = 0

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

// fully consume with a first, never-cancelled enumerator
use cts1 = new CancellationTokenSource()
use enum1 = source.GetAsyncEnumerator(cts1.Token)
let mutable canContinue = true

while canContinue do
let! hasNext = enum1.MoveNextAsync()

if not hasNext then
canContinue <- false

itemsProduced |> should equal 5

// cancel and dispose that first enumerator explicitly, then re-enumerate the same
// taskSeq from scratch with a fresh, non-cancelled token
cts1.Cancel()
do! enum1.DisposeAsync()

use cts2 = new CancellationTokenSource()
use enum2 = source.GetAsyncEnumerator(cts2.Token)
let! hasNext = enum2.MoveNextAsync()

// re-enumeration re-runs the body from scratch: side effects accumulate further,
// and the previous enumerator's cancellation has no bearing on this fresh one
hasNext |> should be True
enum2.Current |> should equal 1
itemsProduced |> should equal 6
}

[<Fact>]
let ``A CancellationToken passed to GetAsyncEnumerator does not prevent re-iteration with a different token`` () = task {
let mutable totalCalls = 0

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

let drain (enum: IAsyncEnumerator<int>) = task {
let items = ResizeArray()
let mutable canContinue = true

while canContinue do
let! hasNext = enum.MoveNextAsync()

if hasNext then
items.Add enum.Current
else
canContinue <- false

return List.ofSeq items
}

use cts = new CancellationTokenSource()
use enum1 = source.GetAsyncEnumerator(cts.Token)
let! first = drain enum1
first |> should equal [ 1; 2; 3 ]
totalCalls |> should equal 3

// re-iterate using CancellationToken.None: side effects re-run independently
use enum2 = source.GetAsyncEnumerator(CancellationToken.None)
let! second = drain enum2
second |> should equal [ 1; 2; 3 ]
totalCalls |> should equal 6
}
Loading