From a3cb3476a15d059e8fd437d3de0dddf16df737ec Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 01:06:40 +0200 Subject: [PATCH 01/10] Add tests for TaskSeq.exactlyOne and TaskSeq.tryExactlyOne --- .../FSharpy.TaskSeq.Test.fsproj | 1 + .../TaskSeq.ExactlyOne.Tests.fs | 57 ++++++++++++ .../TaskSeq.Head.Tests.fs | 88 ++++++++++++++----- src/FSharpy.TaskSeq.Test/TestUtils.fs | 32 +++++-- 4 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.ExactlyOne.Tests.fs diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index dd013994..4a0450ef 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -13,6 +13,7 @@ + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.ExactlyOne.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.ExactlyOne.Tests.fs new file mode 100644 index 00000000..f11ad2ac --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.ExactlyOne.Tests.fs @@ -0,0 +1,57 @@ +module FSharpy.Tests.Head + +open System +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy + + +[] +let ``TaskSeq-head throws on empty sequences`` () = task { + fun () -> TaskSeq.empty |> TaskSeq.head |> Task.ignore + |> should throwAsyncExact typeof +} + +[] +let ``TaskSeq-head throws on empty sequences - variant`` () = task { + fun () -> taskSeq { do () } |> TaskSeq.head |> Task.ignore + |> should throwAsyncExact typeof +} + +[] +let ``TaskSeq-tryHead returns None on empty sequences`` () = task { + let! nothing = TaskSeq.empty |> TaskSeq.tryHead + nothing |> should be None' +} + +[] +let ``TaskSeq-head gets the first item in a longer sequence`` () = task { + let! head = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 |> TaskSeq.head + + head |> should equal 1 +} + +[] +let ``TaskSeq-head gets the only item in a singleton sequence`` () = task { + let! head = taskSeq { yield 10 } |> TaskSeq.head + head |> should equal 10 +} + +[] +let ``TaskSeq-tryHead gets the first item in a longer sequence`` () = task { + let! head = + createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 + |> TaskSeq.tryHead + + head |> should be Some' + head |> should equal (Some 1) +} + +[] +let ``TaskSeq-tryHead gets the only item in a singleton sequence`` () = task { + let! head = taskSeq { yield 10 } |> TaskSeq.tryHead + head |> should be Some' + head |> should equal (Some 10) +} diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs index f11ad2ac..6377e63d 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs @@ -1,4 +1,4 @@ -module FSharpy.Tests.Head +module FSharpy.Tests.ExactlyOne open System open Xunit @@ -9,49 +9,91 @@ open FSharpy [] -let ``TaskSeq-head throws on empty sequences`` () = task { - fun () -> TaskSeq.empty |> TaskSeq.head |> Task.ignore +let ``TaskSeq-exactlyOne throws on empty sequences`` () = task { + fun () -> TaskSeq.empty |> TaskSeq.exactlyOne |> Task.ignore |> should throwAsyncExact typeof } [] -let ``TaskSeq-head throws on empty sequences - variant`` () = task { - fun () -> taskSeq { do () } |> TaskSeq.head |> Task.ignore +let ``TaskSeq-exactlyOne throws on empty sequences - variant`` () = task { + fun () -> taskSeq { do () } |> TaskSeq.exactlyOne |> Task.ignore |> should throwAsyncExact typeof } [] -let ``TaskSeq-tryHead returns None on empty sequences`` () = task { - let! nothing = TaskSeq.empty |> TaskSeq.tryHead +let ``TaskSeq-tryExactlyOne returns None on empty sequences`` () = task { + let! nothing = TaskSeq.empty |> TaskSeq.tryExactlyOne nothing |> should be None' } [] -let ``TaskSeq-head gets the first item in a longer sequence`` () = task { - let! head = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 |> TaskSeq.head +let ``TaskSeq-exactlyOne throws for a sequence of length = two`` () = task { + fun () -> + taskSeq { + yield 1 + yield 2 + } + |> TaskSeq.exactlyOne + |> Task.ignore + |> should throwAsyncExact typeof +} + +[] +let ``TaskSeq-exactlyOne throws for a sequence of length = two - variant`` () = task { + fun () -> + createDummyTaskSeqWith 50L<µs> 1000L<µs> 2 + |> TaskSeq.exactlyOne + |> Task.ignore + |> should throwAsyncExact typeof +} + + +[] +let ``TaskSeq-exactlyOne throws with a larger sequence`` () = task { + fun () -> + createDummyTaskSeqWith 50L<µs> 300L<µs> 200 + |> TaskSeq.exactlyOne + |> Task.ignore + |> should throwAsyncExact typeof +} - head |> should equal 1 +[] +let ``TaskSeq-tryExactlyOne returns None with a larger sequence`` () = task { + let! nothing = + createDummyTaskSeqWith 50L<µs> 300L<µs> 20 + |> TaskSeq.tryExactlyOne + + nothing |> should be None' } [] -let ``TaskSeq-head gets the only item in a singleton sequence`` () = task { - let! head = taskSeq { yield 10 } |> TaskSeq.head - head |> should equal 10 +let ``TaskSeq-exactlyOne gets the only item in a singleton sequence`` () = task { + let! exactlyOne = taskSeq { yield 10 } |> TaskSeq.exactlyOne + exactlyOne |> should equal 10 } [] -let ``TaskSeq-tryHead gets the first item in a longer sequence`` () = task { - let! head = - createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 - |> TaskSeq.tryHead +let ``TaskSeq-tryExactlyOne gets the only item in a singleton sequence`` () = task { + let! exactlyOne = taskSeq { yield 10 } |> TaskSeq.tryExactlyOne + exactlyOne |> should be Some' + exactlyOne |> should equal (Some 10) +} + +[] +let ``TaskSeq-exactlyOne gets the only item in a singleton sequence - variant`` () = task { + let! exactlyOne = + createLongerDummyTaskSeq 50 300 1 + |> TaskSeq.exactlyOne - head |> should be Some' - head |> should equal (Some 1) + exactlyOne |> should equal 1 } [] -let ``TaskSeq-tryHead gets the only item in a singleton sequence`` () = task { - let! head = taskSeq { yield 10 } |> TaskSeq.tryHead - head |> should be Some' - head |> should equal (Some 10) +let ``TaskSeq-tryExactlyOne gets the only item in a singleton sequence - variant`` () = task { + let! exactlyOne = + createLongerDummyTaskSeq 50 300 1 + |> TaskSeq.tryExactlyOne + + exactlyOne |> should be Some' + exactlyOne |> should equal (Some 1) } diff --git a/src/FSharpy.TaskSeq.Test/TestUtils.fs b/src/FSharpy.TaskSeq.Test/TestUtils.fs index 10a46e01..e3e3b85d 100644 --- a/src/FSharpy.TaskSeq.Test/TestUtils.fs +++ b/src/FSharpy.TaskSeq.Test/TestUtils.fs @@ -29,7 +29,7 @@ module DelayHelper = /// True to allow yielding the thread. If this is set to false, on single-proc systems /// this will prevent all other code from running. /// - let delayMicroseconds microseconds (allowThreadYield: bool) = + let spinWaitDelay (microseconds: int64<µs>) (allowThreadYield: bool) = let start = Stopwatch.GetTimestamp() let minimumTicks = int64 microseconds * Stopwatch.Frequency / 1_000_000L @@ -48,7 +48,7 @@ module DelayHelper = /// -/// Creates dummy tasks with a randomized delay and a mutable state, +/// Creates dummy backgroundTasks with a randomized delay and a mutable state, /// to ensure we properly test whether processing is done ordered or not. /// Default for and /// are 10,000µs and 30,000µs respectively (or 10ms and 30ms). @@ -63,10 +63,20 @@ type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) = // DO NOT use Thead.Sleep(), it's blocking! // WARNING: Task.Delay only has a 15ms timer resolution!!! //let! _ = Task.Delay(rnd ()) - let! _ = Task.Delay 0 // this creates a resume state, which seems more efficient than SpinWait.SpinOnce, see DelayHelper. - DelayHelper.delayMicroseconds (rnd ()) false + + // TODO: check this! The following comment may not be correct + // this creates a resume state, which seems more efficient than SpinWait.SpinOnce, see DelayHelper. + let! _ = Task.Delay 0 + let delay = rnd () + + // typical minimum accuracy of Task.Delay is 15.6ms + // for delay-cases shorter than that, we use SpinWait + if delay < 15_000L<µs> then + do DelayHelper.spinWaitDelay (rnd ()) false + else + do! Task.Delay(int <| float delay / 1_000.0) + Interlocked.Increment &x |> ignore - //x <- x + 1 return x // this dereferences the variable } @@ -150,6 +160,18 @@ module TestUtils = yield x } + /// Create a bunch of dummy tasks, with varying millisecond delays. + let createLongerDummyTaskSeq (min: int) max count = + /// Set of delayed tasks in the form of `unit -> Task` + let tasks = DummyTaskFactory(min, max).CreateDelayedTasks count + + taskSeq { + for task in tasks do + // cannot use `yield!` here, as `taskSeq` expects it to return a seq + let! x = task () + yield x + } + /// Create a bunch of dummy tasks, which are sequentially hot-started, WITHOUT artificial spin-wait delays. let createDummyDirectTaskSeq count = /// Set of delayed tasks in the form of `unit -> Task` From 5f819bc4d80686746c80755c47193d1852771669 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 01:29:04 +0200 Subject: [PATCH 02/10] Add test for TaskSeq.ofResizeArray --- src/FSharpy.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs index 0cb51387..589f1112 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs @@ -47,6 +47,12 @@ let ``TaskSeq-ofTaskSeq should succeed`` () = |> TaskSeq.ofTaskSeq |> validateSequence +[] +let ``TaskSeq-ofResizeArray should succeed`` () = + ResizeArray [ 0..9 ] + |> TaskSeq.ofResizeArray + |> validateSequence + [] let ``TaskSeq-ofArray should succeed`` () = Array.init 10 id |> TaskSeq.ofArray |> validateSequence From 6c9800620b25a9d0bb33d5803fb41dd531e4cfed Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 01:29:25 +0200 Subject: [PATCH 03/10] More TaskSeq.empty tests --- .../TaskSeq.Collect.Tests.fs | 6 -- .../TaskSeq.Tests.Other.fs | 58 +++++++++++++++++++ src/FSharpy.TaskSeq/TaskSeq.fs | 1 + 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs index 3d405e2d..04ca6366 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs @@ -57,9 +57,3 @@ let ``TaskSeq-collectSeq with empty sequences`` () = task { Seq.isEmpty sq |> should be True } - -[] -let ``TaskSeq-empty is empty`` () = task { - let! sq = TaskSeq.empty |> TaskSeq.toSeqCachedAsync - Seq.isEmpty sq |> should be True -} diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs index bd0100f7..09d827db 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs @@ -1,5 +1,6 @@ module FSharpy.Tests.``Other functions`` +open System.Threading.Tasks open Xunit open FsUnit.Xunit open FsToolkit.ErrorHandling @@ -14,6 +15,54 @@ let ``TaskSeq-empty returns an empty sequence`` () = task { Seq.length sq |> should equal 0 } +[] +let ``TaskSeq-empty returns an empty sequence - variant`` () = task { + let! isEmpty = TaskSeq.empty |> TaskSeq.isEmpty + isEmpty |> should be True +} + +[] +let ``TaskSeq-empty in a taskSeq context`` () = task { + let! sq = + taskSeq { yield! TaskSeq.empty } + |> TaskSeq.toArrayAsync + + Array.isEmpty sq |> should be True +} + +[] +let ``TaskSeq-empty of unit in a taskSeq context`` () = task { + let! sq = + taskSeq { yield! TaskSeq.empty } + |> TaskSeq.toArrayAsync + + Array.isEmpty sq |> should be True +} + +[] +let ``TaskSeq-empty of more complex type in a taskSeq context`` () = task { + let! sq = + taskSeq { yield! TaskSeq.empty, int>> } + |> TaskSeq.toArrayAsync + + Array.isEmpty sq |> should be True +} + +[] +let ``TaskSeq-empty multiple times in a taskSeq context`` () = task { + let! sq = + taskSeq { + yield! TaskSeq.empty + yield! TaskSeq.empty + yield! TaskSeq.empty + yield! TaskSeq.empty + yield! TaskSeq.empty + } + |> TaskSeq.toArrayAsync + + Array.isEmpty sq |> should be True +} + [] let ``TaskSeq-isEmpty returns true for empty`` () = task { let! isEmpty = TaskSeq.empty |> TaskSeq.isEmpty @@ -25,3 +74,12 @@ let ``TaskSeq-isEmpty returns false for non-empty`` () = task { let! isEmpty = taskSeq { yield 42 } |> TaskSeq.isEmpty isEmpty |> should be False } + +[] +let ``TaskSeq-isEmpty returns false for delayed non-empty sequence`` () = task { + let! isEmpty = + createLongerDummyTaskSeq 200 400 3 + |> TaskSeq.isEmpty + + isEmpty |> should be False +} diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs index b8157f6b..c05334f9 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fs +++ b/src/FSharpy.TaskSeq/TaskSeq.fs @@ -53,6 +53,7 @@ module TaskSeq = e.DisposeAsync().AsTask().Wait() } + // FIXME: incomplete and incorrect code!!! let toSeqOfTasks (taskSeq: taskSeq<'T>) = seq { let e = taskSeq.GetAsyncEnumerator(CancellationToken()) From 8e24bf9ef26d0554701533af02a9eb4a7a512a0b Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 01:40:30 +0200 Subject: [PATCH 04/10] Add TaskSeq.collectAsync and TaskSeq.collectSeqAsync tests and improve existing ones --- .../TaskSeq.Collect.Tests.fs | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs index 04ca6366..1ba39520 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Collect.Tests.fs @@ -6,6 +6,12 @@ open FsToolkit.ErrorHandling open FSharpy +let validateSequence sequence = + sequence + |> Seq.map string + |> String.concat "" + |> should equal "ABBCCDDEEFFGGHHIIJJK" + [] let ``TaskSeq-collect operates in correct order`` () = task { let! sq = @@ -16,10 +22,22 @@ let ``TaskSeq-collect operates in correct order`` () = task { }) |> TaskSeq.toSeqCachedAsync - sq - |> Seq.map string - |> String.concat "" - |> should equal "ABBCCDDEEFFGGHHIIJJK" + validateSequence sq +} + +[] +let ``TaskSeq-collectAsync operates in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectAsync (fun item -> task { + return taskSeq { + yield char (item + 64) + yield char (item + 65) + } + }) + |> TaskSeq.toSeqCachedAsync + + validateSequence sq } [] @@ -32,10 +50,42 @@ let ``TaskSeq-collectSeq operates in correct order`` () = task { }) |> TaskSeq.toSeqCachedAsync - sq - |> Seq.map string - |> String.concat "" - |> should equal "ABBCCDDEEFFGGHHIIJJK" + validateSequence sq +} + +[] +let ``TaskSeq-collectSeq with arrays operates in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectSeq (fun item -> [| char (item + 64); char (item + 65) |]) + |> TaskSeq.toArrayAsync + + validateSequence sq +} + +[] +let ``TaskSeq-collectSeqAsync operates in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectSeqAsync (fun item -> task { + return seq { + yield char (item + 64) + yield char (item + 65) + } + }) + |> TaskSeq.toSeqCachedAsync + + validateSequence sq +} + +[] +let ``TaskSeq-collectSeqAsync with arrays operates in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectSeqAsync (fun item -> task { return [| char (item + 64); char (item + 65) |] }) + |> TaskSeq.toArrayAsync + + validateSequence sq } [] @@ -48,6 +98,16 @@ let ``TaskSeq-collect with empty task sequences`` () = task { Seq.isEmpty sq |> should be True } +[] +let ``TaskSeq-collectAsync with empty task sequences`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectAsync (fun _ -> task { return TaskSeq.empty }) + |> TaskSeq.toSeqCachedAsync + + Seq.isEmpty sq |> should be True +} + [] let ``TaskSeq-collectSeq with empty sequences`` () = task { let! sq = @@ -57,3 +117,13 @@ let ``TaskSeq-collectSeq with empty sequences`` () = task { Seq.isEmpty sq |> should be True } + +[] +let ``TaskSeq-collectSeqAsync with empty sequences`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.collectSeqAsync (fun _ -> task { return Array.empty }) + |> TaskSeq.toSeqCachedAsync + + Seq.isEmpty sq |> should be True +} From 4b283b73c274735db2fa3ef2ca5f669dbb717a10 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 01:55:00 +0200 Subject: [PATCH 05/10] Add TaskSeq.mapi tests and improve TaskSeq.map tests with mutables, same for TaskSeq.iter and iteri --- .../TaskSeq.Iter.Tests.fs | 16 ++- src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs | 98 +++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Iter.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Iter.Tests.fs index f8418b21..3738469e 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Iter.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Iter.Tests.fs @@ -2,10 +2,24 @@ module FSharpy.Tests.Iter open Xunit open FsUnit.Xunit -open FsToolkit.ErrorHandling open FSharpy +[] +let ``TaskSeq-iteri does nothing on empty sequences`` () = task { + let tq = createDummyTaskSeq 10 + let mutable sum = -1 + do! TaskSeq.empty |> TaskSeq.iteri (fun i _ -> sum <- sum + i) + sum |> should equal -1 +} + +[] +let ``TaskSeq-iter does nothing on empty sequences`` () = task { + let tq = createDummyTaskSeq 10 + let mutable sum = -1 + do! TaskSeq.empty |> TaskSeq.iter (fun i -> sum <- sum + i) + sum |> should equal -1 +} [] let ``TaskSeq-iteri should go over all items`` () = task { diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs index 46dc1154..08f94b4f 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs @@ -6,6 +6,11 @@ open FsToolkit.ErrorHandling open FSharpy +let validateSequence sequence = + sequence + |> Seq.map string + |> String.concat "" + |> should equal "ABCDEFGHIJ" [] let ``TaskSeq-map maps in correct order`` () = task { @@ -14,10 +19,47 @@ let ``TaskSeq-map maps in correct order`` () = task { |> TaskSeq.map (fun item -> char (item + 64)) |> TaskSeq.toSeqCachedAsync - sq - |> Seq.map string - |> String.concat "" - |> should equal "ABCDEFGHIJ" + validateSequence sq +} + +[] +let ``TaskSeq-mapi maps in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.mapi (fun i _ -> char (i + 65)) + |> TaskSeq.toSeqCachedAsync + + validateSequence sq +} + +[] +let ``TaskSeq-map can access mutables which are mutated in correct order`` () = task { + let mutable sum = 0 + + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.map (fun item -> + sum <- sum + 1 + char (sum + 64)) + |> TaskSeq.toSeqCachedAsync + + sum |> should equal 10 + validateSequence sq +} + +[] +let ``TaskSeq-mapi can access mutables which are mutated in correct order`` () = task { + let mutable sum = 0 + + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.mapi (fun i _ -> + sum <- i + 1 + char (sum + 64)) + |> TaskSeq.toSeqCachedAsync + + sum |> should equal 10 + validateSequence sq } [] @@ -27,8 +69,48 @@ let ``TaskSeq-mapAsync maps in correct order`` () = task { |> TaskSeq.mapAsync (fun item -> task { return char (item + 64) }) |> TaskSeq.toSeqCachedAsync - sq - |> Seq.map string - |> String.concat "" - |> should equal "ABCDEFGHIJ" + validateSequence sq +} + +[] +let ``TaskSeq-mapiAsync maps in correct order`` () = task { + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.mapiAsync (fun i _ -> task { return char (i + 65) }) + |> TaskSeq.toSeqCachedAsync + + validateSequence sq +} + + +[] +let ``TaskSeq-mapAsync can access mutables which are mutated in correct order`` () = task { + let mutable sum = 0 + + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.mapAsync (fun item -> task { + sum <- sum + 1 + return char (sum + 64) + }) + |> TaskSeq.toSeqCachedAsync + + sum |> should equal 10 + validateSequence sq +} + +[] +let ``TaskSeq-mapiAsync can access mutables which are mutated in correct order`` () = task { + let mutable data = '0' + + let! sq = + createDummyTaskSeq 10 + |> TaskSeq.mapiAsync (fun i _ -> task { + data <- char (i + 65) + return data + }) + |> TaskSeq.toSeqCachedAsync + + data |> should equal (char 74) + validateSequence sq } From 92fcfd0322a44514e9db36a7dbfed3a0c20a0e27 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 02:26:14 +0200 Subject: [PATCH 06/10] Adding tests for TaskSeq.zip --- .../FSharpy.TaskSeq.Test.fsproj | 1 + src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index 4a0450ef..2b309a55 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs new file mode 100644 index 00000000..6fb9e1e7 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -0,0 +1,93 @@ +module FSharpy.Tests.Zip + +open System +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy + +[] +let ``TaskSeq-zip zips in correct order`` () = task { + let one = createDummyTaskSeq 10 + let two = createDummyTaskSeq 10 + let combined = TaskSeq.zip one two + let! combined = TaskSeq.toArrayAsync combined + + combined + |> Array.forall (fun (x, y) -> x = y) + |> should be True + + combined |> should be (haveLength 10) + + combined + |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) +} + +[] +let ``TaskSeq-zip zips in correct order for differently delayed sequences`` () = task { + let one = createDummyDirectTaskSeq 10 + let two = createDummyTaskSeq 10 + let combined = TaskSeq.zip one two + let! combined = TaskSeq.toArrayAsync combined + + combined + |> Array.forall (fun (x, y) -> x = y) + |> should be True + + combined |> should be (haveLength 10) + + combined + |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) +} + +[] +let ``TaskSeq-zip zips large sequences just fine`` length = task { + let one = createDummyTaskSeqWith 10L<µs> 50L<µs> length + let two = createDummyDirectTaskSeq length + let combined = TaskSeq.zip one two + let! combined = TaskSeq.toArrayAsync combined + + combined + |> Array.forall (fun (x, y) -> x = y) + |> should be True + + combined |> should be (haveLength length) + combined |> Array.last |> should equal (length, length) +} + +[] +let ``TaskSeq-zip zips different types`` () = task { + let one = taskSeq { + yield "one" + yield "two" + } + + let two = taskSeq { + yield 42L + yield 43L + } + + let combined = TaskSeq.zip one two + let! combined = TaskSeq.toArrayAsync combined + + combined |> should equal [| ("one", 42L); ("two", 43L) |] +} + +[] +let ``TaskSeq-zip throws on unequal lengths`` () = task { + let one = createDummyTaskSeq 10 + let two = createDummyTaskSeq 11 + let combined = TaskSeq.zip one two + + fun () -> TaskSeq.toArrayAsync combined |> Task.ignore + |> should throwAsyncExact typeof +} + +[] +let ``TaskSeq-zip can zip empty arrays`` () = task { + let combined = TaskSeq.zip TaskSeq.empty TaskSeq.empty + let! combined = TaskSeq.toArrayAsync combined + combined |> should be Empty + Array.isEmpty combined |> should be True +} From b54ef8fbfdce68ec5b530173e0e3c5948e9a212d Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 02:32:55 +0200 Subject: [PATCH 07/10] Fix TaskSeq.zip (consider not throwing an exception? See #32) --- src/FSharpy.TaskSeq/TaskSeqInternal.fs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/FSharpy.TaskSeq/TaskSeqInternal.fs b/src/FSharpy.TaskSeq/TaskSeqInternal.fs index 97d350a8..34c567fd 100644 --- a/src/FSharpy.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharpy.TaskSeq/TaskSeqInternal.fs @@ -177,20 +177,23 @@ module internal TaskSeqInternal = let e2 = taskSequence2.GetAsyncEnumerator(CancellationToken()) let mutable go = true let! step1 = e1.MoveNextAsync() - let! step2 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() go <- step1 && step2 while go do yield e1.Current, e2.Current let! step1 = e1.MoveNextAsync() - let! step2 = e1.MoveNextAsync() - go <- step1 && step2 + let! step2 = e2.MoveNextAsync() + + if step1 <> step2 then + if step1 then + invalidArg "taskSequence1" "The task sequences have different lengths." - if step1 then - invalidArg "taskSequence1" "The task sequences have different lengths." + if step2 then + invalidArg "taskSequence2" "The task sequences have different lengths." + + go <- step1 && step2 - if step2 then - invalidArg "taskSequence2" "The task sequences have different lengths." } let collect (binder: _ -> #IAsyncEnumerable<_>) (taskSequence: taskSeq<_>) = taskSeq { From 4d5b98e4fe23f2373aca2e354d7646a0dd23d229 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 02:47:18 +0200 Subject: [PATCH 08/10] Reaching 100% coverage in TaskSeq.fs and TaskSeqInternal.fs --- src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs | 9 +-------- src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs | 9 +++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs index c70bbd7e..32d1dde8 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs @@ -9,13 +9,6 @@ open FsToolkit.ErrorHandling open FSharpy -[] -let ``ZHang timeout test`` () = task { - let! empty = Task.Delay 30 - - empty |> should be Null -} - [] let ``TaskSeq-choose on an empty sequence`` () = task { let! empty = @@ -50,7 +43,7 @@ let ``TaskSeq-choose can convert and filter`` () = task { let ``TaskSeq-chooseAsync can convert and filter`` () = task { let! alphabet = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 - |> TaskSeq.choose (fun number -> if number <= 26 then Some(char number + '@') else None) + |> TaskSeq.chooseAsync (fun number -> task { return if number <= 26 then Some(char number + '@') else None }) |> TaskSeq.toArrayAsync String alphabet |> should equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs index 6fb9e1e7..4515b75e 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -84,6 +84,15 @@ let ``TaskSeq-zip throws on unequal lengths`` () = task { |> should throwAsyncExact typeof } +[] +let ``TaskSeq-zip throws on unequal lengths, inverted args`` () = task { + let one = createDummyTaskSeq 1 + let combined = TaskSeq.zip one TaskSeq.empty + + fun () -> TaskSeq.toArrayAsync combined |> Task.ignore + |> should throwAsyncExact typeof +} + [] let ``TaskSeq-zip can zip empty arrays`` () = task { let combined = TaskSeq.zip TaskSeq.empty TaskSeq.empty From 41006ef69ad92db5fb645566d030d74b9ecce674 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 02:47:58 +0200 Subject: [PATCH 09/10] Fix one more small bug in TaskSeq.zip, for cases with unequal length and one being empty --- src/FSharpy.TaskSeq/TaskSeqInternal.fs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/FSharpy.TaskSeq/TaskSeqInternal.fs b/src/FSharpy.TaskSeq/TaskSeqInternal.fs index 34c567fd..8b39781d 100644 --- a/src/FSharpy.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharpy.TaskSeq/TaskSeqInternal.fs @@ -173,27 +173,29 @@ module internal TaskSeqInternal = } let zip (taskSequence1: taskSeq<_>) (taskSequence2: taskSeq<_>) = taskSeq { + let inline validate step1 step2 = + if step1 <> step2 then + if step1 then + invalidArg "taskSequence1" "The task sequences have different lengths." + + if step2 then + invalidArg "taskSequence2" "The task sequences have different lengths." + + let e1 = taskSequence1.GetAsyncEnumerator(CancellationToken()) let e2 = taskSequence2.GetAsyncEnumerator(CancellationToken()) let mutable go = true let! step1 = e1.MoveNextAsync() let! step2 = e2.MoveNextAsync() go <- step1 && step2 + validate step1 step2 while go do yield e1.Current, e2.Current let! step1 = e1.MoveNextAsync() let! step2 = e2.MoveNextAsync() - - if step1 <> step2 then - if step1 then - invalidArg "taskSequence1" "The task sequences have different lengths." - - if step2 then - invalidArg "taskSequence2" "The task sequences have different lengths." - + validate step1 step2 go <- step1 && step2 - } let collect (binder: _ -> #IAsyncEnumerable<_>) (taskSequence: taskSeq<_>) = taskSeq { From b43c6727331b292f85475df6befc89135dcb9b0f Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 16 Oct 2022 02:56:56 +0200 Subject: [PATCH 10/10] Clarify a few tests by using InlineData --- src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs index 4515b75e..b27c4691 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -74,20 +74,30 @@ let ``TaskSeq-zip zips different types`` () = task { combined |> should equal [| ("one", 42L); ("two", 43L) |] } -[] -let ``TaskSeq-zip throws on unequal lengths`` () = task { - let one = createDummyTaskSeq 10 - let two = createDummyTaskSeq 11 - let combined = TaskSeq.zip one two +[] +let ``TaskSeq-zip throws on unequal lengths, variant`` leftThrows = task { + let long = createDummyTaskSeq 11 + let short = createDummyTaskSeq 10 + + let combined = + if leftThrows then + TaskSeq.zip short long + else + TaskSeq.zip long short fun () -> TaskSeq.toArrayAsync combined |> Task.ignore |> should throwAsyncExact typeof } -[] -let ``TaskSeq-zip throws on unequal lengths, inverted args`` () = task { +[] +let ``TaskSeq-zip throws on unequal lengths with empty seq`` leftThrows = task { let one = createDummyTaskSeq 1 - let combined = TaskSeq.zip one TaskSeq.empty + + let combined = + if leftThrows then + TaskSeq.zip TaskSeq.empty one + else + TaskSeq.zip one TaskSeq.empty fun () -> TaskSeq.toArrayAsync combined |> Task.ignore |> should throwAsyncExact typeof