8 Commits

Author SHA1 Message Date
Patrick Stevens
6df614ab57 Fix bulk update mode with sequential snapshots (#33)
* Fix bulk update mode with sequential snapshots

Previously, when a test method contained multiple expect blocks in bulk
update mode, if an earlier snapshot passed, the test would immediately
fail with "snapshot matched, but we are in updating mode" and prevent
later snapshots from being processed for update.

This change implements deferred validation:
- Passing snapshots in bulk mode are registered instead of failing immediately
- Tests continue execution to process all snapshots
- Validation is deferred until bulk update completion
- Only fails if ALL snapshots passed (no updates needed)

Fixes the issue where tests with mixed passing/failing snapshots
couldn't be bulk updated.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 18:28:45 +01:00
Patrick Stevens
1cc253cb69 Add CLAUDE.md (#32) 2025-10-15 07:01:07 +00:00
Patrick Stevens
fbce97878f Switch to trusted publishing (#31) 2025-10-03 09:54:37 +00:00
Patrick Stevens
6f613587b5 Bump ApiSurface (#30) 2025-09-08 20:39:47 +00:00
Patrick Stevens
6d9dbc59db Better error message on failure to find keyword (#29) 2025-09-02 07:56:58 +01:00
Patrick Stevens
ebc24f85aa Add snapshotList syntax for sequences (#25) 2025-07-30 08:11:36 +00:00
Patrick Stevens
faacb4770c Simple strings where possible (#27) 2025-07-30 08:06:15 +00:00
Patrick Stevens
d21786ecd4 Make diff generic (#26) 2025-07-30 09:03:02 +01:00
21 changed files with 1082 additions and 184 deletions

View File

@@ -13,6 +13,12 @@
"commands": [
"fsharp-analyzers"
]
},
"woofware.nunittestrunner": {
"version": "0.3.4",
"commands": [
"woofware.nunittestrunner"
]
}
}
}

View File

@@ -249,12 +249,17 @@ jobs:
- name: Identify `dotnet`
id: dotnet-identify
run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT'
- name: Obtain NuGet key
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet
id: publish-success
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
with:
package-name: WoofWare.Expect
nuget-key: ${{ secrets.NUGET_API_KEY }}
nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
nupkg-dir: packed/
dotnet: ${{ steps.dotnet-identify.outputs.dotnet }}

90
CLAUDE.md Normal file
View File

@@ -0,0 +1,90 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
WoofWare.Expect is an F# expect/snapshot testing library (similar to Jest snapshots). The project consists of two main components:
- **WoofWare.Expect**: Core library that provides the `expect` computation expression and snapshot comparison functionality
- **WoofWare.Expect.Test**: Test suite using NUnit that demonstrates and validates the library functionality
## Build and Development Commands
This project uses Nix for development environment management. All commands should be run within the Nix development shell:
```bash
# Restore dependencies
nix develop --command dotnet restore
# Build the project
nix develop --command dotnet build --configuration Release
# Run tests
nix develop --command dotnet test
# Pack NuGet package
nix develop --command dotnet pack --configuration Release
```
### Code Formatting and Analysis
```bash
# Format F# code (via Nix)
nix run .#fantomas -- .
# Check formatting without modifying files
nix run .#fantomas -- --check .
# Run F# analyzers
nix run .#fsharp-analyzers -- --project ./WoofWare.Expect/WoofWare.Expect.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/*/
```
### Single Test Execution
NUnit's filtering is pretty borked.
You can't apply filters that contain special characters in the test name (like a space character).
You have to do e.g. `FullyQualifiedName~singleword` rather than `FullyQualifiedName~single word test`, but this only works on tests whose names are single words to begin with.
Instead of running `dotnet test`, you can perform a build (`dotnet build`) and then run `dotnet woofware.nunittestrunner WoofWare.Expect.Test/bin/Debug/net9.0/WoofWare.Expect.Test.dll`.
This is an NUnit test runner which accepts a `--filter` arg that takes the same filter syntax as `dotnet test`, but actually parses it correctly: test names can contain spaces.
(The most foolproof way to provide test names to WoofWare.NUnitTestRunner is by XML-encoding: e.g. `FullyQualifiedName="MyNamespace.MyTestsClass&lt;ParameterType1%2CParameterType2&gt;.MyTestMethod"`. The `~` query operator is also supported.)
## Architecture
### Core Components
- **Builder.fs**: Contains the `ExpectBuilder` computation expression that powers the `expect` syntax
- **Domain.fs**: Core types including `CallerInfo`, `ExpectState`, and `SnapshotValue`
- **SnapshotUpdate.fs**: Handles updating snapshot files when tests fail
- **AstWalker.fs**: Uses Fantomas.FCS to parse F# source and locate/update snapshot strings
- **Diff.fs**: Provides Patience and Myers diff algorithms for readable test output
- **Dot.fs**: ASCII art rendering of dot files (requires `graph-easy`)
### Key Design Patterns
- **Computation Expression**: The library is built around F#'s computation expression syntax with `expect { }` blocks
- **Source Code Manipulation**: Uses F# compiler services to locate and update snapshot strings in source files
- **Dual Mode Operation**: Tests can run in assertion mode (normal) or update mode (to fix failing snapshots)
### Snapshot Types
- `snapshot`: Plain text comparison using `ToString()`
- `snapshotJson`: JSON serialization with configurable options
- `snapshotList`: Formatted list comparison
- `snapshotThrows`: Exception expectation testing
- `withFormat`: Custom formatting functions
- `withJsonSerializerOptions`: Custom JSON serialization
- `withJsonDocOptions`: Custom JSON parsing (e.g., for comments)
### Test Snapshot Management
- Individual updates: Add `'` to `expect` (making it `expect'`), run test to update, then remove `'`
- Bulk updates: Use `GlobalBuilderConfig.enterBulkUpdateMode()` in test setup and `GlobalBuilderConfig.updateAllSnapshots()` in teardown
## Important Notes
- Use verbatim string literals for snapshots - the update mechanism requires this
- Avoid format strings (`$"..."`) or string concatenation in snapshot definitions
- The project follows strict F# conventions with warnings as errors
- All commits go through comprehensive CI including formatting, analysis, and testing

View File

@@ -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<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:
```fsharp
@@ -64,6 +72,15 @@ let ``Overriding the formatting`` () =
snapshot @"Int32"
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:
@@ -194,4 +211,4 @@ Observe the `OneTimeSetUp` which sets global state to enter "bulk update" mode,
# Licence
MIT.
WoofWare.Expect is licensed to you under the MIT licence, a copy of which can be found at [LICENSE](./LICENSE).

View File

@@ -34,7 +34,7 @@ module MyModule =
expect {
snapshot (* first comment *) (* second comment *)
(* third comment on new line *)
@""updated after many comments""
""updated after many comments""
return 123
}
@@ -100,7 +100,7 @@ module MyModule =
let nestedComments () =
expect {
snapshot (* outer (* inner *) comment *) @""updated after nested comments""
snapshot (* outer (* inner *) comment *) ""updated after nested comments""
return ""nested""
}
@@ -165,7 +165,7 @@ module MyModule =
let commentWithSpecialChars () =
expect {
snapshot (* comment with ""quotes"" and \ backslash *) @""updated after weird comment""
snapshot (* comment with ""quotes"" and \ backslash *) ""updated after weird comment""
return ""special""
}
@@ -233,7 +233,7 @@ module MyModule =
snapshot
@""updated after spaces""
""updated after spaces""
return ""whitespace""
}
@@ -301,7 +301,7 @@ module MyModule =
expect {
snapshotJson (* comment 1 *)
(* comment 2 *)
(* comment 3 *) @""updated after comments""
(* comment 3 *) ""updated after comments""
return 123
}

View File

@@ -31,7 +31,7 @@ open WoofWare.Expect
module MyModule =
let emptyString () =
expect {
snapshot @""now has content""
snapshot ""now has content""
return """"
}
@@ -121,7 +121,7 @@ module MyModule =
let emptyVerbatim () =
expect {
snapshot @""now has content""
snapshot ""now has content""
return """"
}
@@ -211,7 +211,7 @@ module MyModule =
let emptyTripleQuote () =
expect {
snapshot @""now has content""
snapshot ""now has content""
return """"
}
@@ -301,7 +301,7 @@ module MyModule =
let onlyWhitespace () =
expect {
snapshot @""now has content""
snapshot ""now has content""
return ""whitespace""
}
@@ -575,7 +575,7 @@ module MyModule =
let veryLongLine () =
expect {
snapshot
@""this line is short though""
""this line is short though""
return ""long line""
}

View File

@@ -472,7 +472,7 @@ Sixth line""
let windowsLineEndings () =
expect {
snapshot @""down with line endings""
snapshot ""down with line endings""
return ""crlf""
}
"

View File

@@ -32,7 +32,7 @@ open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement""
snapshot ""replacement""
return 123
}
"
@@ -81,7 +81,7 @@ open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement""
snapshot ""replacement""
return 123
}
"

View File

@@ -31,7 +31,7 @@ open WoofWare.Expect
module MyModule =
let emoji () =
expect {
snapshot @""Updated with 🚀🌟✨ more emoji!""
snapshot ""Updated with 🚀🌟✨ more emoji!""
return 123
}
@@ -172,7 +172,7 @@ module MyModule =
let arabicRTL () =
expect {
snapshot @""Updated Arabic: مرحبا بالعالم""
snapshot ""Updated Arabic: مرحبا بالعالم""
return ""rtl test""
}
@@ -241,7 +241,7 @@ module MyModule =
let combiningCharacters () =
expect {
// Combining diacritics: e + ́ = é
snapshot @""updated test with combining: and ä!""
snapshot ""updated test with combining: and ä!""
return ""combining""
}
@@ -309,7 +309,7 @@ module MyModule =
let mixedScripts () =
expect {
snapshotJson @""Updated mixed: English, русский, 日本語, العربية, emoji 🚀""
snapshotJson ""Updated mixed: English, русский, 日本語, العربية, emoji 🚀""
return [ ""multilingual"" ]
}
@@ -377,7 +377,7 @@ module MyModule =
let zeroWidthChars () =
expect {
snapshot @""Updated: Zerowidthspacetest"" // Contains U+200B
snapshot ""Updated: Zerowidthspacetest"" // Contains U+200B
return ""zwsp""
}
@@ -445,7 +445,7 @@ module MyModule =
let mathSymbols () =
expect {
snapshot @""Pretty vacuous, huh: ∀x∈, ∃y: + = 1 |x| 1""
snapshot ""Pretty vacuous, huh: ∀x∈, ∃y: + = 1 |x| 1""
return ""math""
}
"

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

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@@ -16,6 +16,7 @@
<Compile Include="TestDiff.fs" />
<Compile Include="TestDot.fs" />
<Compile Include="TestExceptionThrowing.fs" />
<Compile Include="TestSnapshotList.fs" />
<Compile Include="TestSurface.fs" />
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
<Compile Include="TestSnapshotFinding\TestUnicodeCharacters.fs" />
@@ -36,14 +37,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.1.20" />
<PackageReference Include="ApiSurface" Version="5.0.1" />
<PackageReference Include="FsUnit" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
<!-- TODO: when ApiSurface accepts https://github.com/G-Research/ApiSurface/pull/116, upgrade these -->
<PackageReference Include="System.IO.Abstractions" Version="4.2.13" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="4.2.13" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,219 @@
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
let matches =
results
|> List.filter (fun loc ->
loc.KeywordRange.StartLine <= lineNumber
&& lineNumber <= loc.KeywordRange.EndLine
)
match matches with
| [] ->
failwith
$"Unexpectedly failed to locate snapshot keyword %s{methodName} at line %i{lineNumber} of file %s{infoFilePath}. Please report this along with the contents of the file."
| m :: _ -> m

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Expect
open System.Collections.Generic
open System.IO
open System.Runtime.CompilerServices
open System.Text.Json
@@ -32,6 +33,61 @@ 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 ()
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".
member _.Bind<'U> (state : ExpectState<'U>, f : unit -> ExpectState<'U>) : ExpectState<'U> =
let actual = f ()
@@ -71,6 +127,40 @@ type ExpectBuilder (mode : Mode) =
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>
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
member _.Snapshot<'a>
@@ -102,6 +192,46 @@ type ExpectBuilder (mode : Mode) =
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>
/// Express that the actual value, when converted to JSON, should result in a JSON document
/// which matches the JSON document that is this string.
@@ -142,6 +272,45 @@ type ExpectBuilder (mode : Mode) =
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>
/// Expresses that the given expression throws during evaluation.
/// </summary>
@@ -187,10 +356,54 @@ 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 : 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>
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
/// comparing to the snapshot.
/// this value.
/// </summary>
/// <remarks>
/// 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)
}
/// <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>
/// 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).
@@ -227,6 +457,42 @@ type ExpectBuilder (mode : Mode) =
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>
/// Express that these JsonDocumentOptions should be used when parsing the snapshot string into a JSON object.
/// </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".
member _.Return (() : unit) : ExpectState<'T> =
member _.Return (() : unit) : ExpectStateListy<'T> =
{
Formatter = None
JsonSerialiserOptions = None
JsonDocOptions = None
Snapshot = None
Actual = None
}
/// 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
Formatter = None
JsonDocOptions = None
JsonSerialiserOptions = None
Actual = Some (fun () -> value)
JsonSerialiserOptions = None
JsonDocOptions = None
}
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value", but delayed behind
@@ -291,6 +555,59 @@ type ExpectBuilder (mode : Mode) =
/// Computation expression `Delay`.
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, _ ->
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."
| _, true ->
// In bulk update mode, register passing tests instead of failing immediately
// This allows tests with multiple snapshots to continue processing
GlobalBuilderConfig.registerPassingTest state.Caller
| _ -> ()
| Some (expected, actual) -> raiseError expected actual
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
member _.Run (f : unit -> ExpectState<'T>) : unit =
let state = f () |> CompletedSnapshotGeneric.make
@@ -310,7 +627,7 @@ type ExpectBuilder (mode : Mode) =
|> raise
| Mode.Assert ->
if GlobalBuilderConfig.isBulkUpdateMode () then
GlobalBuilderConfig.registerTest state
GlobalBuilderConfig.registerTest (CompletedSnapshot.makeGuess state)
else
let diff = Diff.patience snapshot actual
@@ -332,10 +649,13 @@ type ExpectBuilder (mode : Mode) =
match CompletedSnapshotGeneric.passesAssertion state with
| None ->
match mode, GlobalBuilderConfig.isBulkUpdateMode () with
| Mode.Update, _
| _, true ->
| Mode.Update, _ ->
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."
"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."
| _, true ->
// In bulk update mode, register passing tests instead of failing immediately
// This allows tests with multiple snapshots to continue processing
GlobalBuilderConfig.registerPassingTest state.Caller
| _ -> ()
| Some (expected, actual) -> raiseError expected actual

View File

@@ -12,6 +12,8 @@ module GlobalBuilderConfig =
let private allTests : ResizeArray<CompletedSnapshot> = ResizeArray ()
let private passingTests : ResizeArray<CallerInfo> = ResizeArray ()
let internal isBulkUpdateMode () : bool =
lock locker (fun () -> bulkUpdate.Value > 0)
@@ -34,19 +36,27 @@ module GlobalBuilderConfig =
)
/// <summary>
/// Clear the set of failing tests registered by any previous bulk-update runs.
/// Clear the set of failing and passing tests registered by any previous bulk-update runs.
/// </summary>
///
/// <remarks>
/// You probably don't need to do this, because your test runner is probably tearing down
/// anyway after the tests have failed; this is mainly here for WoofWare.Expect's own internal testing.
/// </remarks>
let clearTests () = lock locker allTests.Clear
let clearTests () =
lock
locker
(fun () ->
allTests.Clear ()
passingTests.Clear ()
)
let internal registerTest (s : CompletedSnapshotGeneric<'T>) : unit =
let toAdd = s |> CompletedSnapshot.make
let internal registerTest (toAdd : CompletedSnapshot) : unit =
lock locker (fun () -> allTests.Add toAdd)
let internal registerPassingTest (caller : CallerInfo) : unit =
lock locker (fun () -> passingTests.Add caller)
/// <summary>
/// For all tests whose failures have already been registered,
/// transform the files on disk so that the failing snapshots now pass.
@@ -61,8 +71,14 @@ module GlobalBuilderConfig =
locker
(fun () ->
let allTests = Seq.toArray allTests
let passingTestsArray = Seq.toArray passingTests
try
// Check if we only had passing tests in bulk update mode - this should be an error
if allTests.Length = 0 && passingTestsArray.Length > 0 then
failwith
"All snapshot assertions passed, but bulk-update mode was enabled. Disable bulk-update mode by not calling `GlobalBuilderConfig.enterBulkUpdateMode` to return to normal assertion-checking mode."
SnapshotUpdate.updateAll allTests
finally
// double acquiring of reentrant lock is OK, we're not switching threads

View File

@@ -11,45 +11,50 @@ type Position = int<pos>
/// A Patience diff is composed of a sequence of transformations to get from one string to another.
/// This represents those transformations.
type DiffOperation =
type DiffOperation<'line> =
/// This line is the same on both sides of the diff.
/// On the left, it appears at position posA. On the right, at position posB.
| Match of posA : Position * posB : Position * line : string
| Match of posA : Position * posB : Position * line : 'line
/// Delete this line, which is at this position.
| Delete of posA : Position * line : string
| Delete of posA : Position * line : 'line
/// Insert this line at the given position.
| Insert of posB : Position * line : string
| Insert of posB : Position * line : 'line
/// The diff between two line-oriented streams. Normally the generic parameter will be `string`, indicating
/// that the thing being diffed was text.
type Diff'<'line> = private | Diff of DiffOperation<'line> list
/// The diff between two line-oriented pieces of text.
type Diff = private | Diff of DiffOperation list
type Diff = Diff'<string>
/// A match between positions in two sequences
type internal LineMatch =
type internal LineMatch<'line> =
{
PosA : Position
PosB : Position
Line : string
Line : 'line
}
/// Result of finding unique lines in a sequence
type internal UniqueLines =
type internal UniqueLines<'line when 'line : comparison> =
{
/// Map from line content to its position (only for unique lines)
LinePositions : Map<string, Position>
LinePositions : Map<'line, Position>
/// All line counts (for verification)
LineCounts : Map<string, int>
LineCounts : Map<'line, int>
}
/// The diff between two line-oriented pieces of text.
[<RequireQualifiedAccess>]
module Diff =
/// Find lines that appear exactly once in a sequence
let private findUniqueLines (lines : string array) : UniqueLines =
let positions = Dictionary<string, Position> ()
let counts = Dictionary<string, int> ()
let private findUniqueLines (lines : 'line array) : UniqueLines<'line> =
let positions = Dictionary<'line, Position> ()
let counts = Dictionary<'line, int> ()
lines
|> Array.iteri (fun i line ->
if counts.ContainsKey (line) then
if counts.ContainsKey line then
counts.[line] <- counts.[line] + 1
else
counts.[line] <- 1
@@ -70,7 +75,7 @@ module Diff =
}
/// Find longest increasing subsequence based on B positions
let private longestIncreasingSubsequence (matches : LineMatch array) : LineMatch list =
let private longestIncreasingSubsequence (matches : LineMatch<'line> array) : LineMatch<'line> list =
let n = matches.Length
if n = 0 then
@@ -103,9 +108,8 @@ module Diff =
reconstruct endIndex []
/// Simple Myers diff implementation. You probably want to use `patience` instead, for more human-readable diffs.
let myers (a : string array) (b : string array) : Diff =
let rec diffHelper (i : int) (j : int) (acc : DiffOperation list) =
let private myers' (a : 'line array) (b : 'line array) : DiffOperation<'line> list =
let rec diffHelper (i : int) (j : int) (acc : DiffOperation<'line> list) =
match i < a.Length, j < b.Length with
| false, false -> List.rev acc
| true, false ->
@@ -146,11 +150,14 @@ module Diff =
// No close match, just delete and insert
diffHelper (i + 1) j (Delete (i * 1<pos>, a.[i]) :: acc)
diffHelper 0 0 [] |> Diff
diffHelper 0 0 []
/// Simple Myers diff implementation. You probably want to use `patience` instead, for more human-readable diffs.
let myers (a : string array) (b : string array) : Diff = myers' a b |> Diff
/// Patience diff: a human-readable line-based diff.
/// Operates on lines of string; the function `patience` will split on lines for you.
let rec patienceLines (a : string array) (b : string array) : Diff =
let rec patienceLines (a : 'line array) (b : 'line array) : Diff'<'line> =
// Handle empty sequences
match a.Length, b.Length with
| 0, 0 -> [] |> Diff
@@ -177,7 +184,7 @@ module Diff =
if Set.isEmpty commonUniques then
// No unique common lines, fall back to Myers
myers a b
myers' a b |> Diff
else
// Build matches for unique common lines
let matches =
@@ -196,7 +203,7 @@ module Diff =
let anchorMatches = longestIncreasingSubsequence matches |> List.toArray
// Build diff imperatively
let result = ResizeArray<DiffOperation> ()
let result = ResizeArray<DiffOperation<_>> ()
let mutable prevA = 0<pos>
let mutable prevB = 0<pos>
@@ -244,26 +251,32 @@ module Diff =
patienceLines (a.Split '\n') (b.Split '\n')
/// Format the diff as a human-readable string, including line numbers at the left.
let formatWithLineNumbers (Diff ops) : string =
let formatWithLineNumbers' (formatter : 'line -> string) (Diff ops) : string =
ops
|> List.map (fun op ->
match op with
| Match (a, b, line) -> sprintf " %3d %3d %s" a b line
| Delete (a, line) -> sprintf "- %3d %s" a line
| Insert (b, line) -> sprintf "+ %3d %s" b line
| Match (a, b, line) -> sprintf " %3d %3d %s" a b (formatter line)
| Delete (a, line) -> sprintf "- %3d %s" a (formatter line)
| Insert (b, line) -> sprintf "+ %3d %s" b (formatter line)
)
|> 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.
let format' (formatter : 'line -> string) (Diff ops) : string =
ops
|> List.map (fun op ->
match op with
| Match (_, _, line) -> " " + (formatter line)
| Delete (_, line) -> "- " + (formatter line)
| Insert (_, line) -> "+ " + (formatter line)
)
|> String.concat "\n"
/// Format the diff as a human-readable string.
let format (Diff ops) : string =
ops
|> List.map (fun op ->
match op with
| Match (_, _, line) -> sprintf " %s" line
| Delete (_, line) -> sprintf "- %s" line
| Insert (_, line) -> sprintf "+ %s" line
)
|> String.concat "\n"
let format (ops : Diff) : string = format' id ops
/// Compute diff statistics
type internal DiffStats =
@@ -274,7 +287,7 @@ module Diff =
TotalOperations : int
}
let internal computeStats (ops : DiffOperation list) : DiffStats =
let internal computeStats (ops : DiffOperation<'a> list) : DiffStats =
let counts =
ops
|> List.fold

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Expect
open System.Collections
open System.Text.Json
open System.Text.Json.Serialization
@@ -34,6 +35,15 @@ type ExpectState<'T> =
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.
type internal CompletedSnapshotGeneric<'T> =
private
@@ -126,18 +136,69 @@ module internal CompletedSnapshotGeneric =
else
None
/// Represents a snapshot test that has failed and is awaiting update or report to the user.
type CompletedSnapshot =
internal
type internal CompletedListSnapshotGeneric<'elt> =
private
{
CallerInfo : CallerInfo
Replacement : string
Expected : string list
Format : 'elt -> string
Caller : CallerInfo
Actual : unit -> 'elt seq
}
[<RequireQualifiedAccess>]
module internal CompletedSnapshot =
let make (s : CompletedSnapshotGeneric<'T>) =
module internal CompletedListSnapshotGeneric =
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
Replacement = CompletedSnapshotGeneric.replacement s
Expected = snapshot
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

@@ -203,11 +203,22 @@ module internal SnapshotUpdate =
)
)
let internal stringLiteral (content : string) =
if
(content.IndexOf '\n' < 0)
&& (content.IndexOf '\\' < 0)
&& (content.IndexOf '"' < 0)
then
// simple case where there's no escaping
"\"" + content + "\""
else
"@\"" + content.Replace ("\"", "\"\"") + "\""
/// Update the snapshot string with a new value; this doesn't edit the file on disk, but
/// instead returns the new contents.
/// We always write single-quoted @-strings for simplicity.
let private updateSnapshot (lines : string[]) (info : StringLiteralInfo) (newContent : string) : string[] =
let newString = "@\"" + newContent.Replace ("\"", "\"\"") + "\""
let newString = stringLiteral newContent
if info.StartLine = info.EndLine then
// Single line update
@@ -230,7 +241,7 @@ module internal SnapshotUpdate =
let newLines =
if newContent.IndexOf '\n' >= 0 then
let split = newContent.Replace("\"", "\"\"").Split ('\n')
let split = newContent.Replace("\"", "\"\"").Split '\n'
match split with
| [||] -> failwith "expected contents from split string"
@@ -287,6 +298,27 @@ module internal SnapshotUpdate =
|> Seq.sortByDescending fst
|> 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>
/// Update every failed snapshot in the input, editing the files on disk.
/// </summary>
@@ -296,10 +328,19 @@ module internal SnapshotUpdate =
|> Seq.iter (fun (callerFile, callers) ->
let contents = System.IO.File.ReadAllLines callerFile
let sources =
callers |> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc.Replacement)
let newContents = updateAllLines contents sources
let newContents =
callers
|> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc)
|> 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
)

View File

@@ -6,50 +6,50 @@ WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.Expe
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.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 inherit obj, implements WoofWare.Expect.Diff System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.Diff System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.Diff.Equals [method]: (WoofWare.Expect.Diff, 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.Equals [method]: ('line WoofWare.Expect.Diff', System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.DiffModule inherit obj
WoofWare.Expect.DiffModule.format [static method]: WoofWare.Expect.Diff -> string
WoofWare.Expect.DiffModule.formatWithLineNumbers [static method]: WoofWare.Expect.Diff -> string
WoofWare.Expect.DiffModule.myers [static method]: string [] -> string [] -> WoofWare.Expect.Diff
WoofWare.Expect.DiffModule.patience [static method]: string -> string -> WoofWare.Expect.Diff
WoofWare.Expect.DiffModule.patienceLines [static method]: string [] -> string [] -> WoofWare.Expect.Diff
WoofWare.Expect.DiffOperation inherit obj, implements WoofWare.Expect.DiffOperation System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.DiffOperation System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 3 cases
WoofWare.Expect.DiffOperation+Delete inherit WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation+Delete.get_line [method]: unit -> string
WoofWare.Expect.DiffOperation+Delete.get_posA [method]: unit -> int
WoofWare.Expect.DiffOperation+Delete.line [property]: [read-only] string
WoofWare.Expect.DiffOperation+Delete.posA [property]: [read-only] int
WoofWare.Expect.DiffOperation+Insert inherit WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation+Insert.get_line [method]: unit -> string
WoofWare.Expect.DiffOperation+Insert.get_posB [method]: unit -> int
WoofWare.Expect.DiffOperation+Insert.line [property]: [read-only] string
WoofWare.Expect.DiffOperation+Insert.posB [property]: [read-only] int
WoofWare.Expect.DiffOperation+Match inherit WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation+Match.get_line [method]: unit -> string
WoofWare.Expect.DiffOperation+Match.get_posA [method]: unit -> int
WoofWare.Expect.DiffOperation+Match.get_posB [method]: unit -> int
WoofWare.Expect.DiffOperation+Match.line [property]: [read-only] string
WoofWare.Expect.DiffOperation+Match.posA [property]: [read-only] int
WoofWare.Expect.DiffOperation+Match.posB [property]: [read-only] int
WoofWare.Expect.DiffOperation+Tags inherit obj
WoofWare.Expect.DiffOperation+Tags.Delete [static field]: int = 1
WoofWare.Expect.DiffOperation+Tags.Insert [static field]: int = 2
WoofWare.Expect.DiffOperation+Tags.Match [static field]: int = 0
WoofWare.Expect.DiffOperation.Equals [method]: (WoofWare.Expect.DiffOperation, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.DiffOperation.get_IsDelete [method]: unit -> bool
WoofWare.Expect.DiffOperation.get_IsInsert [method]: unit -> bool
WoofWare.Expect.DiffOperation.get_IsMatch [method]: unit -> bool
WoofWare.Expect.DiffOperation.get_Tag [method]: unit -> int
WoofWare.Expect.DiffOperation.IsDelete [property]: [read-only] bool
WoofWare.Expect.DiffOperation.IsInsert [property]: [read-only] bool
WoofWare.Expect.DiffOperation.IsMatch [property]: [read-only] bool
WoofWare.Expect.DiffOperation.NewDelete [static method]: (int, string) -> WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation.NewInsert [static method]: (int, string) -> WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation.NewMatch [static method]: (int, int, string) -> WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation.Tag [property]: [read-only] int
WoofWare.Expect.DiffModule.format [static method]: string WoofWare.Expect.Diff' -> string
WoofWare.Expect.DiffModule.format' [static method]: ('line -> string) -> 'line WoofWare.Expect.Diff' -> string
WoofWare.Expect.DiffModule.formatWithLineNumbers [static method]: string WoofWare.Expect.Diff' -> string
WoofWare.Expect.DiffModule.formatWithLineNumbers' [static method]: ('line -> string) -> 'line WoofWare.Expect.Diff' -> string
WoofWare.Expect.DiffModule.myers [static method]: string [] -> string [] -> string WoofWare.Expect.Diff'
WoofWare.Expect.DiffModule.patience [static method]: string -> string -> string WoofWare.Expect.Diff'
WoofWare.Expect.DiffModule.patienceLines [static method]: 'line [] -> 'line [] -> 'line WoofWare.Expect.Diff'
WoofWare.Expect.DiffOperation`1 inherit obj, implements 'line WoofWare.Expect.DiffOperation System.IEquatable, System.Collections.IStructuralEquatable, 'line WoofWare.Expect.DiffOperation System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 3 cases
WoofWare.Expect.DiffOperation`1+Delete inherit 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1+Delete.get_line [method]: unit -> 'line
WoofWare.Expect.DiffOperation`1+Delete.get_posA [method]: unit -> int
WoofWare.Expect.DiffOperation`1+Delete.line [property]: [read-only] 'line
WoofWare.Expect.DiffOperation`1+Delete.posA [property]: [read-only] int
WoofWare.Expect.DiffOperation`1+Insert inherit 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1+Insert.get_line [method]: unit -> 'line
WoofWare.Expect.DiffOperation`1+Insert.get_posB [method]: unit -> int
WoofWare.Expect.DiffOperation`1+Insert.line [property]: [read-only] 'line
WoofWare.Expect.DiffOperation`1+Insert.posB [property]: [read-only] int
WoofWare.Expect.DiffOperation`1+Match inherit 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1+Match.get_line [method]: unit -> 'line
WoofWare.Expect.DiffOperation`1+Match.get_posA [method]: unit -> int
WoofWare.Expect.DiffOperation`1+Match.get_posB [method]: unit -> int
WoofWare.Expect.DiffOperation`1+Match.line [property]: [read-only] 'line
WoofWare.Expect.DiffOperation`1+Match.posA [property]: [read-only] int
WoofWare.Expect.DiffOperation`1+Match.posB [property]: [read-only] int
WoofWare.Expect.DiffOperation`1+Tags inherit obj
WoofWare.Expect.DiffOperation`1+Tags.Delete [static field]: int = 1
WoofWare.Expect.DiffOperation`1+Tags.Insert [static field]: int = 2
WoofWare.Expect.DiffOperation`1+Tags.Match [static field]: int = 0
WoofWare.Expect.DiffOperation`1.Equals [method]: ('line WoofWare.Expect.DiffOperation, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.DiffOperation`1.get_IsDelete [method]: unit -> bool
WoofWare.Expect.DiffOperation`1.get_IsInsert [method]: unit -> bool
WoofWare.Expect.DiffOperation`1.get_IsMatch [method]: unit -> bool
WoofWare.Expect.DiffOperation`1.get_Tag [method]: unit -> int
WoofWare.Expect.DiffOperation`1.IsDelete [property]: [read-only] bool
WoofWare.Expect.DiffOperation`1.IsInsert [property]: [read-only] bool
WoofWare.Expect.DiffOperation`1.IsMatch [property]: [read-only] bool
WoofWare.Expect.DiffOperation`1.NewDelete [static method]: (int, 'line) -> 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1.NewInsert [static method]: (int, 'line) -> 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1.NewMatch [static method]: (int, int, 'line) -> 'line WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation`1.Tag [property]: [read-only] int
WoofWare.Expect.Dot inherit obj
WoofWare.Expect.Dot+IFileSystem - interface with 3 member(s)
WoofWare.Expect.Dot+IFileSystem.DeleteFile [method]: string -> unit
@@ -72,18 +72,28 @@ WoofWare.Expect.ExpectBuilder inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
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.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.ExpectStateListy) -> (unit -> 'T WoofWare.Expect.ExpectStateListy)
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.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.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.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.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.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.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..ctor [constructor]: string
WoofWare.Expect.ExpectException..ctor [constructor]: unit
@@ -91,6 +101,7 @@ WoofWare.Expect.ExpectException.Equals [method]: (System.Exception, System.Colle
WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
WoofWare.Expect.ExpectException.Message [property]: [read-only] string
WoofWare.Expect.ExpectState`1 inherit obj
WoofWare.Expect.ExpectStateListy`1 inherit obj
WoofWare.Expect.GlobalBuilderConfig inherit obj
WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit
WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit

View File

@@ -21,6 +21,7 @@
<Compile Include="File.fs" />
<Compile Include="Diff.fs" />
<Compile Include="Dot.fs" />
<Compile Include="AstWalker.fs" />
<Compile Include="Domain.fs" />
<Compile Include="SnapshotUpdate.fs" />
<Compile Include="Config.fs" />
@@ -39,8 +40,9 @@
<ItemGroup>
<!-- 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="Fantomas.FCS" Version="7.0.3" />
<!-- Needed for DeepEquals -->
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>

View File

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

View File

@@ -1,14 +1,19 @@
[
{
"pname": "ApiSurface",
"version": "4.1.20",
"hash": "sha256-koWgO9FC9ax+Ij56ug8kxeyknl0yhLqnNLOUdxtqqo4="
"version": "5.0.1",
"hash": "sha256-0GMXEMFgWbbE2OGxW+6h4zGgQHg+IZy1aI13Dn97xSU="
},
{
"pname": "fantomas",
"version": "7.0.2",
"hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA="
},
{
"pname": "Fantomas.FCS",
"version": "7.0.3",
"hash": "sha256-BmCUq+ZQ3b25nrMBTc5tcxdO2soryEjNx9Fn/FJpi1c="
},
{
"pname": "fsharp-analyzers",
"version": "0.31.0",
@@ -16,8 +21,8 @@
},
{
"pname": "FSharp.Core",
"version": "4.7.0",
"hash": "sha256-7aa4bga9XWLkq7J5KXv8Bilf1KGum77lSUqp+ooYIUg="
"version": "8.0.100",
"hash": "sha256-FCjhq+W603ibz9XAA9iH5K6gJhX02/pMHyge6dHb4xs="
},
{
"pname": "FSharp.Core",
@@ -61,8 +66,18 @@
},
{
"pname": "Microsoft.NETCore.Platforms",
"version": "2.0.0",
"hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
"version": "1.1.1",
"hash": "sha256-8hLiUKvy/YirCWlFwzdejD2Db3DaXhHxT7GSZx/znJg="
},
{
"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",
@@ -121,33 +136,33 @@
},
{
"pname": "NuGet.Common",
"version": "6.13.2",
"hash": "sha256-ASLa/Jigg5Eop0ZrXPl98RW2rxnJRC7pbbxhuV74hFw="
"version": "6.14.0",
"hash": "sha256-jDOwt3veI1GSG8CfBnf2+dJxD3E/Nmlc+vHtD4J76Ms="
},
{
"pname": "NuGet.Configuration",
"version": "6.13.2",
"hash": "sha256-z8VW1YdRDanyyRTDYRvRkSv/XPR3c/hMM1y8cNNjx0Y="
"version": "6.14.0",
"hash": "sha256-1PN9s6fhCw3wd2260U6hQ4vG3jIvcG8GIn1oQgxMXA0="
},
{
"pname": "NuGet.Frameworks",
"version": "6.13.2",
"hash": "sha256-caDyc+WgYOo43AUTjtbP0MyvYDb6JweEKDdIul61Cac="
"version": "6.14.0",
"hash": "sha256-3ViM3R1ucQMEL2hQYsivT86kI6veMQK2xDsiAcFcVQk="
},
{
"pname": "NuGet.Packaging",
"version": "6.13.2",
"hash": "sha256-lhO+SFwIYZ4aPHxIGm5ubkkE2a5Ve2xgtroRbNh7hpw="
"version": "6.14.0",
"hash": "sha256-Yafbnxs3maj55bJ1oKQiZ0QkntFUzXdhorL94YEUOhY="
},
{
"pname": "NuGet.Protocol",
"version": "6.13.2",
"hash": "sha256-5lnAHHZjy7A4vgv65AeBAs64mSNpuoUjxW3HnrMpuzY="
"version": "6.14.0",
"hash": "sha256-uLDKfs+QN1MdnuQtTES8qfNzzsmYKM6XB9pwJc4G+eo="
},
{
"pname": "NuGet.Versioning",
"version": "6.13.2",
"hash": "sha256-gmpyBpKnt+GHqgx/2uFKp+J2csbxEAy1E7WdVT117sw="
"version": "6.14.0",
"hash": "sha256-DqdOJgsphKxSvqB8b60zNPCaiLfbiu3WnUJ/90feLrY="
},
{
"pname": "NUnit",
@@ -159,11 +174,31 @@
"version": "5.0.0",
"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",
"version": "4.5.1",
"hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI="
},
{
"pname": "System.Buffers",
"version": "4.6.0",
"hash": "sha256-c2QlgFB16IlfBms5YLsTCFQ/QeKoS6ph1a9mdRkq/Jc="
},
{
"pname": "System.Collections.Immutable",
"version": "8.0.0",
@@ -174,30 +209,25 @@
"version": "5.0.0",
"hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4="
},
{
"pname": "System.Diagnostics.DiagnosticSource",
"version": "8.0.1",
"hash": "sha256-zmwHjcJgKcbkkwepH038QhcnsWMJcHys+PEbFGC0Jgo="
},
{
"pname": "System.Formats.Asn1",
"version": "6.0.0",
"hash": "sha256-KaMHgIRBF7Nf3VwOo+gJS1DcD+41cJDPWFh+TDQ8ee8="
},
{
"pname": "System.Formats.Asn1",
"version": "8.0.1",
"hash": "sha256-may/Wg+esmm1N14kQTG4ESMBi+GQKPp0ZrrBo/o6OXM="
},
{
"pname": "System.IO.Abstractions",
"version": "4.2.13",
"hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg="
"version": "22.0.15",
"hash": "sha256-2deBvDALOzd+BAnhdbnR7ZPjChE71HPv7w61/2tfYOg="
},
{
"pname": "System.IO.Abstractions.TestingHelpers",
"version": "4.2.13",
"hash": "sha256-WGGatXlgyROnptdw0zU3ggf54eD/zusO/fvtf+5wuPU="
},
{
"pname": "System.IO.FileSystem.AccessControl",
"version": "4.5.0",
"hash": "sha256-ck44YBQ0M+2Im5dw0VjBgFD1s0XuY54cujrodjjSBL8="
"version": "22.0.15",
"hash": "sha256-JRm8yApCvhB/cvkPcm3+SKURhVB+ykF1u3IrxSJ7CLQ="
},
{
"pname": "System.IO.Pipelines",
@@ -209,16 +239,31 @@
"version": "4.5.5",
"hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
},
{
"pname": "System.Memory",
"version": "4.6.0",
"hash": "sha256-OhAEKzUM6eEaH99DcGaMz2pFLG/q/N4KVWqqiBYUOFo="
},
{
"pname": "System.Numerics.Vectors",
"version": "4.4.0",
"hash": "sha256-auXQK2flL/JpnB/rEcAcUm4vYMCYMEMiWOCAlIaqu2U="
"version": "4.6.0",
"hash": "sha256-fKS3uWQ2HmR69vNhDHqPLYNOt3qpjiWQOXZDHvRE1HU="
},
{
"pname": "System.Private.Uri",
"version": "4.3.0",
"hash": "sha256-fVfgcoP4AVN1E5wHZbKBIOPYZ/xBeSIdsNF+bdukIRM="
},
{
"pname": "System.Reflection.Metadata",
"version": "8.0.0",
"hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
},
{
"pname": "System.Runtime",
"version": "4.3.1",
"hash": "sha256-R9T68AzS1PJJ7v6ARz9vo88pKL1dWqLOANg4pkQjkA0="
},
{
"pname": "System.Runtime.CompilerServices.Unsafe",
"version": "4.5.3",
@@ -230,9 +275,9 @@
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
},
{
"pname": "System.Security.AccessControl",
"version": "4.5.0",
"hash": "sha256-AFsKPb/nTk2/mqH/PYpaoI8PLsiKKimaXf+7Mb5VfPM="
"pname": "System.Runtime.CompilerServices.Unsafe",
"version": "6.1.0",
"hash": "sha256-NyqqpRcHumzSxpsgRDguD5SGwdUNHBbo0OOdzLTIzCU="
},
{
"pname": "System.Security.Cryptography.Pkcs",
@@ -244,11 +289,6 @@
"version": "4.4.0",
"hash": "sha256-Ri53QmFX8I8UH0x4PikQ1ZA07ZSnBUXStd5rBfGWFOE="
},
{
"pname": "System.Security.Principal.Windows",
"version": "4.5.0",
"hash": "sha256-BkUYNguz0e4NJp1kkW7aJBn3dyH9STwB5N8XqnlCsmY="
},
{
"pname": "System.Text.Encodings.Web",
"version": "9.0.0",
@@ -273,5 +313,30 @@
"pname": "System.Threading.Tasks.Extensions",
"version": "4.5.4",
"hash": "sha256-owSpY8wHlsUXn5xrfYAiu847L6fAKethlvYx97Ri1ng="
},
{
"pname": "TestableIO.System.IO.Abstractions",
"version": "22.0.15",
"hash": "sha256-6YwnBfAnsxM0lEPB2LOFQcs7d1r7CyqjDEmvUBTz+X0="
},
{
"pname": "TestableIO.System.IO.Abstractions.TestingHelpers",
"version": "22.0.15",
"hash": "sha256-xEmfPBCtVVLc7K494cUuPXIYbUc/GPQlFC7UkDXP2jM="
},
{
"pname": "TestableIO.System.IO.Abstractions.Wrappers",
"version": "22.0.15",
"hash": "sha256-KoGuXGzecpf4rTmEth4/2goVFFR9V2aj+iibfZxpR7U="
},
{
"pname": "Testably.Abstractions.FileSystem.Interface",
"version": "9.0.0",
"hash": "sha256-6JW+qDtqQT9StP4oTR7uO0NnmVc2xcjSZ6ds2H71wtg="
},
{
"pname": "WoofWare.NUnitTestRunner",
"version": "0.3.4",
"hash": "sha256-OaBYMEAXUDiz9ei2/Zg4Q1A8BNDK1oaMB44uVz4UG/0="
}
]