Add syntax for exceptions (#19)

This commit is contained in:
Patrick Stevens
2025-07-06 22:30:02 +01:00
committed by GitHub
parent 75899d5668
commit ca74c4816b
8 changed files with 104 additions and 11 deletions

View File

@@ -44,6 +44,13 @@ let ``This test fails: plain text comparison of ToString`` () =
snapshot " 123 "
return 123
}
[<Test>]
let ``With return! and snapshotThrows, you can see exceptions too`` () =
expect {
snapshotThrows @"System.Exception: oh no"
return! (fun () -> failwith<int> "oh no")
}
```
You can adjust the formatting:

View File

@@ -0,0 +1,14 @@
namespace WoofWare.Expect.Test
open NUnit.Framework
open WoofWare.Expect
[<TestFixture>]
module TestExceptionThrowing =
[<Test>]
let ``Can throw an exception`` () =
expect {
snapshotThrows @"System.Exception: oh no"
return! (fun () -> failwith<int> "oh no")
}

View File

@@ -13,6 +13,7 @@
<Compile Include="Assembly.fs" />
<Compile Include="BulkUpdateExample.fs" />
<Compile Include="SimpleTest.fs" />
<Compile Include="TestExceptionThrowing.fs" />
<Compile Include="TestSurface.fs" />
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
<Compile Include="TestSnapshotFinding\TestUnicodeCharacters.fs" />

View File

@@ -142,6 +142,51 @@ type ExpectBuilder (mode : Mode) =
Actual = None
}
/// <summary>
/// Expresses that the given expression throws during evaluation.
/// </summary>
/// <example>
/// <code>
/// expect {
/// snapshotThrows @"System.Exception: oh no"
/// return! (fun () -> failwith "oh no")
/// }
/// </code>
/// </example>
[<CustomOperation("snapshotThrows", MaintainsVariableSpaceUsingBind = true)>]
member _.SnapshotThrows<'a>
(
state : ExpectState<'a>,
snapshot : string,
[<CallerMemberName>] ?memberName : string,
[<CallerLineNumber>] ?callerLine : int,
[<CallerFilePath>] ?filePath : string
)
: ExpectState<'a>
=
match state.Snapshot with
| Some _ -> failwith "snapshot can only be specified once"
| None ->
let memberName = defaultArg memberName "<unknown method>"
let filePath = defaultArg filePath "<unknown file>"
let lineNumber = defaultArg callerLine -1
let callerInfo =
{
MemberName = memberName
FilePath = filePath
LineNumber = lineNumber
}
{
Formatter = None
JsonSerialiserOptions = state.JsonSerialiserOptions
JsonDocOptions = state.JsonDocOptions
Snapshot = Some (SnapshotValue.ThrowsException snapshot, callerInfo)
Actual = None
}
/// <summary>
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
/// comparing to the snapshot.
@@ -156,7 +201,7 @@ type ExpectBuilder (mode : Mode) =
| Some _ -> failwith "Please don't supply withFormat more than once"
| None ->
{ state with
Formatter = Some formatter
Formatter = Some (fun f -> f () |> formatter)
}
/// <summary>
@@ -224,6 +269,17 @@ type ExpectBuilder (mode : Mode) =
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value".
member _.Return (value : 'T) : ExpectState<'T> =
{
Snapshot = None
Formatter = None
JsonDocOptions = None
JsonSerialiserOptions = None
Actual = Some (fun () -> value)
}
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value", but delayed behind
/// a function (by contrast with `Return`).
member _.ReturnFrom (value : unit -> 'T) : ExpectState<'T> =
{
Snapshot = None
Formatter = None

View File

@@ -17,20 +17,21 @@ type CallerInfo =
type private SnapshotValue =
| Json of expected : string
| Formatted of expected : string
| ThrowsException of expected : string
type private CompletedSnapshotValue<'T> =
| Json of expected : string * JsonSerializerOptions * JsonDocumentOptions
| Formatted of expected : string * format : ('T -> string)
| Formatted of expected : string * format : ((unit -> 'T) -> string)
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type ExpectState<'T> =
private
{
Formatter : ('T -> string) option
Formatter : ((unit -> 'T) -> string) option
JsonSerialiserOptions : JsonSerializerOptions option
JsonDocOptions : JsonDocumentOptions option
Snapshot : (SnapshotValue * CallerInfo) option
Actual : 'T option
Actual : (unit -> 'T) option
}
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
@@ -39,7 +40,7 @@ type internal CompletedSnapshotGeneric<'T> =
{
SnapshotValue : CompletedSnapshotValue<'T>
Caller : CallerInfo
Actual : 'T
Actual : unit -> 'T
}
[<RequireQualifiedAccess>]
@@ -68,11 +69,22 @@ module internal CompletedSnapshotGeneric =
| SnapshotValue.Formatted expected ->
let formatter =
match state.Formatter with
| None -> fun x -> x.ToString ()
| None -> fun x -> x().ToString ()
| Some f -> f
CompletedSnapshotValue.Formatted (expected, formatter)
| SnapshotValue.ThrowsException expected ->
CompletedSnapshotValue.Formatted (
expected,
fun x ->
try
x () |> ignore
"<no exception raised>"
with e ->
e.GetType().FullName + ": " + e.Message
)
{
SnapshotValue = snapshot
Caller = source
@@ -84,7 +96,7 @@ module internal CompletedSnapshotGeneric =
let internal replacement (s : CompletedSnapshotGeneric<'T>) =
match s.SnapshotValue with
| CompletedSnapshotValue.Json (_existing, options, _) ->
JsonSerializer.Serialize (s.Actual, options)
JsonSerializer.Serialize (s.Actual (), options)
|> JsonDocument.Parse
|> _.RootElement
|> _.ToString()
@@ -104,7 +116,7 @@ module internal CompletedSnapshotGeneric =
None
let canonicalActual =
JsonSerializer.Serialize (state.Actual, jsonSerOptions) |> JsonDocument.Parse
JsonSerializer.Serialize (state.Actual (), jsonSerOptions) |> JsonDocument.Parse
match canonicalSnapshot with
| None -> Some ("[JSON failed to parse:] " + snapshot, canonicalActual.RootElement.ToString ())

View File

@@ -147,7 +147,8 @@ module internal SnapshotUpdate =
let searchText = String.concat "\n" relevantLines
// Find snapshot keyword
let snapshotMatch = Regex.Match (searchText, @"\b(snapshot|snapshotJson)\b")
let snapshotMatch =
Regex.Match (searchText, @"\b(snapshot|snapshotJson|snapshotThrows)\b")
if not snapshotMatch.Success then
None

View File

@@ -16,9 +16,11 @@ WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectState, un
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.Return [method]: unit -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.ReturnFrom [method]: (unit -> 'T) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> unit
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: (unit WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.SnapshotThrows [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.WithFormat [method]: ('T WoofWare.Expect.ExpectState, 'T -> string) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.WithJsonDocOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonDocumentOptions) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState

View File

@@ -1,5 +1,5 @@
{
"version": "0.4",
"version": "0.5",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],