Skip to content

Commit

Permalink
Merge pull request #209 from fsprojects/implement-take-skip-truncate-…
Browse files Browse the repository at this point in the history
…drop

Implement `take`, `truncate`, `skip` and `drop`
  • Loading branch information
abelbraaksma committed Dec 19, 2023
2 parents f3c02b9 + d199434 commit 7d6e367
Show file tree
Hide file tree
Showing 9 changed files with 694 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand Down Expand Up @@ -35,8 +35,10 @@
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
<Compile Include="TaskSeq.Pick.Tests.fs" />
<Compile Include="TaskSeq.Singleton.Tests.fs" />
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
<Compile Include="TaskSeq.Skip.Tests.fs" />
<Compile Include="TaskSeq.Tail.Tests.fs" />
<Compile Include="TaskSeq.Take.Tests.fs" />
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
<Compile Include="TaskSeq.Zip.Tests.fs" />
<Compile Include="TaskSeq.Tests.CE.fs" />
Expand Down
3 changes: 3 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/Nunit.Extensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,14 @@ module ExtraCustomMatchers =
)

/// <summary>
/// This makes a test BLOCKING!!! (TODO: get a better test framework?)
///
/// Asserts any exception that exactly matches the given exception <see cref="Type" />.
/// Async exceptions are almost always nested in an <see cref="AggregateException" />, however, in an
/// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and
/// in cases where we just call <see cref="Task.Wait" />, and <see cref="AggregateException" /> will be raised regardless.
/// This assertion will go over all nested exceptions and 'self', to find a matching exception.
///
/// Function to evaluate MUST return a <see cref="System.Threading.Tasks.Task" />, not a generic
/// <see cref="Task&lt;'T>" />.
/// Calls <see cref="Assert.ThrowsAnyAsync&lt;Exception>" /> of xUnit to ensure proper evaluation of async.
Expand Down
233 changes: 233 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Skip.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
module TaskSeq.Tests.Skip

open System

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.skip
// TaskSeq.drop
//

exception SideEffectPastEnd of string

[<AutoOpen>]
module With =
/// Turns a sequence of numbers into a string, starting with A for '1'
let verifyAsString expected =
TaskSeq.map char
>> TaskSeq.map ((+) '@')
>> TaskSeq.toArrayAsync
>> Task.map (String >> should equal expected)

module EmptySeq =
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-skip(0) has no effect on empty input`` variant =
// no `task` block needed
Gen.getEmptyVariant variant |> TaskSeq.skip 0 |> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-skip(1) on empty input should throw InvalidOperation`` variant =
fun () ->
Gen.getEmptyVariant variant
|> TaskSeq.skip 1
|> consumeTaskSeq

|> should throwAsyncExact typeof<ArgumentException>

[<Fact>]
let ``TaskSeq-skip(-1) should throw ArgumentException on any input`` () =
fun () -> TaskSeq.empty<int> |> TaskSeq.skip -1 |> consumeTaskSeq
|> should throwAsyncExact typeof<ArgumentException>

fun () -> TaskSeq.init 10 id |> TaskSeq.skip -1 |> consumeTaskSeq
|> should throwAsyncExact typeof<ArgumentException>

[<Fact>]
let ``TaskSeq-skip(-1) should throw ArgumentException before awaiting`` () =
fun () ->
taskSeq {
do! longDelay ()

if false then
yield 0 // type inference
}
|> TaskSeq.skip -1
|> ignore // throws even without running the async. Bad coding, don't ignore a task!

|> should throw typeof<ArgumentException>

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-drop(0) has no effect on empty input`` variant = Gen.getEmptyVariant variant |> TaskSeq.drop 0 |> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-drop(99) does not throw on empty input`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.drop 99
|> verifyEmpty


[<Fact>]
let ``TaskSeq-drop(-1) should throw ArgumentException on any input`` () =
fun () -> TaskSeq.empty<int> |> TaskSeq.drop -1 |> consumeTaskSeq
|> should throwAsyncExact typeof<ArgumentException>

fun () -> TaskSeq.init 10 id |> TaskSeq.drop -1 |> consumeTaskSeq
|> should throwAsyncExact typeof<ArgumentException>

[<Fact>]
let ``TaskSeq-drop(-1) should throw ArgumentException before awaiting`` () =
fun () ->
taskSeq {
do! longDelay ()

if false then
yield 0 // type inference
}
|> TaskSeq.drop -1
|> ignore // throws even without running the async. Bad coding, don't ignore a task!

|> should throw typeof<ArgumentException>

module Immutable =

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-skip skips over exactly 'count' items`` variant = task {

do!
Gen.getSeqImmutable variant
|> TaskSeq.skip 0
|> verifyAsString "ABCDEFGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.skip 1
|> verifyAsString "BCDEFGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.skip 5
|> verifyAsString "FGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.skip 10
|> verifyEmpty
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-skip throws when there are not enough elements`` variant =
fun () -> TaskSeq.init 1 id |> TaskSeq.skip 2 |> consumeTaskSeq

|> should throwAsyncExact typeof<ArgumentException>

fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.skip 11
|> consumeTaskSeq

|> should throwAsyncExact typeof<ArgumentException>

fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.skip 10_000_000
|> consumeTaskSeq

|> should throwAsyncExact typeof<ArgumentException>

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-drop skips over at least 'count' items`` variant = task {
do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 0
|> verifyAsString "ABCDEFGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 1
|> verifyAsString "BCDEFGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 5
|> verifyAsString "FGHIJ"

do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 10
|> verifyEmpty

do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 11 // no exception
|> verifyEmpty

do!
Gen.getSeqImmutable variant
|> TaskSeq.drop 10_000_000 // no exception
|> verifyEmpty
}

module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-skip skips over enough items`` variant =
Gen.getSeqWithSideEffect variant
|> TaskSeq.skip 5
|> verifyAsString "FGHIJ"

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-drop skips over enough items`` variant =
Gen.getSeqWithSideEffect variant
|> TaskSeq.drop 5
|> verifyAsString "FGHIJ"

[<Fact>]
let ``TaskSeq-skip prove we do not skip side effects`` () = task {
let mutable x = 42 // for this test, the potential mutation should not actually occur

let items = taskSeq {
yield x
yield x * 2
x <- x + 1 // we are proving we never get here
}

let! first = items |> TaskSeq.skip 2 |> TaskSeq.toArrayAsync
let! repeat = items |> TaskSeq.skip 2 |> TaskSeq.toArrayAsync

first |> should equal Array.empty<int>
repeat |> should equal Array.empty<int>
x |> should equal 44 // expect: side-effect is executed twice by now
}

[<Fact>]
let ``TaskSeq-skip 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.skip 4 |> consumeTaskSeq // this would raise ArgumentException normally
|> should throwAsyncExact typeof<SideEffectPastEnd>


[<Fact>]
let ``TaskSeq-drop prove we do not skip side effects at the end`` () = task {
let mutable x = 42 // for this test, the potential mutation should not actually occur

let items = taskSeq {
yield x
yield x * 2
x <- x + 1 // we are proving we never get here
}

let! first = items |> TaskSeq.drop 2 |> TaskSeq.toArrayAsync
let! repeat = items |> TaskSeq.drop 2 |> TaskSeq.toArrayAsync

first |> should equal Array.empty<int>
repeat |> should equal Array.empty<int>
x |> should equal 44 // expect: side-effect at end is executed twice by now
}
Loading

0 comments on commit 7d6e367

Please sign in to comment.