commit 62ae417701cf91d5f082b9d20138734009e97d16
Author: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Sun Jun 15 22:27:30 2025 +0100
Initial commit
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..5ad47a3
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,18 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "fantomas": {
+ "version": "7.0.2",
+ "commands": [
+ "fantomas"
+ ]
+ },
+ "fsharp-analyzers": {
+ "version": "0.31.0",
+ "commands": [
+ "fsharp-analyzers"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e207c42
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,40 @@
+root=true
+
+[*]
+charset=utf-8
+trim_trailing_whitespace=true
+insert_final_newline=true
+indent_style=space
+indent_size=4
+
+# ReSharper properties
+resharper_xml_indent_size=2
+resharper_xml_max_line_length=100
+resharper_xml_tab_width=2
+
+[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
+indent_style=space
+indent_size=2
+
+[*.{fs,fsi}]
+fsharp_bar_before_discriminated_union_declaration=true
+fsharp_space_before_uppercase_invocation=true
+fsharp_space_before_class_constructor=true
+fsharp_space_before_member=true
+fsharp_space_before_colon=true
+fsharp_space_before_semicolon=true
+fsharp_multiline_bracket_style=aligned
+fsharp_newline_between_type_definition_and_members=true
+fsharp_align_function_signature_to_indentation=true
+fsharp_alternative_long_member_definitions=true
+fsharp_multi_line_lambda_closing_newline=true
+fsharp_experimental_keep_indent_in_branch=true
+fsharp_max_value_binding_width=80
+fsharp_max_record_width=0
+max_line_length=120
+end_of_line=lf
+
+[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
+indent_style=space
+indent_size=2
+tab_width=2
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.fantomasignore b/.fantomasignore
new file mode 100644
index 0000000..9b42106
--- /dev/null
+++ b/.fantomasignore
@@ -0,0 +1 @@
+.direnv/
diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml
new file mode 100644
index 0000000..68f2502
--- /dev/null
+++ b/.github/workflows/dotnet.yaml
@@ -0,0 +1,175 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json
+name: .NET
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ NUGET_XMLDOC_MODE: ''
+ DOTNET_MULTILEVEL_LOOKUP: 0
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Restore dependencies
+ run: nix develop --command dotnet restore
+ - name: Build
+ run: nix develop --command dotnet build --no-restore --configuration Release
+ - name: Test
+ run: nix develop --command dotnet test
+
+ build-nix:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Build
+ run: nix build
+ - name: Reproducibility check
+ run: nix build --rebuild
+
+ check-dotnet-format:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run Fantomas
+ run: nix run .#fantomas -- --check .
+
+ check-nix-format:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run Alejandra
+ run: nix develop --command alejandra --check .
+
+ flake-check:
+ name: Check flake
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Flake check
+ run: nix flake check
+
+ nuget-pack:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
+ - name: Install Nix
+ uses: cachix/install-nix-action@v31
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Restore dependencies
+ run: nix develop --command dotnet restore
+ - name: Build
+ run: nix develop --command dotnet build --no-restore --configuration Release
+ - name: Pack
+ run: nix develop --command dotnet pack --configuration Release
+ - name: Upload NuGet artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-package
+ path: WoofWare.Expect/bin/Release/WoofWare.Expect.*.nupkg
+
+ expected-pack:
+ needs: [nuget-pack]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download NuGet artifact (plugin)
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ path: packed
+ - name: Check NuGet contents
+ # Verify that there is exactly one nupkg in the artifact that would be NuGet published
+ run: if [[ $(find packed -maxdepth 1 -name 'WoofWare.Expect.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
+
+ github-release-dry-run:
+ runs-on: ubuntu-latest
+ needs: [nuget-pack]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ path: packed
+ - name: Compute package path
+ id: compute-path
+ run: |
+ find . -maxdepth 1 -type f -name 'WoofWare.Expect.*.nupkg' -exec sh -c 'echo "output=$(basename "$1")" >> $GITHUB_OUTPUT' shell {} \;
+ - name: Compute tag name
+ id: compute-tag
+ env:
+ NUPKG_PATH: ${{ steps.compute-path.outputs.output }}
+ run: echo "output=$(basename "$NUPKG_PATH" .nupkg)" >> $GITHUB_OUTPUT
+ - name: Tag and release
+ uses: G-Research/common-actions/github-release@19d7281a0f9f83e13c78f99a610dbc80fc59ba3b
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ target-commitish: ${{ github.sha }}
+ tag: ${{ steps.compute-tag.outputs.output }}
+ binary-contents: ${{ steps.compute-path.outputs.output }}
+ dry-run: true
+
+ linkcheck:
+ name: Check links
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Install Nix
+ uses: cachix/install-nix-action@v31
+ with:
+ extra_nix_config: |
+ access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
+ - name: Run link checker
+ run: nix develop --command markdown-link-check README.md
+
+ all-required-checks-complete:
+ if: ${{ always() }}
+ needs: [check-dotnet-format, check-nix-format, build, build-nix, flake-check, expected-pack, linkcheck]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: G-Research/common-actions/check-required-lite@2b7dc49cb14f3344fbe6019c14a31165e258c059
+ with:
+ needs-context: ${{ toJSON(needs) }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8947337
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
+.idea/
+*.sln.DotSettings.user
+.DS_Store
+result
+.analyzerpackages/
+analysis.sarif
+.direnv/
+.venv/
+.vs/
+
+.fake
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..f0daa0b
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,19 @@
+
+
+ embedded
+ true
+ [UNDEFINED]
+ true
+ true
+ true
+ embedded
+ FS3388,FS3559
+
+
+
+
+
+
+ true
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6c6c4f6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Patrick Stevens
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..08e4298
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# WoofWare.Expect
+
+An [expect-testing](https://blog.janestreet.com/the-joy-of-expect-tests/) library for F#.
+(Also known as "snapshot testing".)
+
+# Current status
+
+Basic mechanism works, but I haven't yet decided how the ergonomic updating of the input text will work.
+Ideally it would edit the input AST, but I don't yet know if that's viable.
+
+# How to use
+
+See [the tests](./WoofWare.Expect.Test/SimpleTest.fs).
+
+```fsharp
+[]
+let ``This test fails: JSON documents are not equal`` () =
+ expect {
+ snapshotJson "123"
+ return 124
+ }
+
+[]
+let ``This test passes: JSON documents are equal`` () =
+ expect {
+ snapshotJson " 123 "
+ return 123
+ }
+
+[]
+let ``This test fails: plain text comparison of ToString`` () =
+ expect {
+ snapshot " 123 "
+ return 123
+ }
+```
+
+# Licence
+
+MIT.
diff --git a/WoofWare.Expect.Test/SimpleTest.fs b/WoofWare.Expect.Test/SimpleTest.fs
new file mode 100644
index 0000000..545a548
--- /dev/null
+++ b/WoofWare.Expect.Test/SimpleTest.fs
@@ -0,0 +1,37 @@
+namespace WoofWare.Expect.Test
+
+open System
+open WoofWare.Expect
+open NUnit.Framework
+
+[]
+module SimpleTest =
+ []
+ let ``JSON is resilient to whitespace changes`` () =
+ expect {
+ snapshotJson "123 "
+ return 123
+ }
+
+ []
+ let ``Example of a failing test`` () =
+ expect {
+ snapshot
+ "snapshot mismatch! snapshot at filepath.fs:32 (Example of a failing test) was:
+
+- 123
+
+actual was:
+
++ 124"
+
+ return
+ Assert
+ .Throws(fun () ->
+ expectWithMockedFilePath "filepath.fs" {
+ snapshot "123"
+ return 124
+ }
+ )
+ .Message
+ }
diff --git a/WoofWare.Expect.Test/TestSurface.fs b/WoofWare.Expect.Test/TestSurface.fs
new file mode 100644
index 0000000..787ec73
--- /dev/null
+++ b/WoofWare.Expect.Test/TestSurface.fs
@@ -0,0 +1,24 @@
+namespace WoofWare.Expect.Test
+
+open ApiSurface
+open NUnit.Framework
+
+[]
+module TestSurface =
+ let assembly = typeof.Assembly
+
+ []
+ let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
+
+ []
+ let ``Update API surface`` () =
+ ApiSurface.writeAssemblyBaseline assembly
+
+ []
+ let ``Ensure public API is fully documented`` () =
+ DocCoverage.assertFullyDocumented assembly
+
+ []
+ // https://github.com/nunit/nunit3-vs-adapter/issues/876
+ let ``EnsureVersionIsMonotonic`` () =
+ MonotonicVersion.validate assembly "WoofWare.Expect"
diff --git a/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj b/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj
new file mode 100644
index 0000000..9421e76
--- /dev/null
+++ b/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj
@@ -0,0 +1,25 @@
+
+
+
+ net9.0
+ latest
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WoofWare.Expect.sln b/WoofWare.Expect.sln
new file mode 100644
index 0000000..32a1a3a
--- /dev/null
+++ b/WoofWare.Expect.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Expect", "WoofWare.Expect\WoofWare.Expect.fsproj", "{01AADDA1-6A03-4C25-86DC-0FD1DB7A2187}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Expect.Test", "WoofWare.Expect.Test\WoofWare.Expect.Test.fsproj", "{980C24F9-41E8-4A8E-8F02-7D3B7C4A3FDD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {01AADDA1-6A03-4C25-86DC-0FD1DB7A2187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {01AADDA1-6A03-4C25-86DC-0FD1DB7A2187}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {01AADDA1-6A03-4C25-86DC-0FD1DB7A2187}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {01AADDA1-6A03-4C25-86DC-0FD1DB7A2187}.Release|Any CPU.Build.0 = Release|Any CPU
+ {980C24F9-41E8-4A8E-8F02-7D3B7C4A3FDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {980C24F9-41E8-4A8E-8F02-7D3B7C4A3FDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {980C24F9-41E8-4A8E-8F02-7D3B7C4A3FDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {980C24F9-41E8-4A8E-8F02-7D3B7C4A3FDD}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/WoofWare.Expect/Library.fs b/WoofWare.Expect/Library.fs
new file mode 100644
index 0000000..12cec80
--- /dev/null
+++ b/WoofWare.Expect/Library.fs
@@ -0,0 +1,203 @@
+namespace WoofWare.Expect
+
+open System.Runtime.CompilerServices
+open System.Text.Json
+open System.Text.Json.Serialization
+
+type private CallerInfo =
+ {
+ MemberName : string
+ FilePath : string
+ LineNumber : int
+ }
+
+type private SnapshotValue =
+ | BareString of string
+ | Json of string
+
+/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
+type ExpectState<'T> =
+ private
+ {
+ Snapshot : (SnapshotValue * CallerInfo) option
+ Actual : 'T option
+ }
+
+[]
+module private Text =
+ let predent (c : char) (s : string) =
+ s.Split '\n' |> Seq.map (sprintf "%c %s" c) |> String.concat "\n"
+
+///
+/// The builder which powers WoofWare.Expect.
+///
+/// You're not expected to construct this explicitly; it's a computation expression, available as Builder.expect.
+/// Override the file paths reported in snapshots, so that your tests can be fully stable even on failure. (You almost certainly don't want to set this.)
+type ExpectBuilder (?filePathOverride : string) =
+ /// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
+ member _.Bind (state : ExpectState, f : unit -> ExpectState<'U>) : ExpectState<'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 ->
+
+ // Pass through the state structure when there's no actual value
+ {
+ Snapshot = state.Snapshot
+ Actual = actual.Actual
+ }
+
+ /// Express that the actual value's ToString should identically equal this string.
+ []
+ member _.Snapshot
+ (
+ state : ExpectState<'a>,
+ snapshot : string,
+ [] ?memberName : string,
+ [] ?callerLine : int,
+ [] ?filePath : string
+ )
+ : ExpectState<'a>
+ =
+ match state.Snapshot with
+ | Some _ -> failwith "snapshot can only be specified once"
+ | None ->
+ let memberName = defaultArg memberName ""
+ let filePath = defaultArg filePath ""
+ let lineNumber = defaultArg callerLine -1
+
+ let callerInfo =
+ {
+ MemberName = memberName
+ FilePath = filePath
+ LineNumber = lineNumber
+ }
+
+ { state with
+ Snapshot = Some (SnapshotValue.BareString snapshot, callerInfo)
+ }
+
+ ///
+ /// Express that the actual value, when converted to JSON, should result in a JSON document
+ /// which matches the JSON document that is this string.
+ ///
+ ///
+ /// For example, snapshot "123" indicates the JSON integer 123.
+ ///
+ []
+ member _.SnapshotJson
+ (
+ state : ExpectState<'a>,
+ snapshot : string,
+ [] ?memberName : string,
+ [] ?callerLine : int,
+ [] ?filePath : string
+ )
+ : ExpectState<'a>
+ =
+ match state.Snapshot with
+ | Some _ -> failwith "snapshot can only be specified once"
+ | None ->
+ let memberName = defaultArg memberName ""
+ let filePath = defaultArg filePath ""
+ let lineNumber = defaultArg callerLine -1
+
+ let callerInfo =
+ {
+ MemberName = memberName
+ FilePath = filePath
+ LineNumber = lineNumber
+ }
+
+ { state with
+ Snapshot = Some (SnapshotValue.Json snapshot, callerInfo)
+ }
+
+ /// MaintainsVariableSpaceUsingBind causes this to be used; it's a dummy representing "no snapshot and no assertion".
+ member _.Return (() : unit) : ExpectState<'T> =
+ {
+ Snapshot = None
+ Actual = None
+ }
+
+ /// Expresses the "actual value" component of the assertion "expected snapshot = actual value".
+ member _.Return (value : 'T) : ExpectState<'T> =
+ {
+ Snapshot = None
+ Actual = Some value
+ }
+
+ /// Computation expression `Delay`.
+ member _.Delay (f : unit -> ExpectState<'T>) : unit -> ExpectState<'T> = f
+
+ /// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
+ member _.Run (f : unit -> ExpectState<'T>) : unit =
+ let state = f ()
+
+ let options = JsonFSharpOptions.Default().ToJsonSerializerOptions ()
+
+ match state.Snapshot, state.Actual with
+ | Some (snapshot, source), Some actual ->
+ match snapshot with
+ | SnapshotValue.Json snapshot ->
+ let canonicalSnapshot = JsonDocument.Parse snapshot
+
+ let canonicalActual =
+ JsonSerializer.Serialize (actual, options) |> JsonDocument.Parse
+
+ if not (JsonElement.DeepEquals (canonicalActual.RootElement, canonicalSnapshot.RootElement)) then
+ failwithf
+ "snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
+ (filePathOverride |> Option.defaultValue source.FilePath)
+ source.LineNumber
+ source.MemberName
+ (canonicalSnapshot.RootElement.ToString () |> Text.predent '-')
+ (canonicalActual.RootElement.ToString () |> Text.predent '-')
+
+ | SnapshotValue.BareString snapshot ->
+ let actual = actual.ToString ()
+
+ if actual <> snapshot then
+ failwithf
+ "snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
+ (filePathOverride |> Option.defaultValue source.FilePath)
+ source.LineNumber
+ source.MemberName
+ (snapshot |> Text.predent '-')
+ (actual |> Text.predent '+')
+
+ | None, _ -> failwith "Must specify snapshot"
+ | _, None -> failwith "Must specify actual value with 'return'"
+
+/// Module containing the `expect` builder.
+[]
+module Builder =
+ /// The primary WoofWare.Expect builder.
+ ///
+ ///
+ /// You are expected to use this like so:
+ ///
+ ///
+ /// expect {
+ /// snapshot "123"
+ /// return 124
+ /// }
+ ///
+ ///
+ /// (That example expectation will fail, because the actual value 124 does not snapshot to the expected snapshot "123".)
+ ///
+ let expect = ExpectBuilder ()
+
+ ///
+ /// This is the `expect` builder, but it mocks out the filepath reported on failure.
+ ///
+ ///
+ /// You probably don't want to use this; use `expect` instead.
+ /// The point of the mocked builder is to allow fully predictable testing of the WoofWare.Expect library itself.
+ ///
+ let expectWithMockedFilePath (path : string) = ExpectBuilder path
diff --git a/WoofWare.Expect/SurfaceBaseline.txt b/WoofWare.Expect/SurfaceBaseline.txt
new file mode 100644
index 0000000..0312b42
--- /dev/null
+++ b/WoofWare.Expect/SurfaceBaseline.txt
@@ -0,0 +1,15 @@
+WoofWare.Expect.Builder inherit obj
+WoofWare.Expect.Builder.expect [static property]: [read-only] WoofWare.Expect.ExpectBuilder
+WoofWare.Expect.Builder.expectWithMockedFilePath [static method]: string -> WoofWare.Expect.ExpectBuilder
+WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.ExpectBuilder
+WoofWare.Expect.ExpectBuilder inherit obj
+WoofWare.Expect.ExpectBuilder..ctor [constructor]: string option
+WoofWare.Expect.ExpectBuilder.Bind [method]: (unit WoofWare.Expect.ExpectState, unit -> 'U WoofWare.Expect.ExpectState) -> 'U WoofWare.Expect.ExpectState
+WoofWare.Expect.ExpectBuilder.Delay [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> (unit -> 'T WoofWare.Expect.ExpectState)
+WoofWare.Expect.ExpectBuilder.Return [method]: 'T -> 'T WoofWare.Expect.ExpectState
+WoofWare.Expect.ExpectBuilder.Return [method]: unit -> 'T WoofWare.Expect.ExpectState
+WoofWare.Expect.ExpectBuilder.Run [method]: (unit -> 'T WoofWare.Expect.ExpectState) -> unit
+WoofWare.Expect.ExpectBuilder.Snapshot [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
+WoofWare.Expect.ExpectBuilder.SnapshotJson [method]: ('a WoofWare.Expect.ExpectState, string, string option, int option, string option) -> 'a WoofWare.Expect.ExpectState
+WoofWare.Expect.ExpectState`1 inherit obj, implements 'T WoofWare.Expect.ExpectState System.IEquatable, System.Collections.IStructuralEquatable, 'T WoofWare.Expect.ExpectState System.IComparable, System.IComparable, System.Collections.IStructuralComparable
+WoofWare.Expect.ExpectState`1.Equals [method]: ('T WoofWare.Expect.ExpectState, System.Collections.IEqualityComparer) -> bool
\ No newline at end of file
diff --git a/WoofWare.Expect/WoofWare.Expect.fsproj b/WoofWare.Expect/WoofWare.Expect.fsproj
new file mode 100644
index 0000000..7070e44
--- /dev/null
+++ b/WoofWare.Expect/WoofWare.Expect.fsproj
@@ -0,0 +1,40 @@
+
+
+
+ netstandard2.0
+ true
+ Patrick Stevens
+ Copyright (c) Patrick Stevens 2025
+ Snapshot/expect testing framework for F#
+ git
+ https://github.com/Smaug123/WoofWare.Expect
+ MIT
+ README.md
+ logo.png
+ fsharp;snapshot;expect;test;testing;jest
+ true
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WoofWare.Expect/version.json b/WoofWare.Expect/version.json
new file mode 100644
index 0000000..31de0e0
--- /dev/null
+++ b/WoofWare.Expect/version.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.1",
+ "publicReleaseRefSpec": [
+ "^refs/heads/main$"
+ ],
+ "pathFilters": [
+ ":/WoofWare.Expect/",
+ ":/README.md",
+ ":/Directory.Build.props",
+ ":/LICENSE"
+ ]
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..e3369ed
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1749903597,
+ "narHash": "sha256-jp0D4vzBcRKwNZwfY4BcWHemLGUs4JrS3X9w5k/JYDA=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "41da1e3ea8e23e094e5e3eeb1e6b830468a7399e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..22dea7d
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,71 @@
+{
+ description = "Expect testing framework";
+
+ inputs = {
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ };
+
+ outputs = {
+ nixpkgs,
+ flake-utils,
+ ...
+ }:
+ flake-utils.lib.eachDefaultSystem (system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ pname = "WoofWare.Expect";
+ dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
+ dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
+ version = "0.1";
+ dotnetTool = dllOverride: toolName: toolVersion: hash:
+ pkgs.stdenvNoCC.mkDerivation rec {
+ name = toolName;
+ version = toolVersion;
+ nativeBuildInputs = [pkgs.makeWrapper];
+ src = pkgs.fetchNuGet {
+ pname = name;
+ version = version;
+ hash = hash;
+ installPhase = ''mkdir -p $out/bin && cp -r tools/net*/any/* $out/bin'';
+ };
+ installPhase = let
+ dll =
+ if isNull dllOverride
+ then name
+ else dllOverride;
+ in
+ # fsharp-analyzers requires the .NET SDK at runtime, so we use that instead of dotnet-runtime.
+ ''
+ runHook preInstall
+ mkdir -p "$out/lib"
+ cp -r ./bin/* "$out/lib"
+ makeWrapper "${dotnet-sdk}/bin/dotnet" "$out/bin/${name}" --set DOTNET_HOST_PATH "${dotnet-sdk}/bin/dotnet" --add-flags "$out/lib/${dll}.dll"
+ runHook postInstall
+ '';
+ };
+ in {
+ packages = let
+ deps = builtins.fromJSON (builtins.readFile ./nix/deps.json);
+ in {
+ fantomas = dotnetTool null "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version (builtins.head (builtins.filter (elem: elem.pname == "fantomas") deps)).hash;
+ fsharp-analyzers = dotnetTool "FSharp.Analyzers.Cli" "fsharp-analyzers" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fsharp-analyzers.version (builtins.head (builtins.filter (elem: elem.pname == "fsharp-analyzers") deps)).hash;
+ default = pkgs.buildDotnetModule {
+ inherit pname version dotnet-sdk dotnet-runtime;
+ name = "WoofWare.Expect";
+ src = ./.;
+ projectFile = "./WoofWare.Expect/WoofWare.Expect.fsproj";
+ testProjectFile = "./WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj";
+ nugetDeps = ./nix/deps.json; # `nix build .#default.fetch-deps && ./result nix/deps.json`
+ doCheck = true;
+ };
+ };
+ devShell = pkgs.mkShell {
+ buildInputs = [dotnet-sdk];
+ packages = [
+ pkgs.alejandra
+ pkgs.nodePackages.markdown-link-check
+ pkgs.shellcheck
+ ];
+ };
+ });
+}
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..8c79ba3
Binary files /dev/null and b/logo.png differ
diff --git a/nix/deps.json b/nix/deps.json
new file mode 100644
index 0000000..368cc1d
--- /dev/null
+++ b/nix/deps.json
@@ -0,0 +1,267 @@
+[
+ {
+ "pname": "ApiSurface",
+ "version": "4.1.20",
+ "hash": "sha256-koWgO9FC9ax+Ij56ug8kxeyknl0yhLqnNLOUdxtqqo4="
+ },
+ {
+ "pname": "fantomas",
+ "version": "7.0.2",
+ "hash": "sha256-BAaENIm/ksTiXrUImRgKoIXTGIlgsX7ch6ayoFjhJXA="
+ },
+ {
+ "pname": "fsharp-analyzers",
+ "version": "0.31.0",
+ "hash": "sha256-PoAvaXbXsmvVw870UsnqdD20HoBHO7u4bzoaz5DXfzM="
+ },
+ {
+ "pname": "FSharp.Core",
+ "version": "4.7.0",
+ "hash": "sha256-7aa4bga9XWLkq7J5KXv8Bilf1KGum77lSUqp+ooYIUg="
+ },
+ {
+ "pname": "FSharp.Core",
+ "version": "9.0.202",
+ "hash": "sha256-64Gub0qemmCoMa1tDus6TeTuB1+5sHfE6KD2j4o84mA="
+ },
+ {
+ "pname": "FSharp.SystemTextJson",
+ "version": "1.4.36",
+ "hash": "sha256-zZEhjP0mdc5E3fBPS4/lqD7sxoaoT5SOspP546RWYdc="
+ },
+ {
+ "pname": "Microsoft.ApplicationInsights",
+ "version": "2.22.0",
+ "hash": "sha256-mUQ63atpT00r49ca50uZu2YCiLg3yd6r3HzTryqcuEA="
+ },
+ {
+ "pname": "Microsoft.Bcl.AsyncInterfaces",
+ "version": "9.0.0",
+ "hash": "sha256-BsXNOWEgfFq3Yz7VTtK6m/ov4/erRqyBzieWSIpmc1U="
+ },
+ {
+ "pname": "Microsoft.CodeCoverage",
+ "version": "17.14.1",
+ "hash": "sha256-f8QytG8GvRoP47rO2KEmnDLxIpyesaq26TFjDdW40Gs="
+ },
+ {
+ "pname": "Microsoft.NET.Test.Sdk",
+ "version": "17.14.1",
+ "hash": "sha256-mZUzDFvFp7x1nKrcnRd0hhbNu5g8EQYt8SKnRgdhT/A="
+ },
+ {
+ "pname": "Microsoft.NETCore.Platforms",
+ "version": "1.1.0",
+ "hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
+ },
+ {
+ "pname": "Microsoft.NETCore.Platforms",
+ "version": "2.0.0",
+ "hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
+ },
+ {
+ "pname": "Microsoft.Testing.Extensions.Telemetry",
+ "version": "1.5.3",
+ "hash": "sha256-bIXwPSa3jkr2b6xINOqMUs6/uj/r4oVFM7xq3uVIZDU="
+ },
+ {
+ "pname": "Microsoft.Testing.Extensions.TrxReport.Abstractions",
+ "version": "1.5.3",
+ "hash": "sha256-IfMRfcyaIKEMRtx326ICKtinDBEfGw/Sv8ZHawJ96Yc="
+ },
+ {
+ "pname": "Microsoft.Testing.Extensions.VSTestBridge",
+ "version": "1.5.3",
+ "hash": "sha256-XpM/yFjhLSsuzyDV+xKubs4V1zVVYiV05E0+N4S1h0g="
+ },
+ {
+ "pname": "Microsoft.Testing.Platform",
+ "version": "1.5.3",
+ "hash": "sha256-y61Iih6w5D79dmrj2V675mcaeIiHoj1HSa1FRit2BLM="
+ },
+ {
+ "pname": "Microsoft.Testing.Platform.MSBuild",
+ "version": "1.5.3",
+ "hash": "sha256-YspvjE5Jfi587TAfsvfDVJXNrFOkx1B3y1CKV6m7YLY="
+ },
+ {
+ "pname": "Microsoft.TestPlatform.ObjectModel",
+ "version": "17.12.0",
+ "hash": "sha256-3XBHBSuCxggAIlHXmKNQNlPqMqwFlM952Av6RrLw1/w="
+ },
+ {
+ "pname": "Microsoft.TestPlatform.ObjectModel",
+ "version": "17.14.1",
+ "hash": "sha256-QMf6O+w0IT+16Mrzo7wn+N20f3L1/mDhs/qjmEo1rYs="
+ },
+ {
+ "pname": "Microsoft.TestPlatform.TestHost",
+ "version": "17.14.1",
+ "hash": "sha256-1cxHWcvHRD7orQ3EEEPPxVGEkTpxom1/zoICC9SInJs="
+ },
+ {
+ "pname": "Nerdbank.GitVersioning",
+ "version": "3.8.38-alpha",
+ "hash": "sha256-gPMrVbjOZxXoofczF/pn6eVkLhjVSJIyQrLO2oljrDc="
+ },
+ {
+ "pname": "NETStandard.Library",
+ "version": "2.0.3",
+ "hash": "sha256-Prh2RPebz/s8AzHb2sPHg3Jl8s31inv9k+Qxd293ybo="
+ },
+ {
+ "pname": "Newtonsoft.Json",
+ "version": "13.0.3",
+ "hash": "sha256-hy/BieY4qxBWVVsDqqOPaLy1QobiIapkbrESm6v2PHc="
+ },
+ {
+ "pname": "NuGet.Common",
+ "version": "6.13.2",
+ "hash": "sha256-ASLa/Jigg5Eop0ZrXPl98RW2rxnJRC7pbbxhuV74hFw="
+ },
+ {
+ "pname": "NuGet.Configuration",
+ "version": "6.13.2",
+ "hash": "sha256-z8VW1YdRDanyyRTDYRvRkSv/XPR3c/hMM1y8cNNjx0Y="
+ },
+ {
+ "pname": "NuGet.Frameworks",
+ "version": "6.13.2",
+ "hash": "sha256-caDyc+WgYOo43AUTjtbP0MyvYDb6JweEKDdIul61Cac="
+ },
+ {
+ "pname": "NuGet.Packaging",
+ "version": "6.13.2",
+ "hash": "sha256-lhO+SFwIYZ4aPHxIGm5ubkkE2a5Ve2xgtroRbNh7hpw="
+ },
+ {
+ "pname": "NuGet.Protocol",
+ "version": "6.13.2",
+ "hash": "sha256-5lnAHHZjy7A4vgv65AeBAs64mSNpuoUjxW3HnrMpuzY="
+ },
+ {
+ "pname": "NuGet.Versioning",
+ "version": "6.13.2",
+ "hash": "sha256-gmpyBpKnt+GHqgx/2uFKp+J2csbxEAy1E7WdVT117sw="
+ },
+ {
+ "pname": "NUnit",
+ "version": "4.3.2",
+ "hash": "sha256-0RWe8uFoxYp6qhPlDDEghOMcKJgyw2ybvEoAqBLebeE="
+ },
+ {
+ "pname": "NUnit3TestAdapter",
+ "version": "5.0.0",
+ "hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
+ },
+ {
+ "pname": "System.Buffers",
+ "version": "4.5.1",
+ "hash": "sha256-wws90sfi9M7kuCPWkv1CEYMJtCqx9QB/kj0ymlsNaxI="
+ },
+ {
+ "pname": "System.Collections.Immutable",
+ "version": "8.0.0",
+ "hash": "sha256-F7OVjKNwpqbUh8lTidbqJWYi476nsq9n+6k0+QVRo3w="
+ },
+ {
+ "pname": "System.Diagnostics.DiagnosticSource",
+ "version": "5.0.0",
+ "hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4="
+ },
+ {
+ "pname": "System.Formats.Asn1",
+ "version": "6.0.0",
+ "hash": "sha256-KaMHgIRBF7Nf3VwOo+gJS1DcD+41cJDPWFh+TDQ8ee8="
+ },
+ {
+ "pname": "System.Formats.Asn1",
+ "version": "8.0.1",
+ "hash": "sha256-may/Wg+esmm1N14kQTG4ESMBi+GQKPp0ZrrBo/o6OXM="
+ },
+ {
+ "pname": "System.IO.Abstractions",
+ "version": "4.2.13",
+ "hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg="
+ },
+ {
+ "pname": "System.IO.FileSystem.AccessControl",
+ "version": "4.5.0",
+ "hash": "sha256-ck44YBQ0M+2Im5dw0VjBgFD1s0XuY54cujrodjjSBL8="
+ },
+ {
+ "pname": "System.IO.Pipelines",
+ "version": "9.0.0",
+ "hash": "sha256-vb0NrPjfEao3kfZ0tavp2J/29XnsQTJgXv3/qaAwwz0="
+ },
+ {
+ "pname": "System.Memory",
+ "version": "4.5.5",
+ "hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
+ },
+ {
+ "pname": "System.Numerics.Vectors",
+ "version": "4.4.0",
+ "hash": "sha256-auXQK2flL/JpnB/rEcAcUm4vYMCYMEMiWOCAlIaqu2U="
+ },
+ {
+ "pname": "System.Reflection.Metadata",
+ "version": "8.0.0",
+ "hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
+ },
+ {
+ "pname": "System.Runtime.CompilerServices.Unsafe",
+ "version": "4.5.3",
+ "hash": "sha256-lnZMUqRO4RYRUeSO8HSJ9yBHqFHLVbmenwHWkIU20ak="
+ },
+ {
+ "pname": "System.Runtime.CompilerServices.Unsafe",
+ "version": "6.0.0",
+ "hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
+ },
+ {
+ "pname": "System.Security.AccessControl",
+ "version": "4.5.0",
+ "hash": "sha256-AFsKPb/nTk2/mqH/PYpaoI8PLsiKKimaXf+7Mb5VfPM="
+ },
+ {
+ "pname": "System.Security.Cryptography.Pkcs",
+ "version": "6.0.4",
+ "hash": "sha256-2e0aRybote+OR66bHaNiYpF//4fCiaO3zbR2e9GABUI="
+ },
+ {
+ "pname": "System.Security.Cryptography.ProtectedData",
+ "version": "4.4.0",
+ "hash": "sha256-Ri53QmFX8I8UH0x4PikQ1ZA07ZSnBUXStd5rBfGWFOE="
+ },
+ {
+ "pname": "System.Security.Principal.Windows",
+ "version": "4.5.0",
+ "hash": "sha256-BkUYNguz0e4NJp1kkW7aJBn3dyH9STwB5N8XqnlCsmY="
+ },
+ {
+ "pname": "System.Text.Encodings.Web",
+ "version": "9.0.0",
+ "hash": "sha256-WGaUklQEJywoGR2jtCEs5bxdvYu5SHaQchd6s4RE5x0="
+ },
+ {
+ "pname": "System.Text.Json",
+ "version": "6.0.10",
+ "hash": "sha256-UijYh0dxFjFinMPSTJob96oaRkNm+Wsa+7Ffg6mRnsc="
+ },
+ {
+ "pname": "System.Text.Json",
+ "version": "8.0.5",
+ "hash": "sha256-yKxo54w5odWT6nPruUVsaX53oPRe+gKzGvLnnxtwP68="
+ },
+ {
+ "pname": "System.Text.Json",
+ "version": "9.0.0",
+ "hash": "sha256-aM5Dh4okLnDv940zmoFAzRmqZre83uQBtGOImJpoIqk="
+ },
+ {
+ "pname": "System.Threading.Tasks.Extensions",
+ "version": "4.5.4",
+ "hash": "sha256-owSpY8wHlsUXn5xrfYAiu847L6fAKethlvYx97Ri1ng="
+ }
+]