mirror of
https://github.com/Smaug123/WoofWare.Expect
synced 2025-10-06 21:18:41 +00:00
Compare commits
12 Commits
WoofWare.E
...
WoofWare.E
Author | SHA1 | Date | |
---|---|---|---|
|
ebc24f85aa | ||
|
faacb4770c | ||
|
d21786ecd4 | ||
|
ad05a9c106 | ||
|
ed352c1b14 | ||
|
e0153ab182 | ||
|
ca74c4816b | ||
|
75899d5668 | ||
|
34a2b460b9 | ||
|
0b64d3dd34 | ||
|
457d7b16de | ||
|
d115185525 |
22
.envrc
22
.envrc
@@ -1 +1,23 @@
|
|||||||
use flake
|
use flake
|
||||||
|
DOTNET_PATH=$(readlink "$(which dotnet)")
|
||||||
|
SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
|
||||||
|
MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)")
|
||||||
|
if [ -f "$SETTINGS_FILE" ] ; then
|
||||||
|
xmlstarlet ed --inplace \
|
||||||
|
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||||
|
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||||
|
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||||
|
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||||
|
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
|
||||||
|
--value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \
|
||||||
|
"$SETTINGS_FILE"
|
||||||
|
|
||||||
|
xmlstarlet ed --inplace \
|
||||||
|
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||||
|
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||||
|
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||||
|
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||||
|
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
|
||||||
|
--value "$MSBUILD" \
|
||||||
|
"$SETTINGS_FILE"
|
||||||
|
fi
|
||||||
|
33
README.md
33
README.md
@@ -16,8 +16,7 @@ An [expect-testing](https://blog.janestreet.com/the-joy-of-expect-tests/) librar
|
|||||||
# Current status
|
# Current status
|
||||||
|
|
||||||
The basic mechanism works.
|
The basic mechanism works.
|
||||||
Snapshot updating is vibe-coded with Opus 4 and is purely text-based; I didn't want to use the F# compiler services because that's a pretty heavyweight dependency which should be confined to a separate test runner entity.
|
It's fairly well tested, but you will almost certainly be able to find ways to break it; try not to be too fancy with your syntax around the `snapshot` statement.
|
||||||
It's not very well tested, and I expect it to be kind of brittle.
|
|
||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
|
|
||||||
@@ -44,8 +43,24 @@ let ``This test fails: plain text comparison of ToString`` () =
|
|||||||
snapshot " 123 "
|
snapshot " 123 "
|
||||||
return 123
|
return 123
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``With return! and snapshotThrows, you can see exceptions too`` () =
|
||||||
|
expect {
|
||||||
|
snapshotThrows @"System.Exception: oh no"
|
||||||
|
return! (fun () -> failwith<int> "oh no")
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``You can do lists more neatly with the snapshotList keyword`` () =
|
||||||
|
expect {
|
||||||
|
snapshotList [ "8" ; "9" ; "10" ; "11" ; "12" ]
|
||||||
|
return [ 8..12 ]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(JSON output for elements is not yet supported with `snapshotList`.)
|
||||||
|
|
||||||
You can adjust the formatting:
|
You can adjust the formatting:
|
||||||
|
|
||||||
```fsharp
|
```fsharp
|
||||||
@@ -57,6 +72,15 @@ let ``Overriding the formatting`` () =
|
|||||||
snapshot @"Int32"
|
snapshot @"Int32"
|
||||||
return 123
|
return 123
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Overriding the formatting with lists`` () =
|
||||||
|
expect {
|
||||||
|
// these two lines *do* have to be in this order, for annoying reasons
|
||||||
|
snapshotList [ "8" ; "9" ; "0" ; "1" ; "2" ]
|
||||||
|
withFormat (fun x -> string<int> (x % 10))
|
||||||
|
return [ 8..12 ]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can override the JSON serialisation if you find the snapshot format displeasing:
|
You can override the JSON serialisation if you find the snapshot format displeasing:
|
||||||
@@ -180,6 +204,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"`).
|
* 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
|
# Licence
|
||||||
|
|
||||||
MIT.
|
MIT.
|
||||||
|
@@ -19,12 +19,9 @@ module SimpleTest =
|
|||||||
let ``Example of a failing test`` () =
|
let ``Example of a failing test`` () =
|
||||||
expect {
|
expect {
|
||||||
snapshot
|
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
|
- 123
|
||||||
|
|
||||||
actual was:
|
|
||||||
|
|
||||||
+ 124"
|
+ 124"
|
||||||
|
|
||||||
return
|
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...
|
// Out of the box, comments in snapshots cause the JSON parser to throw, so the snapshot fails to match...
|
||||||
expect {
|
expect {
|
||||||
snapshot
|
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:] {
|
- [JSON failed to parse:] {
|
||||||
- // a key here
|
- // a key here
|
||||||
- ""a"":3
|
|
||||||
- }
|
|
||||||
|
|
||||||
actual was:
|
|
||||||
|
|
||||||
+ {
|
+ {
|
||||||
+ ""a"": 3
|
""a"": 3
|
||||||
+ }"
|
}"
|
||||||
|
|
||||||
return
|
return
|
||||||
Assert.Throws<ExpectException> (fun () ->
|
Assert.Throws<ExpectException> (fun () ->
|
||||||
expectWithMockedFilePath ("file.fs", 99) {
|
expectWithMockedFilePath ("file.fs", 99) {
|
||||||
snapshotJson
|
snapshotJson
|
||||||
@"{
|
@"{
|
||||||
// a key here
|
// a key here
|
||||||
""a"":3
|
""a"": 3
|
||||||
}"
|
}"
|
||||||
|
|
||||||
return Map.ofList [ "a", 3 ]
|
return Map.ofList [ "a", 3 ]
|
||||||
|
44
WoofWare.Expect.Test/SyntaxCases/CommentsAndSpacing.fs
Normal file
44
WoofWare.Expect.Test/SyntaxCases/CommentsAndSpacing.fs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
@"test with many comments"
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) """nested comment test"""
|
||||||
|
return "nested"
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with "quotes" and \ backslash *) "regular string"
|
||||||
|
return "special"
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
"string after whitespace"
|
||||||
|
|
||||||
|
return "whitespace"
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) @"123"
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
69
WoofWare.Expect.Test/SyntaxCases/EdgeCases.fs
Normal file
69
WoofWare.Expect.Test/SyntaxCases/EdgeCases.fs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot ""
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot " \t\n "
|
||||||
|
return "whitespace"
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @"He said ""Hello"" and she said ""Hi"""
|
||||||
|
return "quotes"
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @"C:\Users\Test\Documents\file.txt"
|
||||||
|
return "path"
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings"
|
||||||
|
|
||||||
|
return "long line"
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
"""
|
||||||
|
|
||||||
|
Starts with newlines"""
|
||||||
|
|
||||||
|
return "leading"
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
"""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "trailing"
|
||||||
|
}
|
84
WoofWare.Expect.Test/SyntaxCases/MultilineComplex.fs
Normal file
84
WoofWare.Expect.Test/SyntaxCases/MultilineComplex.fs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
"""Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
Line 4
|
||||||
|
Line 5
|
||||||
|
Line 6
|
||||||
|
Line 7
|
||||||
|
Line 8
|
||||||
|
Line 9
|
||||||
|
Line 10
|
||||||
|
Indented line 11
|
||||||
|
More indented line 12
|
||||||
|
Line 13
|
||||||
|
Line 14
|
||||||
|
Line 15"""
|
||||||
|
|
||||||
|
return "long"
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"First line
|
||||||
|
|
||||||
|
Third line
|
||||||
|
|
||||||
|
|
||||||
|
Sixth line"
|
||||||
|
|
||||||
|
return "empty lines"
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
"""Special chars:
|
||||||
|
Tab: here
|
||||||
|
Quotes: "double" and 'single'
|
||||||
|
Backslash: \ and \\
|
||||||
|
Unicode: 🎯
|
||||||
|
Regex: .*+?[]"""
|
||||||
|
|
||||||
|
return "special"
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@"{
|
||||||
|
""name"": ""test"",
|
||||||
|
""values"": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
""nested"": {
|
||||||
|
""deep"": true
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = "test"
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot "Line 1\r\nLine 2\r\nLine 3"
|
||||||
|
return "crlf"
|
||||||
|
}
|
28
WoofWare.Expect.Test/SyntaxCases/RegexMetacharacters.fs
Normal file
28
WoofWare.Expect.Test/SyntaxCases/RegexMetacharacters.fs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let regexChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @"test with regex chars: .*+?[]{}()|^$\ and more"
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """regex: .*+?[]{}()|^$\ in triple quotes"""
|
||||||
|
return 456
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInRegularString () =
|
||||||
|
expect {
|
||||||
|
snapshot "escaped regex: \\.\\*\\+\\?\\[\\]\\{\\}\\(\\)\\|\\^\\$\\\\"
|
||||||
|
return 789
|
||||||
|
}
|
||||||
|
|
||||||
|
let complexRegexPattern () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||||
|
return "IP regex"
|
||||||
|
}
|
47
WoofWare.Expect.Test/SyntaxCases/UnicodeCharacters.fs
Normal file
47
WoofWare.Expect.Test/SyntaxCases/UnicodeCharacters.fs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @"Hello 👋 World 🌍 with emoji 🎉🎊"
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """Chinese: 你好世界"""
|
||||||
|
return "hello"
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @"Arabic RTL: مرحبا بالعالم"
|
||||||
|
return "rtl test"
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot "test with combining: e\u0301 and a\u0308"
|
||||||
|
return "combining"
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @"Mixed: English, русский, 日本語, العربية, emoji 🚀"
|
||||||
|
return [ "multilingual" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @"Zerowidthspacetest" // Contains U+200B
|
||||||
|
return "zwsp"
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1"""
|
||||||
|
return "math"
|
||||||
|
}
|
110
WoofWare.Expect.Test/TestDiff.fs
Normal file
110
WoofWare.Expect.Test/TestDiff.fs
Normal 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
|
||||||
|
}
|
110
WoofWare.Expect.Test/TestDot.fs
Normal file
110
WoofWare.Expect.Test/TestDot.fs
Normal 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
|
14
WoofWare.Expect.Test/TestExceptionThrowing.fs
Normal file
14
WoofWare.Expect.Test/TestExceptionThrowing.fs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open NUnit.Framework
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestExceptionThrowing =
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can throw an exception`` () =
|
||||||
|
expect {
|
||||||
|
snapshotThrows @"System.Exception: oh no"
|
||||||
|
return! (fun () -> failwith<int> "oh no")
|
||||||
|
}
|
@@ -0,0 +1,313 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
open NUnit.Framework
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
[<Parallelizable(ParallelScope.Children)>]
|
||||||
|
module TestCommentsAndSpacing =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
|
type Dummy = class end
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Multiple comments between snapshot and string`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "CommentsAndSpacing.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
""updated after many comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) """"""nested comment test""""""
|
||||||
|
return ""nested""
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with ""quotes"" and \ backslash *) ""regular string""
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
""string after whitespace""
|
||||||
|
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) @""123""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 8 "updated after many comments"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Nested comments`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "CommentsAndSpacing.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
@""test with many comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) ""updated after nested comments""
|
||||||
|
return ""nested""
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with ""quotes"" and \ backslash *) ""regular string""
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
""string after whitespace""
|
||||||
|
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) @""123""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 17 "updated after nested comments"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Comment with special chars`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "CommentsAndSpacing.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
@""test with many comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) """"""nested comment test""""""
|
||||||
|
return ""nested""
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with ""quotes"" and \ backslash *) ""updated after weird comment""
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
""string after whitespace""
|
||||||
|
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) @""123""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 23 "updated after weird comment"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Whitespace before`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "CommentsAndSpacing.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
@""test with many comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) """"""nested comment test""""""
|
||||||
|
return ""nested""
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with ""quotes"" and \ backslash *) ""regular string""
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
""updated after spaces""
|
||||||
|
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) @""123""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 29 "updated after spaces"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Mixed whitespace and comments`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "CommentsAndSpacing.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let multipleComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* first comment *) (* second comment *)
|
||||||
|
(* third comment on new line *)
|
||||||
|
@""test with many comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedComments () =
|
||||||
|
expect {
|
||||||
|
snapshot (* outer (* inner *) comment *) """"""nested comment test""""""
|
||||||
|
return ""nested""
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot (* comment with ""quotes"" and \ backslash *) ""regular string""
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
|
||||||
|
|
||||||
|
""string after whitespace""
|
||||||
|
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedWhitespaceAndComments () =
|
||||||
|
expect {
|
||||||
|
snapshotJson (* comment 1 *)
|
||||||
|
(* comment 2 *)
|
||||||
|
(* comment 3 *) ""updated after comments""
|
||||||
|
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 39 "updated after comments"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
781
WoofWare.Expect.Test/TestSnapshotFinding/TestEdgeCases.fs
Normal file
781
WoofWare.Expect.Test/TestSnapshotFinding/TestEdgeCases.fs
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open NUnit.Framework
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestEdgeCases =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
|
type Dummy = class end
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Empty string replacements`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot ""now has content""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 8 "now has content"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Empty string replacements, verbatim`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot ""now has content""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 14 "now has content"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Empty string replacements, triple quotes`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot ""now has content""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 20 "now has content"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Empty string replacements, only whitespace`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot ""now has content""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 26 "now has content"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Quotes in quotes handling`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Updated: He said """"What's up?"""" and replied """"Nothing much.""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine
|
||||||
|
source
|
||||||
|
32
|
||||||
|
"Updated: He said \"What's up?\" and replied \"Nothing much.\""
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Backslashes galore`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""prefer\these\ones""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 38 "prefer\\these\\ones"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Very long line`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""this line is short though""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 44 "this line is short though"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Leading newlines`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""
|
||||||
|
|
||||||
|
Just newlines!
|
||||||
|
|
||||||
|
|
||||||
|
""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Ends with newlines
|
||||||
|
|
||||||
|
|
||||||
|
""""""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 52 "\n\nJust newlines!\n\n\n"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Trailing newlines`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "EdgeCases.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emptyString () =
|
||||||
|
expect {
|
||||||
|
snapshot """"
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyVerbatim () =
|
||||||
|
expect {
|
||||||
|
snapshot @""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""""""""
|
||||||
|
return """"
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyWhitespace () =
|
||||||
|
expect {
|
||||||
|
snapshot "" \t\n ""
|
||||||
|
return ""whitespace""
|
||||||
|
}
|
||||||
|
|
||||||
|
let quotesInQuotes () =
|
||||||
|
expect {
|
||||||
|
snapshot @""He said """"Hello"""" and she said """"Hi""""""
|
||||||
|
return ""quotes""
|
||||||
|
}
|
||||||
|
|
||||||
|
let backslashesGalore () =
|
||||||
|
expect {
|
||||||
|
snapshot @""C:\Users\Test\Documents\file.txt""
|
||||||
|
return ""path""
|
||||||
|
}
|
||||||
|
|
||||||
|
let veryLongLine () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""This is a very long line that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and on and contains over 300 characters to test how the parser handles very long single-line strings""
|
||||||
|
|
||||||
|
return ""long line""
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""
|
||||||
|
|
||||||
|
Starts with newlines""""""
|
||||||
|
|
||||||
|
return ""leading""
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingNewlines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""
|
||||||
|
|
||||||
|
Just newlines!
|
||||||
|
|
||||||
|
|
||||||
|
""
|
||||||
|
|
||||||
|
return ""trailing""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 62 "\n\nJust newlines!\n\n\n"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
483
WoofWare.Expect.Test/TestSnapshotFinding/TestMultilineComplex.fs
Normal file
483
WoofWare.Expect.Test/TestSnapshotFinding/TestMultilineComplex.fs
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open NUnit.Framework
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestMultilineComplex =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
|
type Dummy = class end
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Very long multiline string`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "MultilineComplex.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""Replaced with
|
||||||
|
a different
|
||||||
|
multiline
|
||||||
|
value""
|
||||||
|
|
||||||
|
return ""long""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""First line
|
||||||
|
|
||||||
|
Third line
|
||||||
|
|
||||||
|
|
||||||
|
Sixth line""
|
||||||
|
|
||||||
|
return ""empty lines""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Special chars:
|
||||||
|
Tab: here
|
||||||
|
Quotes: ""double"" and 'single'
|
||||||
|
Backslash: \ and \\
|
||||||
|
Unicode: 🎯
|
||||||
|
Regex: .*+?[]""""""
|
||||||
|
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@""{
|
||||||
|
""""name"""": """"test"""",
|
||||||
|
""""values"""": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
""""nested"""": {
|
||||||
|
""""deep"""": true
|
||||||
|
}
|
||||||
|
}""
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = ""test""
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Line 1\r\nLine 2\r\nLine 3""
|
||||||
|
return ""crlf""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 8 "Replaced with\na different\nmultiline\nvalue"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Multiline with empty lines`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "MultilineComplex.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
Line 4
|
||||||
|
Line 5
|
||||||
|
Line 6
|
||||||
|
Line 7
|
||||||
|
Line 8
|
||||||
|
Line 9
|
||||||
|
Line 10
|
||||||
|
Indented line 11
|
||||||
|
More indented line 12
|
||||||
|
Line 13
|
||||||
|
Line 14
|
||||||
|
Line 15""""""
|
||||||
|
|
||||||
|
return ""long""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""Replaced with
|
||||||
|
|
||||||
|
a different
|
||||||
|
multiline
|
||||||
|
value""
|
||||||
|
|
||||||
|
return ""empty lines""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Special chars:
|
||||||
|
Tab: here
|
||||||
|
Quotes: ""double"" and 'single'
|
||||||
|
Backslash: \ and \\
|
||||||
|
Unicode: 🎯
|
||||||
|
Regex: .*+?[]""""""
|
||||||
|
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@""{
|
||||||
|
""""name"""": """"test"""",
|
||||||
|
""""values"""": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
""""nested"""": {
|
||||||
|
""""deep"""": true
|
||||||
|
}
|
||||||
|
}""
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = ""test""
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Line 1\r\nLine 2\r\nLine 3""
|
||||||
|
return ""crlf""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 30 "Replaced with\n\na different\nmultiline\nvalue"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Multiline with special chars`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "MultilineComplex.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
Line 4
|
||||||
|
Line 5
|
||||||
|
Line 6
|
||||||
|
Line 7
|
||||||
|
Line 8
|
||||||
|
Line 9
|
||||||
|
Line 10
|
||||||
|
Indented line 11
|
||||||
|
More indented line 12
|
||||||
|
Line 13
|
||||||
|
Line 14
|
||||||
|
Line 15""""""
|
||||||
|
|
||||||
|
return ""long""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""First line
|
||||||
|
|
||||||
|
Third line
|
||||||
|
|
||||||
|
|
||||||
|
Sixth line""
|
||||||
|
|
||||||
|
return ""empty lines""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""get rid of it all""
|
||||||
|
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@""{
|
||||||
|
""""name"""": """"test"""",
|
||||||
|
""""values"""": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
""""nested"""": {
|
||||||
|
""""deep"""": true
|
||||||
|
}
|
||||||
|
}""
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = ""test""
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Line 1\r\nLine 2\r\nLine 3""
|
||||||
|
return ""crlf""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 43 "get rid of it all"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Complex nested JSON with Unicode`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "MultilineComplex.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
Line 4
|
||||||
|
Line 5
|
||||||
|
Line 6
|
||||||
|
Line 7
|
||||||
|
Line 8
|
||||||
|
Line 9
|
||||||
|
Line 10
|
||||||
|
Indented line 11
|
||||||
|
More indented line 12
|
||||||
|
Line 13
|
||||||
|
Line 14
|
||||||
|
Line 15""""""
|
||||||
|
|
||||||
|
return ""long""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""First line
|
||||||
|
|
||||||
|
Third line
|
||||||
|
|
||||||
|
|
||||||
|
Sixth line""
|
||||||
|
|
||||||
|
return ""empty lines""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Special chars:
|
||||||
|
Tab: here
|
||||||
|
Quotes: ""double"" and 'single'
|
||||||
|
Backslash: \ and \\
|
||||||
|
Unicode: 🎯
|
||||||
|
Regex: .*+?[]""""""
|
||||||
|
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@""wheeeee""
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = ""test""
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Line 1\r\nLine 2\r\nLine 3""
|
||||||
|
return ""crlf""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return SnapshotUpdate.updateSnapshotAtLine source 56 "wheeeee" |> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Windows line endings`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "MultilineComplex.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let veryLongMultiline () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
Line 4
|
||||||
|
Line 5
|
||||||
|
Line 6
|
||||||
|
Line 7
|
||||||
|
Line 8
|
||||||
|
Line 9
|
||||||
|
Line 10
|
||||||
|
Indented line 11
|
||||||
|
More indented line 12
|
||||||
|
Line 13
|
||||||
|
Line 14
|
||||||
|
Line 15""""""
|
||||||
|
|
||||||
|
return ""long""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithEmptyLines () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@""First line
|
||||||
|
|
||||||
|
Third line
|
||||||
|
|
||||||
|
|
||||||
|
Sixth line""
|
||||||
|
|
||||||
|
return ""empty lines""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineWithSpecialChars () =
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
""""""Special chars:
|
||||||
|
Tab: here
|
||||||
|
Quotes: ""double"" and 'single'
|
||||||
|
Backslash: \ and \\
|
||||||
|
Unicode: 🎯
|
||||||
|
Regex: .*+?[]""""""
|
||||||
|
|
||||||
|
return ""special""
|
||||||
|
}
|
||||||
|
|
||||||
|
let multilineJson () =
|
||||||
|
expect {
|
||||||
|
snapshotJson
|
||||||
|
@""{
|
||||||
|
""""name"""": """"test"""",
|
||||||
|
""""values"""": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
""""nested"""": {
|
||||||
|
""""deep"""": true
|
||||||
|
}
|
||||||
|
}""
|
||||||
|
|
||||||
|
return
|
||||||
|
{
|
||||||
|
name = ""test""
|
||||||
|
values = [ 1 ; 2 ; 3 ]
|
||||||
|
nested =
|
||||||
|
{|
|
||||||
|
deep = true
|
||||||
|
|}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowsLineEndings () =
|
||||||
|
expect {
|
||||||
|
snapshot ""down with line endings""
|
||||||
|
return ""crlf""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 82 "down with line endings"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
@@ -0,0 +1,190 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
open NUnit.Framework
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
[<Parallelizable(ParallelScope.Children)>]
|
||||||
|
module TestRegexMetacharacters =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
|
type Dummy = class end
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Regex metacharacters in verbatim string`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "RegexMetacharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let regexChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""replacement with .*+?[]{}()|^$\ chars""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""regex: .*+?[]{}()|^$\ in triple quotes""""""
|
||||||
|
return 456
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInRegularString () =
|
||||||
|
expect {
|
||||||
|
snapshot ""escaped regex: \\.\\*\\+\\?\\[\\]\\{\\}\\(\\)\\|\\^\\$\\\\""
|
||||||
|
return 789
|
||||||
|
}
|
||||||
|
|
||||||
|
let complexRegexPattern () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$""
|
||||||
|
return ""IP regex""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 8 "replacement with .*+?[]{}()|^$\\ chars"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Regex metacharacters in triple quote`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "RegexMetacharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let regexChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""test with regex chars: .*+?[]{}()|^$\ and more""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot @""new regex: [a-z]+(?:\d{2,4})? pattern""
|
||||||
|
return 456
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInRegularString () =
|
||||||
|
expect {
|
||||||
|
snapshot ""escaped regex: \\.\\*\\+\\?\\[\\]\\{\\}\\(\\)\\|\\^\\$\\\\""
|
||||||
|
return 789
|
||||||
|
}
|
||||||
|
|
||||||
|
let complexRegexPattern () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$""
|
||||||
|
return ""IP regex""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 14 "new regex: [a-z]+(?:\\d{2,4})? pattern"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Regex metacharacters in regular string`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "RegexMetacharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let regexChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""test with regex chars: .*+?[]{}()|^$\ and more""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""regex: .*+?[]{}()|^$\ in triple quotes""""""
|
||||||
|
return 456
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInRegularString () =
|
||||||
|
expect {
|
||||||
|
snapshot @""new regex: [a-z]+(?:\d{2,4})? pattern""
|
||||||
|
return 789
|
||||||
|
}
|
||||||
|
|
||||||
|
let complexRegexPattern () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$""
|
||||||
|
return ""IP regex""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 20 "new regex: [a-z]+(?:\\d{2,4})? pattern"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``IP regex`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "RegexMetacharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let regexChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""test with regex chars: .*+?[]{}()|^$\ and more""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInTripleQuote () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""regex: .*+?[]{}()|^$\ in triple quotes""""""
|
||||||
|
return 456
|
||||||
|
}
|
||||||
|
|
||||||
|
let regexInRegularString () =
|
||||||
|
expect {
|
||||||
|
snapshot ""escaped regex: \\.\\*\\+\\?\\[\\]\\{\\}\\(\\)\\|\\^\\$\\\\""
|
||||||
|
return 789
|
||||||
|
}
|
||||||
|
|
||||||
|
let complexRegexPattern () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""new regex: [a-z]+(?:\d{2,4})? pattern""
|
||||||
|
return ""IP regex""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 26 "new regex: [a-z]+(?:\\d{2,4})? pattern"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
@@ -4,7 +4,16 @@ open WoofWare.Expect
|
|||||||
open NUnit.Framework
|
open NUnit.Framework
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
|
[<Parallelizable(ParallelScope.Children)>]
|
||||||
module TestSnapshotFinding =
|
module TestSnapshotFinding =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
type Dummy = class end
|
type Dummy = class end
|
||||||
|
|
||||||
@@ -23,7 +32,7 @@ open WoofWare.Expect
|
|||||||
module MyModule =
|
module MyModule =
|
||||||
let foo () =
|
let foo () =
|
||||||
expect {
|
expect {
|
||||||
snapshot @""replacement""
|
snapshot ""replacement""
|
||||||
return 123
|
return 123
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -72,7 +81,7 @@ open WoofWare.Expect
|
|||||||
module MyModule =
|
module MyModule =
|
||||||
let foo () =
|
let foo () =
|
||||||
expect {
|
expect {
|
||||||
snapshot @""replacement""
|
snapshot ""replacement""
|
||||||
return 123
|
return 123
|
||||||
}
|
}
|
||||||
"
|
"
|
@@ -0,0 +1,456 @@
|
|||||||
|
namespace WoofWare.Expect.Test
|
||||||
|
|
||||||
|
open NUnit.Framework
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestUnicodeCharacters =
|
||||||
|
[<OneTimeSetUp>]
|
||||||
|
let ``Prepare to bulk-update tests`` () =
|
||||||
|
// GlobalBuilderConfig.enterBulkUpdateMode ()
|
||||||
|
()
|
||||||
|
|
||||||
|
[<OneTimeTearDown>]
|
||||||
|
let ``Update all tests`` () =
|
||||||
|
GlobalBuilderConfig.updateAllSnapshots ()
|
||||||
|
|
||||||
|
type Dummy = class end
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Unicode emoji in string`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Updated with 🚀🌟✨ more emoji!""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 8 "Updated with 🚀🌟✨ more emoji!"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Unicode Chinese characters multi-line`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Chinese poem:
|
||||||
|
静夜思
|
||||||
|
床前明月光
|
||||||
|
疑是地上霜
|
||||||
|
举头望明月
|
||||||
|
低头思故乡""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 14 "Chinese poem:\n静夜思\n床前明月光\n疑是地上霜\n举头望明月\n低头思故乡"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Arabic RTL`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Updated Arabic: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 20 "Updated Arabic: مرحبا بالعالم"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Combining characters`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""updated test with combining: é and ä!""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 27 "updated test with combining: e\u0301 and a\u0308!"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Mixed scripts`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson ""Updated mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 33 "Updated mixed: English, русский, 日本語, العربية, emoji 🚀"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``ZWBS character`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Updated: Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Math: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""""""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 39 "Updated: Zerowidthspacetest"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Maths`` () =
|
||||||
|
let source =
|
||||||
|
Assembly.getEmbeddedResource typeof<Dummy>.Assembly "UnicodeCharacters.fs"
|
||||||
|
|> _.Split('\n')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
snapshot
|
||||||
|
@"namespace BigExample
|
||||||
|
|
||||||
|
open WoofWare.Expect
|
||||||
|
|
||||||
|
module MyModule =
|
||||||
|
let emoji () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Hello 👋 World 🌍 with emoji 🎉🎊""
|
||||||
|
return 123
|
||||||
|
}
|
||||||
|
|
||||||
|
let chineseCharacters () =
|
||||||
|
expect {
|
||||||
|
snapshot """"""Chinese: 你好世界""""""
|
||||||
|
return ""hello""
|
||||||
|
}
|
||||||
|
|
||||||
|
let arabicRTL () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Arabic RTL: مرحبا بالعالم""
|
||||||
|
return ""rtl test""
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningCharacters () =
|
||||||
|
expect {
|
||||||
|
// Combining diacritics: e + ́ = é
|
||||||
|
snapshot ""test with combining: e\u0301 and a\u0308""
|
||||||
|
return ""combining""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixedScripts () =
|
||||||
|
expect {
|
||||||
|
snapshotJson @""Mixed: English, русский, 日本語, العربية, emoji 🚀""
|
||||||
|
return [ ""multilingual"" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroWidthChars () =
|
||||||
|
expect {
|
||||||
|
snapshot @""Zerowidthspacetest"" // Contains U+200B
|
||||||
|
return ""zwsp""
|
||||||
|
}
|
||||||
|
|
||||||
|
let mathSymbols () =
|
||||||
|
expect {
|
||||||
|
snapshot ""Pretty vacuous, huh: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1""
|
||||||
|
return ""math""
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
return
|
||||||
|
SnapshotUpdate.updateSnapshotAtLine source 45 "Pretty vacuous, huh: ∀x∈ℝ, ∃y: x² + y² = 1 ⟹ |x| ≤ 1"
|
||||||
|
|> String.concat "\n"
|
||||||
|
}
|
32
WoofWare.Expect.Test/TestSnapshotList.fs
Normal file
32
WoofWare.Expect.Test/TestSnapshotList.fs
Normal 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 ]
|
||||||
|
}
|
@@ -1,21 +1,39 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<EnableNUnitRunner>true</EnableNUnitRunner>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Assembly.fs" />
|
<Compile Include="Assembly.fs" />
|
||||||
<Compile Include="BulkUpdateExample.fs" />
|
<Compile Include="BulkUpdateExample.fs" />
|
||||||
<Compile Include="SimpleTest.fs" />
|
<Compile Include="SimpleTest.fs" />
|
||||||
<Compile Include="TestSnapshotFinding.fs" />
|
<Compile Include="TestDiff.fs" />
|
||||||
|
<Compile Include="TestDot.fs" />
|
||||||
|
<Compile Include="TestExceptionThrowing.fs" />
|
||||||
|
<Compile Include="TestSnapshotList.fs" />
|
||||||
<Compile Include="TestSurface.fs" />
|
<Compile Include="TestSurface.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestUnicodeCharacters.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestMultilineComplex.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestEdgeCases.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestCommentsAndSpacing.fs" />
|
||||||
|
<Compile Include="TestSnapshotFinding\TestRegexMetacharacters.fs" />
|
||||||
|
|
||||||
<EmbeddedResource Include="SyntaxCases\AtStringOneLine.fs" />
|
<EmbeddedResource Include="SyntaxCases\AtStringOneLine.fs" />
|
||||||
<EmbeddedResource Include="SyntaxCases\SingleQuoteManyLine.fs" />
|
<EmbeddedResource Include="SyntaxCases\SingleQuoteManyLine.fs" />
|
||||||
<EmbeddedResource Include="SyntaxCases\TripleQuoteInterveningComment.fs" />
|
<EmbeddedResource Include="SyntaxCases\TripleQuoteInterveningComment.fs" />
|
||||||
<EmbeddedResource Include="SyntaxCases\TripleQuoteOneLine.fs" />
|
<EmbeddedResource Include="SyntaxCases\TripleQuoteOneLine.fs" />
|
||||||
|
<EmbeddedResource Include="SyntaxCases\RegexMetacharacters.fs" />
|
||||||
|
<EmbeddedResource Include="SyntaxCases\UnicodeCharacters.fs" />
|
||||||
|
<EmbeddedResource Include="SyntaxCases\MultilineComplex.fs" />
|
||||||
|
<EmbeddedResource Include="SyntaxCases\EdgeCases.fs" />
|
||||||
|
<EmbeddedResource Include="SyntaxCases\CommentsAndSpacing.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -24,6 +42,9 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||||
<PackageReference Include="NUnit" Version="4.3.2"/>
|
<PackageReference Include="NUnit" Version="4.3.2"/>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
213
WoofWare.Expect/AstWalker.fs
Normal file
213
WoofWare.Expect/AstWalker.fs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
namespace WoofWare.Expect
|
||||||
|
|
||||||
|
// This file mostly courtesy of Claude 4 Opus.
|
||||||
|
|
||||||
|
open Fantomas.FCS.Diagnostics
|
||||||
|
open Fantomas.FCS.Syntax
|
||||||
|
open Fantomas.FCS.Text
|
||||||
|
|
||||||
|
type internal SnapshotLocation =
|
||||||
|
{
|
||||||
|
KeywordRange : Range
|
||||||
|
Keyword : string
|
||||||
|
ReplacementRange : Range
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal AstWalker =
|
||||||
|
|
||||||
|
let private snapshotSignifier =
|
||||||
|
[ "snapshot" ; "snapshotJson" ; "snapshotList" ; "snapshotThrows" ]
|
||||||
|
|> Set.ofList
|
||||||
|
|
||||||
|
/// Check if this is a call to snapshotList (or any other snapshot method we care about)
|
||||||
|
/// Returns the identifier that is the snapshot invocation, and its range.
|
||||||
|
let private isSnapshotCall (funcExpr : SynExpr) : (string * Range) option =
|
||||||
|
match funcExpr with
|
||||||
|
| SynExpr.Ident ident when snapshotSignifier.Contains ident.idText -> Some (ident.idText, ident.idRange)
|
||||||
|
| SynExpr.LongIdent (_, longIdent, _, _) ->
|
||||||
|
match longIdent.IdentsWithTrivia with
|
||||||
|
| [] -> None
|
||||||
|
| ids ->
|
||||||
|
match List.last ids with
|
||||||
|
| SynIdent.SynIdent (ident, _) ->
|
||||||
|
if snapshotSignifier.Contains ident.idText then
|
||||||
|
Some (ident.idText, ident.idRange)
|
||||||
|
else
|
||||||
|
None
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
/// Extract the argument from a method application
|
||||||
|
let private getMethodArgument (expr : SynExpr) =
|
||||||
|
match expr with
|
||||||
|
| SynExpr.App (_, _, _, argExpr, _) -> Some argExpr
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
/// Walk expressions looking for our target
|
||||||
|
let rec findSnapshotListCalls (targetLine : int) (methodName : string) (expr : SynExpr) : SnapshotLocation list =
|
||||||
|
match expr with
|
||||||
|
// Direct method application
|
||||||
|
| SynExpr.App (_, _, funcExpr, argExpr, range) ->
|
||||||
|
match isSnapshotCall funcExpr with
|
||||||
|
| Some (keyword, keywordRange) ->
|
||||||
|
if range.StartLine <= targetLine && targetLine <= range.EndLine then
|
||||||
|
match argExpr with
|
||||||
|
| SynExpr.ArrayOrList (isList, _, argRange) when isList ->
|
||||||
|
// It's a list literal
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ReplacementRange = argRange
|
||||||
|
KeywordRange = keywordRange
|
||||||
|
Keyword = keyword
|
||||||
|
}
|
||||||
|
] // Text will be extracted separately
|
||||||
|
| SynExpr.ArrayOrListComputed (isArray, _inner, argRange) when not isArray ->
|
||||||
|
// It's a list comprehension
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ReplacementRange = argRange
|
||||||
|
KeywordRange = keywordRange
|
||||||
|
Keyword = keyword
|
||||||
|
}
|
||||||
|
]
|
||||||
|
| _ ->
|
||||||
|
// It could be a variable reference or other expression
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ReplacementRange = argExpr.Range
|
||||||
|
KeywordRange = keywordRange
|
||||||
|
Keyword = keyword
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
| None ->
|
||||||
|
// Other app variations
|
||||||
|
findSnapshotListCalls targetLine methodName funcExpr
|
||||||
|
@ findSnapshotListCalls targetLine methodName argExpr
|
||||||
|
|
||||||
|
// Nested in paren
|
||||||
|
| SynExpr.Paren (innerExpr, _, _, _) -> findSnapshotListCalls targetLine methodName innerExpr
|
||||||
|
|
||||||
|
// Sequential expressions (e.g., in a do block)
|
||||||
|
| SynExpr.Sequential (_, _, expr1, expr2, _, _) ->
|
||||||
|
findSnapshotListCalls targetLine methodName expr1
|
||||||
|
@ findSnapshotListCalls targetLine methodName expr2
|
||||||
|
|
||||||
|
// Let bindings
|
||||||
|
| SynExpr.LetOrUse (_, _, bindings, bodyExpr, _, _) ->
|
||||||
|
let bindingResults =
|
||||||
|
bindings
|
||||||
|
|> List.collect (fun binding ->
|
||||||
|
match binding with
|
||||||
|
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
|
||||||
|
)
|
||||||
|
|
||||||
|
bindingResults @ findSnapshotListCalls targetLine methodName bodyExpr
|
||||||
|
|
||||||
|
// Match expressions
|
||||||
|
| SynExpr.Match (_, _, clauses, _, _) ->
|
||||||
|
clauses
|
||||||
|
|> List.collect (fun (SynMatchClause (resultExpr = expr)) ->
|
||||||
|
findSnapshotListCalls targetLine methodName expr
|
||||||
|
)
|
||||||
|
|
||||||
|
// If/then/else
|
||||||
|
| SynExpr.IfThenElse (_, thenExpr, elseExprOpt, _, _, _, _) ->
|
||||||
|
let thenResults = findSnapshotListCalls targetLine methodName thenExpr
|
||||||
|
|
||||||
|
let elseResults =
|
||||||
|
match elseExprOpt with
|
||||||
|
| Some elseExpr -> findSnapshotListCalls targetLine methodName elseExpr
|
||||||
|
| None -> []
|
||||||
|
|
||||||
|
thenResults @ elseResults
|
||||||
|
|
||||||
|
// Lambda
|
||||||
|
| SynExpr.Lambda (body = bodyExpr) -> findSnapshotListCalls targetLine methodName bodyExpr
|
||||||
|
|
||||||
|
// Computation expression
|
||||||
|
| SynExpr.ComputationExpr (_, innerExpr, _) -> findSnapshotListCalls targetLine methodName innerExpr
|
||||||
|
|
||||||
|
// Default case - no results
|
||||||
|
| _ -> []
|
||||||
|
|
||||||
|
/// Walk a module or namespace looking for expressions
|
||||||
|
let rec findInModuleDecls (targetLine : int) (methodName : string) (decls : SynModuleDecl list) =
|
||||||
|
decls
|
||||||
|
|> List.collect (fun decl ->
|
||||||
|
match decl with
|
||||||
|
| SynModuleDecl.Let (_, bindings, _) ->
|
||||||
|
bindings
|
||||||
|
|> List.collect (fun binding ->
|
||||||
|
match binding with
|
||||||
|
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
|
||||||
|
)
|
||||||
|
|
||||||
|
| SynModuleDecl.Expr (expr, _) -> findSnapshotListCalls targetLine methodName expr
|
||||||
|
|
||||||
|
| SynModuleDecl.NestedModule (decls = nestedDecls) -> findInModuleDecls targetLine methodName nestedDecls
|
||||||
|
|
||||||
|
| SynModuleDecl.Types (typeDefs, _) ->
|
||||||
|
typeDefs
|
||||||
|
|> List.collect (fun typeDef ->
|
||||||
|
match typeDef with
|
||||||
|
| SynTypeDefn (typeRepr = SynTypeDefnRepr.ObjectModel (members = members)) ->
|
||||||
|
members
|
||||||
|
|> List.collect (fun member' ->
|
||||||
|
match member' with
|
||||||
|
| SynMemberDefn.Member (memberBinding, _) ->
|
||||||
|
match memberBinding with
|
||||||
|
| SynBinding (expr = expr) -> findSnapshotListCalls targetLine methodName expr
|
||||||
|
| _ -> []
|
||||||
|
)
|
||||||
|
| _ -> []
|
||||||
|
)
|
||||||
|
|
||||||
|
| SynModuleDecl.HashDirective _
|
||||||
|
| SynModuleDecl.Attributes _
|
||||||
|
| SynModuleDecl.ModuleAbbrev _
|
||||||
|
| SynModuleDecl.Exception _
|
||||||
|
| SynModuleDecl.Open _ -> []
|
||||||
|
| SynModuleDecl.NamespaceFragment (SynModuleOrNamespace (decls = decls)) ->
|
||||||
|
findInModuleDecls targetLine methodName decls
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Main function to find snapshot list locations
|
||||||
|
let findSnapshotList
|
||||||
|
(infoFilePath : string)
|
||||||
|
(lines : string[])
|
||||||
|
(lineNumber : int)
|
||||||
|
(methodName : string) // e.g., "snapshotList"
|
||||||
|
: SnapshotLocation
|
||||||
|
=
|
||||||
|
let sourceText = SourceText.ofString (String.concat "\n" lines)
|
||||||
|
|
||||||
|
// Parse the file
|
||||||
|
let parsedInput, diagnostics = Fantomas.FCS.Parse.parseFile false sourceText []
|
||||||
|
|
||||||
|
// Check for parse errors
|
||||||
|
if
|
||||||
|
diagnostics
|
||||||
|
|> List.exists (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
|
||||||
|
then
|
||||||
|
failwithf $"Parse errors in file %s{infoFilePath}: %A{diagnostics}"
|
||||||
|
|
||||||
|
// Walk the AST
|
||||||
|
let results =
|
||||||
|
match parsedInput with
|
||||||
|
| ParsedInput.ImplFile (ParsedImplFileInput (contents = modules)) ->
|
||||||
|
modules
|
||||||
|
|> List.collect (fun moduleOrNs ->
|
||||||
|
match moduleOrNs with
|
||||||
|
| SynModuleOrNamespace (decls = decls) -> findInModuleDecls lineNumber methodName decls
|
||||||
|
)
|
||||||
|
| ParsedInput.SigFile _ -> failwith "unexpected: signature files can't contain expressions"
|
||||||
|
|
||||||
|
// Find the closest match
|
||||||
|
results
|
||||||
|
|> Seq.filter (fun loc ->
|
||||||
|
loc.KeywordRange.StartLine <= lineNumber
|
||||||
|
&& lineNumber <= loc.KeywordRange.EndLine
|
||||||
|
)
|
||||||
|
|> Seq.exactlyOne
|
@@ -1,5 +1,6 @@
|
|||||||
namespace WoofWare.Expect
|
namespace WoofWare.Expect
|
||||||
|
|
||||||
|
open System.Collections.Generic
|
||||||
open System.IO
|
open System.IO
|
||||||
open System.Runtime.CompilerServices
|
open System.Runtime.CompilerServices
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
@@ -23,8 +24,6 @@ type Mode =
|
|||||||
/// <param name="applyChanges">When running the tests, instead of throwing an exception on failure, update the snapshot.</param>
|
/// <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>
|
/// <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 (mode : Mode) =
|
type ExpectBuilder (mode : Mode) =
|
||||||
member private this.Mode = Unchecked.defaultof<Mode>
|
|
||||||
|
|
||||||
new (sourceOverride : string * int) = ExpectBuilder (Mode.AssertMockingSource sourceOverride)
|
new (sourceOverride : string * int) = ExpectBuilder (Mode.AssertMockingSource sourceOverride)
|
||||||
|
|
||||||
new (update : bool)
|
new (update : bool)
|
||||||
@@ -34,6 +33,61 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
else
|
else
|
||||||
ExpectBuilder Mode.Assert
|
ExpectBuilder Mode.Assert
|
||||||
|
|
||||||
|
/// Combine two `ExpectStateListy`s. The first one is the "expected" snapshot; the second is the "actual".
|
||||||
|
member _.Bind<'U> (state : ExpectStateListy<'U>, f : unit -> ExpectStateListy<'U>) : ExpectStateListy<'U> =
|
||||||
|
let actual = f ()
|
||||||
|
|
||||||
|
match state.Actual with
|
||||||
|
| Some _ -> failwith "somehow came in with an Actual"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
match actual.Snapshot with
|
||||||
|
| Some _ -> failwith "somehow Actual came through with a Snapshot"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
let formatter =
|
||||||
|
match state.Formatter, actual.Formatter with
|
||||||
|
| None, f -> f
|
||||||
|
| Some f, None -> Some f
|
||||||
|
| Some _, Some _ -> failwith "multiple formatters supplied for a single expect!"
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = formatter
|
||||||
|
Snapshot = state.Snapshot
|
||||||
|
Actual = actual.Actual
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine an `ExpectStateListy` with an `ExpectState`. The first one is the "expected" snapshot; the second is
|
||||||
|
/// the "actual".
|
||||||
|
member _.Bind<'U, 'elt when 'U :> IEnumerable<'elt>>
|
||||||
|
(state : ExpectStateListy<'elt>, f : unit -> ExpectState<'U>)
|
||||||
|
: ExpectStateListy<'elt>
|
||||||
|
=
|
||||||
|
let actual = f ()
|
||||||
|
|
||||||
|
match state.Actual with
|
||||||
|
| Some _ -> failwith "somehow came in with an Actual"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
match actual.Snapshot with
|
||||||
|
| Some _ -> failwith "somehow Actual came through with a Snapshot"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
let formatter : ((unit -> 'elt) -> string) option =
|
||||||
|
match state.Formatter, actual.Formatter with
|
||||||
|
| None, None -> None
|
||||||
|
| None, Some _ ->
|
||||||
|
failwith
|
||||||
|
"unexpectedly had a formatter supplied before the snapshotList keyword; I thought this was impossible"
|
||||||
|
| Some f, None -> Some f
|
||||||
|
| Some _, Some _ -> failwith "multiple formatters supplied for a single expect!"
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = formatter
|
||||||
|
Snapshot = state.Snapshot
|
||||||
|
Actual = actual.Actual |> Option.map (fun f () -> Seq.cast<'elt> (f ()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
|
/// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
|
||||||
member _.Bind<'U> (state : ExpectState<'U>, f : unit -> ExpectState<'U>) : ExpectState<'U> =
|
member _.Bind<'U> (state : ExpectState<'U>, f : unit -> ExpectState<'U>) : ExpectState<'U> =
|
||||||
let actual = f ()
|
let actual = f ()
|
||||||
@@ -73,6 +127,40 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
JsonDocOptions = jsonDocOptions
|
JsonDocOptions = jsonDocOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
|
||||||
|
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.Snapshot<'a>
|
||||||
|
(
|
||||||
|
state : ExpectStateListy<'a>,
|
||||||
|
snapshot : string,
|
||||||
|
[<CallerMemberName>] ?memberName : string,
|
||||||
|
[<CallerLineNumber>] ?callerLine : int,
|
||||||
|
[<CallerFilePath>] ?filePath : string
|
||||||
|
)
|
||||||
|
: ExpectState<'a>
|
||||||
|
=
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ -> failwith "snapshot can only be specified once"
|
||||||
|
| None ->
|
||||||
|
let memberName = defaultArg memberName "<unknown method>"
|
||||||
|
let filePath = defaultArg filePath "<unknown file>"
|
||||||
|
let lineNumber = defaultArg callerLine -1
|
||||||
|
|
||||||
|
let callerInfo =
|
||||||
|
{
|
||||||
|
MemberName = memberName
|
||||||
|
FilePath = filePath
|
||||||
|
LineNumber = lineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = state.Formatter
|
||||||
|
JsonSerialiserOptions = None
|
||||||
|
JsonDocOptions = None
|
||||||
|
Snapshot = Some (SnapshotValue.Formatted snapshot, callerInfo)
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
|
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
|
||||||
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
|
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
member _.Snapshot<'a>
|
member _.Snapshot<'a>
|
||||||
@@ -104,6 +192,46 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
Actual = None
|
Actual = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Express that the actual value, when converted to JSON, should result in a JSON document
|
||||||
|
/// which matches the JSON document that is this string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For example, <c>snapshotJson "123"</c> indicates the JSON integer 123.
|
||||||
|
/// </remarks>
|
||||||
|
[<CustomOperation("snapshotJson", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member this.SnapshotJson<'a>
|
||||||
|
(
|
||||||
|
state : ExpectStateListy<unit>,
|
||||||
|
snapshot : string,
|
||||||
|
[<CallerMemberName>] ?memberName : string,
|
||||||
|
[<CallerLineNumber>] ?callerLine : int,
|
||||||
|
[<CallerFilePath>] ?filePath : string
|
||||||
|
)
|
||||||
|
: ExpectState<'a>
|
||||||
|
=
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ -> failwith "snapshot can only be specified once"
|
||||||
|
| None ->
|
||||||
|
let memberName = defaultArg memberName "<unknown method>"
|
||||||
|
let filePath = defaultArg filePath "<unknown file>"
|
||||||
|
let lineNumber = defaultArg callerLine -1
|
||||||
|
|
||||||
|
let callerInfo =
|
||||||
|
{
|
||||||
|
MemberName = memberName
|
||||||
|
FilePath = filePath
|
||||||
|
LineNumber = lineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = None
|
||||||
|
JsonSerialiserOptions = None
|
||||||
|
JsonDocOptions = None
|
||||||
|
Snapshot = Some (SnapshotValue.Json snapshot, callerInfo)
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Express that the actual value, when converted to JSON, should result in a JSON document
|
/// Express that the actual value, when converted to JSON, should result in a JSON document
|
||||||
/// which matches the JSON document that is this string.
|
/// which matches the JSON document that is this string.
|
||||||
@@ -144,10 +272,138 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
Actual = None
|
Actual = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Express that the actual value, which is a sequence, should have elements which individually (in order) match
|
||||||
|
/// this snapshot list.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For example, <c>snapshotList ["123" ; "456"]</c> indicates an exactly-two-element list <c>[123 ; 456]</c>.
|
||||||
|
/// </remarks>
|
||||||
|
[<CustomOperation("snapshotList", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.SnapshotList<'a>
|
||||||
|
(
|
||||||
|
state : ExpectStateListy<unit>,
|
||||||
|
snapshot : string list,
|
||||||
|
[<CallerMemberName>] ?memberName : string,
|
||||||
|
[<CallerLineNumber>] ?callerLine : int,
|
||||||
|
[<CallerFilePath>] ?filePath : string
|
||||||
|
)
|
||||||
|
: ExpectStateListy<'a>
|
||||||
|
=
|
||||||
|
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ -> failwith "snapshot can only be specified once"
|
||||||
|
| None ->
|
||||||
|
let memberName = defaultArg memberName "<unknown method>"
|
||||||
|
let filePath = defaultArg filePath "<unknown file>"
|
||||||
|
let lineNumber = defaultArg callerLine -1
|
||||||
|
|
||||||
|
let callerInfo =
|
||||||
|
{
|
||||||
|
MemberName = memberName
|
||||||
|
FilePath = filePath
|
||||||
|
LineNumber = lineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = None
|
||||||
|
Snapshot = Some (snapshot, callerInfo)
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expresses that the given expression throws during evaluation.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// expect {
|
||||||
|
/// snapshotThrows @"System.Exception: oh no"
|
||||||
|
/// return! (fun () -> failwith "oh no")
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[<CustomOperation("snapshotThrows", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.SnapshotThrows<'a>
|
||||||
|
(
|
||||||
|
state : ExpectState<'a>,
|
||||||
|
snapshot : string,
|
||||||
|
[<CallerMemberName>] ?memberName : string,
|
||||||
|
[<CallerLineNumber>] ?callerLine : int,
|
||||||
|
[<CallerFilePath>] ?filePath : string
|
||||||
|
)
|
||||||
|
: ExpectState<'a>
|
||||||
|
=
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ -> failwith "snapshot can only be specified once"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
let memberName = defaultArg memberName "<unknown method>"
|
||||||
|
let filePath = defaultArg filePath "<unknown file>"
|
||||||
|
let lineNumber = defaultArg callerLine -1
|
||||||
|
|
||||||
|
let callerInfo =
|
||||||
|
{
|
||||||
|
MemberName = memberName
|
||||||
|
FilePath = filePath
|
||||||
|
LineNumber = lineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = None
|
||||||
|
JsonSerialiserOptions = state.JsonSerialiserOptions
|
||||||
|
JsonDocOptions = state.JsonDocOptions
|
||||||
|
Snapshot = Some (SnapshotValue.ThrowsException snapshot, callerInfo)
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expresses that the given expression throws during evaluation.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// expect {
|
||||||
|
/// snapshotThrows @"System.Exception: oh no"
|
||||||
|
/// return! (fun () -> failwith "oh no")
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[<CustomOperation("snapshotThrows", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.SnapshotThrows<'a>
|
||||||
|
(
|
||||||
|
state : ExpectStateListy<'a>,
|
||||||
|
snapshot : string,
|
||||||
|
[<CallerMemberName>] ?memberName : string,
|
||||||
|
[<CallerLineNumber>] ?callerLine : int,
|
||||||
|
[<CallerFilePath>] ?filePath : string
|
||||||
|
)
|
||||||
|
: ExpectState<'a>
|
||||||
|
=
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ -> failwith "snapshot can only be specified once"
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
let memberName = defaultArg memberName "<unknown method>"
|
||||||
|
let filePath = defaultArg filePath "<unknown file>"
|
||||||
|
let lineNumber = defaultArg callerLine -1
|
||||||
|
|
||||||
|
let callerInfo =
|
||||||
|
{
|
||||||
|
MemberName = memberName
|
||||||
|
FilePath = filePath
|
||||||
|
LineNumber = lineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = None
|
||||||
|
JsonSerialiserOptions = None
|
||||||
|
JsonDocOptions = None
|
||||||
|
Snapshot = Some (SnapshotValue.ThrowsException snapshot, callerInfo)
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
|
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
|
||||||
/// comparing to the snapshot.
|
/// comparing to the snapshot.
|
||||||
/// this value.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>.
|
/// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>.
|
||||||
@@ -158,7 +414,24 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
| Some _ -> failwith "Please don't supply withFormat more than once"
|
| Some _ -> failwith "Please don't supply withFormat more than once"
|
||||||
| None ->
|
| None ->
|
||||||
{ state with
|
{ state with
|
||||||
Formatter = Some formatter
|
Formatter = Some (fun f -> f () |> formatter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Express that the <c>return</c> value of this builder should be formatted using this function, before
|
||||||
|
/// comparing to the snapshot.
|
||||||
|
/// In the case of <c>snapshotList</c>, this applies to the elements of the sequence, not to the sequence itself.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For example, <c>withFormat (fun x -> x.ToString ()) "123"</c> is equivalent to <c>snapshot "123"</c>.
|
||||||
|
/// </remarks>
|
||||||
|
[<CustomOperation("withFormat", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.WithFormat<'T> (state : ExpectStateListy<'T>, formatter : 'T -> string) =
|
||||||
|
match state.Formatter with
|
||||||
|
| Some _ -> failwith "Please don't supply withFormat more than once"
|
||||||
|
| None ->
|
||||||
|
{ state with
|
||||||
|
Formatter = Some (fun f -> f () |> formatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -184,6 +457,42 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
JsonSerialiserOptions = Some jsonOptions
|
JsonSerialiserOptions = Some jsonOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Express that these JsonSerializerOptions should be used to construct the JSON object to which the snapshot
|
||||||
|
/// is to be compared (or, in write-out-the-snapshot mode, to construct the JSON object to be written out).
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// If you want your snapshots to be written out compactly, rather than the default indenting:
|
||||||
|
/// <code>
|
||||||
|
/// expect {
|
||||||
|
/// snapshotJson @"{""a"":3}"
|
||||||
|
/// withJsonSerializerOptions (JsonSerializerOptions (WriteIndented = false))
|
||||||
|
/// return Map.ofList ["a", 3]
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[<CustomOperation("withJsonSerializerOptions", MaintainsVariableSpaceUsingBind = true)>]
|
||||||
|
member _.WithJsonSerializerOptions<'T> (state : ExpectStateListy<'T>, jsonOptions : JsonSerializerOptions) =
|
||||||
|
match state.Snapshot with
|
||||||
|
| Some _ ->
|
||||||
|
failwith
|
||||||
|
"I don't know how you've managed to do this; please raise an issue against github.com/Smaug123/WoofWare.Expect . Somehow withJsonSerializerOptions has already got a snapshot."
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
match state.Actual with
|
||||||
|
| Some _ ->
|
||||||
|
failwith
|
||||||
|
"I don't know how you've managed to do this; please raise an issue against github.com/Smaug123/WoofWare.Expect . Somehow withJsonSerializerOptions has already got an expected-value."
|
||||||
|
| None ->
|
||||||
|
|
||||||
|
{
|
||||||
|
Formatter = state.Formatter
|
||||||
|
JsonSerialiserOptions = Some jsonOptions
|
||||||
|
JsonDocOptions = None
|
||||||
|
Snapshot = None
|
||||||
|
Actual = None
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Express that these JsonDocumentOptions should be used when parsing the snapshot string into a JSON object.
|
/// Express that these JsonDocumentOptions should be used when parsing the snapshot string into a JSON object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -215,17 +524,26 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// MaintainsVariableSpaceUsingBind causes this to be used; it's a dummy representing "no snapshot and no assertion".
|
/// MaintainsVariableSpaceUsingBind causes this to be used; it's a dummy representing "no snapshot and no assertion".
|
||||||
member _.Return (() : unit) : ExpectState<'T> =
|
member _.Return (() : unit) : ExpectStateListy<'T> =
|
||||||
{
|
{
|
||||||
Formatter = None
|
Formatter = None
|
||||||
JsonSerialiserOptions = None
|
|
||||||
JsonDocOptions = None
|
|
||||||
Snapshot = None
|
Snapshot = None
|
||||||
Actual = None
|
Actual = None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value".
|
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value".
|
||||||
member _.Return (value : 'T) : ExpectState<'T> =
|
member _.Return<'T> (value : 'T) : ExpectState<'T> =
|
||||||
|
{
|
||||||
|
Snapshot = None
|
||||||
|
Formatter = None
|
||||||
|
Actual = Some (fun () -> value)
|
||||||
|
JsonSerialiserOptions = None
|
||||||
|
JsonDocOptions = None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expresses the "actual value" component of the assertion "expected snapshot = actual value", but delayed behind
|
||||||
|
/// a function (by contrast with `Return`).
|
||||||
|
member _.ReturnFrom (value : unit -> 'T) : ExpectState<'T> =
|
||||||
{
|
{
|
||||||
Snapshot = None
|
Snapshot = None
|
||||||
Formatter = None
|
Formatter = None
|
||||||
@@ -237,6 +555,56 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
/// Computation expression `Delay`.
|
/// Computation expression `Delay`.
|
||||||
member _.Delay (f : unit -> ExpectState<'T>) : unit -> ExpectState<'T> = f
|
member _.Delay (f : unit -> ExpectState<'T>) : unit -> ExpectState<'T> = f
|
||||||
|
|
||||||
|
/// Computation expression `Delay`.
|
||||||
|
member _.Delay (f : unit -> ExpectStateListy<'T>) : unit -> ExpectStateListy<'T> = f
|
||||||
|
|
||||||
|
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
|
||||||
|
member _.Run (f : unit -> ExpectStateListy<'T>) : unit =
|
||||||
|
let state = f () |> CompletedListSnapshotGeneric.make
|
||||||
|
|
||||||
|
let lines = lazy File.ReadAllLines state.Caller.FilePath
|
||||||
|
|
||||||
|
let listSource =
|
||||||
|
lazy
|
||||||
|
AstWalker.findSnapshotList
|
||||||
|
state.Caller.FilePath
|
||||||
|
(lines.Force ())
|
||||||
|
state.Caller.LineNumber
|
||||||
|
state.Caller.MemberName
|
||||||
|
|
||||||
|
let raiseError expected actual =
|
||||||
|
let diff =
|
||||||
|
Diff.patienceLines (Array.ofList expected) (Array.ofList actual) |> Diff.format
|
||||||
|
|
||||||
|
match mode with
|
||||||
|
| Mode.Assert ->
|
||||||
|
if GlobalBuilderConfig.isBulkUpdateMode () then
|
||||||
|
GlobalBuilderConfig.registerTest (CompletedSnapshot.makeFromAst (listSource.Force ()) state)
|
||||||
|
else
|
||||||
|
$"snapshot mismatch! snapshot at %s{state.Caller.FilePath}:%i{state.Caller.LineNumber} (%s{state.Caller.MemberName}) diff:\n%s{diff}"
|
||||||
|
|> ExpectException
|
||||||
|
|> raise
|
||||||
|
| Mode.AssertMockingSource (mockSource, line) ->
|
||||||
|
$"snapshot mismatch! snapshot at %s{mockSource}:%i{line} (%s{state.Caller.MemberName}) diff:\n%s{diff}"
|
||||||
|
|> ExpectException
|
||||||
|
|> raise
|
||||||
|
| Mode.Update ->
|
||||||
|
let lines = lines.Force ()
|
||||||
|
let listSource = listSource.Force ()
|
||||||
|
let result = SnapshotUpdate.updateAtLocation listSource lines actual
|
||||||
|
File.writeAllLines result state.Caller.FilePath
|
||||||
|
failwith ("Snapshot successfully updated. Previous contents:\n" + String.concat "\n" lines)
|
||||||
|
|
||||||
|
match CompletedListSnapshotGeneric.passesAssertion state with
|
||||||
|
| None ->
|
||||||
|
match mode, GlobalBuilderConfig.isBulkUpdateMode () with
|
||||||
|
| Mode.Update, _
|
||||||
|
| _, true ->
|
||||||
|
failwith
|
||||||
|
"Snapshot assertion passed, but we are in snapshot-updating mode. Use the `expect` builder instead of `expect'` to assert the contents of a single snapshot; disable `GlobalBuilderConfig.bulkUpdate` to move back to assertion-checking mode."
|
||||||
|
| _ -> ()
|
||||||
|
| Some (expected, actual) -> raiseError expected actual
|
||||||
|
|
||||||
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
|
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
|
||||||
member _.Run (f : unit -> ExpectState<'T>) : unit =
|
member _.Run (f : unit -> ExpectState<'T>) : unit =
|
||||||
let state = f () |> CompletedSnapshotGeneric.make
|
let state = f () |> CompletedSnapshotGeneric.make
|
||||||
@@ -244,40 +612,42 @@ type ExpectBuilder (mode : Mode) =
|
|||||||
let raiseError (snapshot : string) (actual : string) : unit =
|
let raiseError (snapshot : string) (actual : string) : unit =
|
||||||
match mode with
|
match mode with
|
||||||
| Mode.AssertMockingSource (mockSource, line) ->
|
| Mode.AssertMockingSource (mockSource, line) ->
|
||||||
|
let diff = Diff.patience snapshot actual
|
||||||
|
|
||||||
sprintf
|
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
|
mockSource
|
||||||
line
|
line
|
||||||
state.Caller.MemberName
|
state.Caller.MemberName
|
||||||
(snapshot |> Text.predent '-')
|
(Diff.format diff)
|
||||||
(actual |> Text.predent '+')
|
|
||||||
|> ExpectException
|
|> ExpectException
|
||||||
|> raise
|
|> raise
|
||||||
| Mode.Assert ->
|
| Mode.Assert ->
|
||||||
if GlobalBuilderConfig.bulkUpdate.Value > 0 then
|
if GlobalBuilderConfig.isBulkUpdateMode () then
|
||||||
GlobalBuilderConfig.registerTest state
|
GlobalBuilderConfig.registerTest (CompletedSnapshot.makeGuess state)
|
||||||
else
|
else
|
||||||
|
let diff = Diff.patience snapshot actual
|
||||||
|
|
||||||
sprintf
|
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.FilePath
|
||||||
state.Caller.LineNumber
|
state.Caller.LineNumber
|
||||||
state.Caller.MemberName
|
state.Caller.MemberName
|
||||||
(snapshot |> Text.predent '-')
|
(Diff.format diff)
|
||||||
(actual |> Text.predent '+')
|
|
||||||
|> ExpectException
|
|> ExpectException
|
||||||
|> raise
|
|> raise
|
||||||
| Mode.Update ->
|
| Mode.Update ->
|
||||||
let lines = File.ReadAllLines state.Caller.FilePath
|
let lines = File.ReadAllLines state.Caller.FilePath
|
||||||
let oldContents = String.concat "\n" lines
|
let oldContents = String.concat "\n" lines
|
||||||
let lines = SnapshotUpdate.updateSnapshotAtLine lines state.Caller.LineNumber actual
|
let lines = SnapshotUpdate.updateSnapshotAtLine lines state.Caller.LineNumber actual
|
||||||
File.WriteAllLines (state.Caller.FilePath, lines)
|
File.writeAllLines lines state.Caller.FilePath
|
||||||
failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents)
|
failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents)
|
||||||
|
|
||||||
match CompletedSnapshotGeneric.passesAssertion state with
|
match CompletedSnapshotGeneric.passesAssertion state with
|
||||||
| None ->
|
| None ->
|
||||||
match mode, GlobalBuilderConfig.bulkUpdate.Value with
|
match mode, GlobalBuilderConfig.isBulkUpdateMode () with
|
||||||
| Mode.Update, _
|
| Mode.Update, _
|
||||||
| _, 1 ->
|
| _, true ->
|
||||||
failwith
|
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; disable `GlobalBuilderConfig.bulkUpdate` to move back to assertion-checking mode."
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
|
@@ -1,12 +1,19 @@
|
|||||||
namespace WoofWare.Expect
|
namespace WoofWare.Expect
|
||||||
|
|
||||||
open System.Threading
|
|
||||||
|
|
||||||
/// Module holding global mutable state controlling the behaviour of WoofWare.Expect
|
/// Module holding global mutable state controlling the behaviour of WoofWare.Expect
|
||||||
/// when running in bulk-update mode.
|
/// when running in bulk-update mode.
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module GlobalBuilderConfig =
|
module GlobalBuilderConfig =
|
||||||
let internal bulkUpdate = ref 0
|
/// All access to the global mutable state locks on this.
|
||||||
|
let private locker = obj ()
|
||||||
|
|
||||||
|
// Global mutable state ensuring there is at most one `enterBulkUpdateMode`/`updateAllSnapshots` pair running at once.
|
||||||
|
let private bulkUpdate = ref 0
|
||||||
|
|
||||||
|
let private allTests : ResizeArray<CompletedSnapshot> = ResizeArray ()
|
||||||
|
|
||||||
|
let internal isBulkUpdateMode () : bool =
|
||||||
|
lock locker (fun () -> bulkUpdate.Value > 0)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Call this to make the <c>expect</c> builder register all tests for bulk update as it runs.
|
/// Call this to make the <c>expect</c> builder register all tests for bulk update as it runs.
|
||||||
@@ -16,11 +23,15 @@ module GlobalBuilderConfig =
|
|||||||
/// The implied global mutable state is liable to interfere with other expect builders in other fixtures otherwise.
|
/// The implied global mutable state is liable to interfere with other expect builders in other fixtures otherwise.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
let enterBulkUpdateMode () =
|
let enterBulkUpdateMode () =
|
||||||
if Interlocked.Increment bulkUpdate <> 1 then
|
lock
|
||||||
failwith
|
locker
|
||||||
"WoofWare.Expect requires bulk updates to happen serially: for example, make the test fixture `[<NonParallelizable>]` if you're using NUnit."
|
(fun () ->
|
||||||
|
if bulkUpdate.Value <> 0 then
|
||||||
|
failwith
|
||||||
|
"WoofWare.Expect requires bulk updates to happen serially: for example, make the test fixture `[<NonParallelizable>]` if you're using NUnit."
|
||||||
|
|
||||||
let private allTests : ResizeArray<CompletedSnapshot> = ResizeArray ()
|
bulkUpdate.Value <- bulkUpdate.Value + 1
|
||||||
|
)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clear the set of failing tests registered by any previous bulk-update runs.
|
/// Clear the set of failing tests registered by any previous bulk-update runs.
|
||||||
@@ -30,23 +41,30 @@ module GlobalBuilderConfig =
|
|||||||
/// You probably don't need to do this, because your test runner is probably tearing down
|
/// 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.
|
/// anyway after the tests have failed; this is mainly here for WoofWare.Expect's own internal testing.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
let clearTests () = lock allTests allTests.Clear
|
let clearTests () = lock locker allTests.Clear
|
||||||
|
|
||||||
let internal registerTest (s : CompletedSnapshotGeneric<'T>) : unit =
|
let internal registerTest (toAdd : CompletedSnapshot) : unit =
|
||||||
let toAdd = s |> CompletedSnapshot.make
|
lock locker (fun () -> allTests.Add toAdd)
|
||||||
lock allTests (fun () -> allTests.Add toAdd)
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For all tests whose failures have already been registered,
|
/// For all tests whose failures have already been registered,
|
||||||
/// transform the files on disk so that the failing snapshots now pass.
|
/// transform the files on disk so that the failing snapshots now pass.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
let updateAllSnapshots () =
|
let updateAllSnapshots () =
|
||||||
let bulkUpdate' = Interlocked.Decrement bulkUpdate
|
// It's OK for this to be called when `enterBulkUpdateMode` has not been called, i.e. when `bulkUpdate` has
|
||||||
|
// value 0. That just means we aren't in bulk-update mode, so we expect the following simply to do nothing.
|
||||||
|
// (This is an expected workflow: we expect users to run `updateAllSnapshots` unconditionally in a
|
||||||
|
// one-time tear-down of the test suite, and they use the one-time setup to control whether any work is actually
|
||||||
|
// performed here.)
|
||||||
|
lock
|
||||||
|
locker
|
||||||
|
(fun () ->
|
||||||
|
let allTests = Seq.toArray allTests
|
||||||
|
|
||||||
try
|
try
|
||||||
if bulkUpdate' = 0 then
|
SnapshotUpdate.updateAll allTests
|
||||||
let allTests = lock allTests (fun () -> Seq.toArray allTests)
|
finally
|
||||||
SnapshotUpdate.updateAll allTests
|
// double acquiring of reentrant lock is OK, we're not switching threads
|
||||||
|
clearTests ()
|
||||||
finally
|
bulkUpdate.Value <- 0
|
||||||
clearTests ()
|
)
|
||||||
|
309
WoofWare.Expect/Diff.fs
Normal file
309
WoofWare.Expect/Diff.fs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
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<'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 : 'line
|
||||||
|
/// Delete this line, which is at this position.
|
||||||
|
| Delete of posA : Position * line : 'line
|
||||||
|
/// Insert this line at the given position.
|
||||||
|
| 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 = Diff'<string>
|
||||||
|
|
||||||
|
/// A match between positions in two sequences
|
||||||
|
type internal LineMatch<'line> =
|
||||||
|
{
|
||||||
|
PosA : Position
|
||||||
|
PosB : Position
|
||||||
|
Line : 'line
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of finding unique lines in a sequence
|
||||||
|
type internal UniqueLines<'line when 'line : comparison> =
|
||||||
|
{
|
||||||
|
/// Map from line content to its position (only for unique lines)
|
||||||
|
LinePositions : Map<'line, Position>
|
||||||
|
/// All line counts (for verification)
|
||||||
|
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 : '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
|
||||||
|
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<'line> array) : LineMatch<'line> 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 []
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
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 []
|
||||||
|
|
||||||
|
/// 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 : 'line array) (b : 'line array) : Diff'<'line> =
|
||||||
|
// 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 |> Diff
|
||||||
|
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' (formatter : 'line -> string) (Diff ops) : string =
|
||||||
|
ops
|
||||||
|
|> List.map (fun op ->
|
||||||
|
match op with
|
||||||
|
| 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 (ops : Diff) : string = format' id ops
|
||||||
|
|
||||||
|
/// Compute diff statistics
|
||||||
|
type internal DiffStats =
|
||||||
|
{
|
||||||
|
Matches : int
|
||||||
|
Deletions : int
|
||||||
|
Insertions : int
|
||||||
|
TotalOperations : int
|
||||||
|
}
|
||||||
|
|
||||||
|
let internal computeStats (ops : DiffOperation<'a> 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
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
namespace WoofWare.Expect
|
namespace WoofWare.Expect
|
||||||
|
|
||||||
|
open System.Collections
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
open System.Text.Json.Serialization
|
open System.Text.Json.Serialization
|
||||||
|
|
||||||
@@ -17,20 +18,30 @@ type CallerInfo =
|
|||||||
type private SnapshotValue =
|
type private SnapshotValue =
|
||||||
| Json of expected : string
|
| Json of expected : string
|
||||||
| Formatted of expected : string
|
| Formatted of expected : string
|
||||||
|
| ThrowsException of expected : string
|
||||||
|
|
||||||
type private CompletedSnapshotValue<'T> =
|
type private CompletedSnapshotValue<'T> =
|
||||||
| Json of expected : string * JsonSerializerOptions * JsonDocumentOptions
|
| Json of expected : string * JsonSerializerOptions * JsonDocumentOptions
|
||||||
| Formatted of expected : string * format : ('T -> string)
|
| Formatted of expected : string * format : ((unit -> 'T) -> string)
|
||||||
|
|
||||||
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
|
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
|
||||||
type ExpectState<'T> =
|
type ExpectState<'T> =
|
||||||
private
|
private
|
||||||
{
|
{
|
||||||
Formatter : ('T -> string) option
|
Formatter : ((unit -> 'T) -> string) option
|
||||||
JsonSerialiserOptions : JsonSerializerOptions option
|
JsonSerialiserOptions : JsonSerializerOptions option
|
||||||
JsonDocOptions : JsonDocumentOptions option
|
JsonDocOptions : JsonDocumentOptions option
|
||||||
Snapshot : (SnapshotValue * CallerInfo) option
|
Snapshot : (SnapshotValue * CallerInfo) option
|
||||||
Actual : 'T option
|
Actual : (unit -> 'T) option
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
|
||||||
|
type ExpectStateListy<'T> =
|
||||||
|
private
|
||||||
|
{
|
||||||
|
Formatter : ((unit -> 'T) -> string) option
|
||||||
|
Snapshot : (string list * CallerInfo) option
|
||||||
|
Actual : (unit -> 'T seq) option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
|
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
|
||||||
@@ -39,7 +50,7 @@ type internal CompletedSnapshotGeneric<'T> =
|
|||||||
{
|
{
|
||||||
SnapshotValue : CompletedSnapshotValue<'T>
|
SnapshotValue : CompletedSnapshotValue<'T>
|
||||||
Caller : CallerInfo
|
Caller : CallerInfo
|
||||||
Actual : 'T
|
Actual : unit -> 'T
|
||||||
}
|
}
|
||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
@@ -68,11 +79,22 @@ module internal CompletedSnapshotGeneric =
|
|||||||
| SnapshotValue.Formatted expected ->
|
| SnapshotValue.Formatted expected ->
|
||||||
let formatter =
|
let formatter =
|
||||||
match state.Formatter with
|
match state.Formatter with
|
||||||
| None -> fun x -> x.ToString ()
|
| None -> fun x -> x().ToString ()
|
||||||
| Some f -> f
|
| Some f -> f
|
||||||
|
|
||||||
CompletedSnapshotValue.Formatted (expected, formatter)
|
CompletedSnapshotValue.Formatted (expected, formatter)
|
||||||
|
|
||||||
|
| SnapshotValue.ThrowsException expected ->
|
||||||
|
CompletedSnapshotValue.Formatted (
|
||||||
|
expected,
|
||||||
|
fun x ->
|
||||||
|
try
|
||||||
|
x () |> ignore
|
||||||
|
"<no exception raised>"
|
||||||
|
with e ->
|
||||||
|
e.GetType().FullName + ": " + e.Message
|
||||||
|
)
|
||||||
|
|
||||||
{
|
{
|
||||||
SnapshotValue = snapshot
|
SnapshotValue = snapshot
|
||||||
Caller = source
|
Caller = source
|
||||||
@@ -84,7 +106,7 @@ module internal CompletedSnapshotGeneric =
|
|||||||
let internal replacement (s : CompletedSnapshotGeneric<'T>) =
|
let internal replacement (s : CompletedSnapshotGeneric<'T>) =
|
||||||
match s.SnapshotValue with
|
match s.SnapshotValue with
|
||||||
| CompletedSnapshotValue.Json (_existing, options, _) ->
|
| CompletedSnapshotValue.Json (_existing, options, _) ->
|
||||||
JsonSerializer.Serialize (s.Actual, options)
|
JsonSerializer.Serialize (s.Actual (), options)
|
||||||
|> JsonDocument.Parse
|
|> JsonDocument.Parse
|
||||||
|> _.RootElement
|
|> _.RootElement
|
||||||
|> _.ToString()
|
|> _.ToString()
|
||||||
@@ -104,7 +126,7 @@ module internal CompletedSnapshotGeneric =
|
|||||||
None
|
None
|
||||||
|
|
||||||
let canonicalActual =
|
let canonicalActual =
|
||||||
JsonSerializer.Serialize (state.Actual, jsonSerOptions) |> JsonDocument.Parse
|
JsonSerializer.Serialize (state.Actual (), jsonSerOptions) |> JsonDocument.Parse
|
||||||
|
|
||||||
match canonicalSnapshot with
|
match canonicalSnapshot with
|
||||||
| None -> Some ("[JSON failed to parse:] " + snapshot, canonicalActual.RootElement.ToString ())
|
| None -> Some ("[JSON failed to parse:] " + snapshot, canonicalActual.RootElement.ToString ())
|
||||||
@@ -114,18 +136,69 @@ module internal CompletedSnapshotGeneric =
|
|||||||
else
|
else
|
||||||
None
|
None
|
||||||
|
|
||||||
/// Represents a snapshot test that has failed and is awaiting update or report to the user.
|
type internal CompletedListSnapshotGeneric<'elt> =
|
||||||
type CompletedSnapshot =
|
private
|
||||||
internal
|
|
||||||
{
|
{
|
||||||
CallerInfo : CallerInfo
|
Expected : string list
|
||||||
Replacement : string
|
Format : 'elt -> string
|
||||||
|
Caller : CallerInfo
|
||||||
|
Actual : unit -> 'elt seq
|
||||||
}
|
}
|
||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module internal CompletedSnapshot =
|
module internal CompletedListSnapshotGeneric =
|
||||||
let make (s : CompletedSnapshotGeneric<'T>) =
|
let replacement (s : CompletedListSnapshotGeneric<'T>) =
|
||||||
|
s.Actual () |> unbox<IEnumerable> |> Seq.cast |> Seq.map s.Format |> Seq.toList
|
||||||
|
|
||||||
|
/// Returns None if the assertion passes, or Some (expected, actual) if the assertion fails.
|
||||||
|
let internal passesAssertion (state : CompletedListSnapshotGeneric<'T>) : (string list * string list) option =
|
||||||
|
let actual =
|
||||||
|
state.Actual ()
|
||||||
|
|> unbox<IEnumerable>
|
||||||
|
|> Seq.cast
|
||||||
|
|> Seq.map state.Format
|
||||||
|
|> Seq.toList
|
||||||
|
|
||||||
|
if state.Expected <> actual then
|
||||||
|
Some (state.Expected, actual)
|
||||||
|
else
|
||||||
|
None
|
||||||
|
|
||||||
|
let make (state : ExpectStateListy<'elt>) : CompletedListSnapshotGeneric<'elt> =
|
||||||
|
match state.Actual with
|
||||||
|
| None -> failwith "expected an assertion, but got none"
|
||||||
|
| Some actual ->
|
||||||
|
|
||||||
|
match state.Snapshot with
|
||||||
|
| None -> failwith "expected a snapshotList, but got none"
|
||||||
|
| Some (snapshot, caller) ->
|
||||||
|
|
||||||
|
let formatter =
|
||||||
|
match state.Formatter with
|
||||||
|
| Some f -> fun x -> f (fun () -> x)
|
||||||
|
| None -> fun x -> x.ToString ()
|
||||||
|
|
||||||
{
|
{
|
||||||
CallerInfo = s.Caller
|
Expected = snapshot
|
||||||
Replacement = CompletedSnapshotGeneric.replacement s
|
Format = formatter
|
||||||
|
Caller = caller
|
||||||
|
Actual = actual
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a snapshot test that has failed and is awaiting update or report to the user.
|
||||||
|
type internal CompletedSnapshot =
|
||||||
|
| GuessString of CallerInfo * replacement : string
|
||||||
|
| Known of CallerInfo * replacement : string list * SnapshotLocation
|
||||||
|
|
||||||
|
member this.CallerInfo =
|
||||||
|
match this with
|
||||||
|
| CompletedSnapshot.GuessString (c, _) -> c
|
||||||
|
| CompletedSnapshot.Known (c, _, _) -> c
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal CompletedSnapshot =
|
||||||
|
let makeGuess (s : CompletedSnapshotGeneric<'T>) =
|
||||||
|
CompletedSnapshot.GuessString (s.Caller, CompletedSnapshotGeneric.replacement s)
|
||||||
|
|
||||||
|
let makeFromAst (source : SnapshotLocation) (s : CompletedListSnapshotGeneric<'elt>) =
|
||||||
|
CompletedSnapshot.Known (s.Caller, CompletedListSnapshotGeneric.replacement s, source)
|
||||||
|
91
WoofWare.Expect/Dot.fs
Normal file
91
WoofWare.Expect/Dot.fs
Normal 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"
|
32
WoofWare.Expect/File.fs
Normal file
32
WoofWare.Expect/File.fs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace WoofWare.Expect
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.IO
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal File =
|
||||||
|
|
||||||
|
/// Standard attempt at an atomic file write.
|
||||||
|
/// It may fail to be atomic if the working directory somehow spans multiple volumes,
|
||||||
|
/// and of course with external network storage all bets are off.
|
||||||
|
let writeAllLines (lines : string[]) (path : string) : unit =
|
||||||
|
let file = FileInfo path
|
||||||
|
|
||||||
|
let tempFile =
|
||||||
|
Path.Combine (file.Directory.FullName, file.Name + "." + Guid.NewGuid().ToString () + ".tmp")
|
||||||
|
|
||||||
|
try
|
||||||
|
File.WriteAllLines (tempFile, lines)
|
||||||
|
// Atomicity guarantees are undocumented, but on Unix this is an atomic `rename` call
|
||||||
|
// https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs#L181
|
||||||
|
// and on Windows this is an atomic ReplaceFile:
|
||||||
|
// https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L92
|
||||||
|
// calls https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ReplaceFile.cs#L12
|
||||||
|
// which calls ReplaceFileW, whose atomicity guarantees are again apparently undocumented,
|
||||||
|
// but 4o-turbo, Opus 4, and Gemini 2.5 Flash all think it's atomic.
|
||||||
|
File.Replace (tempFile, path, null)
|
||||||
|
finally
|
||||||
|
try
|
||||||
|
File.Delete tempFile
|
||||||
|
with _ ->
|
||||||
|
()
|
@@ -15,7 +15,7 @@ type private StringLiteralInfo =
|
|||||||
override this.ToString () =
|
override this.ToString () =
|
||||||
sprintf "%i:%i to %i:%i: %s" this.StartLine this.StartColumn this.EndLine this.EndColumn this.Content
|
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
|
Line : int
|
||||||
Column : int
|
Column : int
|
||||||
@@ -28,8 +28,8 @@ module internal SnapshotUpdate =
|
|||||||
let tripleQuote = "\"\"\""
|
let tripleQuote = "\"\"\""
|
||||||
|
|
||||||
/// Convert a string position to line/column
|
/// Convert a string position to line/column
|
||||||
let private positionToLineColumn (text : string) (offset : int) : Position =
|
let private positionToLineColumn (text : string) (offset : int) : SnapshotPosition =
|
||||||
let rec loop (line : int) (col : int) (totalOffset : int) (i : int) : Position =
|
let rec loop (line : int) (col : int) (totalOffset : int) (i : int) : SnapshotPosition =
|
||||||
if i >= text.Length || totalOffset = offset then
|
if i >= text.Length || totalOffset = offset then
|
||||||
{
|
{
|
||||||
Line = line
|
Line = line
|
||||||
@@ -141,13 +141,14 @@ module internal SnapshotUpdate =
|
|||||||
else
|
else
|
||||||
// We need to include enough lines to capture multi-line strings
|
// We need to include enough lines to capture multi-line strings
|
||||||
// Take a reasonable number of lines after the snapshot line
|
// Take a reasonable number of lines after the snapshot line
|
||||||
let maxLines = min 50 (lines.Length - startIdx)
|
let maxLines = lines.Length - startIdx
|
||||||
let relevantLines = lines |> Array.skip startIdx |> Array.take maxLines
|
let relevantLines = lines |> Array.skip startIdx |> Array.take maxLines
|
||||||
|
|
||||||
let searchText = String.concat "\n" relevantLines
|
let searchText = String.concat "\n" relevantLines
|
||||||
|
|
||||||
// Find snapshot keyword
|
// Find snapshot keyword
|
||||||
let snapshotMatch = Regex.Match (searchText, @"\b(snapshot|snapshotJson)\b")
|
let snapshotMatch =
|
||||||
|
Regex.Match (searchText, @"\b(snapshot|snapshotJson|snapshotThrows)\b")
|
||||||
|
|
||||||
if not snapshotMatch.Success then
|
if not snapshotMatch.Success then
|
||||||
None
|
None
|
||||||
@@ -202,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
|
/// Update the snapshot string with a new value; this doesn't edit the file on disk, but
|
||||||
/// instead returns the new contents.
|
/// instead returns the new contents.
|
||||||
/// We always write single-quoted @-strings for simplicity.
|
/// We always write single-quoted @-strings for simplicity.
|
||||||
let private updateSnapshot (lines : string[]) (info : StringLiteralInfo) (newContent : string) : string[] =
|
let private updateSnapshot (lines : string[]) (info : StringLiteralInfo) (newContent : string) : string[] =
|
||||||
let newString = "@\"" + newContent.Replace ("\"", "\"\"") + "\""
|
let newString = stringLiteral newContent
|
||||||
|
|
||||||
if info.StartLine = info.EndLine then
|
if info.StartLine = info.EndLine then
|
||||||
// Single line update
|
// Single line update
|
||||||
@@ -229,7 +241,7 @@ module internal SnapshotUpdate =
|
|||||||
|
|
||||||
let newLines =
|
let newLines =
|
||||||
if newContent.IndexOf '\n' >= 0 then
|
if newContent.IndexOf '\n' >= 0 then
|
||||||
let split = newContent.Replace("\"", "\"\"").Split ('\n')
|
let split = newContent.Replace("\"", "\"\"").Split '\n'
|
||||||
|
|
||||||
match split with
|
match split with
|
||||||
| [||] -> failwith "expected contents from split string"
|
| [||] -> failwith "expected contents from split string"
|
||||||
@@ -286,6 +298,27 @@ module internal SnapshotUpdate =
|
|||||||
|> Seq.sortByDescending fst
|
|> Seq.sortByDescending fst
|
||||||
|> Seq.fold (fun lines (lineNum, replacement) -> updateSnapshotAtLine lines lineNum replacement) fileLines
|
|> Seq.fold (fun lines (lineNum, replacement) -> updateSnapshotAtLine lines lineNum replacement) fileLines
|
||||||
|
|
||||||
|
let internal updateAtLocation (source : SnapshotLocation) (lines : string[]) (actual : string seq) =
|
||||||
|
let indent = String.replicate source.KeywordRange.StartColumn " "
|
||||||
|
|
||||||
|
[|
|
||||||
|
// Range's lines are one-indexed!
|
||||||
|
lines.[0 .. source.KeywordRange.EndLine - 2]
|
||||||
|
[|
|
||||||
|
lines.[source.KeywordRange.EndLine - 1].Substring (0, source.KeywordRange.EndColumn)
|
||||||
|
+ " ["
|
||||||
|
|]
|
||||||
|
actual |> Seq.map (fun s -> indent + " " + stringLiteral s) |> Array.ofSeq
|
||||||
|
[|
|
||||||
|
indent
|
||||||
|
+ "]"
|
||||||
|
+ lines.[source.ReplacementRange.EndLine - 1].Substring source.ReplacementRange.EndColumn
|
||||||
|
|]
|
||||||
|
lines.[source.ReplacementRange.EndLine ..]
|
||||||
|
|]
|
||||||
|
|> Array.concat
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update every failed snapshot in the input, editing the files on disk.
|
/// Update every failed snapshot in the input, editing the files on disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -295,10 +328,19 @@ module internal SnapshotUpdate =
|
|||||||
|> Seq.iter (fun (callerFile, callers) ->
|
|> Seq.iter (fun (callerFile, callers) ->
|
||||||
let contents = System.IO.File.ReadAllLines callerFile
|
let contents = System.IO.File.ReadAllLines callerFile
|
||||||
|
|
||||||
let sources =
|
let newContents =
|
||||||
callers |> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc.Replacement)
|
callers
|
||||||
|
|> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc)
|
||||||
|
|> 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
|
||||||
|
|
||||||
let newContents = updateAllLines contents sources
|
File.writeAllLines newContents callerFile
|
||||||
|
|
||||||
System.IO.File.WriteAllLines (callerFile, newContents)
|
|
||||||
)
|
)
|
||||||
|
@@ -6,22 +6,94 @@ WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.Expe
|
|||||||
WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder
|
WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder
|
||||||
WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CallerInfo System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CallerInfo System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
WoofWare.Expect.CallerInfo.Equals [method]: (WoofWare.Expect.CallerInfo, System.Collections.IEqualityComparer) -> bool
|
WoofWare.Expect.CallerInfo.Equals [method]: (WoofWare.Expect.CallerInfo, System.Collections.IEqualityComparer) -> bool
|
||||||
WoofWare.Expect.CompletedSnapshot inherit obj, implements WoofWare.Expect.CompletedSnapshot System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CompletedSnapshot System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
WoofWare.Expect.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.CompletedSnapshot.Equals [method]: (WoofWare.Expect.CompletedSnapshot, System.Collections.IEqualityComparer) -> bool
|
WoofWare.Expect.Diff'`1.Equals [method]: ('line WoofWare.Expect.Diff', System.Collections.IEqualityComparer) -> bool
|
||||||
|
WoofWare.Expect.DiffModule inherit obj
|
||||||
|
WoofWare.Expect.DiffModule.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
|
||||||
|
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 inherit obj
|
||||||
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
|
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
|
||||||
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
|
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
|
||||||
WoofWare.Expect.ExpectBuilder..ctor [constructor]: WoofWare.Expect.Mode
|
WoofWare.Expect.ExpectBuilder..ctor [constructor]: WoofWare.Expect.Mode
|
||||||
|
WoofWare.Expect.ExpectBuilder.Bind [method]: ('elt WoofWare.Expect.ExpectStateListy, unit -> #('elt seq) WoofWare.Expect.ExpectState) -> 'elt WoofWare.Expect.ExpectStateListy
|
||||||
WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.Bind [method]: ('U WoofWare.Expect.ExpectStateListy, unit -> 'U WoofWare.Expect.ExpectStateListy) -> 'U WoofWare.Expect.ExpectStateListy
|
||||||
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
|
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
|
||||||
|
WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectStateListy) -> (unit -> 'T WoofWare.Expect.ExpectStateListy)
|
||||||
WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> 'T WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> 'T WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectBuilder.Return [method]: unit -> 'T WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.Return [method]: unit -> 'T WoofWare.Expect.ExpectStateListy
|
||||||
|
WoofWare.Expect.ExpectBuilder.ReturnFrom [method]: (unit -> 'T) -> 'T WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> unit
|
WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> unit
|
||||||
|
WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectStateListy) -> unit
|
||||||
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectStateListy, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: (unit WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: (unit WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: (unit WoofWare.Expect.ExpectStateListy, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.SnapshotList [method]: (unit WoofWare.Expect.ExpectStateListy, string list, string option, int option, string option) -> 'a WoofWare.Expect.ExpectStateListy
|
||||||
|
WoofWare.Expect.ExpectBuilder.SnapshotThrows [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.SnapshotThrows [method]: ('a WoofWare.Expect.ExpectStateListy, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectBuilder.WithFormat [method]: ('T WoofWare.Expect.ExpectState, 'T -> string) -> 'T WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.WithFormat [method]: ('T WoofWare.Expect.ExpectState, 'T -> string) -> 'T WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.WithFormat [method]: ('T WoofWare.Expect.ExpectStateListy, 'T -> string) -> 'T WoofWare.Expect.ExpectStateListy
|
||||||
WoofWare.Expect.ExpectBuilder.WithJsonDocOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonDocumentOptions) -> 'T WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.WithJsonDocOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonDocumentOptions) -> 'T WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState
|
WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectState, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState
|
||||||
|
WoofWare.Expect.ExpectBuilder.WithJsonSerializerOptions [method]: ('T WoofWare.Expect.ExpectStateListy, System.Text.Json.JsonSerializerOptions) -> 'T WoofWare.Expect.ExpectState
|
||||||
WoofWare.Expect.ExpectException inherit System.Exception, implements System.Collections.IStructuralEquatable
|
WoofWare.Expect.ExpectException inherit System.Exception, implements System.Collections.IStructuralEquatable
|
||||||
WoofWare.Expect.ExpectException..ctor [constructor]: string
|
WoofWare.Expect.ExpectException..ctor [constructor]: string
|
||||||
WoofWare.Expect.ExpectException..ctor [constructor]: unit
|
WoofWare.Expect.ExpectException..ctor [constructor]: unit
|
||||||
@@ -29,9 +101,11 @@ WoofWare.Expect.ExpectException.Equals [method]: (System.Exception, System.Colle
|
|||||||
WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
|
WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
|
||||||
WoofWare.Expect.ExpectException.Message [property]: [read-only] string
|
WoofWare.Expect.ExpectException.Message [property]: [read-only] string
|
||||||
WoofWare.Expect.ExpectState`1 inherit obj
|
WoofWare.Expect.ExpectState`1 inherit obj
|
||||||
|
WoofWare.Expect.ExpectStateListy`1 inherit obj
|
||||||
WoofWare.Expect.GlobalBuilderConfig inherit obj
|
WoofWare.Expect.GlobalBuilderConfig inherit obj
|
||||||
WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit
|
WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit
|
||||||
WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit
|
WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit
|
||||||
WoofWare.Expect.GlobalBuilderConfig.updateAllSnapshots [static method]: unit -> 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 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.Mode.Equals [method]: (WoofWare.Expect.Mode, System.Collections.IEqualityComparer) -> bool
|
||||||
|
WoofWare.Expect.pos inherit obj
|
@@ -18,6 +18,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="AssemblyInfo.fs" />
|
<Compile Include="AssemblyInfo.fs" />
|
||||||
<Compile Include="Text.fs" />
|
<Compile Include="Text.fs" />
|
||||||
|
<Compile Include="File.fs" />
|
||||||
|
<Compile Include="Diff.fs" />
|
||||||
|
<Compile Include="Dot.fs" />
|
||||||
|
<Compile Include="AstWalker.fs" />
|
||||||
<Compile Include="Domain.fs" />
|
<Compile Include="Domain.fs" />
|
||||||
<Compile Include="SnapshotUpdate.fs" />
|
<Compile Include="SnapshotUpdate.fs" />
|
||||||
<Compile Include="Config.fs" />
|
<Compile Include="Config.fs" />
|
||||||
@@ -36,8 +40,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- FSharp.SystemTextJson requires at least this version -->
|
<!-- FSharp.SystemTextJson requires at least this version -->
|
||||||
<PackageReference Update="FSharp.Core" Version="4.7.0" />
|
<PackageReference Update="FSharp.Core" Version="8.0.100" />
|
||||||
<PackageReference Include="FSharp.SystemTextJson" Version="1.4.36" />
|
<PackageReference Include="FSharp.SystemTextJson" Version="1.4.36" />
|
||||||
|
<PackageReference Include="Fantomas.FCS" Version="7.0.3" />
|
||||||
<!-- Needed for DeepEquals -->
|
<!-- Needed for DeepEquals -->
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.4",
|
"version": "0.8",
|
||||||
"publicReleaseRefSpec": [
|
"publicReleaseRefSpec": [
|
||||||
"^refs/heads/main$"
|
"^refs/heads/main$"
|
||||||
],
|
],
|
||||||
|
@@ -65,6 +65,8 @@
|
|||||||
pkgs.alejandra
|
pkgs.alejandra
|
||||||
pkgs.nodePackages.markdown-link-check
|
pkgs.nodePackages.markdown-link-check
|
||||||
pkgs.shellcheck
|
pkgs.shellcheck
|
||||||
|
pkgs.xmlstarlet
|
||||||
|
pkgs.graph-easy
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -9,6 +9,11 @@
|
|||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA="
|
"hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "Fantomas.FCS",
|
||||||
|
"version": "7.0.3",
|
||||||
|
"hash": "sha256-BmCUq+ZQ3b25nrMBTc5tcxdO2soryEjNx9Fn/FJpi1c="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "fsharp-analyzers",
|
"pname": "fsharp-analyzers",
|
||||||
"version": "0.31.0",
|
"version": "0.31.0",
|
||||||
@@ -16,8 +21,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pname": "FSharp.Core",
|
"pname": "FSharp.Core",
|
||||||
"version": "4.7.0",
|
"version": "8.0.100",
|
||||||
"hash": "sha256-7aa4bga9XWLkq7J5KXv8Bilf1KGum77lSUqp+ooYIUg="
|
"hash": "sha256-FCjhq+W603ibz9XAA9iH5K6gJhX02/pMHyge6dHb4xs="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pname": "FSharp.Core",
|
"pname": "FSharp.Core",
|
||||||
@@ -59,11 +64,26 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
|
"hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "Microsoft.NETCore.Platforms",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"hash": "sha256-8hLiUKvy/YirCWlFwzdejD2Db3DaXhHxT7GSZx/znJg="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "Microsoft.NETCore.Platforms",
|
"pname": "Microsoft.NETCore.Platforms",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
|
"hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "Microsoft.NETCore.Targets",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"hash": "sha256-0AqQ2gMS8iNlYkrD+BxtIg7cXMnr9xZHtKAuN4bjfaQ="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pname": "Microsoft.NETCore.Targets",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"hash": "sha256-WLsf1NuUfRWyr7C7Rl9jiua9jximnVvzy6nk2D2bVRc="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "Microsoft.Testing.Extensions.Telemetry",
|
"pname": "Microsoft.Testing.Extensions.Telemetry",
|
||||||
"version": "1.5.3",
|
"version": "1.5.3",
|
||||||
@@ -159,11 +179,31 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
|
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "runtime.any.System.Runtime",
|
||||||
|
"version": "4.3.0",
|
||||||
|
"hash": "sha256-qwhNXBaJ1DtDkuRacgHwnZmOZ1u9q7N8j0cWOLYOELM="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pname": "runtime.native.System",
|
||||||
|
"version": "4.3.0",
|
||||||
|
"hash": "sha256-ZBZaodnjvLXATWpXXakFgcy6P+gjhshFXmglrL5xD5Y="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pname": "runtime.unix.System.Private.Uri",
|
||||||
|
"version": "4.3.0",
|
||||||
|
"hash": "sha256-c5tXWhE/fYbJVl9rXs0uHh3pTsg44YD1dJvyOA0WoMs="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Buffers",
|
"pname": "System.Buffers",
|
||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI="
|
"hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Buffers",
|
||||||
|
"version": "4.6.0",
|
||||||
|
"hash": "sha256-c2QlgFB16IlfBms5YLsTCFQ/QeKoS6ph1a9mdRkq/Jc="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Collections.Immutable",
|
"pname": "System.Collections.Immutable",
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -174,6 +214,11 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4="
|
"hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Diagnostics.DiagnosticSource",
|
||||||
|
"version": "8.0.1",
|
||||||
|
"hash": "sha256-zmwHjcJgKcbkkwepH038QhcnsWMJcHys+PEbFGC0Jgo="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Formats.Asn1",
|
"pname": "System.Formats.Asn1",
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
@@ -189,6 +234,11 @@
|
|||||||
"version": "4.2.13",
|
"version": "4.2.13",
|
||||||
"hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg="
|
"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",
|
"pname": "System.IO.FileSystem.AccessControl",
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -204,16 +254,31 @@
|
|||||||
"version": "4.5.5",
|
"version": "4.5.5",
|
||||||
"hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
|
"hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Memory",
|
||||||
|
"version": "4.6.0",
|
||||||
|
"hash": "sha256-OhAEKzUM6eEaH99DcGaMz2pFLG/q/N4KVWqqiBYUOFo="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Numerics.Vectors",
|
"pname": "System.Numerics.Vectors",
|
||||||
"version": "4.4.0",
|
"version": "4.6.0",
|
||||||
"hash": "sha256-auXQK2flL/JpnB/rEcAcUm4vYMCYMEMiWOCAlIaqu2U="
|
"hash": "sha256-fKS3uWQ2HmR69vNhDHqPLYNOt3qpjiWQOXZDHvRE1HU="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Private.Uri",
|
||||||
|
"version": "4.3.0",
|
||||||
|
"hash": "sha256-fVfgcoP4AVN1E5wHZbKBIOPYZ/xBeSIdsNF+bdukIRM="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Reflection.Metadata",
|
"pname": "System.Reflection.Metadata",
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
|
"hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Runtime",
|
||||||
|
"version": "4.3.1",
|
||||||
|
"hash": "sha256-R9T68AzS1PJJ7v6ARz9vo88pKL1dWqLOANg4pkQjkA0="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Runtime.CompilerServices.Unsafe",
|
"pname": "System.Runtime.CompilerServices.Unsafe",
|
||||||
"version": "4.5.3",
|
"version": "4.5.3",
|
||||||
@@ -224,6 +289,11 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
|
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pname": "System.Runtime.CompilerServices.Unsafe",
|
||||||
|
"version": "6.1.0",
|
||||||
|
"hash": "sha256-NyqqpRcHumzSxpsgRDguD5SGwdUNHBbo0OOdzLTIzCU="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pname": "System.Security.AccessControl",
|
"pname": "System.Security.AccessControl",
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
|
Reference in New Issue
Block a user