Initial commit

This commit is contained in:
Smaug123
2025-06-15 22:27:30 +01:00
commit 62ae417701
21 changed files with 1108 additions and 0 deletions

18
.config/dotnet-tools.json Normal file
View File

@@ -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"
]
}
}
}

40
.editorconfig Normal file
View File

@@ -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

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

1
.fantomasignore Normal file
View File

@@ -0,0 +1 @@
.direnv/

175
.github/workflows/dotnet.yaml vendored Normal file
View File

@@ -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) }}

16
.gitignore vendored Normal file
View File

@@ -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

19
Directory.Build.props Normal file
View File

@@ -0,0 +1,19 @@
<Project>
<PropertyGroup>
<DebugType Condition=" '$(DebugType)' == '' ">embedded</DebugType>
<Deterministic>true</Deterministic>
<NetCoreTargetingPackRoot>[UNDEFINED]</NetCoreTargetingPackRoot>
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DebugType>embedded</DebugType>
<WarnOn>FS3388,FS3559</WarnOn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.38-alpha" PrivateAssets="all"/>
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com"/>
</ItemGroup>
<PropertyGroup Condition="'$(GITHUB_ACTION)' != ''">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>

21
LICENSE Normal file
View File

@@ -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.

40
README.md Normal file
View File

@@ -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
[<Test>]
let ``This test fails: JSON documents are not equal`` () =
expect {
snapshotJson "123"
return 124
}
[<Test>]
let ``This test passes: JSON documents are equal`` () =
expect {
snapshotJson " 123 "
return 123
}
[<Test>]
let ``This test fails: plain text comparison of ToString`` () =
expect {
snapshot " 123 "
return 123
}
```
# Licence
MIT.

View File

@@ -0,0 +1,37 @@
namespace WoofWare.Expect.Test
open System
open WoofWare.Expect
open NUnit.Framework
[<TestFixture>]
module SimpleTest =
[<Test>]
let ``JSON is resilient to whitespace changes`` () =
expect {
snapshotJson "123 "
return 123
}
[<Test>]
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<Exception>(fun () ->
expectWithMockedFilePath "filepath.fs" {
snapshot "123"
return 124
}
)
.Message
}

View File

@@ -0,0 +1,24 @@
namespace WoofWare.Expect.Test
open ApiSurface
open NUnit.Framework
[<TestFixture>]
module TestSurface =
let assembly = typeof<WoofWare.Expect.ExpectBuilder>.Assembly
[<Test>]
let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
[<Test ; Explicit>]
let ``Update API surface`` () =
ApiSurface.writeAssemblyBaseline assembly
[<Test>]
let ``Ensure public API is fully documented`` () =
DocCoverage.assertFullyDocumented assembly
[<Test ; Explicit "Not yet published">]
// https://github.com/nunit/nunit3-vs-adapter/issues/876
let ``EnsureVersionIsMonotonic`` () =
MonotonicVersion.validate assembly "WoofWare.Expect"

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="SimpleTest.fs" />
<Compile Include="TestSurface.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.1.20" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WoofWare.Expect\WoofWare.Expect.fsproj" />
</ItemGroup>
</Project>

22
WoofWare.Expect.sln Normal file
View File

@@ -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

203
WoofWare.Expect/Library.fs Normal file
View File

@@ -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
}
[<RequireQualifiedAccess>]
module private Text =
let predent (c : char) (s : string) =
s.Split '\n' |> Seq.map (sprintf "%c %s" c) |> String.concat "\n"
/// <summary>
/// The builder which powers WoofWare.Expect.
/// </summary>
/// <remarks>You're not expected to construct this explicitly; it's a computation expression, available as <c>Builder.expect</c>.</remarks>
/// <param name="filePathOverride">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.)</param>
type ExpectBuilder (?filePathOverride : string) =
/// Combine two `ExpectState`s. The first one is the "expected" snapshot; the second is the "actual".
member _.Bind (state : ExpectState<unit>, 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
}
/// <summary>Express that the actual value's <c>ToString</c> should identically equal this string.</summary>
[<CustomOperation("snapshot", MaintainsVariableSpaceUsingBind = true)>]
member _.Snapshot
(
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
}
{ state with
Snapshot = Some (SnapshotValue.BareString snapshot, callerInfo)
}
/// <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>snapshot "123"</c> indicates the JSON integer 123.
/// </remarks>
[<CustomOperation("snapshotJson", MaintainsVariableSpaceUsingBind = true)>]
member _.SnapshotJson
(
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
}
{ 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.
[<AutoOpen>]
module Builder =
/// <summary>The primary WoofWare.Expect builder.</summary>
///
/// <remarks>
/// You are expected to use this like so:
///
/// <code>
/// expect {
/// snapshot "123"
/// return 124
/// }
/// </code>
///
/// (That example expectation will fail, because the actual value 124 does not snapshot to the expected snapshot "123".)
/// </remarks>
let expect = ExpectBuilder ()
/// <summary>
/// This is the `expect` builder, but it mocks out the filepath reported on failure.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
let expectWithMockedFilePath (path : string) = ExpectBuilder path

View File

@@ -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

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Authors>Patrick Stevens</Authors>
<Copyright>Copyright (c) Patrick Stevens 2025</Copyright>
<Description>Snapshot/expect testing framework for F#</Description>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Smaug123/WoofWare.Expect</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>logo.png</PackageIcon>
<PackageTags>fsharp;snapshot;expect;test;testing;jest</PackageTags>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs"/>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\logo.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<EmbeddedResource Include="SurfaceBaseline.txt"/>
<EmbeddedResource Include="version.json"/>
</ItemGroup>
<ItemGroup>
<!-- FSharp.SystemTextJson requires at least this version -->
<PackageReference Update="FSharp.Core" Version="4.7.0" />
<PackageReference Include="FSharp.SystemTextJson" Version="1.4.36" />
<!-- Needed for DeepEquals -->
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"version": "0.1",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": [
":/WoofWare.Expect/",
":/README.md",
":/Directory.Build.props",
":/LICENSE"
]
}

61
flake.lock generated Normal file
View File

@@ -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
}

71
flake.nix Normal file
View File

@@ -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
];
};
});
}

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

267
nix/deps.json Normal file
View File

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