diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj index 42effae..1d38c5f 100644 --- a/src/Fable.Python.fsproj +++ b/src/Fable.Python.fsproj @@ -38,6 +38,10 @@ + + + + diff --git a/src/fable/Testing.fs b/src/fable/Testing.fs new file mode 100644 index 0000000..f1b9193 --- /dev/null +++ b/src/fable/Testing.fs @@ -0,0 +1,79 @@ +/// Cross-platform test utilities for writing tests that run on both .NET (with XUnit) and Python (with pytest). +/// +/// Example usage: +/// ```fsharp +/// open Fable.Python.Testing +/// +/// [] +/// let ``test addition works`` () = +/// let result = 2 + 2 +/// result |> equal 4 +/// +/// [] +/// let ``test throws on invalid input`` () = +/// throwsAnyError (fun () -> failwith "boom") +/// ``` +module Fable.Python.Testing + +open System + +#if FABLE_COMPILER +open Fable.Core.Testing + +/// Assert equality (expected first, then actual - F# style) +let equal expected actual : unit = Assert.AreEqual(actual, expected) + +/// Assert inequality +let notEqual expected actual : unit = Assert.NotEqual(actual, expected) + +/// Attribute to mark test functions (compiles to test_ prefix for pytest) +type FactAttribute() = + inherit System.Attribute() + +#else +open Xunit + +/// Assert equality (expected first, then actual - F# style) +let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual) + +/// Assert inequality +let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual) + +/// FactAttribute is already provided by XUnit +type FactAttribute = Xunit.FactAttribute +#endif + +// Exception testing helpers (work on both platforms) + +let private run (f: unit -> 'a) = + try + f () |> Ok + with e -> + Error e.Message + +/// Assert that a function throws an error with the exact message +let throwsError (expected: string) (f: unit -> 'a) : unit = + match run f with + | Error actual when actual = expected -> () + | Error actual -> equal expected actual + | Ok _ -> equal expected "No error was thrown" + +/// Assert that a function throws an error containing the expected substring +let throwsErrorContaining (expected: string) (f: unit -> 'a) : unit = + match run f with + | Error _ when String.IsNullOrEmpty expected -> () + | Error (actual: string) when actual.Contains expected -> () + | Error actual -> equal (sprintf "Error containing '%s'" expected) actual + | Ok _ -> equal (sprintf "Error containing '%s'" expected) "No error was thrown" + +/// Assert that a function throws any error +let throwsAnyError (f: unit -> 'a) : unit = + match run f with + | Error _ -> () + | Ok _ -> equal "An error" "No error was thrown" + +/// Assert that a function does not throw +let doesntThrow (f: unit -> 'a) : unit = + match run f with + | Ok _ -> () + | Error msg -> equal "No error" (sprintf "Error: %s" msg) diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj index 6292242..539890b 100644 --- a/test/Fable.Python.Test.fsproj +++ b/test/Fable.Python.Test.fsproj @@ -12,7 +12,6 @@ - @@ -26,6 +25,7 @@ + diff --git a/test/TestAst.fs b/test/TestAst.fs index 3cfde42..c733535 100644 --- a/test/TestAst.fs +++ b/test/TestAst.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Ast -open Util.Testing +open Fable.Python.Testing open Fable.Python.Ast [] diff --git a/test/TestAsyncIO.fs b/test/TestAsyncIO.fs index 0f73981..1be8cb5 100644 --- a/test/TestAsyncIO.fs +++ b/test/TestAsyncIO.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.AsyncIO -open Util.Testing +open Fable.Python.Testing open Fable.Python.AsyncIO [] diff --git a/test/TestBase64.fs b/test/TestBase64.fs index 05e0686..3d18999 100644 --- a/test/TestBase64.fs +++ b/test/TestBase64.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Base64 -open Util.Testing +open Fable.Python.Testing open Fable.Python.Base64 open Fable.Python.Builtins diff --git a/test/TestBuiltins.fs b/test/TestBuiltins.fs index 19ea2df..5aee957 100644 --- a/test/TestBuiltins.fs +++ b/test/TestBuiltins.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Builtins -open Util.Testing +open Fable.Python.Testing open Fable.Python.Builtins open Fable.Python.Os diff --git a/test/TestFastAPI.fs b/test/TestFastAPI.fs index 73818d9..4ec54d8 100644 --- a/test/TestFastAPI.fs +++ b/test/TestFastAPI.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.TestFastAPI -open Fable.Python.Tests.Util.Testing +open Fable.Python.Testing #if FABLE_COMPILER open Fable.Core diff --git a/test/TestJson.fs b/test/TestJson.fs index b4e18af..1ba7543 100644 --- a/test/TestJson.fs +++ b/test/TestJson.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Json -open Util.Testing +open Fable.Python.Testing open Fable.Python.Json // Test types for union and record serialization diff --git a/test/TestLogging.fs b/test/TestLogging.fs index 8f95323..939c0c8 100644 --- a/test/TestLogging.fs +++ b/test/TestLogging.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Logging -open Util.Testing +open Fable.Python.Testing open Fable.Python.Logging [] diff --git a/test/TestMath.fs b/test/TestMath.fs index 55349f8..759fc8f 100644 --- a/test/TestMath.fs +++ b/test/TestMath.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Math -open Util.Testing +open Fable.Python.Testing open Fable.Python.Math [] diff --git a/test/TestOs.fs b/test/TestOs.fs index f01ab88..07a1488 100644 --- a/test/TestOs.fs +++ b/test/TestOs.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Os -open Util.Testing +open Fable.Python.Testing open Fable.Python.Os [] diff --git a/test/TestPydantic.fs b/test/TestPydantic.fs index a3f9ed0..81039c3 100644 --- a/test/TestPydantic.fs +++ b/test/TestPydantic.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.TestPydantic -open Fable.Python.Tests.Util.Testing +open Fable.Python.Testing #if FABLE_COMPILER open Fable.Core diff --git a/test/TestRandom.fs b/test/TestRandom.fs index 7dfbae0..ca3e470 100644 --- a/test/TestRandom.fs +++ b/test/TestRandom.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Random -open Util.Testing +open Fable.Python.Testing open Fable.Python.Random [] diff --git a/test/TestString.fs b/test/TestString.fs index d449e77..5d1b808 100644 --- a/test/TestString.fs +++ b/test/TestString.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.String -open Util.Testing +open Fable.Python.Testing open Fable.Python.String [] diff --git a/test/TestTesting.fs b/test/TestTesting.fs new file mode 100644 index 0000000..cead90a --- /dev/null +++ b/test/TestTesting.fs @@ -0,0 +1,115 @@ +module Fable.Python.Tests.Testing + +open Fable.Python.Testing + +// ============================================================================ +// Test that equal works correctly +// ============================================================================ + +[] +let ``test equal passes for equal values`` () = + equal 1 1 + equal "hello" "hello" + equal true true + equal [1; 2; 3] [1; 2; 3] + +[] +let ``test equal fails for unequal values`` () = + // This tests that equal actually fails when values differ + // We use throwsAnyError to verify the assertion throws + throwsAnyError (fun () -> equal 1 2) + +[] +let ``test equal fails for unequal strings`` () = + throwsAnyError (fun () -> equal "hello" "world") + +[] +let ``test equal fails for unequal booleans`` () = + throwsAnyError (fun () -> equal true false) + +// ============================================================================ +// Test that notEqual works correctly +// ============================================================================ + +[] +let ``test notEqual passes for unequal values`` () = + notEqual 1 2 + notEqual "hello" "world" + notEqual true false + +[] +let ``test notEqual fails for equal values`` () = + throwsAnyError (fun () -> notEqual 1 1) + +[] +let ``test notEqual fails for equal strings`` () = + throwsAnyError (fun () -> notEqual "hello" "hello") + +// ============================================================================ +// Test throwsAnyError +// ============================================================================ + +[] +let ``test throwsAnyError passes when function throws`` () = + throwsAnyError (fun () -> failwith "boom") + +[] +let ``test throwsAnyError fails when function does not throw`` () = + // Meta-test: throwsAnyError should fail if the function doesn't throw + throwsAnyError (fun () -> + throwsAnyError (fun () -> 42) + ) + +// ============================================================================ +// Test doesntThrow +// ============================================================================ + +[] +let ``test doesntThrow passes when function succeeds`` () = + doesntThrow (fun () -> 1 + 1) + +[] +let ``test doesntThrow fails when function throws`` () = + throwsAnyError (fun () -> + doesntThrow (fun () -> failwith "boom") + ) + +// ============================================================================ +// Test throwsError with exact message +// ============================================================================ + +[] +let ``test throwsError passes with matching message`` () = + throwsError "exact error" (fun () -> failwith "exact error") + +[] +let ``test throwsError fails with wrong message`` () = + throwsAnyError (fun () -> + throwsError "expected message" (fun () -> failwith "different message") + ) + +[] +let ``test throwsError fails when no error thrown`` () = + throwsAnyError (fun () -> + throwsError "expected error" (fun () -> 42) + ) + +// ============================================================================ +// Test throwsErrorContaining +// ============================================================================ + +[] +let ``test throwsErrorContaining passes when message contains substring`` () = + throwsErrorContaining "partial" (fun () -> failwith "this is a partial match error") + +[] +let ``test throwsErrorContaining fails when message does not contain substring`` () = + throwsAnyError (fun () -> + throwsErrorContaining "notfound" (fun () -> failwith "different error message") + ) + +[] +let ``test throwsErrorContaining fails when no error thrown`` () = + throwsAnyError (fun () -> + throwsErrorContaining "error" (fun () -> 42) + ) diff --git a/test/TestTypes.fs b/test/TestTypes.fs index 66743c8..41a4df4 100644 --- a/test/TestTypes.fs +++ b/test/TestTypes.fs @@ -1,6 +1,6 @@ module Fable.Python.Tests.Types -open Util.Testing +open Fable.Python.Testing open Fable.Python.Fable.Types // Test typeName function diff --git a/test/Util.fs b/test/Util.fs deleted file mode 100644 index d765e47..0000000 --- a/test/Util.fs +++ /dev/null @@ -1,31 +0,0 @@ -module Fable.Python.Tests.Util - -module Testing = -#if FABLE_COMPILER - open Fable.Core - open Fable.Core.PyInterop - - type Assert = - [] - static member AreEqual(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly - - [] - static member NotEqual(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly - - let equal expected actual : unit = Assert.AreEqual(actual, expected) - let notEqual expected actual : unit = Assert.NotEqual(actual, expected) - - type Fact() = - inherit System.Attribute() -#else - open Xunit - type FactAttribute = Xunit.FactAttribute - - let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual) - let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual) -#endif - let rec sumFirstSeq (zs: seq) (n: int) : float = - match n with - | 0 -> 0. - | 1 -> Seq.head zs - | _ -> (Seq.head zs) + sumFirstSeq (Seq.skip 1 zs) (n - 1)