3 Commits

Author SHA1 Message Date
Patrick Stevens
ad05a9c106 Fix typo resulting in failed renders (#24) 2025-07-25 11:35:14 +00:00
Patrick Stevens
ed352c1b14 Add Dot rendering (#23) 2025-07-25 11:07:40 +00:00
Patrick Stevens
e0153ab182 Patience diff (#17) 2025-07-24 08:14:53 +00:00
14 changed files with 707 additions and 27 deletions

View File

@@ -187,6 +187,11 @@ Observe the `OneTimeSetUp` which sets global state to enter "bulk update" mode,
* 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"`).
# Output formats
* The `Diff` module provides a Patience diff and a Myers diff implementation, which you can use to make certain tests much more readable.
* The `Dot` module provides `render`, which renders a dot file as ASCII art. You will need `graph-easy` to use this feature.
# Licence
MIT.

View File

@@ -19,12 +19,9 @@ 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) diff:
- 123
actual was:
+ 124"
return
@@ -64,26 +61,21 @@ actual was:
// Out of the box, comments in snapshots cause the JSON parser to throw, so the snapshot fails to match...
expect {
snapshot
@"snapshot mismatch! snapshot at file.fs:99 (Custom JSON output) was:
@"snapshot mismatch! snapshot at file.fs:99 (Custom JSON output) diff:
- [JSON failed to parse:] {
- // a key here
- ""a"":3
- }
actual was:
- // a key here
+ {
+ ""a"": 3
+ }"
""a"": 3
}"
return
Assert.Throws<ExpectException> (fun () ->
expectWithMockedFilePath ("file.fs", 99) {
snapshotJson
@"{
// a key here
""a"":3
// a key here
""a"": 3
}"
return Map.ofList [ "a", 3 ]

View File

@@ -0,0 +1,110 @@
namespace WoofWare.Expect.Test
open WoofWare.Expect
open NUnit.Framework
[<TestFixture>]
module TestDiff =
[<Test>]
let ``Basic diff`` () =
let textA =
[|
"The quick brown fox"
"jumps over"
"the lazy dog"
"Some unique line here"
"The end"
|]
let textB =
[|
"The quick brown fox"
"Some unique line here"
"jumps over"
"the lazy dog"
"Another line"
"The end"
|]
let diff = Diff.patienceLines textA textB
expect {
snapshot
@" 0 0 The quick brown fox
+ 1 Some unique line here
1 2 jumps over
2 3 the lazy dog
- 3 Some unique line here
+ 4 Another line
4 5 The end"
withFormat Diff.formatWithLineNumbers
return diff
}
expect {
snapshot
@" The quick brown fox
+ Some unique line here
jumps over
the lazy dog
- Some unique line here
+ Another line
The end"
withFormat Diff.format
return diff
}
[<Test>]
let ``An example from Incremental`` () =
let textA =
"""digraph G {
rankdir = TB
bgcolor = transparent
n4 [shape=Mrecord label="{{n4|BindMain|height=2}}" "fontname"="Sans Serif"]
n3 [shape=Mrecord label="{{n3|BindLhsChange|height=1}}" "fontname"="Sans Serif"]
n1 [shape=Mrecord label="{{n1|Const|height=0}}" "fontname"="Sans Serif"]
n2 [shape=Mrecord label="{{n2|Const|height=0}}" "fontname"="Sans Serif"]
n3 -> n4
n2 -> n4
n1 -> n3
}"""
let textB =
"""digraph G {
rankdir = TB
bgcolor = transparent
n4 [shape=box label="{{n4|BindMain|height=2}}" ]
n3 [shape=box label="{{n3|BindLhsChange|height=1}}" ]
n1 [shape=box label="{{n1|Const|height=0}}" ]
n2 [shape=box label="{{n2|Const|height=0}}" ]
n3 -> n4
n2 -> n4
n1 -> n3
}"""
let diff = Diff.patience textA textB
expect {
snapshot
@" digraph G {
rankdir = TB
bgcolor = transparent
- n4 [shape=Mrecord label=""{{n4|BindMain|height=2}}"" ""fontname""=""Sans Serif""]
- n3 [shape=Mrecord label=""{{n3|BindLhsChange|height=1}}"" ""fontname""=""Sans Serif""]
- n1 [shape=Mrecord label=""{{n1|Const|height=0}}"" ""fontname""=""Sans Serif""]
- n2 [shape=Mrecord label=""{{n2|Const|height=0}}"" ""fontname""=""Sans Serif""]
+ n4 [shape=box label=""{{n4|BindMain|height=2}}"" ]
+ n3 [shape=box label=""{{n3|BindLhsChange|height=1}}"" ]
+ n1 [shape=box label=""{{n1|Const|height=0}}"" ]
+ n2 [shape=box label=""{{n2|Const|height=0}}"" ]
n3 -> n4
n2 -> n4
n1 -> n3
}"
withFormat Diff.format
return diff
}

View File

@@ -0,0 +1,110 @@
namespace WoofWare.Expect.Test
#nowarn 0044 // This construct is deprecated
open System
open FsUnitTyped
open WoofWare.Expect
open NUnit.Framework
open System.IO.Abstractions
open System.IO.Abstractions.TestingHelpers
[<TestFixture>]
module TestDot =
let toFs (fs : IFileSystem) : Dot.IFileSystem =
{ new Dot.IFileSystem with
member _.DeleteFile s = fs.File.Delete s
member _.WriteFile path contents = fs.File.WriteAllText (path, contents)
member _.GetTempFileName () = fs.Path.GetTempFileName ()
}
[<Test ; Explicit "requires graph-easy dependency">]
let ``Basic dotfile, real graph-easy`` () =
let s =
"""digraph G {
rankdir = TB
bgcolor = transparent
n2 [shape=box label="{{n2|Map|height=1}}" ]
n1 [shape=box label="{{n1|Const|height=0}}" ]
n1 -> n2
}"""
expect {
snapshot
@"
┌───────────────────────┐
{{n1|Const|height=0}}
└───────────────────────┘
┌───────────────────────┐
{{n2|Map|height=1}}
└───────────────────────┘
"
return Dot.render s
}
[<Test>]
let ``Basic dotfile`` () =
let fs = MockFileSystem ()
let contents =
"""digraph G {
rankdir = TB
bgcolor = transparent
n2 [shape=box label="{{n2|Map|height=1}}" ]
n1 [shape=box label="{{n1|Const|height=0}}" ]
n1 -> n2
}"""
let mutable started = false
let mutable waited = false
let mutable exitCodeObserved = false
let mutable disposed = false
let expected =
"┌───────────────────────┐
{{n1|Const|height=0}}
└───────────────────────┘
┌───────────────────────┐
{{n2|Map|height=1}}
└───────────────────────┘
"
let pr =
{ new Dot.IProcess<IDisposable> with
member _.Start _ =
started <- true
true
member _.Create exe args =
exe |> shouldEqual "graph-easy"
args.StartsWith ("--as=boxart --from=dot ", StringComparison.Ordinal)
|> shouldEqual true
{ new IDisposable with
member _.Dispose () = disposed <- true
}
member _.WaitForExit p = waited <- true
member _.ReadStandardOutput _ = expected
member _.ExitCode _ =
exitCodeObserved <- true
0
}
Dot.render' pr (toFs fs) "graph-easy" contents
|> _.TrimStart()
|> shouldEqual expected
started |> shouldEqual true
waited |> shouldEqual true
exitCodeObserved |> shouldEqual true
disposed |> shouldEqual true

View File

@@ -13,6 +13,8 @@
<Compile Include="Assembly.fs" />
<Compile Include="BulkUpdateExample.fs" />
<Compile Include="SimpleTest.fs" />
<Compile Include="TestDiff.fs" />
<Compile Include="TestDot.fs" />
<Compile Include="TestExceptionThrowing.fs" />
<Compile Include="TestSurface.fs" />
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
@@ -39,6 +41,9 @@
<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" />
</ItemGroup>
<ItemGroup>

View File

@@ -298,26 +298,28 @@ type ExpectBuilder (mode : Mode) =
let raiseError (snapshot : string) (actual : string) : unit =
match mode with
| Mode.AssertMockingSource (mockSource, line) ->
let diff = Diff.patience snapshot actual
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
"snapshot mismatch! snapshot at %s:%i (%s) diff:\n\n%s"
mockSource
line
state.Caller.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
(Diff.format diff)
|> ExpectException
|> raise
| Mode.Assert ->
if GlobalBuilderConfig.isBulkUpdateMode () then
GlobalBuilderConfig.registerTest state
else
let diff = Diff.patience snapshot actual
sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
"snapshot mismatch! snapshot at %s:%i (%s) diff:\n\n%s"
state.Caller.FilePath
state.Caller.LineNumber
state.Caller.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
(Diff.format diff)
|> ExpectException
|> raise
| Mode.Update ->

296
WoofWare.Expect/Diff.fs Normal file
View File

@@ -0,0 +1,296 @@
namespace WoofWare.Expect
open System.Collections.Generic
/// A unit of measure tagging "positions in a sequence".
[<Measure>]
type pos
/// Position in a sequence
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 =
/// 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
/// Delete this line, which is at this position.
| Delete of posA : Position * line : string
/// Insert this line at the given position.
| Insert of posB : Position * line : string
/// The diff between two line-oriented pieces of text.
type Diff = private | Diff of DiffOperation list
/// A match between positions in two sequences
type internal LineMatch =
{
PosA : Position
PosB : Position
Line : string
}
/// Result of finding unique lines in a sequence
type internal UniqueLines =
{
/// Map from line content to its position (only for unique lines)
LinePositions : Map<string, Position>
/// All line counts (for verification)
LineCounts : Map<string, int>
}
[<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> ()
lines
|> Array.iteri (fun i line ->
if counts.ContainsKey (line) then
counts.[line] <- counts.[line] + 1
else
counts.[line] <- 1
positions.[line] <- i * 1<pos>
)
let uniquePositions =
positions
|> Seq.filter (fun kvp -> counts.[kvp.Key] = 1)
|> Seq.map (fun kvp -> (kvp.Key, kvp.Value))
|> Map.ofSeq
let allCounts = counts |> Seq.map (fun kvp -> (kvp.Key, kvp.Value)) |> Map.ofSeq
{
LinePositions = uniquePositions
LineCounts = allCounts
}
/// Find longest increasing subsequence based on B positions
let private longestIncreasingSubsequence (matches : LineMatch array) : LineMatch list =
let n = matches.Length
if n = 0 then
[]
else
// Dynamic programming arrays
let lengths = Array.create n 1
let parents = Array.create n -1
// Build LIS
for i in 1 .. n - 1 do
for j in 0 .. i - 1 do
let bj = matches.[j].PosB
let bi = matches.[i].PosB
if bj < bi && lengths.[j] + 1 > lengths.[i] then
lengths.[i] <- lengths.[j] + 1
parents.[i] <- j
// Find longest sequence
let maxLength = Array.max lengths
let endIndex = Array.findIndex ((=) maxLength) lengths
// Reconstruct sequence
let rec reconstruct idx acc =
if idx = -1 then
acc
else
reconstruct parents.[idx] (matches.[idx] :: acc)
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) =
match i < a.Length, j < b.Length with
| false, false -> List.rev acc
| true, false ->
let deletes =
[ i .. a.Length - 1 ] |> List.map (fun idx -> Delete (idx * 1<pos>, a.[idx]))
(List.rev acc) @ deletes
| false, true ->
let inserts =
[ j .. b.Length - 1 ] |> List.map (fun idx -> Insert (idx * 1<pos>, b.[idx]))
(List.rev acc) @ inserts
| true, true ->
if a.[i] = b.[j] then
diffHelper (i + 1) (j + 1) (Match (i * 1<pos>, j * 1<pos>, a.[i]) :: acc)
else
// Look ahead for matches (simple heuristic)
let lookAhead = 3
let aheadMatch =
[ 1 .. min lookAhead (min (a.Length - i) (b.Length - j)) ]
|> List.tryFind (fun k -> a.[i + k - 1] = b.[j + k - 1])
match aheadMatch with
| Some k when k <= 2 ->
// Delete/insert to get to the match
let ops =
[ 0 .. k - 2 ]
|> List.collect (fun offset ->
[
Delete ((i + offset) * 1<pos>, a.[i + offset])
Insert ((j + offset) * 1<pos>, b.[j + offset])
]
)
diffHelper (i + k - 1) (j + k - 1) (List.rev ops @ acc)
| _ ->
// No close match, just delete and insert
diffHelper (i + 1) j (Delete (i * 1<pos>, a.[i]) :: acc)
diffHelper 0 0 [] |> 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 =
// Handle empty sequences
match a.Length, b.Length with
| 0, 0 -> [] |> Diff
| 0, _ ->
b
|> Array.mapi (fun i line -> Insert (i * 1<pos>, line))
|> Array.toList
|> Diff
| _, 0 ->
a
|> Array.mapi (fun i line -> Delete (i * 1<pos>, line))
|> Array.toList
|> Diff
| _, _ ->
// Find unique lines
let uniqueA = findUniqueLines a
let uniqueB = findUniqueLines b
// Find common unique lines
let commonUniques =
Set.intersect
(uniqueA.LinePositions |> Map.toSeq |> Seq.map fst |> Set.ofSeq)
(uniqueB.LinePositions |> Map.toSeq |> Seq.map fst |> Set.ofSeq)
if Set.isEmpty commonUniques then
// No unique common lines, fall back to Myers
myers a b
else
// Build matches for unique common lines
let matches =
commonUniques
|> Set.toArray
|> Array.map (fun line ->
{
PosA = uniqueA.LinePositions.[line]
PosB = uniqueB.LinePositions.[line]
Line = line
}
)
|> Array.sortBy (fun m -> m.PosA)
// Find LIS
let anchorMatches = longestIncreasingSubsequence matches |> List.toArray
// Build diff imperatively
let result = ResizeArray<DiffOperation> ()
let mutable prevA = 0<pos>
let mutable prevB = 0<pos>
// Process each anchor
for anchor in anchorMatches do
let anchorA = anchor.PosA
let anchorB = anchor.PosB
// Add diff for section before this anchor
if prevA < anchorA || prevB < anchorB then
let sectionA = a.[prevA / 1<pos> .. anchorA / 1<pos> - 1]
let sectionB = b.[prevB / 1<pos> .. anchorB / 1<pos> - 1]
let (Diff sectionDiff) = patienceLines sectionA sectionB
// Adjust positions and add to result
for op in sectionDiff do
match op with
| Match (pa, pb, line) -> result.Add (Match ((pa + prevA), (pb + prevB), line))
| Delete (pa, line) -> result.Add (Delete ((pa + prevA), line))
| Insert (pb, line) -> result.Add (Insert ((pb + prevB), line))
// Add the anchor match
result.Add (Match (anchor.PosA, anchor.PosB, anchor.Line))
// Update positions
prevA <- anchorA + 1<pos>
prevB <- anchorB + 1<pos>
// Handle remaining elements after last anchor
if prevA / 1<pos> < a.Length || prevB / 1<pos> < b.Length then
let remainingA = a.[prevA / 1<pos> ..]
let remainingB = b.[prevB / 1<pos> ..]
let (Diff remainingDiff) = patienceLines remainingA remainingB
for op in remainingDiff do
match op with
| Match (pa, pb, line) -> result.Add (Match ((pa + prevA), (pb + prevB), line))
| Delete (pa, line) -> result.Add (Delete ((pa + prevA), line))
| Insert (pb, line) -> result.Add (Insert ((pb + prevB), line))
result |> Seq.toList |> Diff
/// Patience diff: a human-readable line-based diff.
let patience (a : string) (b : string) =
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 =
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
)
|> 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"
/// Compute diff statistics
type internal DiffStats =
{
Matches : int
Deletions : int
Insertions : int
TotalOperations : int
}
let internal computeStats (ops : DiffOperation list) : DiffStats =
let counts =
ops
|> List.fold
(fun (m, d, i) op ->
match op with
| Match _ -> (m + 1, d, i)
| Delete _ -> (m, d + 1, i)
| Insert _ -> (m, d, i + 1)
)
(0, 0, 0)
let matches, deletions, insertions = counts
{
Matches = matches
Deletions = deletions
Insertions = insertions
TotalOperations = matches + deletions + insertions
}

91
WoofWare.Expect/Dot.fs Normal file
View File

@@ -0,0 +1,91 @@
namespace WoofWare.Expect
open System
open System.Diagnostics
open System.IO
/// Methods for rendering dot files (specifications of graphs).
[<RequireQualifiedAccess>]
module Dot =
/// A mock for System.Diagnostics.Process.
type IProcess<'Process when 'Process :> IDisposable> =
/// Equivalent to Process.Create
abstract Create : exe : string -> args : string -> 'Process
/// Equivalent to Process.Start
abstract Start : 'Process -> bool
/// Equivalent to Process.WaitForExit
abstract WaitForExit : 'Process -> unit
/// Equivalent to Process.StandardOutput.ReadToEnd
abstract ReadStandardOutput : 'Process -> string
/// Equivalent to Process.ExitCode
abstract ExitCode : 'Process -> int
/// The real Process interface, in a form that can be passed to `render'`.
let process' =
{ new IProcess<Process> with
member _.Create exe args =
let psi = ProcessStartInfo exe
psi.RedirectStandardOutput <- true
psi.Arguments <- args
let result = new Process ()
result.StartInfo <- psi
result
member _.Start p = p.Start ()
member _.WaitForExit p = p.WaitForExit ()
member _.ReadStandardOutput p = p.StandardOutput.ReadToEnd ()
member _.ExitCode p = p.ExitCode
}
/// A mock for System.IO
type IFileSystem =
/// Equivalent to Path.GetTempFileName
abstract GetTempFileName : unit -> string
/// Equivalent to File.Delete
abstract DeleteFile : string -> unit
/// Equivalent to File.WriteAllText (curried)
abstract WriteFile : path : string -> contents : string -> unit
/// The real filesystem, in a form that can be passed to `render'`.
let fileSystem =
{ new IFileSystem with
member _.GetTempFileName () = Path.GetTempFileName ()
member _.DeleteFile f = File.Delete f
member _.WriteFile path contents = File.WriteAllText (path, contents)
}
/// writeFile takes the filepath first and the contents second.
/// Due to the impoverished nature of the .NET Standard APIs, you are in charge of making sure the output of
/// fs.GetTempFileName is suitable for interpolation into a command line.
let render'<'Process when 'Process :> IDisposable>
(pr : IProcess<'Process>)
(fs : IFileSystem)
(graphEasyExecutable : string)
(dotFileContents : string)
: string
=
let tempFile = fs.GetTempFileName ()
try
fs.WriteFile tempFile dotFileContents
use p = pr.Create graphEasyExecutable ("--as=boxart --from=dot " + tempFile)
pr.Start p |> ignore<bool>
pr.WaitForExit p
let stdout = pr.ReadStandardOutput p
let exitCode = pr.ExitCode p
if exitCode <> 0 then
failwithf "failed to run; exit code: %i. stdout:\n%s" exitCode stdout
"\n" + stdout
finally
try
fs.DeleteFile tempFile
with _ ->
()
/// Call `graph-easy` to render the dotfile as ASCII art.
/// This is fully mockable, but you must use `render'` to do so.
let render = render' process' fileSystem "graph-easy"

View File

@@ -15,7 +15,7 @@ type private StringLiteralInfo =
override this.ToString () =
sprintf "%i:%i to %i:%i: %s" this.StartLine this.StartColumn this.EndLine this.EndColumn this.Content
type private Position =
type private SnapshotPosition =
{
Line : int
Column : int
@@ -28,8 +28,8 @@ module internal SnapshotUpdate =
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 =
let private positionToLineColumn (text : string) (offset : int) : SnapshotPosition =
let rec loop (line : int) (col : int) (totalOffset : int) (i : int) : SnapshotPosition =
if i >= text.Length || totalOffset = offset then
{
Line = line

View File

@@ -8,6 +8,66 @@ WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo Sy
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.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.Dot inherit obj
WoofWare.Expect.Dot+IFileSystem - interface with 3 member(s)
WoofWare.Expect.Dot+IFileSystem.DeleteFile [method]: string -> unit
WoofWare.Expect.Dot+IFileSystem.GetTempFileName [method]: unit -> string
WoofWare.Expect.Dot+IFileSystem.WriteFile [method]: string -> string -> unit
WoofWare.Expect.Dot+IProcess`1 - interface with 5 member(s)
WoofWare.Expect.Dot+IProcess`1.Create [method]: string -> string -> #(IDisposable)
WoofWare.Expect.Dot+IProcess`1.ExitCode [method]: #(IDisposable) -> int
WoofWare.Expect.Dot+IProcess`1.ReadStandardOutput [method]: #(IDisposable) -> string
WoofWare.Expect.Dot+IProcess`1.Start [method]: #(IDisposable) -> bool
WoofWare.Expect.Dot+IProcess`1.WaitForExit [method]: #(IDisposable) -> unit
WoofWare.Expect.Dot.fileSystem [static property]: [read-only] WoofWare.Expect.Dot+IFileSystem
WoofWare.Expect.Dot.get_fileSystem [static method]: unit -> WoofWare.Expect.Dot+IFileSystem
WoofWare.Expect.Dot.get_process' [static method]: unit -> System.Diagnostics.Process WoofWare.Expect.Dot+IProcess
WoofWare.Expect.Dot.get_render [static method]: unit -> (string -> string)
WoofWare.Expect.Dot.process' [static property]: [read-only] System.Diagnostics.Process WoofWare.Expect.Dot+IProcess
WoofWare.Expect.Dot.render [static property]: [read-only] string -> string
WoofWare.Expect.Dot.render' [static method]: #(IDisposable) WoofWare.Expect.Dot+IProcess -> WoofWare.Expect.Dot+IFileSystem -> string -> string -> string
WoofWare.Expect.ExpectBuilder inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
@@ -37,3 +97,4 @@ WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit ->
WoofWare.Expect.GlobalBuilderConfig.updateAllSnapshots [static method]: unit -> unit
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.pos inherit obj

View File

@@ -19,6 +19,8 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Text.fs" />
<Compile Include="File.fs" />
<Compile Include="Diff.fs" />
<Compile Include="Dot.fs" />
<Compile Include="Domain.fs" />
<Compile Include="SnapshotUpdate.fs" />
<Compile Include="Config.fs" />

View File

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

View File

@@ -66,6 +66,7 @@
pkgs.nodePackages.markdown-link-check
pkgs.shellcheck
pkgs.xmlstarlet
pkgs.graph-easy
];
};
});

View File

@@ -189,6 +189,11 @@
"version": "4.2.13",
"hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg="
},
{
"pname": "System.IO.Abstractions.TestingHelpers",
"version": "4.2.13",
"hash": "sha256-WGGatXlgyROnptdw0zU3ggf54eD/zusO/fvtf+5wuPU="
},
{
"pname": "System.IO.FileSystem.AccessControl",
"version": "4.5.0",