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=" + } +]