Skip to content

test: compare the collection shadows with FSharp.Core in property tests - #50

Open
xperiandri wants to merge 10 commits into
mainfrom
test/value-collections-properties
Open

xperiandri wants to merge 10 commits into
mainfrom
test/value-collections-properties

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Depends on #43. #47 is merged. This branch is built on the branch of #43, so until #43 is merged this pull request also shows its seven commits; its own are the last two. It targets main and not the branch of #43, because the build workflow runs only for pull requests into main.

This answers the question asked in #47: should the collection functions be tested with properties? Yes, and this is the place for it. #47 cannot be stacked on the Hedgehog adapter, because the adapter (#43) is itself written with those functions, so the property tests come after both.

Why properties fit here. Every function of src/Shared/ValueCollections.fs hides the FSharp.Core function of the same name and must mean the same; only the result type differs (voption for option, struct tuples for reference tuples). So each function has an oracle: another implementation that gives the expected answer for any input. A test then needs no hand-written expected values. It runs both on the same input and compares, and the input can be generated.

What changes. tests/Cosmos.Tests/ValueCollectionsTests.fs compared the two on five hand-picked arrays, four predicates and five indexes, in three test methods that differed only in the module (Seq, List, Array). They are now four methods marked [<Property>], grouped by the input they need:

Property Generated input Functions compared, each for Seq, List and Array
first, last and only element an array tryHead, tryLast, tryExactlyOne
element at an index an array and an index tryItem
search an array and a predicate tryFind, tryFindBack, tryFindIndex, tryFindIndexBack, tryPick
pairing two arrays zip

Hedgehog runs each property on 100 generated inputs. On a difference it shrinks the input, which means it retries with smaller arrays and values while the test still fails, and reports the smallest failing input together with a [<Recheck("…")>] attribute that replays it.

The generated input, measured over three runs of 100 cases each:

  • Arrays hold up to eight integers from 0 to 4 (SmallItemsAttribute, declared in the test file). About one array in ten is empty, one in ten holds a single element, and about 60 of 100 repeat a value. These are the inputs on which the lookups differ from each other.
  • The predicate is item <= threshold with a generated threshold from -1 to 4. It holds for no element in about 20 of 100 cases, for every element in about 25, and for some in about 40. Two or more elements match in about half of the cases, and they can differ in value, which is what tells tryFind from tryFindBack.
  • The index runs from -1 to 8: before the first element in about 7 of 100 cases, inside the array in about 45, past its end in the rest.

zip is compared on arrays cut to the shorter length, and Array.zip and List.zip must still throw ArgumentException on different lengths, as FSharp.Core does. The test of what a shadow hides (Seq.tryHead is the shadow, Microsoft.FSharp.Collections.Seq.tryHead is still reachable, Seq.map still resolves to FSharp.Core) states single facts, so it stays a plain [<TestMethod>].

tests/Cosmos.Tests references the adapter project tests/Hedgehog.MSTest and the Hedgehog package, and the rule about property tests in the agent instructions says that a test project needs these two.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Tests only; the package is unchanged.

Checklist

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

  • Do the properties catch a defect? Checked by planting three, one per property, and reverting them afterwards. All three were found:
    • List.tryLast returning the element before the last one when that element is 3: found after 24 cases and shrunk to items = [|0; 3; 0|]. The five hand-picked arrays of the old tests have no 3 in that place: run against the same defect, the old tests pass, 5 of 5.
    • Seq.tryItem stopping one element early: found in the first case.
    • Array.tryFindIndexBack never looking at the last element: found in the second case and shrunk to items = [|0|], threshold = 0.
  • Verification: dotnet build FSharp.Azure.Cosmos.slnx in Debug and Release with the same warnings as the branch of test: add a Hedgehog adapter for MSTest property tests #43. The emulator-free tests (--filter TestCategory=Unit): 82 of 82 in Debug and in Release. The whole project on the Windows emulator in Debug: 190 of 190, the same number as before, because five test methods replace five. --list-tests succeeds, which matters because one property with a wrong signature fails the discovery of the whole assembly. Fantomas is clean.
  • Cost: each property takes under 0.2 seconds, and they run in parallel.
  • Automatic review: Copilot's three findings on this pull request are all in the commits of test: add a Hedgehog adapter for MSTest property tests #43 that it shows: a cancellation that a passing last case hid, two broken documentation comments and the wording of one error message. They are fixed in those commits, so test: add a Hedgehog adapter for MSTest property tests #43 carries the fixes, and this branch is rebuilt on it.
  • No changelog entry: tests only.
  • Namespace: since the review of refactor: return voption and struct tuples from the collection functions #47 the shadows are in the namespace FSharp.Azure.Cosmos, so the test file opens it. The branch is rebased onto main, which has refactor: return voption and struct tuples from the collection functions #47 now, and onto the branch of test: add a Hedgehog adapter for MSTest property tests #43.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 10, 2026 13:46
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cancellation can be reported as success, and two public generator attributes have malformed XML documentation.

3 open findings
What changed in this PR

Adds property-based verification for shared collection shadows, including the stacked collection helpers and Hedgehog MSTest adapter dependencies.

Changes:

  • Adds voption/struct-tuple collection shadows and updates callers.
  • Introduces and tests a Hedgehog adapter for MSTest.
  • Replaces example-based collection tests with property tests against FSharp.Core.
File Description
THIRD-PARTY-NOTICES.md Records Hedgehog-derived code.
tests/​Hedgehog.MSTest/​ReflectionHelpers.fs Adds return-type reflection helpers.
tests/​Hedgehog.MSTest/​RecheckAttribute.fs Adds counterexample replay support.
tests/​Hedgehog.MSTest/​PropertyContext.fs Resolves property settings.
tests/​Hedgehog.MSTest/​PropertyAttribute.fs Implements the MSTest property attribute.
tests/​Hedgehog.MSTest/​PropertiesAttribute.fs Adds class-level property settings.
tests/​Hedgehog.MSTest/​Prelude.fs Adds adapter utilities.
tests/​Hedgehog.MSTest/​KnownTypes.fs Caches reflected framework types.
tests/​Hedgehog.MSTest/​IPropertyAttribute.fs Defines shared property settings.
tests/​Hedgehog.MSTest/​InternalLogic.fs Implements generation, execution, and reporting.
tests/​Hedgehog.MSTest/​Hedgehog.MSTest.fsproj Defines the adapter project.
tests/​Hedgehog.MSTest/​GenAttribute.Prelude.fs Adds built-in generator attributes.
tests/​Hedgehog.MSTest/​GenAttribute.fs Defines custom generator attributes.
tests/​Hedgehog.MSTest/​AutoGenConfig.fs Instantiates generator configurations.
tests/​Hedgehog.MSTest.Tests/​testconfig.json Enables parallel adapter tests.
tests/​Hedgehog.MSTest.Tests/​PropertyTests.fs Ports upstream adapter tests.
tests/​Hedgehog.MSTest.Tests/​MSTestTests.fs Tests MSTest-specific behavior.
tests/​Hedgehog.MSTest.Tests/​Hedgehog.MSTest.Tests.fsproj Defines the adapter test project.
tests/​Hedgehog.MSTest.Tests/​Harness.fs Adds an adapter test harness.
tests/​Hedgehog.MSTest.Tests/​GenAttributePreludeTests.fs Tests built-in generators.
tests/​Hedgehog.MSTest.Tests/​Common.fs Provides shared test generators.
tests/​Directory.Build.props Excludes adapter projects from Cosmos infrastructure.
tests/​Cosmos.Tests/​ValueCollectionsTests.fs Adds collection property tests.
tests/​Cosmos.Tests/​TestCategories.fs Adds the collection-test category.
tests/​Cosmos.Tests/​ReadExtensionsTests.fs Adapts to struct-tuple Array.zip.
tests/​Cosmos.Tests/​FSharp.Azure.Cosmos.Tests.fsproj References Hedgehog and the adapter.
src/​Shared/​ValueCollections.fs Implements collection shadows.
src/​Cosmos/​Read.fs Consumes the new voption result.
FSharp.Azure.Cosmos.slnx Adds adapter projects and notices.
FSharp.Azure.Cosmos.slnf Adds adapter projects to the filter.
Directory.Packages.props Pins Hedgehog 2.0.4.
Directory.Build.props Shares collection helpers across F# projects.
build/​build.fs Adds test discovery and runner support.
.github/​copilot-instructions.md Documents the new testing conventions.
.fantomasignore Excludes upstream-formatted adapter code.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/Hedgehog.MSTest/InternalLogic.fs Outdated
Comment thread tests/Hedgehog.MSTest/AutoGenConfig.fs Outdated
Comment thread tests/Hedgehog.MSTest/GenAttribute.Prelude.fs Outdated
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
xperiandri force-pushed the test/value-collections-properties branch from b1c89da to 2421ba3 Compare October 10, 2026 14:08
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
xperiandri force-pushed the test/value-collections-properties branch from 2421ba3 to 4a1600a Compare October 10, 2026 14:20
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; 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, 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
xperiandri requested a balanced review from Copilot October 10, 2026 16:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The stacked changes introduce a custom test runner and globally alter collection-function resolution, warranting final human review.

0 open findings

3 resolved since last review

🧠 Review effort: Balanced

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; 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, 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 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 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, 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
xperiandri force-pushed the test/value-collections-properties branch from 4a1600a to d27212a Compare October 10, 2026 17:13
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, 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
xperiandri force-pushed the test/value-collections-properties branch from d27212a to 86cf580 Compare October 10, 2026 20:54
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; 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 xperiandri changed the title test: compare the collection shadows with FSharp.Core in property tests test: compare the collection shadows with FSharp.Core in property tests Oct 10, 2026
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 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
xperiandri force-pushed the test/value-collections-properties branch from 86cf580 to 25243e3 Compare October 11, 2026 09:05
xperiandri and others added 2 commits October 11, 2026 12:42
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 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
xperiandri force-pushed the test/value-collections-properties branch 2 times, most recently from e305884 to 44144e4 Compare October 11, 2026 20:56
xperiandri and others added 8 commits October 12, 2026 00:11
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>
Every function of src/Shared/ValueCollections.fs means what the
FSharp.Core function of the same name means, so each one has an oracle.
The tests compared the two on five hand-picked arrays, four predicates
and five indexes, in three methods that differed only in the module.
They are now four properties of the Hedgehog MSTest adapter, grouped by
the input they need: an array; an array and an index; an array and a
predicate; two arrays. Each property compares the Seq, List and Array
shadows with their FSharp.Core functions on the same generated input and
fails at the first difference, which Hedgehog shrinks to a small input.

The arrays hold up to eight integers from 0 to 4 (SmallItemsAttribute):
about one in ten is empty, one in ten holds a single element and most of
the longer ones repeat a value. The predicate holds for the elements up
to a generated threshold, so for none, for all, or for elements of
different values. The index runs from before the first element to past
the last one of the longest array.

zip is compared on arrays cut to one length, and Array.zip and List.zip
must still throw on different lengths. The test of what a shadow hides
stays a plain test method.

tests/Cosmos.Tests references the adapter project and the Hedgehog
package for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule about property tests in the agent instructions names the two
references a test project needs before it can declare one, the adapter
project and the Hedgehog package, and points to tests/Cosmos.Tests,
which has them now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the test/value-collections-properties branch from 44144e4 to 72576d0 Compare October 11, 2026 22:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants