From 3589324697a1cde0c55e2a2fbd283d7ca2174eb0 Mon Sep 17 00:00:00 2001 From: Smaug123 <3138005+Smaug123@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:01:19 +0100 Subject: [PATCH] Comments and break the build to indicate missing functionality --- README.md | 21 +++++++- WoofWare.Expect.Test/TestSnapshotList.fs | 13 ++++- WoofWare.Expect/Builder.fs | 67 +++++++++++++++++++++--- WoofWare.Expect/Diff.fs | 2 + 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c13d08f..aeb9d19 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ An [expect-testing](https://blog.janestreet.com/the-joy-of-expect-tests/) librar # Current status The basic mechanism works. -Snapshot updating is vibe-coded with Opus 4 and is purely text-based; I didn't want to use the F# compiler services because that's a pretty heavyweight dependency which should be confined to a separate test runner entity. -It's fairly well tested, but you will certainly be able to find ways to break it; try not to be too fancy with your syntax around the `snapshot` statement. +It's fairly well tested, but you will almost certainly be able to find ways to break it; try not to be too fancy with your syntax around the `snapshot` statement. # How to use @@ -51,8 +50,17 @@ let ``With return! and snapshotThrows, you can see exceptions too`` () = snapshotThrows @"System.Exception: oh no" return! (fun () -> failwith "oh no") } + +[] +let ``You can do lists more neatly with the snapshotList keyword`` () = + expect { + snapshotList [ "8" ; "9" ; "10" ; "11" ; "12" ] + return [ 8..12 ] + } ``` +(JSON output for elements is not yet supported with `snapshotList`.) + You can adjust the formatting: ```fsharp @@ -64,6 +72,15 @@ let ``Overriding the formatting`` () = snapshot @"Int32" return 123 } + +[] +let ``Overriding the formatting with lists`` () = + expect { + // these two lines *do* have to be in this order, for annoying reasons + snapshotList [ "8" ; "9" ; "0" ; "1" ; "2" ] + withFormat (fun x -> string (x % 10)) + return [ 8..12 ] + } ``` You can override the JSON serialisation if you find the snapshot format displeasing: diff --git a/WoofWare.Expect.Test/TestSnapshotList.fs b/WoofWare.Expect.Test/TestSnapshotList.fs index 2a2b910..66a96e0 100644 --- a/WoofWare.Expect.Test/TestSnapshotList.fs +++ b/WoofWare.Expect.Test/TestSnapshotList.fs @@ -5,18 +5,27 @@ open WoofWare.Expect [] module TestSnapshotList = + [] + let ``Prepare to bulk-update tests`` () = + // If you don't want to enter bulk-update mode, just replace this line with a no-op `()`. + // The `updateAllSnapshots` tear-down below will simply do nothing in that case. + GlobalBuilderConfig.enterBulkUpdateMode () + + [] + let ``Update all tests`` () = + GlobalBuilderConfig.updateAllSnapshots () [] let ``simple list test`` () = expect { - snapshotList [ "1" ; "2" ; "3" ] + snapshotList [] return [ 1..3 ] } [] let ``list test with formatting`` () = expect { - snapshotList [ "8" ; "9" ; "0" ; "1" ; "2" ] + snapshotList [] withFormat (fun x -> string (x % 10)) return [ 8..12 ] } diff --git a/WoofWare.Expect/Builder.fs b/WoofWare.Expect/Builder.fs index 39b039f..5fcb358 100644 --- a/WoofWare.Expect/Builder.fs +++ b/WoofWare.Expect/Builder.fs @@ -17,8 +17,6 @@ type Mode = | Update | AssertMockingSource of (string * int) -type BuilderKindNormal<'T> = | BuilderKindNormal of unit - /// /// The builder which powers WoofWare.Expect. /// @@ -35,6 +33,7 @@ type ExpectBuilder (mode : Mode) = else ExpectBuilder Mode.Assert + /// Combine two `ExpectStateListy`s. The first one is the "expected" snapshot; the second is the "actual". member _.Bind<'U> (state : ExpectStateListy<'U>, f : unit -> ExpectStateListy<'U>) : ExpectStateListy<'U> = let actual = f () @@ -58,6 +57,8 @@ type ExpectBuilder (mode : Mode) = Actual = actual.Actual } + /// Combine an `ExpectStateListy` with an `ExpectState`. The first one is the "expected" snapshot; the second is + /// the "actual". member _.Bind<'U, 'elt when 'U :> IEnumerable<'elt>> (state : ExpectStateListy<'elt>, f : unit -> ExpectState<'U>) : ExpectStateListy<'elt> @@ -126,6 +127,7 @@ type ExpectBuilder (mode : Mode) = JsonDocOptions = jsonDocOptions } + /// Express that the actual value's ToString should identically equal this string. [] member _.Snapshot<'a> ( @@ -190,6 +192,13 @@ type ExpectBuilder (mode : Mode) = Actual = None } + /// + /// Express that the actual value, when converted to JSON, should result in a JSON document + /// which matches the JSON document that is this string. + /// + /// + /// For example, snapshotJson "123" indicates the JSON integer 123. + /// [] member this.SnapshotJson<'a> ( @@ -263,6 +272,13 @@ type ExpectBuilder (mode : Mode) = Actual = None } + /// + /// Express that the actual value, which is a sequence, should have elements which individually (in order) match + /// this snapshot list. + /// + /// + /// For example, snapshotList ["123" ; "456"] indicates an exactly-two-element list [123 ; 456]. + /// [] member _.SnapshotList<'a> ( @@ -340,6 +356,17 @@ type ExpectBuilder (mode : Mode) = Actual = None } + /// + /// Expresses that the given expression throws during evaluation. + /// + /// + /// + /// expect { + /// snapshotThrows @"System.Exception: oh no" + /// return! (fun () -> failwith "oh no") + /// } + /// + /// [] member _.SnapshotThrows<'a> ( @@ -377,7 +404,6 @@ type ExpectBuilder (mode : Mode) = /// /// Express that the return value of this builder should be formatted using this function, before /// comparing to the snapshot. - /// this value. /// /// /// For example, withFormat (fun x -> x.ToString ()) "123" is equivalent to snapshot "123". @@ -391,6 +417,14 @@ type ExpectBuilder (mode : Mode) = Formatter = Some (fun f -> f () |> formatter) } + /// + /// Express that the return value of this builder should be formatted using this function, before + /// comparing to the snapshot. + /// In the case of snapshotList, this applies to the elements of the sequence, not to the sequence itself. + /// + /// + /// For example, withFormat (fun x -> x.ToString ()) "123" is equivalent to snapshot "123". + /// [] member _.WithFormat<'T> (state : ExpectStateListy<'T>, formatter : 'T -> string) = match state.Formatter with @@ -423,6 +457,20 @@ type ExpectBuilder (mode : Mode) = JsonSerialiserOptions = Some jsonOptions } + /// + /// Express that these JsonSerializerOptions should be used to construct the JSON object to which the snapshot + /// is to be compared (or, in write-out-the-snapshot mode, to construct the JSON object to be written out). + /// + /// + /// If you want your snapshots to be written out compactly, rather than the default indenting: + /// + /// expect { + /// snapshotJson @"{""a"":3}" + /// withJsonSerializerOptions (JsonSerializerOptions (WriteIndented = false)) + /// return Map.ofList ["a", 3] + /// } + /// + /// [] member _.WithJsonSerializerOptions<'T> (state : ExpectStateListy<'T>, jsonOptions : JsonSerializerOptions) = match state.Snapshot with @@ -506,6 +554,7 @@ type ExpectBuilder (mode : Mode) = /// Computation expression `Delay`. member _.Delay (f : unit -> ExpectStateListy<'T>) : unit -> ExpectStateListy<'T> = f + /// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails. member _.Run (f : unit -> ExpectStateListy<'T>) : unit = let state = f () @@ -528,14 +577,16 @@ type ExpectBuilder (mode : Mode) = if snapshot <> actual then let diff = - Diff.patienceLines (Array.ofList snapshot) (Array.ofList actual) - |> Diff.format + Diff.patienceLines (Array.ofList snapshot) (Array.ofList actual) |> Diff.format match mode with | Mode.Assert -> - $"snapshot mismatch! snapshot at %s{caller.FilePath}:%i{caller.LineNumber} (%s{caller.MemberName}) diff:\n%s{diff}" - |> ExpectException - |> raise + if GlobalBuilderConfig.isBulkUpdateMode () then + GlobalBuilderConfig.registerTest state + else + $"snapshot mismatch! snapshot at %s{caller.FilePath}:%i{caller.LineNumber} (%s{caller.MemberName}) diff:\n%s{diff}" + |> ExpectException + |> raise | Mode.AssertMockingSource (mockSource, line) -> $"snapshot mismatch! snapshot at %s{mockSource}:%i{line} (%s{caller.MemberName}) diff:\n%s{diff}" |> ExpectException diff --git a/WoofWare.Expect/Diff.fs b/WoofWare.Expect/Diff.fs index 9828fbe..42575d4 100644 --- a/WoofWare.Expect/Diff.fs +++ b/WoofWare.Expect/Diff.fs @@ -44,6 +44,7 @@ type internal UniqueLines<'line when 'line : comparison> = LineCounts : Map<'line, int> } +/// The diff between two line-oriented pieces of text. [] module Diff = /// Find lines that appear exactly once in a sequence @@ -260,6 +261,7 @@ module Diff = ) |> String.concat "\n" + /// Format the diff as a human-readable string, including line numbers at the left. let formatWithLineNumbers (d : Diff) : string = formatWithLineNumbers' id d /// Format the diff as a human-readable string.