Compare commits

..

19 Commits

Author SHA1 Message Date
Patrick Stevens
2fc8ba958c Add IAsyncDisposable support in mock generators (#456)
* Add IAsyncDisposable support to mock generators

Extend both GenerateMock and GenerateCapturingMock to support interfaces
that inherit IAsyncDisposable, mirroring the existing IDisposable pattern.

Changes:
- Add IAsyncDisposable to KnownInheritance type
- Detect IAsyncDisposable inheritance in interface parsing
- Generate DisposeAsync field with ValueTask return type
- Initialize DisposeAsync with completed ValueTask() in Empty mock
- Implement System.IAsyncDisposable interface explicitly
- Support interfaces inheriting both IDisposable and IAsyncDisposable

Test cases added:
- TypeWithAsyncDisposable: interface with only IAsyncDisposable
- TypeWithBothDisposables: interface with both disposal interfaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-11-20 08:59:30 +00:00
Patrick Stevens
67e051b6b3 Add --no- prefix for bools (#455)
Implements support for --no- prefix negation on boolean and flag DU fields
when marked with [<ArgumentNegateWithPrefix>]. This allows both --flag and
--no-flag forms to be accepted, with --no- variants negating the value.

Changes:
- Extend ArgParserGenerator to generate --no- prefix handling
- Add conflict detection for overlapping --no- prefixed arguments
- Update help text to display both forms (e.g., --verbose / --no-verbose)
- Add test examples in ArgParserNegationTests.fs demonstrating:
  - Boolean field negation
  - Flag DU negation
  - Multiple ArgumentLongForm with negation
  - Combined features (defaults, help text)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 07:53:15 +00:00
dependabot[bot]
6dee454229 Bump ApiSurface from 5.0.2 to 5.0.3 (#452)
* Bump ApiSurface from 5.0.2 to 5.0.3

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: ApiSurface
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump fsharp-analyzers from 0.33.1 to 0.34.1

---
updated-dependencies:
- dependency-name: fsharp-analyzers
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Microsoft.NET.Test.Sdk from 18.0.0 to 18.0.1

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

* Analyzers too

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-11-17 22:29:40 +00:00
patrick-conscriptus[bot]
e1767f5ed0 Automated commit (#451)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-16 01:43:42 +00:00
dependabot[bot]
f95437aa48 Bump FsCheck from 3.3.1 to 3.3.2 (#449)
* Bump FsCheck from 3.3.1 to 3.3.2

---
updated-dependencies:
- dependency-name: FsCheck
  dependency-version: 3.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Nerdbank.GitVersioning from 3.8.118 to 3.9.50

---
updated-dependencies:
- dependency-name: Nerdbank.GitVersioning
  dependency-version: 3.9.50
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-11-10 17:50:04 +00:00
patrick-conscriptus[bot]
e9edf9dabc Automated commit (#448)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-09 01:33:34 +00:00
patrick-conscriptus[bot]
d873850acf Automated commit (#447)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-02 01:41:54 +00:00
dependabot[bot]
bd4905a236 Bump actions/upload-artifact from 4 to 5 (#446)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Stevens <3138005+Smaug123@users.noreply.github.com>
2025-10-30 07:43:57 +00:00
dependabot[bot]
62daa84b26 Bump actions/download-artifact from 5 to 6 (#445) 2025-10-27 11:11:53 +00:00
patrick-conscriptus[bot]
4d5830d147 Automated commit (#444)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-26 01:32:39 +00:00
patrick-conscriptus[bot]
6c2280c300 Automated commit (#443)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-24 19:57:59 +00:00
dependabot[bot]
ecbe425f66 Bump WoofWare.Expect from 0.8.3 to 0.8.4 (#442)
* Bump WoofWare.Expect from 0.8.3 to 0.8.4

---
updated-dependencies:
- dependency-name: WoofWare.Expect
  dependency-version: 0.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-20 13:15:33 +00:00
dependabot[bot]
30eb28d00a Bump WoofWare.Whippet.Fantomas from 0.6.3 to 0.6.4 (#440) 2025-10-20 11:13:03 +00:00
patrick-conscriptus[bot]
62e1cf2c46 Upgrade Nix flake and deps (#439)
* Automated commit

* Fix link

---------

Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-19 04:54:20 +00:00
dependabot[bot]
fa81119cec Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0 (#434)
* Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump NUnit3TestAdapter from 5.0.0 to 5.2.0

---
updated-dependencies:
- dependency-name: NUnit3TestAdapter
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit3TestAdapter
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump WoofWare.Expect from 0.8.2 to 0.8.3

---
updated-dependencies:
- dependency-name: WoofWare.Expect
  dependency-version: 0.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump ApiSurface from 5.0.1 to 5.0.2

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: ApiSurface
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump TypeEquality from 0.3.0 to 0.4.2

---
updated-dependencies:
- dependency-name: TypeEquality
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-13 17:39:07 +00:00
patrick-conscriptus[bot]
7907cefaee Automated commit (#433)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-12 01:29:47 +00:00
patrick-conscriptus[bot]
e83ec1f152 Automated commit (#432)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-05 01:31:29 +00:00
Patrick Stevens
9d8cef8fdc Switch to trusted publishing (#431) 2025-10-03 09:37:32 +00:00
Patrick Stevens
1721ad1ac0 Unconditional function for empty generating mock (#430) 2025-09-30 21:52:59 +00:00
33 changed files with 2606 additions and 272 deletions

View File

@@ -9,10 +9,16 @@
] ]
}, },
"fsharp-analyzers": { "fsharp-analyzers": {
"version": "0.32.1", "version": "0.34.1",
"commands": [ "commands": [
"fsharp-analyzers" "fsharp-analyzers"
] ]
},
"woofware.nunittestrunner": {
"version": "0.3.9",
"commands": [
"woofware.nunittestrunner"
]
} }
} }
} }

View File

@@ -167,12 +167,12 @@ jobs:
- name: Pack - name: Pack
run: nix develop --command dotnet pack --configuration Release run: nix develop --command dotnet pack --configuration Release
- name: Upload NuGet artifact (plugin) - name: Upload NuGet artifact (plugin)
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: nuget-package-plugin name: nuget-package-plugin
path: WoofWare.Myriad.Plugins/bin/Release/WoofWare.Myriad.Plugins.*.nupkg path: WoofWare.Myriad.Plugins/bin/Release/WoofWare.Myriad.Plugins.*.nupkg
- name: Upload NuGet artifact (attributes) - name: Upload NuGet artifact (attributes)
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: nuget-package-attribute name: nuget-package-attribute
path: WoofWare.Myriad.Plugins.Attributes/bin/Release/WoofWare.Myriad.Plugins.Attributes.*.nupkg path: WoofWare.Myriad.Plugins.Attributes/bin/Release/WoofWare.Myriad.Plugins.Attributes.*.nupkg
@@ -182,7 +182,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download NuGet artifact (plugin) - name: Download NuGet artifact (plugin)
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-plugin name: nuget-package-plugin
path: packed-plugin path: packed-plugin
@@ -190,7 +190,7 @@ jobs:
# Verify that there is exactly one nupkg in the artifact that would be NuGet published # Verify that there is exactly one nupkg in the artifact that would be NuGet published
run: if [[ $(find packed-plugin -maxdepth 1 -name 'WoofWare.Myriad.Plugins.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi run: if [[ $(find packed-plugin -maxdepth 1 -name 'WoofWare.Myriad.Plugins.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
- name: Download NuGet artifact (attributes) - name: Download NuGet artifact (attributes)
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-attribute name: nuget-package-attribute
path: packed-attribute path: packed-attribute
@@ -209,7 +209,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: ${{ matrix.artifact }} name: ${{ matrix.artifact }}
- name: Compute package path - name: Compute package path
@@ -249,7 +249,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-attribute name: nuget-package-attribute
path: packed path: packed
@@ -268,7 +268,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-plugin name: nuget-package-plugin
path: packed path: packed
@@ -294,19 +294,24 @@ jobs:
extra_nix_config: | extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-attribute name: nuget-package-attribute
path: packed path: packed
- name: Identify `dotnet` - name: Identify `dotnet`
id: dotnet-identify id: dotnet-identify
run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT'
- name: Obtain NuGet key
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet - name: Publish to NuGet
id: publish-success id: publish-success
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
with: with:
package-name: WoofWare.Myriad.Plugins.Attributes package-name: WoofWare.Myriad.Plugins.Attributes
nuget-key: ${{ secrets.NUGET_API_KEY }} nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
nupkg-dir: packed/ nupkg-dir: packed/
dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} dotnet: ${{ steps.dotnet-identify.outputs.dotnet }}
@@ -327,19 +332,24 @@ jobs:
extra_nix_config: | extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: nuget-package-plugin name: nuget-package-plugin
path: packed path: packed
- name: Identify `dotnet` - name: Identify `dotnet`
id: dotnet-identify id: dotnet-identify
run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT'
- name: Obtain NuGet key
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet - name: Publish to NuGet
id: publish-success id: publish-success
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
with: with:
package-name: WoofWare.Myriad.Plugins package-name: WoofWare.Myriad.Plugins
nuget-key: ${{ secrets.NUGET_API_KEY }} nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
nupkg-dir: packed/ nupkg-dir: packed/
dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} dotnet: ${{ steps.dotnet-identify.outputs.dotnet }}
@@ -358,7 +368,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Download NuGet artifact - name: Download NuGet artifact
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: ${{ matrix.artifact }} name: ${{ matrix.artifact }}
- name: Compute package path - name: Compute package path

View File

@@ -1,5 +1,16 @@
Notable changes are recorded here. Notable changes are recorded here.
# WoofWare.Myriad.Plugins 9.1.1, WoofWare.Myriad.Plugins.Attributes 3.8.1
Adds the `[<ArgumentNegateWithPrefix>]` attribute, which can be placed on a boolean or flag-valued field when using the `ArgParser` generator.
This causes the boolean to be specifiable with the `--no-` prefix to negate its value.
(For example, `Foo : bool` is normally specified as `--foo`; this new attribute lets the user additionally give `--no-foo` to get the same semantics as `--foo=false`.)
# WoofWare.Myriad.Plugins 9.0.1
Converts the `static member Empty` field on each generated mock (from `GeneratedMock`) into a function, so as to permit the `GeneratedCapturingMock` to have the same signature.
(`GeneratedCapturingMock` contains mutable state, so must be created afresh each time.)
# WoofWare.Myriad.Plugins 8.1.1 # WoofWare.Myriad.Plugins 8.1.1
Adds `GenerateCapturingMock`, which is `GenerateMock` but additionally records the calls made to each function. Adds `GenerateCapturingMock`, which is `GenerateMock` but additionally records the calls made to each function.

View File

@@ -44,7 +44,7 @@ git config blame.ignoreRevsFile .git-blame-ignore-revs
## Dependencies ## Dependencies
I try to keep this repository's dependencies as few as possible, because (for example) any consumer of the source generator will also consume this project via the attributes. I try to keep this repository's dependencies as few as possible, because (for example) any consumer of the source generator will also consume this project via the attributes.
When adding dependencies, you will need to `nix run .#fetchDeps` to obtain a new copy of [the dependency lockfile](./nix/deps.nix). When adding dependencies, you will need to `nix run .#fetchDeps` to obtain a new copy of [the dependency lockfile](./nix/deps.json).
## Branch strategy ## Branch strategy

View File

@@ -0,0 +1,124 @@
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
// This file contains test cases for conflict detection in the ArgParser generator.
// These are expected to FAIL at build time with appropriate error messages.
// Uncomment each section one at a time to test the specific conflict detection.
// ============================================================================
// Test 1: Field named NoFooBar conflicts with FooBar's --no- variant
// ============================================================================
// Expected error: Argument name conflict: '--no-foo-bar' collides with the --no- variant
// of field 'FooBar' (which has [<ArgumentNegateWithPrefix>])
(*
[<ArgParser>]
type ConflictingFieldNames =
{
[<ArgumentNegateWithPrefix>]
FooBar : bool
NoFooBar : bool
}
*)
// ============================================================================
// Test 2: ArgumentLongForm "no-foo" conflicts with Foo's --no- variant
// ============================================================================
// Expected error: Argument name conflict: '--no-foo' collides with the --no- variant
// of field 'Foo' (which has [<ArgumentNegateWithPrefix>])
(*
[<ArgParser>]
type ConflictingLongForm =
{
[<ArgumentNegateWithPrefix>]
Foo : bool
[<ArgumentLongForm "no-foo">]
Bar : bool
}
*)
// ============================================================================
// Test 3: Multiple ArgumentLongForm, one conflicts
// ============================================================================
// Expected error: Argument name conflict: '--no-verbose' collides with...
(*
[<ArgParser>]
type ConflictingMultipleLongForms =
{
[<ArgumentLongForm "verbose">]
[<ArgumentLongForm "v">]
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
[<ArgumentLongForm "no-verbose">]
Quiet : bool
}
*)
// ============================================================================
// Test 4: ArgumentNegateWithPrefix on non-boolean field
// ============================================================================
// Expected error: [<ArgumentNegateWithPrefix>] can only be applied to boolean
// or flag DU fields, but was applied to field NotABool of type string
(*
[<ArgParser>]
type InvalidAttributeOnNonBool =
{
[<ArgumentNegateWithPrefix>]
NotABool : string
}
*)
// ============================================================================
// Test 5: ArgumentNegateWithPrefix on non-flag int field
// ============================================================================
// Expected error: [<ArgumentNegateWithPrefix>] can only be applied to boolean
// or flag DU fields
(*
[<ArgParser>]
type InvalidAttributeOnInt =
{
[<ArgumentNegateWithPrefix>]
NotAFlag : int
}
*)
// ============================================================================
// Test 6: Complex conflict with custom names
// ============================================================================
// This tests a more complex scenario where a custom ArgumentLongForm creates
// a conflict with a different field's negated form
(*
[<ArgParser>]
type ComplexConflict =
{
[<ArgumentLongForm "enable">]
[<ArgumentNegateWithPrefix>]
FeatureA : bool
[<ArgumentLongForm "no-enable">]
DisableAll : bool
}
*)
// ============================================================================
// Test 7: Valid usage - no conflicts (this SHOULD compile)
// ============================================================================
[<ArgParser>]
type NoConflict =
{
[<ArgumentNegateWithPrefix>]
EnableFeature : bool
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
NormalField : string
}

View File

@@ -0,0 +1,48 @@
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
// Test types for ArgumentNegateWithPrefix functionality
type TestDryRunMode =
| [<ArgumentFlag false>] Wet
| [<ArgumentFlag true>] Dry
[<ArgParser true>]
type BoolNegation =
{
[<ArgumentNegateWithPrefix>]
EnableFeature : bool
}
[<ArgParser true>]
type FlagNegation =
{
[<ArgumentNegateWithPrefix>]
DryRun : TestDryRunMode
}
[<ArgParser true>]
type MultipleFormsNegation =
{
[<ArgumentLongForm "verbose">]
[<ArgumentLongForm "v">]
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
}
[<ArgParser true>]
type CombinedFeatures =
{
[<ArgumentNegateWithPrefix>]
[<ArgumentDefaultFunction>]
Verbose : Choice<bool, bool>
[<ArgumentNegateWithPrefix>]
[<ArgumentHelpText "Enable debug mode">]
Debug : bool
NormalBool : bool
}
static member DefaultVerbose () = false

View File

@@ -55,3 +55,15 @@ type TypeWithProperties =
abstract Mem1 : string option -> string[] Async abstract Mem1 : string option -> string[] Async
abstract Prop1 : int abstract Prop1 : int
abstract Prop2 : unit Async abstract Prop2 : unit Async
[<GenerateCapturingMock>]
type TypeWithAsyncDisposable =
inherit IAsyncDisposable
abstract Mem1 : string option -> string[] Async
abstract Mem2 : unit -> string[] Async
[<GenerateCapturingMock>]
type TypeWithBothDisposables =
inherit IDisposable
inherit IAsyncDisposable
abstract Mem1 : string -> int

View File

@@ -89,6 +89,17 @@
<Compile Include="GeneratedArgs.fs"> <Compile Include="GeneratedArgs.fs">
<MyriadFile>Args.fs</MyriadFile> <MyriadFile>Args.fs</MyriadFile>
</Compile> </Compile>
<Compile Include="ArgParserNegationTests.fs" />
<Compile Include="GeneratedArgParserNegationTests.fs">
<MyriadFile>ArgParserNegationTests.fs</MyriadFile>
</Compile>
<!-- Not compiled, because by design they *don't* compile. That makes them very hard to test in an automated way! -->
<None Include="ArgParserConflictTests.fs" />
<!-- To run the conflict tests:
<Compile Include="GeneratedArgParserConflictTests.fs">
<MyriadFile>ArgParserConflictTests.fs</MyriadFile>
</Compile>
-->
<None Include="swagger-gitea.json" /> <None Include="swagger-gitea.json" />
<Compile Include="GeneratedSwaggerGitea.fs"> <Compile Include="GeneratedSwaggerGitea.fs">
<MyriadFile>swagger-gitea.json</MyriadFile> <MyriadFile>swagger-gitea.json</MyriadFile>

File diff suppressed because it is too large Load Diff

View File

@@ -36,8 +36,8 @@ type internal PublicTypeMock =
Mem3 : int * System.Threading.CancellationToken option -> string Mem3 : int * System.Threading.CancellationToken option -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeMock = static member Empty () : PublicTypeMock =
{ {
Calls = PublicTypeMockCalls.Calls.Empty () Calls = PublicTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -89,8 +89,8 @@ type public PublicTypeInternalFalseMock =
Mem3 : int * System.Threading.CancellationToken option -> string Mem3 : int * System.Threading.CancellationToken option -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeInternalFalseMock = static member Empty () : PublicTypeInternalFalseMock =
{ {
Calls = PublicTypeInternalFalseMockCalls.Calls.Empty () Calls = PublicTypeInternalFalseMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -139,8 +139,8 @@ type internal InternalTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : InternalTypeMock = static member Empty () : InternalTypeMock =
{ {
Calls = InternalTypeMockCalls.Calls.Empty () Calls = InternalTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -184,8 +184,8 @@ type private PrivateTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeMock = static member Empty () : PrivateTypeMock =
{ {
Calls = PrivateTypeMockCalls.Calls.Empty () Calls = PrivateTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -229,8 +229,8 @@ type private PrivateTypeInternalFalseMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeInternalFalseMock = static member Empty () : PrivateTypeInternalFalseMock =
{ {
Calls = PrivateTypeInternalFalseMockCalls.Calls.Empty () Calls = PrivateTypeInternalFalseMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -271,7 +271,7 @@ type internal VeryPublicTypeMock<'a, 'b> =
Mem1 : 'a -> 'b Mem1 : 'a -> 'b
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty () : VeryPublicTypeMock<'a, 'b> = static member Empty () : VeryPublicTypeMock<'a, 'b> =
{ {
Calls = VeryPublicTypeMockCalls.Calls.Empty () Calls = VeryPublicTypeMockCalls.Calls.Empty ()
@@ -365,7 +365,7 @@ type internal CurriedMock<'a> =
Mem6 : int * string -> 'a * int -> string Mem6 : int * string -> 'a * int -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty () : CurriedMock<'a> = static member Empty () : CurriedMock<'a> =
{ {
Calls = CurriedMockCalls.Calls.Empty () Calls = CurriedMockCalls.Calls.Empty ()
@@ -486,8 +486,8 @@ type internal TypeWithInterfaceMock =
Mem2 : unit -> string[] Async Mem2 : unit -> string[] Async
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithInterfaceMock = static member Empty () : TypeWithInterfaceMock =
{ {
Calls = TypeWithInterfaceMockCalls.Calls.Empty () Calls = TypeWithInterfaceMockCalls.Calls.Empty ()
Dispose = (fun () -> ()) Dispose = (fun () -> ())
@@ -540,8 +540,8 @@ type internal TypeWithPropertiesMock =
Prop2 : unit -> unit Async Prop2 : unit -> unit Async
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithPropertiesMock = static member Empty () : TypeWithPropertiesMock =
{ {
Calls = TypeWithPropertiesMockCalls.Calls.Empty () Calls = TypeWithPropertiesMockCalls.Calls.Empty ()
Dispose = (fun () -> ()) Dispose = (fun () -> ())
@@ -560,3 +560,103 @@ type internal TypeWithPropertiesMock =
interface System.IDisposable with interface System.IDisposable with
member this.Dispose () : unit = this.Dispose () member this.Dispose () : unit = this.Dispose ()
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal TypeWithAsyncDisposableMockCalls =
/// All the calls made to a TypeWithAsyncDisposableMock mock
type internal Calls =
{
Mem1 : ResizeArray<string option>
Mem2 : ResizeArray<unit>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type internal TypeWithAsyncDisposableMock =
{
Calls : TypeWithAsyncDisposableMockCalls.Calls
/// Implementation of IAsyncDisposable.DisposeAsync
DisposeAsync : unit -> System.Threading.Tasks.ValueTask
Mem1 : string option -> string[] Async
Mem2 : unit -> string[] Async
}
/// An implementation where every non-disposal method throws.
static member Empty () : TypeWithAsyncDisposableMock =
{
Calls = TypeWithAsyncDisposableMockCalls.Calls.Empty ()
DisposeAsync = (fun () -> (System.Threading.Tasks.ValueTask ()))
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface TypeWithAsyncDisposable with
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
member this.Mem2 () =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (()))
this.Mem2 (())
interface System.IAsyncDisposable with
member this.DisposeAsync () : System.Threading.Tasks.ValueTask = this.DisposeAsync ()
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal TypeWithBothDisposablesMockCalls =
/// All the calls made to a TypeWithBothDisposablesMock mock
type internal Calls =
{
Mem1 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
}
/// Mock record type for an interface
type internal TypeWithBothDisposablesMock =
{
Calls : TypeWithBothDisposablesMockCalls.Calls
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
/// Implementation of IAsyncDisposable.DisposeAsync
DisposeAsync : unit -> System.Threading.Tasks.ValueTask
Mem1 : string -> int
}
/// An implementation where every non-disposal method throws.
static member Empty () : TypeWithBothDisposablesMock =
{
Calls = TypeWithBothDisposablesMockCalls.Calls.Empty ()
Dispose = (fun () -> ())
DisposeAsync = (fun () -> (System.Threading.Tasks.ValueTask ()))
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
}
interface TypeWithBothDisposables with
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
interface System.IDisposable with
member this.Dispose () : unit = this.Dispose ()
interface System.IAsyncDisposable with
member this.DisposeAsync () : System.Threading.Tasks.ValueTask = this.DisposeAsync ()

View File

@@ -35,8 +35,8 @@ type internal PublicTypeNoAttrMock =
Mem3 : int * System.Threading.CancellationToken option -> string Mem3 : int * System.Threading.CancellationToken option -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeNoAttrMock = static member Empty () : PublicTypeNoAttrMock =
{ {
Calls = PublicTypeNoAttrMockCalls.Calls.Empty () Calls = PublicTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -87,8 +87,8 @@ type public PublicTypeInternalFalseNoAttrMock =
Mem3 : int * System.Threading.CancellationToken option -> string Mem3 : int * System.Threading.CancellationToken option -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeInternalFalseNoAttrMock = static member Empty () : PublicTypeInternalFalseNoAttrMock =
{ {
Calls = PublicTypeInternalFalseNoAttrMockCalls.Calls.Empty () Calls = PublicTypeInternalFalseNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -136,8 +136,8 @@ type internal InternalTypeNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : InternalTypeNoAttrMock = static member Empty () : InternalTypeNoAttrMock =
{ {
Calls = InternalTypeNoAttrMockCalls.Calls.Empty () Calls = InternalTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -180,8 +180,8 @@ type private PrivateTypeNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeNoAttrMock = static member Empty () : PrivateTypeNoAttrMock =
{ {
Calls = PrivateTypeNoAttrMockCalls.Calls.Empty () Calls = PrivateTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -224,8 +224,8 @@ type private PrivateTypeInternalFalseNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeInternalFalseNoAttrMock = static member Empty () : PrivateTypeInternalFalseNoAttrMock =
{ {
Calls = PrivateTypeInternalFalseNoAttrMockCalls.Calls.Empty () Calls = PrivateTypeInternalFalseNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -265,7 +265,7 @@ type internal VeryPublicTypeNoAttrMock<'a, 'b> =
Mem1 : 'a -> 'b Mem1 : 'a -> 'b
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> = static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> =
{ {
Calls = VeryPublicTypeNoAttrMockCalls.Calls.Empty () Calls = VeryPublicTypeNoAttrMockCalls.Calls.Empty ()
@@ -358,7 +358,7 @@ type internal CurriedNoAttrMock<'a> =
Mem6 : int * string -> 'a * int -> string Mem6 : int * string -> 'a * int -> string
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty () : CurriedNoAttrMock<'a> = static member Empty () : CurriedNoAttrMock<'a> =
{ {
Calls = CurriedNoAttrMockCalls.Calls.Empty () Calls = CurriedNoAttrMockCalls.Calls.Empty ()
@@ -478,8 +478,8 @@ type internal TypeWithInterfaceNoAttrMock =
Mem2 : unit -> string[] Async Mem2 : unit -> string[] Async
} }
/// An implementation where every non-unit method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithInterfaceNoAttrMock = static member Empty () : TypeWithInterfaceNoAttrMock =
{ {
Calls = TypeWithInterfaceNoAttrMockCalls.Calls.Empty () Calls = TypeWithInterfaceNoAttrMockCalls.Calls.Empty ()
Dispose = (fun () -> ()) Dispose = (fun () -> ())

View File

@@ -16,7 +16,7 @@ type internal PublicTypeMock =
Mem3 : int * option<System.Threading.CancellationToken> -> string Mem3 : int * option<System.Threading.CancellationToken> -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeMock = static member Empty : PublicTypeMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -41,7 +41,7 @@ type public PublicTypeInternalFalseMock =
Mem3 : int * option<System.Threading.CancellationToken> -> string Mem3 : int * option<System.Threading.CancellationToken> -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeInternalFalseMock = static member Empty : PublicTypeInternalFalseMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -65,7 +65,7 @@ type internal InternalTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : InternalTypeMock = static member Empty : InternalTypeMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -87,7 +87,7 @@ type private PrivateTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeMock = static member Empty : PrivateTypeMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -109,7 +109,7 @@ type private PrivateTypeInternalFalseMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeInternalFalseMock = static member Empty : PrivateTypeInternalFalseMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -130,7 +130,7 @@ type internal VeryPublicTypeMock<'a, 'b> =
Mem1 : 'a -> 'b Mem1 : 'a -> 'b
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty () : VeryPublicTypeMock<'a, 'b> = static member Empty () : VeryPublicTypeMock<'a, 'b> =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -154,7 +154,7 @@ type internal CurriedMock<'a> =
Mem6 : int * string -> 'a * int -> string Mem6 : int * string -> 'a * int -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty () : CurriedMock<'a> = static member Empty () : CurriedMock<'a> =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -192,7 +192,7 @@ type internal TypeWithInterfaceMock =
Mem2 : unit -> string[] Async Mem2 : unit -> string[] Async
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithInterfaceMock = static member Empty : TypeWithInterfaceMock =
{ {
Dispose = (fun () -> ()) Dispose = (fun () -> ())
@@ -221,7 +221,7 @@ type internal TypeWithPropertiesMock =
Mem1 : string option -> string[] Async Mem1 : string option -> string[] Async
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithPropertiesMock = static member Empty : TypeWithPropertiesMock =
{ {
Dispose = (fun () -> ()) Dispose = (fun () -> ())
@@ -237,3 +237,62 @@ type internal TypeWithPropertiesMock =
interface System.IDisposable with interface System.IDisposable with
member this.Dispose () : unit = this.Dispose () member this.Dispose () : unit = this.Dispose ()
namespace SomeNamespace
open System
open WoofWare.Myriad.Plugins
/// Mock record type for an interface
type internal TypeWithAsyncDisposableMock =
{
/// Implementation of IAsyncDisposable.DisposeAsync
DisposeAsync : unit -> System.Threading.Tasks.ValueTask
Mem1 : string option -> string[] Async
Mem2 : unit -> string[] Async
}
/// An implementation where every non-disposal method throws.
static member Empty : TypeWithAsyncDisposableMock =
{
DisposeAsync = (fun () -> (System.Threading.Tasks.ValueTask ()))
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface TypeWithAsyncDisposable with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem2 () = this.Mem2 (())
interface System.IAsyncDisposable with
member this.DisposeAsync () : System.Threading.Tasks.ValueTask = this.DisposeAsync ()
namespace SomeNamespace
open System
open WoofWare.Myriad.Plugins
/// Mock record type for an interface
type internal TypeWithBothDisposablesMock =
{
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
/// Implementation of IAsyncDisposable.DisposeAsync
DisposeAsync : unit -> System.Threading.Tasks.ValueTask
Mem1 : string -> int
}
/// An implementation where every non-disposal method throws.
static member Empty : TypeWithBothDisposablesMock =
{
Dispose = (fun () -> ())
DisposeAsync = (fun () -> (System.Threading.Tasks.ValueTask ()))
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
}
interface TypeWithBothDisposables with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
interface System.IDisposable with
member this.Dispose () : unit = this.Dispose ()
interface System.IAsyncDisposable with
member this.DisposeAsync () : System.Threading.Tasks.ValueTask = this.DisposeAsync ()

View File

@@ -15,7 +15,7 @@ type internal PublicTypeNoAttrMock =
Mem3 : int * option<System.Threading.CancellationToken> -> string Mem3 : int * option<System.Threading.CancellationToken> -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeNoAttrMock = static member Empty : PublicTypeNoAttrMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -39,7 +39,7 @@ type public PublicTypeInternalFalseNoAttrMock =
Mem3 : int * option<System.Threading.CancellationToken> -> string Mem3 : int * option<System.Threading.CancellationToken> -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PublicTypeInternalFalseNoAttrMock = static member Empty : PublicTypeInternalFalseNoAttrMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -62,7 +62,7 @@ type internal InternalTypeNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : InternalTypeNoAttrMock = static member Empty : InternalTypeNoAttrMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -83,7 +83,7 @@ type private PrivateTypeNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeNoAttrMock = static member Empty : PrivateTypeNoAttrMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -104,7 +104,7 @@ type private PrivateTypeInternalFalseNoAttrMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : PrivateTypeInternalFalseNoAttrMock = static member Empty : PrivateTypeInternalFalseNoAttrMock =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -124,7 +124,7 @@ type internal VeryPublicTypeNoAttrMock<'a, 'b> =
Mem1 : 'a -> 'b Mem1 : 'a -> 'b
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> = static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -147,7 +147,7 @@ type internal CurriedNoAttrMock<'a> =
Mem6 : int * string -> 'a * int -> string Mem6 : int * string -> 'a * int -> string
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty () : CurriedNoAttrMock<'a> = static member Empty () : CurriedNoAttrMock<'a> =
{ {
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1")) Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
@@ -184,7 +184,7 @@ type internal TypeWithInterfaceNoAttrMock =
Mem2 : unit -> string[] Async Mem2 : unit -> string[] Async
} }
/// An implementation where every method throws. /// An implementation where every non-disposal method throws.
static member Empty : TypeWithInterfaceNoAttrMock = static member Empty : TypeWithInterfaceNoAttrMock =
{ {
Dispose = (fun () -> ()) Dispose = (fun () -> ())

View File

@@ -55,3 +55,15 @@ type TypeWithProperties =
abstract Mem1 : string option -> string[] Async abstract Mem1 : string option -> string[] Async
abstract Prop1 : int abstract Prop1 : int
abstract Prop2 : unit Async abstract Prop2 : unit Async
[<GenerateMock>]
type TypeWithAsyncDisposable =
inherit IAsyncDisposable
abstract Mem1 : string option -> string[] Async
abstract Mem2 : unit -> string[] Async
[<GenerateMock>]
type TypeWithBothDisposables =
inherit IDisposable
inherit IAsyncDisposable
abstract Mem1 : string -> int

View File

@@ -10,7 +10,7 @@
<WarnOn>FS3388,FS3559</WarnOn> <WarnOn>FS3388,FS3559</WarnOn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118" PrivateAssets="all" /> <PackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" PrivateAssets="all" />
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com" /> <SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition="'$(GITHUB_ACTION)' != ''"> <PropertyGroup Condition="'$(GITHUB_ACTION)' != ''">

View File

@@ -652,13 +652,13 @@ For example, [PureGymDto.fs](./ConsumePlugin/PureGymDto.fs) is a real-world set
* In your `.fsproj` file, define a helper variable so that subsequent steps don't all have to be kept in sync: * In your `.fsproj` file, define a helper variable so that subsequent steps don't all have to be kept in sync:
```xml ```xml
<PropertyGroup> <PropertyGroup>
<WoofWareMyriadPluginVersion>2.0.1</WoofWareMyriadPluginVersion> <WoofWareMyriadPluginVersion>9.0.1</WoofWareMyriadPluginVersion>
</PropertyGroup> </PropertyGroup>
``` ```
* Take a reference on `WoofWare.Myriad.Plugins.Attributes` (which has no other dependencies), to obtain access to the attributes which the generator will recognise: * Take a reference on `WoofWare.Myriad.Plugins.Attributes` (which has no other dependencies), to obtain access to the attributes which the generator will recognise:
```xml ```xml
<ItemGroup> <ItemGroup>
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="2.0.2" /> <PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.7.2" />
</ItemGroup> </ItemGroup>
``` ```
* Take a reference (with private assets, to prevent these from propagating to your own assembly) on `WoofWare.Myriad.Plugins`, to obtain the plugins which Myriad will run, and on `Myriad.Sdk`, to obtain the Myriad binary itself: * Take a reference (with private assets, to prevent these from propagating to your own assembly) on `WoofWare.Myriad.Plugins`, to obtain the plugins which Myriad will run, and on `Myriad.Sdk`, to obtain the Myriad binary itself:

View File

@@ -100,3 +100,24 @@ type ArgumentFlagAttribute (flagValue : bool) =
[<AttributeUsage(AttributeTargets.Field, AllowMultiple = true)>] [<AttributeUsage(AttributeTargets.Field, AllowMultiple = true)>]
type ArgumentLongForm (s : string) = type ArgumentLongForm (s : string) =
inherit Attribute () inherit Attribute ()
/// Attribute indicating that this boolean or flag field should accept `--no-` prefix for negation.
/// When this attribute is present on a boolean or flag DU field, the generated parser will accept
/// both --field-name and --no-field-name as argument forms.
///
/// For boolean fields:
/// --field-name (or --field-name=true) sets the value to true
/// --no-field-name (or --no-field-name=true) sets the value to false
/// --field-name=false sets the value to false
/// --no-field-name=false sets the value to true
///
/// For flag DU fields with [<ArgumentFlag>]:
/// --field-name (or --field-name=true) sets to the case marked with [<ArgumentFlag true>]
/// --no-field-name (or --no-field-name=true) sets to the case marked with [<ArgumentFlag false>]
/// --field-name=false sets to the [<ArgumentFlag false>] case
/// --no-field-name=false sets to the [<ArgumentFlag true>] case
///
/// This attribute can only be applied to bool fields or flag DU fields (two-case DUs with [<ArgumentFlag>]).
[<AttributeUsage(AttributeTargets.Field, AllowMultiple = false)>]
type ArgumentNegateWithPrefixAttribute () =
inherit Attribute ()

View File

@@ -13,6 +13,8 @@ WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute..ctor [constructor]: string WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.ArgumentLongForm inherit System.Attribute WoofWare.Myriad.Plugins.ArgumentLongForm inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentLongForm..ctor [constructor]: string WoofWare.Myriad.Plugins.ArgumentLongForm..ctor [constructor]: string
WoofWare.Myriad.Plugins.ArgumentNegateWithPrefixAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentNegateWithPrefixAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.CreateCatamorphismAttribute inherit System.Attribute WoofWare.Myriad.Plugins.CreateCatamorphismAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.CreateCatamorphismAttribute..ctor [constructor]: string WoofWare.Myriad.Plugins.CreateCatamorphismAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.GenerateCapturingMockAttribute inherit System.Attribute WoofWare.Myriad.Plugins.GenerateCapturingMockAttribute inherit System.Attribute

View File

@@ -17,10 +17,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ApiSurface" Version="5.0.1" /> <PackageReference Include="ApiSurface" Version="5.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/> <PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/> <PackageReference Include="NUnit3TestAdapter" Version="5.2.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@@ -0,0 +1,372 @@
namespace WoofWare.Myriad.Plugins.Test
open System.Threading
open NUnit.Framework
open FsUnitTyped
open ConsumePlugin
[<TestFixture>]
module TestArgParserNegation =
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo=true sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature=true" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo true sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ; "true" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo=false sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature=false" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo false sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ; "false" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo=true sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature=true" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo true sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "true" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo=false sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature=false" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo false sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "false" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run=true sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run=true" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run true sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ; "true" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run=false sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run=false" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run false sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ; "false" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run=true sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run=true" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run true sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ; "true" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run=false sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run=false" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run false sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ; "false" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--verbose" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --v`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--v" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-verbose" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-v`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-v" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --verbose=false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--verbose=false" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-v=false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-v=false" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``Help text shows both standard and negated forms for boolean`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> BoolNegation.parse' getEnvVar [ "--help" ] |> ignore<BoolNegation>)
exc.Message
|> shouldEqual
"""Help text requested.
--enable-feature / --no-enable-feature bool"""
[<Test>]
let ``Help text shows both standard and negated forms for flag DU`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> FlagNegation.parse' getEnvVar [ "--help" ] |> ignore<FlagNegation>)
exc.Message
|> shouldEqual
"""Help text requested.
--dry-run / --no-dry-run bool"""
[<Test>]
let ``Help text with multiple long forms shows all variants`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () ->
MultipleFormsNegation.parse' getEnvVar [ "--help" ]
|> ignore<MultipleFormsNegation>
)
exc.Message
|> shouldEqual
"""Help text requested.
--verbose / --v / --no-verbose / --no-v bool"""
[<Test>]
let ``Multiple occurrences error: --foo and --no-foo both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--enable-feature" ; "--no-enable-feature" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``Multiple occurrences error: --no-foo and --foo both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "--enable-feature" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``Multiple occurrences error: --foo=true and --no-foo=false both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--enable-feature=true" ; "--no-enable-feature=false" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``CombinedFeatures: verbose with default value works with negation`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = CombinedFeatures.parse' getEnvVar [ "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice2Of2 false)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: can override default with --verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result =
CombinedFeatures.parse' getEnvVar [ "--verbose" ; "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice1Of2 true)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: can override default with --no-verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result =
CombinedFeatures.parse' getEnvVar [ "--no-verbose" ; "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice1Of2 false)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: --no-debug sets debug to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = CombinedFeatures.parse' getEnvVar [ "--no-debug" ; "--normal-bool" ]
result.Debug |> shouldEqual false
[<Test>]
let ``CombinedFeatures: help text shows negation for fields with attribute`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> CombinedFeatures.parse' getEnvVar [ "--help" ] |> ignore<CombinedFeatures>)
// Verbose and Debug should have --no- forms, NormalBool should not
exc.Message |> shouldContainText "--verbose / --no-verbose"
exc.Message |> shouldContainText "--debug / --no-debug"
exc.Message |> shouldContainText "--normal-bool bool"
exc.Message |> shouldNotContainText "--no-normal-bool"
[<Test>]
let ``Case insensitivity: --NO-ENABLE-FEATURE works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--NO-ENABLE-FEATURE" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Case insensitivity: --No-Enable-Feature works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--No-Enable-Feature" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Case insensitivity: --NO-DRY-RUN works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--NO-DRY-RUN" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet

View File

@@ -11,7 +11,7 @@ module TestCapturingMockGenerator =
[<Test>] [<Test>]
let ``Example of use: IPublicType`` () = let ``Example of use: IPublicType`` () =
let mock : IPublicType = let mock : IPublicType =
{ PublicTypeMock.Empty with { PublicTypeMock.Empty () with
Mem1 = fun (s, count) -> List.replicate count s Mem1 = fun (s, count) -> List.replicate count s
} }
:> _ :> _
@@ -38,7 +38,7 @@ module TestCapturingMockGenerator =
[<Test>] [<Test>]
let ``Example of use: properties`` () = let ``Example of use: properties`` () =
let mock : TypeWithProperties = let mock : TypeWithProperties =
{ TypeWithPropertiesMock.Empty with { TypeWithPropertiesMock.Empty () with
Mem1 = fun i -> async { return Option.toArray i } Mem1 = fun i -> async { return Option.toArray i }
Prop1 = fun () -> 44 Prop1 = fun () -> 44
} }
@@ -70,3 +70,63 @@ module TestCapturingMockGenerator =
bar = 3 bar = 3
Arg1 = "hello" Arg1 = "hello"
} }
[<Test>]
let ``Example of use IAsyncDisposable`` () =
let mock' =
{ TypeWithAsyncDisposableMock.Empty () with
Mem1 = fun i -> async { return Option.toArray i }
Mem2 = fun () -> async { return [||] }
}
let mock = mock' :> TypeWithAsyncDisposable
mock.Mem1 (Some "hi") |> Async.RunSynchronously |> shouldEqual [| "hi" |]
mock.Mem2 () |> Async.RunSynchronously |> shouldEqual [||]
// Test that DisposeAsync returns a completed ValueTask
let asyncDisposable = mock :> IAsyncDisposable
let valueTask = asyncDisposable.DisposeAsync ()
valueTask.IsCompleted |> shouldEqual true
// Verify calls were captured
lock mock'.Calls.Mem1 (fun () -> Seq.toList mock'.Calls.Mem1)
|> List.exactlyOne
|> shouldEqual (Some "hi")
lock mock'.Calls.Mem2 (fun () -> Seq.toList mock'.Calls.Mem2)
|> List.exactlyOne
|> shouldEqual ()
[<Test>]
let ``Example of use: Both IDisposable and IAsyncDisposable`` () =
let mutable disposed = false
let mutable disposedAsync = false
let mock' =
{ TypeWithBothDisposablesMock.Empty () with
Dispose = fun () -> disposed <- true
DisposeAsync =
fun () ->
disposedAsync <- true
System.Threading.Tasks.ValueTask ()
Mem1 = fun s -> s.Length
}
let mock = mock' :> TypeWithBothDisposables
mock.Mem1 "hello" |> shouldEqual 5
mock.Mem1 "world" |> shouldEqual 5
// Test IDisposable.Dispose
(mock :> IDisposable).Dispose ()
disposed |> shouldEqual true
// Test IAsyncDisposable.DisposeAsync
let valueTask = (mock :> IAsyncDisposable).DisposeAsync ()
valueTask.IsCompleted |> shouldEqual true
disposedAsync |> shouldEqual true
// Verify calls were captured
lock mock'.Calls.Mem1 (fun () -> Seq.toList mock'.Calls.Mem1)
|> shouldEqual [ "hello" ; "world" ]

View File

@@ -11,7 +11,7 @@ module TestCapturingMockGeneratorNoAttr =
[<Test>] [<Test>]
let ``Example of use: IPublicType`` () = let ``Example of use: IPublicType`` () =
let mock : IPublicTypeNoAttr = let mock : IPublicTypeNoAttr =
{ PublicTypeNoAttrMock.Empty with { PublicTypeNoAttrMock.Empty () with
Mem1 = fun (s, count) -> List.replicate count s Mem1 = fun (s, count) -> List.replicate count s
} }
:> _ :> _

View File

@@ -47,3 +47,45 @@ module TestMockGenerator =
mock.Mem1 (Some "hi") |> Async.RunSynchronously |> shouldEqual [| "hi" |] mock.Mem1 (Some "hi") |> Async.RunSynchronously |> shouldEqual [| "hi" |]
mock.Prop1 |> shouldEqual 44 mock.Prop1 |> shouldEqual 44
[<Test>]
let ``Example of use: IAsyncDisposable`` () =
let mock : TypeWithAsyncDisposable =
{ TypeWithAsyncDisposableMock.Empty with
Mem1 = fun i -> async { return Option.toArray i }
}
:> _
mock.Mem1 (Some "hi") |> Async.RunSynchronously |> shouldEqual [| "hi" |]
// Test that DisposeAsync returns a completed ValueTask
let asyncDisposable = mock :> IAsyncDisposable
let valueTask = asyncDisposable.DisposeAsync ()
valueTask.IsCompleted |> shouldEqual true
[<Test>]
let ``Example of use: Both IDisposable and IAsyncDisposable`` () =
let mutable disposed = false
let mutable disposedAsync = false
let mock : TypeWithBothDisposables =
{ TypeWithBothDisposablesMock.Empty with
Dispose = fun () -> disposed <- true
DisposeAsync =
fun () ->
disposedAsync <- true
System.Threading.Tasks.ValueTask ()
Mem1 = fun s -> s.Length
}
:> _
mock.Mem1 "hello" |> shouldEqual 5
// Test IDisposable.Dispose
(mock :> IDisposable).Dispose ()
disposed |> shouldEqual true
// Test IAsyncDisposable.DisposeAsync
let valueTask = (mock :> IAsyncDisposable).DisposeAsync ()
valueTask.IsCompleted |> shouldEqual true
disposedAsync |> shouldEqual true

View File

@@ -9,7 +9,6 @@
I have not yet seen a single instance where I care about this warning I have not yet seen a single instance where I care about this warning
--> -->
<NoWarn>$(NoWarn),NU1903</NoWarn> <NoWarn>$(NoWarn),NU1903</NoWarn>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -38,6 +37,7 @@
<Compile Include="TestCataGenerator\TestMyList.fs" /> <Compile Include="TestCataGenerator\TestMyList.fs" />
<Compile Include="TestCataGenerator\TestMyList2.fs" /> <Compile Include="TestCataGenerator\TestMyList2.fs" />
<Compile Include="TestArgParser\TestArgParser.fs" /> <Compile Include="TestArgParser\TestArgParser.fs" />
<Compile Include="TestArgParser\TestArgParserNegation.fs" />
<Compile Include="TestSwagger\TestSwaggerParse.fs" /> <Compile Include="TestSwagger\TestSwaggerParse.fs" />
<Compile Include="TestSwagger\TestOpenApi3Parse.fs" /> <Compile Include="TestSwagger\TestOpenApi3Parse.fs" />
<EmbeddedResource Include="TestSwagger\api-with-examples.json" /> <EmbeddedResource Include="TestSwagger\api-with-examples.json" />
@@ -55,13 +55,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ApiSurface" Version="5.0.1" /> <PackageReference Include="ApiSurface" Version="5.0.3" />
<PackageReference Include="FsCheck" Version="3.3.1" /> <PackageReference Include="FsCheck" Version="3.3.2" />
<PackageReference Include="FsUnit" Version="7.1.1" /> <PackageReference Include="FsUnit" Version="7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="NUnit" Version="4.3.2" /> <PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="WoofWare.Expect" Version="0.8.2" /> <PackageReference Include="WoofWare.Expect" Version="0.8.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -72,16 +72,38 @@ type private ParseFunction<'acc> =
/// and choices and so on. /// and choices and so on.
TargetType : SynType TargetType : SynType
Accumulation : 'acc Accumulation : 'acc
/// If true, this boolean/flag field accepts --no- prefix for negation (has [<ArgumentNegateWithPrefix>])
AcceptsNegation : bool
} }
/// A SynExpr of type `string` which we can display to the user at generated-program runtime to display all /// A SynExpr of type `string` which we can display to the user at generated-program runtime to display all
/// the ways they can refer to this arg. /// the ways they can refer to this arg.
member arg.HumanReadableArgForm : SynExpr = member arg.HumanReadableArgForm : SynExpr =
let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / " if arg.AcceptsNegation then
// Include both standard and --no- variants
// E.g., "--foo / --bar / --no-foo / --no-bar"
let standardFormatString =
List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm) let negatedFormatString =
||> List.fold SynExpr.applyFunction List.replicate arg.ArgForm.Length "--no-%s" |> String.concat " / "
|> SynExpr.paren
let combinedFormatString = standardFormatString + " / " + negatedFormatString
// Apply all arg forms twice (once for standard, once for negated)
let allArgForms = arg.ArgForm @ arg.ArgForm
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst combinedFormatString),
allArgForms)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
else
// Standard behavior: just --foo / --bar
let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
type private ChoicePositional = type private ChoicePositional =
@@ -222,8 +244,8 @@ module private ParseTree =
nonPos @ nonPos2, Some pos nonPos @ nonPos2, Some pos
|> fun (nonPos, pos) -> |> fun (nonPos, pos) ->
let duplicateArgs = // Extract all arg form strings for validation
// This is best-effort. We can't necessarily detect all SynExprs here, but usually it'll be strings. let allArgForms =
Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm) Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm)
|> Seq.concat |> Seq.concat
|> Seq.choose (fun expr -> |> Seq.choose (fun expr ->
@@ -232,14 +254,57 @@ module private ParseTree =
| _ -> None | _ -> None
) )
|> List.ofSeq |> List.ofSeq
// Check for direct duplicates
let duplicateArgs =
allArgForms
|> List.groupBy id |> List.groupBy id
|> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None) |> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None)
match duplicateArgs with match duplicateArgs with
| [] -> nonPos, pos | dups when not dups.IsEmpty ->
| dups ->
let dups = dups |> String.concat " " let dups = dups |> String.concat " "
failwith $"Duplicate args detected! %s{dups}" failwith $"Duplicate args detected! %s{dups}"
| _ ->
// Check for --no- prefix conflicts
// Build a map of arg names that have AcceptsNegation=true
let negatedForms =
nonPos
|> List.filter _.AcceptsNegation
|> List.collect (fun pf ->
pf.ArgForm
|> List.choose (fun expr ->
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String (s, _, _), _) -> Some (pf.FieldName.idText, s)
| _ -> None
)
)
|> List.map (fun (fieldName, argForm) -> $"no-%s{argForm}", fieldName)
|> Map.ofList
// Check if any existing arg form conflicts with a --no- variant
let conflicts =
allArgForms
|> List.choose (fun argForm ->
match negatedForms.TryFind argForm with
| Some fieldWithNegation -> Some (argForm, fieldWithNegation)
| None -> None
)
match conflicts with
| [] -> ()
| conflicts ->
let conflictMessages =
conflicts
|> List.map (fun (argForm, fieldWithNegation) ->
$"Argument name conflict: '--%s{argForm}' collides with the --no- variant of field '%s{fieldWithNegation}' (which has [<ArgumentNegateWithPrefix>])"
)
|> String.concat "\n"
failwith $"Conflicting argument names detected:\n%s{conflictMessages}"
nonPos, pos
/// Build the return value. /// Build the return value.
let rec instantiate<'a> (tree : ParseTree<'a>) : SynExpr = let rec instantiate<'a> (tree : ParseTree<'a>) : SynExpr =
@@ -615,6 +680,7 @@ module internal ArgParserGenerator =
ArgForm = longForms ArgForm = longForms
Help = helpText Help = helpText
BoolCases = isBoolLike BoolCases = isBoolLike
AcceptsNegation = false
} }
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl) |> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.List Accumulation.Required -> | Accumulation.List Accumulation.Required ->
@@ -627,6 +693,7 @@ module internal ArgParserGenerator =
ArgForm = longForms ArgForm = longForms
Help = helpText Help = helpText
BoolCases = isBoolLike BoolCases = isBoolLike
AcceptsNegation = false
} }
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl) |> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.Choice _ | Accumulation.Choice _
@@ -651,6 +718,27 @@ module internal ArgParserGenerator =
Some (Choice2Of2 ()) Some (Choice2Of2 ())
| parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2 | parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2
let hasNegateAttr =
attrs
|> List.exists (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (ident, _, _) ->
match (List.last ident).idText with
| "ArgumentNegateWithPrefixAttribute"
| "ArgumentNegateWithPrefix" -> true
| _ -> false
)
let acceptsNegation =
if hasNegateAttr then
match isBoolLike with
| Some _ -> true
| None ->
failwith
$"[<ArgumentNegateWithPrefix>] can only be applied to boolean or flag DU fields, but was applied to field %s{ident.idText} of type %O{fieldType}"
else
false
{ {
FieldName = ident FieldName = ident
Parser = parser Parser = parser
@@ -660,6 +748,7 @@ module internal ArgParserGenerator =
ArgForm = longForms ArgForm = longForms
Help = helpText Help = helpText
BoolCases = isBoolLike BoolCases = isBoolLike
AcceptsNegation = acceptsNegation
} }
|> fun t -> ParseTree.NonPositionalLeaf (t, Teq.refl) |> fun t -> ParseTree.NonPositionalLeaf (t, Teq.refl)
|> ParseTreeCrate.make |> ParseTreeCrate.make
@@ -773,6 +862,37 @@ module internal ArgParserGenerator =
) )
|> SynBinding.basic [ Ident.create "helpText" ] [ SynPat.unit ] |> SynBinding.basic [ Ident.create "helpText" ] [ SynPat.unit ]
/// Helper to create a negated parser for boolean/flag fields.
/// Returns a SynExpr that represents: string -> (negated bool or negated flag DU)
/// For booleans: `fun x -> not (Boolean.Parse x)`
/// For flag DUs: `fun x -> FlagDu.FromBoolean flagDu (not (Boolean.Parse x))`
let private createNegatedParser (arg : ParseFunction<'acc>) : SynExpr =
match arg.BoolCases with
| None -> failwith $"LOGIC ERROR: createNegatedParser called on non-boolean field %s{arg.FieldName.idText}"
| Some (Choice2Of2 ()) ->
// Boolean: parse and negate
// fun x -> not (System.Boolean.Parse x)
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> SynExpr.createLambda "x"
| Some (Choice1Of2 flagDu) ->
// Flag DU: parse as bool, negate, then convert to flag DU
// fun x -> x |> System.Boolean.Parse |> not |> FlagDu.FromBoolean flagDu
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> FlagDu.FromBoolean flagDu
|> SynExpr.createLambda "x"
/// `let processKeyValue (key : string) (value : string) : Result<unit, string option> = ...` /// `let processKeyValue (key : string) (value : string) : Result<unit, string option> = ...`
/// Returns a possible error. /// Returns a possible error.
/// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do /// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do
@@ -786,109 +906,209 @@ module internal ArgParserGenerator =
let args = let args =
args args
|> List.map (fun arg -> |> List.map (fun arg ->
match arg.Accumulation with let assignmentExpr =
| Accumulation.Required match arg.Accumulation with
| Accumulation.Choice _ | Accumulation.Required
| Accumulation.Optional -> | Accumulation.Choice _
let multipleErrorMessage = | Accumulation.Optional ->
SynExpr.createIdent "sprintf" let multipleErrorMessage =
|> SynExpr.applyTo (SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s") SynExpr.createIdent "sprintf"
|> SynExpr.applyTo arg.HumanReadableArgForm |> SynExpr.applyTo (
|> SynExpr.applyTo (SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren) SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s"
|> SynExpr.applyTo ( )
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren |> SynExpr.applyTo arg.HumanReadableArgForm
) |> SynExpr.applyTo (
SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let performAssignment = let performAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[ [
SynExpr.createIdent "value" SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser |> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") |> SynExpr.pipeThroughFunction (
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable) SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ()) SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
] ]
|> SynExpr.sequential |> SynExpr.sequential
[ // Return (argForms, assignmentExpr), argMetadata
SynMatchClause.create (arg.ArgForm, assignmentExpr), Some arg
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
|> fun expr -> arg.ArgForm, expr
) )
let posArg = let posArg =
match pos with match pos with
| None -> [] | None -> []
| Some pos -> | Some pos ->
[ let posExpr =
SynExpr.createIdent "value" [
|> SynExpr.pipeThroughFunction pos.Parser SynExpr.createIdent "value"
|> fun p -> |> SynExpr.pipeThroughFunction pos.Parser
match pos.Accumulation with |> fun p ->
| ChoicePositional.Choice _ -> match pos.Accumulation with
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2") | ChoicePositional.Choice _ ->
| ChoicePositional.Normal _ -> p p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
|> SynExpr.pipeThroughFunction ( | ChoicePositional.Normal _ -> p
SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ] |> SynExpr.pipeThroughFunction (
) SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ]
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok") )
] SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
|> SynExpr.sequential ]
|> fun expr -> pos.ArgForm, expr |> SynExpr.sequential
|> List.singleton
// Positional args don't support negation, so metadata is None
[ (pos.ArgForm, posExpr), None ]
(SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), posArg @ args) (SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), posArg @ args)
||> List.fold (fun finalBranch (argForm, arg) -> ||> List.fold (fun finalBranch ((argForm, arg), argMetadata) ->
(finalBranch, argForm) (finalBranch, argForm)
||> List.fold (fun finalBranch argForm -> ||> List.fold (fun finalBranch argForm ->
arg // Standard match: --argForm
|> SynExpr.ifThenElse let standardMatch =
(SynExpr.applyFunction arg
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) |> SynExpr.ifThenElse
(SynExpr.tuple (SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
finalBranch
// If this arg accepts negation, also match --no-argForm
match argMetadata with
| None -> standardMatch
| Some (parseFn : ParseFunctionNonPositional) when parseFn.AcceptsNegation ->
// Create negated assignment (same structure as `arg` but with negated parser)
let negatedParser = createNegatedParser parseFn
let negatedArg =
match parseFn.Accumulation with
| Accumulation.Required
| Accumulation.Choice _
| Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (
SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s"
)
|> SynExpr.applyTo parseFn.HumanReadableArgForm
|> SynExpr.applyTo (
SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let performNegatedAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction negatedParser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI parseFn.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[ [
SynExpr.createIdent "key" SynMatchClause.create
SynExpr.applyFunction (SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.applyFunction (SynExpr.sequential
(SynExpr.createIdent "sprintf") [
(SynExpr.CreateConst "--%s")) multipleErrorMessage
argForm |> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
])) )
finalBranch SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performNegatedAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' parseFn.TargetVariable)
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction negatedParser
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ parseFn.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
| _ ->
failwith
"WoofWare.Myriad invariant violated: unexpected accumulation type for negated arg"
// Match --no-argForm
negatedArg
|> SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--no-%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
standardMatch
| Some _ -> standardMatch
) )
) )
|> SynBinding.basic |> SynBinding.basic
@@ -909,6 +1129,34 @@ module internal ArgParserGenerator =
|> PreXmlDoc.create' |> PreXmlDoc.create'
) )
/// Try to extract a constant bool value from a SynExpr.
/// Returns Some true/false if the expr is a constant bool, None otherwise.
let private tryGetConstBool (expr : SynExpr) : bool option =
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.Bool v, _) -> Some v
| _ -> None
/// Helper to get the "false" case for a boolean/flag field.
/// For booleans: `false`
/// For flag DUs: the case marked with [<ArgumentFlag false>]
let private getFalseCase (flag : ParseFunction<'a>) : SynExpr =
match flag.BoolCases with
| None -> failwith $"LOGIC ERROR: getFalseCase called on non-boolean field %s{flag.FieldName.idText}"
| Some (Choice2Of2 ()) ->
// Boolean: return false
SynExpr.CreateConst false
| Some (Choice1Of2 flagDu) ->
// Flag DU: return the case associated with false
// Check which case has false as its arg
match tryGetConstBool flagDu.Case1Arg, tryGetConstBool flagDu.Case2Arg with
| Some false, _ -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ]
| _, Some false -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ]
| Some true, _ -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ]
| _, Some true -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ]
| None, None ->
// Can't determine at compile time, use FlagDu.FromBoolean with false
FlagDu.FromBoolean flagDu (SynExpr.CreateConst false)
/// `let setFlagValue (key : string) : bool = ...` /// `let setFlagValue (key : string) : bool = ...`
/// The second member of the `flags` list tuple is the constant "true" with which we will interpret the /// The second member of the `flags` list tuple is the constant "true" with which we will interpret the
/// arity-0 `--foo`. So in the case of a boolean-typed field, this is `true`; in the case of a Flag-typed field, /// arity-0 `--foo`. So in the case of a boolean-typed field, this is `true`; in the case of a Flag-typed field,
@@ -949,21 +1197,72 @@ module internal ArgParserGenerator =
(finalExpr, flag.ArgForm) (finalExpr, flag.ArgForm)
||> List.fold (fun finalExpr argForm -> ||> List.fold (fun finalExpr argForm ->
SynExpr.ifThenElse // Standard match: --argForm sets to trueCase
(SynExpr.applyFunction let standardMatch =
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) SynExpr.ifThenElse
(SynExpr.tuple (SynExpr.applyFunction
[ (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
SynExpr.createIdent "key" (SynExpr.tuple
SynExpr.applyFunction [
(SynExpr.applyFunction SynExpr.createIdent "key"
(SynExpr.createIdent "sprintf") SynExpr.applyFunction
(SynExpr.CreateConst "--%s")) (SynExpr.applyFunction
argForm (SynExpr.createIdent "sprintf")
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] (SynExpr.CreateConst "--%s"))
])) argForm
finalExpr SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
matchFlag ]))
finalExpr
matchFlag
// If this flag accepts negation, also match --no-argForm sets to falseCase
if flag.AcceptsNegation then
let falseCase = getFalseCase flag
let matchNegatedFlag =
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
// This is an error, but it's one we can gracefully report at the end.
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.CreateConst true
])
SynMatchClause.create
(SynPat.named "None")
([
SynExpr.assign
(SynLongIdent.createI flag.TargetVariable)
(SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") falseCase)
SynExpr.CreateConst true
]
|> SynExpr.sequential)
]
|> SynExpr.createMatch (SynExpr.createIdent' flag.TargetVariable)
// Match --no-argForm
SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--no-%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
standardMatch
matchNegatedFlag
else
standardMatch
) )
) )
|> SynBinding.basic [ Ident.create "setFlagValue" ] [ SynPat.annotateType SynType.string (SynPat.named "key") ] |> SynBinding.basic [ Ident.create "setFlagValue" ] [ SynPat.annotateType SynType.string (SynPat.named "key") ]

View File

@@ -19,7 +19,9 @@ module internal CapturingInterfaceMockGenerator =
open Fantomas.FCS.Text.Range open Fantomas.FCS.Text.Range
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
type private KnownInheritance = | IDisposable type private KnownInheritance =
| IDisposable
| IAsyncDisposable
/// Expects the input `args` list to have more than one element. /// Expects the input `args` list to have more than one element.
let private createTypeForArgs let private createTypeForArgs
@@ -209,6 +211,8 @@ module internal CapturingInterfaceMockGenerator =
| [] -> failwith "Unexpected empty identifier in inheritance declaration" | [] -> failwith "Unexpected empty identifier in inheritance declaration"
| [ "IDisposable" ] | [ "IDisposable" ]
| [ "System" ; "IDisposable" ] -> KnownInheritance.IDisposable | [ "System" ; "IDisposable" ] -> KnownInheritance.IDisposable
| [ "IAsyncDisposable" ]
| [ "System" ; "IAsyncDisposable" ] -> KnownInheritance.IAsyncDisposable
| _ -> failwithf $"Unrecognised inheritance identifier: %+A{name}" | _ -> failwithf $"Unrecognised inheritance identifier: %+A{name}"
| x -> failwithf $"Unrecognised type in inheritance: %+A{x}" | x -> failwithf $"Unrecognised type in inheritance: %+A{x}"
) )
@@ -250,12 +254,26 @@ module internal CapturingInterfaceMockGenerator =
let emptyRecordFieldInstantiations = let emptyRecordFieldInstantiations =
let interfaceExtras = let interfaceExtras =
if inherits.Contains KnownInheritance.IDisposable then let disposable =
let unitFun = SynExpr.createThunk (SynExpr.CreateConst ()) if inherits.Contains KnownInheritance.IDisposable then
let unitFun = SynExpr.createThunk (SynExpr.CreateConst ())
[ SynLongIdent.createS "Dispose", unitFun ]
else
[]
[ SynLongIdent.createS "Dispose", unitFun ] let asyncDisposable =
else if inherits.Contains KnownInheritance.IAsyncDisposable then
[] let valueTaskCtor =
SynExpr.createLongIdent [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynExpr.paren
|> SynExpr.createLambda "()"
[ SynLongIdent.createS "DisposeAsync", valueTaskCtor ]
else
[]
disposable @ asyncDisposable
let originalMembers = let originalMembers =
fields fields
@@ -273,28 +291,44 @@ module internal CapturingInterfaceMockGenerator =
let staticMemberEmpty = let staticMemberEmpty =
SynBinding.basic SynBinding.basic
[ Ident.create "Empty" ] [ Ident.create "Empty" ]
(if interfaceType.Generics.IsNone then [ SynPat.unit ]
[]
else
[ SynPat.unit ])
(SynExpr.createRecord None emptyRecordFieldInstantiations) (SynExpr.createRecord None emptyRecordFieldInstantiations)
|> SynBinding.withXmlDoc (PreXmlDoc.create "An implementation where every non-unit method throws.") |> SynBinding.withXmlDoc (PreXmlDoc.create "An implementation where every non-disposal method throws.")
|> SynBinding.withReturnAnnotation constructorReturnType |> SynBinding.withReturnAnnotation constructorReturnType
|> SynMemberDefn.staticMember |> SynMemberDefn.staticMember
let recordFields = let recordFields =
let extras = let extras =
if inherits.Contains KnownInheritance.IDisposable then let disposable =
{ if inherits.Contains KnownInheritance.IDisposable then
Attrs = [] {
Ident = Some (Ident.create "Dispose") Attrs = []
Type = SynType.funFromDomain SynType.unit SynType.unit Ident = Some (Ident.create "Dispose")
} Type = SynType.funFromDomain SynType.unit SynType.unit
|> SynField.make }
|> SynField.withDocString (PreXmlDoc.create "Implementation of IDisposable.Dispose") |> SynField.make
|> List.singleton |> SynField.withDocString (PreXmlDoc.create "Implementation of IDisposable.Dispose")
else |> List.singleton
[] else
[]
let asyncDisposable =
if inherits.Contains KnownInheritance.IAsyncDisposable then
{
Attrs = []
Ident = Some (Ident.create "DisposeAsync")
Type =
SynType.funFromDomain
SynType.unit
(SynType.createLongIdent' [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ])
}
|> SynField.make
|> SynField.withDocString (PreXmlDoc.create "Implementation of IAsyncDisposable.DisposeAsync")
|> List.singleton
else
[]
disposable @ asyncDisposable
let nonExtras = let nonExtras =
fields |> Map.toSeq |> Seq.map (fun (_, (field, _)) -> field) |> Seq.toList fields |> Map.toSeq |> Seq.map (fun (_, (field, _)) -> field) |> Seq.toList
@@ -531,6 +565,23 @@ module internal CapturingInterfaceMockGenerator =
Some [ mem ], Some [ mem ],
range0 range0
) )
| KnownInheritance.IAsyncDisposable ->
let mem =
SynExpr.createLongIdent [ "this" ; "DisposeAsync" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "this" ; Ident.create "DisposeAsync" ] [ SynPat.unit ]
|> SynBinding.withReturnAnnotation (
SynType.createLongIdent' [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ]
)
|> SynMemberDefn.memberImplementation
SynMemberDefn.Interface (
SynType.createLongIdent' [ "System" ; "IAsyncDisposable" ],
Some range0,
Some [ mem ],
range0
)
) )
|> Seq.toList |> Seq.toList

View File

@@ -20,7 +20,9 @@ module internal InterfaceMockGenerator =
| Some id -> id | Some id -> id
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
type private KnownInheritance = | IDisposable type private KnownInheritance =
| IDisposable
| IAsyncDisposable
let createType let createType
(spec : GenerateMockOutputSpec) (spec : GenerateMockOutputSpec)
@@ -39,6 +41,8 @@ module internal InterfaceMockGenerator =
| [] -> failwith "Unexpected empty identifier in inheritance declaration" | [] -> failwith "Unexpected empty identifier in inheritance declaration"
| [ "IDisposable" ] | [ "IDisposable" ]
| [ "System" ; "IDisposable" ] -> KnownInheritance.IDisposable | [ "System" ; "IDisposable" ] -> KnownInheritance.IDisposable
| [ "IAsyncDisposable" ]
| [ "System" ; "IAsyncDisposable" ] -> KnownInheritance.IAsyncDisposable
| _ -> failwithf "Unrecognised inheritance identifier: %+A" name | _ -> failwithf "Unrecognised inheritance identifier: %+A" name
| x -> failwithf "Unrecognised type in inheritance: %+A" x | x -> failwithf "Unrecognised type in inheritance: %+A" x
) )
@@ -69,12 +73,26 @@ module internal InterfaceMockGenerator =
let constructorFields = let constructorFields =
let extras = let extras =
if inherits.Contains KnownInheritance.IDisposable then let disposable =
let unitFun = SynExpr.createThunk (SynExpr.CreateConst ()) if inherits.Contains KnownInheritance.IDisposable then
let unitFun = SynExpr.createThunk (SynExpr.CreateConst ())
[ SynLongIdent.createS "Dispose", unitFun ]
else
[]
[ SynLongIdent.createS "Dispose", unitFun ] let asyncDisposable =
else if inherits.Contains KnownInheritance.IAsyncDisposable then
[] let valueTaskCtor =
SynExpr.createLongIdent [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynExpr.paren
|> SynExpr.createLambda "()"
[ SynLongIdent.createS "DisposeAsync", valueTaskCtor ]
else
[]
disposable @ asyncDisposable
let nonExtras = let nonExtras =
fields fields
@@ -90,23 +108,42 @@ module internal InterfaceMockGenerator =
else else
[ SynPat.unit ]) [ SynPat.unit ])
(SynExpr.createRecord None constructorFields) (SynExpr.createRecord None constructorFields)
|> SynBinding.withXmlDoc (PreXmlDoc.create "An implementation where every method throws.") |> SynBinding.withXmlDoc (PreXmlDoc.create "An implementation where every non-disposal method throws.")
|> SynBinding.withReturnAnnotation constructorReturnType |> SynBinding.withReturnAnnotation constructorReturnType
|> SynMemberDefn.staticMember |> SynMemberDefn.staticMember
let fields = let fields =
let extras = let extras =
if inherits.Contains KnownInheritance.IDisposable then let disposable =
{ if inherits.Contains KnownInheritance.IDisposable then
Attrs = [] {
Ident = Some (Ident.create "Dispose") Attrs = []
Type = SynType.funFromDomain SynType.unit SynType.unit Ident = Some (Ident.create "Dispose")
} Type = SynType.funFromDomain SynType.unit SynType.unit
|> SynField.make }
|> SynField.withDocString (PreXmlDoc.create "Implementation of IDisposable.Dispose") |> SynField.make
|> List.singleton |> SynField.withDocString (PreXmlDoc.create "Implementation of IDisposable.Dispose")
else |> List.singleton
[] else
[]
let asyncDisposable =
if inherits.Contains KnownInheritance.IAsyncDisposable then
{
Attrs = []
Ident = Some (Ident.create "DisposeAsync")
Type =
SynType.funFromDomain
SynType.unit
(SynType.createLongIdent' [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ])
}
|> SynField.make
|> SynField.withDocString (PreXmlDoc.create "Implementation of IAsyncDisposable.DisposeAsync")
|> List.singleton
else
[]
disposable @ asyncDisposable
extras @ fields extras @ fields
@@ -212,6 +249,23 @@ module internal InterfaceMockGenerator =
Some [ mem ], Some [ mem ],
range0 range0
) )
| KnownInheritance.IAsyncDisposable ->
let mem =
SynExpr.createLongIdent [ "this" ; "DisposeAsync" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "this" ; Ident.create "DisposeAsync" ] [ SynPat.unit ]
|> SynBinding.withReturnAnnotation (
SynType.createLongIdent' [ "System" ; "Threading" ; "Tasks" ; "ValueTask" ]
)
|> SynMemberDefn.memberImplementation
SynMemberDefn.Interface (
SynType.createLongIdent' [ "System" ; "IAsyncDisposable" ],
Some range0,
Some [ mem ],
range0
)
) )
|> Seq.toList |> Seq.toList

View File

@@ -21,8 +21,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Myriad.Core" Version="0.8.3" /> <PackageReference Include="Myriad.Core" Version="0.8.3" />
<PackageReference Include="TypeEquality" Version="0.3.0" /> <PackageReference Include="TypeEquality" Version="0.4.2" />
<PackageReference Include="WoofWare.Whippet.Fantomas" Version="0.6.3" /> <PackageReference Include="WoofWare.Whippet.Fantomas" Version="0.6.4" />
<!-- the lowest version allowed by Myriad.Core --> <!-- the lowest version allowed by Myriad.Core -->
<PackageReference Update="FSharp.Core" Version="6.0.1" PrivateAssets="all"/> <PackageReference Update="FSharp.Core" Version="6.0.1" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,5 +1,5 @@
{ {
"version": "8.1", "version": "9.1",
"publicReleaseRefSpec": [ "publicReleaseRefSpec": [
"^refs/heads/main$" "^refs/heads/main$"
], ],
@@ -11,4 +11,4 @@
":/README.md", ":/README.md",
":/Directory.Build.props" ":/Directory.Build.props"
] ]
} }

View File

@@ -10,7 +10,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.17.0]" /> <PackageDownload Include="WoofWare.FSharpAnalyzers" Version="[0.2.2]" />
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.20.0]" />
</ItemGroup> </ItemGroup>
</Project> </Project>

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1758976413, "lastModified": 1763191728,
"narHash": "sha256-hEIDTaIqvW1NMfaNgz6pjhZPZKTmACJmXxGr/H6isIg=", "narHash": "sha256-esRhOS0APE6k40Hs/jjReXg+rx+J5LkWw7cuWFKlwYA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e3a3b32cc234f1683258d36c6232f150d57df015", "rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,8 +1,8 @@
[ [
{ {
"pname": "ApiSurface", "pname": "ApiSurface",
"version": "5.0.1", "version": "5.0.3",
"hash": "sha256-0GMXEMFgWbbE2OGxW+6h4zGgQHg+IZy1aI13Dn97xSU=" "hash": "sha256-uU5mqLL6zMt17oPYMzhB57ryYC6O6FzSjmdTFg7LvNo="
}, },
{ {
"pname": "fantomas", "pname": "fantomas",
@@ -26,13 +26,13 @@
}, },
{ {
"pname": "FsCheck", "pname": "FsCheck",
"version": "3.3.1", "version": "3.3.2",
"hash": "sha256-k65ksdOSOGz+meRUUND+yuqJtm5ChaKuaxmRIdKzx2Y=" "hash": "sha256-3ydyTGpqySynjbcWbmFVeCBnT3KDH3miPSJYJlyxrGs="
}, },
{ {
"pname": "fsharp-analyzers", "pname": "fsharp-analyzers",
"version": "0.32.1", "version": "0.34.1",
"hash": "sha256-le6rPnAF7cKGBZ2w8H2u9glK+6rT2ZjiAVnrkH2IhrM=" "hash": "sha256-Y6PzfVGob2EgX29ZhZIde5EhiZ28Y1+U2pJ6ybIsHV0="
}, },
{ {
"pname": "FSharp.Core", "pname": "FSharp.Core",
@@ -61,8 +61,8 @@
}, },
{ {
"pname": "Microsoft.ApplicationInsights", "pname": "Microsoft.ApplicationInsights",
"version": "2.22.0", "version": "2.23.0",
"hash": "sha256-mUQ63atpT00r49ca50uZu2YCiLg3yd6r3HzTryqcuEA=" "hash": "sha256-5sf3bg7CZZjHseK+F3foOchEhmVeioePxMZVvS6Rjb0="
}, },
{ {
"pname": "Microsoft.AspNetCore.App.Ref", "pname": "Microsoft.AspNetCore.App.Ref",
@@ -91,13 +91,13 @@
}, },
{ {
"pname": "Microsoft.CodeCoverage", "pname": "Microsoft.CodeCoverage",
"version": "17.14.1", "version": "18.0.1",
"hash": "sha256-f8QytG8GvRoP47rO2KEmnDLxIpyesaq26TFjDdW40Gs=" "hash": "sha256-G6y5iyHZ3R2shlLCW/uTusio/UqcnWT79X+UAbxvDQY="
}, },
{ {
"pname": "Microsoft.NET.Test.Sdk", "pname": "Microsoft.NET.Test.Sdk",
"version": "17.14.1", "version": "18.0.1",
"hash": "sha256-mZUzDFvFp7x1nKrcnRd0hhbNu5g8EQYt8SKnRgdhT/A=" "hash": "sha256-0c3/rp9di0w7E5UmfRh6Prrm3Aeyi8NOj5bm2i6jAh0="
}, },
{ {
"pname": "Microsoft.NETCore.App.Host.linux-arm64", "pname": "Microsoft.NETCore.App.Host.linux-arm64",
@@ -166,43 +166,48 @@
}, },
{ {
"pname": "Microsoft.Testing.Extensions.Telemetry", "pname": "Microsoft.Testing.Extensions.Telemetry",
"version": "1.5.3", "version": "1.9.0",
"hash": "sha256-bIXwPSa3jkr2b6xINOqMUs6/uj/r4oVFM7xq3uVIZDU=" "hash": "sha256-JT91ThKLEyoRS/8ZJqZwlSTT7ofC2QhNqPFI3pYmMaw="
}, },
{ {
"pname": "Microsoft.Testing.Extensions.TrxReport.Abstractions", "pname": "Microsoft.Testing.Extensions.TrxReport.Abstractions",
"version": "1.5.3", "version": "1.9.0",
"hash": "sha256-IfMRfcyaIKEMRtx326ICKtinDBEfGw/Sv8ZHawJ96Yc=" "hash": "sha256-oscZOEKw7gM6eRdDrOS3x+CwqIvXWRmfmi0ugCxBRw0="
}, },
{ {
"pname": "Microsoft.Testing.Extensions.VSTestBridge", "pname": "Microsoft.Testing.Extensions.VSTestBridge",
"version": "1.5.3", "version": "1.9.0",
"hash": "sha256-XpM/yFjhLSsuzyDV+xKubs4V1zVVYiV05E0+N4S1h0g=" "hash": "sha256-CadXLWD093sUDaWhnppzD9LvpxSRqqt93ZEOFiIAPyw="
}, },
{ {
"pname": "Microsoft.Testing.Platform", "pname": "Microsoft.Testing.Platform",
"version": "1.5.3", "version": "1.9.0",
"hash": "sha256-y61Iih6w5D79dmrj2V675mcaeIiHoj1HSa1FRit2BLM=" "hash": "sha256-6nzjoYbJOh7v/GB7d+TDuM0l/xglCshFX6KWjg7+cFI="
}, },
{ {
"pname": "Microsoft.Testing.Platform.MSBuild", "pname": "Microsoft.Testing.Platform.MSBuild",
"version": "1.5.3", "version": "1.9.0",
"hash": "sha256-YspvjE5Jfi587TAfsvfDVJXNrFOkx1B3y1CKV6m7YLY=" "hash": "sha256-/bileP4b+9RZp8yjgS6eynXwc2mohyyzf6p/0LZJd8I="
},
{
"pname": "Microsoft.TestPlatform.AdapterUtilities",
"version": "17.13.0",
"hash": "sha256-Vr+3Tad/h/nk7f/5HMExn3HvCGFCarehFAzJSfCBaOc="
}, },
{ {
"pname": "Microsoft.TestPlatform.ObjectModel", "pname": "Microsoft.TestPlatform.ObjectModel",
"version": "17.12.0", "version": "17.13.0",
"hash": "sha256-3XBHBSuCxggAIlHXmKNQNlPqMqwFlM952Av6RrLw1/w=" "hash": "sha256-6S0fjfj8vA+h6dJVNwLi6oZhYDO/I/6hBZaq2VTW+Uk="
}, },
{ {
"pname": "Microsoft.TestPlatform.ObjectModel", "pname": "Microsoft.TestPlatform.ObjectModel",
"version": "17.14.1", "version": "18.0.1",
"hash": "sha256-QMf6O+w0IT+16Mrzo7wn+N20f3L1/mDhs/qjmEo1rYs=" "hash": "sha256-oJbS7SZ46RzyOQ+gCysW7qJRy7V8RlQVa5d8uajb91M="
}, },
{ {
"pname": "Microsoft.TestPlatform.TestHost", "pname": "Microsoft.TestPlatform.TestHost",
"version": "17.14.1", "version": "18.0.1",
"hash": "sha256-1cxHWcvHRD7orQ3EEEPPxVGEkTpxom1/zoICC9SInJs=" "hash": "sha256-OXYf5vg4piDr10ve0bZ2ZSb+nb3yOiHayJV3cu5sMV4="
}, },
{ {
"pname": "Myriad.Core", "pname": "Myriad.Core",
@@ -216,8 +221,8 @@
}, },
{ {
"pname": "Nerdbank.GitVersioning", "pname": "Nerdbank.GitVersioning",
"version": "3.8.118", "version": "3.9.50",
"hash": "sha256-Hmyy0ZKOmwN4zIhI4+MqoN8geZNc1sd033aZJ6APrO8=" "hash": "sha256-BiBfXwr8ob2HTaFk2L5TwAgtvd/EPoqudSI9nhAjQPI="
}, },
{ {
"pname": "NETStandard.Library", "pname": "NETStandard.Library",
@@ -271,8 +276,8 @@
}, },
{ {
"pname": "NUnit3TestAdapter", "pname": "NUnit3TestAdapter",
"version": "5.0.0", "version": "5.2.0",
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0=" "hash": "sha256-ybTutL4VkX/fq61mS+O3Ruh+adic4fpv+MKgQ0IZvGg="
}, },
{ {
"pname": "RestEase", "pname": "RestEase",
@@ -376,17 +381,22 @@
}, },
{ {
"pname": "TypeEquality", "pname": "TypeEquality",
"version": "0.3.0", "version": "0.4.2",
"hash": "sha256-V50xAOzzyUJrY+MYPRxtnqW5MVeATXCes89wPprv1r4=" "hash": "sha256-YxK6BGHjcuP76j5BbTDi814jxGqOevQSgS00IJcjZSA="
}, },
{ {
"pname": "WoofWare.Expect", "pname": "WoofWare.Expect",
"version": "0.8.2", "version": "0.8.4",
"hash": "sha256-iqt4FPkUr24/4dnRfh7P5nPNNlaPzaItP6/yrGRrQyo=" "hash": "sha256-UI7f2nt4g4Gg1Ke/IChrA4fpVOYAChXpvR6zkKfkmzE="
},
{
"pname": "WoofWare.NUnitTestRunner",
"version": "0.3.9",
"hash": "sha256-+QVx5NYdY1JZoMcWfJRwFgvEj2dBxWlJU0mu1Hmnlhs="
}, },
{ {
"pname": "WoofWare.Whippet.Fantomas", "pname": "WoofWare.Whippet.Fantomas",
"version": "0.6.3", "version": "0.6.4",
"hash": "sha256-FkW/HtVp8/HE2k6d7yFpnJcnP3FNNe9kGlkoIWmNgDw=" "hash": "sha256-ScZ7EEcxLvXyam2ZVqDK58QlK3RcePWghzRvtLLLdZI="
} }
] ]