diff --git a/tests/FsMath.Tests/FsMath.Tests.fsproj b/tests/FsMath.Tests/FsMath.Tests.fsproj index 80cd413..e590b72 100644 --- a/tests/FsMath.Tests/FsMath.Tests.fsproj +++ b/tests/FsMath.Tests/FsMath.Tests.fsproj @@ -15,6 +15,7 @@ + diff --git a/tests/FsMath.Tests/VectorQuotationTests.fs b/tests/FsMath.Tests/VectorQuotationTests.fs new file mode 100644 index 0000000..a7ec6f0 --- /dev/null +++ b/tests/FsMath.Tests/VectorQuotationTests.fs @@ -0,0 +1,384 @@ +namespace FsMath.Tests.Vector + +open System +open Xunit +open FsMath +open FsMath.Tests.AssertHelpers +open FSharp.Linq.RuntimeHelpers + +/// +/// Quotation-based tests for Vector static methods (inline functions). +/// These tests use F# quotation evaluation to ensure that inline functions +/// are actually executed during test coverage measurement. +/// +/// Background: F# inline functions are not tracked by coverage tools because +/// they are inlined at compile time. Using quotations forces the code to be +/// evaluated dynamically, which allows coverage tools to track execution. +/// +/// See: https://github.com/fslaborg/FsMath/discussions/5 +/// +module VectorQuotationTests = + + /// Helper to evaluate quotations that return a value + let inline evalQ<'T> (expr: Microsoft.FSharp.Quotations.Expr<'T>) : 'T = + LeafExpressionConverter.EvaluateQuotation expr :?> 'T + + // ======================================== + // Quotation tests for zeroCreate + // ======================================== + + [] + let ``zeroCreate_Q: creates vector of zeros`` () = + let result = evalQ <@ Vector.zeroCreate 5 @> + floatArrayClose [| 0.0; 0.0; 0.0; 0.0; 0.0 |] result 1e-10 + + [] + let ``zeroCreate_Q: creates empty vector with size 0`` () = + let result = evalQ <@ Vector.zeroCreate 0 @> + Assert.Empty(result) + + [] + let ``zeroCreate_Q: works with int type`` () = + let result = evalQ <@ Vector.zeroCreate 3 @> + Assert.Equal>([| 0; 0; 0 |], result) + + // ======================================== + // Quotation tests for add + // ======================================== + + [] + let ``add_Q: adds two vectors element-wise`` () = + let v1 = [| 1.0; 2.0; 3.0 |] + let v2 = [| 4.0; 5.0; 6.0 |] + let result = evalQ <@ Vector.add v1 v2 @> + floatArrayClose [| 5.0; 7.0; 9.0 |] result 1e-10 + + [] + let ``add_Q: works with empty vectors`` () = + let v1 = Array.empty + let v2 = Array.empty + let result = evalQ <@ Vector.add v1 v2 @> + Assert.Empty(result) + + [] + let ``add_Q: works with single element`` () = + let v1 = [| 5.0 |] + let v2 = [| 3.0 |] + let result = evalQ <@ Vector.add v1 v2 @> + floatArrayClose [| 8.0 |] result 1e-10 + + [] + let ``add_Q: throws on dimension mismatch`` () = + let v1 = [| 1.0; 2.0 |] + let v2 = [| 1.0; 2.0; 3.0 |] + Assert.Throws(fun () -> evalQ <@ Vector.add v1 v2 @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for subtract + // ======================================== + + [] + let ``subtract_Q: subtracts two vectors element-wise`` () = + let v1 = [| 5.5; 5.0; 5.75 |] + let v2 = [| 1.5; 2.0; 3.25 |] + let result = evalQ <@ Vector.subtract v1 v2 @> + floatArrayClose [| 4.0; 3.0; 2.5 |] result 1e-10 + + [] + let ``subtract_Q: works with negative values`` () = + let v1 = [| 1.0; 2.0; 3.0 |] + let v2 = [| 4.0; 5.0; 6.0 |] + let result = evalQ <@ Vector.subtract v1 v2 @> + floatArrayClose [| -3.0; -3.0; -3.0 |] result 1e-10 + + [] + let ``subtract_Q: throws on dimension mismatch`` () = + let v1 = [| 1.0; 2.0 |] + let v2 = [| 1.0; 2.0; 3.0 |] + Assert.Throws(fun () -> evalQ <@ Vector.subtract v1 v2 @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for multiply + // ======================================== + + [] + let ``multiply_Q: multiplies two vectors element-wise`` () = + let v1 = [| 2.0; 3.0; 4.0 |] + let v2 = [| 5.0; 6.0; 7.0 |] + let result = evalQ <@ Vector.multiply v1 v2 @> + floatArrayClose [| 10.0; 18.0; 28.0 |] result 1e-10 + + [] + let ``multiply_Q: handles zeros`` () = + let v1 = [| 0.0; 5.0; 0.0 |] + let v2 = [| 10.0; 2.0; 15.0 |] + let result = evalQ <@ Vector.multiply v1 v2 @> + floatArrayClose [| 0.0; 10.0; 0.0 |] result 1e-10 + + [] + let ``multiply_Q: throws on dimension mismatch`` () = + let v1 = [| 1.0; 2.0 |] + let v2 = [| 1.0; 2.0; 3.0 |] + Assert.Throws(fun () -> evalQ <@ Vector.multiply v1 v2 @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for divide + // ======================================== + + [] + let ``divide_Q: divides two vectors element-wise`` () = + let v1 = [| 10.0; 20.0; 30.0 |] + let v2 = [| 2.0; 5.0; 5.0 |] + let result = evalQ <@ Vector.divide v1 v2 @> + floatArrayClose [| 5.0; 4.0; 6.0 |] result 1e-10 + + [] + let ``divide_Q: handles fractional results`` () = + let v1 = [| 1.0; 1.0; 1.0 |] + let v2 = [| 3.0; 4.0; 5.0 |] + let result = evalQ <@ Vector.divide v1 v2 @> + floatArrayClose [| 0.333333; 0.25; 0.2 |] result 1e-5 + + [] + let ``divide_Q: throws on dimension mismatch`` () = + let v1 = [| 1.0; 2.0 |] + let v2 = [| 1.0; 2.0; 3.0 |] + Assert.Throws(fun () -> evalQ <@ Vector.divide v1 v2 @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for addScalar + // ======================================== + + [] + let ``addScalar_Q: adds scalar to each element`` () = + let v = [| 2.5; 4.0; 6.0 |] + let result = evalQ <@ Vector.addScalar 2.0 v @> + floatArrayClose [| 4.5; 6.0; 8.0 |] result 1e-10 + + [] + let ``addScalar_Q: works with negative scalar`` () = + let v = [| 5.0; 10.0; 15.0 |] + let result = evalQ <@ Vector.addScalar -2.0 v @> + floatArrayClose [| 3.0; 8.0; 13.0 |] result 1e-10 + + [] + let ``addScalar_Q: works with empty vector`` () = + let v = Array.empty + let result = evalQ <@ Vector.addScalar 5.0 v @> + Assert.Empty(result) + + // ======================================== + // Quotation tests for subtractScalar + // ======================================== + + [] + let ``subtractScalar_Q: subtracts scalar from each element`` () = + let v = [| 2.5; 4.0; 6.0 |] + let result = evalQ <@ Vector.subtractScalar 0.5 v @> + floatArrayClose [| 2.0; 3.5; 5.5 |] result 1e-10 + + [] + let ``subtractScalar_Q: works with negative scalar`` () = + let v = [| 5.0; 10.0; 15.0 |] + let result = evalQ <@ Vector.subtractScalar -2.0 v @> + floatArrayClose [| 7.0; 12.0; 17.0 |] result 1e-10 + + // ======================================== + // Quotation tests for multiplyScalar + // ======================================== + + [] + let ``multiplyScalar_Q: multiplies each element by scalar`` () = + let v = [| 2.5; 4.0; 6.0 |] + let result = evalQ <@ Vector.multiplyScalar 1.5 v @> + floatArrayClose [| 3.75; 6.0; 9.0 |] result 1e-10 + + [] + let ``multiplyScalar_Q: works with zero scalar`` () = + let v = [| 5.0; 10.0; 15.0 |] + let result = evalQ <@ Vector.multiplyScalar 0.0 v @> + floatArrayClose [| 0.0; 0.0; 0.0 |] result 1e-10 + + [] + let ``multiplyScalar_Q: works with negative scalar`` () = + let v = [| 1.0; 2.0; 3.0 |] + let result = evalQ <@ Vector.multiplyScalar -2.0 v @> + floatArrayClose [| -2.0; -4.0; -6.0 |] result 1e-10 + + // ======================================== + // Quotation tests for divideScalar + // ======================================== + + [] + let ``divideScalar_Q: divides each element by scalar`` () = + let v = [| 10.0; 20.0; 30.0 |] + let result = evalQ <@ Vector.divideScalar 10.0 v @> + floatArrayClose [| 1.0; 2.0; 3.0 |] result 1e-10 + + [] + let ``divideScalar_Q: works with fractional divisor`` () = + let v = [| 1.0; 2.0; 3.0 |] + let result = evalQ <@ Vector.divideScalar 2.0 v @> + floatArrayClose [| 0.5; 1.0; 1.5 |] result 1e-10 + + // ======================================== + // Quotation tests for sum + // ======================================== + + [] + let ``sum_Q: computes sum of all elements`` () = + let v = [| 1.0; 2.0; 3.0; 4.0 |] + let result = evalQ <@ Vector.sum v @> + floatEqual 10.0 result 1e-10 + + [] + let ``sum_Q: works with empty vector`` () = + let v = Array.empty + let result = evalQ <@ Vector.sum v @> + floatEqual 0.0 result 1e-10 + + [] + let ``sum_Q: works with negative values`` () = + let v = [| -1.0; 2.0; -3.0; 4.0 |] + let result = evalQ <@ Vector.sum v @> + floatEqual 2.0 result 1e-10 + + // ======================================== + // Quotation tests for product + // ======================================== + + [] + let ``product_Q: multiplies all elements`` () = + let v = [| 1.0; 2.0; 3.0 |] + let result = evalQ <@ Vector.product v @> + floatEqual 6.0 result 1e-10 + + [] + let ``product_Q: works with zeros`` () = + let v = [| 1.0; 0.0; 3.0 |] + let result = evalQ <@ Vector.product v @> + floatEqual 0.0 result 1e-10 + + [] + let ``product_Q: works with empty vector`` () = + let v = Array.empty + let result = evalQ <@ Vector.product v @> + floatEqual 1.0 result 1e-10 + + // ======================================== + // Quotation tests for mean + // ======================================== + + [] + let ``mean_Q: computes mean of elements`` () = + let v = [| 2.0; 4.0; 6.0; 8.0 |] + let result = evalQ <@ Vector.mean v @> + floatEqual 5.0 result 1e-10 + + [] + let ``mean_Q: works with single element`` () = + let v = [| 42.0 |] + let result = evalQ <@ Vector.mean v @> + floatEqual 42.0 result 1e-10 + + [] + let ``mean_Q: throws on empty vector`` () = + let v = Array.empty + Assert.Throws(fun () -> evalQ <@ Vector.mean v @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for dot + // ======================================== + + [] + let ``dot_Q: computes dot product of two vectors`` () = + let v1 = [| 1.0; 2.0; 3.0 |] + let v2 = [| 4.0; 5.0; 6.0 |] + let result = evalQ <@ Vector.dot v1 v2 @> + floatEqual 32.0 result 1e-10 + + [] + let ``dot_Q: works with empty vectors`` () = + let v1 = Array.empty + let v2 = Array.empty + let result = evalQ <@ Vector.dot v1 v2 @> + floatEqual 0.0 result 1e-10 + + [] + let ``dot_Q: works with orthogonal vectors`` () = + let v1 = [| 1.0; 0.0 |] + let v2 = [| 0.0; 1.0 |] + let result = evalQ <@ Vector.dot v1 v2 @> + floatEqual 0.0 result 1e-10 + + [] + let ``dot_Q: throws on dimension mismatch`` () = + let v1 = [| 1.0; 2.0 |] + let v2 = [| 1.0; 2.0; 3.0 |] + Assert.Throws(fun () -> evalQ <@ Vector.dot v1 v2 @> |> ignore) |> ignore + + // ======================================== + // Quotation tests for norm + // ======================================== + + [] + let ``norm_Q: computes Euclidean norm`` () = + let v = [| 3.0; 4.0 |] + let result = evalQ <@ Vector.norm v @> + floatEqual 5.0 result 1e-10 + + [] + let ``norm_Q: works with single element`` () = + let v = [| 7.0 |] + let result = evalQ <@ Vector.norm v @> + floatEqual 7.0 result 1e-10 + + [] + let ``norm_Q: works with zero vector`` () = + let v = [| 0.0; 0.0; 0.0 |] + let result = evalQ <@ Vector.norm v @> + floatEqual 0.0 result 1e-10 + + // ======================================== + // Quotation tests for min + // ======================================== + + [] + let ``min_Q: finds minimum value`` () = + let v = [| 5.0; 1.0; 3.0; 0.5; 2.0 |] + let result = evalQ <@ Vector.min v @> + floatEqual 0.5 result 1e-10 + + [] + let ``min_Q: works with single element`` () = + let v = [| 42.0 |] + let result = evalQ <@ Vector.min v @> + floatEqual 42.0 result 1e-10 + + [] + let ``min_Q: handles negative values`` () = + let v = [| -5.0; -1.0; -10.0; -3.0 |] + let result = evalQ <@ Vector.min v @> + floatEqual -10.0 result 1e-10 + + // ======================================== + // Quotation tests for max + // ======================================== + + [] + let ``max_Q: finds maximum value`` () = + let v = [| 1.0; 5.0; 3.0; 7.0; 2.0 |] + let result = evalQ <@ Vector.max v @> + floatEqual 7.0 result 1e-10 + + [] + let ``max_Q: works with single element`` () = + let v = [| 42.0 |] + let result = evalQ <@ Vector.max v @> + floatEqual 42.0 result 1e-10 + + [] + let ``max_Q: handles negative values`` () = + let v = [| -5.0; -1.0; -10.0; -3.0 |] + let result = evalQ <@ Vector.max v @> + floatEqual -1.0 result 1e-10