1 Commits

Author SHA1 Message Date
Patrick Stevens
ebc24f85aa Add snapshotList syntax for sequences (#25) 2025-07-30 08:11:36 +00:00
12 changed files with 777 additions and 34 deletions

View File

@@ -16,8 +16,7 @@ An [expect-testing](https://blog.janestreet.com/the-joy-of-expect-tests/) librar
# Current status # Current status
The basic mechanism works. 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 almost 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 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 # How to use
@@ -51,8 +50,17 @@ let ``With return! and snapshotThrows, you can see exceptions too`` () =
snapshotThrows @"System.Exception: oh no" snapshotThrows @"System.Exception: oh no"
return! (fun () -> failwith<int> "oh no") return! (fun () -> failwith<int> "oh no")
} }
[<Test>]
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: You can adjust the formatting:
```fsharp ```fsharp
@@ -64,6 +72,15 @@ let ``Overriding the formatting`` () =
snapshot @"Int32" snapshot @"Int32"
return 123 return 123
} }
[<Test>]
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<int> (x % 10))
return [ 8..12 ]
}
``` ```
You can override the JSON serialisation if you find the snapshot format displeasing: You can override the JSON serialisation if you find the snapshot format displeasing:

View File

@@ -0,0 +1,32 @@
namespace WoofWare.Expect.Test
open NUnit.Framework
open WoofWare.Expect
[<TestFixture>]
module TestSnapshotList =
[<OneTimeSetUp>]
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 ()
()
[<OneTimeTearDown>]
let ``Update all tests`` () =
GlobalBuilderConfig.updateAllSnapshots ()
[<Test>]
let ``simple list test`` () =
expect {
snapshotList [ "1" ; "2" ; "3" ]
return [ 1..3 ]
}
[<Test>]
let ``list test with formatting`` () =
expect {
snapshotList [ "8" ; "9" ; "0" ; "1" ; "2" ]
withFormat (fun x -> string<int> (x % 10))
return [ 8..12 ]
}

View File

@@ -16,6 +16,7 @@
<Compile Include="TestDiff.fs" /> <Compile Include="TestDiff.fs" />
<Compile Include="TestDot.fs" /> <Compile Include="TestDot.fs" />
<Compile Include="TestExceptionThrowing.fs" /> <Compile Include="TestExceptionThrowing.fs" />
<Compile Include="TestSnapshotList.fs" />
<Compile Include="TestSurface.fs" /> <Compile Include="TestSurface.fs" />
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" /> <Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
<Compile Include="TestSnapshotFinding\TestUnicodeCharacters.fs" /> <Compile Include="TestSnapshotFinding\TestUnicodeCharacters.fs" />

View File

@@ -0,0 +1,213 @@
namespace WoofWare.Expect
// This file mostly courtesy of Claude 4 Opus.
open Fantomas.FCS.Diagnostics
open Fantomas.FCS.Syntax
open Fantomas.FCS.Text
type internal SnapshotLocation =
{
KeywordRange : Range
Keyword : string
ReplacementRange : Range
}
[<RequireQualifiedAccess>]
module internal AstWalker =
let private snapshotSignifier =
[ "snapshot" ; "snapshotJson" ; "snapshotList" ; "snapshotThrows" ]
|> Set.ofList
/// Check if this is a call to snapshotList (or any other snapshot method we care about)
/// Returns the identifier that is the snapshot invocation, and its range.
let private isSnapshotCall (funcExpr : SynExpr) : (string * Range) option =
match funcExpr with
| SynExpr.Ident ident when snapshotSignifier.Contains ident.idText -> Some (ident.idText, ident.idRange)
| SynExpr.LongIdent (_, longIdent, _, _) ->
match longIdent.IdentsWithTrivia with
| [] -> None
| ids ->
match List.last ids with
| SynIdent.SynIdent (ident, _) ->
if snapshotSignifier.Contains ident.idText then
Some (ident.idText, ident.idRange)
else
None
| _ -> None
/// Extract the argument from a method application
let private getMethodArgument (expr : SynExpr) =
match expr with
| SynExpr.App (_, _, _, argExpr, _) -> Some argExpr
| _ -> None
/// Walk expressions looking for our target
let rec findSnapshotListCalls (targetLine : int) (methodName : string) (expr : SynExpr) : SnapshotLocation list =
match expr with
// Direct method application
| SynExpr.App (_, _, funcExpr, argExpr, range) ->
match isSnapshotCall funcExpr with
| Some (keyword, keywordRange) ->
if range.StartLine <= targetLine && targetLine <= range.EndLine then
match argExpr with
| SynExpr.ArrayOrList (isList, _, argRange) when isList ->
// It's a list literal
[
{
ReplacementRange = argRange
KeywordRange = keywordRange
Keyword = keyword
}
] // Text will be extracted separately
| SynExpr.ArrayOrListComputed (isArray, _inner, argRange) when not isArray ->
// It's a list comprehension
[
{
ReplacementRange = argRange
KeywordRange = keywordRange
Keyword = keyword
}
]
| _ ->
// It could be a variable reference or other expression
[
{
ReplacementRange = argExpr.Range
KeywordRange = keywordRange
Keyword = keyword
}
]
else
[]
| None ->
// Other app variations
findSnapshotListCalls targetLine methodName funcExpr
@ findSnapshotListCalls targetLine methodName argExpr
// Nested in paren
| SynExpr.Paren (innerExpr, _, _, _) -> findSnapshotListCalls targetLine methodName innerExpr
// Sequential expressions (e.g., in a do block)
| SynExpr.Sequential (_, _, expr1, expr2, _, _) ->
findSnapshotListCalls targetLine methodName expr1
@ findSnapshotListCalls targetLine methodName expr2
// Let bindings
| SynExpr.LetOrUse (_, _, bindings, bodyExpr, _, _) ->
let bindingResults =
bindings
|> List.collect (fun binding ->
match binding with
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
)
bindingResults @ findSnapshotListCalls targetLine methodName bodyExpr
// Match expressions
| SynExpr.Match (_, _, clauses, _, _) ->
clauses
|> List.collect (fun (SynMatchClause (resultExpr = expr)) ->
findSnapshotListCalls targetLine methodName expr
)
// If/then/else
| SynExpr.IfThenElse (_, thenExpr, elseExprOpt, _, _, _, _) ->
let thenResults = findSnapshotListCalls targetLine methodName thenExpr
let elseResults =
match elseExprOpt with
| Some elseExpr -> findSnapshotListCalls targetLine methodName elseExpr
| None -> []
thenResults @ elseResults
// Lambda
| SynExpr.Lambda (body = bodyExpr) -> findSnapshotListCalls targetLine methodName bodyExpr
// Computation expression
| SynExpr.ComputationExpr (_, innerExpr, _) -> findSnapshotListCalls targetLine methodName innerExpr
// Default case - no results
| _ -> []
/// Walk a module or namespace looking for expressions
let rec findInModuleDecls (targetLine : int) (methodName : string) (decls : SynModuleDecl list) =
decls
|> List.collect (fun decl ->
match decl with
| SynModuleDecl.Let (_, bindings, _) ->
bindings
|> List.collect (fun binding ->
match binding with
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
)
| SynModuleDecl.Expr (expr, _) -> findSnapshotListCalls targetLine methodName expr
| SynModuleDecl.NestedModule (decls = nestedDecls) -> findInModuleDecls targetLine methodName nestedDecls
| SynModuleDecl.Types (typeDefs, _) ->
typeDefs
|> List.collect (fun typeDef ->
match typeDef with
| SynTypeDefn (typeRepr = SynTypeDefnRepr.ObjectModel (members = members)) ->
members
|> List.collect (fun member' ->
match member' with
| SynMemberDefn.Member (memberBinding, _) ->
match memberBinding with
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
| _ -> []
)
| _ -> []
)
| SynModuleDecl.HashDirective _
| SynModuleDecl.Attributes _
| SynModuleDecl.ModuleAbbrev _
| SynModuleDecl.Exception _
| SynModuleDecl.Open _ -> []
| SynModuleDecl.NamespaceFragment (SynModuleOrNamespace (decls = decls)) ->
findInModuleDecls targetLine methodName decls
)
/// Main function to find snapshot list locations
let findSnapshotList
(infoFilePath : string)
(lines : string[])
(lineNumber : int)
(methodName : string) // e.g., "snapshotList"
: SnapshotLocation
=
let sourceText = SourceText.ofString (String.concat "\n" lines)
// Parse the file
let parsedInput, diagnostics = Fantomas.FCS.Parse.parseFile false sourceText []
// Check for parse errors
if
diagnostics
|> List.exists (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
then
failwithf $"Parse errors in file %s{infoFilePath}: %A{diagnostics}"
// Walk the AST
let results =
match parsedInput with
| ParsedInput.ImplFile (ParsedImplFileInput (contents = modules)) ->
modules
|> List.collect (fun moduleOrNs ->
match moduleOrNs with
| SynModuleOrNamespace (decls = decls) -> findInModuleDecls lineNumber methodName decls
)
| ParsedInput.SigFile _ -> failwith "unexpected: signature files can't contain expressions"
// Find the closest match
results
|> Seq.filter (fun loc ->
loc.KeywordRange.StartLine <= lineNumber
&& lineNumber <= loc.KeywordRange.EndLine
)
|> Seq.exactlyOne

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Expect namespace WoofWare.Expect
open System.Collections.Generic
open System.IO open System.IO
open System.Runtime.CompilerServices open System.Runtime.CompilerServices
open System.Text.Json open System.Text.Json
@@ -32,6 +33,61 @@ type ExpectBuilder (mode : Mode) =
else else
ExpectBuilder Mode.Assert 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 ()
match state.Actual with
| Some _ -> failwith "somehow came in with an Actual"
| None ->
match actual.Snapshot with
| Some _ -> failwith "somehow Actual came through with a Snapshot"
| None ->
let formatter =
match state.Formatter, actual.Formatter with
| None, f -> f
| Some f, None -> Some f
| Some _, Some _ -> failwith "multiple formatters supplied for a single expect!"
{
Formatter = formatter
Snapshot = state.Snapshot
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>
=
let actual = f ()
match state.Actual with
| Some _ -> failwith "somehow came in with an Actual"
| None ->
match actual.Snapshot with
| Some _ -> failwith "somehow Actual came through with a Snapshot"
| None ->
let formatter : ((unit -> 'elt) -> string) option =
match state.Formatter, actual.Formatter with
| None, None -> None
| None, Some _ ->
failwith
"unexpectedly had a formatter supplied before the snapshotList keyword; I thought this was impossible"
| Some f, None -> Some f
| Some _, Some _ -> failwith "multiple formatters supplied for a single expect!"
{
Formatter = formatter
Snapshot = state.Snapshot
Actual = actual.Actual |> Option.map (fun f () -> Seq.cast<'elt> (f ()))
}
/// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual". /// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
member _.Bind<'U> (state : ExpectState<'U>, f : unit -> ExpectState<'U>) : ExpectState<'U> = member _.Bind<'U> (state : ExpectState<'U>, f : unit -> ExpectState<'U>) : ExpectState<'U> =
let actual = f () let actual = f ()
@@ -71,6 +127,40 @@ type ExpectBuilder (mode : Mode) =
JsonDocOptions = jsonDocOptions JsonDocOptions = jsonDocOptions
} }
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
member _.Snapshot<'a>
(
state : ExpectStateListy<'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 = state.Formatter
JsonSerialiserOptions = None
JsonDocOptions = None
Snapshot = Some (SnapshotValue.Formatted snapshot, callerInfo)
Actual = None
}
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary> /// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>] [<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
member _.Snapshot<'a> member _.Snapshot<'a>
@@ -102,6 +192,46 @@ type ExpectBuilder (mode : Mode) =
Actual = None Actual = None
} }
/// <summary>
/// Express that the actual value, when converted to JSON, should result in a JSON document
/// which matches the JSON document that is this string.
/// </summary>
/// <remarks>
/// For example, <c>snapshotJson "123"</c> indicates the JSON integer 123.
/// </remarks>
[<CustomOperation("snapshotJson", MaintainsVariableSpaceUsingBind = true)>]
member this.SnapshotJson<'a>
(
state : ExpectStateListy<unit>,
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 = None
JsonDocOptions = None
Snapshot = Some (SnapshotValue.Json snapshot, callerInfo)
Actual = None
}
/// <summary> /// <summary>
/// Express that the actual value, when converted to JSON, should result in a JSON document /// Express that the actual value, when converted to JSON, should result in a JSON document
/// which matches the JSON document that is this string. /// which matches the JSON document that is this string.
@@ -142,6 +272,45 @@ type ExpectBuilder (mode : Mode) =
Actual = None Actual = None
} }
/// <summary>
/// Express that the actual value, which is a sequence, should have elements which individually (in order) match
/// this snapshot list.
/// </summary>
/// <remarks>
/// For example, <c>snapshotList ["123" ; "456"]</c> indicates an exactly-two-element list <c>[123 ; 456]</c>.
/// </remarks>
[<CustomOperation("snapshotList", MaintainsVariableSpaceUsingBind = true)>]
member _.SnapshotList<'a>
(
state : ExpectStateListy<unit>,
snapshot : string list,
[<CallerMemberName>] ?memberName : string,
[<CallerLineNumber>] ?callerLine : int,
[<CallerFilePath>] ?filePath : string
)
: ExpectStateListy<'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
Snapshot = Some (snapshot, callerInfo)
Actual = None
}
/// <summary> /// <summary>
/// Expresses that the given expression throws during evaluation. /// Expresses that the given expression throws during evaluation.
/// </summary> /// </summary>
@@ -187,10 +356,54 @@ type ExpectBuilder (mode : Mode) =
Actual = None 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 : ExpectStateListy<'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 = None
JsonDocOptions = None
Snapshot = Some (SnapshotValue.ThrowsException snapshot, callerInfo)
Actual = None
}
/// <summary> /// <summary>
/// Express that the <c>return</c> value of this builder should be formatted using this function, before /// Express that the <c>return</c> value of this builder should be formatted using this function, before
/// comparing to the snapshot. /// comparing to the snapshot.
/// this value.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>. /// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>.
@@ -204,6 +417,23 @@ type ExpectBuilder (mode : Mode) =
Formatter = Some (fun f -> f () |> formatter) Formatter = Some (fun f -> f () |> formatter)
} }
/// <summary>
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
/// comparing to the snapshot.
/// In the case of <c>snapshotList</c>, this applies to the elements of the sequence, not to the sequence itself.
/// </summary>
/// <remarks>
/// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>.
/// </remarks>
[<CustomOperation("withFormat", MaintainsVariableSpaceUsingBind = true)>]
member _.WithFormat<'T> (state : ExpectStateListy<'T>, formatter : 'T -> string) =
match state.Formatter with
| Some _ -> failwith "Please don't supply withFormat more than once"
| None ->
{ state with
Formatter = Some (fun f -> f () |> formatter)
}
/// <summary> /// <summary>
/// Express that these JsonSerializerOptions should be used to construct the JSON object to which the snapshot /// 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). /// is to be compared (or, in write-out-the-snapshot mode, to construct the JSON object to be written out).
@@ -227,6 +457,42 @@ type ExpectBuilder (mode : Mode) =
JsonSerialiserOptions = Some jsonOptions JsonSerialiserOptions = Some jsonOptions
} }
/// <summary>
/// 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).
/// </summary>
/// <example>
/// If you want your snapshots to be written out compactly, rather than the default indenting:
/// <code>
/// expect {
/// snapshotJson @"{""a"":3}"
/// withJsonSerializerOptions (JsonSerializerOptions (WriteIndented = false))
/// return Map.ofList ["a", 3]
/// }
/// </code>
/// </example>
[<CustomOperation("withJsonSerializerOptions", MaintainsVariableSpaceUsingBind = true)>]
member _.WithJsonSerializerOptions<'T> (state : ExpectStateListy<'T>, jsonOptions : JsonSerializerOptions) =
match state.Snapshot with
| Some _ ->
failwith
"I don't know how you've managed to do this; please raise an issue against github.com/Smaug123/WoofWare.Expect . Somehow withJsonSerializerOptions has already got a snapshot."
| None ->
match state.Actual with
| Some _ ->
failwith
"I don't know how you've managed to do this; please raise an issue against github.com/Smaug123/WoofWare.Expect . Somehow withJsonSerializerOptions has already got an expected-value."
| None ->
{
Formatter = state.Formatter
JsonSerialiserOptions = Some jsonOptions
JsonDocOptions = None
Snapshot = None
Actual = None
}
/// <summary> /// <summary>
/// Express that these JsonDocumentOptions should be used when parsing the snapshot string into a JSON object. /// Express that these JsonDocumentOptions should be used when parsing the snapshot string into a JSON object.
/// </summary> /// </summary>
@@ -258,23 +524,21 @@ type ExpectBuilder (mode : Mode) =
} }
/// MaintainsVariableSpaceUsingBind causes this to be used; it's a dummy representing "no snapshot and no assertion". /// MaintainsVariableSpaceUsingBind causes this to be used; it's a dummy representing "no snapshot and no assertion".
member _.Return (() : unit) : ExpectState<'T> = member _.Return (() : unit) : ExpectStateListy<'T> =
{ {
Formatter = None Formatter = None
JsonSerialiserOptions = None
JsonDocOptions = None
Snapshot = None Snapshot = None
Actual = None Actual = None
} }
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value". /// Expresses the "actual value" component of the assertion "expected snapshot = actual value".
member _.Return (value : 'T) : ExpectState<'T> = member _.Return<'T> (value : 'T) : ExpectState<'T> =
{ {
Snapshot = None Snapshot = None
Formatter = None Formatter = None
JsonDocOptions = None
JsonSerialiserOptions = None
Actual = Some (fun () -> value) Actual = Some (fun () -> value)
JsonSerialiserOptions = None
JsonDocOptions = None
} }
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value", but delayed behind /// Expresses the "actual value" component of the assertion "expected snapshot = actual value", but delayed behind
@@ -291,6 +555,56 @@ type ExpectBuilder (mode : Mode) =
/// Computation expression `Delay`. /// Computation expression `Delay`.
member _.Delay (f : unit -> ExpectState<'T>) : unit -> ExpectState<'T> = f member _.Delay (f : unit -> ExpectState<'T>) : unit -> ExpectState<'T> = f
/// 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 () |> CompletedListSnapshotGeneric.make
let lines = lazy File.ReadAllLines state.Caller.FilePath
let listSource =
lazy
AstWalker.findSnapshotList
state.Caller.FilePath
(lines.Force ())
state.Caller.LineNumber
state.Caller.MemberName
let raiseError expected actual =
let diff =
Diff.patienceLines (Array.ofList expected) (Array.ofList actual) |> Diff.format
match mode with
| Mode.Assert ->
if GlobalBuilderConfig.isBulkUpdateMode () then
GlobalBuilderConfig.registerTest (CompletedSnapshot.makeFromAst (listSource.Force ()) state)
else
$"snapshot mismatch! snapshot at %s{state.Caller.FilePath}:%i{state.Caller.LineNumber} (%s{state.Caller.MemberName}) diff:\n%s{diff}"
|> ExpectException
|> raise
| Mode.AssertMockingSource (mockSource, line) ->
$"snapshot mismatch! snapshot at %s{mockSource}:%i{line} (%s{state.Caller.MemberName}) diff:\n%s{diff}"
|> ExpectException
|> raise
| Mode.Update ->
let lines = lines.Force ()
let listSource = listSource.Force ()
let result = SnapshotUpdate.updateAtLocation listSource lines actual
File.writeAllLines result state.Caller.FilePath
failwith ("Snapshot successfully updated. Previous contents:\n" + String.concat "\n" lines)
match CompletedListSnapshotGeneric.passesAssertion state with
| None ->
match mode, GlobalBuilderConfig.isBulkUpdateMode () with
| Mode.Update, _
| _, true ->
failwith
"Snapshot assertion passed, but we are in snapshot-updating mode. Use the `expect` builder instead of `expect'` to assert the contents of a single snapshot; disable `GlobalBuilderConfig.bulkUpdate` to move back to assertion-checking mode."
| _ -> ()
| Some (expected, actual) -> raiseError expected actual
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails. /// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
member _.Run (f : unit -> ExpectState<'T>) : unit = member _.Run (f : unit -> ExpectState<'T>) : unit =
let state = f () |> CompletedSnapshotGeneric.make let state = f () |> CompletedSnapshotGeneric.make
@@ -310,7 +624,7 @@ type ExpectBuilder (mode : Mode) =
|> raise |> raise
| Mode.Assert -> | Mode.Assert ->
if GlobalBuilderConfig.isBulkUpdateMode () then if GlobalBuilderConfig.isBulkUpdateMode () then
GlobalBuilderConfig.registerTest state GlobalBuilderConfig.registerTest (CompletedSnapshot.makeGuess state)
else else
let diff = Diff.patience snapshot actual let diff = Diff.patience snapshot actual

View File

@@ -43,8 +43,7 @@ module GlobalBuilderConfig =
/// </remarks> /// </remarks>
let clearTests () = lock locker allTests.Clear let clearTests () = lock locker allTests.Clear
let internal registerTest (s : CompletedSnapshotGeneric<'T>) : unit = let internal registerTest (toAdd : CompletedSnapshot) : unit =
let toAdd = s |> CompletedSnapshot.make
lock locker (fun () -> allTests.Add toAdd) lock locker (fun () -> allTests.Add toAdd)
/// <summary> /// <summary>

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Expect namespace WoofWare.Expect
open System.Collections
open System.Text.Json open System.Text.Json
open System.Text.Json.Serialization open System.Text.Json.Serialization
@@ -34,6 +35,15 @@ type ExpectState<'T> =
Actual : (unit -> 'T) option Actual : (unit -> 'T) option
} }
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type ExpectStateListy<'T> =
private
{
Formatter : ((unit -> 'T) -> string) option
Snapshot : (string list * CallerInfo) option
Actual : (unit -> 'T seq) option
}
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type. /// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type internal CompletedSnapshotGeneric<'T> = type internal CompletedSnapshotGeneric<'T> =
private private
@@ -126,18 +136,69 @@ module internal CompletedSnapshotGeneric =
else else
None None
/// Represents a snapshot test that has failed and is awaiting update or report to the user. type internal CompletedListSnapshotGeneric<'elt> =
type CompletedSnapshot = private
internal
{ {
CallerInfo : CallerInfo Expected : string list
Replacement : string Format : 'elt -> string
Caller : CallerInfo
Actual : unit -> 'elt seq
} }
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module internal CompletedSnapshot = module internal CompletedListSnapshotGeneric =
let make (s : CompletedSnapshotGeneric<'T>) = let replacement (s : CompletedListSnapshotGeneric<'T>) =
s.Actual () |> unbox<IEnumerable> |> Seq.cast |> Seq.map s.Format |> Seq.toList
/// Returns None if the assertion passes, or Some (expected, actual) if the assertion fails.
let internal passesAssertion (state : CompletedListSnapshotGeneric<'T>) : (string list * string list) option =
let actual =
state.Actual ()
|> unbox<IEnumerable>
|> Seq.cast
|> Seq.map state.Format
|> Seq.toList
if state.Expected <> actual then
Some (state.Expected, actual)
else
None
let make (state : ExpectStateListy<'elt>) : CompletedListSnapshotGeneric<'elt> =
match state.Actual with
| None -> failwith "expected an assertion, but got none"
| Some actual ->
match state.Snapshot with
| None -> failwith "expected a snapshotList, but got none"
| Some (snapshot, caller) ->
let formatter =
match state.Formatter with
| Some f -> fun x -> f (fun () -> x)
| None -> fun x -> x.ToString ()
{ {
CallerInfo = s.Caller Expected = snapshot
Replacement = CompletedSnapshotGeneric.replacement s Format = formatter
Caller = caller
Actual = actual
} }
/// Represents a snapshot test that has failed and is awaiting update or report to the user.
type internal CompletedSnapshot =
| GuessString of CallerInfo * replacement : string
| Known of CallerInfo * replacement : string list * SnapshotLocation
member this.CallerInfo =
match this with
| CompletedSnapshot.GuessString (c, _) -> c
| CompletedSnapshot.Known (c, _, _) -> c
[<RequireQualifiedAccess>]
module internal CompletedSnapshot =
let makeGuess (s : CompletedSnapshotGeneric<'T>) =
CompletedSnapshot.GuessString (s.Caller, CompletedSnapshotGeneric.replacement s)
let makeFromAst (source : SnapshotLocation) (s : CompletedListSnapshotGeneric<'elt>) =
CompletedSnapshot.Known (s.Caller, CompletedListSnapshotGeneric.replacement s, source)

View File

@@ -298,6 +298,27 @@ module internal SnapshotUpdate =
|> Seq.sortByDescending fst |> Seq.sortByDescending fst
|> Seq.fold (fun lines (lineNum, replacement) -> updateSnapshotAtLine lines lineNum replacement) fileLines |> Seq.fold (fun lines (lineNum, replacement) -> updateSnapshotAtLine lines lineNum replacement) fileLines
let internal updateAtLocation (source : SnapshotLocation) (lines : string[]) (actual : string seq) =
let indent = String.replicate source.KeywordRange.StartColumn " "
[|
// Range's lines are one-indexed!
lines.[0 .. source.KeywordRange.EndLine - 2]
[|
lines.[source.KeywordRange.EndLine - 1].Substring (0, source.KeywordRange.EndColumn)
+ " ["
|]
actual |> Seq.map (fun s -> indent + " " + stringLiteral s) |> Array.ofSeq
[|
indent
+ "]"
+ lines.[source.ReplacementRange.EndLine - 1].Substring source.ReplacementRange.EndColumn
|]
lines.[source.ReplacementRange.EndLine ..]
|]
|> Array.concat
/// <summary> /// <summary>
/// Update every failed snapshot in the input, editing the files on disk. /// Update every failed snapshot in the input, editing the files on disk.
/// </summary> /// </summary>
@@ -307,10 +328,19 @@ module internal SnapshotUpdate =
|> Seq.iter (fun (callerFile, callers) -> |> Seq.iter (fun (callerFile, callers) ->
let contents = System.IO.File.ReadAllLines callerFile let contents = System.IO.File.ReadAllLines callerFile
let sources = let newContents =
callers |> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc.Replacement) callers
|> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc)
let newContents = updateAllLines contents sources |> Seq.sortByDescending fst
|> Seq.fold
(fun lines (lineNum, replacement) ->
match replacement with
| CompletedSnapshot.GuessString (_, replacement) ->
updateSnapshotAtLine lines lineNum replacement
| CompletedSnapshot.Known (_, replacement, snapshotLocation) ->
updateAtLocation snapshotLocation lines replacement
)
contents
File.writeAllLines newContents callerFile File.writeAllLines newContents callerFile
) )

View File

@@ -6,8 +6,6 @@ WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.Expe
WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CallerInfo System.IComparable, System.IComparable, System.Collections.IStructuralComparable WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CallerInfo System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.CallerInfo.Equals [method]: (WoofWare.Expect.CallerInfo, System.Collections.IEqualityComparer) -> bool WoofWare.Expect.CallerInfo.Equals [method]: (WoofWare.Expect.CallerInfo, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.CompletedSnapshot inherit obj, implements WoofWare.Expect.CompletedSnapshot System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CompletedSnapshot System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.CompletedSnapshot.Equals [method]: (WoofWare.Expect.CompletedSnapshot, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.Diff'`1 inherit obj, implements 'line WoofWare.Expect.Diff' System.IEquatable, System.Collections.IStructuralEquatable, 'line WoofWare.Expect.Diff' System.IComparable, System.IComparable, System.Collections.IStructuralComparable WoofWare.Expect.Diff'`1 inherit obj, implements 'line WoofWare.Expect.Diff' System.IEquatable, System.Collections.IStructuralEquatable, 'line WoofWare.Expect.Diff' System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.Diff'`1.Equals [method]: ('line WoofWare.Expect.Diff', System.Collections.IEqualityComparer) -> bool WoofWare.Expect.Diff'`1.Equals [method]: ('line WoofWare.Expect.Diff', System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.DiffModule inherit obj WoofWare.Expect.DiffModule inherit obj
@@ -74,18 +72,28 @@ WoofWare.Expect.ExpectBuilder inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int) WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
WoofWare.Expect.ExpectBuilder..ctor [constructor]: WoofWare.Expect.Mode WoofWare.Expect.ExpectBuilder..ctor [constructor]: WoofWare.Expect.Mode
WoofWare.Expect.ExpectBuilder.Bind [method]: ('elt WoofWare.Expect.ExpectStateListy, unit -> #('elt seq) WoofWare.Expect.ExpectState) -> 'elt WoofWare.Expect.ExpectStateListy
WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectStateListy, unit -> 'U WoofWare.Expect.ExpectStateListy) -> 'U WoofWare.Expect.ExpectStateListy
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState) WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectStateListy) -> (unit -> 'T WoofWare.Expect.ExpectStateListy)
WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> '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.Return [method]: unit -> 'T WoofWare.Expect.ExpectStateListy
WoofWare.Expect.ExpectBuilder.ReturnFrom [method]: (unit -> 'T) -> '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.Run [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> unit
WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectStateListy) -> unit
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectStateListy, 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.SnapshotJson [method]: (unit WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: (unit WoofWare.Expect.ExpectStateListy, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.SnapshotList [method]: (unit WoofWare.Expect.ExpectStateListy, string list, string option, int option, string option) -> 'a WoofWare.Expect.ExpectStateListy
WoofWare.Expect.ExpectBuilder.SnapshotThrows [method]: ('a 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.SnapshotThrows [method]: ('a WoofWare.Expect.ExpectStateListy, 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.WithFormat [method]: ('T WoofWare.Expect.ExpectState, 'T -> string) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.WithFormat [method]: ('T WoofWare.Expect.ExpectStateListy, 'T -> string) -> 'T WoofWare.Expect.ExpectStateListy
WoofWare.Expect.ExpectBuilder.WithJsonDocOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonDocumentOptions) -> '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 WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectStateListy, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectException inherit System.Exception, implements System.Collections.IStructuralEquatable WoofWare.Expect.ExpectException inherit System.Exception, implements System.Collections.IStructuralEquatable
WoofWare.Expect.ExpectException..ctor [constructor]: string WoofWare.Expect.ExpectException..ctor [constructor]: string
WoofWare.Expect.ExpectException..ctor [constructor]: unit WoofWare.Expect.ExpectException..ctor [constructor]: unit
@@ -93,6 +101,7 @@ WoofWare.Expect.ExpectException.Equals [method]: (System.Exception, System.Colle
WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
WoofWare.Expect.ExpectException.Message [property]: [read-only] string WoofWare.Expect.ExpectException.Message [property]: [read-only] string
WoofWare.Expect.ExpectState`1 inherit obj WoofWare.Expect.ExpectState`1 inherit obj
WoofWare.Expect.ExpectStateListy`1 inherit obj
WoofWare.Expect.GlobalBuilderConfig inherit obj WoofWare.Expect.GlobalBuilderConfig inherit obj
WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit
WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit

View File

@@ -21,6 +21,7 @@
<Compile Include="File.fs" /> <Compile Include="File.fs" />
<Compile Include="Diff.fs" /> <Compile Include="Diff.fs" />
<Compile Include="Dot.fs" /> <Compile Include="Dot.fs" />
<Compile Include="AstWalker.fs" />
<Compile Include="Domain.fs" /> <Compile Include="Domain.fs" />
<Compile Include="SnapshotUpdate.fs" /> <Compile Include="SnapshotUpdate.fs" />
<Compile Include="Config.fs" /> <Compile Include="Config.fs" />
@@ -39,8 +40,9 @@
<ItemGroup> <ItemGroup>
<!-- FSharp.SystemTextJson requires at least this version --> <!-- FSharp.SystemTextJson requires at least this version -->
<PackageReference Update="FSharp.Core" Version="4.7.0" /> <PackageReference Update="FSharp.Core" Version="8.0.100" />
<PackageReference Include="FSharp.SystemTextJson" Version="1.4.36" /> <PackageReference Include="FSharp.SystemTextJson" Version="1.4.36" />
<PackageReference Include="Fantomas.FCS" Version="7.0.3" />
<!-- Needed for DeepEquals --> <!-- Needed for DeepEquals -->
<PackageReference Include="System.Text.Json" Version="9.0.0" /> <PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup> </ItemGroup>

View File

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

View File

@@ -9,6 +9,11 @@
"version": "7.0.2", "version": "7.0.2",
"hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA=" "hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA="
}, },
{
"pname": "Fantomas.FCS",
"version": "7.0.3",
"hash": "sha256-BmCUq+ZQ3b25nrMBTc5tcxdO2soryEjNx9Fn/FJpi1c="
},
{ {
"pname": "fsharp-analyzers", "pname": "fsharp-analyzers",
"version": "0.31.0", "version": "0.31.0",
@@ -16,8 +21,8 @@
}, },
{ {
"pname": "FSharp.Core", "pname": "FSharp.Core",
"version": "4.7.0", "version": "8.0.100",
"hash": "sha256-7aa4bga9XWLkq7J5KXv8Bilf1KGum77lSUqp+ooYIUg=" "hash": "sha256-FCjhq+W603ibz9XAA9iH5K6gJhX02/pMHyge6dHb4xs="
}, },
{ {
"pname": "FSharp.Core", "pname": "FSharp.Core",
@@ -59,11 +64,26 @@
"version": "1.1.0", "version": "1.1.0",
"hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM=" "hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
}, },
{
"pname": "Microsoft.NETCore.Platforms",
"version": "1.1.1",
"hash": "sha256-8hLiUKvy/YirCWlFwzdejD2Db3DaXhHxT7GSZx/znJg="
},
{ {
"pname": "Microsoft.NETCore.Platforms", "pname": "Microsoft.NETCore.Platforms",
"version": "2.0.0", "version": "2.0.0",
"hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro=" "hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
}, },
{
"pname": "Microsoft.NETCore.Targets",
"version": "1.1.0",
"hash": "sha256-0AqQ2gMS8iNlYkrD+BxtIg7cXMnr9xZHtKAuN4bjfaQ="
},
{
"pname": "Microsoft.NETCore.Targets",
"version": "1.1.3",
"hash": "sha256-WLsf1NuUfRWyr7C7Rl9jiua9jximnVvzy6nk2D2bVRc="
},
{ {
"pname": "Microsoft.Testing.Extensions.Telemetry", "pname": "Microsoft.Testing.Extensions.Telemetry",
"version": "1.5.3", "version": "1.5.3",
@@ -159,11 +179,31 @@
"version": "5.0.0", "version": "5.0.0",
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0=" "hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
}, },
{
"pname": "runtime.any.System.Runtime",
"version": "4.3.0",
"hash": "sha256-qwhNXBaJ1DtDkuRacgHwnZmOZ1u9q7N8j0cWOLYOELM="
},
{
"pname": "runtime.native.System",
"version": "4.3.0",
"hash": "sha256-ZBZaodnjvLXATWpXXakFgcy6P+gjhshFXmglrL5xD5Y="
},
{
"pname": "runtime.unix.System.Private.Uri",
"version": "4.3.0",
"hash": "sha256-c5tXWhE/fYbJVl9rXs0uHh3pTsg44YD1dJvyOA0WoMs="
},
{ {
"pname": "System.Buffers", "pname": "System.Buffers",
"version": "4.5.1", "version": "4.5.1",
"hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI=" "hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI="
}, },
{
"pname": "System.Buffers",
"version": "4.6.0",
"hash": "sha256-c2QlgFB16IlfBms5YLsTCFQ/QeKoS6ph1a9mdRkq/Jc="
},
{ {
"pname": "System.Collections.Immutable", "pname": "System.Collections.Immutable",
"version": "8.0.0", "version": "8.0.0",
@@ -174,6 +214,11 @@
"version": "5.0.0", "version": "5.0.0",
"hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4=" "hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4="
}, },
{
"pname": "System.Diagnostics.DiagnosticSource",
"version": "8.0.1",
"hash": "sha256-zmwHjcJgKcbkkwepH038QhcnsWMJcHys+PEbFGC0Jgo="
},
{ {
"pname": "System.Formats.Asn1", "pname": "System.Formats.Asn1",
"version": "6.0.0", "version": "6.0.0",
@@ -209,16 +254,31 @@
"version": "4.5.5", "version": "4.5.5",
"hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI=" "hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
}, },
{
"pname": "System.Memory",
"version": "4.6.0",
"hash": "sha256-OhAEKzUM6eEaH99DcGaMz2pFLG/q/N4KVWqqiBYUOFo="
},
{ {
"pname": "System.Numerics.Vectors", "pname": "System.Numerics.Vectors",
"version": "4.4.0", "version": "4.6.0",
"hash": "sha256-auXQK2flL/JpnB/rEcAcUm4vYMCYMEMiWOCAlIaqu2U=" "hash": "sha256-fKS3uWQ2HmR69vNhDHqPLYNOt3qpjiWQOXZDHvRE1HU="
},
{
"pname": "System.Private.Uri",
"version": "4.3.0",
"hash": "sha256-fVfgcoP4AVN1E5wHZbKBIOPYZ/xBeSIdsNF+bdukIRM="
}, },
{ {
"pname": "System.Reflection.Metadata", "pname": "System.Reflection.Metadata",
"version": "8.0.0", "version": "8.0.0",
"hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE=" "hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
}, },
{
"pname": "System.Runtime",
"version": "4.3.1",
"hash": "sha256-R9T68AzS1PJJ7v6ARz9vo88pKL1dWqLOANg4pkQjkA0="
},
{ {
"pname": "System.Runtime.CompilerServices.Unsafe", "pname": "System.Runtime.CompilerServices.Unsafe",
"version": "4.5.3", "version": "4.5.3",
@@ -229,6 +289,11 @@
"version": "6.0.0", "version": "6.0.0",
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I=" "hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
}, },
{
"pname": "System.Runtime.CompilerServices.Unsafe",
"version": "6.1.0",
"hash": "sha256-NyqqpRcHumzSxpsgRDguD5SGwdUNHBbo0OOdzLTIzCU="
},
{ {
"pname": "System.Security.AccessControl", "pname": "System.Security.AccessControl",
"version": "4.5.0", "version": "4.5.0",