Repository navigation
test: add a Hedgehog adapter for MSTest property tests - #43
Open
xperiandri wants to merge 8 commits into
Open
xperiandri wants to merge 8 commits into
xperiandri wants to merge 8 commits into
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Generator edge cases, replay guidance, generic configuration inference, and cancellation cleanup contain correctness defects.
Review effort: Balanced
Findings: 4
Open (5)
Infer generic arguments from nested and repeated parameter types · New Handle zero endpoints without creating empty ranges · New Dispose generated arguments when cancellation skips invocation · New Preserve Property settings in ready-to-paste recheck hints · New Correct misspelling of overridden in target names and tests · New
What changed in this PR
Adds a Hedgehog-to-MSTest adapter and test suite to support replayable property tests for planned quotation/query translation work.
Changes:
- Adds property generation, shrinking, replay, cancellation, and MSTest lifecycle integration.
- Adds comprehensive adapter tests and third-party attribution.
- Integrates test discovery into the build pipeline.
| File | Description |
|---|---|
.fantomasignore |
Excludes upstream-ported code from formatting. |
.github/copilot-instructions.md |
Documents property-test conventions. |
Directory.Packages.props |
Adds Hedgehog dependency version. |
FSharp.Azure.Cosmos.slnf |
Adds adapter projects to the filter. |
FSharp.Azure.Cosmos.slnx |
Adds projects and notices. |
THIRD-PARTY-NOTICES.md |
Records copied code and license. |
build/build.fs |
Adds test-discovery target. |
tests/Directory.Build.props |
Excludes adapter projects from Cosmos infrastructure. |
tests/Hedgehog.MSTest/AutoGenConfig.fs |
Instantiates generator configurations. |
tests/Hedgehog.MSTest/GenAttribute.Prelude.fs |
Provides built-in generator attributes. |
tests/Hedgehog.MSTest/GenAttribute.fs |
Defines generator attribute abstractions. |
tests/Hedgehog.MSTest/Hedgehog.MSTest.fsproj |
Defines the adapter library. |
tests/Hedgehog.MSTest/InternalLogic.fs |
Implements generation, invocation, and reporting. |
tests/Hedgehog.MSTest/IPropertyAttribute.fs |
Defines shared property settings. |
tests/Hedgehog.MSTest/Prelude.fs |
Adds internal helpers. |
tests/Hedgehog.MSTest/PropertiesAttribute.fs |
Adds class-level defaults. |
tests/Hedgehog.MSTest/PropertyAttribute.fs |
Integrates Hedgehog with MSTest. |
tests/Hedgehog.MSTest/PropertyContext.fs |
Resolves effective property settings. |
tests/Hedgehog.MSTest/RecheckAttribute.fs |
Adds counterexample replay metadata. |
tests/Hedgehog.MSTest/ReflectionHelpers.fs |
Adds return-type inspection helpers. |
tests/Hedgehog.MSTest.Tests/Common.fs |
Defines shared test generators. |
tests/Hedgehog.MSTest.Tests/GenAttributePreludeTests.fs |
Tests built-in generator attributes. |
tests/Hedgehog.MSTest.Tests/Harness.fs |
Provides an adapter test harness. |
tests/Hedgehog.MSTest.Tests/Hedgehog.MSTest.Tests.fsproj |
Defines the adapter test application. |
tests/Hedgehog.MSTest.Tests/MSTestTests.fs |
Tests MSTest-specific behavior. |
tests/Hedgehog.MSTest.Tests/PropertyTests.fs |
Tests generation, shrinking, and settings. |
tests/Hedgehog.MSTest.Tests/testconfig.json |
Enables method-level parallel testing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
2 times, most recently
from
October 10, 2026 07:27
7a11186 to
c843b9b
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The review of #43 found that the paste-ready [<Property; Recheck("...")>] of a failed property replaces the method's own Property attribute, so settings such as its AutoGenConfig would be lost and the recheck data would no longer replay the reported case. The adapter now reports a [<Recheck("...")>] to add next to the Property attribute; the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The review of #43 found that the paste-ready [<Property; Recheck("...")>] of a failed property replaces the method's own Property attribute, so settings such as its AutoGenConfig would be lost and the recheck data would no longer replay the reported case. The adapter now reports a [<Recheck("...")>] to add next to the Property attribute; the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
commented
Oct 10, 2026
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The Hedgehog MSTest adapter of #43 is proposed upstream as hedgehogqa/fsharp-hedgehog#487. The record notes how the proposal differs from the in-repository copy (net8.0 and F# 8, upstream's documentation in the shared files, VSTest), that MSTest 4.2 is the adapter's lowest version, and that one test of #43 stops compiling on MSTest 4.5.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 6 tasks
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. Two things stay upstream's, so that the two copies remain comparable: the formatting, which is why .fantomasignore keeps the project, and nullness checking, which is off as in every upstream project. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 10, 2026 12:17
c843b9b to
6263965
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. Two things stay upstream's, so that the two copies remain comparable: the formatting, which is why .fantomasignore keeps the project, and nullness checking, which is off as in every upstream project. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 10, 2026 12:39
6263965 to
b9c2165
Compare
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 10, 2026 12:41
b9c2165 to
93ad18c
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. Two things stay upstream's, so that the two copies remain comparable: the formatting, which is why .fantomasignore keeps the project, and nullness checking, which is off as in every upstream project. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
2 times, most recently
from
October 10, 2026 13:19
1fb8a39 to
2206265
Compare
3 of 6 tasks
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The maintainer's reviews of 2026-10-10 changed several things the record describes, and it now says so where each belongs. The Hedgehog MSTest adapter (#43): the in-repository copy follows the coding guidelines of this repository, while the upstream pull request keeps upstream's style in a copy of its own, so a change of behaviour goes into both; an invocation that MSTest could not run stops the run instead of being shrunk; Odd and Even stay inside their range; DateOnly and TimeOnly get generators, upstream through hedgehogqa/fsharp-hedgehog#488. The open question of the adapter's code style is answered, with formatting and nullness checking still open. Collections (section 4): the shared collection functions of #47 that return voption and struct tuples, tested with properties in #50, and the rule of one module per pipeline of #49. Build (section 5): why FAKE's DotNet.test cannot run the test applications, and fsprojects/FAKE#2903, which adds DotNet.testMTP. The spec model (sections 6 and 16): EquatableArray and the ImmutableArray patterns are #48 and the model itself #42; the validator returns an ImmutableArray of struct errors. Golden baselines (section 13): a literal type provider was evaluated on the question asked in #46 and rejected, with the reasons. Tests (section 22): test category attributes sorted by name, Pascal-case assertion functions and ImmutableArray for shared test data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. - The error for an AutoGenConfig type without exactly one config member says "member" where upstream says "property": a method that takes AutoGenConfigArgs is accepted as well (automatic review of #50). Two things stay upstream's, so that the two copies remain comparable: the formatting, which is why .fantomasignore keeps the project, and nullness checking, which is off as in every upstream project. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 10, 2026
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 6 tasks
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 6 tasks
Start the Hedgehog MSTest adapter, tests/Hedgehog.MSTest (ADR 0001, sections 5, 14 and 22.6), with the nine files that Hedgehog.NUnit and Hedgehog.Xunit of hedgehogqa/fsharp-hedgehog share and that do not depend on a test framework: Prelude, ReflectionHelpers, AutoGenConfig, IPropertyAttribute, RecheckAttribute, GenAttribute, GenAttribute.Prelude, PropertyContext and InternalLogic, copied from src/Hedgehog.NUnit at the Hedgehog 2.0.4 release commit a469772 in upstream's file layout and compile order. The only change is the namespace, Hedgehog.NUnit becomes Hedgehog.MSTest, so this commit can be compared with upstream line by line; the MSTest attribute and the changes it needs follow separately. The code is Apache-2.0: every copied file names its origin, the copyright and the license in a header comment, and the new THIRD-PARTY-NOTICES.md records the source, the commit, the files and the license text. The same code is proposed upstream as src/Hedgehog.MSTest, so the project keeps upstream's conventions rather than the repository's: nullness checking is off (upstream enables it nowhere, and the copied files give 23 nullness warnings under the repository's Nullable=enable) and .fantomasignore excludes the project, so the Fantomas run of the build leaves upstream's formatting alone. It sets IsTestProject=false and IsPackable=false, because tests/Directory.Build.props marks every project under tests/ as a test project, references MSTest.TestFramework rather than the MSTest metapackage, and is excluded from the test infrastructure reference of tests/Directory.Build.props, because the adapter must not depend on this repository. The copied code calls functions of FSharp.Core that return an option, such as Seq.tryHead and Seq.tryFind. The collection helpers that the root Directory.Build.props compiles into every F# project replace exactly these functions with ones that return a voption, so the project removes that file from its compilation for now; the commit that moves the adapter to the repository's conventions takes the removal out. The project joins both FSharp.Azure.Cosmos.slnx and FSharp.Azure.Cosmos.slnf, which FAKE builds, so every build compiles it, and THIRD-PARTY-NOTICES.md joins the solution items. Directory.Packages.props gets Hedgehog 2.0.4 (Apache-2.0; FSharp.Core >= 8.0.403 and TypeShape 9.0.0 transitively, checked in the nuspec), which contains the former Hedgehog.Experimental auto-generation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turn the ported sources into an MSTest adapter (ADR 0001, section 22.6).
PropertyAttribute derives from TestMethodAttribute and, like it, applies
to methods only and is not inherited. It keeps upstream's six
constructor overloads and named settings (AutoGenConfig,
AutoGenConfigArgs, Tests, Shrinks, Size), forwards the caller
information of every constructor to the base constructor, as
STATestMethodAttribute does, and overrides ExecuteAsync. MSTest has no
counterpart to NUnit's test builder or xUnit's discoverer, so this one
attribute is the framework layer; PropertiesAttribute is copied from
upstream with the same settings.
Each generated case and each shrink step is one call of
ITestMethod.InvokeAsync, never MethodInfo.Invoke, so MSTest creates the
test class, injects the TestContext and runs TestInitialize, TestCleanup
and Dispose for every case, and applies a [<Timeout>] to each of them.
The values of a [<DataRow>] or [<DynamicData>] row, which MSTest passes
in ITestMethod.Arguments, are the leading arguments and also appear in
the report; only the parameters after them are generated. Generated
IDisposable arguments are disposed after their invocation. The run is
folded into one TestResult, because several results from one
ExecuteAsync appear as separate results of one test: a failure carries
Hedgehog's report with the shrunk arguments, the exception of the
counterexample as inner exception (unwrapped from MSTest's internal
TestFailedException), the output of its invocation and a
[<Recheck("...")>] to add next to the method's Property attribute,
which keeps the attribute's settings, such as its AutoGenConfig, so that
the recheck data replays this counterexample. A run that gives up after
Hedgehog's 100 discards fails, as in upstream.
Additions beyond upstream: a uint64 Seed setting on both attributes;
Assert.Inconclusive in an invocation discards the case, and fails it
while rechecking, because a recheck cannot discard its only case; a run
whose TestContext cancellation token is cancelled stops without
shrinking, reading the token before every invocation, because MSTest
gives the TestContext a new token source before each TestCleanup, and
checking it again after the invocation, whatever its outcome: MSTest
reports a test method that returns without observing its cancelled
token as passed, and after the last case nothing else would notice the
token, so the property stops in the same way wherever the cancellation
reached it; the run still disposes the arguments generated for a case
it keeps from running;
an invocation whose outcome is none of Passed, Failed, Inconclusive and
Timeout, such as Error, was not run as a test by MSTest, so the run
stops at it without shrinking and the property reports the outcome and
the exception of that invocation instead of a counterexample.
The token comes from TestContext.Current, experimental in MSTest 4.2.3
(MSTESTEXP), in one helper with a scoped #nowarn "57".
Departures from upstream, each recorded in the header of the changed
file: class settings come from the reflected type of the method, so a
property inherited from an abstract base gets the settings of each
derived test class, and a derived class's [<Properties>] win over its
base's; a GenAttribute is found through a new non-generic GenAttribute
base wherever it sits in the inheritance chain (upstream checks only the
direct base type and silently auto-generates otherwise); generic
methods, which MSTest 4.2.3 discovers, and return types other than unit,
Task and ValueTask, which MSTest 4 cannot discover, are rejected before
any generator is built, so the return-value handling and the
reflection-based invocation of upstream's InternalLogic and
ReflectionHelpers are removed; every invocation is memoised per
shrink-tree node, because Hedgehog 2.0.4 runs an asynchronous case again
whenever it unwraps it and would invoke the counterexample a second time
to read its journal; the recheck, which is synchronous in Hedgehog
2.0.4, runs on the thread pool; the generic arguments of an
AutoGenConfig method are inferred from every place a generic parameter
occurs in its parameter types, nested ones such as 'a[] included, and
every place must give the same type, where upstream takes one argument
type per parameter that is itself a generic parameter, which fails for
'a list or a repeated 'a; NonZeroInt skips a 0 at either end of its
range, where upstream generates from an empty part, and rejects the
range from 0 to 0; Odd and Even are generated from the odd or even
values inside their range and reject a range without one, where
upstream sets or clears the lowest bit of any value of the range and
so leaves an even upper bound of Odd or an odd lower bound of Even by
one; any exception, an invalid AutoGenConfig
type or malformed recheck data included, becomes a result with the
outcome Error instead of escaping ExecuteAsync.
InternalLogic.executeAsync, a function from the method, the data row,
an invoker and a function that reads the cancellation token to a
TestResult, is the entry point of ExecuteAsync; InternalsVisibleTo
exposes it to the adapter's suite, so the suite can run properties that
fail by design without failing itself. The repository turns
GenerateAssemblyInfo off for the FAKE-written AssemblyInfo.fs of the src
projects, so this project turns it back on for the SDK to emit the
InternalsVisibleTo attribute.
Every public type and member is documented, and every reference to a
type or member is a documentation ID that resolves in the documentation
files of the referenced assemblies, including the primary constructors,
whose comments sit before their parameters. THIRD-PARTY-NOTICES.md lists
the two attribute files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The review of #43 found that the paste-ready [<Property; Recheck("...")>] of a failed property replaces the method's own Property attribute, so settings such as its AutoGenConfig would be lost and the recheck data would no longer replay the reported case. The adapter now reports a [<Recheck("...")>] to add next to the Property attribute; the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The Hedgehog MSTest adapter of #43 is proposed upstream as hedgehogqa/fsharp-hedgehog#487. The record notes how the proposal differs from the in-repository copy (net8.0 and F# 8, upstream's documentation in the shared files, VSTest), that MSTest 4.2 is the adapter's lowest version, and that one test of #43 stops compiling on MSTest 4.5.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The maintainer's reviews of 2026-10-10 changed several things the record describes, and it now says so where each belongs. The Hedgehog MSTest adapter (#43): the in-repository copy follows the coding guidelines of this repository, while the upstream pull request keeps upstream's style in a copy of its own, so a change of behaviour goes into both; an invocation that MSTest could not run stops the run instead of being shrunk; the cancellation token is checked after every invocation, whatever its outcome, which the automatic review of #50 asked for; Odd and Even stay inside their range; DateOnly and TimeOnly get generators, upstream through hedgehogqa/fsharp-hedgehog#488. The open question of the adapter's code style is answered, formatting and nullness checking included: the two projects are formatted with Fantomas and build with nullness checking on, and only the verbatim port keeps upstream's settings. Collections (section 4): the shared collection functions of #47 that return voption and struct tuples, tested with properties in #50, and the rule of one module per pipeline of #49. Build (section 5): why FAKE's DotNet.test cannot run the test applications, and fsprojects/FAKE#2903, which adds DotNet.testMTP. The spec model (sections 6 and 16): EquatableArray and the ImmutableArray patterns are #48 and the model itself #42; the validator returns an ImmutableArray of struct errors. Golden baselines (section 13): a literal type provider was evaluated on the question asked in #46 and rejected, with the reasons. Tests (section 22): test category attributes sorted by name, in #51 with an instruction scoped to those files, Pascal-case assertion functions and ImmutableArray for shared test data. Two more answers of the same day: the Windows emulator lane of #45 runs weekly instead of nightly (section 2), and the shared collection functions of #47 are in the namespace FSharp.Azure.Cosmos instead of the global one (section 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, its formatting and nullness checking included, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. The three files that use them open FSharp.Azure.Cosmos, the namespace of these functions. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. - The error for an AutoGenConfig type without exactly one config member says "member" where upstream says "property": a method that takes AutoGenConfigArgs is accepted as well (automatic review of #50). - The project is formatted with Fantomas: .fantomasignore no longer lists it, so the Fantomas run of the build covers it. - Nullness checking is on, as everywhere in the repository. A data row and the arguments of an invocation or of a config method may hold null, so they are arrays of objnull. A null array is checked for where it enters: in the setters of AutoGenConfigArgs, which take it as no arguments, and at the entry point that receives the data row from MSTest. Nested exceptions and base types are matched on null instead of tested with isNull, reflection lookups that cannot fail are asserted with nonNull, and a config method that returns no IAutoGenConfig is an error. One call keeps a scoped #nowarn "3261", in a helper of its own: ITestMethod.InvokeAsync declares the elements of its arguments as non-null, although MSTest itself passes it ITestMethod.Arguments, whose elements it declares as nullable. - The pipeline that builds the journal ends in Seq.toArray, so that it stays in one module. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 11, 2026 11:03
52e79bd to
3720c58
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 6 tasks
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 11, 2026 20:55
3720c58 to
ece9762
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
…ADR 0001 Hedgehog 2.0.4 cannot generate the two types, and hedgehogqa/fsharp-hedgehog#488 adds the generators upstream. Until a release has them, the adapter of this repository registers the same two for the parameters of a property (#43). The record of the adapter says so, and when the stand-in goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The version-sensitive custom MSTest execution layer warrants final human validation.
7 open findings
Move Microsoft namespace after System · New Move Microsoft namespace after System · New Move Microsoft namespace after System · New Move Microsoft namespace after System · New Use Seq.toArray for pipeline materialization · New Reorder open block to match repository convention · New Reorder open declarations by namespace group · New
🧠 Review effort: Balanced
The adapter was ported in upstream's style, because the same code was to go into the upstream pull request. That pull request now exists, hedgehogqa/fsharp-hedgehog#487, with its own copy in upstream's style, so the copy in this repository follows the coding guidelines of this repository, its formatting and nullness checking included, as the review of #43 asks: - A setting that is not given is a voption instead of an option: the members of IPropertyAttribute, the fields of PropertyContext, the private constructors and fields of PropertyAttribute and PropertiesAttribute, the generic arguments that AutoGenConfig infers and the generator that a GenAttribute gives. - The project compiles src/Shared/ValueCollections.fs again, like every other F# project, and uses its Seq.tryHead, Seq.tryFind, Seq.tryExactlyOne and Seq.zip, which return a voption or struct tuples. The three files that use them open FSharp.Azure.Cosmos, the namespace of these functions. Prelude loses Option.requireSome and Seq.seqTryExactlyOne. - The generated arguments are an array instead of a list. Hedgehog 2.0.4 has Gen.sequence for sequences and Gen.sequenceList for lists, both over one traversal, and nothing for arrays, so Prelude adds Gen.sequenceArray over Gen.sequence; the values shrink as they did through Gen.sequenceList. Type.getAllAttributes returns an array, and the two parts of a NonZeroInt range are an array. - Collections are joined in array and sequence expressions instead of Array.append and Seq.append: the arguments of an invocation, the values of the journal and the attributes that PropertyContext folds. - The types that the adapter compares other types with are in one module, KnownTypes: Void, Task, ValueTask, IAutoGenConfig and the generic definitions of ValueTask<'T>, Async<'T> and Result<'T, 'E>. typeof is one ldtoken, but typedefof calls GetGenericTypeDefinition on every use (read in the compiled IL), so each type is resolved once, and every comparison goes through the operators of System.Type instead of generic equality. - withTests, withShrinks and withSeed are functions with named parameters instead of values that hold a lambda. - Attributes are found through GetCustomAttributes<'T> and OfType<'T> instead of a type test and a cast per attribute. - The error for an AutoGenConfig type without exactly one config member says "member" where upstream says "property": a method that takes AutoGenConfigArgs is accepted as well (automatic review of #50). - The project is formatted with Fantomas: .fantomasignore no longer lists it, so the Fantomas run of the build covers it. - Nullness checking is on, as everywhere in the repository. A data row and the arguments of an invocation or of a config method may hold null, so they are arrays of objnull. A null array is checked for where it enters: in the setters of AutoGenConfigArgs, which take it as no arguments, and at the entry point that receives the data row from MSTest. Nested exceptions and base types are matched on null instead of tested with isNull, reflection lookups that cannot fail are asserted with nonNull, and a config method that returns no IAutoGenConfig is an error. One call keeps a scoped #nowarn "3261", in a helper of its own: ITestMethod.InvokeAsync declares the elements of its arguments as non-null, although MSTest itself passes it ITestMethod.Arguments, whose elements it declares as nullable. - The pipeline that builds the journal ends in Seq.toArray, so that it stays in one module. The header of every changed file records the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add tests/Hedgehog.MSTest.Tests, an MSTest runner executable (OutputType Exe, EnableMSTestRunner) with method-level parallelism in testconfig.json, that runs 137 tests against the adapter (ADR 0001, section 22.6). Upstream's F# xUnit and NUnit adapter tests at a469772 are rewritten as [<TestClass>] types: generating and shrinking, AutoGenConfig types and their error messages, AutoGenConfigArgs with generic, concrete and mixed arguments, class-level settings and their overrides, attribute subclasses, tuples, shrink limits, recheck data and Size, IDisposable arguments, GenAttribute and the 18 built-in generator attributes. The cases MSTest 4 cannot discover (bool, Result, Async, Task<'T> and Property returns, unresolved generics, module-level properties) and the xUnit-specific ones are left out. Added to them are tests of a generic config parameter used for two arguments, nested in an array type and given two types, of NonZeroInt with 0 at either end of its range and from 0 to 0, and of Odd and Even at the ends of their range, next to Int32.MinValue and Int32.MaxValue and over a range without such a value; three misspelled test names are corrected. The ported files and the shared configurations carry upstream's Apache-2.0 attribution, and THIRD-PARTY-NOTICES.md lists them. The targets that must shrink exactly once fix their seed: with a random one, a run whose first failing value is exactly 2500 has nothing smaller to shrink to, which happened in 8 of 20,000 simulated runs. MSTest-specific tests cover what the adapter adds or does differently, each pinning MSTest behaviour that the adapter relies on: a new instance, TestContext, TestInitialize, TestCleanup and Dispose for every case, with property and constructor injection; DataRow, DynamicData and TestDataRow<struct (string * int)> rows as leading arguments; a property inherited from an abstract base reading the settings of each derived test class, and a derived class's settings and configuration winning over its base's; a GenAttribute derived from a built-in one; the caller information of each of the six PropertyAttribute constructors; Task and ValueTask bodies; Assert.Inconclusive as a discard and as a failure while rechecking; give-ups failing; generic methods and unsupported return types giving an error result; the same Seed giving the same cases; the counterexample invoked once; a token cancelled at a failing case that has smaller values to shrink to, and a timed-out invocation, stopping the run without shrinking, with a per-invocation [<Timeout>] that a whole property exceeds and a TestCleanup that makes MSTest replace the token source; a token cancelled during a passing invocation, an earlier one or the last, stopping the run as well; the arguments of a case that a cancelled token keeps from running still disposed; an invocation that ends with the outcome Error or NotFound, and an exception of the invoker itself, stopping the run without shrinking and giving the property that outcome and that exception instead of a failure. Properties that fail by design never fail the suite. Targets in classes without [<TestClass>], which MSTest does not run, go through Harness and the adapter's entry point InternalLogic.executeAsync with an invoker that creates the class and awaits the method, so the tests inspect the report, the journal and the result. FailingPropertyAttribute runs a property through the real MSTest pipeline instead and passes when its single result has the expected outcome, message, output and inner exception: a failure reports its counterexample, its recheck data and the output of its invocation, [<Recheck>] replays the counterexample of the seed-42 run and passes once the bug is fixed, a data row appears among the parameters, malformed recheck data gives an error result and a give-up fails. Twenty behaviours of the adapter, from the reflected type and the memoised invocations to the single result and the cancellation check before each invocation, were each reverted in the adapter on its own: a test fails for every one. So do the three tests of an invocation that MSTest could not run when such an outcome is treated as a falsified case again. Like the adapter, the suite follows the coding guidelines of this repository, its formatting and nullness checking included; one test pins that a null array of AutoGenConfigArgs, which a caller compiled without nullness checking can give, counts as no arguments. The suite compiles the collection helpers of src/Shared/ValueCollections.fs and references no project of this repository but the adapter, so tests/Directory.Build.props leaves the Cosmos test infrastructure out of it too. The suite joins both FSharp.Azure.Cosmos.slnx and FSharp.Azure.Cosmos.slnf; testsGlob, already narrowed to tests/**/*.Tests.??proj, picks it up and leaves the adapter library out, so DotnetTest runs Hedgehog.MSTest.Tests next to FSharp.Azure.Cosmos.Tests. The coverage report excludes the adapter and Hedgehog, which the suite loads and whose package embeds its PDB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hog MSTest adapter Hedgehog 2.0.4 cannot auto-generate a DateOnly or a TimeOnly: Gen.auto throws for both (run in dotnet fsi against the package), and upstream's adapters have a generator attribute for DateTime and DateTimeOffset only. A property with a parameter of one of these types therefore needed a generator of its own; the review of #43 asks for the two attributes. DateOnlyAttribute generates a date between two dates, both included, and TimeOnlyAttribute a time of day between two times, both included, at the precision of TimeOnly (100 nanoseconds). Both ranges are constant, like those of DateTimeAttribute: they do not grow with the size of a case, and a value shrinks towards the lower end. Without arguments DateOnly generates within the 3650 days from 2000-01-01, the dates of DateTimeAttribute's default range, and TimeOnly any time of day. An attribute argument cannot be a DateOnly or a TimeOnly, so each attribute also takes its range as integers, [<DateOnly(2024, 2, 28, 2024, 3, 1)>] and [<TimeOnly(9, 0, 17, 30)>]; the constructor that takes the two values themselves serves attributes that derive from these. Six tests cover the default and the given ranges through [<Property>] methods and check on 200 sampled values that a range of three values gives each of them, both ends included, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tribute Hedgehog 2.0.4 has no generator of the two types: `Gen.auto` calls a constructor of the type with integers that it generates, and the constructor refuses them. So a parameter of such a type, or of a type that holds one, needed `[<DateOnly>]`, `[<TimeOnly>]` or a generator of the test. hedgehogqa/fsharp-hedgehog#488 adds the two default generators to Hedgehog itself. Until a release has them, the adapter registers the same two for the parameters of a property: the configuration that a property starts from is `AutoGenConfig.propertyDefaults`, the defaults of Hedgehog and these generators. The configuration that a test names is merged over it as before, so a generator of the test still wins, and so does an attribute on the parameter. A date lies around 2000-01-01, further away the larger a case is. A time of day lies anywhere in the day, later the larger a case is. The suite has a test that fails as soon as Hedgehog generates the two types by itself: `propertyDefaults` goes then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them A new DotnetListTests target, between DotnetBuild and DotnetTest in both build chains, runs `dotnet test --list-tests` for every test application (ADR 0001, section 22.1). MSTest 4 fails the discovery of a whole assembly when one test method has a signature it cannot run, such as a [<Property>] method that returns bool, while the F# build gives no warning; checked by adding such a method to the adapter's suite: the build succeeded without a warning, and the listing failed with UTA007 and exit code -532462766. Listing reports that before the run, which needs the emulator, starts. DotnetTest and DotnetListTests share dotnetTestApplications, which runs `dotnet test --no-build` in the configuration of the executing targets for every project that testsGlob matches, so the loop over the test applications is written once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The solution tree of the agent instructions lists tests/Hedgehog.MSTest and its suite, and the libraries list Hedgehog. The testing rules say how to write a property test: a method of a [<TestClass>] type marked [<Property>], with generated parameters, never Property.check inside a [<TestMethod>]; that it is an instance method that returns unit, Task or ValueTask and fails by throwing, because MSTest 4 rejects static methods and other return types and one such method fails the discovery of the whole assembly, which --list-tests shows before a run; that MSTest runs [<TestInitialize>] and [<TestCleanup>] for every generated case and every shrink step; and that a failure reports a [<Recheck>] that replays it; and that a DateOnly or TimeOnly parameter needs its generator attribute, because Hedgehog 2.0.4 cannot auto-generate these types. They also record that the adapter and its suite are the exception to the shared test infrastructure reference, the suite referencing the adapter alone, and that their code follows the rules of the file, formatting and nullness checking included, while the copy in the upstream pull request keeps upstream's style, with Apache-2.0 headers and THIRD-PARTY-NOTICES.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
test/hedgehog-mstest-adapter
branch
from
October 11, 2026 22:20
ece9762 to
ed8c41e
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 11, 2026
The generators of the SQL model and the printer laws need both: the syntax tree, printer and validator of #42 and the property attribute of #43. This merge is only the base of the stacked branch; it goes away when both pull requests are in main and the branch is rebased onto it. THIRD-PARTY-NOTICES.md is added by both; the result keeps the text of #42 and appends the fsharp-hedgehog section of #43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Proposed Changes
The query translator planned in ADR 0001 (#34) is meant to be checked with property-based tests: the SQL printer against generated syntax trees, and generated predicates against both F# and the emulator. Hedgehog, the property-testing library the plan uses, ships adapters for xUnit and NUnit but not for MSTest, which this repository uses. Without one, a property has to be a
[<TestMethod>]that callsProperty.checkin its body: MSTest sees one opaque test, a failure is a single exception with no way to replay it, and[<TestInitialize>],[<TestCleanup>], data rows and timeouts do not apply to the individual generated cases.This pull request adds that adapter,
tests/Hedgehog.MSTest, and its own suite,tests/Hedgehog.MSTest.Tests.A property is a test that must hold for every input Hedgehog generates. When an input fails, Hedgehog shrinks it: it searches for the smallest input that still fails, the counterexample. With the adapter, a property is an ordinary test method whose parameters Hedgehog generates:
[<Property>]derives from MSTest'sTestMethodAttribute. MSTest invokes the method once for every generated case and every shrink step, each time with a new test class instance, its[<TestInitialize>]and[<TestCleanup>], and its own timeout. The whole run is reported as one test result. A failure shows:[<Recheck("33_7178138842248401496_3704456211830240771_4-4-4")>], to add next to the method's[<Property>].That string is the recheck data: the size, seed and shrink path of the counterexample. Added to the method, next to its
[<Property>]so that settings such as itsAutoGenConfigstay, it replays exactly that one case.The adapter is a port of
Hedgehog.NUnitandHedgehog.Xunitfrom hedgehogqa/fsharp-hedgehog at the Hedgehog 2.0.4 release commita469772. The first commit copies the nine framework-neutral files unchanged except for the namespace, so the port can be compared with upstream line by line. The adapter is also proposed upstream, assrc/Hedgehog.MSTestin hedgehogqa/fsharp-hedgehog#487, where it has its own copy in upstream's style. Both projects here:voption, struct tuples, arrays, the collection functions of refactor: returnvoptionand struct tuples from the collection functions #47), applied in a commit of its own after the port;THIRD-PARTY-NOTICES.md.Once upstream publishes the package, it replaces this copy.
The adapter keeps upstream's attribute surface:
[<Property>]with its six constructor overloads and theAutoGenConfig,AutoGenConfigArgs,Tests,ShrinksandSizesettings;[<Properties>]and[<Recheck>];GenAttribute<'T>with its 18 built-in subclasses ([<Int>],[<Email>], …), which set the generator of one parameter;IAutoGenConfig, the generators to use per parameter type.On top of that:
Seedsetting, so a suite can fix the cases it generates;[<DataRow>]or[<DynamicData>]row are the leading arguments, and only the remaining parameters are generated;Assert.Inconclusivediscards a case (its precondition does not hold), and a give-up (Hedgehog stopping after 100 discards) fails the test;[<Properties>], and a derived class's settings win over its base's;GenAttributeis found through the whole inheritance chain; upstream silently ignores one derived from a built-in attribute;unit,TaskorValueTask, gives an error result before anything is generated;TestContexttoken is cancelled, by a[<Timeout>](which applies to each invocation) or by the test run, the property stops without shrinking; the token is checked before every invocation and after it, whatever its outcome;ErrororNotFoundinstead ofPassed,Failed,InconclusiveorTimeout, or an exception ofInvokeAsyncitself – stops the property without shrinking, and the property reports the outcome and the exception of that invocation instead of a counterexample;DateOnlyandTimeOnly: Hedgehog 2.0.4 cannot generate these two types on its own (Gen.auto<DateOnly>passes arbitrary integers to the constructor and throwsArgumentOutOfRangeException). Generators, auto-generation in the core and the attributes[<DateOnly>]and[<TimeOnly>]for all three adapters are proposed upstream: Generate DateOnly and TimeOnly: Gen functions, auto-generation and attributes hedgehogqa/fsharp-hedgehog#488 and, for MSTest, Add Hedgehog.MSTest: Property attributes for MSTest 4 hedgehogqa/fsharp-hedgehog#487. Until a release of Hedgehog has them, this adapter brings both halves itself:The build gains a
DotnetListTeststarget that runs beforeDotnetTest. MSTest 4 fails the discovery of a whole assembly when one test method has a signature it cannot run, and a[<Property>]returningboolcompiles without a warning. Listing the tests reports that before the emulator run starts. The agent instructions now explain how to write property tests.What the suite covers
149 tests:
Upstream's F# xUnit and NUnit adapter tests, rewritten for MSTest:
Size, shrink limits;AutoGenConfigArgs;IDisposablearguments;GenAttributewith the 18 built-in attributes.Cases MSTest 4 cannot discover (
bool,AsyncorPropertyreturns, for example) are left out.Every addition and departure above, each pinning the MSTest behaviour the adapter relies on:
TestContext,[<TestInitialize>],[<TestCleanup>]andDisposefor every case;TaskandValueTaskbodies;Assert.Inconclusiveand give-ups;[<DateOnly>]and[<TimeOnly>]: default and given ranges, both ends of a range reached;DateOnlyandTimeOnlywithout an attribute: as parameters and inside a record, how the values spread with the size of a case, with a configuration of the test that has a generator of its own and with one that has none. One test states that Hedgehog cannot generate the two types by itself; it fails when a release of Hedgehog can, and the adapter's own generators go then.Failure reporting through the real MSTest pipeline: a failing property reports its counterexample, recheck data and output in exactly one result, and
[<Recheck>]replays the counterexample and passes once the bug is fixed.Properties that fail by design never fail the suite. They run in one of two ways:
[<FailingProperty>]attribute that passes when MSTest's single result has the expected outcome and message.Twenty behaviours of the adapter were each reverted on their own, and every one makes at least one test fail. So do seven one-line defects planted in the default generators of
DateOnlyandTimeOnly: a generator that is not registered, a property that starts from Hedgehog's own defaults, a range that does not grow with the size of a case or grows the wrong way, a time cut to whole seconds.Verification
dotnet build FSharp.Azure.Cosmos.slnx, Debug and Release, gives 0 errors. The warnings are the same as onmain: nullness warnings in existing test code, none from the adapter projects.--list-testsdiscovers the tests of both test applications.voptionand struct tuples from the collection functions #47).build.cmd DotnetListTests -slists the tests of both test applications.Changes after the Copilot review
AutoGenConfigmethods: a generic argument is now inferred from every place its parameter occurs, also nested ('a[]), and all places must agree. Upstream takes one argument type per parameter that is itself a generic parameter, which fails for'a listor a repeated'a.NonZeroIntskips a 0 at either end of its range ([0, 5],[-5, 0]) instead of generating from an empty part, and rejects[0, 0].IDisposablearguments too.[<Recheck("…")>]to add next to the[<Property>], so the property's own settings stay and the replay uses the same generators.Each fix has a test (7 new tests); the disposal test fails without its fix.
Changes after the maintainer's review
The branch was rebuilt on top of #47; the review threads are answered one by one.
voptioninstead ofoptionfor every setting that may be absent: the members ofIPropertyAttribute, the fields ofPropertyContext, the private constructors of both attributes, the generic arguments thatAutoGenConfiginfers.voptionand struct tuples from the collection functions #47 (Seq.tryHead,Seq.tryFind,Seq.tryExactlyOne,Seq.zip) replace FSharp.Core's and the two helpers that upstream'sPreludehad for this.Gen.sequence(sequences) andGen.sequenceList(lists) but noGen.sequenceArray, so the adapter'sPreludeadds one overGen.sequence. Both existing functions share one traversal, so values shrink as before: the pinned recheck data and the seed-42 expectations of the suite pass unchanged.Array.appendandSeq.append, everywhere in the adapter.KnownTypes, one internal module with the types the adapter compares with (Task,ValueTask,Void, the generic definitions ofValueTask<'T>,Async<'T>,Result<'T, 'E>,IAutoGenConfig), compared throughType.(=).withTests,withShrinksandwithSeed.Erroroutcome was shrunk like a counterexample and reported asFailed. Three new tests fail when that handling is reverted.[<DateOnly>]and[<TimeOnly>]with six tests.[<Odd>]and[<Even>]stay inside their range. This is upstream's code, and the review of the upstream pull request flagged it:Odd(-1, 0)could give 1 andEven(1, 2)could give 0. Both now generate from the odd or even values inside the range and reject a range without one. Eight tests.Changes after the automatic review of #50
#50 is built on this branch, so its Copilot review read these commits too. It found three things, all fixed here in the commits they belong to:
[<Timeout>]). Such a case stopped the property only when a later case read the token, so a property failed when its first case overran the timeout and passed when its last one did. The token is now checked after every invocation, whatever its outcome. A new test with two rows cancels the token during the third and during the last of twenty passing cases; the second row fails without the fix. The upstream pull request has the same change.AlphaNumStringAttributeto that ofDateOnlyAttribute, so neither started with/// <summary>and the compiler escaped their tags without a warning. The generated XML has no escaped tag left.AutoGenConfigtype without exactly one config member: a static method that takesAutoGenConfigArgsis accepted as well.Formatting and nullness checking
Asked for after the second review round: the two projects no longer keep upstream's formatting and
Nullable=disable. The branch is also rebased onto the currentmain..fantomasignoreno longer lists the two projects, and their 17 F# files are formatted with the settings of this repository.null, so they are arrays ofobjnull;nullarray is checked for where it enters: the setters ofAutoGenConfigArgstake it as no arguments (one new test), and the entry point takes the data row as MSTest hands it in;nullinstead of tested withisNull, and reflection lookups that cannot fail are asserted withnonNull;IAutoGenConfigis an error with a message of its own;#nowarn "3261"remains, in a helper of its own (InternalLogic.invokerOf). MSTest declares the elements of the arguments ofITestMethod.InvokeAsyncas non-null, although it passesITestMethod.Arguments, whose elements it declares as nullable, to that method itself.Seq.toArray, after the rule of one module per pipeline thatmainhas now.voptionand struct tuples from the collection functions #47 are in the namespaceFSharp.Azure.Cosmosnow (review of refactor: returnvoptionand struct tuples from the collection functions #47), so the three adapter files that use them open it, with a comment that says what for..fantomasignoreandNullable=disable, so that the port can still be compared with upstream line by line. The commit that applies the conventions turns both on, and the suite is added formatted and with nullness checking.Types of changes
Checklist
Further comments
Nothing ships: both projects are test projects, so no package, public API or changelog entry changes.
Framework layer: MSTest has no counterpart to NUnit's test builder or xUnit's discoverer, so this layer is a single attribute that overrides
TestMethodAttribute.ExecuteAsync.MSTest behaviour the API does not promise. The adapter relies on:
ITestMethod.InvokeAsync;TestContext.Current(warning FS0057, suppressed in one helper);Each is pinned by a test, so an MSTest update that changes one fails the suite.
Departures from upstream: every one is recorded in the header of the changed file.
Changes after the maintainer's question about
DateOnlyandTimeOnlyThe agent instructions said that a
DateOnlyorTimeOnlyparameter needs an attribute or a generator of its own, and did not say that the missing generators are contributed upstream. Both are corrected, in a commit of its own (9736d79) and in the commit of the instructions:AutoGenConfig.propertyDefaults, internal): a date around 2000-01-01, further away the larger a case is, and a time of day anywhere in the day, later the larger a case is. The precedence does not change: an attribute on the parameter, then the configuration of the method, then that of the class, then this base.Gen.autogenerates the two types by itself, which is when the stand-in goes.Gen.autooutside a property does not have the generators before a release of Hedgehog does.Changes after the automatic review of 2026-10-11
opendeclarations are in the order of the repository in the two files of the adapter and the four files of the suite that had another one:System, thenMicrosoft, then the other libraries, then the namespaces of this solution, of whichHedgehog.MSTestis one here.Seq.toArrayfor a sequence instead ofArray.ofSeq, in the suite and in thePreludeof the adapter.🤖 Generated with Claude Code