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 dbf1cfc..10ef602 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -57,6 +57,8 @@ + + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Builder.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Builder.Tests.fs new file mode 100644 index 0000000..86c1bc5 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Builder.Tests.fs @@ -0,0 +1,351 @@ +module TaskSeq.Tests.Builder + +open System +open System.Reflection +open System.Threading.Tasks +open System.Collections.Generic +open System.Threading + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// Tests for TaskSeq computation expression builder edge cases +// These test specific edge cases and internal builder functionality +// + +[] +let ``taskSeq builder should handle empty computation expression`` () = task { + let emptySeq = taskSeq { () } + + let! items = TaskSeq.toListAsync emptySeq + items |> should be Empty +} + +[] +let ``taskSeq builder should handle single yield`` () = task { + let seq = taskSeq { + yield 42 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [42] +} + +[] +let ``taskSeq builder should handle multiple yields`` () = task { + let seq = taskSeq { + yield 1 + yield 2 + yield 3 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 2; 3] +} + +[] +let ``taskSeq builder should handle yield! with another TaskSeq`` () = task { + let innerSeq = taskSeq { + yield 1 + yield 2 + } + + let outerSeq = taskSeq { + yield 0 + yield! innerSeq + yield 3 + } + + let! items = TaskSeq.toListAsync outerSeq + items |> should equal [0; 1; 2; 3] +} + +[] +let ``taskSeq builder should handle yield! with regular sequence`` () = task { + let seq = taskSeq { + yield 0 + yield! [1; 2; 3] + yield 4 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [0; 1; 2; 3; 4] +} + +[] +let ``taskSeq builder should handle async operations in do!`` () = task { + let mutable sideEffect = 0 + + let seq = taskSeq { + do! Task.Delay(1) + sideEffect <- 42 + yield sideEffect + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [42] + sideEffect |> should equal 42 +} + +[] +let ``taskSeq builder should handle let! with async values`` () = task { + let seq = taskSeq { + let! value = Task.FromResult(42) + yield value * 2 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [84] +} + +[] +let ``taskSeq builder should handle conditional yields`` () = task { + let seq = taskSeq { + for i in 1..5 do + if i % 2 = 0 then + yield i + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [2; 4] +} + +[] +let ``taskSeq builder should handle nested for loops`` () = task { + let seq = taskSeq { + for i in 1..2 do + for j in 1..2 do + yield (i, j) + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [(1, 1); (1, 2); (2, 1); (2, 2)] +} + +[] +let ``taskSeq builder should handle try-with exception handling`` () = task { + let seq = taskSeq { + try + yield 1 + failwith "test error" + yield 2 + with + | _ -> yield 999 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 999] +} + +[] +let ``taskSeq builder should handle try-finally`` () = task { + let mutable finalized = false + + let seq = taskSeq { + try + yield 1 + yield 2 + finally + finalized <- true + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 2] + finalized |> should equal true +} + +[] +let ``taskSeq builder should handle use for disposable resources`` () = task { + let mutable disposed = false + + let disposable = { new IDisposable with + member _.Dispose() = disposed <- true } + + let seq = taskSeq { + use d = disposable + yield 42 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [42] + disposed |> should equal true +} + +[] +let ``taskSeq builder should handle use! for async disposable resources`` () = task { + let mutable disposed = false + + let asyncDisposable = { new IAsyncDisposable with + member _.DisposeAsync() = + disposed <- true + ValueTask.CompletedTask } + + let seq = taskSeq { + use! d = Task.FromResult(asyncDisposable) + yield 42 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [42] + disposed |> should equal true +} + +[] +let ``taskSeq builder should handle while loops`` () = task { + let seq = taskSeq { + let mutable i = 0 + while i < 3 do + yield i + i <- i + 1 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [0; 1; 2] +} + +[] +let ``taskSeq builder should handle match expressions`` () = task { + let getValue x = + match x with + | 1 -> "one" + | 2 -> "two" + | _ -> "other" + + let seq = taskSeq { + for i in 1..3 do + yield getValue i + } + + let! items = TaskSeq.toListAsync seq + items |> should equal ["one"; "two"; "other"] +} + +[] +let ``taskSeq builder should handle complex async computations`` () = task { + let asyncComputation x = task { + do! Task.Delay(1) + return x * x + } + + let seq = taskSeq { + for i in 1..3 do + let! squared = asyncComputation i + yield squared + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 4; 9] +} + +[] +let ``taskSeq builder should handle cancellation token propagation`` () = task { + use cts = new CancellationTokenSource() + cts.CancelAfter(100) + + let seq = taskSeq { + for i in 1..1000 do + do! Task.Delay(10, cts.Token) + yield i + } + + let asyncEnumerator = seq.GetAsyncEnumerator(cts.Token) + + let testAction() = task { + let mutable count = 0 + let mutable hasMore = true + + while hasMore do + let! moveNext = asyncEnumerator.MoveNextAsync() + hasMore <- moveNext + if hasMore then count <- count + 1 + + return count + } + + // Should be cancelled before reaching 1000 items + let ex = Assert.ThrowsAsync(fun () -> testAction()) + let! _ = ex + () +} + +[] +let ``taskSeq builder should handle empty yield! with empty sequence`` () = task { + let seq = taskSeq { + yield 1 + yield! TaskSeq.empty + yield 2 + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 2] +} + +[] +let ``taskSeq builder should handle multiple yield! operations`` () = task { + let seq1 = taskSeq { yield 1; yield 2 } + let seq2 = taskSeq { yield 3; yield 4 } + + let combined = taskSeq { + yield 0 + yield! seq1 + yield! seq2 + yield 5 + } + + let! items = TaskSeq.toListAsync combined + items |> should equal [0; 1; 2; 3; 4; 5] +} + +[] +let ``taskSeq builder should handle async functions with side effects`` () = task { + let mutable callCount = 0 + + let asyncSideEffect () = task { + callCount <- callCount + 1 + return callCount + } + + let seq = taskSeq { + let! first = asyncSideEffect() + yield first + let! second = asyncSideEffect() + yield second + } + + let! items = TaskSeq.toListAsync seq + items |> should equal [1; 2] + callCount |> should equal 2 +} + +[] +let ``taskSeq builder should handle nested taskSeq expressions`` () = task { + let innerSeq x = taskSeq { + for i in 1..x do + yield i * 10 + } + + let outerSeq = taskSeq { + for x in 1..2 do + yield! innerSeq x + } + + let! items = TaskSeq.toListAsync outerSeq + items |> should equal [10; 10; 20] +} + +[] +let ``taskSeq builder should handle computation expression return`` () = task { + let seq = taskSeq { + if true then + return 42 + else + yield 0 + } + + // Should complete immediately with no items when using return + let! items = TaskSeq.toListAsync seq + items |> should be Empty +} \ No newline at end of file diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Internal.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Internal.Tests.fs new file mode 100644 index 0000000..a9b17ef --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Internal.Tests.fs @@ -0,0 +1,261 @@ +module TaskSeq.Tests.Internal + +open System +open System.Reflection +open System.Threading.Tasks +open System.Collections.Generic +open System.Threading + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// NOTE: This test module tests internal functions in TaskSeqInternal module +// using reflection where necessary, as these functions are not exposed publicly +// + +[] +let ``checkNonNull should throw ArgumentNullException for null argument`` () = + // Get internal module type and method + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let checkNonNullMethod = internalType.GetMethod("checkNonNull", BindingFlags.Static ||| BindingFlags.Public) + + // Test with null argument + let testAction() = + checkNonNullMethod.Invoke(null, [| "testArg"; null |]) |> ignore + + testAction |> should throwWithMessage "testArg" + +[] +let ``checkNonNull should not throw for non-null argument`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let checkNonNullMethod = internalType.GetMethod("checkNonNull", BindingFlags.Static ||| BindingFlags.Public) + + // Test with non-null argument + let result = checkNonNullMethod.Invoke(null, [| "testArg"; "validValue" |]) + result |> should equal null // void return + +[] +let ``raiseEmptySeq should throw correct ArgumentException`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let raiseEmptySeqMethod = internalType.GetMethod("raiseEmptySeq", BindingFlags.Static ||| BindingFlags.Public) + + let testAction() = raiseEmptySeqMethod.Invoke(null, [||]) |> ignore + + let ex = Assert.Throws(testAction) + let innerEx = ex.InnerException :?> ArgumentException + innerEx.ParamName |> should equal "source" + innerEx.Message |> should contain "empty" + +[] +let ``raiseCannotBeNegative should throw for negative values`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseCannotBeNegative", BindingFlags.Static ||| BindingFlags.Public) + + let testAction() = method.Invoke(null, [| "count"; -1 |]) |> ignore + + let ex = Assert.Throws(testAction) + let innerEx = ex.InnerException :?> ArgumentException + innerEx.ParamName |> should equal "count" + innerEx.Message |> should contain "non-negative" + +[] +let ``raiseCannotBeNegative should not throw for zero`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseCannotBeNegative", BindingFlags.Static ||| BindingFlags.Public) + + let result = method.Invoke(null, [| "count"; 0 |]) + result |> should equal null + +[] +let ``raiseCannotBeNegative should not throw for positive values`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseCannotBeNegative", BindingFlags.Static ||| BindingFlags.Public) + + let result = method.Invoke(null, [| "count"; 42 |]) + result |> should equal null + +[] +let ``raiseOutOfBounds should throw correct ArgumentException`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseOutOfBounds", BindingFlags.Static ||| BindingFlags.Public) + + let testAction() = method.Invoke(null, [| "index" |]) |> ignore + + let ex = Assert.Throws(testAction) + let innerEx = ex.InnerException :?> ArgumentException + innerEx.ParamName |> should equal "index" + innerEx.Message |> should contain "bounds" + +[] +let ``raiseInsufficient should throw correct ArgumentException`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseInsufficient", BindingFlags.Static ||| BindingFlags.Public) + + let testAction() = method.Invoke(null, [||]) |> ignore + + let ex = Assert.Throws(testAction) + let innerEx = ex.InnerException :?> ArgumentException + innerEx.ParamName |> should equal "source" + innerEx.Message |> should contain "insufficient" + +[] +let ``raiseNotFound should throw KeyNotFoundException`` () = + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("raiseNotFound", BindingFlags.Static ||| BindingFlags.Public) + + let testAction() = method.Invoke(null, [||]) |> ignore + + let ex = Assert.Throws(testAction) + let innerEx = ex.InnerException :?> KeyNotFoundException + innerEx.Message |> should contain "predicate" + +[] +let ``isEmpty should return true for empty sequence`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("isEmpty", BindingFlags.Static ||| BindingFlags.Public) + + let emptySeq = TaskSeq.empty + let result = method.Invoke(null, [| emptySeq |]) :?> Task + let! isEmpty = result + isEmpty |> should equal true +} + +[] +let ``isEmpty should return false for non-empty sequence`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("isEmpty", BindingFlags.Static ||| BindingFlags.Public) + + let nonEmptySeq = TaskSeq.singleton 42 + let result = method.Invoke(null, [| nonEmptySeq |]) :?> Task + let! isEmpty = result + isEmpty |> should equal false +} + +[] +let ``empty should create proper empty sequence`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let property = internalType.GetProperty("empty", BindingFlags.Static ||| BindingFlags.Public) + + let emptySeq = property.GetValue(null) :?> TaskSeq + + use enumerator = emptySeq.GetAsyncEnumerator(CancellationToken.None) + let! hasItems = enumerator.MoveNextAsync() + hasItems |> should equal false +} + +[] +let ``singleton should create sequence with single item`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("singleton", BindingFlags.Static ||| BindingFlags.Public) + + let singletonSeq = method.Invoke(null, [| 42 |]) :?> TaskSeq + + use enumerator = singletonSeq.GetAsyncEnumerator(CancellationToken.None) + + // Should have first item + let! hasFirst = enumerator.MoveNextAsync() + hasFirst |> should equal true + enumerator.Current |> should equal 42 + + // Should not have second item + let! hasSecond = enumerator.MoveNextAsync() + hasSecond |> should equal false +} + +[] +let ``moveFirstOrRaiseUnsafe should not throw for non-empty sequence`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("moveFirstOrRaiseUnsafe", BindingFlags.Static ||| BindingFlags.Public) + + let seq = TaskSeq.singleton 42 + use enumerator = seq.GetAsyncEnumerator(CancellationToken.None) + + let result = method.Invoke(null, [| enumerator |]) :?> Task + do! result + + enumerator.Current |> should equal 42 +} + +[] +let ``moveFirstOrRaiseUnsafe should throw for empty sequence`` () = task { + let assembly = typeof>.Assembly + let internalType = assembly.GetType("FSharp.Control.TaskSeqInternal") + let method = internalType.GetMethod("moveFirstOrRaiseUnsafe", BindingFlags.Static ||| BindingFlags.Public) + + let emptySeq = TaskSeq.empty + use enumerator = emptySeq.GetAsyncEnumerator(CancellationToken.None) + + let result = method.Invoke(null, [| enumerator |]) :?> Task + + let ex = Assert.ThrowsAsync(fun () -> result) + let! exception = ex + exception.ParamName |> should equal "source" + exception.Message |> should contain "empty" +} + +// Test internal discriminated union types +[] +let ``AsyncEnumStatus enum values should exist`` () = + let assembly = typeof>.Assembly + let statusType = assembly.GetType("FSharp.Control.AsyncEnumStatus") + + statusType |> should not' (equal null) + statusType.IsEnum |> should equal false // It's a DU, not an enum + statusType.IsValueType |> should equal true // Struct DU + +[] +let ``TakeOrSkipKind enum values should exist`` () = + let assembly = typeof>.Assembly + let kindType = assembly.GetType("FSharp.Control.TakeOrSkipKind") + + kindType |> should not' (equal null) + kindType.IsValueType |> should equal true + +[] +let ``Action union type should exist`` () = + let assembly = typeof>.Assembly + let actionType = assembly.GetTypes() |> Array.find (fun t -> t.Name.StartsWith("Action") && t.IsValueType) + + actionType |> should not' (equal null) + actionType.IsValueType |> should equal true + +[] +let ``FolderAction union type should exist`` () = + let assembly = typeof>.Assembly + let folderType = assembly.GetTypes() |> Array.find (fun t -> t.Name.StartsWith("FolderAction") && t.IsValueType) + + folderType |> should not' (equal null) + folderType.IsValueType |> should equal true + +[] +let ``ChooserAction union type should exist`` () = + let assembly = typeof>.Assembly + let chooserType = assembly.GetTypes() |> Array.find (fun t -> t.Name.StartsWith("ChooserAction") && t.IsValueType) + + chooserType |> should not' (equal null) + chooserType.IsValueType |> should equal true + +[] +let ``PredicateAction union type should exist`` () = + let assembly = typeof>.Assembly + let predType = assembly.GetTypes() |> Array.find (fun t -> t.Name.StartsWith("PredicateAction") && t.IsValueType) + + predType |> should not' (equal null) + predType.IsValueType |> should equal true \ No newline at end of file