|
1 | 1 | module TaskSeq.Tests.CancellationToken |
2 | 2 |
|
3 | 3 | open System |
| 4 | +open System.Collections.Generic |
4 | 5 | open System.Threading |
5 | 6 | open System.Threading.Tasks |
6 | 7 |
|
@@ -152,3 +153,82 @@ module Cancellation = |
152 | 153 | hasNext |> should be True |
153 | 154 | enum2.Current |> should equal 1 |
154 | 155 | } |
| 156 | + |
| 157 | +module SideEffects = |
| 158 | + |
| 159 | + [<Fact>] |
| 160 | + let ``Cancelling one enumerator does not affect side effects of a fresh enumerator over the same taskSeq`` () = task { |
| 161 | + let mutable itemsProduced = 0 |
| 162 | + |
| 163 | + let source = taskSeq { |
| 164 | + for i in 1..5 do |
| 165 | + itemsProduced <- itemsProduced + 1 |
| 166 | + yield i |
| 167 | + } |
| 168 | + |
| 169 | + // fully consume with a first, never-cancelled enumerator |
| 170 | + use cts1 = new CancellationTokenSource() |
| 171 | + use enum1 = source.GetAsyncEnumerator(cts1.Token) |
| 172 | + let mutable canContinue = true |
| 173 | + |
| 174 | + while canContinue do |
| 175 | + let! hasNext = enum1.MoveNextAsync() |
| 176 | + |
| 177 | + if not hasNext then |
| 178 | + canContinue <- false |
| 179 | + |
| 180 | + itemsProduced |> should equal 5 |
| 181 | + |
| 182 | + // cancel and dispose that first enumerator explicitly, then re-enumerate the same |
| 183 | + // taskSeq from scratch with a fresh, non-cancelled token |
| 184 | + cts1.Cancel() |
| 185 | + do! enum1.DisposeAsync() |
| 186 | + |
| 187 | + use cts2 = new CancellationTokenSource() |
| 188 | + use enum2 = source.GetAsyncEnumerator(cts2.Token) |
| 189 | + let! hasNext = enum2.MoveNextAsync() |
| 190 | + |
| 191 | + // re-enumeration re-runs the body from scratch: side effects accumulate further, |
| 192 | + // and the previous enumerator's cancellation has no bearing on this fresh one |
| 193 | + hasNext |> should be True |
| 194 | + enum2.Current |> should equal 1 |
| 195 | + itemsProduced |> should equal 6 |
| 196 | + } |
| 197 | + |
| 198 | + [<Fact>] |
| 199 | + let ``A CancellationToken passed to GetAsyncEnumerator does not prevent re-iteration with a different token`` () = task { |
| 200 | + let mutable totalCalls = 0 |
| 201 | + |
| 202 | + let source = taskSeq { |
| 203 | + for i in 1..3 do |
| 204 | + totalCalls <- totalCalls + 1 |
| 205 | + yield i |
| 206 | + } |
| 207 | + |
| 208 | + let drain (enum: IAsyncEnumerator<int>) = task { |
| 209 | + let items = ResizeArray() |
| 210 | + let mutable canContinue = true |
| 211 | + |
| 212 | + while canContinue do |
| 213 | + let! hasNext = enum.MoveNextAsync() |
| 214 | + |
| 215 | + if hasNext then |
| 216 | + items.Add enum.Current |
| 217 | + else |
| 218 | + canContinue <- false |
| 219 | + |
| 220 | + return List.ofSeq items |
| 221 | + } |
| 222 | + |
| 223 | + use cts = new CancellationTokenSource() |
| 224 | + use enum1 = source.GetAsyncEnumerator(cts.Token) |
| 225 | + let! first = drain enum1 |
| 226 | + first |> should equal [ 1; 2; 3 ] |
| 227 | + totalCalls |> should equal 3 |
| 228 | + |
| 229 | + // re-iterate using CancellationToken.None: side effects re-run independently |
| 230 | + use enum2 = source.GetAsyncEnumerator(CancellationToken.None) |
| 231 | + let! second = drain enum2 |
| 232 | + second |> should equal [ 1; 2; 3 ] |
| 233 | + totalCalls |> should equal 6 |
| 234 | + } |
0 commit comments