diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 4c28722..ff7ae5e 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -27,6 +27,7 @@ + @@ -36,6 +37,7 @@ + @@ -43,6 +45,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs new file mode 100644 index 0000000..ad8a8c8 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs @@ -0,0 +1,548 @@ +module TaskSeq.Tests.InsertAt + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + + +// +// TaskSeq.insertAt +// TaskSeq.insertManyAt +// + +exception SideEffectPastEnd of string + +module EmptySeq = + [)>] + let ``TaskSeq-insertAt(0) on empty input returns singleton`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.insertAt 0 42 + |> verifySingleton 42 + + [)>] + let ``TaskSeq-insertAt(1) on empty input should throw ArgumentException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.insertAt 1 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertAt(-1) should throw ArgumentException on any input`` () = + fun () -> + TaskSeq.empty + |> TaskSeq.insertAt -1 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + TaskSeq.init 10 id + |> TaskSeq.insertAt -1 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertAt(-1) should throw ArgumentException before awaiting`` () = + fun () -> + taskSeq { + do! longDelay () + + if false then + yield 0 // type inference + } + |> TaskSeq.insertAt -1 42 + |> ignore // throws even without running the async. Bad coding, don't ignore a task! + + // test without awaiting the async + |> should throw typeof + + [)>] + let ``TaskSeq-insertManyAt(0) on empty input returns singleton`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.insertManyAt 0 (TaskSeq.ofArray [| 42; 43; 44 |]) + |> verifyDigitsAsString "jkl" + + [)>] + let ``TaskSeq-insertManyAt(1) on empty input should throw InvalidOperation`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.insertManyAt 1 (TaskSeq.ofArray [| 42; 43; 44 |]) + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertManyAt(-1) should throw ArgumentException on any input`` () = + fun () -> + TaskSeq.empty + |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |]) + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + TaskSeq.init 10 id + |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |]) + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertManyAt(-1) should throw ArgumentException before awaiting`` () = + fun () -> + taskSeq { + do! longDelay () + + if false then + yield 0 // type inference + } + |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |]) + |> ignore // throws even without running the async. Bad coding, don't ignore a task! + + // test without awaiting the async + |> should throw typeof + + [] + let ``TaskSeq-insertManyAt() with empty sequenc as source`` () = + TaskSeq.empty + |> TaskSeq.insertManyAt 0 TaskSeq.empty + |> verifyEmpty + + [] + let ``TaskSeq-insertManyAt() with empty sequence as source applies to non-empty sequence`` () = + TaskSeq.init 10 id + |> TaskSeq.insertManyAt 2 TaskSeq.empty + |> verify0To9 + +module Immutable = + [)>] + let ``TaskSeq-insertAt can insert after end of sequence`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 10 99 + |> verifyDigitsAsString "ABCDEFGHIJ£" + } + + [)>] + let ``TaskSeq-insertAt inserts item immediately after the indexed position`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 0 99 + |> verifyDigitsAsString "£ABCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 1 99 + |> verifyDigitsAsString "A£BCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 5 99 + |> verifyDigitsAsString "ABCDE£FGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 10 99 + |> verifyDigitsAsString "ABCDEFGHIJ£" + } + + + [)>] + let ``TaskSeq-insertAt can be repeated in a chain`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 0 99 + |> TaskSeq.insertAt 0 99 + |> TaskSeq.insertAt 0 99 + |> TaskSeq.insertAt 0 99 + |> TaskSeq.insertAt 0 99 + |> verifyDigitsAsString "£££££ABCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 10 99 + |> TaskSeq.insertAt 11 99 + |> TaskSeq.insertAt 12 99 + |> TaskSeq.insertAt 13 99 + |> TaskSeq.insertAt 14 99 + |> verifyDigitsAsString "ABCDEFGHIJ£££££" + } + + [)>] + let ``TaskSeq-insertAt applies to a position in the new sequence`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 0 99 + |> TaskSeq.insertAt 2 99 + |> TaskSeq.insertAt 4 99 + |> TaskSeq.insertAt 6 99 + |> TaskSeq.insertAt 8 99 + |> TaskSeq.insertAt 10 99 + |> TaskSeq.insertAt 12 99 + |> TaskSeq.insertAt 14 99 + |> TaskSeq.insertAt 16 99 + |> TaskSeq.insertAt 18 99 + |> TaskSeq.insertAt 20 99 + |> verifyDigitsAsString "£A£B£C£D£E£F£G£H£I£J£" + } + + [] + let ``TaskSeq-insertAt can be applied to an infinite task sequence`` () = + TaskSeq.initInfinite id + |> TaskSeq.insertAt 100 12345 + |> TaskSeq.item 100 + |> Task.map (should equal 12345) + + + [)>] + let ``TaskSeq-insertAt throws when there are not enough elements`` variant = + fun () -> + TaskSeq.singleton 1 + // insert after 1 + |> TaskSeq.insertAt 2 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 11 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.insertAt 10_000_000 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-insertManyAt can insert after end of sequence`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 10 (TaskSeq.ofArray [| 99; 100; 101 |]) + |> verifyDigitsAsString "ABCDEFGHIJ£¤¥" + } + + [)>] + let ``TaskSeq-insertManyAt inserts item immediately after the indexed position`` variant = task { + let values = TaskSeq.ofArray [| 99; 100; 101 |] + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 0 values + |> verifyDigitsAsString "£¤¥ABCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 1 values + |> verifyDigitsAsString "A£¤¥BCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 5 values + |> verifyDigitsAsString "ABCDE£¤¥FGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 10 values + |> verifyDigitsAsString "ABCDEFGHIJ£¤¥" + } + + + [)>] + let ``TaskSeq-insertManyAt can be repeated in a chain`` variant = task { + let values = TaskSeq.ofArray [| 99; 100; 101 |] + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 0 values + |> TaskSeq.insertManyAt 0 values + |> TaskSeq.insertManyAt 0 values + |> TaskSeq.insertManyAt 0 values + |> TaskSeq.insertManyAt 0 values + |> verifyDigitsAsString "£¤¥£¤¥£¤¥£¤¥£¤¥ABCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 10 values + |> TaskSeq.insertManyAt 11 values + |> TaskSeq.insertManyAt 12 values + |> TaskSeq.insertManyAt 13 values + |> TaskSeq.insertManyAt 14 values + |> verifyDigitsAsString "ABCDEFGHIJ£££££¤¥¤¥¤¥¤¥¤¥" + } + + [)>] + let ``TaskSeq-insertManyAt applies to a position in the new sequence`` variant = task { + let values = TaskSeq.ofArray [| 99; 100; 101 |] + + do! + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 0 values + |> TaskSeq.insertManyAt 4 values + |> TaskSeq.insertManyAt 8 values + |> TaskSeq.insertManyAt 12 values + |> TaskSeq.insertManyAt 16 values + |> TaskSeq.insertManyAt 20 values + |> TaskSeq.insertManyAt 24 values + |> TaskSeq.insertManyAt 28 values + |> TaskSeq.insertManyAt 32 values + |> TaskSeq.insertManyAt 36 values + |> TaskSeq.insertManyAt 40 values + |> verifyDigitsAsString "£¤¥A£¤¥B£¤¥C£¤¥D£¤¥E£¤¥F£¤¥G£¤¥H£¤¥I£¤¥J£¤¥" + } + + [] + let ``TaskSeq-insertManyAt (infinite) can be applied to an infinite task sequence`` () = + TaskSeq.initInfinite id + |> TaskSeq.insertManyAt 100 (TaskSeq.init 10 id) + |> TaskSeq.item 109 + |> Task.map (should equal 9) + + + + [] + let ``TaskSeq-insertManyAt (infinite) with infinite task sequence as argument`` () = + TaskSeq.init 100 id + |> TaskSeq.insertManyAt 100 (TaskSeq.initInfinite id) + |> TaskSeq.item 1999 + |> Task.map (should equal 1899) // the inserted infinite sequence started at 100, with value 0. + + [] + let ``TaskSeq-insertManyAt (infinite) with source and values both as infinite task sequence`` () = task { + + // using two infinite task sequences + let ts = + TaskSeq.initInfinite id + |> TaskSeq.insertManyAt 1000 (TaskSeq.initInfinite id) + + // the inserted infinite sequence started at 1000, with value 0. + do! ts |> TaskSeq.item 999 |> Task.map (should equal 999) + do! ts |> TaskSeq.item 1000 |> Task.map (should equal 0) + do! ts |> TaskSeq.item 2000 |> Task.map (should equal 1000) + } + + [)>] + let ``TaskSeq-insertManyAt throws when there are not enough elements`` variant = + let values = TaskSeq.ofArray [| 99; 100; 101 |] + + fun () -> + TaskSeq.singleton 1 + // insert after 1 + |> TaskSeq.insertManyAt 2 values + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 11 values + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.insertManyAt 10_000_000 values + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + + +module SideEffects = + + // PoC test + [] + let ``Seq-insertAt (poc-proof) will execute side effect before index`` () = + // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module + // this shows that Seq.insertAt executes more side effects than necessary. + + let mutable x = 42 + + let items = seq { + x <- x + 1 // we are proving this gets executed with insertAt(0) + yield x + yield x * 2 + } + + items + |> Seq.insertAt 0 99 + |> Seq.item 0 // put enumerator to inserted item + |> ignore + + x |> should equal 43 // one time side effect executed. QED + + [] + let ``TaskSeq-insertAt(0) will execute side effects at start of sequence`` () = + // NOTE: while not strictly necessary, this mirrors behavior of Seq.insertAt + + let mutable x = 42 // for this test, the potential mutation should not actually occur + + let items = taskSeq { + x <- x + 1 // this is executed even with insertAt(0) + yield x + yield x * 2 + } + + items + |> TaskSeq.insertAt 0 99 + |> TaskSeq.item 0 // consume only the first item + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // the mutable was updated + + [] + let ``TaskSeq-insertAt will execute last side effect when inserting past end`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + yield x * 2 + yield x * 4 + x <- x + 1 // this is executed when inserting past last item + } + + items + |> TaskSeq.insertAt 3 99 + |> TaskSeq.item 3 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index + + + [] + let ``TaskSeq-insertAt will execute side effect just before index`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + x <- x + 1 // this is executed, even though we insert after the first item + yield x * 2 + yield x * 4 + } + + items + |> TaskSeq.insertAt 1 99 + |> TaskSeq.item 1 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index + + [] + let ``TaskSeq-insertAt exception at insertion index is thrown`` () = + fun () -> + taskSeq { + yield 1 + yield! [ 2; 3 ] + do SideEffectPastEnd "at the end" |> raise // this is raised + yield 4 + } + |> TaskSeq.insertAt 3 99 + |> TaskSeq.item 3 + |> Task.ignore + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertAt prove that an exception from the taskSeq is thrown instead of exception from function`` () = + let items = taskSeq { + yield 42 + yield! [ 1; 2 ] + do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised + } + + fun () -> items |> TaskSeq.insertAt 4 99 |> consumeTaskSeq // this would raise ArgumentException normally, but not now + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertManyAt(0) will execute side effects at start of sequence`` () = + // NOTE: while not strictly necessary, this mirrors behavior of Seq.insertManyAt + + let mutable x = 42 // for this test, the potential mutation should not actually occur + + let items = taskSeq { + x <- x + 1 // this is executed even with insertManyAt(0) + yield x + yield x * 2 + } + + items + |> TaskSeq.insertManyAt 0 (taskSeq { yield! [ 99; 100 ] }) + |> TaskSeq.item 0 // consume only the first item + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // the mutable was updated + + [] + let ``TaskSeq-insertManyAt will execute last side effect when inserting past end`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + yield x * 2 + yield x * 4 + x <- x + 1 // this is executed when inserting past last item + } + + items + |> TaskSeq.insertManyAt 3 (taskSeq { yield! [ 99; 100 ] }) + |> TaskSeq.item 3 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index + + + [] + let ``TaskSeq-insertManyAt will execute side effect just before index`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + x <- x + 1 // this is executed, even though we insert after the first item + yield x * 2 + yield x * 4 + } + + items + |> TaskSeq.insertManyAt 1 (taskSeq { yield! [ 99; 100 ] }) + |> TaskSeq.item 1 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index + + [] + let ``TaskSeq-insertManyAt exception at insertion index is thrown`` () = + fun () -> + taskSeq { + yield 1 + yield! [ 2; 3 ] + do SideEffectPastEnd "at the end" |> raise // this is raised + yield 4 + } + |> TaskSeq.insertManyAt 3 (taskSeq { yield! [ 99; 100 ] }) + |> TaskSeq.item 3 + |> Task.ignore + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-insertManyAt prove that an exception from the taskSeq is thrown instead of exception from function`` () = + let items = taskSeq { + yield 42 + yield! [ 1; 2 ] + do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised + } + + fun () -> + items + |> TaskSeq.insertManyAt 4 (taskSeq { yield! [ 99; 100 ] }) + |> consumeTaskSeq // this would raise ArgumentException normally, but not now + + |> should throwAsyncExact typeof diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs new file mode 100644 index 0000000..9c60052 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs @@ -0,0 +1,292 @@ +module TaskSeq.Tests.RemoveAt + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + + +// +// TaskSeq.removeAt +// TaskSeq.removeManyAt +// + +exception SideEffectPastEnd of string + +module EmptySeq = + [)>] + let ``TaskSeq-removeAt(0) on empty input raises`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.removeAt 0 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-removeManyAt(0) on empty input raises`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.removeManyAt 0 0 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-removeAt(-1) on empty input should throw ArgumentException without consuming`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.removeAt -1 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> Gen.getEmptyVariant variant |> TaskSeq.removeAt -1 |> ignore // task is not awaited + + |> should throw typeof + + [)>] + let ``TaskSeq-removeManyAt(-1) on empty input should throw ArgumentException without consuming`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.removeManyAt -1 0 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.removeManyAt -1 0 + |> ignore + + |> should throw typeof + +module Immutable = + [)>] + let ``TaskSeq-removeAt can remove last item`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 9 + |> verifyDigitsAsString "ABCDEFGHI" + } + + [)>] + let ``TaskSeq-removeAt removes the item at indexed positions`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 0 + |> verifyDigitsAsString "BCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 1 + |> verifyDigitsAsString "ACDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 5 + |> verifyDigitsAsString "ABCDEGHIJ" + + } + + + [)>] + let ``TaskSeq-removeAt can be repeated in a chain`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 0 + |> TaskSeq.removeAt 0 + |> TaskSeq.removeAt 0 + |> TaskSeq.removeAt 0 + |> TaskSeq.removeAt 0 + |> verifyDigitsAsString "FGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 9 + |> TaskSeq.removeAt 8 + |> TaskSeq.removeAt 7 + |> TaskSeq.removeAt 6 + |> TaskSeq.removeAt 5 // sequence gets shorter, pick last + |> verifyDigitsAsString "ABCDE" + } + + [] + let ``TaskSeq-removeAt can be applied to an infinite task sequence`` () = + TaskSeq.initInfinite id + |> TaskSeq.removeAt 10_000 + |> TaskSeq.item 10_000 + |> Task.map (should equal 10_001) + + + [)>] + let ``TaskSeq-removeAt throws when there are not enough elements`` variant = + fun () -> + TaskSeq.singleton 1 + // remove after 1 + |> TaskSeq.removeAt 2 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 10 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.removeAt 10_000_000 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-removeManyAt can remove last item`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 9 1 + |> verifyDigitsAsString "ABCDEFGHI" + } + + [)>] + let ``TaskSeq-removeManyAt can remove multiple items`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 1 5 + |> verifyDigitsAsString "AGHIJ" + } + + [)>] + let ``TaskSeq-removeManyAt can with a large count past the end of the sequence is fine`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 2 20_000 // try to remove too many is fine, like Seq.removeManyAt (regardless the docs at time of writing) + |> verifyDigitsAsString "AB" + } + + [)>] + let ``TaskSeq-removeManyAt does not remove any item when count is zero`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 9 0 + |> verifyDigitsAsString "ABCDEFGHIJ" + } + + [)>] + let ``TaskSeq-removeManyAt does not remove any item when count is negative`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 1 -99 + |> verifyDigitsAsString "ABCDEFGHIJ" + } + + [)>] + let ``TaskSeq-removeManyAt removes items at indexed positions`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 0 5 + |> verifyDigitsAsString "FGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 1 3 + |> verifyDigitsAsString "AEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 5 5 + |> verifyDigitsAsString "ABCDE" + + } + + + [)>] + let ``TaskSeq-removeManyAt can be repeated in a chain`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 0 1 + |> TaskSeq.removeManyAt 0 2 + |> TaskSeq.removeManyAt 0 3 + |> verifyDigitsAsString "GHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 9 1 // pick last, result ABCDEFGHIJ + |> TaskSeq.removeManyAt 6 1 // then from 6th pos, result ABCDEFHI + |> TaskSeq.removeManyAt 3 2 // from 3rd pos take 2, result ABCFHI + |> TaskSeq.removeManyAt 0 2 // from start, take 2, result CFHI + |> verifyDigitsAsString "CFHI" + } + + [] + let ``TaskSeq-removeManyAt can be applied to an infinite task sequence`` () = + TaskSeq.initInfinite id + |> TaskSeq.removeManyAt 10_000 5_000 + |> TaskSeq.item 12_000 + |> Task.map (should equal 17_000) + + + [)>] + let ``TaskSeq-removeManyAt throws when there are not enough elements for index`` variant = + // NOTE: only raises if INDEX is out of bounds, not when COUNT is out of bounds!!! + + fun () -> + TaskSeq.singleton 1 + // remove after 1 + |> TaskSeq.removeManyAt 2 0 // regardless of count, it should raise + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 10 5 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.removeManyAt 10_000_000 -5 // even with neg. count + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + + +module SideEffects = + + // NOTES: + // + // no tests, it is not possible to create a meaningful side-effect test, as any consuming after + // removing an item would logically require the side effect to be executed the normal way + + // PoC test + [] + let ``Seq-removeAt (poc-proof) will execute side effect before index`` () = + // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module + // this shows that Seq.removeAt executes more side effects than necessary. + + let mutable x = 42 + + let items = seq { + yield x + x <- x + 1 // we are proving this gets executed with removeAt(0), BUT this is the result of Seq.item + yield x * 2 + } + + items + |> Seq.removeAt 0 + |> Seq.item 0 // consume anything (this is why there's nothing to test with Seq.removeAt, as this is always true after removing an item) + |> ignore + + x |> should equal 43 // one time side effect executed. QED diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs new file mode 100644 index 0000000..0bf7e9d --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs @@ -0,0 +1,254 @@ +module TaskSeq.Tests.UpdateAt + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + + +// +// TaskSeq.updateAt +// + +exception SideEffectPastEnd of string + +module EmptySeq = + [)>] + let ``TaskSeq-updateAt(0) on empty input should throw ArgumentException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.updateAt 0 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-updateAt(-1) should throw ArgumentException on any input`` () = + fun () -> + TaskSeq.empty + |> TaskSeq.updateAt -1 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + TaskSeq.init 10 id + |> TaskSeq.updateAt -1 42 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [] + let ``TaskSeq-updateAt(-1) should throw ArgumentException before awaiting`` () = + fun () -> + taskSeq { + do! longDelay () + + if false then + yield 0 // type inference + } + |> TaskSeq.updateAt -1 42 + |> ignore // throws even without running the async. Bad coding, don't ignore a task! + + // test without awaiting the async + |> should throw typeof + +module Immutable = + [)>] + let ``TaskSeq-updateAt can update at end of sequence`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 9 99 + |> verifyDigitsAsString "ABCDEFGHI£" + } + + [)>] + let ``TaskSeq-updateAt past end of sequence throws ArgumentException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 10 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-updateAt updates item immediately after the indexed position`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 0 99 + |> verifyDigitsAsString "£BCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 1 99 + |> verifyDigitsAsString "A£CDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 5 99 + |> verifyDigitsAsString "ABCDE£GHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 9 99 + |> verifyDigitsAsString "ABCDEFGHI£" + } + + + [)>] + let ``TaskSeq-updateAt can be repeated in a chain`` variant = task { + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 0 99 + |> TaskSeq.updateAt 1 99 + |> TaskSeq.updateAt 2 99 + |> TaskSeq.updateAt 3 99 + |> TaskSeq.updateAt 4 99 + |> verifyDigitsAsString "£££££FGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 9 99 + |> TaskSeq.updateAt 8 99 + |> TaskSeq.updateAt 6 99 + |> TaskSeq.updateAt 4 99 + |> TaskSeq.updateAt 2 99 + |> verifyDigitsAsString "AB£D£F£H££" + } + + + [] + let ``TaskSeq-updateAt can be applied to an infinite task sequence`` () = + TaskSeq.initInfinite id + |> TaskSeq.updateAt 1_000_000 12345 + |> TaskSeq.item 1_000_000 + |> Task.map (should equal 12345) + + + [)>] + let ``TaskSeq-updateAt throws when there are not enough elements`` variant = + fun () -> + TaskSeq.singleton 1 + // update after 1 + |> TaskSeq.updateAt 2 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 10 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.updateAt 10_000_000 99 + |> consumeTaskSeq + + |> should throwAsyncExact typeof + + +module SideEffects = + + // PoC test + [] + let ``Seq-updateAt (poc-proof) will NOT execute side effect just after index`` () = + // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module + // this shows that Seq.updateAt executes no extra side effects. + + let mutable x = 42 + + let items = seq { + yield x + x <- x + 1 // we are proving this gets executed with updateAt(0) + yield x * 2 + } + + items + |> Seq.updateAt 0 99 + |> Seq.item 0 // put enumerator to updated item + |> ignore + + x |> should equal 42 // one time side effect executed. QED + + [] + let ``TaskSeq-updateAt(0) will execute side effects at start of sequence`` () = + // NOTE: while not strictly necessary, this mirrors behavior of Seq.updateAt + + let mutable x = 42 // for this test, the potential mutation should not actually occur + + let items = taskSeq { + x <- x + 1 // this is executed even with updateAt(0) + yield x + yield x * 2 + } + + items + |> TaskSeq.updateAt 0 99 + |> TaskSeq.item 0 // consume only the first item + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 43) // the mutable was updated + + [] + let ``TaskSeq-updateAt will NOT execute last side effect when inserting past end`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + yield x * 2 + yield x * 4 + x <- x + 1 // this is executed when inserting past last item + } + + items + |> TaskSeq.updateAt 2 99 + |> TaskSeq.item 2 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS + + + [] + let ``TaskSeq-updateAt will NOT execute side effect just before index`` () = + let mutable x = 42 + + let items = taskSeq { + yield x + x <- x + 1 // this is executed, even though we insert after the first item + yield x * 2 + yield x * 4 + } + + items + |> TaskSeq.updateAt 0 99 + |> TaskSeq.item 0 + |> Task.map (should equal 99) + |> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS + + [] + let ``TaskSeq-updateAt exception at update index is NOT thrown`` () = + taskSeq { + yield 1 + yield! [ 2; 3 ] + do SideEffectPastEnd "at the end" |> raise // this is NOT raised + yield 4 + } + |> TaskSeq.updateAt 2 99 + |> TaskSeq.item 2 + |> Task.map (should equal 99) + + [] + let ``TaskSeq-updateAt prove that an exception from the taskSeq is thrown instead of exception from function`` () = + let items = taskSeq { + yield 42 + yield! [ 1; 2 ] + do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised + } + + fun () -> items |> TaskSeq.updateAt 4 99 |> consumeTaskSeq // this would raise ArgumentException normally, but not now + |> should throwAsyncExact typeof diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs index a1488a9..67225b1 100644 --- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs +++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs @@ -139,12 +139,24 @@ module TestUtils = |> TaskSeq.toArrayAsync |> Task.map (Array.isEmpty >> should be True) + /// Verifies that a task sequence contains exactly one item + let verifySingleton value ts = + ts + |> TaskSeq.toArrayAsync + |> Task.map (should equal [| value |]) + /// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing. let verify1To10 ts = ts |> TaskSeq.toArrayAsync |> Task.map (should equal [| 1..10 |]) + /// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing. + let verify0To9 ts = + ts + |> TaskSeq.toArrayAsync + |> Task.map (should equal [| 0..9 |]) + /// Turns a sequence of integers into a string, starting with A for '1', Z for 26 etc. let verifyDigitsAsString expected = TaskSeq.map (char: int -> char) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 520b3a3..357a378 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -315,6 +315,12 @@ type TaskSeq private () = static member tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source static member tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source + static member insertAt index value source = Internal.insertAt index (One value) source + static member insertManyAt index values source = Internal.insertAt index (Many values) source + static member removeAt index source = Internal.removeAt index source + static member removeManyAt index count source = Internal.removeManyAt index count source + static member updateAt index value source = Internal.updateAt index value source + static member except itemsToExclude source = Internal.except itemsToExclude source static member exceptOfSeq itemsToExclude source = Internal.exceptOfSeq itemsToExclude source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 579b55c..5a3b16f 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1241,6 +1241,7 @@ type TaskSeq = /// /// The first input task sequence. /// The second input task sequence. + /// The result task sequence of tuples. /// Thrown when either of the two input task sequences is null. static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U> @@ -1275,3 +1276,65 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State> + + /// + /// Return a new task sequence with a new item inserted before the given index. + /// + /// + /// The index where the item should be inserted. + /// The value to insert. + /// The input task sequence. + /// The result task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when index is below 0 or greater than source length. + static member insertAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Return a new task sequence with the new items inserted before the given index. + /// + /// + /// The index where the items should be inserted. + /// The values to insert. + /// The input task sequence. + /// The result task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when index is below 0 or greater than source length. + static member insertManyAt: index: int -> values: TaskSeq<'T> -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Return a new task sequence with the item at the given index removed. + /// + /// + /// The index where the item should be removed. + /// The input task sequence. + /// The result task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when index is below 0 or greater than source length. + static member removeAt: index: int -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Return a new task sequence with the number of items starting at a given index removed. + /// If is negative or zero, no items are removed. If + /// + is greater than source length, but is not, then + /// all items until end of sequence are removed. + /// + /// + /// The index where the items should be removed. + /// The number of items to remove. + /// The input task sequence. + /// The result task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when index is below 0 or greater than source length. + static member removeManyAt: index: int -> count: int -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Return a new task sequence with the item at a given index set to the new value. + /// + /// + /// The index of the item to be replaced. + /// The new value. + /// The input task sequence. + /// The result task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when index is below 0 or greater than source length. + static member updateAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T> diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index c48821f..5827637 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -49,6 +49,11 @@ type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> = | InitAction of init_item: (int -> 'T) | InitActionAsync of async_init_item: (int -> 'TaskT) +[] +type internal ManyOrOne<'T> = + | Many of source_seq: TaskSeq<'T> + | One of source_item: 'T + module internal TaskSeqInternal = /// Raise an NRE for arguments that are null. Only used for 'source' parameters, never for function parameters. let inline checkNonNull argName arg = @@ -57,7 +62,10 @@ module internal TaskSeqInternal = let inline raiseEmptySeq () = invalidArg "source" "The input task sequence was empty." - let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative" + let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative." + + let inline raiseOutOfBounds name = + invalidArg name "The value or index must be within the bounds of the task sequence." let inline raiseInsufficient () = // this is correct, it is NOT an InvalidOperationException (see Seq.fs in F# Core) @@ -861,6 +869,90 @@ module internal TaskSeqInternal = yield e.Current } + /// InsertAt or InsertManyAt + let insertAt index valueOrValues (source: TaskSeq<_>) = + if index < 0 then + raiseCannotBeNegative (nameof index) + + taskSeq { + let mutable i = 0 + + for item in source do + if i = index then + match valueOrValues with + | Many values -> yield! values + | One value -> yield value + + yield item + i <- i + 1 + + // allow inserting at the end + if i = index then + match valueOrValues with + | Many values -> yield! values + | One value -> yield value + + if i < index then + raiseOutOfBounds (nameof index) + } + + let removeAt index (source: TaskSeq<'T>) = + if index < 0 then + raiseCannotBeNegative (nameof index) + + taskSeq { + let mutable i = 0 + + for item in source do + if i <> index then + yield item + + i <- i + 1 + + // cannot remove past end of sequence + if i <= index then + raiseOutOfBounds (nameof index) + } + + let removeManyAt index count (source: TaskSeq<'T>) = + if index < 0 then + raiseCannotBeNegative (nameof index) + + taskSeq { + let mutable i = 0 + let indexEnd = index + count + + for item in source do + if i < index || i >= indexEnd then + yield item + + i <- i + 1 + + // cannot remove past end of sequence + if i <= index then + raiseOutOfBounds (nameof index) + } + + let updateAt index value (source: TaskSeq<'T>) = + if index < 0 then + raiseCannotBeNegative (nameof index) + + taskSeq { + let mutable i = 0 + + for item in source do + if i <> index then // most common scenario on top (cpu prediction) + yield item + else + yield value + + i <- i + 1 + + // cannot update past end of sequence + if i <= index then + raiseOutOfBounds (nameof index) + } + // Consider turning using an F# version of this instead? // https://github.com/i3arnon/ConcurrentHashSet type ConcurrentHashSet<'T when 'T: equality>(ct) =