Add ability to update snapshots (#4)

This commit is contained in:
Patrick Stevens
2025-06-16 11:53:26 +01:00
committed by GitHub
parent 39370d5235
commit 9d20d18954
18 changed files with 727 additions and 34 deletions

View File

@@ -124,6 +124,27 @@ jobs:
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
run: if [[ $(find packed -maxdepth 1 -name 'WoofWare.Expect.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
analyzers:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Prepare analyzers
run: nix develop --command dotnet restore analyzers/analyzers.fsproj
- name: Build project
run: nix develop --command dotnet build ./WoofWare.Expect/WoofWare.Expect.fsproj
- name: Run analyzers
run: nix run .#fsharp-analyzers -- --project ./WoofWare.Expect/WoofWare.Expect.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/*/ --verbosity detailed --report ./analysis.sarif --treat-as-error GRA-STRING-001 GRA-STRING-002 GRA-STRING-003 GRA-UNIONCASE-001 GRA-INTERPOLATED-001 GRA-TYPE-ANNOTATE-001 GRA-VIRTUALCALL-001 GRA-IMMUTABLECOLLECTIONEQUALITY-001 GRA-JSONOPTS-001 GRA-LOGARGFUNCFULLAPP-001 GRA-DISPBEFOREASYNC-001 --exclude-analyzers PartialAppAnalyzer
github-release-dry-run:
runs-on: ubuntu-latest
needs: [nuget-pack]
@@ -167,7 +188,7 @@ jobs:
all-required-checks-complete:
if: ${{ always() }}
needs: [check-dotnet-format, check-nix-format, build, build-nix, flake-check, expected-pack, linkcheck]
needs: [check-dotnet-format, check-nix-format, build, build-nix, flake-check, expected-pack, linkcheck, analyzers]
runs-on: ubuntu-latest
steps:
- uses: G-Research/common-actions/check-required-lite@2b7dc49cb14f3344fbe6019c14a31165e258c059

View File

@@ -5,8 +5,9 @@ An [expect-testing](https://blog.janestreet.com/the-joy-of-expect-tests/) librar
# Current status
Basic mechanism works, but I haven't yet decided how the ergonomic updating of the input text will work.
Ideally it would edit the input AST, but I don't yet know if that's viable.
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 not very well tested, and I expect it to be kind of brittle.
# How to use
@@ -35,6 +36,10 @@ let ``This test fails: plain text comparison of ToString`` () =
}
```
# Limitations
* The snapshot updating mechanism *requires* you to use verbatim string literals. While the test assertions will work correctly if you do `snapshot ("foo" + "bar" + f 3)`, for example, the updating code is liable to do something undefined in that case. Also do not use format strings (`$"blah"`).
# Licence
MIT.

View File

@@ -0,0 +1,22 @@
namespace WoofWare.Expect.Test
open System
open System.IO
open System.Reflection
[<RequireQualifiedAccess>]
module Assembly =
let getEmbeddedResource (assembly : Assembly) (name : string) : string =
let names = assembly.GetManifestResourceNames ()
let names =
names |> Seq.filter (fun s -> s.EndsWith (name, StringComparison.Ordinal))
use s =
names
|> Seq.exactlyOne
|> assembly.GetManifestResourceStream
|> fun s -> new StreamReader (s)
s.ReadToEnd ()

View File

@@ -1,6 +1,5 @@
namespace WoofWare.Expect.Test
namespace WoofWare.Expect.Test
open System
open WoofWare.Expect
open NUnit.Framework
@@ -9,7 +8,7 @@ module SimpleTest =
[<Test>]
let ``JSON is resilient to whitespace changes`` () =
expect {
snapshotJson "123 "
snapshotJson " 123 "
return 123
}
@@ -17,7 +16,7 @@ module SimpleTest =
let ``Example of a failing test`` () =
expect {
snapshot
"snapshot mismatch! snapshot at filepath.fs:99 (Example of a failing test) was:
@"snapshot mismatch! snapshot at filepath.fs:99 (Example of a failing test) was:
- 123
@@ -39,6 +38,6 @@ actual was:
[<Test>]
let ``Basic example`` () =
expect {
snapshot "123"
snapshot @"123"
return 123
}

View File

@@ -0,0 +1,10 @@
namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @"test ""quotes"" here"
return 123
}

View File

@@ -0,0 +1,14 @@
namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot
"test
with
newlines"
return 123
}

View File

@@ -0,0 +1,13 @@
namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot (* comment *)
"""test
"""
return 123
}

View File

@@ -0,0 +1,10 @@
namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot """test"""
return 123
}

View File

@@ -0,0 +1,213 @@
namespace WoofWare.Expect.Test
open WoofWare.Expect
open NUnit.Framework
[<TestFixture>]
module TestSnapshotFinding =
type Dummy = class end
[<Test>]
let ``Triple-quote, one line, one-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "TripleQuoteOneLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement""
return 123
}
"
return SnapshotUpdate.updateSnapshotAtLine source 8 "replacement" |> String.concat "\n"
}
[<Test>]
let ``Triple-quote, one line, multi-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "TripleQuoteOneLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement
more""
return 123
}
"
return
SnapshotUpdate.updateSnapshotAtLine source 8 "replacement\nmore"
|> String.concat "\n"
}
[<Test>]
let ``At-string, one line, one-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "AtStringOneLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement""
return 123
}
"
return SnapshotUpdate.updateSnapshotAtLine source 8 "replacement" |> String.concat "\n"
}
[<Test>]
let ``At-string, one line, multi-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "AtStringOneLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot @""replacement
more""
return 123
}
"
return
SnapshotUpdate.updateSnapshotAtLine source 8 "replacement\nmore"
|> String.concat "\n"
}
[<Test>]
let ``Triple-quote, intervening comment, one-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "TripleQuoteInterveningComment.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot (* comment *)
@""replacement""
return 123
}
"
return SnapshotUpdate.updateSnapshotAtLine source 8 "replacement" |> String.concat "\n"
}
[<Test>]
let ``Triple-quote, intervening comment, multi-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "TripleQuoteInterveningComment.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot (* comment *)
@""replacement
more""
return 123
}
"
return
SnapshotUpdate.updateSnapshotAtLine source 8 "replacement\nmore"
|> String.concat "\n"
}
[<Test>]
let ``Single-quote, many lines, one-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "SingleQuoteManyLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot
@""replacement""
return 123
}
"
return SnapshotUpdate.updateSnapshotAtLine source 8 "replacement" |> String.concat "\n"
}
[<Test>]
let ``Single-quote, many lines, multi-line replacement`` () =
let source =
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "SingleQuoteManyLine.fs"
|> _.Split('\n')
expect {
snapshot
@"namespace BigExample
open WoofWare.Expect
module MyModule =
let foo () =
expect {
snapshot
@""replacement
more""
return 123
}
"
return
SnapshotUpdate.updateSnapshotAtLine source 8 "replacement\nmore"
|> String.concat "\n"
}

View File

@@ -7,12 +7,19 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="SimpleTest.fs" />
<Compile Include="TestSurface.fs" />
<Compile Include="Assembly.fs" />
<Compile Include="SimpleTest.fs" />
<Compile Include="TestSnapshotFinding.fs" />
<Compile Include="TestSurface.fs" />
<EmbeddedResource Include="SyntaxCases\AtStringOneLine.fs" />
<EmbeddedResource Include="SyntaxCases\SingleQuoteManyLine.fs" />
<EmbeddedResource Include="SyntaxCases\TripleQuoteInterveningComment.fs" />
<EmbeddedResource Include="SyntaxCases\TripleQuoteOneLine.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.1.20" />
<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"/>

View File

@@ -0,0 +1,6 @@
module internal WoofWare.Expect.AssemblyInfo
open System.Runtime.CompilerServices
[<assembly : InternalsVisibleTo("WoofWare.Expect.Test")>]
do ()

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Expect
open System.IO
open System.Runtime.CompilerServices
open System.Text.Json
open System.Text.Json.Serialization
@@ -35,12 +36,33 @@ module private Text =
let predent (c : char) (s : string) =
s.Split '\n' |> Seq.map (sprintf "%c %s" c) |> String.concat "\n"
/// <summary>Specify how the Expect computation expression treats failures.</summary>
/// <remarks>You probably don't want to use this directly; use the computation expression definitions
/// like <c>expect</c> in the <c>Builder</c> module instead.</remarks>
type Mode =
private
| Assert
| Update
| AssertMockingSource of (string * int)
/// <summary>
/// The builder which powers WoofWare.Expect.
/// </summary>
/// <remarks>You're not expected to construct this explicitly; it's a computation expression, available as <c>Builder.expect</c>.</remarks>
/// <param name="applyChanges">When running the tests, instead of throwing an exception on failure, update the snapshot.</param>
/// <param name="sourceOverride">Override the file path and line numbers reported in snapshots, so that your tests can be fully stable even on failure. (You almost certainly don't want to set this.)</param>
type ExpectBuilder (?sourceOverride : string * int) =
type ExpectBuilder (mode : Mode) =
member private this.Mode = Unchecked.defaultof<Mode>
new (sourceOverride : string * int) = ExpectBuilder (Mode.AssertMockingSource sourceOverride)
new (update : bool)
=
if update then
ExpectBuilder Mode.Update
else
ExpectBuilder Mode.Assert
/// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
member _.Bind
(state : ExpectState<YouHaveSuppliedMultipleSnapshots>, f : unit -> ExpectState<'U>)
@@ -155,6 +177,35 @@ type ExpectBuilder (?sourceOverride : string * int) =
match state.Snapshot, state.Actual with
| Some (snapshot, source), Some actual ->
let raiseError (snapshot : string) (actual : string) : unit =
match mode with
| Mode.AssertMockingSource (mockSource, line) ->
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
mockSource
line
source.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
|> ExpectException
|> raise
| Mode.Assert ->
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
source.FilePath
source.LineNumber
source.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
|> ExpectException
|> raise
| Mode.Update ->
let lines = File.ReadAllLines source.FilePath
let oldContents = String.concat "\n" lines
let lines = SnapshotUpdate.updateSnapshotAtLine lines source.LineNumber actual
File.WriteAllLines (source.FilePath, lines)
failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents)
match snapshot with
| SnapshotValue.Json snapshot ->
let canonicalSnapshot = JsonDocument.Parse snapshot
@@ -163,29 +214,25 @@ type ExpectBuilder (?sourceOverride : string * int) =
JsonSerializer.Serialize (actual, options) |> JsonDocument.Parse
if not (JsonElement.DeepEquals (canonicalActual.RootElement, canonicalSnapshot.RootElement)) then
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
(sourceOverride |> Option.map fst |> Option.defaultValue source.FilePath)
(sourceOverride |> Option.map snd |> Option.defaultValue source.LineNumber)
source.MemberName
(canonicalSnapshot.RootElement.ToString () |> Text.predent '-')
(canonicalActual.RootElement.ToString () |> Text.predent '-')
|> ExpectException
|> raise
raiseError (canonicalSnapshot.RootElement.ToString ()) (canonicalActual.RootElement.ToString ())
else
match mode 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 snapshot."
| _ -> ()
| SnapshotValue.BareString snapshot ->
let actual = actual.ToString ()
if actual <> snapshot then
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
(sourceOverride |> Option.map fst |> Option.defaultValue source.FilePath)
(sourceOverride |> Option.map snd |> Option.defaultValue source.LineNumber)
source.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
|> ExpectException
|> raise
raiseError snapshot actual
else
match mode 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 snapshot."
| _ -> ()
| None, _ -> failwith "Must specify snapshot"
| _, None -> failwith "Must specify actual value with 'return'"
@@ -207,7 +254,32 @@ module Builder =
///
/// (That example expectation will fail, because the actual value 124 does not snapshot to the expected snapshot "123".)
/// </remarks>
let expect = ExpectBuilder ()
let expect = ExpectBuilder false
/// <summary>The WoofWare.Expect builder, but in "replace snapshot on failure" mode.</summary>
///
/// <remarks>
/// Take an existing failing snapshot test:
///
/// <code>
/// expect {
/// snapshot "123"
/// return 124
/// }
/// </code>
///
/// Add the <c>'</c> marker to the <c>expect</c> builder:
/// <code>
/// expect' {
/// snapshot "123"
/// return 124
/// }
/// </code>
///
/// Rerun, and observe that the snapshot becomes updated.
/// This rerun will throw an exception, to help make sure you don't commit the snapshot builder while it's flipped to "update" mode.
/// </remarks>
let expect' = ExpectBuilder true
/// <summary>
/// This is the `expect` builder, but it mocks out the filepath reported on failure.

View File

@@ -0,0 +1,272 @@
namespace WoofWare.Expect
open System
open System.Text.RegularExpressions
type private StringLiteralInfo =
{
StartLine : int
StartColumn : int
EndLine : int
EndColumn : int
Content : string
}
override this.ToString () =
sprintf "%i:%i to %i:%i: %s" this.StartLine this.StartColumn this.EndLine this.EndColumn this.Content
type private Position =
{
Line : int
Column : int
TotalOffset : int
}
[<RequireQualifiedAccess>]
module internal SnapshotUpdate =
[<Literal>]
let tripleQuote = "\"\"\""
/// Convert a string position to line/column
let private positionToLineColumn (text : string) (offset : int) : Position =
let rec loop (line : int) (col : int) (totalOffset : int) (i : int) : Position =
if i >= text.Length || totalOffset = offset then
{
Line = line
Column = col
TotalOffset = totalOffset
}
elif text.[i] = '\n' then
loop (line + 1) 0 (totalOffset + 1) (i + 1)
else
loop line (col + 1) (totalOffset + 1) (i + 1)
loop 0 0 0 0
/// Skip whitespace and comments, returning the position after them
let rec private skipWhitespaceAndComments (text : string) (startPos : int) : int option =
let rec skipComment (depth : int) (pos : int) : int option =
if pos >= text.Length - 1 then
None
elif pos + 1 < text.Length && text.[pos] = '(' && text.[pos + 1] = '*' then
skipComment (depth + 1) (pos + 2)
elif pos + 1 < text.Length && text.[pos] = '*' && text.[pos + 1] = ')' then
if depth = 1 then
Some (pos + 2)
else
skipComment (depth - 1) (pos + 2)
else
skipComment depth (pos + 1)
let rec loop pos =
if pos >= text.Length then
None
elif pos + 1 < text.Length && text.[pos] = '(' && text.[pos + 1] = '*' then
skipComment 1 (pos + 2) |> Option.bind loop
elif Char.IsWhiteSpace (text.[pos]) then
loop (pos + 1)
else
Some pos
loop startPos
/// Parse a regular string literal
let private parseRegularString (text : string) (startPos : int) : (string * int) option =
let rec loop pos content escaped =
if pos >= text.Length then
None
elif escaped then
let unescaped =
match text.[pos] with
| 'n' -> "\n"
| 'r' -> "\r"
| 't' -> "\t"
| '\\' -> "\\"
| '"' -> "\""
| c -> string<char> c
loop (pos + 1) (content + unescaped) false
elif text.[pos] = '\\' then
loop (pos + 1) content true
elif text.[pos] = '"' then
Some (content, pos + 1)
else
loop (pos + 1) (content + string<char> text.[pos]) false
loop (startPos + 1) "" false
/// Parse a verbatim string literal (@"...")
let private parseVerbatimString (text : string) (startPos : int) : (string * int) option =
let rec loop pos content =
if pos >= text.Length then
None
elif pos + 1 < text.Length && text.[pos] = '"' && text.[pos + 1] = '"' then
// Escaped quote in verbatim string
loop (pos + 2) (content + "\"")
elif text.[pos] = '"' then
// End of string
Some (content, pos + 1)
else
loop (pos + 1) (content + string<char> text.[pos])
// Skip the @" prefix
loop (startPos + 2) ""
/// Parse a triple-quoted string literal
let private parseTripleQuotedString (text : string) (startPos : int) : (string * int) option =
// startPos points to the first "
if
startPos + 2 >= text.Length
|| text.[startPos] <> '"'
|| text.[startPos + 1] <> '"'
|| text.[startPos + 2] <> '"'
then
None
else
let contentStart = startPos + 3
let closePos = text.IndexOf (tripleQuote, contentStart, StringComparison.Ordinal)
if closePos = -1 then
None
else
let content = text.Substring (contentStart, closePos - contentStart)
Some (content, closePos + 3)
/// Find the string literal after a snapshot keyword
let private findSnapshotString (lines : string[]) (snapshotLine : int) : StringLiteralInfo option =
let startIdx = snapshotLine - 1
if startIdx >= lines.Length then
None
else
// We need to include enough lines to capture multi-line strings
// Take a reasonable number of lines after the snapshot line
let maxLines = min 50 (lines.Length - startIdx)
let relevantLines = lines |> Array.skip startIdx |> Array.take maxLines
let searchText = String.concat "\n" relevantLines
// Find snapshot keyword
let snapshotMatch = Regex.Match (searchText, @"\b(snapshot|snapshotJson)\b")
if not snapshotMatch.Success then
None
else
// Work with positions relative to searchText throughout
let snapshotEnd = snapshotMatch.Index + snapshotMatch.Length
// Skip whitespace and comments after "snapshot"
skipWhitespaceAndComments searchText snapshotEnd
|> Option.bind (fun stringStart ->
if stringStart >= searchText.Length then
None
else
// Check what type of string literal we have
let parseResult =
if
stringStart + 2 < searchText.Length
&& searchText.[stringStart] = '"'
&& searchText.[stringStart + 1] = '"'
&& searchText.[stringStart + 2] = '"'
then
// Triple-quoted string
parseTripleQuotedString searchText stringStart
|> Option.map (fun (content, endPos) -> (content, stringStart, endPos))
elif
stringStart + 1 < searchText.Length
&& searchText.[stringStart] = '@'
&& searchText.[stringStart + 1] = '"'
then
// Verbatim string
parseVerbatimString searchText stringStart
|> Option.map (fun (content, endPos) -> (content, stringStart, endPos))
elif searchText.[stringStart] = '"' then
// Regular string
parseRegularString searchText stringStart
|> Option.map (fun (content, endPos) -> (content, stringStart, endPos))
else
None
parseResult
|> Option.map (fun (content, stringStartPos, stringEndPos) ->
let startPos = positionToLineColumn searchText stringStartPos
let endPos = positionToLineColumn searchText stringEndPos
{
StartLine = startIdx + startPos.Line + 1
StartColumn = startPos.Column
EndLine = startIdx + endPos.Line + 1
EndColumn = endPos.Column
Content = content
}
)
)
/// 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 ("\"", "\"\"") + "\""
if info.StartLine = info.EndLine then
// Single line update
lines
|> Array.mapi (fun i line ->
if i = info.StartLine - 1 then
let before = line.Substring (0, info.StartColumn)
let after = line.Substring info.EndColumn
before + newString + after
else
line
)
else
// Multi-line update
let startLineIdx = info.StartLine - 1
let endLineIdx = info.EndLine - 1
let before = lines.[startLineIdx].Substring (0, info.StartColumn)
let after = lines.[endLineIdx].Substring info.EndColumn
let newLines =
if newContent.IndexOf '\n' >= 0 then
let split = newContent.Replace("\"", "\"\"").Split ('\n')
match split with
| [||] -> failwith "expected contents from split string"
| [| single |] -> [| before + "@\"" + single + "\"" + after |]
| [| first ; last |] -> [| before + "@\"" + first ; last + "\"" + after |]
| split ->
[|
yield before + "@\"" + split.[0]
yield! split.[1 .. split.Length - 2]
yield split.[split.Length - 1] + "\"" + after
|]
else
// Convert to single-line verbatim string
[| before + "@\"" + newContent.Replace ("\"", "\"\"") + "\"" + after |]
[|
yield! lines |> Array.take startLineIdx
yield! newLines
yield! lines |> Array.skip (endLineIdx + 1)
|]
/// <remarks>Example usage:
/// <c>updateSnapshotAtLine [|lines-of-file|] 42 "new test output"</c>
///
/// This will find a snapshot call on line 42 like:
/// snapshot "old value" -> snapshot @"new test output"
/// snapshot @"old value" -> snapshot @"new test output"
/// snapshot """old value""" -> snapshot @"new test output"
/// snapshot """multi
/// line""" -> snapshot """multi
/// line"""
/// snapshot "has \"\"\" in it" -> snapshot @"has """""" in it"
/// </remarks>
let updateSnapshotAtLine (fileLines : string[]) (snapshotLine : int) (newValue : string) : string[] =
match findSnapshotString fileLines snapshotLine with
| Some info ->
Console.Error.WriteLine ("String literal to update: " + string<StringLiteralInfo> info)
updateSnapshot fileLines info newValue
| None -> failwithf "Could not find string literal after snapshot at line %d" snapshotLine

View File

@@ -1,9 +1,13 @@
WoofWare.Expect.Builder inherit obj
WoofWare.Expect.Builder.expect [static property]: [read-only] WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.expect' [static property]: [read-only] WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.expectWithMockedFilePath [static method]: (string, int) -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.ExpectBuilder inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int) option
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]: (WoofWare.Expect.YouHaveSuppliedMultipleSnapshots WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> 'T WoofWare.Expect.ExpectState
@@ -19,5 +23,7 @@ WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
WoofWare.Expect.ExpectException.Message [property]: [read-only] string
WoofWare.Expect.ExpectState`1 inherit obj, implements 'T WoofWare.Expect.ExpectState System.IEquatable, System.Collections.IStructuralEquatable, 'T WoofWare.Expect.ExpectState System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.ExpectState`1.Equals [method]: ('T WoofWare.Expect.ExpectState, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.Mode inherit obj, implements WoofWare.Expect.Mode System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.Mode System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.Mode.Equals [method]: (WoofWare.Expect.Mode, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.YouHaveSuppliedMultipleSnapshots inherit obj, implements WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.YouHaveSuppliedMultipleSnapshots.Equals [method]: (WoofWare.Expect.YouHaveSuppliedMultipleSnapshots, System.Collections.IEqualityComparer) -> bool

View File

@@ -16,7 +16,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs"/>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="SnapshotUpdate.fs" />
<Compile Include="Builder.fs" />
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>

View File

@@ -1,5 +1,5 @@
{
"version": "0.1",
"version": "0.2",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
@@ -9,4 +9,4 @@
":/Directory.Build.props",
":/LICENSE"
]
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.Build.NoTargets/1.0.80"> <!-- This is not a project we want to build. -->
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
<RestorePackagesPath>../.analyzerpackages/</RestorePackagesPath>
<TargetFramework>net6.0</TargetFramework>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages> <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose.-->
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.15.0]" />
</ItemGroup>
</Project>

View File

@@ -29,6 +29,11 @@
"version": "1.4.36",
"hash": "sha256-zZEhjP0mdc5E3fBPS4/lqD7sxoaoT5SOspP546RWYdc="
},
{
"pname": "FsUnit",
"version": "7.0.1",
"hash": "sha256-K85CIdxMeFSHEKZk6heIXp/oFjWAn7dBILKrw49pJUY="
},
{
"pname": "Microsoft.ApplicationInsights",
"version": "2.22.0",