diff --git a/.github/dependabot-autofix-prompt.md b/.github/dependabot-autofix-prompt.md index fb9bf71c..f620fc42 100644 --- a/.github/dependabot-autofix-prompt.md +++ b/.github/dependabot-autofix-prompt.md @@ -65,7 +65,7 @@ The header must match `[([,...])][!]: CHANGELOG.md (FirstClassErrors + FirstClassErrors.Testing + FirstClassErrors.RequestBinder) # cli -> FirstClassErrors.Cli/CHANGELOG.md (the fce .NET tool) -# dum -> JustDummies/CHANGELOG.md (the standalone JustDummies library) on: workflow_dispatch: inputs: @@ -17,7 +16,6 @@ on: options: - lib - cli - - dum required: true from_ref: description: "Previous tag to diff from. Leave blank to auto-detect the train's latest tag; if the train has no tag yet, the whole history is used." @@ -92,7 +90,7 @@ jobs: tools/changelog/collect-prs.sh "$COMPONENT" > prs.json echo "count=$(jq 'length' prs.json)" >> "$GITHUB_OUTPUT" echo "Collected $(jq 'length' prs.json) ${COMPONENT}-facing pull request(s) for this changelog." - # COMPONENT is a choice input (lib|cli|dum only), safe to pass straight through. + # COMPONENT is a choice input (lib|cli only), safe to pass straight through. # from_ref is free text, so it travels via the environment (never inlined # into the script) and is only ever passed to git (rev-parse, then log) as a ref argument. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5825bca8..af5e9d22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,13 +98,9 @@ jobs: # Only these library test projects carry the net472 leg (EnableNet472Floor adds it): the tooling test # projects (Roslyn / GenDoc / Cli) stay net10-only by design, and RequestBinder.UnitTests stays net10-only # because its fixtures bind DateOnly, a .NET 6+ type absent from net472. RequestBinder is still floored here - # through its property tests, and JustDummies through its own contract suite (JustDummies.UnitTests), running on the - # netstandard2.0 asset .NET Framework consumers load — its net8-only tests are conditioned out (issue #215). - # JustDummies.Xunit is floored the same way: it ships netstandard2.0, so a .NET Framework consumer loads that - # asset, and its adapter suite must prove it works there and not only on net10. JustDummies.PropertyTests - # joins them so the generator INVARIANTS, not just the example-based contract, hold on the floor. + # through its property tests. # A per-project loop (not a solution-wide -f net472, which would force the TFM onto the net10-only projects - # and fail) keeps the net472 scope exactly these six. + # and fail) keeps the net472 scope exactly these three. - name: Test the netstandard2.0 libraries on .NET Framework 4.7.2 shell: bash run: | @@ -112,10 +108,7 @@ jobs: for proj in \ FirstClassErrors.UnitTests \ FirstClassErrors.PropertyTests \ - FirstClassErrors.RequestBinder.PropertyTests \ - JustDummies.UnitTests \ - JustDummies.PropertyTests \ - JustDummies.Xunit.UnitTests ; do + FirstClassErrors.RequestBinder.PropertyTests ; do echo "::group::$proj (net472)" dotnet test "$proj/$proj.csproj" -c Release -f net472 -p:EnableNet472Floor=true \ --logger "console;verbosity=normal" diff --git a/.github/workflows/dependabot-autofix.yml b/.github/workflows/dependabot-autofix.yml index 396bfb8c..0beeeae3 100644 --- a/.github/workflows/dependabot-autofix.yml +++ b/.github/workflows/dependabot-autofix.yml @@ -53,7 +53,6 @@ on: # zizmor: ignore[dangerous-triggers] - sonar - analyzers - commit-lint - - justdummies - dependency-review - codeql types: diff --git a/.github/workflows/justdummies-mutation.yml b/.github/workflows/justdummies-mutation.yml deleted file mode 100644 index 37970fb4..00000000 --- a/.github/workflows/justdummies-mutation.yml +++ /dev/null @@ -1,287 +0,0 @@ -name: justdummies-mutation - -# Mutation testing for the JustDummies libraries, kept in a workflow of their own rather than as two -# more legs of `mutation.yml`. JustDummies is destined for a repository of its own (it is already a -# standalone, error-agnostic package — ADR-0011), and this file is written so that the move is a -# FILE MOVE, not an edit: take this workflow, `build/stryker/justdummies*.json`, -# `.config/dotnet-tools.json` and the reference page, then repoint the `solution` field in the two -# configs at the new solution. Nothing here refers to a FirstClassErrors project. -# -# The mechanism is identical to `mutation.yml`; keep the two in step when you change one. - -on: - pull_request: - branches: - - main - # Weekly full sweep. The pull-request legs only mutate what a pull request touched, so nothing ever - # re-measures the parts of the libraries no one has edited: this is the run that does. Offset from - # `mutation`'s slot so the two sweeps do not contend for runners. - schedule: - - cron: '47 3 * * 1' - workflow_dispatch: - -# Cancel superseded runs on the same branch / PR. -concurrency: - group: justdummies-mutation-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Least privilege is declared PER JOB below rather than here. A workflow-level grant reaches every -# job, including any added later that does not need it, so each job states the narrowest scope it can: -# `changed` and `full` check the repository out and take `contents: read`, the advisory `gate` checks -# nothing out and declares none at all (Sonar githubactions:S8264). A job added here MUST carry its own -# `permissions` block — with none it inherits the repository default instead of a floor set here. - -env: - DOTNET_NOLOGO: 'true' - DOTNET_CLI_TELEMETRY_OPTOUT: 'true' - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' - -jobs: - # The gate. One leg per shipped library the diff-scoped run can afford, each mutating ONLY what the - # pull request changed (Stryker's `--since`), so the cost follows the diff and not the size of the - # library — which holds for these two, and is exactly what stopped holding for the generator - # (ADR-0049): `--since` selects per FILE, so on a large source the cost follows the file instead. - # - # `gate` below aggregates these legs into the single check to mark as required on `main`. - changed: - name: Mutate the diff (${{ matrix.name }}) - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - # Checks the repository out, builds and runs tests; it calls no API and writes nothing back. - permissions: - contents: read - # Both remaining legs finish in about ninety seconds, touched or untouched. The cap is a runaway - # guard, not a budget: the leg that needed one — the generator's — no longer runs here (ADR-0049). - timeout-minutes: 20 - strategy: - # Run both legs to completion: a survivor in the adapter is worth seeing even when the - # analyzers are red. - fail-fast: false - matrix: - # The JustDummies packages' xUnit v3 adapter (ADR-0039) and the analyzers packed inside the - # generator package (ADR-0044). The FirstClassErrors libraries live in `mutation.yml`. - # See ADR-0043. - # - # The GENERATOR is deliberately absent (ADR-0049) — it is the only leg this matrix cannot - # afford. Stryker selects per changed FILE, not per changed line, so a diff of ~100 lines - # touching one of its bigger sources pulls in that whole file: measured at 844 mutants, still - # running after an hour, no score. Every in-tool lever tops out around -36% against the -95% - # such a leg would need, sharding is floored by the largest changed file, and line-scoped - # `mutate` patterns select nothing at all. The weekly `full` sweep below still measures it. - include: - - name: justdummies-xunit - config: build/stryker/justdummies-xunit.json - - name: justdummies-analyzers - config: build/stryker/justdummies-analyzers.json - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - # Stryker's `--since` diffs the working tree against a commit, so the full history must be - # present; a shallow clone would leave that commit unreachable. - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - dotnet-version: '10.0.x' - - - name: Restore the local tools - # Pins dotnet-stryker through .config/dotnet-tools.json, so CI and a maintainer's machine run - # the same mutation engine — a newer engine invents new mutants and would move every score on - # its own, without a line of code changing. - run: dotnet tool restore - - # `pull_request` checks out the merge commit, so what this branch actually changed is measured - # from the FORK POINT — not from the base branch tip, which may have moved on since the branch - # was cut and would drag every file changed on `main` in the meantime into the "changed" set. - - name: Resolve the fork point - id: base - # A real commit SHA, not a rev expression: `--since:HEAD` is rejected outright by Stryker - # ("No branch or tag or commit found with given target"). `set -e` makes a failed merge-base - # fail the step, rather than handing Stryker an empty target that fails it later and vaguer. - run: | - set -euo pipefail - base=$(git merge-base '${{ github.event.pull_request.base.sha }}' HEAD) - echo "fork point: $base" - echo "sha=$base" >> "$GITHUB_OUTPUT" - - - name: Mutate the changed files - # The runner mode, the thresholds and the reporters all live in the config file, so this is - # the same command a maintainer runs locally. `--break-at` is deliberately NOT passed here: - # overriding the threshold from the YAML is what would let CI and a local run disagree. - # - # Stryker exits 0 — "unable to calculate a mutation score" — when the diff touches none of - # this library's sources, which is the common case for a leg nobody is working on. - run: >- - dotnet stryker --config-file ${{ matrix.config }} - --since:${{ steps.base.outputs.sha }} - --output artifacts/mutation/${{ matrix.name }} - - - name: Summarise the surviving mutants - # The score alone does not say what to fix. This turns the JSON report into the actionable - # half — file, line and the kind of rewrite that went unnoticed — in the run summary, so a - # failing gate can be diagnosed without downloading the artifact. `if: always()` because the - # interesting case is the failing one. - if: always() - run: | - set -euo pipefail - report="artifacts/mutation/${{ matrix.name }}/reports/mutation-report.json" - if [ ! -f "$report" ]; then - echo "${{ matrix.name }}: the diff selected no mutant" | tee -a "$GITHUB_STEP_SUMMARY" - exit 0 - fi - # Stryker writes a report even when it selects nothing, so "no mutant" has to be read from - # the report's contents, not from the file's absence: count the mutants that were actually - # put to the test, i.e. neither filtered out nor rejected by the compiler. - tested=$(jq '[.files[].mutants[] - | select(.status != "Ignored" and .status != "CompileError")] | length' "$report") - rows=$(jq -r --arg ws "$GITHUB_WORKSPACE/" ' - .files | to_entries[] | .key as $f | .value.mutants[] - | select(.status == "Survived" or .status == "Timeout" or .status == "NoCoverage") - | "| \(.status) | \($f | ltrimstr($ws)):\(.location.start.line) | \(.mutatorName) |" - ' "$report") - { - echo "### Mutation — ${{ matrix.name }}" - echo - if [ "$tested" -eq 0 ]; then - echo "This pull request changed nothing this library is mutated from." - elif [ -z "$rows" ]; then - echo "All $tested mutants were killed." - else - echo "| Status | Location | Mutation |" - echo "| --- | --- | --- |" - echo "$rows" - fi - } | tee -a "$GITHUB_STEP_SUMMARY" - - - name: Upload the mutation report - # Always: the report is most useful precisely when the run failed the threshold — the HTML - # view shows each surviving mutant in its source, which the summary table cannot. - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: mutation-report-${{ matrix.name }} - path: artifacts/mutation/${{ matrix.name }}/reports/ - # A leg that selected no mutant writes no report; that is a pass, not a packaging failure. - if-no-files-found: ignore - - # The single check that reports the per-PR mutation legs for the JustDummies packages. Kept as one - # job (and one check name) so branch protection has a single entry to point at. ADVISORY on pull - # requests (ADR-0046): the enforced bar is the weekly `full` sweep, so this job summarises the diff - # legs without ever turning a pull request red — the per-PR score is a signal to read, not a gate, - # and the whole-file cost of Stryker's per-file `--since` selection must not block a merge. - gate: - name: JustDummies mutation gate - if: always() && github.event_name == 'pull_request' - needs: changed - runs-on: ubuntu-latest - timeout-minutes: 5 - # The narrowest of the three: this job checks nothing out and calls no API. - # `needs.changed.result` is substituted by GitHub before the shell runs, and `::warning::` is a - # runner workflow command written to stdout, not a REST call. A job-level block REPLACES whatever - # the job would otherwise inherit rather than merging with it, so `{}` — GitHub's explicit "no - # scopes" form — leaves this job's token with nothing at all (Sonar githubactions:S8264). It must - # stay the explicit flow mapping: a bare `permissions:` is a null, not an empty map. Widen it if a - # checkout or a `gh` call is ever added here, or that step will fail on a 403. - permissions: {} - steps: - - name: Report the mutation legs - # `needs.changed.result` is the matrix's aggregate: 'success' only when every leg succeeded, - # 'skipped' when the whole matrix is skipped by design, 'cancelled' when a superseding push - # cancelled the run. None of these blocks the merge: a genuine leg failure is surfaced as a - # warning to investigate, and a cancelled superseded run is simply noise, not a failure. - run: | - result='${{ needs.changed.result }}' - echo "mutation legs: $result" - if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then - echo "::warning::JustDummies mutation legs did not all succeed ($result) — advisory only; see the 'Mutate the diff' legs and the weekly sweep" - fi - - # The weekly sweep: every mutant of both JustDummies packages, not just the ones a pull request - # touched. Advisory by construction — `--break-at 0` disables the threshold — because its job is to - # publish a trend, not to turn `main` red on a Monday morning over code nobody changed. - full: - name: Full sweep (${{ matrix.name }}) - if: github.event_name != 'pull_request' - runs-on: ubuntu-latest - # Same scope as `changed`: checkout, build, test — nothing written back. - permissions: - contents: read - # The sweep runs the library's whole test suite once per mutant, and `JustDummies` carries a few - # thousand of them — this is the longest job in the repository, and the reason the sweep is - # weekly and the gate is diff-scoped. Measured locally on four cores, it runs well past an hour; - # the cap therefore sits just under the six-hour ceiling GitHub imposes on a job, because this - # run is meant to take as long as it takes rather than be cut short. - timeout-minutes: 350 - strategy: - fail-fast: false - matrix: - include: - - name: justdummies - config: build/stryker/justdummies.json - - name: justdummies-xunit - config: build/stryker/justdummies-xunit.json - - name: justdummies-analyzers - config: build/stryker/justdummies-analyzers.json - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - - name: Setup .NET - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - dotnet-version: '10.0.x' - - - name: Restore the local tools - run: dotnet tool restore - - - name: Mutate everything - run: >- - dotnet stryker --config-file ${{ matrix.config }} - --break-at 0 - --output artifacts/mutation/${{ matrix.name }} - - - name: Summarise the surviving mutants - # Same table as the gate legs produce. On the sweep it is the deliverable, not a diagnosis - # aid: this is the list of behaviours nothing in the suite asserts, library by library. - if: always() - run: | - set -euo pipefail - report="artifacts/mutation/${{ matrix.name }}/reports/mutation-report.json" - if [ ! -f "$report" ]; then - echo "${{ matrix.name }}: no report produced" | tee -a "$GITHUB_STEP_SUMMARY" - exit 0 - fi - # Stryker writes a report even when it selects nothing, so "no mutant" has to be read from - # the report's contents, not from the file's absence: count the mutants that were actually - # put to the test, i.e. neither filtered out nor rejected by the compiler. - tested=$(jq '[.files[].mutants[] - | select(.status != "Ignored" and .status != "CompileError")] | length' "$report") - rows=$(jq -r --arg ws "$GITHUB_WORKSPACE/" ' - .files | to_entries[] | .key as $f | .value.mutants[] - | select(.status == "Survived" or .status == "Timeout" or .status == "NoCoverage") - | "| \(.status) | \($f | ltrimstr($ws)):\(.location.start.line) | \(.mutatorName) |" - ' "$report") - { - echo "### Mutation sweep — ${{ matrix.name }}" - echo - if [ "$tested" -eq 0 ]; then - echo "No mutant was selected — check the run's Stryker output." - elif [ -z "$rows" ]; then - echo "All $tested mutants were killed." - else - echo "| Status | Location | Mutation |" - echo "| --- | --- | --- |" - echo "$rows" - fi - } | tee -a "$GITHUB_STEP_SUMMARY" - - - name: Upload the mutation report - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: mutation-report-full-${{ matrix.name }} - path: artifacts/mutation/${{ matrix.name }}/reports/ - # The sweep always produces a report; nothing to upload means the run never got that far. - if-no-files-found: error diff --git a/.github/workflows/justdummies.yml b/.github/workflows/justdummies.yml deleted file mode 100644 index fba16e0d..00000000 --- a/.github/workflows/justdummies.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: justdummies - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -# Cancel superseded runs on the same branch / PR. -concurrency: - group: justdummies-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Least privilege: this workflow only checks out, packs and runs, so the token -# needs nothing beyond read access to the repository contents. -permissions: - contents: read - -env: - DOTNET_NOLOGO: 'true' - DOTNET_CLI_TELEMETRY_OPTOUT: 'true' - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' - -jobs: - # The ci workflow builds JustDummies for both TFMs and runs JustDummies.UnitTests on net10.0 — which resolves the - # NEAREST compatible asset (net8.0). So the netstandard2.0 asset that ships in the package is compiled but - # never EXECUTED by a test. This job closes that gap: it packs the real .nupkg and consumes it from an - # isolated project (tools/justdummies-check) once per consumer TFM, each forcing a different packaged asset, - # then proves by reflection which asset loaded and exercises the common smoke surface on it. - packaged-assets: - name: JustDummies packaged-asset compatibility - runs-on: ubuntu-latest - # Pack (~10s) plus two short consumer runs; cap a hung run like the other workflows. - timeout-minutes: 15 - env: - # Single source of truth for the throwaway package version, passed to the pack (-p:Version) and BOTH - # consume (-p:JustDummiesCheckVersion) steps. The run-number.attempt suffix makes every run produce a version - # NuGet has never cached, so the consume steps always restore the freshly packed .nupkg instead of a - # stale copy; run_attempt (which changes on a re-run) closes the door should a ~/.nuget cache ever be - # added. JustDummiesCheck.csproj pins this EXACT version, and nuget.config maps the id to the local feed only, - # so it can never resolve a future stable JustDummies from nuget.org. - JUSTDUMMIESCHECK_VERSION: 1.0.0-justdummiescheck.${{ github.run_number }}.${{ github.run_attempt }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - # Three SDKs on purpose: 10.0.x is the SDK release.yml packs with, so the pack step produces the exact - # artifact consumers receive; 8.0.x and 6.0.x bring the .NET 8 and .NET 6 RUNTIMES so each consumer leg - # executes on its own advertised runtime (default roll-forward stays within a major, so a net6.0 consumer - # binds .NET 6 and can never roll onto net8/net10). - - name: Setup .NET (pack SDK + downlevel runtimes) - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - dotnet-version: | - 6.0.x - 8.0.x - 10.0.x - - - name: Pack JustDummies under the release SDK - # Pack from the repo root so the root global.json selects the .NET 10 SDK — the one release.yml packs - # with — producing the exact multi-asset artifact a consumer restores (lib/netstandard2.0 + lib/net8.0). - # GenerateSBOM is off here: the SBOM is a release-pipeline concern (release.yml), and this job only needs - # the lib/ assets. The feed and the project-local package cache are wiped first so the consume steps can - # only see this run's package — a no-op on a fresh runner, but it keeps a reused workspace idempotent. - run: | - rm -rf tools/justdummies-check/local-feed tools/justdummies-check/packages - dotnet pack JustDummies/JustDummies.csproj -c Release -p:Version="$JUSTDUMMIESCHECK_VERSION" -p:GenerateSBOM=false -o tools/justdummies-check/local-feed - - - name: Validate the net8.0 asset (net8.0 consumer) - # A net8.0 consumer resolves lib/net8.0: the modern generators (DateOnly/Int128/Half/...) must be - # PRESENT. The program self-checks and exits non-zero on any mismatch; the greps are positive proof that - # it ran AND loaded the intended asset (a program that silently no-oped would exit 0 with no banner). - working-directory: tools/justdummies-check - run: | - dotnet run -c Release --framework net8.0 -p:JustDummiesCheckVersion="$JUSTDUMMIESCHECK_VERSION" > net8.log 2>&1 || { cat net8.log; exit 1; } - cat net8.log - grep -q 'ASSET=.NETCoreApp,Version=v8.0' net8.log - grep -q 'RESULT=PASS' net8.log - - - name: Validate the netstandard2.0 asset (net6.0 consumer) - # A net6.0 consumer resolves lib/netstandard2.0: the modern generators must be ABSENT, while the common - # surface (scalars, constraints, composition, collections, seeded reproducibility) still works. This is - # the leg the net10.0 test project can never exercise — the whole reason this workflow exists. - working-directory: tools/justdummies-check - run: | - dotnet run -c Release --framework net6.0 -p:JustDummiesCheckVersion="$JUSTDUMMIESCHECK_VERSION" > netstandard.log 2>&1 || { cat netstandard.log; exit 1; } - cat netstandard.log - grep -q 'ASSET=.NETStandard,Version=v2.0' netstandard.log - grep -q 'RESULT=PASS' netstandard.log - - - name: Cross-TFM seed equality (net8.0 asset vs netstandard2.0 asset) - # Each leg above printed a SEEDBATCH= line: the SAME fixed seed drawn from the COMMON surface, each on its - # OWN runtime from the asset that consumer TFM forced. The two packaged assets must produce an identical - # seeded sequence — new Random(seed) keeps the legacy algorithm on modern .NET so they SHOULD agree, but - # nothing else asserts it and Random reserves the right to differ across framework versions (issue #215). - # Compare byte-for-byte; the empty-guard fails closed so a missing/renamed banner can never pass silently. - working-directory: tools/justdummies-check - run: | - set -euo pipefail - net8_seq="$(sed -n 's/^SEEDBATCH=//p' net8.log)" - netstd_seq="$(sed -n 's/^SEEDBATCH=//p' netstandard.log)" - echo "net8.0 asset : ${net8_seq}" - echo "netstandard2.0 asset: ${netstd_seq}" - if [ -z "$net8_seq" ] || [ -z "$netstd_seq" ]; then - echo "FAIL: a SEEDBATCH banner was missing (net8.0='${net8_seq}', netstandard2.0='${netstd_seq}')" - exit 1 - fi - if [ "$net8_seq" != "$netstd_seq" ]; then - echo "FAIL: cross-TFM seed divergence — the two packaged assets produced different seeded sequences" - exit 1 - fi - echo "PASS: both assets produced an identical seeded sequence" diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index c5f01b18..f1cc8d9d 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -1,8 +1,8 @@ name: mutation -# Mutation testing for the FirstClassErrors libraries. JustDummies has its own, identical workflow -# — `justdummies-mutation.yml` — because it is destined for a repository of its own: the split has to -# be a file move, not an edit of a shared matrix. Keep the two in step when you change one. +# Mutation testing for the FirstClassErrors libraries. JustDummies used to ride an identical workflow +# of its own, written so that the repository split would be a file move rather than an edit of a shared +# matrix. The split happened (ADR-0069) and the file moved; nothing here needs a twin any more. on: pull_request: @@ -57,7 +57,6 @@ jobs: # shipped behaviour), and so does `GenDoc.Worker`: nothing unit-tests it in process — it is a # process entry point, exercised end to end by the `floor` job of `ci` — so mutating it would # report survivors that no test could ever kill. - # JustDummies and JustDummies.Xunit live in `justdummies-mutation.yml`. See ADR-0043. include: - name: core config: build/stryker/core.json diff --git a/.github/workflows/release-dryrun.yml b/.github/workflows/release-dryrun.yml index 9982e9f6..1630b6f9 100644 --- a/.github/workflows/release-dryrun.yml +++ b/.github/workflows/release-dryrun.yml @@ -70,7 +70,6 @@ jobs: run: | tools/packaging/pack.sh "$DRYRUN_VERSION" lib tools/packaging/pack.sh "$DRYRUN_VERSION" cli - tools/packaging/pack.sh "$DRYRUN_VERSION" dum # Rehearse the train-scoped release-notes generation too (release.yml's other production-only path), # so a bug in it surfaces here instead of during a real release. Prints to the log; publishes nothing. @@ -78,4 +77,3 @@ jobs: run: | echo "----- lib notes -----"; tools/packaging/release-notes.sh lib HEAD echo "----- cli notes -----"; tools/packaging/release-notes.sh cli HEAD - echo "----- dum notes -----"; tools/packaging/release-notes.sh dum HEAD diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 502cb4e5..f58e09b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,11 +5,9 @@ on: # Publish on a train-prefixed semantic-version tag. The trains version independently: # lib-v1.2.3 -> FirstClassErrors + FirstClassErrors.Testing + FirstClassErrors.RequestBinder # cli-v1.2.3 -> FirstClassErrors.Cli (the fce .NET tool) - # dum-v1.2.3 -> JustDummies (the standalone arbitrary-test-value library) tags: - 'lib-v*.*.*' - 'cli-v*.*.*' - - 'dum-v*.*.*' workflow_dispatch: inputs: component: @@ -18,7 +16,6 @@ on: options: - lib - cli - - dum required: true version: description: 'Package version, without any prefix (e.g. 1.2.3; a dry run can use any SemVer, e.g. 0.0.0-dry.1)' @@ -105,13 +102,12 @@ jobs: case "$REF_NAME" in lib-v*) COMPONENT="lib"; VERSION="${REF_NAME#lib-v}" ;; cli-v*) COMPONENT="cli"; VERSION="${REF_NAME#cli-v}" ;; - dum-v*) COMPONENT="dum"; VERSION="${REF_NAME#dum-v}" ;; - *) echo "::error::Tag '$REF_NAME' is not a release tag; expected lib-v*.*.*, cli-v*.*.* or dum-v*.*.*."; exit 1 ;; + *) echo "::error::Tag '$REF_NAME' is not a release tag; expected lib-v*.*.* or cli-v*.*.*."; exit 1 ;; esac fi case "$COMPONENT" in - lib|cli|dum) ;; - *) echo "::error::Invalid component '$COMPONENT'; expected 'lib', 'cli' or 'dum'."; exit 1 ;; + lib|cli) ;; + *) echo "::error::Invalid component '$COMPONENT'; expected 'lib' or 'cli'."; exit 1 ;; esac # Build metadata (+...) is deliberately REJECTED even though SemVer allows it: NuGet strips it # from the package identity, so a tag like lib-v1.2.3+build5 packs as FirstClassErrors.1.2.3.nupkg. @@ -190,7 +186,7 @@ jobs: echo "ok: GenDoc's error catalog has a breaking change since $PREVIOUS_TAG, matched by the major version bump ($PREVIOUS_MAJOR -> $NEW_MAJOR)." # Pack only the train this release targets (lib -> FirstClassErrors + .Testing + .RequestBinder; - # cli -> the fce tool; dum -> JustDummies), so a lib release never republishes the CLI and vice versa. + # cli -> the fce tool), so a lib release never republishes the CLI and vice versa. # The analyzer is bundled inside the main package and the GenDoc worker inside the CLI tool; the # samples are not published. # GenerateSBOM activates Microsoft.Sbom.Targets in each packable project: each package embeds its SPDX diff --git a/.gitignore b/.gitignore index 7fdfff19..5305c092 100644 --- a/.gitignore +++ b/.gitignore @@ -293,12 +293,6 @@ __pycache__/ tools/floor-check/local-feed/ tools/floor-check/packages/ tools/floor-check/build.log -# JustDummies packaged-asset compatibility check (tools/justdummies-check): CI-generated artifacts, never committed. -# local-feed holds the packed .nupkg the justdummies job produces; packages/ is the project-local NuGet -# extraction folder (RestorePackagesPath); *.log are the job's per-asset run outputs. -tools/justdummies-check/local-feed/ -tools/justdummies-check/packages/ -tools/justdummies-check/*.log # Stryker mutation runs (the `mutation` workflow, or `dotnet stryker` locally): reports and the temporary # copies of the solution Stryker compiles the mutants in. The workflow writes under artifacts/ (already # ignored above); a local run defaults to StrykerOutput/ next to the config file. diff --git a/AGENTS.md b/AGENTS.md index 4199736b..7b8cbcc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,12 +8,6 @@ Two roles are covered: **writing code** and **reviewing pull requests**. - .NET Standard 2.0 library. Errors are first-class, documented, diagnosable concepts. - Build: `dotnet build FirstClassErrors.sln` - Test: `dotnet test FirstClassErrors.sln` (analyzer tests: `dotnet test FirstClassErrors.Analyzers.UnitTests`). -- Adding a `JustDummies` test? It belongs to exactly one of two suites: - `JustDummies.PropertyTests` for invariants that hold for every legal constraint - argument, `JustDummies.UnitTests` for specific named cases — message content, - argument validation, structural conventions, dated regressions. Read - [`doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md`](doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md) - first; the decision behind it is ADR-0040. - Repository language is **English** (code, comments, commits, PRs, issues, and review comments). French lives only in `doc/handwritten/for-users/README.fr.md` and must stay in sync with the English README. diff --git a/CLAUDE.md b/CLAUDE.md index dd9d1bb8..aea078af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,16 +22,8 @@ errors should stay structured, documented, and close to the code. * Test: `dotnet test FirstClassErrors.sln` * Run the analyzer tests when touching analyzers: `dotnet test FirstClassErrors.Analyzers.UnitTests` -* `JustDummies` has two test suites, and a new test belongs to exactly one of them: - `JustDummies.PropertyTests` owns invariants that hold for every legal constraint - argument, `JustDummies.UnitTests` owns specific named cases (message content, - argument validation, structural conventions, dated regressions). The rule and how - to apply it are in - [`doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md`](doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md) - (decision: ADR-0040). Read it before adding a JustDummies test. * Mutation testing measures every pull request on the files it changed, for every - project whose code ships or runs, through two independent checks — one for the - FirstClassErrors libraries and tooling, one for the JustDummies packages + project whose code ships or runs (decisions: ADR-0043, and ADR-0046 which made the per-PR check **advisory** — it reports the diff's score but does not block the merge; the enforced bar is the weekly full sweep). A test that *executes* new code without *asserting* it will @@ -39,8 +31,7 @@ errors should stay structured, documented, and close to the code. with `dotnet tool restore && dotnet stryker --config-file build/stryker/.json --since:$(git merge-base origin/main HEAD)`; the configurations and the reasons behind them are in - [`mutation.en.md`](doc/handwritten/for-maintainers/workflows/mutation.en.md) and - [`justdummies-mutation.en.md`](doc/handwritten/for-maintainers/workflows/justdummies-mutation.en.md). + [`mutation.en.md`](doc/handwritten/for-maintainers/workflows/mutation.en.md). * Only report tests as passing if you actually ran the corresponding command. * If you did not run a relevant command, say so explicitly. @@ -142,7 +133,7 @@ The essentials, inlined so they hold even if `AGENTS.md` is not read: * Follow `.github/pull_request_template.md` for every pull request. * Do not open a pull request unless I explicitly ask for one. * PR titles, descriptions, commits, and branch names must be written in English. -* Write every commit message per [`CONTRIBUTING.md`](CONTRIBUTING.md): Conventional Commits, a closed type list, the scopes `core, analyzers, binder, cli, justdummies, gendoc, testing`, an imperative header within 72 characters, and `Refs: #NN` in a footer when a GitHub issue exists (issue-closing keywords belong in the PR description, not the commit). +* Write every commit message per [`CONTRIBUTING.md`](CONTRIBUTING.md): Conventional Commits, a closed type list, the scopes `core, analyzers, binder, cli, gendoc, testing`, an imperative header within 72 characters, and `Refs: #NN` in a footer when a GitHub issue exists (issue-closing keywords belong in the PR description, not the commit). * Write every pull request title per [`CONTRIBUTING.md`](CONTRIBUTING.md): name the whole change in English; a single-intention PR mirrors its commit header (`type(scope): description`), a multi-intention PR uses a short descriptive title, and issue references stay in the description, not the title. * Enable the local commit-message hook once per clone with `git config core.hooksPath .githooks`; the same check runs in CI on every pull request. * Before opening a pull request — and after pushing more commits to an open one — read the branch against a fresh `origin/main` and, if the history is messy (pending `fixup!`/`squash!`, wip/typo/"address review" commits, headers the lint rejects, one change split across non-standalone commits or two folded into one), **propose** a cleanup and rewrite only after I approve — while the branch is yours alone, with `git push --force-with-lease`, leaving the diff against `origin/main` unchanged. This repository merges with a merge commit, so a messy branch reaches `main`. Full rule in [`AGENTS.md`](AGENTS.md) ("Tidying history before a pull request"); the `/tidy-history` command runs it. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04c1f3e7..ae66f0ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,10 +13,6 @@ library produces. This guide defines how commits are written here. * Build: `dotnet build FirstClassErrors.sln` * Test: `dotnet test FirstClassErrors.sln` * Analyzer tests, when touching analyzers: `dotnet test FirstClassErrors.Analyzers.UnitTests` -* `JustDummies` tests are split across two suites — properties for invariants that - hold for every constraint argument, examples for specific named cases. See - [Writing JustDummies tests](doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md) - before adding one. See [`CLAUDE.md`](CLAUDE.md) for the project layout and the broader change guidelines. @@ -24,8 +20,8 @@ guidelines. ## Public API baseline The shipping libraries — `FirstClassErrors`, `FirstClassErrors.Testing`, -`FirstClassErrors.RequestBinder` (the `lib` train) and `JustDummies` (the `dum` -train) — carry a committed public-API baseline, so every change to their public +`FirstClassErrors.RequestBinder` (the `lib` train) — carry a committed +public-API baseline, so every change to their public surface is a reviewed diff and an accidental breaking change (a removed overload, a narrowed return type, a renamed member) cannot ship silently under a version number that promises compatibility. Two guards, wired once in @@ -39,8 +35,9 @@ number that promises compatibility. Two guards, wired once in surface change fails the build until the same change updates the baseline. * **Package validation** (`EnablePackageValidation`) runs ApiCompat during `dotnet pack`. With no baseline version set it performs the same-package - cross-target-framework check (it proves `JustDummies`' net8.0 surface never drops - API a netstandard2.0 consumer sees). To additionally gate against a published + cross-target-framework check, which matters for any multi-targeting package: it + proves the modern leg never drops API a netstandard2.0 consumer sees. To + additionally gate against a published version, set `PackageValidationBaselineVersion`; `0.1.0-preview.1` is the first such baseline available for the `lib` train. @@ -303,7 +300,6 @@ When present it MUST be lowercase and MUST be one of: | `analyzers` | `FirstClassErrors.Analyzers` — the Roslyn analyzers and their `FCExxx` diagnostics | | `binder` | `FirstClassErrors.RequestBinder` — the request binder for the primary-adapter boundary | | `cli` | `FirstClassErrors.Cli` — the command-line tool | -| `justdummies` | `JustDummies` — the standalone arbitrary-test-value generator | | `gendoc` | `FirstClassErrors.GenDoc` and its worker — the documentation generator | | `testing` | `FirstClassErrors.Testing` — the test-support package | @@ -315,8 +311,8 @@ The scope is load-bearing for the release record. The tooling partitions commits into **release trains** by scope — `tools/trains.sh` is the single source of truth — and each train publishes independently: `lib` (scopes `core`, `analyzers`, `testing`, `binder` → `FirstClassErrors`, `FirstClassErrors.Testing` and -`FirstClassErrors.RequestBinder`), `cli` (scopes `cli`, `gendoc` → the `fce` tool) -and `dum` (scope `justdummies` → `JustDummies`). A commit's scope decides which train's +`FirstClassErrors.RequestBinder`) and `cli` (scopes `cli`, `gendoc` → the `fce` +tool). A commit's scope decides which train's release notes and changelog it lands in; see [Adding a release train](doc/handwritten/for-maintainers/AddingAReleaseTrain.en.md). diff --git a/Directory.Build.props b/Directory.Build.props index 852e574f..353430c8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - - - - - - - - - - - - - - diff --git a/JustDummies.Analyzers/AnalyzerReleases.Shipped.md b/JustDummies.Analyzers/AnalyzerReleases.Shipped.md deleted file mode 100644 index f50bb1fe..00000000 --- a/JustDummies.Analyzers/AnalyzerReleases.Shipped.md +++ /dev/null @@ -1,2 +0,0 @@ -; Shipped analyzer releases -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/JustDummies.Analyzers/AnalyzerReleases.Unshipped.md b/JustDummies.Analyzers/AnalyzerReleases.Unshipped.md deleted file mode 100644 index 3f91912f..00000000 --- a/JustDummies.Analyzers/AnalyzerReleases.Unshipped.md +++ /dev/null @@ -1,35 +0,0 @@ -; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - -### New Rules - -Rule ID | Category | Severity | Notes ---------|-----------------------------|----------|------------------------------------------- -JD001 | JustDummies.Reproducibility | Error | AsyncBodyPassedToReproduciblyAnalyzer -JD002 | JustDummies.Reproducibility | Error | DiscardedReproduciblyAsyncResultAnalyzer -JD003 | JustDummies.Reproducibility | Error | AwaitableBodyPassedToReproduciblyAnalyzer -JD004 | JustDummies.Reproducibility | Error | DiscardedSeedingResultAnalyzer -JD005 | JustDummies.Usage | Error | GeneratorRenderedAsTextAnalyzer -JD006 | JustDummies.Usage | Warning | DiscardedGeneratorResultAnalyzer -JD007 | JustDummies.Reproducibility | Warning | DrawOutsideThePinnedScopeAnalyzer -JD008 | JustDummies.Reproducibility | Warning | ArbitraryValueInTheoryDataAnalyzer -JD009 | JustDummies.Reproducibility | Warning | DrawInStaticInitializerAnalyzer -JD010 | JustDummies.Reproducibility | Warning | ReproducibleOnNonTestMethodAnalyzer -JD011 | JustDummies.Usage | Disabled | GeneratorWhereValueExpectedAnalyzer -JD012 | JustDummies.Usage | Warning | GeneratorPooledAsValueAnalyzer -JD013 | JustDummies.Usage | Warning | HeldCollectionPassedToOneOfAnalyzer -JD014 | JustDummies.Constraints | Warning | RejectedConstantArgumentAnalyzer -JD015 | JustDummies.Constraints | Warning | StringConstraintsAdmitNoValueAnalyzer -JD016 | JustDummies.Constraints | Warning | CollectionConstraintsAdmitNoValueAnalyzer -JD017 | JustDummies.Constraints | Warning | EnumUniverseViolationAnalyzer -JD018 | JustDummies.Reproducibility | Warning | NestedReproducibilityScopeAnalyzer -JD019 | JustDummies.Reproducibility | Disabled | CommittedReplaySeedAnalyzer -JD020 | JustDummies.Reproducibility | Info | SharedStaticAnyContextAnalyzer -JD021 | JustDummies.Reproducibility | Warning | BlankReplaySnippetAnalyzer -JD022 | JustDummies.Reproducibility | Info | ParallelDrawWithoutPerItemSeedAnalyzer -JD023 | JustDummies.Constraints | Warning | ScalarChainAdmitsNoValueAnalyzer -JD024 | JustDummies.Constraints | Info | ScalarChainAdmitsNoValueAnalyzer -JD025 | JustDummies.Constraints | Warning | DuplicatePoolValueAnalyzer -JD026 | JustDummies.Constraints | Warning | EmptyRelativeUriAnalyzer -JD027 | JustDummies.Composition | Warning | UnusedCombineOperandAnalyzer -JD028 | JustDummies.Composition | Warning | InertDistinctnessAnalyzer diff --git a/JustDummies.Analyzers/AnyChainFacts.cs b/JustDummies.Analyzers/AnyChainFacts.cs deleted file mode 100644 index 3f94000f..00000000 --- a/JustDummies.Analyzers/AnyChainFacts.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// Walks a fluent generator chain back to the factory that started it, and yields the constraint calls in the -/// order they were written. Every rule that reasons about a combination of constraints — rather than about -/// one argument — needs this, because the library's own conflict detection is order-sensitive in its messages and -/// order-insensitive in its verdict. -/// -/// -/// Syntactic on purpose. The chain must be written as one expression; a generator passed through a local, a field -/// or a helper is not followed. That is a deliberate limit, not an oversight: following it would mean dataflow, -/// and a rule that claims a chain is unsatisfiable must be certain of every constraint the chain carries. -/// -internal static class AnyChainFacts { - - /// - /// The constraint calls of the chain belongs to, outermost call last, together - /// with the factory that rooted it. Returns false when the chain does not start at a - /// JustDummies.Any / AnyContext factory, which is the only case a rule can reason about - /// completely. - /// - public static bool TryGetChain(IInvocationOperation invocation, KnownSymbols symbols, out IReadOnlyList constraints, out IInvocationOperation? factory) { - List collected = []; - constraints = collected; - factory = null; - - // Climb to the outermost invocation of the chain, so a rule registered on any link sees the whole of it. - IInvocationOperation outermost = invocation; - while (outermost.Parent is IInvocationOperation parent && ReferenceEquals(GeneratorFacts.Unwrap(parent.Instance ?? parent), outermost)) { - outermost = parent; - } - - // Unbounded on purpose, and it terminates: every branch below either returns or steps `current` one - // link down the receiver chain, which a syntax tree makes finite. Written `;;` rather than with a - // `current is not null` guard because that guard could never be false — `next` is pattern-matched - // non-null — which left the exit path unreachable (Sonar csharpsquid:S2583). Same shape as the - // redraw loop in AnyPattern. - for (IInvocationOperation current = outermost;;) { - if (current.Instance is null) { - // A static call roots the chain: it is the factory when it belongs to Any, otherwise this is not a - // JustDummies chain at all. - if (!IsFactoryOwner(current.TargetMethod.ContainingType, symbols)) { return false; } - - factory = current; - collected.Reverse(); - - return true; - } - - IOperation receiver = GeneratorFacts.Unwrap(current.Instance); - - // An instance call on AnyContext roots the chain the same way Any's static factories do. - if (receiver is not IInvocationOperation next) { - if (IsFactoryOwner(current.TargetMethod.ContainingType, symbols)) { - factory = current; - collected.Reverse(); - - return true; - } - - return false; - } - - collected.Add(current); - current = next; - } - } - - private static bool IsFactoryOwner(INamedTypeSymbol? type, KnownSymbols symbols) { - return SymbolEqualityComparer.Default.Equals(type, symbols.Any) - || SymbolEqualityComparer.Default.Equals(type, symbols.AnyContext); - } - -} diff --git a/JustDummies.Analyzers/ArbitraryValueInTheoryDataAnalyzer.cs b/JustDummies.Analyzers/ArbitraryValueInTheoryDataAnalyzer.cs deleted file mode 100644 index c2922998..00000000 --- a/JustDummies.Analyzers/ArbitraryValueInTheoryDataAnalyzer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD008 — reports a value drawn inside a theory's data provider. xUnit evaluates providers at discovery, -/// before any test case runs: the draw happens once for the whole run, outside every seed scope, and every case of -/// the theory shares the one value. The theory reads as if it enumerated arbitrary cases and enumerates a constant. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ArbitraryValueInTheoryDataAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.ArbitraryValueInTheoryData); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - // Nothing to reason about without xUnit: a data provider is an xUnit concept. - if (symbols.MemberDataAttribute is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!GeneratorFacts.IsGenerateCall(invocation, symbols.IAny!)) { return; } - if (!GeneratorFacts.RootsAtAmbientAny(invocation, symbols.Any!)) { return; } - if (!XunitFacts.IsTheoryDataProvider(context.ContainingSymbol, symbols)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.ArbitraryValueInTheoryData, invocation.Syntax.GetLocation())); - } - -} diff --git a/JustDummies.Analyzers/AsyncBodyPassedToReproduciblyAnalyzer.cs b/JustDummies.Analyzers/AsyncBodyPassedToReproduciblyAnalyzer.cs deleted file mode 100644 index f1def24f..00000000 --- a/JustDummies.Analyzers/AsyncBodyPassedToReproduciblyAnalyzer.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD001 — reports an async lambda passed to the synchronous Any.Reproducibly(Action). Bound to an -/// it becomes async void, so the body's failures after the first await -/// escape the reproducible scope entirely and never fail the test. Use Any.ReproduciblyAsync(Func<Task>) -/// and await it. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class AsyncBodyPassedToReproduciblyAnalyzer : DiagnosticAnalyzer { - - private const string AnyMetadataName = "JustDummies.Any"; - private const string ReproduciblyMethodName = "Reproducibly"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.AsyncBodyPassedToReproducibly); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - INamedTypeSymbol? anyType = context.Compilation.GetTypeByMetadataName(AnyMetadataName); - if (anyType is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, anyType), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol anyType) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != ReproduciblyMethodName || !SymbolEqualityComparer.Default.Equals(method.ContainingType, anyType)) { return; } - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (TryGetAsyncLambda(argument.Value, out IAnonymousFunctionOperation? lambda)) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AsyncBodyPassedToReproducibly, lambda!.Syntax.GetLocation())); - } - } - } - - // An async lambda bound to the Action parameter is wrapped in a delegate creation; unwrap it and read the - // anonymous function's own IsAsync (its return is void — that is precisely the async-void hazard). - private static bool TryGetAsyncLambda(IOperation value, out IAnonymousFunctionOperation? lambda) { - IOperation inner = value is IDelegateCreationOperation delegateCreation ? delegateCreation.Target : value; - if (inner is IAnonymousFunctionOperation { Symbol.IsAsync: true } anonymous) { - lambda = anonymous; - - return true; - } - - lambda = null; - - return false; - } - -} diff --git a/JustDummies.Analyzers/AwaitableBodyPassedToReproduciblyAnalyzer.cs b/JustDummies.Analyzers/AwaitableBodyPassedToReproduciblyAnalyzer.cs deleted file mode 100644 index cc82345f..00000000 --- a/JustDummies.Analyzers/AwaitableBodyPassedToReproduciblyAnalyzer.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD003 — reports the two asynchronous bodies Any.Reproducibly(Action) accepts that JD001 does not see: a -/// synchronous lambda whose body is an awaitable the call then drops, and an async void method passed -/// as a method group. Both reproduce JD001's damage — the reproducible scope returns before the body's assertions -/// run, and their failures never reach the test — while compiling without a single diagnostic, CS4014 -/// included, because the enclosing lambda is not itself async. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class AwaitableBodyPassedToReproduciblyAnalyzer : DiagnosticAnalyzer { - - private const string ReproduciblyMethodName = "Reproducibly"; - private const string GetAwaiterMethodName = "GetAwaiter"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.AwaitableBodyPassedToReproducibly); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols.Any), OperationKind.Invocation); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with \"LINQ\" expressions", - Justification = - "The rule asks for Select(argument => argument.Value). The loop unwraps a delegate creation before testing what it found and " + - "reports on the lambda body, so the projection would rename the loop variable away from what it is without removing a single " + - "step: `argument` is an IArgumentOperation, and the unwrapping still has to happen inside.")] - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol anyType) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != ReproduciblyMethodName || !SymbolEqualityComparer.Default.Equals(method.ContainingType, anyType)) { return; } - - foreach (IArgumentOperation argument in invocation.Arguments) { - IOperation value = argument.Value is IDelegateCreationOperation delegateCreation ? delegateCreation.Target : argument.Value; - - if (value is IAnonymousFunctionOperation { Symbol.IsAsync: false } lambda) { - ReportDroppedAwaitables(context, lambda.Body); - } else if (IsAsyncVoidMethodReference(value)) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AwaitableBodyPassedToReproducibly, value.Syntax.GetLocation())); - } - } - } - - // A synchronous lambda bound to the Action parameter whose body produces an awaitable nobody awaits. JD001 reads - // the lambda's own IsAsync and so passes this by; here the lambda is synchronous and it is the *body's* task that - // is dropped — the same silent green, through the door JD001 leaves open. - private static void ReportDroppedAwaitables(OperationAnalysisContext context, IOperation node) { - // A nested lambda or local function has its own binding and its own author intent; a fire-and-forget there is - // not this call's business. - if (node is IAnonymousFunctionOperation or ILocalFunctionOperation) { return; } - - if (node is IExpressionStatementOperation statement && IsAwaitable(statement.Operation.Type)) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AwaitableBodyPassedToReproducibly, statement.Operation.Syntax.GetLocation())); - - return; - } - - foreach (IOperation child in node.ChildOperations) { ReportDroppedAwaitables(context, child); } - } - - // `async void` reaches Reproducibly as a method group: the delegate creation binds it to Action with no warning, - // and the body's post-await exception escapes the scope's try/catch exactly as JD001's async lambda would. - private static bool IsAsyncVoidMethodReference(IOperation value) { - return value is IMethodReferenceOperation { Method: { IsAsync: true, ReturnsVoid: true } }; - } - - // Awaitability is a shape, not a type: anything exposing GetAwaiter() qualifies, which covers Task, Task, - // ValueTask, ValueTask and a consumer's own awaitable without naming any of them. - private static bool IsAwaitable(ITypeSymbol? type) { - if (type is null || type.SpecialType == SpecialType.System_Void) { return false; } - - for (ITypeSymbol? current = type; current is not null; current = current.BaseType) { - if (current.GetMembers(GetAwaiterMethodName) - .Any(member => member is IMethodSymbol { Parameters.IsEmpty: true, DeclaredAccessibility: Accessibility.Public })) { return true; } - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/BlankReplaySnippetAnalyzer.cs b/JustDummies.Analyzers/BlankReplaySnippetAnalyzer.cs deleted file mode 100644 index 93b7b182..00000000 --- a/JustDummies.Analyzers/BlankReplaySnippetAnalyzer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD021 — reports a blank replay snippet handed to Any.UseSeed(int, string). The guard rejects it at run -/// time, and because that scope is normally opened from a test-framework adapter's hook, the throw surfaces as an -/// infrastructure failure on every test in the suite rather than as one failing assertion — a -/// disproportionately expensive way to learn about a typo the compiler can already see. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class BlankReplaySnippetAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.BlankReplaySnippet); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - INamedTypeSymbol any = symbols.Any; - - context.RegisterOperationAction(operationContext => Analyze(operationContext, any), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol any) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.TargetMethod.Name != "UseSeed") { return; } - if (!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, any)) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.Parameter?.Name != "replaySnippet") { continue; } - if (!ConstantFacts.TryGetString(argument.Value, out string snippet)) { continue; } - if (snippet.Trim().Length != 0) { continue; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.BlankReplaySnippet, argument.Value.Syntax.GetLocation())); - - return; - } - } - -} diff --git a/JustDummies.Analyzers/CollectionConstraintsAdmitNoValueAnalyzer.cs b/JustDummies.Analyzers/CollectionConstraintsAdmitNoValueAnalyzer.cs deleted file mode 100644 index 89347333..00000000 --- a/JustDummies.Analyzers/CollectionConstraintsAdmitNoValueAnalyzer.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD016 — reports a collection chain whose constant count constraints cannot all hold, or which asks for more -/// distinct elements than its element generator can produce. Both throw at declaration time, so this moves an -/// arrange-time red to a build-time red — worth it because the chain usually sits in a helper several call frames -/// away from the test that dies on it. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class CollectionConstraintsAdmitNoValueAnalyzer : DiagnosticAnalyzer { - - /// - /// How many values Any.Boolean() can produce. Restated here rather than read from the library: an - /// analyzer ships without it and reasons over symbols, so a fact about a generator's domain has to be - /// written down on this side too. - /// - private const int BooleanValueCount = 2; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.CollectionConstraintsAdmitNoValue); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null || !IsCollectionFactory(factory.TargetMethod.Name)) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - AnalyzeConstraints(context, factory, constraints); - } - - /// - /// Reads the counts, the distinctness and the contained elements the chain declares, then reports the first - /// combination of them no collection can satisfy. - /// - /// - /// Split from , which answers a different question: whether this chain is one the rule - /// reasons about at all. Everything below assumes that answer is yes. - /// - private static void AnalyzeConstraints(OperationAnalysisContext context, IInvocationOperation factory, IReadOnlyList constraints) { - int? exact = null; - int? minimum = null; - int? maximum = null; - bool distinct = factory.TargetMethod.Name is "SetOf" or "DictionaryOf"; - int contained = 0; - IOperation? at = null; - - foreach (IInvocationOperation constraint in constraints) { - at = constraint; - - switch (constraint.TargetMethod.Name) { - case "Empty": exact = 0; break; - case "NonEmpty": minimum = System.Math.Max(minimum ?? 0, 1); break; - case "Distinct": distinct = true; break; - case "Containing" or "ContainingKey" or "ContainingEntry": contained++; break; - - case "WithCount" when TryConstant(constraint, out int count): exact = count; break; - case "WithMinCount" when TryConstant(constraint, out int min): minimum = System.Math.Max(minimum ?? 0, min); break; - case "WithMaxCount" when TryConstant(constraint, out int max): maximum = System.Math.Min(maximum ?? int.MaxValue, max); break; - - case "WithCountBetween" when constraint.Arguments.Length == 2 - && ConstantFacts.TryGetInt32(constraint.Arguments[0].Value, out int low) - && ConstantFacts.TryGetInt32(constraint.Arguments[1].Value, out int high): - minimum = System.Math.Max(minimum ?? 0, low); - maximum = System.Math.Min(maximum ?? int.MaxValue, high); - - break; - } - } - - if (at is null) { return; } - - int effectiveMin = System.Math.Max(minimum ?? 0, exact ?? 0); - int effectiveMax = System.Math.Min(maximum ?? int.MaxValue, exact ?? int.MaxValue); - - if (effectiveMin > effectiveMax) { - Report(context, at, $"the declared counts require at least {effectiveMin} element(s) and at most {effectiveMax}"); - - return; - } - - if (contained > effectiveMax) { - Report(context, at, $"{contained} element(s) are required to be contained, which cannot fit in a collection of at most {effectiveMax}"); - - return; - } - - // The cardinality gate (ADR-0013): a distinct collection cannot hold more elements than its element - // generator has distinct values to give. - if (!distinct) { return; } - if (!TryGetProvableCardinality(factory, out int cardinality)) { return; } - if (effectiveMin <= cardinality) { return; } - - Report(context, at, $"{effectiveMin} distinct element(s) are required, but the element generator can produce only {cardinality}"); - } - - private static void Report(OperationAnalysisContext context, IOperation at, string reason) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.CollectionConstraintsAdmitNoValue, at.Syntax.GetLocation(), reason)); - } - - private static bool TryConstant(IInvocationOperation constraint, out int value) { - value = 0; - - return constraint.Arguments.Length == 1 && ConstantFacts.TryGetInt32(constraint.Arguments[0].Value, out value); - } - - private static bool IsCollectionFactory(string name) { - return name is "ListOf" or "ArrayOf" or "SequenceOf" or "SetOf" or "DictionaryOf"; - } - - /// - /// An upper bound on the element generator's distinct domain, for the shapes the compiler can settle. Anything - /// else answers false: an unprovable domain must never be treated as a small one. - /// - private static bool TryGetProvableCardinality(IInvocationOperation factory, out int cardinality) { - cardinality = 0; - - if (factory.Arguments.Length == 0) { return false; } - if (GeneratorFacts.Unwrap(factory.Arguments[0].Value) is not IInvocationOperation element) { return false; } - - // Walk the element chain back to its own factory, watching what the constraints along the way do to the - // domain. AllowingCombinations() WIDENS an enum's universe to the OR-closure of its declared members — eight - // values for four flags — so counting declared members there would under-report the domain and condemn a legal - // chain. An unprovable domain must never be treated as a small one, so the rule stands down instead of - // computing the closure: a deliberate false negative, not an oversight. - IInvocationOperation root = element; - while (root.Instance is not null && GeneratorFacts.Unwrap(root.Instance) is IInvocationOperation inner) { - if (root.TargetMethod.Name == "AllowingCombinations") { return false; } - - root = inner; - } - - switch (root.TargetMethod.Name) { - case "Boolean": - cardinality = BooleanValueCount; - - return true; - - case "Enum" when root.TargetMethod.TypeArguments.Length == 1 && root.TargetMethod.TypeArguments[0] is INamedTypeSymbol enumType: - cardinality = enumType.GetMembers().OfType().Count(field => field.HasConstantValue); - - return cardinality > 0; - - case "OneOf" or "ElementOf": - return TryCountDistinctConstants(root, out cardinality); - - default: - return false; - } - } - - private static bool TryCountDistinctConstants(IInvocationOperation pool, out int cardinality) { - cardinality = 0; - - foreach (IArgumentOperation argument in pool.Arguments) { - if (argument.ArgumentKind != ArgumentKind.ParamArray) { continue; } - if (argument.Value is not IArrayCreationOperation { Initializer: { } initializer }) { return false; } - - HashSet distinct = []; - foreach (IOperation element in initializer.ElementValues) { - Optional constant = GeneratorFacts.Unwrap(element).ConstantValue; - if (!constant.HasValue) { return false; } - - distinct.Add(constant.Value); - } - - cardinality = distinct.Count; - - return cardinality > 0; - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/CommittedReplaySeedAnalyzer.cs b/JustDummies.Analyzers/CommittedReplaySeedAnalyzer.cs deleted file mode 100644 index a053e25e..00000000 --- a/JustDummies.Analyzers/CommittedReplaySeedAnalyzer.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD019 — reports a replay seed pinned in committed code. The seeded overloads exist to replay a run a -/// failure reported: correct while you are reproducing, wrong the moment it is committed, because the test then -/// draws the same values for ever and stops surfacing the coupling the library exists to reveal. -/// -/// -/// Opt-in, and it must be. This repository's own maintainer guide instructs the opposite for a whole class of -/// tests — "Pin a seed for anything statistical" — so a rule enabled by default would fight documented practice. -/// It earns its keep as a pre-release sweep, not as a standing check. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class CommittedReplaySeedAnalyzer : DiagnosticAnalyzer { - - private const string SeedPropertyName = "Seed"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.CommittedReplaySeed); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - context.RegisterOperationAction(operationContext => AnalyzeInvocation(operationContext, symbols), OperationKind.Invocation); - - if (symbols.ReproducibleAttribute is not null) { - context.RegisterSymbolAction(symbolContext => AnalyzeAttribute(symbolContext, symbols.ReproducibleAttribute), SymbolKind.Method, SymbolKind.NamedType); - } - } - - private static void AnalyzeInvocation(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.TargetMethod.Name is not ("Reproducibly" or "ReproduciblyAsync" or "UseSeed" or "WithSeed")) { return; } - if (!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, symbols.Any)) { return; } - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.Parameter?.Name != "seed") { continue; } - if (argument.ArgumentKind == ArgumentKind.DefaultValue) { continue; } - if (!ConstantFacts.TryGetInt32(argument.Value, out int seed)) { continue; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.CommittedReplaySeed, argument.Value.Syntax.GetLocation(), seed)); - - return; - } - } - - private static void AnalyzeAttribute(SymbolAnalysisContext context, INamedTypeSymbol reproducibleAttribute) { - foreach (AttributeData attribute in context.Symbol.GetAttributes()) { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, reproducibleAttribute)) { continue; } - - foreach (KeyValuePair named in attribute.NamedArguments) { - if (named.Key != SeedPropertyName || named.Value.Value is not int seed) { continue; } - - Location location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() - ?? context.Symbol.Locations[0]; - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.CommittedReplaySeed, location, seed)); - - return; - } - } - } - -} diff --git a/JustDummies.Analyzers/ConstantFacts.cs b/JustDummies.Analyzers/ConstantFacts.cs deleted file mode 100644 index a2b13308..00000000 --- a/JustDummies.Analyzers/ConstantFacts.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// Reads the statically-known value of an argument. Deliberately wider than on -/// IOperation.ConstantValue: a is never a C# constant, yet -/// TimeSpan.Zero and TimeSpan.FromSeconds(1) are as statically known as any literal, and the -/// constraints that take one are exactly the ones worth checking. -/// -/// -/// Every reader answers "no" rather than guessing. A rule built on this can therefore only under-report, which is -/// the right failure direction for a diagnostic that claims a call is certainly wrong. -/// -internal static class ConstantFacts { - - private const string TimeSpanMetadataName = "System.TimeSpan"; - private const string ZeroFieldName = "Zero"; - - /// Reads a constant , folding named constants as the compiler does. - public static bool TryGetInt32(IOperation operation, out int value) { - value = 0; - - IOperation unwrapped = GeneratorFacts.Unwrap(operation); - if (unwrapped.ConstantValue is not { HasValue: true, Value: int constant }) { return false; } - - value = constant; - - return true; - } - - /// Reads a constant . A null literal answers false: a null argument is - /// the null-guard's business, not a constraint rule's. - public static bool TryGetString(IOperation operation, out string value) { - value = string.Empty; - - IOperation unwrapped = GeneratorFacts.Unwrap(operation); - if (unwrapped.ConstantValue is not { HasValue: true, Value: string constant }) { return false; } - - value = constant; - - return true; - } - - /// - /// Whether the operation is a statically-known non-positive — the shape - /// every granularity guard rejects. Recognises TimeSpan.Zero and the TimeSpan.FromXxx(constant) - /// factories; anything else answers false. - /// - public static bool IsNonPositiveTimeSpan(IOperation operation, Compilation compilation) { - INamedTypeSymbol? timeSpan = compilation.GetTypeByMetadataName(TimeSpanMetadataName); - if (timeSpan is null) { return false; } - - IOperation unwrapped = GeneratorFacts.Unwrap(operation); - - if (unwrapped is IFieldReferenceOperation field) { - return field.Field.Name == ZeroFieldName && SymbolEqualityComparer.Default.Equals(field.Field.ContainingType, timeSpan); - } - - // TimeSpan.FromSeconds(0), FromMinutes(-1) ... — a single constant numeric argument settles the sign. - if (unwrapped is IInvocationOperation { Arguments.Length: 1 } invocation - && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, timeSpan) - && invocation.TargetMethod.Name.StartsWith("From", System.StringComparison.Ordinal)) { - - IOperation argument = GeneratorFacts.Unwrap(invocation.Arguments[0].Value); - if (argument.ConstantValue is { HasValue: true, Value: { } raw }) { - return raw switch { - double d => d <= 0, - int i => i <= 0, - long l => l <= 0, - _ => false, - }; - } - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/Descriptors.cs b/JustDummies.Analyzers/Descriptors.cs deleted file mode 100644 index f5146556..00000000 --- a/JustDummies.Analyzers/Descriptors.cs +++ /dev/null @@ -1,297 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace JustDummies.Analyzers; - -/// -/// The for every JustDummies rule. One field per JDxxx. -/// -internal static class Descriptors { - - public static readonly DiagnosticDescriptor AsyncBodyPassedToReproducibly = new( - id: DiagnosticIds.AsyncBodyPassedToReproducibly, - title: "An asynchronous body is passed to Any.Reproducibly", - messageFormat: "Pass the asynchronous body to Any.ReproduciblyAsync and await it: Any.Reproducibly takes an Action, so an async lambda runs as 'async void' and its failures never reach the test", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Any.Reproducibly takes a synchronous Action. An async lambda bound to it becomes 'async void', whose exceptions escape the reproducible scope entirely and never fail the test. Use Any.ReproduciblyAsync(Func) and await it.", - helpLinkUri: HelpLinks.For(DiagnosticIds.AsyncBodyPassedToReproducibly)); - - public static readonly DiagnosticDescriptor DiscardedReproduciblyAsyncResult = new( - id: DiagnosticIds.DiscardedReproduciblyAsyncResult, - title: "The task returned by Any.ReproduciblyAsync is discarded", - messageFormat: "Await the task returned by Any.ReproduciblyAsync; discarding it silently drops the body's failures", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Any.ReproduciblyAsync returns a Task that faults with the body's exception. Discarding it (as a bare statement or via '_ =') lets a failing test pass green. Await it.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DiscardedReproduciblyAsyncResult)); - - public static readonly DiagnosticDescriptor AwaitableBodyPassedToReproducibly = new( - id: DiagnosticIds.AwaitableBodyPassedToReproducibly, - title: "An asynchronous body reaches Any.Reproducibly without being awaited", - messageFormat: "Pass the asynchronous body to Any.ReproduciblyAsync and await it: bound to Any.Reproducibly's Action the body is never awaited, so the scope returns before the assertions run and their failures never reach the test", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Any.Reproducibly takes a synchronous Action. A synchronous lambda whose body produces a task drops that task, and an 'async void' method group bound to the Action raises its failures outside the scope entirely. Neither is reported by the compiler — CS4014 does not fire when the enclosing lambda is not itself async. Use Any.ReproduciblyAsync(Func) and await it.", - helpLinkUri: HelpLinks.For(DiagnosticIds.AwaitableBodyPassedToReproducibly)); - - public static readonly DiagnosticDescriptor DiscardedSeedingResult = new( - id: DiagnosticIds.DiscardedSeedingResult, - title: "The result of a seeding call is discarded", - messageFormat: "Do not discard the result of Any.{0}: {1}", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Any.UseSeed returns the handle that closes the scope it opened; dropping it leaves the seed pinned for whatever runs next in the same execution context, silently making later tests replay one fixed sequence. Any.WithSeed returns an isolated context and pins nothing, so discarding it is dead code at a call site that reads as if the run had been seeded.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DiscardedSeedingResult)); - - public static readonly DiagnosticDescriptor GeneratorRenderedAsText = new( - id: DiagnosticIds.GeneratorRenderedAsText, - title: "A generator is rendered as text instead of the value it would draw", - messageFormat: "Call Generate() on the {0}: rendered as text a generator yields its own type name, not an arbitrary value", - category: DiagnosticCategories.Usage, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "A generator is an immutable recipe, and no JustDummies generator overrides ToString(). Interpolating, concatenating or calling ToString() on one therefore produces the builder's type name — a non-empty, plausible, run-invariant string that flows into the code under test as if it were an arbitrary value. Materialize the value with Generate().", - helpLinkUri: HelpLinks.For(DiagnosticIds.GeneratorRenderedAsText)); - - public static readonly DiagnosticDescriptor DiscardedGeneratorResult = new( - id: DiagnosticIds.DiscardedGeneratorResult, - title: "The generator returned by a constraint is discarded", - messageFormat: "Assign the result of {0} back: a generator is an immutable recipe, so a constraint whose result is discarded constrains nothing", - category: DiagnosticCategories.Usage, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Every constraint returns a new generator rather than mutating the receiver. A discarded result therefore silently drops the invariant the arrangement declared, and the generator keeps drawing from the wider domain — so the test passes on most runs and fails on the one that draws outside it, with a value nobody can reproduce.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DiscardedGeneratorResult)); - - public static readonly DiagnosticDescriptor DrawOutsideThePinnedScope = new( - id: DiagnosticIds.DrawOutsideThePinnedScope, - title: "An arbitrary value is drawn before [Reproducible] pins the seed", - messageFormat: "Draw this value inside the test body: {0} runs before [Reproducible] opens the seed scope, so the seed the failure reports does not replay it", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "xUnit constructs the test-class instance, and awaits IAsyncLifetime.InitializeAsync, before running the hooks the adapter pins the seed from. A value drawn there comes from the unseeded ambient source, so the test advertises full reproducibility while part of its arrangement is unpinned: pinning the reported seed does not bring the failure back.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DrawOutsideThePinnedScope)); - - public static readonly DiagnosticDescriptor ArbitraryValueInTheoryData = new( - id: DiagnosticIds.ArbitraryValueInTheoryData, - title: "A theory's data provider draws an arbitrary value", - messageFormat: "Draw this value in the test body, or let the provider yield the generator: theory data is produced at discovery, before any seed is pinned, and every case shares the one value", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "xUnit evaluates a theory's data provider at discovery time, once for the whole run and outside every seed scope. The drawn value is therefore shared by every case of the theory, replayable from no reported seed, and constant where the theory reads as if it enumerated arbitrary cases.", - helpLinkUri: HelpLinks.For(DiagnosticIds.ArbitraryValueInTheoryData)); - - public static readonly DiagnosticDescriptor DrawInStaticInitializer = new( - id: DiagnosticIds.DrawInStaticInitializer, - title: "An arbitrary value is drawn in a static initializer", - messageFormat: "Hold the generator rather than the value: a static initializer draws once for the whole suite, under whichever test happened to run first", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "A type initializer runs once, lazily, when the first test touches the type. The value is drawn under whatever ambient context that test had pinned, is shared by every other test in the class, and is replayable from none of their reported seeds — so the tests become order-dependent and stop varying between runs. Store the generator in the static field and call Generate() per test.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DrawInStaticInitializer)); - - public static readonly DiagnosticDescriptor ReproducibleOnNonTestMethod = new( - id: DiagnosticIds.ReproducibleOnNonTestMethod, - title: "[Reproducible] is applied to a method that is not a test", - messageFormat: "Remove [Reproducible] from '{0}' or make it a test: xUnit collects the attribute from the test method, its class and the assembly only, so here it pins nothing", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The adapter's hooks are collected from a test method, its declaring class and the assembly. On a helper — or on a method whose [Fact] was removed during a refactor — the attribute is never read: it pins no seed and reports none. Because a working [Reproducible] is silent on a passing test by design, nothing else distinguishes the inert form from the working one.", - helpLinkUri: HelpLinks.For(DiagnosticIds.ReproducibleOnNonTestMethod)); - - public static readonly DiagnosticDescriptor GeneratorWhereValueExpected = new( - id: DiagnosticIds.GeneratorWhereValueExpected, - title: "A generator reaches a position that expected its value", - messageFormat: "Call Generate() on the {0}: passed where an object is expected, the recipe itself is stored, compared or asserted on, never the value it would draw", - category: DiagnosticCategories.Usage, - defaultSeverity: DiagnosticSeverity.Warning, - // Opt-in, on the evidence ADR-0059's follow-up asked for rather than on intuition: dogfooded over this - // repository's suites the rule found no true positive and two false ones, both in a convention test that - // collects generators into a List on purpose. That shape is indistinguishable from the theory-row - // mistake this rule exists to catch, so it cannot be narrowed away. The rule earns its keep in a consumer - // suite, where object-typed assertion helpers are common and reflection over generators is not. - isEnabledByDefault: false, - description: "Generators are reference types, so an object, dynamic or params object[] position accepts one with no conversion — the residue the removal of the implicit conversions could not close. An assertion helper taking object then inspects the recipe (Assert.NotNull(Any.String()) is green for ever), a theory row carries the recipe into the code under test, and Equals against a value is false for every run and every seed. Opt-in: a suite that manipulates generators as objects on purpose would see this fire on legitimate code.", - helpLinkUri: HelpLinks.For(DiagnosticIds.GeneratorWhereValueExpected)); - - public static readonly DiagnosticDescriptor GeneratorPooledAsValue = new( - id: DiagnosticIds.GeneratorPooledAsValue, - title: "A choice pool is built from generators rather than values", - messageFormat: "Call Generate() on each pooled generator: Any.{0} inferred a pool of recipes, so drawing from it yields a recipe rather than a value", - category: DiagnosticCategories.Usage, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Any.OneOf(Any.Int32(), Any.Int32()) compiles and infers the builder type as the pool's element type, so the pool holds recipes. What makes this a trap rather than an obvious mistake is that the surface is inconsistent about it: pooling generators of different types fails type inference and the compiler catches it, while two of the same type bind cleanly.", - helpLinkUri: HelpLinks.For(DiagnosticIds.GeneratorPooledAsValue)); - - public static readonly DiagnosticDescriptor HeldCollectionPassedToOneOf = new( - id: DiagnosticIds.HeldCollectionPassedToOneOf, - title: "A held collection is passed to Any.OneOf, making a pool of one", - messageFormat: "Use Any.ElementOf to draw from the collection's elements: passed to OneOf it binds T to {0}, so the pool holds one item and every draw returns the same one", - category: DiagnosticCategories.Usage, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Any.OneOf takes params T[], so a single collection argument binds T to the collection type itself rather than to its elements. The call compiles, draws succeed, and every one of them returns the same collection — the arbitrary choice the test claims to make never varies. Any.ElementOf is the entry point that draws from a collection's elements; an explicit type argument states the opposite intent and is left alone.", - helpLinkUri: HelpLinks.For(DiagnosticIds.HeldCollectionPassedToOneOf)); - - public static readonly DiagnosticDescriptor RejectedConstantArgument = new( - id: DiagnosticIds.RejectedConstantArgument, - title: "A constant argument is one the generator rejects", - messageFormat: "{0} throws for this argument: {1}", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The argument is a compile-time constant the generator's own guard refuses, so the call throws every time it runs. Nothing is decided at run time that is not already decided here, and the failure otherwise surfaces late — inside an arrange helper shared by many tests, where it reads as a library problem rather than as the transposition typo it usually is. The run-time guards stay for every argument this cannot see.", - helpLinkUri: HelpLinks.For(DiagnosticIds.RejectedConstantArgument)); - - public static readonly DiagnosticDescriptor StringConstraintsAdmitNoValue = new( - id: DiagnosticIds.StringConstraintsAdmitNoValue, - title: "The declared string constraints admit no value", - messageFormat: "No string satisfies this chain: {0}", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The constraints contradict each other for the constants written at the call site, so the chain throws a ConflictingAnyConstraintException the moment the arrange line runs. This is the case ADR-0035 names as the one an analyzer should carry: Numeric().StartingWith(\"ORD-\") conflicts while Numeric().StartingWith(\"123\") does not, from identical call sites and identical static types — only the argument value tells them apart.", - helpLinkUri: HelpLinks.For(DiagnosticIds.StringConstraintsAdmitNoValue)); - - public static readonly DiagnosticDescriptor CollectionConstraintsAdmitNoValue = new( - id: DiagnosticIds.CollectionConstraintsAdmitNoValue, - title: "The declared collection constraints admit no value", - messageFormat: "No collection satisfies this chain: {0}", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The count constraints contradict each other for the constants written at the call site, or the chain asks for more distinct elements than its element generator can produce — the cardinality gate ADR-0013 records. Both throw at declaration time, so the value here is a build-time red rather than an arrange-time one: the chain usually sits in a helper several call frames away from the test that dies on it.", - helpLinkUri: HelpLinks.For(DiagnosticIds.CollectionConstraintsAdmitNoValue)); - - public static readonly DiagnosticDescriptor EnumUniverseViolation = new( - id: DiagnosticIds.EnumUniverseViolation, - title: "An enum constraint steps outside the generator's universe", - messageFormat: "Any.Enum draws only declared members: {0}", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Any.Enum() draws uniformly across T's declared members and never an undeclared numeric value. That is deliberate and surprising: on a [Flags] enum, writing a combination in OneOf is the natural thing to do and the generator refuses it unless AllowingCombinations() is declared. An exclusion that removes every declared member is the same category error from the other side.", - helpLinkUri: HelpLinks.For(DiagnosticIds.EnumUniverseViolation)); - - public static readonly DiagnosticDescriptor NestedReproducibilityScope = new( - id: DiagnosticIds.NestedReproducibilityScope, - title: "A reproducibility scope is nested inside another", - messageFormat: "This Any.Reproducibly runs inside {0}, whose reported seed then replays nothing: the inner scope draws a fresh seed on every run", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Any.Reproducibly takes its seed from Guid.NewGuid().GetHashCode(), not from the ambient source, so an inner scope ignores whatever the outer one pinned and draws afresh every run. The outer mechanism still reports its own seed, so the failure names a seed that reproduces nothing — a wrong instruction rather than a wrong result. The seeded overload is left alone: pinning a chosen seed inside is deliberate.", - helpLinkUri: HelpLinks.For(DiagnosticIds.NestedReproducibilityScope)); - - public static readonly DiagnosticDescriptor CommittedReplaySeed = new( - id: DiagnosticIds.CommittedReplaySeed, - title: "A replay seed is pinned in committed code", - messageFormat: "Seed {0} is pinned: the values stop varying between runs, so the test no longer surfaces a dependency on one particular value", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Info, - // Opt-in, and it must be: this repository's own maintainer guide instructs the opposite for a whole class of - // tests ("Pin a seed for anything statistical"), so a rule enabled by default would fight documented practice. - isEnabledByDefault: false, - description: "The seeded overloads exist to replay a run a failure reported — correct while reproducing, wrong once committed, because the test then draws the same values for ever and stops surfacing the coupling the library exists to reveal. Opt-in: a statistical test legitimately pins a seed, and this repository's maintainer guide says so, which makes the rule a pre-release sweep rather than a standing check.", - helpLinkUri: HelpLinks.For(DiagnosticIds.CommittedReplaySeed)); - - public static readonly DiagnosticDescriptor SharedStaticAnyContext = new( - id: DiagnosticIds.SharedStaticAnyContext, - title: "An AnyContext is shared through a static field", - messageFormat: "Give each unit of work its own context: '{0}' is shared, and interleaved draws make neither the sequence nor the multiset stable across runs", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "AnyContext's own documentation states the hazard: a context is safe to draw from concurrently, but sharing one across threads costs the replay rather than the values. A static context looks maximally deterministic — a literal seed, right there in the source — while a parallel suite gets a different value per test per run from it.", - helpLinkUri: HelpLinks.For(DiagnosticIds.SharedStaticAnyContext)); - - public static readonly DiagnosticDescriptor BlankReplaySnippet = new( - id: DiagnosticIds.BlankReplaySnippet, - title: "Any.UseSeed is given a blank replay snippet", - messageFormat: "Pass the code a reader copies to replay the run, or drop the argument: a blank snippet is rejected at run time", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Any.UseSeed(int, string) rejects a blank snippet. Because that scope is normally opened from a test-framework adapter's hook, the throw surfaces as an infrastructure failure on every test in the suite rather than as one failing assertion — a disproportionately expensive way to learn about a typo the compiler can already see.", - helpLinkUri: HelpLinks.For(DiagnosticIds.BlankReplaySnippet)); - - public static readonly DiagnosticDescriptor ParallelDrawWithoutPerItemSeed = new( - id: DiagnosticIds.ParallelDrawWithoutPerItemSeed, - title: "A parallel work item draws without its own seed scope", - messageFormat: "Open an Any.UseSeed scope inside the work item: the ambient scope reaches every worker, so the draws interleave and the run replays nothing", - category: DiagnosticCategories.Reproducibility, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "The ambient seed scope flows with the execution context, so a scope opened around a parallel loop reaches every worker and their draws interleave: neither the sequence nor the multiset is stable across runs. A scope opened inside the loop body gives each unit of work its own sequence, and the whole run replays — the shape the library's documentation names.", - helpLinkUri: HelpLinks.For(DiagnosticIds.ParallelDrawWithoutPerItemSeed)); - - public static readonly DiagnosticDescriptor ScalarChainAdmitsNoValue = new( - id: DiagnosticIds.ScalarChainAdmitsNoValue, - title: "The declared scalar constraints admit no value", - messageFormat: "No value satisfies this chain once {0} is applied", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The constant constraints narrow the domain to nothing, so the chain throws a ConflictingAnyConstraintException the moment the arrange line runs. The library computes this with one emptiness test over bounds, lattice and allow-list; this rule runs the same test over the constants written at the call site, and stays silent for every argument it cannot fold.", - helpLinkUri: HelpLinks.For(DiagnosticIds.ScalarChainAdmitsNoValue)); - - public static readonly DiagnosticDescriptor ConstraintWithNoEffect = new( - id: DiagnosticIds.ConstraintWithNoEffect, - title: "A constraint narrows nothing", - messageFormat: "This constraint changes nothing: {0}", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "The constraint is legal and inert: the domain it produces is the one that already existed. This is the only member of the constraint family the run time NEVER reports — every other contradiction throws eventually and loudly, while an inert constraint leaves the test green and exercising a domain the author did not write. The dangerous case is an exclusion of a sentinel the generator could never draw: it silently misses, and starts mattering the day someone widens the range.", - helpLinkUri: HelpLinks.For(DiagnosticIds.ConstraintWithNoEffect)); - - public static readonly DiagnosticDescriptor DuplicatePoolValue = new( - id: DiagnosticIds.DuplicatePoolValue, - title: "The same value is listed twice in a pool", - messageFormat: "This value is already in the pool; a duplicate neither weights it nor widens the domain", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "A pool is deduplicated under the default equality when it is built, so a value listed twice contributes exactly once. The library declines to weight a pool on purpose — writing a value twice therefore cannot mean 'draw this more often', and the pool is one value smaller than it reads. That gap surfaces far from here, when a distinct collection over the pool gates against the real distinct count and reports a number the author cannot find in their source.", - helpLinkUri: HelpLinks.For(DiagnosticIds.DuplicatePoolValue)); - - public static readonly DiagnosticDescriptor EmptyRelativeUri = new( - id: DiagnosticIds.EmptyRelativeUri, - title: "The declared relative URI is empty", - messageFormat: "A relative URI with exactly 0 path segments and no query, fragment or root is empty, which is not a valid URI reference", - category: DiagnosticCategories.Constraints, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The chain describes the empty reference, which no URI can be. The library reports it, but only at Generate() — this is the one constraint family member whose failure lands at act time rather than at the arrange line, so the stack points at the code under test instead of at the declaration that is wrong. Add WithQuery(), WithFragment(), Rooted(), or a positive segment count.", - helpLinkUri: HelpLinks.For(DiagnosticIds.EmptyRelativeUri)); - - public static readonly DiagnosticDescriptor UnusedCombineOperand = new( - id: DiagnosticIds.UnusedCombineOperand, - title: "A Combine operand never reaches the composed value", - messageFormat: "This generator is drawn and thrown away: the composer never reads its parameter '{0}'", - category: DiagnosticCategories.Composition, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Combine draws every operand before calling the composer, so an operand the composer ignores is still generated — constraints, conflict checks and all — and then dropped. Nothing fails: the composed value is well-formed, and simply does not carry the part the call site says it carries. The usual causes are a constructor argument forgotten during a refactor and a composer whose parameters no longer line up with its operands. Rename the parameter to '_' to say the draw is deliberate.", - helpLinkUri: HelpLinks.For(DiagnosticIds.UnusedCombineOperand)); - - public static readonly DiagnosticDescriptor InertDistinctness = new( - id: DiagnosticIds.InertDistinctness, - title: "Distinctness is declared over an element type that has no value equality", - messageFormat: "Distinctness cannot bind here: '{0}' inherits reference equality, so every freshly generated element already counts as distinct", - category: DiagnosticCategories.Composition, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The element type neither overrides Equals nor implements IEquatable, so the default comparer falls back to reference equality — and every element the generator produces is a new instance. Distinctness is therefore satisfied by construction and constrains nothing: the collection can hold the same value several times, which is precisely what the declaration asks it not to. The library cannot report this, because from its side the requirement is met. Give the type value equality, or pass an explicit comparer.", - helpLinkUri: HelpLinks.For(DiagnosticIds.InertDistinctness)); - -} diff --git a/JustDummies.Analyzers/DiagnosticCategories.cs b/JustDummies.Analyzers/DiagnosticCategories.cs deleted file mode 100644 index d4e819ee..00000000 --- a/JustDummies.Analyzers/DiagnosticCategories.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace JustDummies.Analyzers; - -/// -/// Categories used to group JustDummies diagnostics in the IDE and in .editorconfig. -/// -internal static class DiagnosticCategories { - - public const string Reproducibility = "JustDummies.Reproducibility"; - - /// - /// Rules about the recipe-versus-value distinction the library teaches: a generator is an immutable recipe, - /// and Generate() is the only thing that materializes a value from it. - /// - public const string Usage = "JustDummies.Usage"; - - /// - /// Rules that front-load, to build time, the subset of the library's run-time constraint checks that is - /// decidable from compile-time constants. The run-time checks stay: they cover every argument these cannot see. - /// - public const string Constraints = "JustDummies.Constraints"; - - /// - /// Rules about assembling generators into bigger ones — Combine's operands, and the element contract a - /// collection generator relies on. What they share is that nothing goes wrong: the composed generator builds, - /// draws and returns a value. It is simply not the value the call site describes. - /// - public const string Composition = "JustDummies.Composition"; - -} diff --git a/JustDummies.Analyzers/DiagnosticIds.cs b/JustDummies.Analyzers/DiagnosticIds.cs deleted file mode 100644 index eee40c94..00000000 --- a/JustDummies.Analyzers/DiagnosticIds.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace JustDummies.Analyzers; - -/// -/// Stable identifiers for every JustDummies diagnostic. JD is the JustDummies prefix, mirroring -/// FCE for FirstClassErrors; the number is only a stable handle. -/// -internal static class DiagnosticIds { - - // Category: Reproducibility - public const string AsyncBodyPassedToReproducibly = "JD001"; - public const string DiscardedReproduciblyAsyncResult = "JD002"; - public const string AwaitableBodyPassedToReproducibly = "JD003"; - public const string DiscardedSeedingResult = "JD004"; - - // Category: Usage - public const string GeneratorRenderedAsText = "JD005"; - public const string DiscardedGeneratorResult = "JD006"; - - // Category: Reproducibility — draws that escape the pinned seed scope - public const string DrawOutsideThePinnedScope = "JD007"; - public const string ArbitraryValueInTheoryData = "JD008"; - public const string DrawInStaticInitializer = "JD009"; - public const string ReproducibleOnNonTestMethod = "JD010"; - - // Category: Usage — a recipe reaching a position that wanted the value - public const string GeneratorWhereValueExpected = "JD011"; - public const string GeneratorPooledAsValue = "JD012"; - public const string HeldCollectionPassedToOneOf = "JD013"; - - // Category: Constraints — decidable from compile-time constants - public const string RejectedConstantArgument = "JD014"; - public const string StringConstraintsAdmitNoValue = "JD015"; - public const string CollectionConstraintsAdmitNoValue = "JD016"; - public const string EnumUniverseViolation = "JD017"; - - // Category: Reproducibility — the seeding long tail - public const string NestedReproducibilityScope = "JD018"; - public const string CommittedReplaySeed = "JD019"; - public const string SharedStaticAnyContext = "JD020"; - public const string BlankReplaySnippet = "JD021"; - public const string ParallelDrawWithoutPerItemSeed = "JD022"; - - public const string ScalarChainAdmitsNoValue = "JD023"; - public const string ConstraintWithNoEffect = "JD024"; - - public const string DuplicatePoolValue = "JD025"; - public const string EmptyRelativeUri = "JD026"; - - // Category: Composition — a part that reaches no result, a constraint that cannot bind - public const string UnusedCombineOperand = "JD027"; - public const string InertDistinctness = "JD028"; - -} diff --git a/JustDummies.Analyzers/DiscardedGeneratorResultAnalyzer.cs b/JustDummies.Analyzers/DiscardedGeneratorResultAnalyzer.cs deleted file mode 100644 index 472ff01c..00000000 --- a/JustDummies.Analyzers/DiscardedGeneratorResultAnalyzer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD006 — reports a generator-returning call whose result is thrown away. A generator is an immutable recipe: -/// every constraint returns a new generator rather than mutating the receiver, so numbers.NonEmpty(); looks -/// like it constrains numbers and silently constrains nothing. The declared invariant is lost, the test keeps -/// drawing from the wider domain, and it fails only on the run that happens to draw outside it. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DiscardedGeneratorResultAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DiscardedGeneratorResult); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.IAny is null) { return; } - - INamedTypeSymbol iAny = symbols.IAny; - - context.RegisterOperationAction(operationContext => Analyze(operationContext, iAny), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!GeneratorFacts.IsGenerator(invocation.TargetMethod.ReturnType, iAny)) { return; } - if (!IsResultDiscarded(invocation)) { return; } - - // A test asserting that the constraint throws writes the illegal call as the whole body of a lambda argument; - // reporting there would fight the suite that documents the conflict. - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DiscardedGeneratorResult, invocation.Syntax.GetLocation(), invocation.TargetMethod.Name)); - } - - // Only the bare statement, deliberately — not `_ = generator.NonEmpty();`. What makes this rule worth an entry is - // that the mistake is *silent*: the call reads as if it mutated the receiver. An explicit discard cannot be - // misread that way, and it is how a test that only wants the construction to throw spells its intent (see - // JustDummies.PropertyTests/PatternRoundTripProperties.cs). JD002 and JD004 report `_ =` because discarding is - // never right there; here it is a legitimate, self-documenting choice. - private static bool IsResultDiscarded(IInvocationOperation invocation) { - return invocation.Parent is IExpressionStatementOperation; - } - -} diff --git a/JustDummies.Analyzers/DiscardedReproduciblyAsyncResultAnalyzer.cs b/JustDummies.Analyzers/DiscardedReproduciblyAsyncResultAnalyzer.cs deleted file mode 100644 index 4096e79f..00000000 --- a/JustDummies.Analyzers/DiscardedReproduciblyAsyncResultAnalyzer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD002 — reports a call to Any.ReproduciblyAsync(...) whose returned -/// is discarded (the call stands alone as a statement, or is assigned to _). The task faults with the body's -/// exception; discarding it lets a failing test pass green. Await it. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DiscardedReproduciblyAsyncResultAnalyzer : DiagnosticAnalyzer { - - private const string AnyMetadataName = "JustDummies.Any"; - private const string ReproduciblyAsyncMethodName = "ReproduciblyAsync"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DiscardedReproduciblyAsyncResult); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - INamedTypeSymbol? anyType = context.Compilation.GetTypeByMetadataName(AnyMetadataName); - if (anyType is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, anyType), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol anyType) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != ReproduciblyAsyncMethodName || !SymbolEqualityComparer.Default.Equals(method.ContainingType, anyType)) { return; } - if (!IsResultDiscarded(invocation)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DiscardedReproduciblyAsyncResult, invocation.Syntax.GetLocation())); - } - - // The result is thrown away either when the call stands alone as a statement or when it is explicitly discarded - // (`_ = Any.ReproduciblyAsync(...);`). Either way the body's failures are lost. - private static bool IsResultDiscarded(IInvocationOperation invocation) { - return invocation.Parent switch { - IExpressionStatementOperation => true, - ISimpleAssignmentOperation { Target: IDiscardOperation } => true, - _ => false, - }; - } - -} diff --git a/JustDummies.Analyzers/DiscardedSeedingResultAnalyzer.cs b/JustDummies.Analyzers/DiscardedSeedingResultAnalyzer.cs deleted file mode 100644 index 7fce4e08..00000000 --- a/JustDummies.Analyzers/DiscardedSeedingResultAnalyzer.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD004 — reports a seeding call whose result is thrown away. Any.UseSeed(...) returns the handle that -/// closes the scope: dropping it is the leak the library's own documentation warns about, and it leaves the seed -/// pinned for whatever runs next in the same execution context. Any.WithSeed(...) returns an isolated -/// context and pins nothing at all, so a discarded call is dead code that reads as if it had seeded the run. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DiscardedSeedingResultAnalyzer : DiagnosticAnalyzer { - - private const string UseSeedMethodName = "UseSeed"; - private const string WithSeedMethodName = "WithSeed"; - - private const string UseSeedConsequence = "the scope is never closed, so the seed stays pinned for whatever runs next in the same execution context; hold the handle in a using declaration"; - private const string WithSeedConsequence = "Any.WithSeed returns an isolated context and pins nothing — the ambient Any.* entry points keep drawing unseeded; capture the context and draw from it, or use Any.UseSeed to pin the ambient source"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DiscardedSeedingResult); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols.Any), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol anyType) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, anyType)) { return; } - - string consequence = method.Name switch { - UseSeedMethodName => UseSeedConsequence, - WithSeedMethodName => WithSeedConsequence, - _ => string.Empty, - }; - - if (consequence.Length == 0) { return; } - if (!IsResultDiscarded(invocation)) { return; } - - // A test asserting that the seeding call rejects its argument never opens a scope, so there is nothing to - // leak; the call being the whole body of a lambda argument is that shape. - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DiscardedSeedingResult, invocation.Syntax.GetLocation(), method.Name, consequence)); - } - - // The same two shapes JD002 reports for ReproduciblyAsync: the call stands alone as a statement, or its result is - // explicitly discarded. - private static bool IsResultDiscarded(IInvocationOperation invocation) { - return invocation.Parent switch { - IExpressionStatementOperation => true, - ISimpleAssignmentOperation { Target: IDiscardOperation } => true, - _ => false, - }; - } - -} diff --git a/JustDummies.Analyzers/DrawInStaticInitializerAnalyzer.cs b/JustDummies.Analyzers/DrawInStaticInitializerAnalyzer.cs deleted file mode 100644 index 79c14d1d..00000000 --- a/JustDummies.Analyzers/DrawInStaticInitializerAnalyzer.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD009 — reports a value drawn in a static field initializer or a static constructor. The type initializer runs -/// once, lazily, on whichever test first touches the type: one value is shared by every test in the class, drawn -/// under whatever seed that first test happened to pin, and replayable from none of them. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DrawInStaticInitializerAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DrawInStaticInitializer); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!GeneratorFacts.IsGenerateCall(invocation, symbols.IAny!)) { return; } - if (!GeneratorFacts.RootsAtAmbientAny(invocation, symbols.Any!)) { return; } - if (!IsStaticInitialization(context.ContainingSymbol)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DrawInStaticInitializer, invocation.Syntax.GetLocation())); - } - - private static bool IsStaticInitialization(ISymbol symbol) { - return symbol switch { - IFieldSymbol { IsStatic: true } => true, - IPropertySymbol { IsStatic: true } => true, - IMethodSymbol { IsStatic: true, MethodKind: MethodKind.StaticConstructor } => true, - _ => false, - }; - } - -} diff --git a/JustDummies.Analyzers/DrawOutsideThePinnedScopeAnalyzer.cs b/JustDummies.Analyzers/DrawOutsideThePinnedScopeAnalyzer.cs deleted file mode 100644 index 27a8c6e6..00000000 --- a/JustDummies.Analyzers/DrawOutsideThePinnedScopeAnalyzer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD007 — reports a value drawn during a [Reproducible] test class's construction, which xUnit runs -/// before the adapter opens the seed scope. The draw comes from the unseeded ambient source, so the seed the -/// failure reports replays the body and not the arrangement: the reader pins it, the run still differs, and the -/// test looks unreplayable while advertising that it is not. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DrawOutsideThePinnedScopeAnalyzer : DiagnosticAnalyzer { - - private const string InitializeAsyncMethodName = "InitializeAsync"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DrawOutsideThePinnedScope); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null || symbols.ReproducibleAttribute is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!GeneratorFacts.IsGenerateCall(invocation, symbols.IAny!)) { return; } - if (!GeneratorFacts.RootsAtAmbientAny(invocation, symbols.Any!)) { return; } - - ISymbol containing = context.ContainingSymbol; - if (!RunsBeforeTheScopeOpens(containing, out string? phase)) { return; } - if (!XunitFacts.IsCoveredByReproducible(containing, symbols.ReproducibleAttribute!)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DrawOutsideThePinnedScope, invocation.Syntax.GetLocation(), phase)); - } - - // xUnit constructs the test-class instance, and awaits IAsyncLifetime.InitializeAsync, before it runs the - // BeforeAfterTestAttribute hooks the adapter pins the seed from. Everything drawn there is outside the scope. - private static bool RunsBeforeTheScopeOpens(ISymbol symbol, out string? phase) { - phase = null; - - if (symbol is IFieldSymbol { IsStatic: false } or IPropertySymbol { IsStatic: false }) { - phase = "a field initializer"; - - return true; - } - - if (symbol is not IMethodSymbol method || method.IsStatic) { return false; } - - if (method.MethodKind == MethodKind.Constructor) { - phase = "the test class constructor"; - - return true; - } - - if (method.Name == InitializeAsyncMethodName) { - phase = "InitializeAsync"; - - return true; - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/DuplicatePoolValueAnalyzer.cs b/JustDummies.Analyzers/DuplicatePoolValueAnalyzer.cs deleted file mode 100644 index abe6d03e..00000000 --- a/JustDummies.Analyzers/DuplicatePoolValueAnalyzer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD025 — reports a constant listed twice in the same pool. Any.OneOf(a, b, a) is deduplicated when the -/// generator is built, so the pool is one value smaller than it reads, and nothing anywhere says so. -/// -/// -/// Weighting is the reading this rule exists to refuse: listing a value twice looks like "draw this one more -/// often", and the library declines to weight a pool on purpose. The consequence surfaces somewhere else -/// entirely — a distinct collection over the pool gates against the real distinct count and names a number the -/// author cannot find in their source. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DuplicatePoolValueAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.DuplicatePoolValue); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.TargetMethod.Name is not ("OneOf" or "ElementOf")) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out _, out IInvocationOperation? factory) || factory is null) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - HashSet seen = []; - - foreach (IOperation element in PoolElements(invocation)) { - // The element's own constant, not the unwrapped operand's: the conversion to the pool's element type is - // what decides whether two literals written differently are the same pooled value. - Optional constant = element.ConstantValue; - - // One unfoldable element and the pool stops being knowable: a later duplicate of THAT value would go - // unseen, and reporting the ones this walk can see would claim a completeness the walk does not have. - if (!constant.HasValue) { return; } - if (seen.Add(constant.Value)) { continue; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.DuplicatePoolValue, element.Syntax.GetLocation())); - - return; - } - } - - private static IEnumerable PoolElements(IInvocationOperation invocation) { - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.ArgumentKind == ArgumentKind.ParamArray) { - if (argument.Value is IArrayCreationOperation { Initializer: { } initializer }) { - foreach (IOperation element in initializer.ElementValues) { yield return element; } - } - - continue; - } - - // ElementOf takes a materialized collection; only an inline collection expression or array creation is - // knowable here — anything held in a variable is not this rule's business. - if (GeneratorFacts.Unwrap(argument.Value) is IArrayCreationOperation { Initializer: { } inline }) { - foreach (IOperation element in inline.ElementValues) { yield return element; } - } - } - } - -} diff --git a/JustDummies.Analyzers/EmptyRelativeUriAnalyzer.cs b/JustDummies.Analyzers/EmptyRelativeUriAnalyzer.cs deleted file mode 100644 index 6b1be037..00000000 --- a/JustDummies.Analyzers/EmptyRelativeUriAnalyzer.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD026 — reports a relative-URI chain that describes the empty reference: -/// Any.Uri().Relative().WithPathSegments(0) with no query, no fragment and no root. -/// -/// -/// The whole point is when the library reports it. Every other unsatisfiable chain throws at the arrange -/// line; this one cannot, because emptiness is only settled once the components have been drawn — so it throws -/// inside Generate(), at act time, with a stack pointing at the code under test rather than at the -/// declaration that is wrong. Moving it to build time is the entire value of the rule. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class EmptyRelativeUriAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.EmptyRelativeUri); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null || factory.TargetMethod.Name != "Uri") { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - // Only the relative family can render empty. Web, WebSocket and FTP carry an authority, so a path of zero - // segments still renders as "/" and the reference stays valid. - bool relative = false; - IInvocationOperation? zeroSegments = null; - - foreach (IInvocationOperation constraint in constraints) { - switch (constraint.TargetMethod.Name) { - case "Relative": relative = true; break; - - // Any one of these three saves the reference from being empty. - case "WithQuery" or "WithFragment" or "Rooted": return; - - case "WithPathSegments" when constraint.Arguments.Length == 1 && ConstantFacts.TryGetInt32(constraint.Arguments[0].Value, out int count): - if (count != 0) { return; } - - zeroSegments = constraint; - - break; - } - } - - if (!relative || zeroSegments is null) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.EmptyRelativeUri, zeroSegments.Syntax.GetLocation())); - } - -} diff --git a/JustDummies.Analyzers/EnumUniverseViolationAnalyzer.cs b/JustDummies.Analyzers/EnumUniverseViolationAnalyzer.cs deleted file mode 100644 index 5711d3c4..00000000 --- a/JustDummies.Analyzers/EnumUniverseViolationAnalyzer.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD017 — reports an enum constraint that steps outside the generator's universe. Any.Enum<T>() draws -/// only declared members, which is deliberate and surprising: on a [Flags] enum, writing a -/// combination in OneOf is the natural thing to do and the generator refuses it unless -/// AllowingCombinations() is declared. -/// -/// -/// Kept apart from the interval rules because the domain is metadata — the declared members — rather than -/// arithmetic, and because the mistake has its own teachable model: the generator yields declared members, so a -/// value that is not one is not a narrowing but a category error. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class EnumUniverseViolationAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.EnumUniverseViolation); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null || factory.TargetMethod.Name != "Enum") { return; } - if (factory.TargetMethod.TypeArguments.Length != 1 || factory.TargetMethod.TypeArguments[0] is not INamedTypeSymbol enumType) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - HashSet declared = [.. enumType.GetMembers() - .OfType() - .Where(field => field.HasConstantValue) - .Select(field => field.ConstantValue)]; - - if (declared.Count == 0) { return; } - - bool combinationsAllowed = constraints.Any(constraint => constraint.TargetMethod.Name == "AllowingCombinations"); - - HashSet excluded = []; - - foreach ((string name, IOperation value, object? constant) in ConstrainedValues(constraints)) { - if (name is "Except" or "DifferentFrom") { excluded.Add(constant); } - - // AllowingCombinations widens the universe to the OR-closure of the declared members, which no longer - // matches a declared value one for one — so the rule stands down rather than approximate it. - if (combinationsAllowed || declared.Contains(constant)) { continue; } - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.EnumUniverseViolation, value.Syntax.GetLocation(), - $"{constant} is not a declared member of {enumType.Name}" - + (enumType.GetAttributes().Any(IsFlagsAttribute) ? "; declare AllowingCombinations() to draw flag combinations" : string.Empty))); - - return; - } - - if (excluded.Count == 0 || !declared.All(excluded.Contains)) { return; } - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.EnumUniverseViolation, invocation.Syntax.GetLocation(), - $"no declared {enumType.Name} member remains once every exclusion is applied")); - } - - /// - /// The constant values the universe rules reason about, each paired with the constraint that declared it — - /// OneOf, Except and DifferentFrom, in the order they were written, and only where the - /// argument is a constant the rule can compare against a declared member. - /// - /// - /// Flattened here rather than inline so the rule states what it does with a value without also spelling - /// out how to reach one: which constraints carry values, how a params array unfolds, and that a - /// non-constant argument is skipped are all one concern, and it is not the universe check. - /// - private static IEnumerable<(string Name, IOperation Value, object? Constant)> ConstrainedValues(IReadOnlyList constraints) { - foreach (IInvocationOperation constraint in constraints) { - string name = constraint.TargetMethod.Name; - if (name is not ("OneOf" or "Except" or "DifferentFrom")) { continue; } - - foreach (IOperation value in ConstantArguments(constraint)) { - Optional constant = value.ConstantValue; - if (constant.HasValue) { yield return (name, value, constant.Value); } - } - } - } - - private static IEnumerable ConstantArguments(IInvocationOperation constraint) { - foreach (IArgumentOperation argument in constraint.Arguments) { - if (argument.ArgumentKind == ArgumentKind.ParamArray) { - if (argument.Value is IArrayCreationOperation { Initializer: { } initializer }) { - foreach (IOperation element in initializer.ElementValues) { yield return GeneratorFacts.Unwrap(element); } - } - - continue; - } - - yield return GeneratorFacts.Unwrap(argument.Value); - } - } - - private static bool IsFlagsAttribute(AttributeData attribute) { - return attribute.AttributeClass?.Name == "FlagsAttribute"; - } - -} diff --git a/JustDummies.Analyzers/EqualityFacts.cs b/JustDummies.Analyzers/EqualityFacts.cs deleted file mode 100644 index 67e444a7..00000000 --- a/JustDummies.Analyzers/EqualityFacts.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace JustDummies.Analyzers; - -/// -/// Whether a type carries value equality, decided the way EqualityComparer<T>.Default decides it: an -/// IEquatable<T> implementation, or an Equals(object) override somewhere below -/// object. A type with neither falls back to reference equality. -/// -/// -/// The answer is only ever used to claim that equality cannot distinguish two values, which is a claim that -/// has to be certain — so every uncertainty resolves to "it has value equality" and the caller stands down. The -/// sealed requirement is the largest of those: an open class says nothing about the instance a generator actually -/// produces, since a derived type is free to add the equality the base lacks. -/// -internal static class EqualityFacts { - - /// - /// Whether provably compares by reference under the default comparer. - /// - public static bool UsesReferenceEquality(ITypeSymbol? type) { - // A value type never compares by reference; a type parameter, an interface, an array or a delegate is either - // substitutable or already carries its own equality. Only a sealed class settles the question here. - if (type is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsSealed: true, IsRecord: false } named) { return false; } - if (named.SpecialType == SpecialType.System_Object) { return false; } - if (ImplementsIEquatable(named)) { return false; } - - for (INamedTypeSymbol? current = named; current is not null && current.SpecialType != SpecialType.System_Object; current = current.BaseType) { - if (OverridesEquals(current)) { return false; } - } - - return true; - } - - private static bool ImplementsIEquatable(INamedTypeSymbol type) { - return type.AllInterfaces.Any(implemented => implemented is { IsGenericType: true, Name: "IEquatable" } - && implemented.ContainingNamespace is { Name: "System", ContainingNamespace.IsGlobalNamespace: true }); - } - - private static bool OverridesEquals(INamedTypeSymbol type) { - return type.GetMembers("Equals") - .Any(member => member is IMethodSymbol { IsOverride: true, Parameters.Length: 1, ReturnType.SpecialType: SpecialType.System_Boolean }); - } - -} diff --git a/JustDummies.Analyzers/GeneratorFacts.cs b/JustDummies.Analyzers/GeneratorFacts.cs deleted file mode 100644 index eee36362..00000000 --- a/JustDummies.Analyzers/GeneratorFacts.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// Facts about the generator surface: recognising a value that is an IAny<T> recipe rather than the -/// value that recipe would draw. Every rule in the JustDummies.Usage category rests on this distinction. -/// -internal static class GeneratorFacts { - - private const string GenerateMethodName = "Generate"; - - /// - /// Whether is a JustDummies generator — the IAny<T> interface itself, or - /// any type implementing it. Matching the interface rather than a list of concrete builders keeps the rules - /// correct for As(...) and Combine(...) derivations, and for a consumer's own generator. - /// - public static bool IsGenerator(ITypeSymbol? type, INamedTypeSymbol iAnyType) { - if (type is null) { return false; } - - if (type is INamedTypeSymbol named && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, iAnyType)) { return true; } - - return type.AllInterfaces.Any(implemented => SymbolEqualityComparer.Default.Equals(implemented.OriginalDefinition, iAnyType)); - } - - /// - /// Whether is a materializing Generate() call — the single member of - /// IAny<T>, and the only thing that turns a recipe into a value. - /// - public static bool IsGenerateCall(IInvocationOperation invocation, INamedTypeSymbol iAnyType) { - IMethodSymbol method = invocation.TargetMethod; - - return method.Name == GenerateMethodName - && method.Parameters.IsEmpty - && IsGenerator(method.ContainingType, iAnyType); - } - - /// - /// Whether the chain the Generate() call sits on provably starts at a static JustDummies.Any - /// factory — that is, whether the value is drawn from the ambient random source that a seed scope pins. - /// - /// - /// Deliberately conservative: it answers "yes" only for a chain written inline from Any. A generator - /// reached through a local, a field or a parameter answers "no" and is not reported, which under-reports rather - /// than misfiring on a draw from an isolated AnyContext — that context is unaffected by the ambient - /// scope, so reporting it would be plainly wrong. - /// - public static bool RootsAtAmbientAny(IInvocationOperation invocation, INamedTypeSymbol anyType) { - for (IOperation? current = invocation; current is IInvocationOperation call;) { - if (call.Instance is null) { - // A static call: ambient only when it is one of Any's own factories. - return SymbolEqualityComparer.Default.Equals(call.TargetMethod.ContainingType, anyType); - } - - current = Unwrap(call.Instance); - } - - return false; - } - - /// - /// Strips the implicit conversions Roslyn inserts around a generator when it flows into an object or - /// string position, so the rule sees the recipe rather than the conversion wrapping it. - /// - public static IOperation Unwrap(IOperation operation) { - IOperation current = operation; - while (current is IConversionOperation { IsImplicit: true } conversion) { - current = conversion.Operand; - } - - return current; - } - -} diff --git a/JustDummies.Analyzers/GeneratorPooledAsValueAnalyzer.cs b/JustDummies.Analyzers/GeneratorPooledAsValueAnalyzer.cs deleted file mode 100644 index 94658b57..00000000 --- a/JustDummies.Analyzers/GeneratorPooledAsValueAnalyzer.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD012 — reports a choice pool built from generators rather than values. Any.OneOf(Any.Int32(), Any.Int32()) -/// compiles and infers T = AnyInt32, so the pool holds recipes and drawing from it yields a recipe rather -/// than a number. The surface is inconsistent about it, which is what makes it a trap: pooled generators of -/// different types fail inference and are caught by the compiler, while two of the same type sail through. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class GeneratorPooledAsValueAnalyzer : DiagnosticAnalyzer { - - private const string OneOfMethodName = "OneOf"; - private const string ElementOfMethodName = "ElementOf"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.GeneratorPooledAsValue); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null || symbols.AnyContext is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name is not (OneOfMethodName or ElementOfMethodName)) { return; } - if (!IsChoiceFactory(method, symbols)) { return; } - if (method.TypeArguments.Length != 1) { return; } - - if (!GeneratorFacts.IsGenerator(method.TypeArguments[0], symbols.IAny!)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.GeneratorPooledAsValue, invocation.Syntax.GetLocation(), method.Name)); - } - - // Both entry points are mirrored on Any and AnyContext (SurfaceParityTests enforces the mirror), so the rule must - // recognise either receiver or it would fire on half the surface. - private static bool IsChoiceFactory(IMethodSymbol method, KnownSymbols symbols) { - return SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.Any) - || SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.AnyContext); - } - -} diff --git a/JustDummies.Analyzers/GeneratorRenderedAsTextAnalyzer.cs b/JustDummies.Analyzers/GeneratorRenderedAsTextAnalyzer.cs deleted file mode 100644 index 6084880e..00000000 --- a/JustDummies.Analyzers/GeneratorRenderedAsTextAnalyzer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD005 — reports a generator rendered as text instead of the value it would draw. No JustDummies generator -/// overrides , so an interpolation hole, a string concatenation or an explicit -/// ToString() over a recipe yields the builder's type name — the literal text "JustDummies.AnyString" -/// — which is non-empty, plausible, constant on every run, and flows into the assertion as if it were a value. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class GeneratorRenderedAsTextAnalyzer : DiagnosticAnalyzer { - - private const string ToStringMethodName = "ToString"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.GeneratorRenderedAsText); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.IAny is null) { return; } - - INamedTypeSymbol iAny = symbols.IAny; - - context.RegisterOperationAction(operationContext => AnalyzeInterpolation(operationContext, iAny), OperationKind.Interpolation); - context.RegisterOperationAction(operationContext => AnalyzeConcatenation(operationContext, iAny), OperationKind.Binary); - context.RegisterOperationAction(operationContext => AnalyzeToString(operationContext, iAny), OperationKind.Invocation); - } - - private static void AnalyzeInterpolation(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IInterpolationOperation interpolation = (IInterpolationOperation)context.Operation; - IOperation expression = GeneratorFacts.Unwrap(interpolation.Expression); - - Report(context, expression, iAny); - } - - private static void AnalyzeConcatenation(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IBinaryOperation binary = (IBinaryOperation)context.Operation; - - if (binary.OperatorKind != BinaryOperatorKind.Add) { return; } - if (binary.Type?.SpecialType != SpecialType.System_String) { return; } - - Report(context, GeneratorFacts.Unwrap(binary.LeftOperand), iAny); - Report(context, GeneratorFacts.Unwrap(binary.RightOperand), iAny); - } - - private static void AnalyzeToString(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != ToStringMethodName || !method.Parameters.IsEmpty) { return; } - - // Only the inherited object.ToString() is a defect. A consumer's own generator that meaningfully overrides - // ToString() resolves to its own override, and is deliberately left alone. - if (method.ContainingType?.SpecialType != SpecialType.System_Object) { return; } - if (invocation.Instance is null) { return; } - - Report(context, GeneratorFacts.Unwrap(invocation.Instance), iAny); - } - - private static void Report(OperationAnalysisContext context, IOperation expression, INamedTypeSymbol iAny) { - if (!GeneratorFacts.IsGenerator(expression.Type, iAny)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.GeneratorRenderedAsText, expression.Syntax.GetLocation(), expression.Type!.Name)); - } - -} diff --git a/JustDummies.Analyzers/GeneratorWhereValueExpectedAnalyzer.cs b/JustDummies.Analyzers/GeneratorWhereValueExpectedAnalyzer.cs deleted file mode 100644 index b485a37b..00000000 --- a/JustDummies.Analyzers/GeneratorWhereValueExpectedAnalyzer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD011 — reports a generator reaching a position that accepts object. Generators are reference types, so -/// no conversion stands in the way and none was removed with the implicit ones: the recipe is stored, passed or -/// compared where the drawn value was meant. An assertion helper taking object then checks the recipe — -/// Assert.NotNull(Any.String()) is green for ever and asserts nothing — and a theory row built as -/// object[] feeds the generator itself to the code under test. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class GeneratorWhereValueExpectedAnalyzer : DiagnosticAnalyzer { - - private const string EqualsMethodName = "Equals"; - private const string ReferenceEqualsMethodName = "ReferenceEquals"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.GeneratorWhereValueExpected); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.IAny is null) { return; } - - INamedTypeSymbol iAny = symbols.IAny; - - context.RegisterOperationAction(operationContext => AnalyzeConversion(operationContext, iAny), OperationKind.Conversion); - context.RegisterOperationAction(operationContext => AnalyzeEquals(operationContext, iAny), OperationKind.Invocation); - } - - private static void AnalyzeConversion(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IConversionOperation conversion = (IConversionOperation)context.Operation; - - if (!conversion.IsImplicit) { return; } - if (!IsObjectLike(conversion.Type)) { return; } - - IOperation operand = GeneratorFacts.Unwrap(conversion.Operand); - if (!GeneratorFacts.IsGenerator(operand.Type, iAny)) { return; } - - // A test asserting that the chain throws writes it as the whole body of a lambda argument, which binds to - // Func rather than Action and so produces a real generator-to-object conversion. Reporting it would - // fight every throws-assertion in the suite. - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(operand.Syntax)) { return; } - - // Comparing two recipes is a deliberate operation — it is how an immutability test proves a constraint - // returned a new generator rather than mutating the receiver. Generate() there would destroy the very - // property under test, so the identity comparisons belong to the Equals branch below, which reports only - // the mixed comparison. - if (IsOperandOfAGeneratorComparison(conversion, iAny)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.GeneratorWhereValueExpected, operand.Syntax.GetLocation(), operand.Type!.Name)); - } - - // gen.Equals(value) resolves to object.Equals — reference equality against an unrelated object, false for ever. - private static void AnalyzeEquals(OperationAnalysisContext context, INamedTypeSymbol iAny) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != EqualsMethodName) { return; } - if (method.ContainingType?.SpecialType != SpecialType.System_Object) { return; } - - IOperation? receiver = invocation.Instance is null ? null : GeneratorFacts.Unwrap(invocation.Instance); - if (receiver is null || invocation.Arguments.Length != 1) { return; } - - IOperation argument = GeneratorFacts.Unwrap(invocation.Arguments[0].Value); - - bool receiverIsGenerator = GeneratorFacts.IsGenerator(receiver.Type, iAny); - bool argumentIsGenerator = GeneratorFacts.IsGenerator(argument.Type, iAny); - - // Comparing two generators is a deliberate identity check — this repository's own immutability tests do it. - // Only the mixed comparison is the mistake, and only the generator side needs the Generate(). - if (receiverIsGenerator == argumentIsGenerator) { return; } - - IOperation offending = receiverIsGenerator ? receiver : argument; - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.GeneratorWhereValueExpected, offending.Syntax.GetLocation(), offending.Type!.Name)); - } - - // True when the conversion feeds object.Equals or object.ReferenceEquals and some other operand of that same call - // is itself a generator — that is, when the call compares two recipes rather than a recipe against a value. - private static bool IsOperandOfAGeneratorComparison(IConversionOperation conversion, INamedTypeSymbol iAny) { - if (conversion.Parent is not IArgumentOperation { Parent: IInvocationOperation call }) { return false; } - if (call.TargetMethod.Name is not (EqualsMethodName or ReferenceEqualsMethodName)) { return false; } - if (call.TargetMethod.ContainingType?.SpecialType != SpecialType.System_Object) { return false; } - - if (call.Instance is not null && GeneratorFacts.IsGenerator(GeneratorFacts.Unwrap(call.Instance).Type, iAny)) { return true; } - - foreach (IArgumentOperation argument in call.Arguments) { - if (ReferenceEquals(argument, conversion.Parent)) { continue; } - if (GeneratorFacts.IsGenerator(GeneratorFacts.Unwrap(argument.Value).Type, iAny)) { return true; } - } - - return false; - } - - private static bool IsObjectLike(ITypeSymbol? type) { - return type is not null && (type.SpecialType == SpecialType.System_Object || type.TypeKind == TypeKind.Dynamic); - } - -} diff --git a/JustDummies.Analyzers/HeldCollectionPassedToOneOfAnalyzer.cs b/JustDummies.Analyzers/HeldCollectionPassedToOneOfAnalyzer.cs deleted file mode 100644 index e7490d79..00000000 --- a/JustDummies.Analyzers/HeldCollectionPassedToOneOfAnalyzer.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD013 — reports a held collection handed to Any.OneOf. The parameter is params T[], so a single -/// List<Order> argument binds T = List<Order> and yields a pool of exactly one: -/// every draw returns the same list, and the "arbitrary order" the test claims to exercise never varies. -/// Any.ElementOf is the entry point that takes a collection and draws from its elements. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class HeldCollectionPassedToOneOfAnalyzer : DiagnosticAnalyzer { - - private const string OneOfMethodName = "OneOf"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.HeldCollectionPassedToOneOf); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.AnyContext is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocation.TargetMethod; - - if (method.Name != OneOfMethodName) { return; } - if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.Any) - && !SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.AnyContext)) { return; } - - // An explicit type argument states the intent — Any.OneOf>(orders) really does want a pool of one. - if (!IsTypeArgumentInferred(invocation)) { return; } - if (method.TypeArguments.Length != 1) { return; } - - if (!TryGetSingleExpandedArgument(invocation, out IOperation? single)) { return; } - if (!IsHeldCollection(single!.Type, method.TypeArguments[0])) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.HeldCollectionPassedToOneOf, invocation.Syntax.GetLocation(), method.TypeArguments[0].ToDisplayString())); - } - - private static bool IsTypeArgumentInferred(IInvocationOperation invocation) { - return invocation.Syntax is Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax { Expression: Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax { Name: not Microsoft.CodeAnalysis.CSharp.Syntax.GenericNameSyntax } }; - } - - // The params array was built by the compiler from exactly one argument — the shape that silently makes a pool of one. - private static bool TryGetSingleExpandedArgument(IInvocationOperation invocation, out IOperation? single) { - single = null; - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.ArgumentKind != ArgumentKind.ParamArray) { continue; } - if (argument.Value is not IArrayCreationOperation { Initializer: { } initializer }) { return false; } - if (initializer.ElementValues.Length != 1) { return false; } - - single = GeneratorFacts.Unwrap(initializer.ElementValues[0]); - - return true; - } - - return false; - } - - // A string is IEnumerable, so a single-string pool would otherwise be reported — and it is perfectly normal. - private static bool IsHeldCollection(ITypeSymbol? argumentType, ITypeSymbol inferred) { - if (argumentType is null) { return false; } - if (argumentType.SpecialType == SpecialType.System_String) { return false; } - if (inferred.TypeKind == TypeKind.TypeParameter) { return false; } - - foreach (INamedTypeSymbol implemented in argumentType.AllInterfaces) { - if (implemented.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) { return true; } - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/HelpLinks.cs b/JustDummies.Analyzers/HelpLinks.cs deleted file mode 100644 index f73a9f62..00000000 --- a/JustDummies.Analyzers/HelpLinks.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace JustDummies.Analyzers; - -/// -/// Builds the documentation URL surfaced by each diagnostic (the "help link" in the IDE). Per-rule pages live -/// under doc/handwritten/for-users/analyzers/. -/// -internal static class HelpLinks { - - private const string Base = "https://github.com/Reefact/first-class-errors/blob/main/doc/handwritten/for-users/analyzers"; - - public static string For(string diagnosticId) { - return $"{Base}/{diagnosticId}.en.md"; - } - -} diff --git a/JustDummies.Analyzers/InertDistinctnessAnalyzer.cs b/JustDummies.Analyzers/InertDistinctnessAnalyzer.cs deleted file mode 100644 index c286a75f..00000000 --- a/JustDummies.Analyzers/InertDistinctnessAnalyzer.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD028 — reports distinctness declared over an element type that has no value equality. The default comparer -/// falls back to reference equality, every generated element is a new instance, and the requirement is therefore -/// satisfied by construction: the collection can hold the same value several times, which is exactly what the -/// declaration asks it not to. -/// -/// -/// The library cannot report this, and that is the point: from its side the requirement is met, the draws are -/// pairwise unequal, and there is nothing to complain about. Only the element type's equality tells the two apart, -/// and it is visible here. Measured on the built library: six "distinct" elements over a two-value domain came back -/// as [1, 1, 1, 2, 1, 2], green. Give the type value equality — a record, an IEquatable -/// implementation, an Equals override — or pass an explicit comparer. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class InertDistinctnessAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.InertDistinctness); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - string factoryName = factory.TargetMethod.Name; - if (factoryName is not ("ListOf" or "ArrayOf" or "SequenceOf" or "SetOf" or "DictionaryOf")) { return; } - - IInvocationOperation? declaration = DistinctnessDeclaration(factory, factoryName, constraints); - if (declaration is null) { return; } - if (factory.TargetMethod.TypeArguments.Length == 0 || factory.Arguments.Length == 0) { return; } - - // A dictionary is distinct on its KEYS, which is its first type argument — the same position the collection - // generators use for their element. - ITypeSymbol element = factory.TargetMethod.TypeArguments[0]; - if (!EqualityFacts.UsesReferenceEquality(element)) { return; } - if (!ProducesFreshInstances(factory.Arguments[0].Value)) { return; } - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.InertDistinctness, declaration.Syntax.GetLocation(), element.Name)); - } - - /// - /// Where distinctness was declared: the factory itself for the set-like generators, otherwise the first - /// Distinct() in the chain. null when none was declared, and also when one was but a comparer - /// came with it. - /// - /// - /// The two null answers are deliberately the same answer, because the rule does the same thing with - /// them: stand down. A comparer supplied to the factory or to Distinct() answers the equality question - /// itself, whatever the element type does — so there is nothing inert to report, exactly as when distinctness - /// was never asked for. - /// - private static IInvocationOperation? DistinctnessDeclaration(IInvocationOperation factory, string factoryName, IReadOnlyList constraints) { - bool impliedByFactory = factoryName is "SetOf" or "DictionaryOf"; - IInvocationOperation? declaration = impliedByFactory ? factory : null; - - if (impliedByFactory && CarriesComparer(factory)) { return null; } - - foreach (IInvocationOperation constraint in constraints) { - if (constraint.TargetMethod.Name != "Distinct") { continue; } - if (CarriesComparer(constraint)) { return null; } - - declaration ??= constraint; - } - - return declaration; - } - - /// - /// Whether the element generator provably hands back a new instance on every draw, which is what makes - /// reference equality unable to bind. - /// - /// - /// The narrowing dogfooding forced. A pool generator returns the very references it was handed, so - /// Any.SetOf(Any.OneOf(first, second)) is a legal and meaningful declaration: drawing first twice - /// yields the same reference and the set rejects it exactly as asked. The rule's premise — every element is a - /// new instance — holds only where the chain builds the value here, so that is the only shape it claims. - /// - private static bool ProducesFreshInstances(IOperation element) { - if (GeneratorFacts.Unwrap(element) is not IInvocationOperation invocation) { return false; } - if (invocation.TargetMethod.Name is not ("As" or "Combine")) { return false; } - if (invocation.Arguments.Length == 0) { return false; } - - IArgumentOperation last = invocation.Arguments[invocation.Arguments.Length - 1]; - if (GeneratorFacts.Unwrap(last.Value) is not IDelegateCreationOperation { Target: IAnonymousFunctionOperation builder }) { return false; } - - foreach (IOperation statement in builder.Body.Operations) { - if (statement is IReturnOperation { ReturnedValue: { } value } && GeneratorFacts.Unwrap(value) is IObjectCreationOperation) { return true; } - } - - return false; - } - - private static bool CarriesComparer(IInvocationOperation invocation) { - return invocation.TargetMethod.Parameters.Any(parameter => parameter.Type is INamedTypeSymbol { Name: "IEqualityComparer" }); - } - -} diff --git a/JustDummies.Analyzers/JustDummies.Analyzers.csproj b/JustDummies.Analyzers/JustDummies.Analyzers.csproj deleted file mode 100644 index 33fcf7d5..00000000 --- a/JustDummies.Analyzers/JustDummies.Analyzers.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - - netstandard2.0 - enable - enable - latest - - - true - - - false - false - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - diff --git a/JustDummies.Analyzers/KnownSymbols.cs b/JustDummies.Analyzers/KnownSymbols.cs deleted file mode 100644 index 04f2616d..00000000 --- a/JustDummies.Analyzers/KnownSymbols.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace JustDummies.Analyzers; - -/// -/// Resolves the types an analyzer matches against, by metadata name. Analyzers never reference the JustDummies -/// assembly directly — they are loaded into the consumer's compiler — so a null field simply means the type is not -/// part of the analyzed compilation, in which case the rule that needs it stays silent. -/// -/// -/// The xUnit and adapter lookups are what let a rule reason about a test's lifecycle without JustDummies ever -/// depending on either: a consumer who uses neither gets a compilation where those fields are null, and the rules -/// that need them never register. -/// -internal sealed class KnownSymbols { - - public const string AnyMetadataName = "JustDummies.Any"; - public const string IAnyMetadataName = "JustDummies.IAny`1"; - public const string AnyContextMetadataName = "JustDummies.AnyContext"; - - public const string ReproducibleAttributeMetadataName = "JustDummies.Xunit.ReproducibleAttribute"; - public const string FactAttributeMetadataName = "Xunit.v3.IFactAttribute"; - public const string MemberDataAttributeMetadataName = "Xunit.MemberDataAttribute"; - - private KnownSymbols(Compilation compilation) { - Any = compilation.GetTypeByMetadataName(AnyMetadataName); - IAny = compilation.GetTypeByMetadataName(IAnyMetadataName); - AnyContext = compilation.GetTypeByMetadataName(AnyContextMetadataName); - ReproducibleAttribute = compilation.GetTypeByMetadataName(ReproducibleAttributeMetadataName); - FactAttribute = compilation.GetTypeByMetadataName(FactAttributeMetadataName); - MemberDataAttribute = compilation.GetTypeByMetadataName(MemberDataAttributeMetadataName); - } - - /// The JustDummies.Any façade, or null when the compilation does not reference JustDummies. - public INamedTypeSymbol? Any { get; } - - /// The unbound JustDummies.IAny<T> generator interface, or null as above. - public INamedTypeSymbol? IAny { get; } - - /// The isolated JustDummies.AnyContext, whose draws are unaffected by the ambient seed scope. - public INamedTypeSymbol? AnyContext { get; } - - /// The xUnit adapter's [Reproducible], or null when the adapter is not referenced. - public INamedTypeSymbol? ReproducibleAttribute { get; } - - /// The interface every xUnit test attribute implements — [Fact], [Theory] and derivatives. - public INamedTypeSymbol? FactAttribute { get; } - - /// xUnit's [MemberData], which names the member producing a theory's cases. - public INamedTypeSymbol? MemberDataAttribute { get; } - - public static KnownSymbols From(Compilation compilation) { - return new KnownSymbols(compilation); - } - -} diff --git a/JustDummies.Analyzers/NegativeTestGuard.cs b/JustDummies.Analyzers/NegativeTestGuard.cs deleted file mode 100644 index bfa66778..00000000 --- a/JustDummies.Analyzers/NegativeTestGuard.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace JustDummies.Analyzers; - -/// -/// Suppresses a diagnostic on the shape a test uses to assert that a call fails: the offending expression is the -/// entire body of an expression-bodied lambda handed to another call — Assert.Throws(() => illegal), -/// Check.ThatCode(() => illegal), Should.Throw(() => illegal). -/// -/// -/// Deliberately framework-agnostic: it names no assertion library, so it covers the ones this repository uses and -/// the ones a consumer brings. It is also deliberately narrow — the expression must be the whole lambda body, so -/// arrange code inside Any.Reproducibly(() => { ... }) stays reported, the call there being one statement -/// of a block rather than the body itself. -/// -internal static class NegativeTestGuard { - - public static bool IsSoleBodyOfLambdaArgument(SyntaxNode expression) { - if (expression.Parent is not LambdaExpressionSyntax lambda) { return false; } - if (lambda.Body != expression) { return false; } - - return lambda.Parent is ArgumentSyntax; - } - -} diff --git a/JustDummies.Analyzers/NestedReproducibilityScopeAnalyzer.cs b/JustDummies.Analyzers/NestedReproducibilityScopeAnalyzer.cs deleted file mode 100644 index 04cd30f5..00000000 --- a/JustDummies.Analyzers/NestedReproducibilityScopeAnalyzer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD018 — reports a reproducibility scope opened inside another one. Both mechanisms report a replay -/// instruction, and nesting makes the outer instruction false: Any.Reproducibly takes its seed from -/// Guid.NewGuid().GetHashCode(), not from the ambient source, so the inner scope draws a brand-new seed on -/// every run whatever the outer one pinned. The failure names a seed that reproduces nothing. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class NestedReproducibilityScopeAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.NestedReproducibilityScope); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!IsRunner(invocation, symbols)) { return; } - - // The seeded overload pins a chosen seed deliberately; only the seedless form silently overrides the outer - // instruction with one nobody recorded. - if (HasSeedArgument(invocation)) { return; } - - if (IsInsideAnotherRunner(invocation, symbols)) { - Report(context, invocation, "another Any.Reproducibly scope"); - - return; - } - - if (symbols.ReproducibleAttribute is null || symbols.FactAttribute is null) { return; } - if (context.ContainingSymbol is not IMethodSymbol method) { return; } - if (!XunitFacts.IsTestMethod(method, symbols.FactAttribute)) { return; } - if (!XunitFacts.IsCoveredByReproducible(method, symbols.ReproducibleAttribute)) { return; } - - Report(context, invocation, "a [Reproducible] test"); - } - - private static void Report(OperationAnalysisContext context, IInvocationOperation invocation, string outer) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.NestedReproducibilityScope, invocation.Syntax.GetLocation(), outer)); - } - - private static bool IsRunner(IInvocationOperation invocation, KnownSymbols symbols) { - return invocation.TargetMethod.Name is "Reproducibly" or "ReproduciblyAsync" - && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, symbols.Any); - } - - private static bool HasSeedArgument(IInvocationOperation invocation) { - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.Parameter?.Name == "seed" && argument.ArgumentKind != ArgumentKind.DefaultValue) { return true; } - } - - return false; - } - - private static bool IsInsideAnotherRunner(IInvocationOperation invocation, KnownSymbols symbols) { - for (IOperation? current = invocation.Parent; current is not null; current = current.Parent) { - if (current is IInvocationOperation outer && !ReferenceEquals(outer, invocation) && IsRunner(outer, symbols)) { return true; } - } - - return false; - } - -} diff --git a/JustDummies.Analyzers/ParallelDrawWithoutPerItemSeedAnalyzer.cs b/JustDummies.Analyzers/ParallelDrawWithoutPerItemSeedAnalyzer.cs deleted file mode 100644 index 65021c0d..00000000 --- a/JustDummies.Analyzers/ParallelDrawWithoutPerItemSeedAnalyzer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD022 — reports an ambient draw inside a Parallel work item that opens no seed scope of its own. The -/// ambient scope flows into every worker, so one shared scope reaches them all and the draws interleave: the -/// sequence is stable for nobody and the run replays nothing. The library's own documentation names this shape — -/// a scope opened inside the loop body gives each unit of work its own sequence, and the whole run replays. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ParallelDrawWithoutPerItemSeedAnalyzer : DiagnosticAnalyzer { - - private const string ParallelMetadataName = "System.Threading.Tasks.Parallel"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.ParallelDrawWithoutPerItemSeed); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - INamedTypeSymbol? parallel = context.Compilation.GetTypeByMetadataName(ParallelMetadataName); - if (parallel is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols, parallel), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols, INamedTypeSymbol parallel) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!GeneratorFacts.IsGenerateCall(invocation, symbols.IAny!)) { return; } - if (!GeneratorFacts.RootsAtAmbientAny(invocation, symbols.Any!)) { return; } - - IAnonymousFunctionOperation? body = EnclosingParallelBody(invocation, parallel); - if (body is null) { return; } - if (OpensASeedScope(body, symbols.Any!)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.ParallelDrawWithoutPerItemSeed, invocation.Syntax.GetLocation())); - } - - // The innermost lambda that is an argument to a Parallel.* call — the work item. - private static IAnonymousFunctionOperation? EnclosingParallelBody(IOperation operation, INamedTypeSymbol parallel) { - for (IOperation? current = operation.Parent; current is not null; current = current.Parent) { - if (current is not IAnonymousFunctionOperation lambda) { continue; } - - for (IOperation? outer = lambda.Parent; outer is not null; outer = outer.Parent) { - if (outer is IInvocationOperation call) { - return SymbolEqualityComparer.Default.Equals(call.TargetMethod.ContainingType, parallel) ? lambda : null; - } - - if (outer is IAnonymousFunctionOperation) { break; } - } - } - - return null; - } - - private static bool OpensASeedScope(IOperation node, INamedTypeSymbol any) { - if (node is IInvocationOperation call - && call.TargetMethod.Name == "UseSeed" - && SymbolEqualityComparer.Default.Equals(call.TargetMethod.ContainingType, any)) { return true; } - - return node.ChildOperations.Any(child => OpensASeedScope(child, any)); - } - -} diff --git a/JustDummies.Analyzers/RejectedConstantArgumentAnalyzer.cs b/JustDummies.Analyzers/RejectedConstantArgumentAnalyzer.cs deleted file mode 100644 index f8d9dc7a..00000000 --- a/JustDummies.Analyzers/RejectedConstantArgumentAnalyzer.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System.Collections.Immutable; -using System.Linq; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD014 — reports a constraint argument that is a compile-time constant the generator's own guard rejects, so the -/// call throws every time it runs. The mistake is fully determined by a literal at the call site, yet it survives -/// the build and only fires when that arrange line executes — often deep inside a helper shared by many tests, -/// where the failure reads as a library problem rather than as the transposition typo it usually is. -/// -/// -/// One rule over one table rather than a rule per method: the library validates these in one place, with one -/// message shape, and a reader who learns "a constant the guard rejects" has learned all of them. The table -/// mirrors SizeGuard and the per-generator guards exactly; where it cannot be certain it stays silent. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -[System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with \"LINQ\" expressions", - Justification = - "Every TryCheck* member walks the argument list and reports the first offender through its out parameters. The rule asks for " + - "Select(argument => argument.Value), a projection that buys nothing and renames the loop variable away from what it is: `argument` " + - "is an IArgumentOperation, and `argument.Value` reads as the operation behind it. TryCheckSize cannot honour it at all — its filter " + - "produces the `out int value` its body then reports on, so a projection would force a second TryGetInt32 call. The family reads the " + - "same way on purpose, so it is declared once here rather than four times below.")] -public sealed class RejectedConstantArgumentAnalyzer : DiagnosticAnalyzer { - - // SizeGuard.MaxProducibleSize — a size the generator must actually produce is capped here. - private const int MaxProducibleSize = 1_000_000; - private const int MaxDecimalScale = 28; - private const int MinPort = 1; - private const int MaxPort = 65535; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.RejectedConstantArgument); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (!IsJustDummiesMember(invocation.TargetMethod, symbols)) { return; } - - // A test asserting that the guard rejects the argument writes the illegal call as the whole body of a lambda - // argument. This repository alone holds hundreds of them. - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - if (TryFindViolation(invocation, context.Compilation, out IOperation? offending, out string? reason)) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.RejectedConstantArgument, offending!.Syntax.GetLocation(), invocation.TargetMethod.Name, reason)); - } - } - - private static bool TryFindViolation(IInvocationOperation invocation, Compilation compilation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - string name = invocation.TargetMethod.Name; - - switch (name) { - case "WithLength" or "WithMinLength" or "WithCount" or "WithMinCount": - return TryCheckSize(invocation, capped: true, out offending, out reason); - - case "WithMaxLength" or "WithMaxCount" or "WithPathSegments": - return TryCheckSize(invocation, capped: false, out offending, out reason); - - case "WithLengthBetween" or "WithCountBetween": - return TryCheckSizeRange(invocation, out offending, out reason); - - case "Between": - return TryCheckOrderedPair(invocation, out offending, out reason); - - case "MultipleOf": - return TryCheckStrictlyPositive(invocation, out offending, out reason); - - case "WithGranularity": - return TryCheckGranularity(invocation, compilation, out offending, out reason); - - case "WithScale": - return TryCheckRange(invocation, 0, MaxDecimalScale, "the scale must be in the inclusive range [0, 28]", out offending, out reason); - - case "WithPort": - return TryCheckRange(invocation, MinPort, MaxPort, "the port must be between 1 and 65535", out offending, out reason); - - case "StartingWith" or "EndingWith" or "Containing" or "WithChars" or "WithHost": - return TryCheckNonEmptyText(invocation, out offending, out reason); - - case "OneOf" or "Except": - return TryCheckNonEmptyPool(invocation, out offending, out reason); - - default: - return false; - } - } - - private static bool TryCheckSize(IInvocationOperation invocation, bool capped, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in NumericArguments(invocation)) { - if (!ConstantFacts.TryGetInt32(argument.Value, out int value)) { continue; } - - if (value < 0) { - offending = argument.Value; - reason = "it must not be negative"; - - return true; - } - - if (capped && value > MaxProducibleSize) { - offending = argument.Value; - reason = $"it must not exceed {MaxProducibleSize:N0}, the largest size the generator will produce"; - - return true; - } - } - - return false; - } - - private static bool TryCheckSizeRange(IInvocationOperation invocation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - IArgumentOperation[] arguments = NumericArguments(invocation).ToArray(); - if (arguments.Length != 2) { return false; } - - if (ConstantFacts.TryGetInt32(arguments[0].Value, out int minimum) && minimum < 0) { - offending = arguments[0].Value; - reason = "it must not be negative"; - - return true; - } - - if (ConstantFacts.TryGetInt32(arguments[0].Value, out int min) && min > MaxProducibleSize) { - offending = arguments[0].Value; - reason = $"it must not exceed {MaxProducibleSize:N0}, the largest size the generator will produce"; - - return true; - } - - if (ConstantFacts.TryGetInt32(arguments[1].Value, out int maximum) && maximum < 0) { - offending = arguments[1].Value; - reason = "it must not be negative"; - - return true; - } - - return TryCheckOrderedPair(invocation, out offending, out reason); - } - - private static bool TryCheckOrderedPair(IInvocationOperation invocation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - IArgumentOperation[] arguments = NumericArguments(invocation).ToArray(); - if (arguments.Length != 2) { return false; } - - if (!ConstantFacts.TryGetInt32(arguments[0].Value, out int minimum)) { return false; } - if (!ConstantFacts.TryGetInt32(arguments[1].Value, out int maximum)) { return false; } - if (minimum <= maximum) { return false; } - - offending = arguments[0].Value; - reason = $"the minimum ({minimum}) must be less than or equal to the maximum ({maximum}) — the two arguments look transposed"; - - return true; - } - - private static bool TryCheckStrictlyPositive(IInvocationOperation invocation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in NumericArguments(invocation)) { - if (!ConstantFacts.TryGetInt32(argument.Value, out int value) || value > 0) { continue; } - - offending = argument.Value; - reason = "it must be strictly positive"; - - return true; - } - - return false; - } - - private static bool TryCheckGranularity(IInvocationOperation invocation, Compilation compilation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (!ConstantFacts.IsNonPositiveTimeSpan(argument.Value, compilation)) { continue; } - - offending = argument.Value; - reason = "the granularity must be strictly positive"; - - return true; - } - - return false; - } - - private static bool TryCheckRange(IInvocationOperation invocation, int minimum, int maximum, string requirement, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in NumericArguments(invocation)) { - if (!ConstantFacts.TryGetInt32(argument.Value, out int value)) { continue; } - if (value >= minimum && value <= maximum) { continue; } - - offending = argument.Value; - reason = requirement; - - return true; - } - - return false; - } - - private static bool TryCheckNonEmptyText(IInvocationOperation invocation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.Parameter?.Type.SpecialType != SpecialType.System_String) { continue; } - if (!ConstantFacts.TryGetString(argument.Value, out string text)) { continue; } - - if (text.Length == 0) { - offending = argument.Value; - reason = "it must not be empty"; - - return true; - } - - if (invocation.TargetMethod.Name == "WithChars" && text.Any(char.IsSurrogate)) { - offending = argument.Value; - reason = "a character pool must not contain a surrogate: an astral code point spans two UTF-16 units, which the draw would split. Use OneOf(...) to draw such values as whole strings"; - - return true; - } - } - - return false; - } - - private static bool TryCheckNonEmptyPool(IInvocationOperation invocation, out IOperation? offending, out string? reason) { - offending = null; - reason = null; - - foreach (IArgumentOperation argument in invocation.Arguments) { - if (argument.ArgumentKind != ArgumentKind.ParamArray) { continue; } - if (argument.Value is not IArrayCreationOperation { Initializer: { } initializer }) { continue; } - if (!initializer.ElementValues.IsEmpty) { continue; } - - offending = invocation; - reason = "at least one value is required"; - - return true; - } - - return false; - } - - // Only the arguments a size or bound guard inspects: an int parameter. This keeps Containing(TItem) on a - // collection, or Between(DateTime, DateTime), out of the integer checks rather than misreading them. - private static System.Collections.Generic.IEnumerable NumericArguments(IInvocationOperation invocation) { - return invocation.Arguments.Where(argument => argument.Parameter?.Type.SpecialType == SpecialType.System_Int32); - } - - private static bool IsJustDummiesMember(IMethodSymbol method, KnownSymbols symbols) { - return SymbolEqualityComparer.Default.Equals(method.ContainingType?.ContainingAssembly, symbols.IAny!.ContainingAssembly); - } - -} diff --git a/JustDummies.Analyzers/ReproducibleOnNonTestMethodAnalyzer.cs b/JustDummies.Analyzers/ReproducibleOnNonTestMethodAnalyzer.cs deleted file mode 100644 index 39191ac3..00000000 --- a/JustDummies.Analyzers/ReproducibleOnNonTestMethodAnalyzer.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace JustDummies.Analyzers; - -/// -/// JD010 — reports [Reproducible] on a method xUnit never treats as a test. The adapter's hooks are -/// collected from the test method, its class and the assembly only, so an attribute on a helper — or on a method -/// whose [Fact] was removed during a refactor — pins nothing and reports nothing. It is invisible when it -/// works (a passing test stays silent by design), so nothing else can tell the two apart. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ReproducibleOnNonTestMethodAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.ReproducibleOnNonTestMethod); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.ReproducibleAttribute is null || symbols.FactAttribute is null) { return; } - - context.RegisterSymbolAction(symbolContext => Analyze(symbolContext, symbols), SymbolKind.Method); - } - - private static void Analyze(SymbolAnalysisContext context, KnownSymbols symbols) { - IMethodSymbol method = (IMethodSymbol)context.Symbol; - - foreach (AttributeData attribute in method.GetAttributes()) { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbols.ReproducibleAttribute)) { continue; } - if (XunitFacts.IsTestMethod(method, symbols.FactAttribute!)) { return; } - - Location location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() - ?? method.Locations[0]; - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.ReproducibleOnNonTestMethod, location, method.Name)); - - return; - } - } - -} diff --git a/JustDummies.Analyzers/ScalarChainAdmitsNoValueAnalyzer.cs b/JustDummies.Analyzers/ScalarChainAdmitsNoValueAnalyzer.cs deleted file mode 100644 index ca26d960..00000000 --- a/JustDummies.Analyzers/ScalarChainAdmitsNoValueAnalyzer.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD023 — reports a scalar chain whose constant constraints leave no value at all, and JD024 — a constraint that -/// narrows nothing. The two share one walk because they read the same state from opposite sides: one asks whether -/// anything remains, the other whether anything changed. -/// -/// -/// JD024 is the only member of the constraint family the run time never reports. Every other contradiction throws -/// eventually and loudly; an inert constraint leaves the test green while it exercises a domain the author did not -/// write. That is why it is worth an Info rule rather than nothing. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ScalarChainAdmitsNoValueAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.ScalarChainAdmitsNoValue, Descriptors.ConstraintWithNoEffect); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null || !IsIntegerFactory(factory.TargetMethod.Name)) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - AnalyzeConstraints(context, constraints); - } - - /// - /// Walks the constraints in the order they were written, carrying the interval they describe, and reports the - /// first one that empties it, that narrows nothing, or that excludes nothing. - /// - /// - /// Split from , which answers a different question: whether this chain is one the rule - /// reasons about at all. Everything below assumes that answer is yes. - /// - private static void AnalyzeConstraints(OperationAnalysisContext context, IReadOnlyList constraints) { - ScalarConstraintState state = ScalarConstraintState.Unconstrained(); - - foreach (IInvocationOperation constraint in constraints) { - if (!TryReadArguments(constraint, out IReadOnlyList arguments)) { return; } - - string name = constraint.TargetMethod.Name; - - // The exclusion that removes nothing: silent at run time, and the reason JD024 exists. - if (name is "Except" or "DifferentFrom" && state.ExclusionIsInert(arguments)) { - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.ConstraintWithNoEffect, constraint.Syntax.GetLocation(), - $"{name} removes no value the generator could produce")); - - return; - } - - ScalarConstraintState? next = state.Apply(name, arguments); - if (next is null) { return; } - - if (next.IsEmpty()) { - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.ScalarChainAdmitsNoValue, constraint.Syntax.GetLocation(), name)); - - return; - } - - if (IsNarrowingConstraint(name) && state.NarrowsNothing(next)) { - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.ConstraintWithNoEffect, constraint.Syntax.GetLocation(), - $"{name} is already implied by the constraints declared before it")); - - return; - } - - state = next; - } - } - - // A bound whose job is to narrow. Applying one that changes nothing is what JD024 reports; Positive() after - // GreaterThan(5) is the shape, and it reads as a tightening that is not one. - private static bool IsNarrowingConstraint(string name) { - return name is "GreaterThan" or "GreaterThanOrEqualTo" or "LessThan" or "LessThanOrEqualTo" or "Between" or "Positive" or "Negative"; - } - - private static bool TryReadArguments(IInvocationOperation constraint, out IReadOnlyList arguments) { - List values = []; - arguments = values; - - foreach (IArgumentOperation argument in constraint.Arguments) { - if (argument.ArgumentKind == ArgumentKind.ParamArray) { - if (argument.Value is not IArrayCreationOperation { Initializer: { } initializer }) { return false; } - - foreach (IOperation element in initializer.ElementValues) { - if (!TryReadInteger(element, out long value)) { return false; } - - values.Add(value); - } - - continue; - } - - if (!TryReadInteger(argument.Value, out long single)) { return false; } - - values.Add(single); - } - - return true; - } - - private static bool TryReadInteger(IOperation operation, out long value) { - value = 0; - - Optional constant = GeneratorFacts.Unwrap(operation).ConstantValue; - if (!constant.HasValue) { return false; } - - switch (constant.Value) { - case int i: value = i; return true; - case long l: value = l; return true; - case short s: value = s; return true; - case byte b: value = b; return true; - case sbyte sb: value = sb; return true; - default: return false; - } - } - - // Only the integer generators: the model is integer arithmetic, and a floating-point or decimal domain does not - // behave like one. - private static bool IsIntegerFactory(string name) { - return name is "Int32" or "Int16" or "Int64" or "Byte" or "SByte" or "UInt16" or "UInt32" or "UInt64"; - } - -} diff --git a/JustDummies.Analyzers/ScalarConstraintState.cs b/JustDummies.Analyzers/ScalarConstraintState.cs deleted file mode 100644 index 177dd519..00000000 --- a/JustDummies.Analyzers/ScalarConstraintState.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// The integer domain a scalar chain has narrowed to, rebuilt constraint by constraint in declaration order. -/// Both JD023 (the domain became empty) and JD024 (a constraint narrowed nothing) read it: one asks -/// whether anything remains, the other whether anything changed. -/// -/// -/// Integers only, and only where every argument folds to a constant. A chain carrying one unfoldable argument -/// stops being tracked rather than being guessed at: a rule that claims a chain is unsatisfiable must be certain. -/// Bounds are kept in so a constraint at cannot overflow the -/// arithmetic that tests them. -/// -internal sealed class ScalarConstraintState { - - /// - /// How many values the emptiness checks will walk before giving up and answering "not empty". It bounds both - /// walks below — the range and the lattice — so the rule stays cheap on a chain over a huge domain, at the - /// price of a deliberate false negative on one no realistic exclusion set could empty anyway. - /// - private const long MaxWalkLength = 64; - - private ScalarConstraintState(long minimum, long maximum, long? multipleOf, HashSet? allowed, HashSet excluded, bool saturated = false) { - Minimum = minimum; - Maximum = maximum; - MultipleOf = multipleOf; - Allowed = allowed; - Excluded = excluded; - Saturated = saturated; - } - - public long Minimum { get; } - public long Maximum { get; } - public long? MultipleOf { get; } - public HashSet? Allowed { get; } - public HashSet Excluded { get; } - - /// - /// Set when a bound asked for values beyond the representable range — GreaterThan(long.MaxValue). The - /// domain is empty, and saying so needs a flag rather than an out-of-range bound, because the bounds run to - /// the extremes: LessThanOrEqualTo(long.MinValue) is a legal chain that yields exactly one value. - /// - public bool Saturated { get; } - - public static ScalarConstraintState Unconstrained() { - return new ScalarConstraintState(long.MinValue, long.MaxValue, null, null, []); - } - - /// Whether no value at all survives the constraints declared so far. - public bool IsEmpty() { - if (Saturated) { return true; } - if (Allowed is not null) { return !Allowed.Any(Admits); } - if (Minimum > Maximum) { return true; } - - // A small finite range can be emptied by its exclusions alone, with the bounds still consistent: - // Zero().NonZero() pins [0, 0] and then forbids the only value in it. - if (Excluded.Count > 0 && FitsInAWalk()) { - for (long value = Minimum; value <= Maximum; value++) { - if (Admits(value)) { return false; } - } - - return true; - } - - return MultipleOf is long step && !HasMultipleInRange(step); - } - - // Only walk a range small enough to enumerate, and far enough from the extremes that the arithmetic cannot - // overflow. A wider range is never declared empty by exclusions: no realistic exclusion set could empty it. - private bool FitsInAWalk() { - return Minimum > long.MinValue / 2 && Maximum < long.MaxValue / 2 && Maximum - Minimum < MaxWalkLength; - } - - /// Whether survives every constraint declared so far. - public bool Admits(long value) { - if (value < Minimum || value > Maximum) { return false; } - if (Excluded.Contains(value)) { return false; } - if (MultipleOf is long step && step != 0 && value % step != 0) { return false; } - - return Allowed is null || Allowed.Contains(value); - } - - /// - /// Applies one constraint, returning the narrowed state — or null when the constraint is one this model - /// does not track, which abandons the chain rather than misreading it. - /// - public ScalarConstraintState? Apply(string name, IReadOnlyList arguments) { - switch (name) { - case "Positive": return WithMinimum(1); - case "Negative": return WithMaximum(-1); - case "Zero": return WithMinimum(0)?.WithMaximum(0); - case "NonZero": return WithExcluded(0); - - // Nothing is greater than the largest representable value, nor less than the smallest: those two ask for - // an empty domain rather than for a bound, and computing one would overflow. - case "GreaterThan" when arguments.Count == 1: - return arguments[0] == long.MaxValue ? Saturate() : WithMinimum(arguments[0] + 1); - - case "LessThan" when arguments.Count == 1: - return arguments[0] == long.MinValue ? Saturate() : WithMaximum(arguments[0] - 1); - - case "GreaterThanOrEqualTo" when arguments.Count == 1: return WithMinimum(arguments[0]); - case "LessThanOrEqualTo" when arguments.Count == 1: return WithMaximum(arguments[0]); - - case "Between" when arguments.Count == 2: return WithMinimum(arguments[0])?.WithMaximum(arguments[1]); - case "MultipleOf" when arguments.Count == 1 && arguments[0] != 0: - return new ScalarConstraintState(Minimum, Maximum, arguments[0] < 0 ? -arguments[0] : arguments[0], Allowed, Excluded); - - case "OneOf" when arguments.Count > 0: - return new ScalarConstraintState(Minimum, Maximum, MultipleOf, [.. arguments], Excluded); - - case "Except" or "DifferentFrom" when arguments.Count > 0: { - HashSet excluded = [.. Excluded, .. arguments]; - - return new ScalarConstraintState(Minimum, Maximum, MultipleOf, Allowed, excluded); - } - - // Anything else — a granularity, a scale, a name this model has never seen — ends the walk. - default: return null; - } - } - - /// Whether applying would leave the domain exactly as it is. - public bool NarrowsNothing(ScalarConstraintState candidate) { - return candidate.Minimum == Minimum - && candidate.Maximum == Maximum - && candidate.MultipleOf == MultipleOf - && candidate.Excluded.Count == Excluded.Count - && (candidate.Allowed?.Count ?? -1) == (Allowed?.Count ?? -1); - } - - /// - /// Whether an exclusion removes a value the domain could never have produced anyway — the silent case, where - /// the author excluded a sentinel the generator was never going to draw. - /// - public bool ExclusionIsInert(IReadOnlyList values) { - return values.Count > 0 && values.All(value => !Admits(value)); - } - - private ScalarConstraintState Saturate() { - return new ScalarConstraintState(Minimum, Maximum, MultipleOf, Allowed, Excluded, saturated: true); - } - - private ScalarConstraintState? WithMinimum(long minimum) { - return minimum <= Minimum - ? new ScalarConstraintState(Minimum, Maximum, MultipleOf, Allowed, Excluded) - : new ScalarConstraintState(minimum, Maximum, MultipleOf, Allowed, Excluded); - } - - private ScalarConstraintState? WithMaximum(long maximum) { - return maximum >= Maximum - ? new ScalarConstraintState(Minimum, Maximum, MultipleOf, Allowed, Excluded) - : new ScalarConstraintState(Minimum, maximum, MultipleOf, Allowed, Excluded); - } - - private ScalarConstraintState WithExcluded(long value) { - HashSet excluded = [.. Excluded, value]; - - return new ScalarConstraintState(Minimum, Maximum, MultipleOf, Allowed, excluded); - } - - // Is there any multiple of step inside [Minimum, Maximum] that survives the exclusions? The range can be huge, so - // this walks the lattice from its first multiple rather than the range itself, and gives up (answering "yes") - // once the walk is longer than any realistic exclusion set could rule out. - private bool HasMultipleInRange(long step) { - if (step == 0) { return false; } - - long first = Minimum >= 0 - ? (Minimum + step - 1) / step * step - : -((-Minimum) / step) * step; - - for (long candidate = first, seen = 0; candidate <= Maximum && seen < MaxWalkLength; candidate += step, seen++) { - if (!Excluded.Contains(candidate) && (Allowed is null || Allowed.Contains(candidate))) { return true; } - } - - // The walk gave up before finding one; only a range genuinely wider than the walk can still hold a multiple. - // Compare by division so the subtraction cannot overflow at the representable extremes — which is why both - // sides are halved, the right one carrying half the walk length rather than the whole of it. - return Maximum / 2 - Minimum / 2 >= step * (MaxWalkLength / 2); - } - -} diff --git a/JustDummies.Analyzers/SharedStaticAnyContextAnalyzer.cs b/JustDummies.Analyzers/SharedStaticAnyContextAnalyzer.cs deleted file mode 100644 index 007d548f..00000000 --- a/JustDummies.Analyzers/SharedStaticAnyContextAnalyzer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace JustDummies.Analyzers; - -/// -/// JD020 — reports an AnyContext held in a static field. It looks maximally deterministic — a literal seed, -/// right there in the source — and is not: the type's own documentation states that sharing one context across -/// threads "costs the replay rather than the values", because interleaved draws make neither the sequence nor the -/// multiset stable. A suite that runs its classes in parallel therefore gets a different value per test per run, -/// from a context that reads as pinned. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class SharedStaticAnyContextAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.SharedStaticAnyContext); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.AnyContext is null) { return; } - - INamedTypeSymbol anyContext = symbols.AnyContext; - - context.RegisterSymbolAction(symbolContext => Analyze(symbolContext, anyContext), SymbolKind.Field, SymbolKind.Property); - } - - private static void Analyze(SymbolAnalysisContext context, INamedTypeSymbol anyContext) { - ITypeSymbol? type = context.Symbol switch { - IFieldSymbol { IsStatic: true } field => field.Type, - IPropertySymbol { IsStatic: true } property => property.Type, - _ => null, - }; - - if (type is null || !SymbolEqualityComparer.Default.Equals(type, anyContext)) { return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.SharedStaticAnyContext, context.Symbol.Locations[0], context.Symbol.Name)); - } - -} diff --git a/JustDummies.Analyzers/StringConstraintsAdmitNoValueAnalyzer.cs b/JustDummies.Analyzers/StringConstraintsAdmitNoValueAnalyzer.cs deleted file mode 100644 index 4286fba2..00000000 --- a/JustDummies.Analyzers/StringConstraintsAdmitNoValueAnalyzer.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD015 — reports an AnyString chain whose constant constraints admit no value: an anchored fragment -/// holding a character the declared character family forbids, or fragments that cannot fit the declared length. -/// -/// -/// This is the case ADR-0035 names by hand as the one an analyzer should carry and the type system cannot: -/// Numeric().StartingWith("ORD-") conflicts while Numeric().StartingWith("123") does not, from -/// identical call sites and identical static types. Only the argument's value tells them apart, which is exactly -/// what makes it value-dependent — and what puts it on the analyzer's side of the ADR's line. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class StringConstraintsAdmitNoValueAnalyzer : DiagnosticAnalyzer { - - private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; - private const string Digits = "0123456789"; - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.StringConstraintsAdmitNoValue); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null || symbols.IAny is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - // Analyse each chain once, from its outermost call. - if (invocation.Parent is IInvocationOperation) { return; } - if (!AnyChainFacts.TryGetChain(invocation, symbols, out IReadOnlyList constraints, out IInvocationOperation? factory)) { return; } - if (factory is null || factory.TargetMethod.Name != "String") { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - AnalyzeConstraints(context, constraints); - } - - /// - /// Reads what the chain declares about the string — its pool, its casing, its fragments and its length budget — - /// then reports the first declaration no value can satisfy. - /// - /// - /// Split from , which answers a different question: whether this chain is one the rule - /// reasons about at all. Everything below assumes that answer is yes. - /// - private static void AnalyzeConstraints(OperationAnalysisContext context, IReadOnlyList constraints) { - string? pool = null; - string? poolName = null; - bool requireUpper = false; - bool requireLower = false; - bool hasValueSet = false; - List<(string Text, IOperation At)> fragments = []; - int? fixedLength = null; - int? maximum = null; - - foreach (IInvocationOperation constraint in constraints) { - switch (constraint.TargetMethod.Name) { - case "Alpha": pool = UpperLetters + LowerLetters; poolName = "Alpha()"; break; - case "Numeric": pool = Digits; poolName = "Numeric()"; break; - case "AlphaNumeric": pool = UpperLetters + LowerLetters + Digits; poolName = "AlphaNumeric()"; break; - - // Casing is not a character set: it constrains the CASE of a fragment's letters and says nothing - // about its other characters. UpperCase().StartingWith("ORD-") is legal — the '-' is not a letter — - // while UpperCase().StartingWith("abc") is not. - case "UpperCase": requireUpper = true; break; - case "LowerCase": requireLower = true; break; - - // A terminal value set changes what the fragments are checked against: they are matched against the - // pooled values rather than laid out side by side, so the length budget below no longer applies. - case "OneOf": hasValueSet = true; break; - - case "WithChars" when constraint.Arguments.Length == 1 && ConstantFacts.TryGetString(constraint.Arguments[0].Value, out string declared): - pool = declared; - poolName = $"WithChars(\"{declared}\")"; - - break; - - case "StartingWith" or "EndingWith" or "Containing" when constraint.Arguments.Length == 1 && ConstantFacts.TryGetString(constraint.Arguments[0].Value, out string fragment): - fragments.Add((fragment, constraint.Arguments[0].Value)); - - break; - - case "WithLength" when constraint.Arguments.Length == 1 && ConstantFacts.TryGetInt32(constraint.Arguments[0].Value, out int length): - fixedLength = length; - - break; - - case "WithMaxLength" when constraint.Arguments.Length == 1 && ConstantFacts.TryGetInt32(constraint.Arguments[0].Value, out int max): - maximum = maximum is null ? max : System.Math.Min(maximum.Value, max); - - break; - } - } - - if (ReportCharacterOutsidePool(context, pool, poolName, fragments)) { return; } - if (ReportLetterAgainstCasing(context, requireUpper, requireLower, fragments)) { return; } - if (hasValueSet) { return; } - - ReportLengthBudget(context, fragments, fixedLength, maximum); - } - - private static bool ReportLetterAgainstCasing(OperationAnalysisContext context, bool requireUpper, bool requireLower, List<(string Text, IOperation At)> fragments) { - if (!requireUpper && !requireLower) { return false; } - - foreach ((string text, IOperation at) in fragments) { - foreach (char character in text) { - if (!char.IsLetter(character)) { continue; } - - bool offends = requireUpper ? char.IsLower(character) : char.IsUpper(character); - if (!offends) { continue; } - - string constraint = requireUpper ? "UpperCase()" : "LowerCase()"; - string wrongCase = requireUpper ? "lowercase" : "uppercase"; - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.StringConstraintsAdmitNoValue, at.Syntax.GetLocation(), - $"{constraint} forbids the {wrongCase} letter '{character}'")); - - return true; - } - } - - return false; - } - - private static bool ReportCharacterOutsidePool(OperationAnalysisContext context, string? pool, string? poolName, List<(string Text, IOperation At)> fragments) { - if (pool is null || pool.Length == 0) { return false; } - - foreach ((string text, IOperation at) in fragments) { - foreach (char character in text) { - if (pool.IndexOf(character) >= 0) { continue; } - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.StringConstraintsAdmitNoValue, at.Syntax.GetLocation(), - $"'{character}' is not a character {poolName} can draw")); - - return true; - } - } - - return false; - } - - private static void ReportLengthBudget(OperationAnalysisContext context, List<(string Text, IOperation At)> fragments, int? fixedLength, int? maximum) { - if (fragments.Count == 0) { return; } - - int required = fragments.Sum(fragment => fragment.Text.Length); - int? cap = fixedLength ?? maximum; - if (cap is null || required <= cap.Value) { return; } - - string capName = fixedLength is not null ? $"WithLength({fixedLength})" : $"WithMaxLength({maximum})"; - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.StringConstraintsAdmitNoValue, fragments[fragments.Count - 1].At.Syntax.GetLocation(), - $"the anchored fragments need at least {required} characters, which {capName} cannot hold")); - } - -} diff --git a/JustDummies.Analyzers/UnusedCombineOperandAnalyzer.cs b/JustDummies.Analyzers/UnusedCombineOperandAnalyzer.cs deleted file mode 100644 index a41eeb40..00000000 --- a/JustDummies.Analyzers/UnusedCombineOperandAnalyzer.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace JustDummies.Analyzers; - -/// -/// JD027 — reports a Combine operand whose value never reaches the composed result, because the composer -/// lambda never reads the parameter it is bound to. -/// -/// -/// The operand is still drawn: Combine generates every part before calling the composer, so the constraints -/// are built, the conflict checks run, and the value is dropped on the floor. Nothing fails — the composed value is -/// well-formed and simply does not carry the part the call site says it carries. Naming the parameter _ is -/// the acknowledgement that switches the rule off, the same escape hatch C# already gives for a discard. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UnusedCombineOperandAnalyzer : DiagnosticAnalyzer { - - /// - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(Descriptors.UnusedCombineOperand); - - /// - public override void Initialize(AnalysisContext context) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) { - KnownSymbols symbols = KnownSymbols.From(context.Compilation); - if (symbols.Any is null) { return; } - - context.RegisterOperationAction(operationContext => Analyze(operationContext, symbols), OperationKind.Invocation); - } - - private static void Analyze(OperationAnalysisContext context, KnownSymbols symbols) { - IInvocationOperation invocation = (IInvocationOperation)context.Operation; - - if (invocation.TargetMethod.Name != "Combine") { return; } - if (!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, symbols.Any)) { return; } - if (invocation.Arguments.Length < 3) { return; } - if (NegativeTestGuard.IsSoleBodyOfLambdaArgument(invocation.Syntax)) { return; } - - // The composer is the last argument, and it has to be a lambda written here: a method group's body is not - // this compilation's to read, so an operand it ignores is not knowable. - IArgumentOperation composerArgument = invocation.Arguments[invocation.Arguments.Length - 1]; - if (GeneratorFacts.Unwrap(composerArgument.Value) is not IDelegateCreationOperation { Target: IAnonymousFunctionOperation composer }) { return; } - - int operandCount = invocation.Arguments.Length - 1; - if (composer.Symbol.Parameters.Length != operandCount) { return; } - if (ComposesNothing(composer)) { return; } - - HashSet read = ReadParameters(composer); - - for (int index = 0; index < operandCount; index++) { - IParameterSymbol parameter = composer.Symbol.Parameters[index]; - - // '_' is how C# spells "I know, and I mean it" — for a discard parameter and for one merely named like - // one. Either way the author has said the draw is deliberate. - if (parameter.Name is "_" or "") { continue; } - if (read.Contains(parameter)) { continue; } - - context.ReportDiagnostic(Diagnostic.Create( - Descriptors.UnusedCombineOperand, invocation.Arguments[index].Value.Syntax.GetLocation(), parameter.Name)); - } - } - - // A composer whose whole body is a throw reads no parameter, and that is not an ignored operand: it composes - // nothing at all, on purpose, which is how a test exercises the failure path Combine wraps. Found by dogfooding - // this rule on the library's own suite, where the arity-8 case is written exactly that way. - // - // The spellings are one shape seen from several places in the tree. An expression-bodied '=> throw ...' is a - // Return CARRYING the throw — and carrying it through a conversion to the composer's result type, so the throw is - // not the returned operation but the operand under it. Both facts had to be measured; guessing either one wrong - // let the very site that motivated this guard through. - private static bool ComposesNothing(IAnonymousFunctionOperation composer) { - foreach (IOperation statement in composer.Body.Operations) { - if (statement is IThrowOperation or IExpressionStatementOperation { Operation: IThrowOperation }) { return true; } - if (statement is IReturnOperation { ReturnedValue: { } returned } && GeneratorFacts.Unwrap(returned) is IThrowOperation) { return true; } - } - - return false; - } - - // Every parameter the composer's body reads, including through a nested lambda that captures it. - private static HashSet ReadParameters(IAnonymousFunctionOperation composer) { - HashSet read = new(SymbolEqualityComparer.Default); - - foreach (IOperation descendant in composer.Body.Descendants()) { - if (descendant is IParameterReferenceOperation reference) { read.Add(reference.Parameter); } - } - - return read; - } - -} diff --git a/JustDummies.Analyzers/XunitFacts.cs b/JustDummies.Analyzers/XunitFacts.cs deleted file mode 100644 index 599ef53e..00000000 --- a/JustDummies.Analyzers/XunitFacts.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using Microsoft.CodeAnalysis; - -namespace JustDummies.Analyzers; - -/// -/// Facts about an xUnit test's shape and lifecycle, needed by the rules that reason about when a value is -/// drawn relative to the seed scope [Reproducible] opens. -/// -/// -/// Every lookup flows through , so a compilation that references neither xUnit nor the -/// JustDummies adapter simply never reaches these rules — JustDummies stays standalone and error-agnostic. -/// -internal static class XunitFacts { - - /// - /// Whether is covered by [Reproducible] — declared on the member itself, on - /// its containing type or a base type, or on the assembly. These are the three levels the adapter honours. - /// - public static bool IsCoveredByReproducible(ISymbol symbol, INamedTypeSymbol reproducibleAttribute) { - if (HasAttribute(symbol, reproducibleAttribute)) { return true; } - - for (INamedTypeSymbol? type = symbol.ContainingType; type is not null; type = type.BaseType) { - if (HasAttribute(type, reproducibleAttribute)) { return true; } - } - - return HasAttribute(symbol.ContainingAssembly, reproducibleAttribute); - } - - /// - /// Whether carries an attribute xUnit treats as a test — anything implementing - /// IFactAttribute, which covers [Fact], [Theory] and third-party derivatives alike. - /// - public static bool IsTestMethod(IMethodSymbol method, INamedTypeSymbol factAttribute) { - return method.GetAttributes().Any(attribute => attribute.AttributeClass is not null && Implements(attribute.AttributeClass, factAttribute)); - } - - /// - /// Whether produces a theory's cases — a member xUnit evaluates at discovery, - /// before any test runs and outside every seed scope. - /// - /// - /// Four shapes are recognised, because a provider is written in four ways: a member named by a - /// [MemberData] in the same type; a member returning TheoryData; a member returning a sequence of - /// object arrays; and a type implementing that sequence, which is the [ClassData] shape. - /// - public static bool IsTheoryDataProvider(ISymbol symbol, KnownSymbols symbols) { - // A draw inside a property's body reports the accessor (get_Cases), not the property, so normalize first — - // otherwise a [MemberData(nameof(Cases))] never matches the member it names. - ISymbol member = symbol is IMethodSymbol { AssociatedSymbol: not null } accessor ? accessor.AssociatedSymbol : symbol; - - if (IsNamedByMemberData(member, symbols)) { return true; } - - ITypeSymbol? returnType = member switch { - IMethodSymbol method => method.ReturnType, - IPropertySymbol property => property.Type, - _ => null, - }; - - if (returnType is not null && (IsTheoryData(returnType) || IsObjectArraySequence(returnType))) { return true; } - - // The [ClassData] shape: the containing type is itself the sequence of cases. - return member.ContainingType is not null && IsObjectArraySequence(member.ContainingType); - } - - private static bool IsNamedByMemberData(ISymbol symbol, KnownSymbols symbols) { - if (symbols.MemberDataAttribute is null || symbol.ContainingType is null) { return false; } - - IEnumerable attributes = symbol.ContainingType.GetMembers() - .OfType() - .SelectMany(member => member.GetAttributes()); - - foreach (AttributeData attribute in attributes) { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbols.MemberDataAttribute)) { continue; } - if (attribute.ConstructorArguments.Length == 0) { continue; } - if (attribute.ConstructorArguments[0].Value as string == symbol.Name) { return true; } - } - - return false; - } - - // Matched by name rather than by symbol: xUnit v3 roots TheoryData<...> in TheoryDataBase<,>, and which generic - // base carries which arity has moved between releases. The namespace plus the TheoryData prefix is the stable part. - private static bool IsTheoryData(ITypeSymbol type) { - for (ITypeSymbol? current = type; current is not null; current = current.BaseType) { - bool inXunit = current.ContainingNamespace is { IsGlobalNamespace: false } ns && ns.ToDisplayString() == "Xunit"; - if (inXunit && current.Name.StartsWith("TheoryData", System.StringComparison.Ordinal)) { return true; } - } - - return false; - } - - // IEnumerable — the raw shape xUnit ultimately consumes, and what a [ClassData] type implements. - private static bool IsObjectArraySequence(ITypeSymbol type) { - IEnumerable candidates = type is INamedTypeSymbol named - ? named.AllInterfaces.Concat(new[] { named }) - : type.AllInterfaces; - - foreach (INamedTypeSymbol candidate in candidates) { - if (candidate.OriginalDefinition.SpecialType != SpecialType.System_Collections_Generic_IEnumerable_T) { continue; } - if (candidate.TypeArguments.Length == 1 && candidate.TypeArguments[0] is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Object }) { return true; } - } - - return false; - } - - private static bool HasAttribute(ISymbol? symbol, INamedTypeSymbol attributeType) { - if (symbol is null) { return false; } - - return symbol.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)); - } - - private static bool Implements(INamedTypeSymbol type, INamedTypeSymbol @interface) { - if (SymbolEqualityComparer.Default.Equals(type, @interface)) { return true; } - - return type.AllInterfaces.Any(implemented => SymbolEqualityComparer.Default.Equals(implemented, @interface)); - } - -} diff --git a/JustDummies.PropertyTests/CollectionProperties.cs b/JustDummies.PropertyTests/CollectionProperties.cs deleted file mode 100644 index 2aeb29f0..00000000 --- a/JustDummies.PropertyTests/CollectionProperties.cs +++ /dev/null @@ -1,374 +0,0 @@ -#region Usings declarations - -using System.Runtime.CompilerServices; - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the collection generators — , , -/// , and . The -/// example-based suite pins a handful of hand-picked sizes (WithCount(5), WithCountBetween(4, 6)) -/// and can only prove the count algebra right for those; these quantify over the counts themselves — every size -/// from empty to thirty, every ordered bound pair, every pool size against every requested count — so a count -/// that is resolved one element short, or a distinctness gate that fires one element too early, is found and -/// shrunk to its minimal counter-example. -/// -/// -/// The count and the element domain are quantified together wherever they interact, because that is where -/// the interesting behaviour lives: a distinct collection is satisfiable or contradictory depending on how the -/// requested count compares to the cardinality its element generator advertises, and the library promises to -/// decide that at declaration time rather than while drawing. A property that fixed either side would only ever -/// visit one side of that frontier. -/// -[TestSubject(typeof(AnyList))] -public sealed class CollectionProperties { - - #region Statics members declarations - - /// - /// Draws per generator for the properties asserting over several collection shapes at once. Lower than the - /// shared default, so covering five shapes in one property costs about what one shape costs elsewhere. - /// - private const int DrawsPerShape = 4; - - /// - /// The number of elements a collection may reach above whatever its declared minimum is — the library's - /// unconstrained spread, mirrored here because the suite is black-box. Smaller than a string's, since - /// elements are themselves generated values. - /// - private const int DefaultCountSpread = 8; - - /// Negative counts, including the extremes an argument check that reasoned on magnitude would let through. - private static Gen NegativeCount() { - return Generators.WithEdges(Gen.Choose(-30, -1), int.MinValue, int.MinValue + 1, -1); - } - - /// The pool 1..size — the same domain as Any.Int32().Between(1, size), held as an explicit set of values. - private static int[] Pool(int size) { - return Enumerable.Range(1, size).ToArray(); - } - - /// - /// Requires each of in turn, so a property can quantify over how many values - /// a collection is required to contain rather than pinning that number in the test. - /// - private static AnyList RequiringAll(AnyList generator, int[] values) { - AnyList required = generator; - foreach (int value in values) { required = required.Containing(value); } - - return required; - } - - #endregion - - [Fact(DisplayName = "WithMaxCount only caps: it never widens the draw beyond the unconstrained spread.")] - public void WithMaxCountNeverWidensTheDraw() { - // ADR-0050: a maximum is a permission, not a size hint. It composes with the default spread instead of - // replacing it, so a loose cap must keep yielding the small unconstrained collection — which matters more - // here than for strings, since every extra element is itself a generated value. - Prop.ForAll(Generators.WithEdges(Generators.Count(200), 0, 1, DefaultCountSpread, DefaultCountSpread + 1, 200).ToArbitrary(), - maximum => Expect.EveryDraw(Any.ListOf(Any.Int32()).WithMaxCount(maximum), list => list.Count <= Math.Min(maximum, DefaultCountSpread), DrawsPerShape) - && Expect.EveryDraw(Any.ArrayOf(Any.Int32()).WithMaxCount(maximum), array => array.Length <= Math.Min(maximum, DefaultCountSpread), DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithMaxCount(maximum), map => map.Count <= Math.Min(maximum, DefaultCountSpread), DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithCount fixes the size exactly, for every count and every collection shape.")] - public void WithCountFixesTheSize() { - Prop.ForAll(Generators.Count(30).ToArbitrary(), - count => Expect.EveryDraw(Any.ListOf(Any.Int32()).WithCount(count), list => list.Count == count, DrawsPerShape) - && Expect.EveryDraw(Any.ArrayOf(Any.Int32()).WithCount(count), array => array.Length == count, DrawsPerShape) - && Expect.EveryDraw(Any.SequenceOf(Any.Int32()).WithCount(count), sequence => sequence.Count() == count, DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(Any.Int32()).WithCount(count), set => set.Count == count, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithCount(count), map => map.Count == count, DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithMinCount floors the size, for every minimum.")] - public void WithMinCountFloorsTheSize() { - Prop.ForAll(Generators.Count(30).ToArbitrary(), - minimum => Expect.EveryDraw(Any.ListOf(Any.Int32()).WithMinCount(minimum), list => list.Count >= minimum, DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(Any.Int32()).WithMinCount(minimum), set => set.Count >= minimum, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithMinCount(minimum), map => map.Count >= minimum, DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithMaxCount caps the size, for every maximum — including zero.")] - public void WithMaxCountCapsTheSize() { - Prop.ForAll(Generators.Count(30).ToArbitrary(), - maximum => Expect.EveryDraw(Any.ArrayOf(Any.Int32()).WithMaxCount(maximum), array => array.Length <= maximum, DrawsPerShape) - && Expect.EveryDraw(Any.SequenceOf(Any.Int32()).WithMaxCount(maximum), sequence => sequence.Count() <= maximum, DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(Any.Int32()).WithMaxCount(maximum), set => set.Count <= maximum, DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithCountBetween keeps the size within its inclusive bounds, for every ordered pair.")] - public void WithCountBetweenStaysWithinItsBounds() { - // Degenerate pairs (min == max) are deliberately kept: a range that pins the count is the corner where a - // range resolved as half-open would show up as an off-by-one. - Prop.ForAll(Generators.OrderedPair(Generators.Count(30)).ToArbitrary(), - bounds => Expect.EveryDraw(Any.ListOf(Any.Int32()).WithCountBetween(bounds.Min, bounds.Max), - list => list.Count >= bounds.Min && list.Count <= bounds.Max, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithCountBetween(bounds.Min, bounds.Max), - map => map.Count >= bounds.Min && map.Count <= bounds.Max, DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Crossed WithCountBetween arguments are an argument error, never a silent swap.")] - public void CrossedCountBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Generators.Count(30)).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.ListOf(Any.Int32()).WithCountBetween(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A negative count is rejected as an argument error by every count method.")] - public void NegativeCountsAreAnArgumentError() { - // Argument validation precedes conflict checking, so these must be ArgumentOutOfRangeException whatever else - // the generator already carries — a negative count is never a "contradiction with a declared constraint". - Prop.ForAll(NegativeCount().ToArbitrary(), - count => Expect.Throws(() => Any.ListOf(Any.Int32()).WithCount(count)) - && Expect.Throws(() => Any.SetOf(Any.Int32()).WithMinCount(count)) - && Expect.Throws(() => Any.ArrayOf(Any.Int32()).WithMaxCount(count)) - && Expect.Throws(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).WithCountBetween(count, 0))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Empty always yields nothing and NonEmpty never does, whatever the element domain.")] - public void EmptyAndNonEmptyHoldOverEveryElementDomain() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => { - // A pinned element domain (Min == Max) is the corner that matters here: NonEmpty() must still - // resolve a count a distinct collection can fill, which for a single-value domain is exactly - // one element — not a conflict, and not an empty draw. - AnyInt32 element = Any.Int32().Between(bounds.Min, bounds.Max); - - return Expect.EveryDraw(Any.ListOf(element).Empty(), list => list.Count == 0, DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(element).Empty(), set => set.Count == 0, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(element, Any.Int32()).Empty(), map => map.Count == 0, DrawsPerShape) - && Expect.EveryDraw(Any.ListOf(element).NonEmpty(), list => list.Count > 0, DrawsPerShape) - && Expect.EveryDraw(Any.ArrayOf(element).NonEmpty(), array => array.Length > 0, DrawsPerShape) - && Expect.EveryDraw(Any.SequenceOf(element).NonEmpty(), sequence => sequence.Any(), DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(element).NonEmpty(), set => set.Count > 0, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(element, Any.Int32()).NonEmpty(), map => map.Count > 0, DrawsPerShape); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Containing places the value and leaves the count untouched, for every value.")] - public void ContainingPlacesTheValue() { - Prop.ForAll((from value in Generators.Int32() - from count in Gen.Choose(1, 12) - select (value, count)).ToArbitrary(), - testCase => Expect.EveryDraw(Any.ListOf(Any.Int32()).WithCount(testCase.count).Containing(testCase.value), - list => list.Count == testCase.count && list.Contains(testCase.value), DrawsPerShape) - && Expect.EveryDraw(Any.ArrayOf(Any.Int32()).WithCount(testCase.count).Containing(testCase.value), - array => array.Length == testCase.count && array.Contains(testCase.value), DrawsPerShape) - && Expect.EveryDraw(Any.SequenceOf(Any.Int32()).WithCount(testCase.count).Containing(testCase.value), - sequence => sequence.Count() == testCase.count && sequence.Contains(testCase.value), DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(Any.Int32()).WithCount(testCase.count).Containing(testCase.value), - set => set.Count == testCase.count && set.Contains(testCase.value), DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "ContainingKey places the key, and ContainingEntry pins exactly that mapping.")] - public void DictionaryContainmentPlacesTheKeyAndPinsTheEntry() { - Prop.ForAll((from key in Generators.Int32() - from value in Generators.Int32() - from count in Gen.Choose(1, 12) - select (key, value, count)).ToArbitrary(), - testCase => Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithCount(testCase.count).ContainingKey(testCase.key), - map => map.Count == testCase.count && map.ContainsKey(testCase.key), DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(Any.Int32(), Any.Int32()).WithCount(testCase.count).ContainingEntry(testCase.key, testCase.value), - map => map.Count == testCase.count - && map.ContainsKey(testCase.key) - && map[testCase.key] == testCase.value, DrawsPerShape)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Distinct yields no duplicate and still honours the count, for every count the domain can hold.")] - public void DistinctYieldsNoDuplicateAndHonoursTheCount() { - Prop.ForAll((from count in Generators.Count(24) - from slack in Generators.Count(16) - select (count, slack)).ToArbitrary(), - testCase => { - // The domain is at its narrowest exactly one value wider than the request, so the count always - // fits — this property is about the dedup-draw filling it, not about the eager gate below. The - // tightest fits are where a fill that gave up early, or one that let a duplicate through, shows. - AnyInt32 element = Any.Int32().Between(1, testCase.count + testCase.slack + 1); - - return Expect.EveryDraw(Any.ListOf(element).WithCount(testCase.count).Distinct(), - list => list.Count == testCase.count && new HashSet(list).Count == testCase.count, DrawsPerShape) - && Expect.EveryDraw(Any.ArrayOf(element).WithCount(testCase.count).Distinct(), - array => array.Length == testCase.count && new HashSet(array).Count == testCase.count, DrawsPerShape) - && Expect.EveryDraw(Any.SequenceOf(element).WithCount(testCase.count).Distinct(), - sequence => sequence.Count() == testCase.count && new HashSet(sequence).Count == testCase.count, DrawsPerShape); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A set and a dictionary are distinct by nature: their size still matches the requested count.")] - public void SetAndDictionaryAreDistinctByNature() { - Prop.ForAll((from count in Generators.Count(24) - from slack in Generators.Count(16) - select (count, slack)).ToArbitrary(), - testCase => { - AnyInt32 element = Any.Int32().Between(1, testCase.count + testCase.slack + 1); - - // A HashSet collapses a repeated element silently and a Dictionary a repeated key, so a size - // equal to the request IS the distinctness assertion: a duplicate could only surface as a - // collection one element short of what was asked for. - return Expect.EveryDraw(Any.SetOf(element).WithCount(testCase.count), - set => set.Count == testCase.count, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(element, Any.Int32()).WithCount(testCase.count), - map => map.Count == testCase.count, DrawsPerShape); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A distinct count beyond the element generator's advertised cardinality conflicts eagerly, and one within it generates.")] - public void DistinctCountBeyondTheAdvertisedCardinalityConflictsEagerly() { - Prop.ForAll((from poolSize in Gen.Choose(1, 8) - from count in Generators.Count(12) - select (poolSize, count)).ToArbitrary(), - testCase => { - // Two generators over the very same domain {1..poolSize}, one bounded and one pooled: both - // advertise a cardinality, so both must decide feasibility at declaration time. Quantifying - // over the pool size AND the requested count walks the whole frontier between the two verdicts - // — an example can only ever stand on one side of it. - AnyInt32 bounded = Any.Int32().Between(1, testCase.poolSize); - AnyOneOf pooled = Any.OneOf(Pool(testCase.poolSize)); - - if (testCase.count > testCase.poolSize) { - return Expect.Throws(() => Any.SetOf(bounded).WithCount(testCase.count)) - && Expect.Throws(() => Any.SetOf(pooled).WithCount(testCase.count)) - && Expect.Throws(() => Any.ListOf(pooled).WithCount(testCase.count).Distinct()) - && Expect.Throws(() => Any.DictionaryOf(bounded, Any.Int32()).WithCount(testCase.count)); - } - - return Expect.EveryDraw(Any.SetOf(bounded).WithCount(testCase.count), - set => set.Count == testCase.count && set.All(value => value >= 1 && value <= testCase.poolSize), DrawsPerShape) - && Expect.EveryDraw(Any.SetOf(pooled).WithCount(testCase.count), - set => set.Count == testCase.count && set.All(value => value >= 1 && value <= testCase.poolSize), DrawsPerShape) - && Expect.EveryDraw(Any.ListOf(pooled).WithCount(testCase.count).Distinct(), - list => list.Count == testCase.count && new HashSet(list).Count == testCase.count, DrawsPerShape) - && Expect.EveryDraw(Any.DictionaryOf(bounded, Any.Int32()).WithCount(testCase.count), - map => map.Count == testCase.count, DrawsPerShape); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "More required values than WithMaxCount allows conflicts; up to that many are all placed.")] - public void RequiredValuesBeyondTheCountCapConflict() { - Gen requiredValues = Gen.NonEmptyListOf(Generators.Int32()).Select(drawn => drawn.Distinct().Take(6).ToArray()); - - Prop.ForAll((from values in requiredValues - from maximum in Generators.Count(8) - select (values, maximum)).ToArbitrary(), - testCase => { - // Each required value takes one element's room, so the verdict is decided by a single - // comparison — and the property holds it over every (how many, how big a cap) pair rather than - // over the one pair an example would pin. - if (testCase.values.Length > testCase.maximum) { - return Expect.Throws( - () => RequiringAll(Any.ListOf(Any.Int32()).WithMaxCount(testCase.maximum), testCase.values)); - } - - return Expect.EveryDraw(RequiringAll(Any.ListOf(Any.Int32()).WithMaxCount(testCase.maximum), testCase.values), - list => list.Count <= testCase.maximum - && testCase.values.All(value => list.Contains(value))); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Under a comparer stricter than the default one, every pinned reference-distinct value extends the domain.")] - public void AStricterComparerLetsEveryPinnedValueExtendTheDomain() { - Gen<(int Pool, int Pinned)> cases = - from pool in Gen.Choose(1, 4) - from pinned in Gen.Choose(1, 4) - select (Pool: pool, Pinned: pinned); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // The pool holds Tag(0)..Tag(pool-1) — distinct by value, so AnyOneOf keeps all of them (it - // deduplicates under the DEFAULT comparer, which is why the pool cannot itself carry - // reference-distinct twins). The pinned values are fresh instances of the SAME values, so - // each is value-equal to a pool member and reference-distinct from it. Under ReferenceComparer - // the effective domain is therefore pool + pinned for every pair — the input space the pinned - // example cannot reach, and the one the eager check got wrong for every pinned >= 1 by - // consulting a membership answered under the default comparer. - Tag[] pooled = Enumerable.Range(0, testCase.Pool).Select(value => new Tag(value)).ToArray(); - Tag[] pinned = Enumerable.Range(0, testCase.Pinned).Select(value => new Tag(value % testCase.Pool)).ToArray(); - - AnyList generator = Any.ListOf(Any.OneOf(pooled)).Distinct(new ReferenceComparer()); - foreach (Tag value in pinned) { generator = generator.Containing(value); } - - List list = generator.WithCount(testCase.Pool + testCase.Pinned).Generate(); - - // Reference-counted on both sides: every pinned value present exactly once, every pooled value - // present exactly once, and nothing else — the collection really did keep the twins apart. - return list.Count == testCase.Pool + testCase.Pinned - && pinned.All(value => list.Count(element => ReferenceEquals(element, value)) == 1) - && pooled.All(value => list.Count(element => ReferenceEquals(element, value)) == 1); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A generated sequence is fully materialized: enumerating it twice yields the same elements.")] - public void SequenceIsFullyMaterialized() { - Prop.ForAll(Generators.Count(30).ToArbitrary(), - count => { - IEnumerable sequence = Any.SequenceOf(Any.Int32()).WithCount(count).Generate(); - - List first = sequence.ToList(); - List second = sequence.ToList(); - - return first.Count == count && first.SequenceEqual(second); - }) - .QuickCheckThrowOnFailure(); - } - - #region Nested types - - // A value-equal reference type: two Tag(1) are one value under the default comparer and two under reference - // equality. That gap between the two comparers is exactly what the eager cardinality check has to respect. - private sealed class Tag { - - private readonly int _value; - - public Tag(int value) { - _value = value; - } - - public override bool Equals(object? obj) { - return obj is Tag tag && tag._value == _value; - } - - public override int GetHashCode() { - return _value; - } - - } - - private sealed class ReferenceComparer : IEqualityComparer { - - public bool Equals(Tag? x, Tag? y) { - return ReferenceEquals(x, y); - } - - public int GetHashCode(Tag obj) { - return RuntimeHelpers.GetHashCode(obj); - } - - } - - #endregion - -} diff --git a/JustDummies.PropertyTests/CompositionProperties.cs b/JustDummies.PropertyTests/CompositionProperties.cs deleted file mode 100644 index 28f62d56..00000000 --- a/JustDummies.PropertyTests/CompositionProperties.cs +++ /dev/null @@ -1,477 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the composition seams — , -/// OrNull(), , PairOf/TripleOf — and for the explicit -/// pools (OneOf, ElementOf). Where the example-based suite pins one hand-picked constraint per part -/// (Between(0, 100), WithLength(12)) and, at the higher arities, passes the same generator to -/// every slot, these quantify over the constraint of each part independently — so a part routed to the wrong slot, -/// a constraint dropped on the way through a seam, or a pool value invented out of nothing is found and shrunk to -/// its minimal counter-example. -/// -[TestSubject(typeof(AnyExtensions))] -public sealed class CompositionProperties { - - #region Statics members declarations - - /// Arbitrary non-empty pools of integers — the explicit domains OneOf and ElementOf draw from. - private static Gen IntegerPools() { - return Gen.NonEmptyListOf(Generators.Int32()).Select(values => values.Take(24).ToArray()); - } - - /// - /// Arbitrary non-empty pools of non-null strings. The values are built from a drawn number rather than taken - /// from FsCheck's own string generator, which yields null — an element the library rejects by design, and - /// a case the dedicated null-element property covers on purpose rather than by accident. - /// - private static Gen StringPools() { - return Gen.NonEmptyListOf(Gen.Choose(0, 20).Select(value => "v" + value)).Select(values => values.Take(24).ToArray()); - } - - /// - /// A copy of carrying a null at (clamped to the pool's - /// length), so the null-element rejection is exercised at every position rather than only at the end. - /// - private static string[] Poisoned(string[] pool, int index) { - List poisoned = [.. pool]; - poisoned.Insert(Math.Min(index, poisoned.Count), null!); - - return poisoned.ToArray(); - } - - /// A generator pinned to a single value — one distinct part per slot, so a mis-routed slot changes the result. - private static IAny Pinned(int value) { - return Any.Int32().Between(value, value); - } - - #endregion - - [Fact(DisplayName = "As projects every draw: the composed value is the factory's image of a value the source constraint allows.")] - public void AsProjectsEveryDrawThroughTheFactory() { - // Doubling is invertible, so the projected value can be mapped back and checked against the source interval — - // which is what "the image of a value satisfying the source constraint" means, stated without naming the draw. - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Int32().Between(bounds.Min, bounds.Max).As(value => (long)value * 2), - projected => projected % 2 == 0 - && projected / 2 >= bounds.Min - && projected / 2 <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "As bridges to a value object: constraints inside the factory's invariant always pass, constraints entirely outside it always fail.")] - public void AsBridgesToAValueObjectFactory() { - Prop.ForAll((from inside in Generators.OrderedPair(Gen.Choose(0, 100)) - from outside in Generators.OrderedPair(Gen.Choose(101, 100_000)) - select (inside, outside)).ToArbitrary(), - testCase => { - bool accepted = Expect.EveryDraw(Any.Int32().Between(testCase.inside.Min, testCase.inside.Max).As(Ratio.Create), - ratio => ratio.Value >= testCase.inside.Min && ratio.Value <= testCase.inside.Max); - - // Constraints weaker than the invariant the factory enforces are the documented cause of a - // generation failure. Entirely outside the window every draw is rejected, so the wrap is - // certain rather than probable — no interval in the quantified space can slip through. - bool rejected = Expect.Throws( - () => Any.Int32().Between(testCase.outside.Min, testCase.outside.Max).As(Ratio.Create).Generate()); - - return accepted && rejected; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A factory that throws surfaces as AnyGenerationException carrying the original failure and the seed that replays it.")] - public void AsWrapsFactoryFailuresPreservingTheCause() { - Prop.ForAll((from bounds in Generators.OrderedPair(Generators.Int32()) - from seed in Generators.Seed() - select (bounds, seed)).ToArbitrary(), - testCase => { - IAny generator = Any.WithSeed(testCase.seed) - .Int32() - .Between(testCase.bounds.Min, testCase.bounds.Max) - .As(_ => throw new FactoryRejection()); - - try { - generator.Generate(); - - return false; - } catch (AnyGenerationException exception) { - return exception.InnerException is FactoryRejection && exception.Seed == testCase.seed; - } - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OrNull on a value type: every non-null draw satisfies the wrapped generator's constraint.")] - public void ValueTypeOrNullKeepsTheWrappedConstraint() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Int32().Between(bounds.Min, bounds.Max).OrNull(), - value => value is null || (value.Value >= bounds.Min && value.Value <= bounds.Max))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OrNull on a reference type: every non-null draw satisfies the wrapped generator's constraint.")] - public void ReferenceTypeOrNullKeepsTheWrappedConstraint() { - Prop.ForAll(Gen.Choose(1, 12).ToArbitrary(), - length => Expect.EveryDraw(Any.String().WithLength(length).OrNull(), - value => value is null || value.Length == length)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OrNull is a coin flip: over enough draws from any seed, both the null and the value branch appear.")] - public void OrNullEventuallyYieldsBothBranches() { - // The null decision is an even coin flip, so 64 draws miss a branch with probability about 2^-63 — vanishing, - // but a probability nonetheless. Drawing from an Any.WithSeed(...) context removes the residual flakiness: each - // FsCheck case is a fixed, replayable run, so a case that passes passes identically on every execution. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext context = Any.WithSeed(seed); - - List values = Expect.Draws(context.Int32().Between(1, 100).OrNull(), 64); - List references = Expect.Draws(context.String().WithLength(4).OrNull(), 64); - - return values.Any(value => value is null) - && values.Any(value => value is not null) - && values.All(value => value is null || (value.Value >= 1 && value.Value <= 100)) - && references.Any(reference => reference is null) - && references.Any(reference => reference is not null) - && references.All(reference => reference is null || reference.Length == 4); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A null draw from OrNull does not consume a value from the wrapped generator.")] - public void OrNullDoesNotConsumeTheWrappedGeneratorOnANullDraw() { - // Counting the wrapped generator's draws is the only way to observe this: the wrapped values themselves cannot - // distinguish "not drawn" from "drawn and discarded". - Prop.ForAll(Gen.Choose(1, 40).ToArbitrary(), - drawCount => { - CountingAny wrapped = new(7); - - List values = Expect.Draws(wrapped.OrNull(), drawCount); - - return wrapped.Draws == values.Count(value => value is not null) - && values.All(value => value is null || value.Value == 7); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Combine of two parts: the composed value carries each part's own constraint.")] - public void CombineOfTwoPartsCarriesBothConstraints() { - Gen<(int Min, int Max)> intervals = Generators.OrderedPair(Generators.Int32()); - - Prop.ForAll((from first in intervals - from second in intervals - select (first, second)).ToArbitrary(), - testCase => Expect.EveryDraw( - Any.Combine(Any.Int32().Between(testCase.first.Min, testCase.first.Max), - Any.Int32().Between(testCase.second.Min, testCase.second.Max), - (one, two) => (Head: one, Tail: two)), - composed => composed.Head >= testCase.first.Min - && composed.Head <= testCase.first.Max - && composed.Tail >= testCase.second.Min - && composed.Tail <= testCase.second.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Combine of three parts routes each constraint to its own slot, across types.")] - public void CombineOfThreePartsCarriesEveryConstraint() { - Gen<(int Min, int Max)> intervals = Generators.OrderedPair(Generators.Int32()); - - Prop.ForAll((from first in intervals - from length in Gen.Choose(1, 12) - from third in intervals - select (first, length, third)).ToArbitrary(), - testCase => Expect.EveryDraw( - Any.Combine(Any.Int32().Between(testCase.first.Min, testCase.first.Max), - Any.String().WithLength(testCase.length), - Any.Int32().Between(testCase.third.Min, testCase.third.Max), - (one, two, three) => (Head: one, Text: two, Tail: three)), - composed => composed.Head >= testCase.first.Min - && composed.Head <= testCase.first.Max - && composed.Text.Length == testCase.length - && composed.Tail >= testCase.third.Min - && composed.Tail <= testCase.third.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Combine at its arity ceiling passes every part to its own slot, in order.")] - public void CombineAtArityEightRoutesEveryPart() { - Gen pins = Generators.Int32(); - - Prop.ForAll((from one in pins - from two in pins - from three in pins - from four in pins - from five in pins - from six in pins - from seven in pins - from eight in pins - select new[] { one, two, three, four, five, six, seven, eight }).ToArbitrary(), - expected => { - // Each of the eight parts is pinned to a value of its own, so two slots swapped change the - // composed array. The example-based suite passes the SAME generator to all eight slots and - // therefore cannot see such a mix-up at all — only the arity itself. - IAny generator = Any.Combine( - Pinned(expected[0]), Pinned(expected[1]), Pinned(expected[2]), Pinned(expected[3]), - Pinned(expected[4]), Pinned(expected[5]), Pinned(expected[6]), Pinned(expected[7]), - (one, two, three, four, five, six, seven, eight) => new[] { one, two, three, four, five, six, seven, eight }); - - return Expect.EveryDraw(generator, parts => parts.SequenceEqual(expected), 4); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "PairOf and TripleOf keep every component within its own constraint.")] - public void PairOfAndTripleOfKeepEveryComponentConstraint() { - Gen<(int Min, int Max)> intervals = Generators.OrderedPair(Generators.Int32()); - - Prop.ForAll((from first in intervals - from length in Gen.Choose(1, 12) - from third in intervals - select (first, length, third)).ToArbitrary(), - testCase => { - AnyInt32 head = Any.Int32().Between(testCase.first.Min, testCase.first.Max); - AnyString text = Any.String().WithLength(testCase.length); - AnyInt32 tail = Any.Int32().Between(testCase.third.Min, testCase.third.Max); - - bool pairs = Expect.EveryDraw(Any.PairOf(head, text), - pair => pair.Item1 >= testCase.first.Min - && pair.Item1 <= testCase.first.Max - && pair.Item2.Length == testCase.length); - - bool triples = Expect.EveryDraw(Any.TripleOf(head, text, tail), - triple => triple.Item1 >= testCase.first.Min - && triple.Item1 <= testCase.first.Max - && triple.Item2.Length == testCase.length - && triple.Item3 >= testCase.third.Min - && triple.Item3 <= testCase.third.Max); - - return pairs && triples; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OneOf and ElementOf draw only from the pool they were given, whatever the pool and whichever overload.")] - public void PoolGeneratorsStayWithinTheirPool() { - Prop.ForAll(IntegerPools().ToArbitrary(), - pool => Expect.EveryDraw(Any.OneOf(pool), value => pool.Contains(value)) - && Expect.EveryDraw(Any.ElementOf((IReadOnlyList)pool), value => pool.Contains(value)) - && Expect.EveryDraw(Any.ElementOf(pool.Select(value => value)), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OneOf reaches exactly the distinct values of its pool: duplicates collapse, nothing is dropped and nothing is invented.")] - public void OneOfReachesExactlyTheDistinctValuesOfItsPool() { - // At most four distinct values over 96 draws: one of them is missed with probability at most 4 x (3/4)^96, - // about 1e-11. A seeded context turns that residual chance into a fixed, replayable run per FsCheck case. - // The pool is drawn from a four-value alphabet on purpose, so duplicates are the common case, not the corner. - Gen pools = Gen.NonEmptyListOf(Gen.Choose(0, 3)).Select(values => values.Take(6).ToArray()); - - Prop.ForAll((from pool in pools - from seed in Generators.Seed() - select (pool, seed)).ToArbitrary(), - testCase => { - HashSet distinct = [.. testCase.pool]; - HashSet drawn = [.. Expect.Draws(Any.WithSeed(testCase.seed).OneOf(testCase.pool), 96)]; - - return drawn.SetEquals(distinct); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Any.String().OneOf draws only from its value set, whatever the set and whichever overload.")] - public void StringOneOfStaysWithinItsValueSet() { - Prop.ForAll(StringPools().ToArbitrary(), - pool => Expect.EveryDraw(Any.String().OneOf(pool), value => pool.Contains(value)) - && Expect.EveryDraw(Any.String().OneOf(pool.Select(value => value)), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Excluding part of a pool leaves exactly the complement; excluding all of it conflicts.")] - public void ExcludingAPoolLeavesItsComplement() { - Gen<(string[] Pool, string[] Excluded)> cases = - from pool in StringPools() - from taken in Gen.Choose(0, 24) - select (Pool: pool, Excluded: pool.Distinct().Take(taken).ToArray()); - - Prop.ForAll(cases.ToArbitrary(), - // The verdict follows the values: the generator survives exactly when some pooled value escapes - // the exclusion, and it then draws from the complement and nothing else. An exclusion carrying - // no value at all is an argument error, not a domain question, so it is left to the example suite. - testCase => { - if (testCase.Excluded.Length == 0) { return true; } - - string[] surviving = testCase.Pool.Distinct().Except(testCase.Excluded).ToArray(); - - return surviving.Length == 0 - ? Expect.Throws(() => Any.OneOf(testCase.Pool).Except(testCase.Excluded)) - : Expect.EveryDraw(Any.OneOf(testCase.Pool).Except(testCase.Excluded), value => surviving.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "ElementOf materializes its sequence once, however many values are drawn from it.")] - public void ElementOfMaterializesItsSequenceOnce() { - Prop.ForAll((from pool in IntegerPools() - from drawCount in Gen.Choose(1, 40) - select (pool, drawCount)).ToArbitrary(), - testCase => { - int enumerations = 0; - - IEnumerable LazyPool() { - enumerations++; - foreach (int value in testCase.pool) { - yield return value; - } - } - - AnyOneOf generator = Any.ElementOf(LazyPool()); - List drawn = Expect.Draws(generator, testCase.drawCount); - - // One enumeration at construction, none per draw: a lazy query re-run per draw would both cost - // and, for a non-deterministic source, silently change the pool between two values. - return enumerations == 1 && drawn.All(value => testCase.pool.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A null anywhere in the pool is an argument error, at every position and on every pool entry point.")] - public void ANullPoolElementIsAnArgumentError() { - Prop.ForAll((from pool in StringPools() - from index in Gen.Choose(0, 24) - select (pool, index)).ToArbitrary(), - testCase => { - string[] poisoned = Poisoned(testCase.pool, testCase.index); - - return Expect.Throws(() => Any.OneOf(poisoned)) - && Expect.Throws(() => Any.ElementOf((IReadOnlyList)poisoned)) - && Expect.Throws(() => Any.ElementOf(poisoned.Select(value => value))) - && Expect.Throws(() => Any.String().OneOf(poisoned)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An absent pool is a null-argument error and an empty one an argument error, on the ambient and seeded entry points alike.")] - public void AbsentAndEmptyPoolsAreArgumentErrors() { - // There is nothing to quantify inside the pool — it is absent or empty by definition — so the quantification - // runs over the context instead: Any and Any.WithSeed(...) mirror the same surface and must reject identically. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext context = Any.WithSeed(seed); - - return Expect.Throws(() => Any.OneOf((string[])null!)) - && Expect.Throws(() => Any.ElementOf((IReadOnlyList)null!)) - && Expect.Throws(() => Any.ElementOf((IEnumerable)null!)) - && Expect.Throws(() => Any.String().OneOf((string[])null!)) - && Expect.Throws(() => context.OneOf((string[])null!)) - && Expect.Throws(() => Any.OneOf()) - && Expect.Throws(() => Any.ElementOf(new List())) - && Expect.Throws(() => Any.ElementOf(Enumerable.Empty())) - && Expect.Throws(() => Any.String().OneOf()) - && Expect.Throws(() => context.ElementOf(new List())); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "The composition seams reject a null generator or a null lambda, whatever the source constraint.")] - public void CompositionSeamsRejectNullArguments() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => { - AnyInt32 part = Any.Int32().Between(bounds.Min, bounds.Max); - - return Expect.Throws(() => part.As(null!)) - && Expect.Throws(() => AnyExtensions.As(null!, (int value) => value)) - && Expect.Throws(() => ((IAny)null!).OrNull()) - && Expect.Throws(() => ((IAny)null!).OrNull()) - && Expect.Throws(() => Any.Combine(null!, part, (int one, int two) => one + two)) - && Expect.Throws(() => Any.Combine(part, null!, (int one, int two) => one + two)) - && Expect.Throws(() => Any.Combine(part, part, (Func)null!)) - && Expect.Throws(() => Any.PairOf(part, (IAny)null!)) - && Expect.Throws(() => Any.TripleOf(part, (IAny)null!, part)) - && Expect.Throws( - () => Any.Combine(part, part, part, part, part, part, part, (IAny)null!, - (int one, int two, int three, int four, int five, int six, int seven, int eight) => one)); - }) - .QuickCheckThrowOnFailure(); - } - - #region Nested types - - /// - /// A minimal value object whose factory enforces an invariant — the shape As exists to bridge to, and the - /// one that tells a well-constrained source from a source weaker than the invariant. - /// - private sealed class Ratio { - - #region Statics members declarations - - internal static Ratio Create(int value) { - if (value is < 0 or > 100) { throw new ArgumentOutOfRangeException(nameof(value)); } - - return new Ratio(value); - } - - #endregion - - private Ratio(int value) { - Value = value; - } - - internal int Value { get; } - - } - - /// The failure a factory raises, distinguishable from anything the library itself could throw. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3871:Exception types should be \"public\"", - Justification = - "A fixture, not part of any contract. It exists so a test factory can raise a failure distinguishable from " + - "anything the library itself throws; making it public would export a type from a test assembly for no reader.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3376:Attribute, EventArgs, and Exception type names should end with the type being extended", - Justification = - "Named for what it reads as at the throw site inside the property. The Exception suffix would say nothing the " + - "base type does not, and this type is private to one test class.")] - private sealed class FactoryRejection : Exception { - - internal FactoryRejection() : base("The factory rejected the generated value.") { } - - } - - /// - /// A generator counting how many times it is asked for a value. Foreign on purpose — it implements - /// only — which is exactly what makes the count observable from outside the library. - /// - /// The type of the generated values. - private sealed class CountingAny : IAny { - - #region Fields declarations - - private readonly T _value; - - #endregion - - internal CountingAny(T value) { - _value = value; - } - - internal int Draws { get; private set; } - - /// - public T Generate() { - Draws++; - - return _value; - } - - } - - #endregion - -} diff --git a/JustDummies.PropertyTests/ConflictMessageTruthfulnessProperties.cs b/JustDummies.PropertyTests/ConflictMessageTruthfulnessProperties.cs deleted file mode 100644 index 6d696433..00000000 --- a/JustDummies.PropertyTests/ConflictMessageTruthfulnessProperties.cs +++ /dev/null @@ -1,242 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// The one property in this suite that reads a conflict message rather than only its exception type. The -/// rule "assert exception types, never message text" (ADR-0040) exists because message wording is unstable; -/// the invariant proven here is not the wording but its truthfulness — for every legal combination of -/// constraints that empties the domain, the emitted message must make only claims that are literally true, must -/// name no falsehood, and must not echo the applied constraint on both sides of "because". That correctness has a -/// genuine input space (every bound / lattice / allow-list / exclusion combination) and no type-level proxy, so it -/// is a property, and the audit that first found the defect (issue #312: an allow-list narrowed by a bound was -/// reported as forbidden "entirely" by the exclusion) becomes a standing guard rather than a one-off check. -/// -/// -/// -/// The oracle recomputes feasibility independently over a small integer universe and rejects any message whose -/// universal claim ("forbids every …", "every value between …") is not backed by that ground truth, any bare -/// allow-list claim used when the allow-list was in fact narrowed by another constraint, and any "Cannot apply -/// X because X forbids …" echo. It is deliberately coupled to the stable claim fragments, not the full prose: -/// a reworded message that stops making a checkable claim would pass, but a message that makes a false -/// claim cannot. This property fails on the pre-fix engines (the audit counted tens of thousands of false -/// messages), which is the falsifiability the suite requires. -/// -/// -/// The engines share one exhaustion path, so a builder per family — ordinal (), decimal, -/// continuous () — proves them all; the 128-bit sibling is checked in -/// ModernTypeInvariantProperties, which the net472 floor leg excludes. -/// -/// -[TestSubject(typeof(ConflictingAnyConstraintException))] -public sealed class ConflictMessageTruthfulnessProperties { - - #region Statics members declarations - - /// Small, exact-in-every-numeric-type values; some (10) fall outside the generated Between window on purpose, to drive the narrowed-allow-list case. - internal static readonly int[] Universe = [0, 1, 2, 3, 5, 10]; - - /// Builds an engine's chain in the order Between?, MultipleOf?, OneOf?, Except?; returns the conflict message, or null when the chain is satisfiable. - internal delegate string? EngineBuilder(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl); - - /// Runs the truthfulness property against one engine, quantifying over the constraint combinations it supports. - internal static void CheckEngine(EngineBuilder build, bool supportsLattice) { - Gen<(bool HasBetween, int Lo, int Hi, int Step, int[] Allow, int[] Excl)> combos = - from hasBetween in Gen.Elements(true, false) - from a in Gen.Choose(0, 4) - from b in Gen.Choose(0, 4) - from step in Gen.Choose(1, supportsLattice ? 3 : 1) - from allowMask in Gen.Choose(0, (1 << Universe.Length) - 1) - from exclMask in Gen.Choose(0, (1 << Universe.Length) - 1) - select (hasBetween, Math.Min(a, b), Math.Max(a, b), step, Subset(allowMask), Subset(exclMask)); - - Prop.ForAll(combos.ToArbitrary(), - combo => { - string? message = build(combo.HasBetween, combo.Lo, combo.Hi, combo.Step, combo.Allow, combo.Excl); - if (message is null) { return true; } - - (int step, int[] allow, int[] excl) = InEffect(combo.HasBetween, combo.Lo, combo.Hi, combo.Step, combo.Allow, combo.Excl); - - return MessageIsTruthful(message, combo.HasBetween, combo.Lo, combo.Hi, step, allow, excl); - }) - .QuickCheckThrowOnFailure(); - } - - private static int[] Subset(int mask) { - List chosen = []; - for (int i = 0; i < Universe.Length; i++) { - if ((mask & (1 << i)) != 0) { chosen.Add(Universe[i]); } - } - - return chosen.ToArray(); - } - - /// The constraints actually in force at the throw: the prefix of the fixed build order up to the first one that empties the domain. - private static (int Step, int[] Allow, int[] Excl) InEffect(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - int curStep = 1; - int[] curAllow = []; - int[] curExcl = []; - - if (Feasible(hasBetween, lo, hi, curStep, curAllow, curExcl).Count == 0) { return (curStep, curAllow, curExcl); } - if (step > 1) { curStep = step; if (Feasible(hasBetween, lo, hi, curStep, curAllow, curExcl).Count == 0) { return (curStep, curAllow, curExcl); } } - if (allow.Length > 0) { curAllow = allow; if (Feasible(hasBetween, lo, hi, curStep, curAllow, curExcl).Count == 0) { return (curStep, curAllow, curExcl); } } - if (excl.Length > 0) { curExcl = excl; } - - return (curStep, curAllow, curExcl); - } - - private static List Feasible(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - int rangeLo = hasBetween ? lo : -50; - int rangeHi = hasBetween ? hi : 50; - - List feasible = []; - for (int value = rangeLo; value <= rangeHi; value++) { - if (allow.Length > 0 && !allow.Contains(value)) { continue; } - if (excl.Contains(value)) { continue; } - if (step > 1 && value % step != 0) { continue; } - feasible.Add(value); - } - - return feasible; - } - - /// The oracle: true when every checkable claim the message makes is backed by independently computed ground truth. - private static bool MessageIsTruthful(string message, bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - // A conflict was thrown, so the in-effect domain must genuinely be empty. - if (Feasible(hasBetween, lo, hi, step, allow, excl).Count != 0) { return false; } - - if (!AllowListClaimHolds(message, hasBetween, lo, hi, step, allow, excl)) { return false; } - if (!RangeClaimHolds(message, hasBetween, lo, hi, excl)) { return false; } - if (!LatticeClaimHolds(message, hasBetween, lo, hi, step, excl)) { return false; } - - return MessageReadsWell(message); - } - - /// - /// Allow-list claims. The bare form asserts every allowed value is forbidden; the qualified form asserts only - /// the values the bounds and lattice still permit are forbidden. True when the message makes no such claim. - /// - private static bool AllowListClaimHolds(string message, bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - if (!message.Contains("every value") || !message.Contains("allows") || allow.Length == 0) { return true; } - - int rangeLo = hasBetween ? lo : -50; - int rangeHi = hasBetween ? hi : 50; - int[] reachable = allow.Where(a => a >= rangeLo && a <= rangeHi && (step <= 1 || a % step == 0)).ToArray(); - - bool qualified = message.Contains("that the other constraints leave"); - bool claimTrue = qualified ? reachable.All(a => excl.Contains(a)) : allow.All(a => excl.Contains(a)); - if (!claimTrue) { return false; } - - // The bare form must not be used when a bound or the lattice already dropped an allowed value. - return qualified || reachable.Length == allow.Length; - } - - /// A range claim must hold for every value in the window. True when the message makes no such claim. - private static bool RangeClaimHolds(string message, bool hasBetween, int lo, int hi, int[] excl) { - if (!hasBetween || !message.Contains($"every value between {lo} and {hi}")) { return true; } - - return Enumerable.Range(lo, hi - lo + 1).All(value => excl.Contains(value)); - } - - /// A lattice claim must hold for every on-lattice value in the window. True when the message makes no such claim. - private static bool LatticeClaimHolds(string message, bool hasBetween, int lo, int hi, int step, int[] excl) { - if (!hasBetween || step <= 1 || !message.Contains($"every MultipleOf({step}) value between {lo} and {hi}")) { return true; } - - return Enumerable.Range(lo, hi - lo + 1).Where(value => value % step == 0).All(value => excl.Contains(value)); - } - - /// Comprehensibility: no malformed fragment, and no "Cannot apply X because X forbids …" echo. - private static bool MessageReadsWell(string message) { - if (message.Contains(" ") || message.Contains(" ,") || message.Contains("forbids ,") || message.Contains("forbid ,")) { return false; } - - int because = message.IndexOf(" because ", StringComparison.Ordinal); - if (because < 0 || !message.StartsWith("Cannot apply ", StringComparison.Ordinal)) { return true; } - - int prefix = "Cannot apply ".Length; - string applied = message.Substring(prefix, because - prefix); - string clause = message.Substring(because + " because ".Length); - - return !clause.StartsWith(applied + " forbids", StringComparison.Ordinal) && !clause.StartsWith(applied + " forbid", StringComparison.Ordinal); - } - - #endregion - - [Fact(DisplayName = "Ordinal: every exclusion-caused conflict message makes only true claims, over the whole combination space.")] - public void OrdinalConflictMessagesAreTruthful() { - CheckEngine(BuildInt32, supportsLattice: true); - } - - [Fact(DisplayName = "Decimal: every exclusion-caused conflict message makes only true claims, over the whole combination space.")] - public void DecimalConflictMessagesAreTruthful() { - CheckEngine(BuildDecimal, supportsLattice: false); - } - - [Fact(DisplayName = "Continuous: every exclusion-caused conflict message makes only true claims, over the whole combination space.")] - public void ContinuousConflictMessagesAreTruthful() { - CheckEngine(BuildDouble, supportsLattice: false); - } - - #region Engine builders - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S1854:Unused assignments should be removed", - Justification = - "The assignment is dead and the CALL is not. These builders exist to provoke the declaration-time conflict, " + - "so what matters is that Except() runs; nothing reads the spec afterwards because the verdict is the exception " + - "or its absence. Dropping `spec =` from the last line alone would break the uniform chain that makes the " + - "sequence of constraints readable.")] - private static string? BuildInt32(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - try { - AnyInt32 spec = Any.Int32(); - if (hasBetween) { spec = spec.Between(lo, hi); } - if (step > 1) { spec = spec.MultipleOf(step); } - if (allow.Length > 0) { spec = spec.OneOf(allow); } - if (excl.Length > 0) { spec = spec.Except(excl); } - - return null; - } catch (ConflictingAnyConstraintException exception) { return exception.Message; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S1854:Unused assignments should be removed", - Justification = - "The assignment is dead and the CALL is not. These builders exist to provoke the declaration-time conflict, " + - "so what matters is that Except() runs; nothing reads the spec afterwards because the verdict is the exception " + - "or its absence. Dropping `spec =` from the last line alone would break the uniform chain that makes the " + - "sequence of constraints readable.")] - private static string? BuildDecimal(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - try { - AnyDecimal spec = Any.Decimal(); - if (hasBetween) { spec = spec.Between(lo, hi); } - if (allow.Length > 0) { spec = spec.OneOf(allow.Select(value => (decimal)value).ToArray()); } - if (excl.Length > 0) { spec = spec.Except(excl.Select(value => (decimal)value).ToArray()); } - - return null; - } catch (ConflictingAnyConstraintException exception) { return exception.Message; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S1854:Unused assignments should be removed", - Justification = - "The assignment is dead and the CALL is not. These builders exist to provoke the declaration-time conflict, " + - "so what matters is that Except() runs; nothing reads the spec afterwards because the verdict is the exception " + - "or its absence. Dropping `spec =` from the last line alone would break the uniform chain that makes the " + - "sequence of constraints readable.")] - private static string? BuildDouble(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - try { - AnyDouble spec = Any.Double(); - if (hasBetween) { spec = spec.Between(lo, hi); } - if (allow.Length > 0) { spec = spec.OneOf(allow.Select(value => (double)value).ToArray()); } - if (excl.Length > 0) { spec = spec.Except(excl.Select(value => (double)value).ToArray()); } - - return null; - } catch (ConflictingAnyConstraintException exception) { return exception.Message; } - } - - #endregion - -} diff --git a/JustDummies.PropertyTests/ContinuousIntervalProperties.cs b/JustDummies.PropertyTests/ContinuousIntervalProperties.cs deleted file mode 100644 index 1fc30617..00000000 --- a/JustDummies.PropertyTests/ContinuousIntervalProperties.cs +++ /dev/null @@ -1,433 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the continuous interval algebra — , -/// and . Where the example-based suite pins a couple of hand-picked ranges -/// (Between(1, 2)), these quantify over the whole bound space: the finite ends of each domain, the -/// off-by-one representable neighbours around them, degenerate pinned intervals, and the non-finite arguments the -/// binary floating-point generators must refuse rather than propagate. -/// -/// -/// The three types share one contract but not one engine — double and float ride a binary -/// next-representable ladder, decimal expresses exclusive bounds as an inclusive bound plus a point -/// exclusion — so each invariant is stated once per type rather than once for a representative. The last two -/// properties are the reachability guard for issue #206, generalized: the historical defect was not that a draw -/// left its range but that half of every range was silently unreachable, which only a property over the interval -/// itself, not over a fixed one, can rule out across the constraint space. -/// -[TestSubject(typeof(AnyDouble))] -public sealed class ContinuousIntervalProperties { - - #region Statics members declarations - - /// - /// Draws per reachability case: large enough that a uniform sampler covers both halves of a range with - /// overwhelming probability, small enough that a hundred FsCheck cases stay cheap. - /// - private const int ReachabilityDrawCount = 300; - - /// The magnitude an arbitrary number stays within unless the declared bounds leave no room (ADR-0052). - private const double OrdinaryMagnitude = 1_000_000d; - - /// - /// Finiteness, spelled the way the .NET Framework 4.7.2 floor leg understands: double.IsFinite arrived - /// with .NET Core 3.0, and this suite is built against the support floor too. - /// - private static bool IsFinite(double value) { - return !double.IsNaN(value) && !double.IsInfinity(value); - } - - /// - /// The interval a generator actually draws from: the declared one clipped to the ordinary magnitude window, - /// or the declared one untouched when that clip would leave nothing (ADR-0052). - /// - /// - /// Mirrored here so the two reachability properties can name the range they expect covered. The split of - /// responsibility is deliberate: those properties own the sampler covers the range it draws from — - /// issue #206 was a bit-level defect in assembling the fraction, magnitude-independent, and a fraction stuck - /// below one half still fails them with this helper in place. That the range is the right one is owned - /// by the windowing properties, which assert it against the API rather than against a mirror. - /// - private static (double Min, double Max) DrawnFrom(double min, double max) { - double lower = Math.Max(min, -OrdinaryMagnitude); - double upper = Math.Min(max, OrdinaryMagnitude); - - return lower > upper ? (min, max) : (lower, upper); - } - - /// The counterpart of . - private static (decimal Min, decimal Max) DrawnFrom(decimal min, decimal max) { - decimal lower = Math.Max(min, -(decimal)OrdinaryMagnitude); - decimal upper = Math.Min(max, (decimal)OrdinaryMagnitude); - - return lower > upper ? (min, max) : (lower, upper); - } - - /// Arbitrary finite s — the Generators.Double() recipe on the narrow type. - private static Gen Singles() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor().Where(value => !float.IsNaN(value) && !float.IsInfinity(value)), - float.MinValue, -1f, 0f, 1f, float.MaxValue); - } - - /// - /// Arbitrary s of moderate magnitude, with a few decimal places. Used where a constraint - /// adds a point exclusion (GreaterThan, LessThan): has no - /// next-representable ladder, so the engine steps a colliding draw by 1E-28 — an increment that vanishes - /// in rounding near and fails the generation loudly. That documented - /// extreme-magnitude behaviour is not the invariant under test, so the bounds stay well inside it. - /// - private static Gen ModerateDecimals() { - return Gen.Choose(-1_000_000_000, 1_000_000_000).Select(value => value / 1000m); - } - - /// The three values a floating-point generator must refuse as a bound instead of quietly carrying. - private static Gen NonFiniteDoubles() { - return Gen.Elements(double.NaN, double.PositiveInfinity, double.NegativeInfinity); - } - - /// The counterpart of . - private static Gen NonFiniteSingles() { - return Gen.Elements(float.NaN, float.PositiveInfinity, float.NegativeInfinity); - } - - /// - /// Seeded, comfortably wide intervals at three magnitudes. Deliberately kept away from the - /// domain edges: reachability asks whether the sampler covers a range, and a midpoint taken over the full domain - /// cannot be formed without the arithmetic itself becoming the subject. - /// - private static Gen<(int Seed, double Min, double Max)> DoubleIntervals() { - return from seed in Generators.Seed() - from low in Gen.Choose(-1_000_000, 1_000_000) - from width in Gen.Choose(1, 1_000_000) - from unit in Gen.Elements(0.0001d, 1d, 1000d) - select (Seed: seed, Min: low * unit, Max: (low + width) * unit); - } - - /// The counterpart of . - private static Gen<(int Seed, decimal Min, decimal Max)> DecimalIntervals() { - return from seed in Generators.Seed() - from low in Gen.Choose(-1_000_000, 1_000_000) - from width in Gen.Choose(1, 1_000_000) - from unit in Gen.Elements(0.0001m, 1m, 1000m) - select (Seed: seed, Min: low * unit, Max: (low + width) * unit); - } - - #endregion - - [Fact(DisplayName = "Unconstrained double and float draws are finite, whatever the seed.")] - public void UnconstrainedDrawsAreAlwaysFinite() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext any = Any.WithSeed(seed); - - return Expect.EveryDraw(any.Double(), value => !double.IsNaN(value) && !double.IsInfinity(value)) - && Expect.EveryDraw(any.Single(), value => !float.IsNaN(value) && !float.IsInfinity(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: Between contains — every draw falls within the declared inclusive bounds.")] - public void DoubleBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Generators.Double()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Double().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: Between contains — the quantized draw never escapes the bounds it was narrowed to.")] - public void SingleBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Singles()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Single().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: Between contains, across the whole decimal range.")] - public void DecimalBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Generators.Decimal()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Decimal().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: Between with equal bounds pins the value, for every value.")] - public void DoubleBetweenWithEqualBoundsPins() { - Prop.ForAll(Generators.Double().ToArbitrary(), - value => Expect.EveryDraw(Any.Double().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: Between with equal bounds pins the value, for every value.")] - public void SingleBetweenWithEqualBoundsPins() { - Prop.ForAll(Singles().ToArbitrary(), - value => Expect.EveryDraw(Any.Single().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: Between with equal bounds pins the value, for every value.")] - public void DecimalBetweenWithEqualBoundsPins() { - Prop.ForAll(Generators.Decimal().ToArbitrary(), - value => Expect.EveryDraw(Any.Decimal().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: GreaterThanOrEqualTo and LessThanOrEqualTo are inclusive — the bound itself stays legal.")] - public void DoubleInclusiveBoundsAreInclusive() { - Prop.ForAll(Generators.Double().ToArbitrary(), - bound => Expect.EveryDraw(Any.Double().GreaterThanOrEqualTo(bound), value => value >= bound) - && Expect.EveryDraw(Any.Double().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: GreaterThanOrEqualTo and LessThanOrEqualTo are inclusive — the bound itself stays legal.")] - public void SingleInclusiveBoundsAreInclusive() { - Prop.ForAll(Singles().ToArbitrary(), - bound => Expect.EveryDraw(Any.Single().GreaterThanOrEqualTo(bound), value => value >= bound) - && Expect.EveryDraw(Any.Single().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: GreaterThanOrEqualTo and LessThanOrEqualTo are inclusive — the bound itself stays legal.")] - public void DecimalInclusiveBoundsAreInclusive() { - Prop.ForAll(Generators.Decimal().ToArbitrary(), - bound => Expect.EveryDraw(Any.Decimal().GreaterThanOrEqualTo(bound), value => value >= bound) - && Expect.EveryDraw(Any.Decimal().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: GreaterThan and LessThan are strict, and conflict at the finite ends of the domain.")] - public void DoubleStrictBoundsAreStrictAndConflictAtTheDomainEnds() { - Prop.ForAll(Generators.Double().ToArbitrary(), - bound => { - // Nothing representable lies above double.MaxValue or below double.MinValue, so the exclusive - // bound has no value left to name: a conflict at declaration, not an empty draw at generation. - bool above = bound == double.MaxValue - ? Expect.Throws(() => Any.Double().GreaterThan(bound)) - : Expect.EveryDraw(Any.Double().GreaterThan(bound), value => value > bound); - bool below = bound == double.MinValue - ? Expect.Throws(() => Any.Double().LessThan(bound)) - : Expect.EveryDraw(Any.Double().LessThan(bound), value => value < bound); - - return above && below; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: GreaterThan and LessThan are strict on the float ladder, and conflict at its ends.")] - public void SingleStrictBoundsAreStrictAndConflictAtTheDomainEnds() { - Prop.ForAll(Singles().ToArbitrary(), - bound => { - // The step is taken on the float ladder, not the double one: a sub-ulp double step would - // re-quantize onto the same float and stall the strictness this asserts. - bool above = bound == float.MaxValue - ? Expect.Throws(() => Any.Single().GreaterThan(bound)) - : Expect.EveryDraw(Any.Single().GreaterThan(bound), value => value > bound); - bool below = bound == float.MinValue - ? Expect.Throws(() => Any.Single().LessThan(bound)) - : Expect.EveryDraw(Any.Single().LessThan(bound), value => value < bound); - - return above && below; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: GreaterThan and LessThan are strict — the inclusive bound plus a point exclusion.")] - public void DecimalStrictBoundsAreStrict() { - Prop.ForAll(ModerateDecimals().ToArbitrary(), - bound => Expect.EveryDraw(Any.Decimal().GreaterThan(bound), value => value > bound) - && Expect.EveryDraw(Any.Decimal().LessThan(bound), value => value < bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Positive and Negative are strict, Zero pins and NonZero excludes — for all three types, whatever the seed.")] - public void SignConstraintsHoldForEverySeed() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext any = Any.WithSeed(seed); - - return Expect.EveryDraw(any.Double().Positive(), value => value > 0d) - && Expect.EveryDraw(any.Double().Negative(), value => value < 0d) - && Expect.EveryDraw(any.Double().Zero(), value => value == 0d) - && Expect.EveryDraw(any.Double().NonZero(), value => value != 0d) - && Expect.EveryDraw(any.Single().Positive(), value => value > 0f) - && Expect.EveryDraw(any.Single().Negative(), value => value < 0f) - && Expect.EveryDraw(any.Single().Zero(), value => value == 0f) - && Expect.EveryDraw(any.Single().NonZero(), value => value != 0f) - && Expect.EveryDraw(any.Decimal().Positive(), value => value > 0m) - && Expect.EveryDraw(any.Decimal().Negative(), value => value < 0m) - && Expect.EveryDraw(any.Decimal().Zero(), value => value == 0m) - && Expect.EveryDraw(any.Decimal().NonZero(), value => value != 0m); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: NaN and the infinities are rejected as argument errors by every entry point taking a bound.")] - public void DoubleRejectsNonFiniteArguments() { - Prop.ForAll((from finite in Generators.Double() - from nonFinite in NonFiniteDoubles() - select (finite, nonFinite)).ToArbitrary(), - testCase => Expect.Throws(() => Any.Double().GreaterThan(testCase.nonFinite)) - && Expect.Throws(() => Any.Double().GreaterThanOrEqualTo(testCase.nonFinite)) - && Expect.Throws(() => Any.Double().LessThan(testCase.nonFinite)) - && Expect.Throws(() => Any.Double().LessThanOrEqualTo(testCase.nonFinite)) - && Expect.Throws(() => Any.Double().Between(testCase.nonFinite, testCase.finite)) - && Expect.Throws(() => Any.Double().Between(testCase.finite, testCase.nonFinite)) - && Expect.Throws(() => Any.Double().OneOf(testCase.finite, testCase.nonFinite)) - && Expect.Throws(() => Any.Double().Except(testCase.nonFinite)) - && Expect.Throws(() => Any.Double().DifferentFrom(testCase.nonFinite))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: NaN and the infinities are rejected as argument errors by every entry point taking a bound.")] - public void SingleRejectsNonFiniteArguments() { - Prop.ForAll((from finite in Singles() - from nonFinite in NonFiniteSingles() - select (finite, nonFinite)).ToArbitrary(), - testCase => Expect.Throws(() => Any.Single().GreaterThan(testCase.nonFinite)) - && Expect.Throws(() => Any.Single().GreaterThanOrEqualTo(testCase.nonFinite)) - && Expect.Throws(() => Any.Single().LessThan(testCase.nonFinite)) - && Expect.Throws(() => Any.Single().LessThanOrEqualTo(testCase.nonFinite)) - && Expect.Throws(() => Any.Single().Between(testCase.nonFinite, testCase.finite)) - && Expect.Throws(() => Any.Single().Between(testCase.finite, testCase.nonFinite)) - && Expect.Throws(() => Any.Single().OneOf(testCase.finite, testCase.nonFinite)) - && Expect.Throws(() => Any.Single().Except(testCase.nonFinite)) - && Expect.Throws(() => Any.Single().DifferentFrom(testCase.nonFinite))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: crossed Between arguments are an argument error, never a silent swap.")] - public void DoubleCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Generators.Double()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Double().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: crossed Between arguments are an argument error, never a silent swap.")] - public void SingleCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Singles()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Single().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: crossed Between arguments are an argument error, never a silent swap.")] - public void DecimalCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Generators.Decimal()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Decimal().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: OneOf draws only from the supplied pool, whatever the pool.")] - public void DoubleOneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(Generators.Double()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.Double().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Single: OneOf draws only from the supplied pool, whatever the pool.")] - public void SingleOneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(Singles()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.Single().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: OneOf draws only from the supplied pool, whatever the pool.")] - public void DecimalOneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(Generators.Decimal()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.Decimal().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An unconstrained draw is an ordinary number, whatever the seed — arithmetic on it stays finite.")] - public void UnconstrainedDrawsAreOrdinary() { - // ADR-0052. Stated as arithmetic rather than as a magnitude on purpose: what a dummy owes its test is that - // using it does not sabotage the test. Before this, a sixth of Positive() doubles overflowed to Infinity on - // a single multiplication, and the decimal equivalent threw OverflowException. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext any = Any.WithSeed(seed); - - return Expect.EveryDraw(any.Double(), value => IsFinite(value * 1.2d)) - && Expect.EveryDraw(any.Double().Positive(), value => IsFinite(value * 1.2d) && value > 0d) - && Expect.EveryDraw(any.Single(), value => IsFinite(value * 1.2f)) - && Expect.EveryDraw(any.Decimal(), value => Expect.DoesNotThrow(() => _ = value * 1.2m)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An interval that merely permits large values still yields ordinary ones, for every upper bound.")] - public void PermittingALargeValueIsNotRequestingOne() { - // The heart of ADR-0052: a bound is a permission, not a request. Any.Double().LessThan(huge) says what the - // value may not exceed, so widening that bound must not enlarge the draw — exactly as a size maximum does - // not enlarge a string under ADR-0050. - Prop.ForAll(Gen.Elements(1e7d, 1e50d, 1e200d, 1e308d, double.MaxValue).ToArbitrary(), - permitted => Expect.EveryDraw(Any.Double().Between(0d, permitted), value => value <= OrdinaryMagnitude) - && Expect.EveryDraw(Any.Double().LessThan(permitted), value => Math.Abs(value) <= OrdinaryMagnitude)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An interval lying wholly beyond the ordinary window is drawn from as declared, for every such interval.")] - public void AnIntervalBeyondTheWindowIsHonouredAsDeclared() { - // The other half of the rule, and the one that keeps it from being a silent cap: a caller who names a - // magnitude gets that magnitude. Without this the window would not clip the draw, it would break the bound. - Prop.ForAll(Generators.OrderedPair(Gen.Elements(1e7d, 1e20d, 1e100d, 1e250d, 1e307d)).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.EveryDraw(Any.Double().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Decimal: repeated draws over an arbitrary interval straddle its midpoint — neither half is unreachable.")] - public void DecimalBetweenReachesBothHalves() { - // Issue #206, generalized from its fixed 0..100 regression: the fraction was assembled from three - // non-negative Random.Next() draws, so each limb's top bit stayed zero, the fraction never crossed ~0.5, - // and every value of every range landed in its lower half. Membership held throughout — only reachability - // caught it, and only a property over the interval itself proves it for intervals nobody thought to pin. - Prop.ForAll(DecimalIntervals().ToArbitrary(), - interval => { - (decimal Min, decimal Max) drawn = DrawnFrom(interval.Min, interval.Max); - decimal midpoint = drawn.Min / 2m + drawn.Max / 2m; - - List values = Expect.Draws(Any.WithSeed(interval.Seed).Decimal().Between(interval.Min, interval.Max), - ReachabilityDrawCount); - - return values.Min() < midpoint && values.Max() > midpoint; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Double: repeated draws over an arbitrary interval straddle its midpoint — neither half is unreachable.")] - public void DoubleBetweenReachesBothHalves() { - // The binary engine samples as midpoint ± half rather than by interpolation, so it fails differently from - // the decimal one — which is exactly why the guard is stated per engine instead of once for a representative. - Prop.ForAll(DoubleIntervals().ToArbitrary(), - interval => { - (double Min, double Max) drawn = DrawnFrom(interval.Min, interval.Max); - double midpoint = drawn.Min / 2d + drawn.Max / 2d; - - List values = Expect.Draws(Any.WithSeed(interval.Seed).Double().Between(interval.Min, interval.Max), - ReachabilityDrawCount); - - return values.Min() < midpoint && values.Max() > midpoint; - }) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/EnumCombinationProperties.cs b/JustDummies.PropertyTests/EnumCombinationProperties.cs deleted file mode 100644 index c90457a0..00000000 --- a/JustDummies.PropertyTests/EnumCombinationProperties.cs +++ /dev/null @@ -1,128 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for . Where the example-based suite pins -/// the universe a handful of named enum shapes yield, these quantify over the constraints applied on top of -/// it: the allow-list and the exclusion set are drawn from the universe itself, so a draw escaping the universe, an -/// exclusion silently read as a bit mask, or a pool that empties without conflicting is found and shrunk. -/// -/// -/// The universe is fixed per enum type, so it is the constraint sets — not the type — that carry the input space. -/// is used throughout because its four declared members give a universe of eight values: -/// small enough to enumerate in the assertion, wide enough that a subset drawn from it is rarely trivial. -/// -[TestSubject(typeof(AnyEnum<>))] -public sealed class EnumCombinationProperties { - - #region Statics members declarations - - /// Every value AllowingCombinations() must be able to draw for . - private static readonly Permissions[] Universe = Enumerable.Range(0, 8).Select(bits => (Permissions)bits).ToArray(); - - /// - /// A non-empty subset of the universe, in an arbitrary order and possibly with repetitions — an allow-list or - /// an exclusion set as a caller would write it. Repetitions are kept on purpose: Except and - /// OneOf both have to absorb a duplicate without changing the pool they compute. - /// - private static Gen Subsets() { - return Gen.NonEmptyListOf(Gen.Elements(Universe)).Select(values => values.ToArray()); - } - - #endregion - - [Fact(DisplayName = "AllowingCombinations: every draw is a combination of declared members, for every exclusion set.")] - public void EveryDrawStaysInTheUniverse() { - Prop.ForAll(Subsets().ToArbitrary(), - excluded => { - // A subset drawn with repetition can cover the whole universe; only then is a conflict owed, - // so the property branches on the drawn values rather than on the call shape. - Permissions[] distinct = excluded.Distinct().ToArray(); - if (distinct.Length == Universe.Length) { - return Expect.Throws( - () => Any.Enum().AllowingCombinations().Except(excluded)); - } - - return Expect.EveryDraw(Any.Enum().AllowingCombinations().Except(excluded), - value => Universe.Contains(value) && !distinct.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "AllowingCombinations: exclusions compare by equality, never as a bit mask.")] - public void ExclusionsCompareByEquality() { - Prop.ForAll(Gen.Elements(Universe.Where(value => value != 0).ToArray()).ToArbitrary(), - excluded => { - // Every strict superset of the excluded value's bits is a DIFFERENT value, so it must remain - // reachable: reading Except as "no value carrying these bits" would make all of them vanish. - Permissions[] survivors = Universe.Where(value => value != excluded && (value & excluded) == excluded).ToArray(); - if (survivors.Length == 0) { return true; } - - List draws = Expect.Draws(Any.Enum().AllowingCombinations().Except(excluded), 200); - - return draws.All(value => value != excluded) && survivors.Any(draws.Contains); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "AllowingCombinations: an allow-list of combinations is honoured exactly, for every subset.")] - public void AnAllowListOfCombinationsIsHonoured() { - Prop.ForAll(Subsets().ToArbitrary(), - allowed => { - Permissions[] distinct = allowed.Distinct().ToArray(); - - return Expect.EveryDraw(Any.Enum().AllowingCombinations().OneOf(allowed), - distinct.Contains); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "AllowingCombinations: the declared-members default is untouched, for every exclusion set.")] - public void TheDefaultRemainsDeclaredMembersOnly() { - Permissions[] declared = [Permissions.None, Permissions.Read, Permissions.Write, Permissions.Exec]; - - Prop.ForAll(Gen.Choose(0, 2).ToArbitrary(), - size => { - Permissions[] excluded = declared.Take(size).ToArray(); - AnyEnum generator = excluded.Length == 0 - ? Any.Enum() - : Any.Enum().Except(excluded); - - // Without the opt-in no combination is ever drawn, whatever else was declared — the contract - // AllowingCombinations() exists precisely to leave alone. - return Expect.EveryDraw(generator, value => declared.Contains(value) && !excluded.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "AllowingCombinations: two contexts on the same seed draw the same combinations, for every seed.")] - public void CombinationsAreReproducibleForEverySeed() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - List first = Expect.Draws(Any.WithSeed(seed).Enum().AllowingCombinations(), 12); - List second = Expect.Draws(Any.WithSeed(seed).Enum().AllowingCombinations(), 12); - - return first.SequenceEqual(second); - }) - .QuickCheckThrowOnFailure(); - } - - [Flags] - private enum Permissions { - - None = 0, - Read = 1, - Write = 2, - Exec = 4 - - } - -} diff --git a/JustDummies.PropertyTests/Int32IntervalProperties.cs b/JustDummies.PropertyTests/Int32IntervalProperties.cs deleted file mode 100644 index 33495a0f..00000000 --- a/JustDummies.PropertyTests/Int32IntervalProperties.cs +++ /dev/null @@ -1,130 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for 's interval algebra. Where the example-based suite pins a few -/// hand-picked intervals, these quantify over the whole bound space — [int.MinValue, int.MaxValue], -/// degenerate intervals, and the off-by-one edges around them — so a bound that overflows or truncates for one -/// interval in a million is found and shrunk to its minimal counter-example rather than missed. -/// -[TestSubject(typeof(AnyInt32))] -public sealed class Int32IntervalProperties { - - [Fact(DisplayName = "Between contains: every draw falls within the declared inclusive bounds.")] - public void BetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Int32().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Between with equal bounds pins the value, for every value.")] - public void BetweenWithEqualBoundsPins() { - Prop.ForAll(Generators.Int32().ToArbitrary(), - value => Expect.EveryDraw(Any.Int32().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "GreaterThanOrEqualTo is inclusive: every draw is at least the bound.")] - public void GreaterThanOrEqualToIsInclusive() { - Prop.ForAll(Generators.Int32().ToArbitrary(), - bound => Expect.EveryDraw(Any.Int32().GreaterThanOrEqualTo(bound), value => value >= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "LessThanOrEqualTo is inclusive: every draw is at most the bound.")] - public void LessThanOrEqualToIsInclusive() { - Prop.ForAll(Generators.Int32().ToArbitrary(), - bound => Expect.EveryDraw(Any.Int32().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "GreaterThan is strict below int.MaxValue, and conflicts at it.")] - public void GreaterThanIsStrictAndConflictsAtTheCeiling() { - Prop.ForAll(Generators.Int32().ToArbitrary(), - bound => bound == int.MaxValue - ? Expect.Throws(() => Any.Int32().GreaterThan(bound)) - : Expect.EveryDraw(Any.Int32().GreaterThan(bound), value => value > bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "LessThan is strict above int.MinValue, and conflicts at it.")] - public void LessThanIsStrictAndConflictsAtTheFloor() { - Prop.ForAll(Generators.Int32().ToArbitrary(), - bound => bound == int.MinValue - ? Expect.Throws(() => Any.Int32().LessThan(bound)) - : Expect.EveryDraw(Any.Int32().LessThan(bound), value => value < bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Between never yields a value excluded by a subsequent Except.")] - public void ExceptRemovesTheValueFromTheInterval() { - Gen<(int Min, int Max)> intervals = Generators.OrderedPair(Generators.Count(40)); - - Prop.ForAll((from bounds in intervals - from excluded in Gen.Choose(bounds.Min, bounds.Max) - select (bounds, excluded)).ToArbitrary(), - testCase => { - // Excluding the single value of a pinned interval empties it: that is a conflict, not a draw. - if (testCase.bounds.Min == testCase.bounds.Max) { - return Expect.Throws( - () => Any.Int32().Between(testCase.bounds.Min, testCase.bounds.Max).Except(testCase.excluded)); - } - - return Expect.EveryDraw(Any.Int32().Between(testCase.bounds.Min, testCase.bounds.Max).Except(testCase.excluded), - value => value != testCase.excluded - && value >= testCase.bounds.Min - && value <= testCase.bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OneOf draws only from the supplied pool, whatever the pool.")] - public void OneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(Generators.Int32()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.Int32().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Crossed Between arguments are an argument error, never a silent swap.")] - public void CrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Int32().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Bounds that cannot both hold conflict, for every crossed pair.")] - public void ImpossibleBoundPairsConflict() { - Prop.ForAll(Generators.OrderedPair(Generators.Int32()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws( - () => Any.Int32().GreaterThan(bounds.Max).LessThan(bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A generator is an immutable recipe: constraining it never narrows the original.")] - public void ConstrainingNeverMutatesTheOriginal() { - Prop.ForAll(Generators.OrderedPair(Generators.Count(60)).ToArbitrary(), - bounds => { - AnyInt32 original = Any.Int32().Between(bounds.Min, bounds.Max); - AnyInt32 narrowed = original.GreaterThanOrEqualTo(bounds.Max); - - return !ReferenceEquals(original, narrowed) - && Expect.EveryDraw(original, value => value >= bounds.Min && value <= bounds.Max) - && Expect.EveryDraw(narrowed, value => value == bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/JustDummies.PropertyTests.csproj b/JustDummies.PropertyTests/JustDummies.PropertyTests.csproj deleted file mode 100644 index 711864e8..00000000 --- a/JustDummies.PropertyTests/JustDummies.PropertyTests.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - enable - enable - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - diff --git a/JustDummies.PropertyTests/LatticeConstraintProperties.cs b/JustDummies.PropertyTests/LatticeConstraintProperties.cs deleted file mode 100644 index 085765e5..00000000 --- a/JustDummies.PropertyTests/LatticeConstraintProperties.cs +++ /dev/null @@ -1,352 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the three lattice constraints — MultipleOf on the integers, -/// on , and WithGranularity on the temporals. -/// Where the example-based suite pins a handful of hand-picked steps (MultipleOf(100), -/// WithScale(2), WithGranularity(15 minutes)) and can only prove the grid right for those, these -/// quantify over the whole step space: the step, the interval it must live inside, and the allow-list it must -/// intersect are all drawn by FsCheck, so a grid that drifts off its anchor, empties without saying so, or -/// silently escapes its declared range is found and shrunk to its minimal counter-example. -/// -/// -/// Two rules shape almost every property here. A lattice is declared once, but the second declaration is -/// only a conflict when it really is a second lattice — a step of one (and a granularity of one tick) is a -/// no-op, and re-declaring the same step is idempotent — so the properties branch on the drawn value rather -/// than assuming the call shape decides. And WithScale is a value lattice, not a representation -/// contract: the drawn value lies on the 10^-scale grid but is not padded with trailing zeros, so it is -/// checked with Math.Round(value, scale) and never through decimal.GetBits or ToString(). -/// -[TestSubject(typeof(AnyDecimal))] -public sealed class LatticeConstraintProperties { - - #region Statics members declarations - - /// - /// A strictly positive lattice step, kept modest so the constrained domain never thins out to nothing and - /// the draw stays cheap — the invariant under test is about the grid, not about arithmetic at 2^31. - /// - private static Gen Steps() { - return Gen.Choose(1, 1000); - } - - /// - /// A strictly positive granularity: fine tick-level steps mixed with the round durations real code asks for. - /// All stay far below the width of every temporal domain, so the lattice is never empty on its own. - /// - private static Gen Granularities() { - TimeSpan[] realistic = [ - TimeSpan.FromTicks(1), TimeSpan.FromMilliseconds(1), TimeSpan.FromSeconds(1), - TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15), TimeSpan.FromHours(1), TimeSpan.FromDays(1) - ]; - - return Gen.OneOf(Gen.Choose(1, 10000).Select(ticks => TimeSpan.FromTicks(ticks)), Gen.Elements(realistic)); - } - - /// - /// A granularity drawn from a deliberately tiny pool, so two independent draws collide often enough to - /// exercise the idempotent re-declaration alongside the conflicting one. - /// - private static Gen CollidingGranularities() { - TimeSpan[] pool = [TimeSpan.FromTicks(1), TimeSpan.FromMilliseconds(1), TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(1)]; - - return Gen.Elements(pool); - } - - /// - /// Whether the inclusive interval [, ] holds at least - /// one multiple of . The oracle strides down from the top of the interval in integer - /// arithmetic rather than reusing the library's own lattice walk, so it cannot inherit the very off-by-one it - /// is meant to catch. - /// - private static bool ContainsMultiple(int minimum, int maximum, int step) { - int remainder = maximum % step; // C# gives the remainder the sign of the dividend - int largest = maximum - (remainder < 0 ? remainder + step : remainder); // the largest multiple at or below the maximum - - return largest >= minimum; - } - - /// - /// One step of the 10^-scale grid, built by exact division so the oracle stays - /// free of the binary rounding a double power would smuggle in. - /// - private static decimal GridStep(int scale) { - decimal step = 1m; - for (int i = 0; i < scale; i++) { step /= 10m; } - - return step; - } - - #endregion - - [Fact(DisplayName = "MultipleOf: every draw of every integer width lands on the grid, for every step.")] - public void MultipleOfLandsOnTheGrid() { - Prop.ForAll((from step in Steps() - from narrowStep in Gen.Choose(1, byte.MaxValue) - select (step, narrowStep)).ToArbitrary(), - testCase => { - int signedStep = testCase.step; - uint unsignedStep = (uint)testCase.step; - byte byteStep = (byte)testCase.narrowStep; - - // The signed widths and the unsigned ones map onto the shared ordinal engine differently; - // the grid is anchored at zero for all of them, so the invariant reads the same everywhere. - return Expect.EveryDraw(Any.Int32().MultipleOf(signedStep), value => value % signedStep == 0) - && Expect.EveryDraw(Any.Int64().MultipleOf(signedStep), value => value % signedStep == 0) - && Expect.EveryDraw(Any.UInt32().MultipleOf(unsignedStep), value => value % unsignedStep == 0) - && Expect.EveryDraw(Any.Byte().MultipleOf(byteStep), value => value % byteStep == 0); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "MultipleOf: an interval keeps the grid inside it, and an interval holding no grid point conflicts.")] - public void MultipleOfComposesWithAnInterval() { - Prop.ForAll((from bounds in Generators.OrderedPair(Gen.Choose(-2000, 2000)) - from step in Steps() - select (bounds, step)).ToArbitrary(), - testCase => { - int minimum = testCase.bounds.Min; - int maximum = testCase.bounds.Max; - int gridStep = testCase.step; - - // An interval narrower than the step can fall entirely between two grid points — the whole - // point of drawing the bounds and the step independently. The lattice is then empty, and the - // library owes the caller a conflict at the fluent call, not a failure at Generate(). - if (!ContainsMultiple(minimum, maximum, gridStep)) { - return Expect.Throws(() => Any.Int32().Between(minimum, maximum).MultipleOf(gridStep)); - } - - return Expect.EveryDraw(Any.Int32().Between(minimum, maximum).MultipleOf(gridStep), - value => value % gridStep == 0 && value >= minimum && value <= maximum); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "MultipleOf: an allow-list is filtered to its grid points, and an allow-list missing them conflicts.")] - public void MultipleOfFiltersAnAllowList() { - Gen pools = Gen.NonEmptyListOf(Gen.Choose(-200, 200)).Select(values => values.Distinct().ToArray()); - - Prop.ForAll((from pool in pools - from step in Gen.Choose(1, 20) - select (pool, step)).ToArbitrary(), - testCase => { - int[] survivors = testCase.pool.Where(value => value % testCase.step == 0).ToArray(); - - // Nothing in the pool on the grid means the intersection is empty: eager conflict, again at - // the call. The example-based suite can only pin one pool; this quantifies over all of them. - if (survivors.Length == 0) { - return Expect.Throws( - () => Any.Int32().OneOf(testCase.pool).MultipleOf(testCase.step)); - } - - return Expect.EveryDraw(Any.Int32().OneOf(testCase.pool).MultipleOf(testCase.step), - value => survivors.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "MultipleOf: a step that is not strictly positive is an argument error, for every such step.")] - public void NonPositiveMultipleOfIsAnArgumentError() { - Prop.ForAll(Gen.Choose(-1000, 0).ToArbitrary(), - step => Expect.Throws(() => Any.Int32().MultipleOf(step)) - && Expect.Throws(() => Any.Int64().MultipleOf(step))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "MultipleOf: a second, genuinely different step conflicts; a no-op or a repeat does not.")] - public void MultipleOfIsDeclaredOnce() { - Prop.ForAll((from first in Gen.Choose(1, 6) - from second in Gen.Choose(1, 6) - select (first, second)).ToArbitrary(), - testCase => { - int firstStep = testCase.first; - int secondStep = testCase.second; - - // A step of one constrains nothing, and the same step twice is idempotent — neither is a - // second lattice. Only a real second lattice conflicts, so the verdict comes from the drawn - // values rather than from the call shape. - if (firstStep != secondStep && firstStep != 1 && secondStep != 1) { - return Expect.Throws(() => Any.Int32().MultipleOf(firstStep).MultipleOf(secondStep)); - } - - // In every accepted case exactly one of the two steps survives: the coarser one. - int surviving = Math.Max(firstStep, secondStep); - - return Expect.EveryDraw(Any.Int32().MultipleOf(firstStep).MultipleOf(secondStep), value => value % surviving == 0); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithScale: every draw lies on the 10^-scale value grid, for every supported scale.")] - public void WithScaleLandsOnTheDecimalGrid() { - Prop.ForAll(Gen.Choose(0, 28).ToArbitrary(), - scale => Expect.EveryDraw(Any.Decimal().WithScale(scale), - // A value lattice: the value is expressible in `scale` decimals. Its - // rendered form is deliberately not asserted — the library pads nothing. - value => Math.Round(value, scale, MidpointRounding.ToEven) == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithScale: a grid-aligned interval keeps every draw both in range and on the grid.")] - public void WithScaleComposesWithABoundedInterval() { - Prop.ForAll((from scale in Gen.Choose(0, 28) - from start in Gen.Choose(-1000, 1000) - from width in Gen.Choose(0, 1000) - select (scale, start, width)).ToArbitrary(), - testCase => { - // Bounds placed ON the grid and only a few grid points apart: the window is genuinely narrow, - // so the draw has to land on one of a handful of points instead of anywhere in a vast range. - // A zero width pins the interval to a single grid point — the degenerate corner kept on purpose. - decimal step = GridStep(testCase.scale); - decimal minimum = testCase.start * step; - decimal maximum = (testCase.start + testCase.width) * step; - - return Expect.EveryDraw(Any.Decimal().Between(minimum, maximum).WithScale(testCase.scale), - value => Math.Round(value, testCase.scale, MidpointRounding.ToEven) == value - && value >= minimum - && value <= maximum); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithScale: an interval lying strictly inside one grid cell conflicts at the call.")] - public void WithScaleOnAnIntervalWithoutGridPointConflicts() { - Prop.ForAll((from scale in Gen.Choose(0, 27) - from cell in Gen.Choose(-100, 100) - select (scale, cell)).ToArbitrary(), - testCase => { - // A window from a tenth to nine tenths of the way through one grid cell: it holds no value - // expressible in `scale` decimals, whichever cell and whichever scale were drawn. The scale - // stops at 27 so that a tenth of a step is still a representable decimal. - decimal step = GridStep(testCase.scale); - decimal finer = GridStep(testCase.scale + 1); - decimal lower = testCase.cell * step + finer; - decimal upper = testCase.cell * step + 9m * finer; - - return Expect.Throws( - () => Any.Decimal().Between(lower, upper).WithScale(testCase.scale)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithScale: a scale outside [0, 28] is an argument error, for every such scale.")] - public void WithScaleOutsideTheSupportedRangeIsAnArgumentError() { - Prop.ForAll(Gen.OneOf(Gen.Choose(-1000, -1), Gen.Choose(29, 1000)).ToArbitrary(), - scale => Expect.Throws(() => Any.Decimal().WithScale(scale))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithScale: a second, different scale conflicts; the same scale again does not.")] - public void WithScaleIsDeclaredOnce() { - Prop.ForAll((from first in Gen.Choose(0, 6) - from second in Gen.Choose(0, 6) - select (first, second)).ToArbitrary(), - testCase => { - // Unlike MultipleOf, no scale is a no-op: scale zero is the integer grid, a constraint in its - // own right. Only re-declaring the very same scale is idempotent. - if (testCase.first != testCase.second) { - return Expect.Throws( - () => Any.Decimal().WithScale(testCase.first).WithScale(testCase.second)); - } - - return Expect.EveryDraw(Any.Decimal().WithScale(testCase.first).WithScale(testCase.second), - value => Math.Round(value, testCase.first, MidpointRounding.ToEven) == value); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithGranularity: every draw sits on the grid anchored at its own type's origin.")] - public void WithGranularityLandsOnTheAnchoredGrid() { - Prop.ForAll(Granularities().ToArbitrary(), - granularity => { - long step = granularity.Ticks; - - // The anchor is part of the contract and differs per type — TimeSpan.Zero for a duration, - // MinValue for an instant — so it is written out rather than folded into a bare modulo: - // an anchor drifting onto the wrong origin is exactly what this property exists to catch. - // AnyTimeSpan is the sharpest of the three, since its unconstrained domain is signed and a - // misplaced anchor shows up on the negative side. - return Expect.EveryDraw(Any.TimeSpan().WithGranularity(granularity), - value => (value.Ticks - TimeSpan.Zero.Ticks) % step == 0) - && Expect.EveryDraw(Any.DateTime().WithGranularity(granularity), - value => (value.Ticks - DateTime.MinValue.Ticks) % step == 0) - // The DateTimeOffset lattice lives on the instant, which is what its own ordering - // compares; unconstrained, the offset is TimeSpan.Zero and the two tick counts agree. - && Expect.EveryDraw(Any.DateTimeOffset().WithGranularity(granularity), - value => (value.UtcTicks - DateTimeOffset.MinValue.UtcTicks) % step == 0); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithGranularity: a range keeps the grid inside it, on all three temporal types.")] - public void WithGranularityComposesWithARange() { - Prop.ForAll((from granularity in Granularities() - from signedStart in Gen.Choose(-1000, 1000) - from unsignedStart in Gen.Choose(0, 1000) - from width in Gen.Choose(0, 1000) - select (granularity, signedStart, unsignedStart, width)).ToArbitrary(), - testCase => { - // Bounds a whole number of granularities from each anchor, a few grid points apart: the - // window is narrow enough that a draw off the grid, or one step outside the range, shows up. - // The instant types start at or after their own minimum; the duration one straddles zero. - long step = testCase.granularity.Ticks; - TimeSpan durationFrom = TimeSpan.FromTicks(testCase.signedStart * step); - TimeSpan durationTo = TimeSpan.FromTicks((testCase.signedStart + testCase.width) * step); - DateTime instantFrom = new(testCase.unsignedStart * step, DateTimeKind.Utc); - DateTime instantTo = new((testCase.unsignedStart + testCase.width) * step, DateTimeKind.Utc); - DateTimeOffset offsetFrom = new(instantFrom.Ticks, TimeSpan.Zero); - DateTimeOffset offsetTo = new(instantTo.Ticks, TimeSpan.Zero); - - return Expect.EveryDraw(Any.TimeSpan().Between(durationFrom, durationTo).WithGranularity(testCase.granularity), - value => value.Ticks % step == 0 && value >= durationFrom && value <= durationTo) - && Expect.EveryDraw(Any.DateTime().Between(instantFrom, instantTo).WithGranularity(testCase.granularity), - value => value.Ticks % step == 0 && value >= instantFrom && value <= instantTo) - && Expect.EveryDraw(Any.DateTimeOffset().Between(offsetFrom, offsetTo).WithGranularity(testCase.granularity), - value => value.UtcTicks % step == 0 && value >= offsetFrom && value <= offsetTo); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithGranularity: a granularity that is not strictly positive is an argument error.")] - public void NonPositiveGranularityIsAnArgumentError() { - Prop.ForAll(Gen.Choose(-10000, 0).Select(ticks => TimeSpan.FromTicks(ticks)).ToArbitrary(), - granularity => Expect.Throws(() => Any.TimeSpan().WithGranularity(granularity)) - && Expect.Throws(() => Any.DateTime().WithGranularity(granularity)) - && Expect.Throws(() => Any.DateTimeOffset().WithGranularity(granularity))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithGranularity: a second, genuinely different granularity conflicts; a no-op or a repeat does not.")] - public void WithGranularityIsDeclaredOnce() { - Prop.ForAll((from first in CollidingGranularities() - from second in CollidingGranularities() - select (first, second)).ToArbitrary(), - testCase => { - TimeSpan declared = testCase.first; - TimeSpan redeclared = testCase.second; - - // One tick constrains nothing, and the same granularity twice is idempotent — the same rule - // as MultipleOf, since both ride the one lattice the interval engine carries. - if (declared != redeclared && declared.Ticks != 1 && redeclared.Ticks != 1) { - return Expect.Throws(() => Any.TimeSpan().WithGranularity(declared).WithGranularity(redeclared)) - && Expect.Throws(() => Any.DateTime().WithGranularity(declared).WithGranularity(redeclared)); - } - - long surviving = Math.Max(declared.Ticks, redeclared.Ticks); - - return Expect.EveryDraw(Any.TimeSpan().WithGranularity(declared).WithGranularity(redeclared), value => value.Ticks % surviving == 0) - && Expect.EveryDraw(Any.DateTime().WithGranularity(declared).WithGranularity(redeclared), value => value.Ticks % surviving == 0); - }) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/ModernTypeInvariantProperties.cs b/JustDummies.PropertyTests/ModernTypeInvariantProperties.cs deleted file mode 100644 index 1a6dff99..00000000 --- a/JustDummies.PropertyTests/ModernTypeInvariantProperties.cs +++ /dev/null @@ -1,464 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the five generators the netstandard2.0 asset cannot carry — -/// , , , and -/// . Where the example-based suite pins one anchor date, one anchor time and a handful -/// of tiny hand-picked intervals (Between(1, 3), Between(1f, 2f)), these draw the bounds, the -/// lattice steps and the seeds themselves, over the whole 128-bit domain, the whole ten-thousand-year day-number -/// range and the whole day of ticks, so a bound that overflows or truncates for one interval in a million is -/// found and shrunk to its minimal counter-example rather than missed. -/// -/// -/// The file is excluded from the .NET Framework 4.7.2 floor by the project file, because the types themselves -/// are: its invariants are net8-and-later by construction, and the rest of the suite proves the netstandard2.0 -/// contract on the floor. -/// -/// Three traps shape the properties here. The 128-bit generators ride an ordinal mapping (a sign-bit -/// flip for , the identity for ), so the domain edges are exactly -/// where the mapping could fold — the values are therefore assembled from two drawn 64-bit words, edges -/// included, instead of from FsCheck's size-bounded numerics, which would never leave the neighbourhood of -/// zero. carries about three decimal digits and tops out around 65504, so bounds are -/// drawn as small whole numbers and the intervals stay deliberately coarse: the point is the interval -/// algebra, not the rounding. And legality is value-dependent: an exclusive bound at the edge of a -/// domain, or a ceiling on the wrong side of zero for Positive(), is a conflict rather than a -/// narrowing, so the properties decide the expectation from the drawn value instead of assuming the call -/// shape settles it. -/// -/// -[TestSubject(typeof(AnyInt128))] -public sealed class ModernTypeInvariantProperties { - - #region Statics members declarations - - /// The widest interval the exclusion property opens — small enough that excluding one value stays a visible event. - private const int MaxWindow = 40; - - /// The domain split into blocks of a billion ticks: 863 * 1_000_000_000 + 999_999_999 is exactly 's tick count. - private const long TicksPerBlock = 1_000_000_000L; - - /// The number of whole billion-tick blocks in a day — see . - private const int TickBlocks = 863; - - /// The coarse magnitude the bounds stay within: every whole number up to 2048 is exactly representable, so a drawn bound survives the cast unrounded. - private const int MaxHalfMagnitude = 1024; - - /// - /// A 64-bit word drawn over its whole range, assembled from three narrow draws so no single draw has to span - /// more than a 32-bit range. FsCheck's own numeric generators are size-bounded and cluster around zero, which - /// for the halves of a 128-bit value would mean "always a small number in a huge domain" — precisely the part - /// of the domain an ordinal mapping cannot get wrong. - /// - private static Gen Word64() { - return from high in Gen.Choose(0, (1 << 22) - 1) - from middle in Gen.Choose(0, (1 << 21) - 1) - from low in Gen.Choose(0, (1 << 21) - 1) - // Added rather than or-ed: the three fields occupy disjoint bit ranges, so the sum is the same - // word, without or-ing an operand the compiler sees as sign-extended (CS0675). - select ((ulong)high << 42) + ((ulong)middle << 21) + (ulong)low; - } - - /// Arbitrary s over the whole domain, biased towards the ends of the range and towards the sign change at zero. - private static Gen Int128Values() { - Gen anywhere = from upper in Word64() - from lower in Word64() - select new Int128(upper, lower); - - return Generators.WithEdges(anywhere, Int128.MinValue, Int128.MinValue + Int128.One, Int128.NegativeOne, - Int128.Zero, Int128.One, Int128.MaxValue - Int128.One, Int128.MaxValue); - } - - /// Arbitrary s over the whole domain, biased towards the ends of the range — where an unsigned floor at zero hides its off-by-one. - private static Gen UInt128Values() { - Gen anywhere = from upper in Word64() - from lower in Word64() - select new UInt128(upper, lower); - - return Generators.WithEdges(anywhere, UInt128.MinValue, UInt128.One, UInt128.MaxValue - UInt128.One, UInt128.MaxValue); - } - - /// - /// Arbitrary bounds: whole numbers within a coarse magnitude, plus the edges of the type. - /// Coarse is deliberate — carries about three decimal digits, so a bound drawn with more - /// precision than that would be testing the cast rather than the interval algebra. - /// - private static Gen Halves() { - Gen anywhere = Gen.Choose(-MaxHalfMagnitude, MaxHalfMagnitude).Select(value => (Half)value); - - return Generators.WithEdges(anywhere, Half.MinValue, Half.NegativeOne, Half.Zero, Half.Epsilon, Half.One, Half.MaxValue); - } - - /// Arbitrary dates over the whole domain, its edges included: the day number is the ordinal the generator works in. - private static Gen Dates() { - Gen anywhere = Gen.Choose(DateOnly.MinValue.DayNumber, DateOnly.MaxValue.DayNumber).Select(dayNumber => DateOnly.FromDayNumber(dayNumber)); - - return Generators.WithEdges(anywhere, DateOnly.MinValue, DateOnly.MinValue.AddDays(1), - DateOnly.MaxValue.AddDays(-1), DateOnly.MaxValue); - } - - /// - /// Arbitrary times of day over the whole domain, drawn as a tick count so the - /// sub-second end of the range is reached as often as the hours — a granularity property is only worth - /// anything when the values it constrains are tick-precise to begin with. - /// - private static Gen Times() { - Gen anywhere = from block in Gen.Choose(0, TickBlocks) - from offset in Gen.Choose(0, (int)TicksPerBlock - 1) - select new TimeOnly(block * TicksPerBlock + offset); - - return Generators.WithEdges(anywhere, TimeOnly.MinValue, new TimeOnly(1), - new TimeOnly(TimeOnly.MaxValue.Ticks - 1), TimeOnly.MaxValue); - } - - /// - /// Arbitrary lattice steps for , spanning the units a caller - /// actually asks for — a tick, a millisecond, a second, a minute, an hour — and deliberately reaching below - /// zero: a non-positive granularity is an argument error, and that half of the contract deserves the same - /// quantification as the lattice itself. - /// - private static Gen Granularities() { - Gen units = Gen.Elements(1L, TimeSpan.TicksPerMillisecond, TimeSpan.TicksPerSecond, TimeSpan.TicksPerMinute, TimeSpan.TicksPerHour); - - return from unit in units - from count in Gen.Choose(-4, 24) - select TimeSpan.FromTicks(unit * count); - } - - #endregion - - [Fact(DisplayName = "Int128: Between contains — every draw falls within the declared inclusive bounds.")] - public void Int128BetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Int128Values()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Int128().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: Between with equal bounds pins the value, for every value.")] - public void Int128BetweenWithEqualBoundsPins() { - Prop.ForAll(Int128Values().ToArbitrary(), - value => Expect.EveryDraw(Any.Int128().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: crossed Between arguments are an argument error, never a silent swap.")] - public void Int128CrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Int128Values()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Int128().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: GreaterThan is strict below Int128.MaxValue, and conflicts at it.")] - public void Int128GreaterThanIsStrictAndConflictsAtTheCeiling() { - Prop.ForAll(Int128Values().ToArbitrary(), - bound => bound == Int128.MaxValue - ? Expect.Throws(() => Any.Int128().GreaterThan(bound)) - : Expect.EveryDraw(Any.Int128().GreaterThan(bound), value => value > bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: LessThan is strict above Int128.MinValue, and conflicts at it.")] - public void Int128LessThanIsStrictAndConflictsAtTheFloor() { - Prop.ForAll(Int128Values().ToArbitrary(), - bound => bound == Int128.MinValue - ? Expect.Throws(() => Any.Int128().LessThan(bound)) - : Expect.EveryDraw(Any.Int128().LessThan(bound), value => value < bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: every exclusion-caused conflict message makes only true claims, over the whole combination space.")] - public void Int128ConflictMessagesAreTruthful() { - // The 128-bit sibling of the ordinal engine; the shared oracle lives in ConflictMessageTruthfulnessProperties. - ConflictMessageTruthfulnessProperties.CheckEngine(BuildInt128, supportsLattice: true); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S1854:Unused assignments should be removed", - Justification = - "The assignment is dead and the CALL is not. These builders exist to provoke the declaration-time conflict, " + - "so what matters is that Except() runs; nothing reads the spec afterwards because the verdict is the exception " + - "or its absence. Dropping `spec =` from the last line alone would break the uniform chain that makes the " + - "sequence of constraints readable.")] - private static string? BuildInt128(bool hasBetween, int lo, int hi, int step, int[] allow, int[] excl) { - try { - AnyInt128 spec = Any.Int128(); - if (hasBetween) { spec = spec.Between(lo, hi); } - if (step > 1) { spec = spec.MultipleOf(step); } - if (allow.Length > 0) { spec = spec.OneOf(allow.Select(value => (Int128)value).ToArray()); } - if (excl.Length > 0) { spec = spec.Except(excl.Select(value => (Int128)value).ToArray()); } - - return null; - } catch (ConflictingAnyConstraintException exception) { return exception.Message; } - } - - [Fact(DisplayName = "Int128: Positive and Negative meet a bound on their own side of zero, and conflict with one on the other.")] - public void Int128SignConstraintsMeetABoundOrConflict() { - Prop.ForAll(Int128Values().ToArbitrary(), - bound => { - // Positive() has already pinned the minimum to one, so a ceiling at or below zero leaves the - // interval empty — and the library owes a conflict at the fluent call, not a failure at - // Generate(). The mirror image holds for Negative() and a floor at or above zero. - bool positive = bound <= Int128.Zero - ? Expect.Throws(() => Any.Int128().Positive().LessThanOrEqualTo(bound)) - : Expect.EveryDraw(Any.Int128().Positive().LessThanOrEqualTo(bound), - value => value > Int128.Zero && value <= bound); - bool negative = bound >= Int128.Zero - ? Expect.Throws(() => Any.Int128().Negative().GreaterThanOrEqualTo(bound)) - : Expect.EveryDraw(Any.Int128().Negative().GreaterThanOrEqualTo(bound), - value => value < Int128.Zero && value >= bound); - - return positive && negative; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: MultipleOf puts every draw on the lattice, and rejects a step that is not strictly positive.")] - public void Int128MultipleOfPutsEveryDrawOnTheGrid() { - Prop.ForAll(Int128Values().ToArbitrary(), - step => step <= Int128.Zero - ? Expect.Throws(() => Any.Int128().MultipleOf(step)) - : Expect.EveryDraw(Any.Int128().MultipleOf(step), value => value % step == Int128.Zero)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int128: Except removes the value from the interval, whatever the interval and the value.")] - public void Int128ExceptRemovesTheValueFromTheInterval() { - // The window is anchored on a drawn 64-bit value and widened by at most MaxWindow, so it reaches far into - // the 128-bit domain while its top can never overflow past Int128.MaxValue. - Gen<(Int128 Start, int Span, int Offset)> windows = from start in Generators.Int64() - from span in Gen.Choose(0, MaxWindow) - from offset in Gen.Choose(0, span) - select ((Int128)start, span, offset); - - Prop.ForAll(windows.ToArbitrary(), - window => { - Int128 minimum = window.Start; - Int128 maximum = minimum + window.Span; - Int128 excluded = minimum + window.Offset; - - // Excluding the single value of a pinned interval empties it: that is a conflict, not a draw. - if (window.Span == 0) { - return Expect.Throws( - () => Any.Int128().Between(minimum, maximum).Except(excluded)); - } - - return Expect.EveryDraw(Any.Int128().Between(minimum, maximum).Except(excluded), - value => value != excluded && value >= minimum && value <= maximum); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt128: Between contains — every draw falls within the declared inclusive bounds.")] - public void UInt128BetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(UInt128Values()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.UInt128().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt128: Between with equal bounds pins the value, for every value.")] - public void UInt128BetweenWithEqualBoundsPins() { - Prop.ForAll(UInt128Values().ToArbitrary(), - value => Expect.EveryDraw(Any.UInt128().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt128: the exclusive bounds are strict, and conflict at the ends of an unsigned domain.")] - public void UInt128ExclusiveBoundsAreStrictAndConflictAtTheDomainEdges() { - Prop.ForAll(UInt128Values().ToArbitrary(), - bound => { - // Nothing lies above UInt128.MaxValue, and — the unsigned specificity — nothing below zero: - // there the exclusive bound empties the domain rather than narrowing it. - bool above = bound == UInt128.MaxValue - ? Expect.Throws(() => Any.UInt128().GreaterThan(bound)) - : Expect.EveryDraw(Any.UInt128().GreaterThan(bound), value => value > bound); - bool below = bound == UInt128.MinValue - ? Expect.Throws(() => Any.UInt128().LessThan(bound)) - : Expect.EveryDraw(Any.UInt128().LessThan(bound), value => value < bound); - - return above && below; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt128: MultipleOf puts every draw on the lattice, and rejects a step of zero.")] - public void UInt128MultipleOfPutsEveryDrawOnTheGrid() { - Prop.ForAll(UInt128Values().ToArbitrary(), - step => step == UInt128.Zero - ? Expect.Throws(() => Any.UInt128().MultipleOf(step)) - : Expect.EveryDraw(Any.UInt128().MultipleOf(step), value => value % step == UInt128.Zero)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt128: OneOf draws only from the supplied pool, whatever the pool.")] - public void UInt128OneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(UInt128Values()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.UInt128().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Half: every draw is finite — NaN and the infinities are never generated, whatever the seed.")] - public void HalfDrawsAreAlwaysFiniteWhateverTheSeed() { - // Quantifying over the seed is what an example cannot do: the guarantee is about every sequence the - // generator can ever produce, not about the one the ambient context happens to produce today. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => Expect.EveryDraw(Any.WithSeed(seed).Half(), - value => !Half.IsNaN(value) && !Half.IsInfinity(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Half: Between contains — every draw falls within the declared inclusive bounds.")] - public void HalfBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Halves()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Half().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Half: crossed Between arguments are an argument error, not a conflict — argument validation comes first.")] - public void HalfCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Halves()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.Half().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Half: Positive and Negative meet a bound on their own side of zero, and conflict with one on the other.")] - public void HalfSignConstraintsMeetABoundOrConflict() { - Prop.ForAll(Halves().ToArbitrary(), - bound => { - // Positive() pins the minimum to the smallest representable half above zero, so no positive - // bound can ever fall below it: the legality line sits exactly at zero, on both sides. - bool positive = bound <= Half.Zero - ? Expect.Throws(() => Any.Half().Positive().LessThanOrEqualTo(bound)) - : Expect.EveryDraw(Any.Half().Positive().LessThanOrEqualTo(bound), - value => value > Half.Zero && value <= bound); - bool negative = bound >= Half.Zero - ? Expect.Throws(() => Any.Half().Negative().GreaterThanOrEqualTo(bound)) - : Expect.EveryDraw(Any.Half().Negative().GreaterThanOrEqualTo(bound), - value => value < Half.Zero && value >= bound); - - return positive && negative; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Half: Zero pins the value, and only excluding zero itself empties it.")] - public void HalfZeroPinsUnlessTheExclusionEmptiesIt() { - Prop.ForAll(Halves().ToArbitrary(), - excluded => excluded == Half.Zero - ? Expect.Throws(() => Any.Half().Zero().DifferentFrom(excluded)) - : Expect.EveryDraw(Any.Half().Zero().DifferentFrom(excluded), value => value == Half.Zero)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateOnly: Between contains — every draw falls within the declared inclusive dates.")] - public void DateOnlyBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Dates()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.DateOnly().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateOnly: Between with equal dates pins the value, for every date.")] - public void DateOnlyBetweenWithEqualBoundsPins() { - Prop.ForAll(Dates().ToArbitrary(), - date => Expect.EveryDraw(Any.DateOnly().Between(date, date), drawn => drawn == date)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateOnly: After and Before are exclusive, and conflict at the edges of the domain.")] - public void DateOnlyAfterAndBeforeAreExclusive() { - Prop.ForAll(Dates().ToArbitrary(), - date => { - // No date lies after DateOnly.MaxValue, and none before DateOnly.MinValue: there the - // exclusive bound empties the domain, and the library owes a conflict at the fluent call. - bool after = date == DateOnly.MaxValue - ? Expect.Throws(() => Any.DateOnly().After(date)) - : Expect.EveryDraw(Any.DateOnly().After(date), value => value > date); - bool before = date == DateOnly.MinValue - ? Expect.Throws(() => Any.DateOnly().Before(date)) - : Expect.EveryDraw(Any.DateOnly().Before(date), value => value < date); - - return after && before; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateOnly: AfterOrEqualTo and BeforeOrEqualTo keep their own bound, for every date.")] - public void DateOnlyInclusiveBoundsKeepTheirEdge() { - Prop.ForAll(Dates().ToArbitrary(), - date => { - // A half-bounded draw only shows the bound is respected — an exclusive reading would pass - // that too. Closing the interval on the very same date is what proves it inclusive: read - // exclusively, those two constraints would leave nothing to draw. - bool lower = Expect.EveryDraw(Any.DateOnly().AfterOrEqualTo(date), value => value >= date); - bool upper = Expect.EveryDraw(Any.DateOnly().BeforeOrEqualTo(date), value => value <= date); - bool closed = Expect.EveryDraw(Any.DateOnly().AfterOrEqualTo(date).BeforeOrEqualTo(date), value => value == date); - - return lower && upper && closed; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateOnly: crossed Between arguments are an argument error, never a silent swap.")] - public void DateOnlyCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Dates()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.DateOnly().Between(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeOnly: Between contains — every draw falls within the declared inclusive times of day.")] - public void TimeOnlyBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Times()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.TimeOnly().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeOnly: After and Before are exclusive, AfterOrEqualTo and BeforeOrEqualTo inclusive, at every time of day.")] - public void TimeOnlyBoundsCarryTheirInclusivity() { - Prop.ForAll(Times().ToArbitrary(), - time => { - // A time of day does not wrap: nothing lies after the last tick of the day, nor before - // midnight, so both exclusive bounds empty the domain at their own end of it. - bool after = time == TimeOnly.MaxValue - ? Expect.Throws(() => Any.TimeOnly().After(time)) - : Expect.EveryDraw(Any.TimeOnly().After(time), value => value > time); - bool before = time == TimeOnly.MinValue - ? Expect.Throws(() => Any.TimeOnly().Before(time)) - : Expect.EveryDraw(Any.TimeOnly().Before(time), value => value < time); - // Closing the interval on the very same time is what proves the other pair inclusive: read - // exclusively, those two constraints would leave nothing to draw. - bool closed = Expect.EveryDraw(Any.TimeOnly().AfterOrEqualTo(time).BeforeOrEqualTo(time), value => value == time); - - return after && before && closed; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeOnly: WithGranularity puts every draw on the lattice anchored at midnight, and rejects a non-positive step.")] - public void TimeOnlyWithGranularityPutsEveryDrawOnTheLattice() { - // The anchor is TimeOnly.MinValue, not the drawn value: the lattice belongs to the domain, so a granularity - // yields the same grid whatever else has been declared — and the value is built on it, never snapped onto it. - Prop.ForAll(Granularities().ToArbitrary(), - granularity => granularity <= TimeSpan.Zero - ? Expect.Throws(() => Any.TimeOnly().WithGranularity(granularity)) - : Expect.EveryDraw(Any.TimeOnly().WithGranularity(granularity), - value => (value.Ticks - TimeOnly.MinValue.Ticks) % granularity.Ticks == 0L)) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/PatternRoundTripProperties.cs b/JustDummies.PropertyTests/PatternRoundTripProperties.cs deleted file mode 100644 index 49a0d659..00000000 --- a/JustDummies.PropertyTests/PatternRoundTripProperties.cs +++ /dev/null @@ -1,643 +0,0 @@ -#region Usings declarations - -using System.Globalization; -using System.Text.RegularExpressions; - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for , built around a round trip: FsCheck generates a -/// pattern from the supported regular subset, JustDummies generates a value from it, and the real .NET regex -/// engine is asked whether that value matches. The example-based suite pins some fifty hand-written patterns and -/// can only prove the walk right for those; here the pattern itself is the quantified variable, so a class, -/// quantifier or grouping combination nobody thought to write down is reached, and a failure is shrunk to its -/// minimal counter-example. -/// -/// -/// -/// The oracle is ^(?:P)$ rather than P: JustDummies generates a whole matching string, so -/// anchoring turns the partial-match into a whole-string test that catches -/// under-generation (too few characters) and over-generation (trailing junk) alike, and keeps a top-level -/// alternation from binding looser than intended. -/// -/// -/// The pattern generator is deliberately narrower than the supported subset. It emits no anchors — the -/// wrapper supplies them, and a generated ^ or $ would either duplicate them or land where the -/// parser rightly refuses it — and it never nests an unbounded quantifier inside a repeated group, because -/// (a+)+ legitimately overruns the generation ceiling with an . -/// Unbounded quantifiers therefore apply to single-character atoms only, group repeats stay at or below two, -/// and nesting stops at : a narrow round trip that always holds is worth more -/// than a broad one that flakes. -/// -/// -/// Every rejection is asserted by type, never on message text, and the taxonomy itself is under test: -/// a well-formed but non-regular construct is an , while a pattern the -/// real engine cannot compile is a plain . The two are not interchangeable, so -/// the malformed property asks the real engine for its verdict first, and refuses an unsupported-construct -/// answer. -/// -/// -[TestSubject(typeof(AnyPattern))] -public sealed class PatternRoundTripProperties { - - /// How deep a generated pattern may nest groups. Shallow on purpose — see the class remarks. - private const int MaxNestingDepth = 3; - - /// How many parts a generated concatenation may hold. - private const int MaxSequenceParts = 3; - - /// How many branches a generated alternation may hold. - private const int MaxAlternationBranches = 3; - - /// - /// How many repetitions above its minimum an unbounded quantifier may add — the library's own - /// RegexRepeat.UnboundedExtra, restated here because it is internal. - /// - private const int UnboundedExtra = 8; - - /// - /// The character ceiling a generation may not cross — the library's own AnyPattern.GenerationLimit, - /// restated here because it is private. A quantifier minimum above it can only be refused, never built. - /// - private const int GenerationLimit = 65536; - - /// - /// How many values the minimum-honouring property draws per case. Kept low because most of its cases ask for a - /// minimum far above , and each such draw walks the ceiling before refusing. - /// - private const int MinimumHonouredDrawCount = 3; - - /// - /// How many values the alternation-reachability property draws. Four branches missed by 120 uniform draws is a - /// one-in-a-quadrillion event, so the property is deterministic in practice while staying a genuine reachability - /// claim rather than a containment one. - /// - private const int BranchSampleCount = 120; - - /// - /// Characters that stand for themselves in a pattern. Metacharacters are excluded (they appear only escaped), - /// and so are the space and #: those two are the only characters - /// reads differently, and the property that asserts that - /// option is refused must not risk the constructor throwing before JustDummies is reached. - /// - private const string LiteralAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_:/@=,;!~"; - - /// Letters and digits — what class ranges, hexadecimal escapes and alternation branches are built from. - private const string AlphaNumericAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - /// A group name must open on a letter: a name opening on a digit is an explicit capture number instead. - private const string NameHeadAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - /// After the first character a group name may hold any word character. - private const string NameTailAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789_"; - - #region Statics members declarations - - /// - /// Escaped single characters: the metacharacters, plus the control escapes the parser resolves to a real - /// character rather than to its letter. \n and \r are left out — nothing in the subset needs - /// them, and a newline is the one character whose interaction with $ in the oracle is not a plain - /// end-of-string test. - /// - private static readonly string[] EscapedLiterals = { - @"\.", @"\*", @"\+", @"\?", @"\(", @"\)", @"\[", @"\]", @"\{", @"\}", @"\|", @"\\", @"\^", @"\$", @"\-", @"\/", - @"\t", @"\a", @"\f", @"\v", @"\e" - }; - - /// The class shorthands, all six of them. - private static readonly string[] Shorthands = { @"\d", @"\D", @"\w", @"\W", @"\s", @"\S" }; - - /// - /// The shorthands a negated class may hold. Only the positive ones: a negated class that excludes the - /// whole printable-ASCII universe ([^\s\S], [^\w\W]) is refused as unsupported, and excluding - /// digits, word characters and whitespace always leaves the punctuation behind. - /// - private static readonly string[] PositiveShorthands = { @"\d", @"\w", @"\s" }; - - /// - /// Escaped members a character class may hold. The control escapes are dropped and every punctuation member is - /// escaped, so no bare -, [ or ] can ever turn a member into a range endpoint, a - /// class subtraction or an early close. - /// - private static readonly string[] ClassEscapedMembers = { - @"\.", @"\*", @"\+", @"\?", @"\(", @"\)", @"\[", @"\]", @"\{", @"\}", @"\|", @"\\", @"\^", @"\$", @"\-", @"\/" - }; - - /// A class range stays inside one of these, so its endpoints are always in order and always readable. - private static readonly string[] RangeAlphabets = { - "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789" - }; - - /// - /// Options that may accompany without changing whether the - /// pattern compiles, so the refusal is proven to depend on that one flag and not on being alone. - /// - private static readonly RegexOptions[] CompanionOptions = { - RegexOptions.None, RegexOptions.IgnoreCase, RegexOptions.Singleline, RegexOptions.Multiline, - RegexOptions.ExplicitCapture, RegexOptions.CultureInvariant - }; - - /// - /// Constructs the real engine compiles but JustDummies declines: they are well-formed, and either non-regular - /// (lookaround, backreference, balancing group, word boundary) or not honourable by a plain left-to-right walk - /// (atomic group, class subtraction, Unicode category, group option, conditional, comment). - /// - private static readonly string[] UnsupportedConstructs = { - "(?=abc)", "(?!abc)", "(?<=abc)", "(?abc)", "(?#note)", "(?i:abc)", "(?(a)b|c)", - @"\bword", @"\Bx", @"\Ax", @"x\z", @"x\Z", @"\Gx", @"\p{L}", @"\P{L}", @"(\w)\1", @"(?a)\k", - "(?y)?(?<-a>x)", "[a-z-[aeiou]]" - }; - - /// - /// Patterns the real .NET engine refuses to compile. JustDummies must mirror that verdict as a plain - /// — reporting them as unsupported would claim the caller wrote something - /// merely out of scope rather than something broken. - /// - private static readonly string[] MalformedPatterns = { - "[a-", "(abc", "abc)", "(?", "a{3,1}", "*abc", @"a\", "a*+", "a**", "a*??", "[]", @"\q", @"\x4", @"\c1", - "{2}", "(?<>a)", "(?<1a>x)", "(?<0>x)", "(?<01>x)", "(?'0'x)", "(?x)", "(?x)" - }; - - /// - /// The oracle: the pattern anchored at both ends, so a partial match cannot pass for a whole one. A match - /// timeout is attached as a safety net — a generated pattern that somehow made the backtracking engine crawl - /// should fail the suite, never hang it. - /// - private static Regex Anchored(string pattern, RegexOptions options) { - return new Regex("^(?:" + pattern + ")$", options, TimeSpan.FromSeconds(10)); - } - - /// Whether the real .NET engine compiles at all — the reference verdict on well-formedness. - private static bool CompilesInTheRealEngine(string pattern) { - try { - _ = new Regex(pattern); - - return true; - } catch (ArgumentException) { - return false; - } - } - - /// - /// Whether is refused as malformed — an . - /// An is explicitly not an acceptable answer: the two verdicts - /// say different things to the caller, so the taxonomy is asserted rather than merely "it threw". - /// - private static bool ThrowsMalformed(string pattern) { - try { - _ = Any.StringMatching(pattern); - - return false; - } catch (UnsupportedRegexException) { - return false; - } catch (ArgumentException) { - return true; - } - } - - /// An integer rendered for a quantifier bound, independently of the ambient culture. - private static string Digits(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - /// A pattern from the supported subset, nesting at most levels of groups. - private static Gen SupportedPattern() { - return Pattern(MaxNestingDepth); - } - - /// - /// An alternation of one to branches. One branch is kept in the mix: the - /// unalternated pattern is the common case, and dropping it would leave every generated pattern lopsided. - /// - private static Gen Pattern(int depth) { - Gen branch = Sequence(depth); - - return from count in Gen.Choose(1, MaxAlternationBranches) - from branches in Gen.ArrayOf(branch, count) - select string.Join("|", branches); - } - - /// A concatenation of one to quantified atoms. - private static Gen Sequence(int depth) { - Gen part = Quantified(depth); - - return from count in Gen.Choose(1, MaxSequenceParts) - from parts in Gen.ArrayOf(part, count) - select string.Concat(parts); - } - - /// - /// An atom with an optional quantifier. The split between the two quantifier generators is what keeps the - /// recursion safe: only a single-character atom may carry an unbounded quantifier, so no (a+)+ — a - /// pattern that legitimately overruns the generation ceiling — can ever be built. - /// - private static Gen Quantified(int depth) { - Gen quantifiedLeaf = from atom in Leaf() - from quantifier in LeafQuantifier() - select atom + quantifier; - - if (depth <= 0) { return quantifiedLeaf; } - - Gen quantifiedGroup = from atom in Group(depth) - from quantifier in GroupQuantifier() - select atom + quantifier; - - // Groups stay a minority of the atoms: the nesting is what makes a pattern expensive, both to generate and - // to match, and a suite of 100 cases is worth more spent on breadth than on depth. - return Gen.Frequency((6, quantifiedLeaf), (2, quantifiedGroup)); - } - - /// A group in each of the four supported forms: capturing, non-capturing, and named in both syntaxes. - private static Gen Group(int depth) { - Gen body = Pattern(depth - 1); - - return from inner in body - from name in GroupName() - from kind in Gen.Choose(0, 3) - select kind switch { - 0 => "(" + inner + ")", - 1 => "(?:" + inner + ")", - 2 => "(?<" + name + ">" + inner + ")", - _ => "(?'" + name + "'" + inner + ")" - }; - } - - /// - /// A group name the real engine accepts: a letter followed by up to two word characters. Names opening on a - /// digit are left out — those are explicit capture numbers, with their own validity rules, and the - /// malformed property covers them instead. - /// - private static Gen GroupName() { - return from head in Gen.Elements(NameHeadAlphabet.ToCharArray()) - from tail in Gen.ArrayOf(Gen.Elements(NameTailAlphabet.ToCharArray()), 2) - from length in Gen.Choose(0, 2) - select head.ToString() + new string(tail, 0, length); - } - - /// - /// An atom that emits exactly one character: a literal, an escaped literal, a shorthand, a hexadecimal escape, - /// a character class or the dot. - /// - private static Gen Leaf() { - return Gen.Frequency((8, Gen.Elements(LiteralAlphabet.ToCharArray()).Select(character => character.ToString())), - (3, Gen.Elements(EscapedLiterals)), - (4, Gen.Elements(Shorthands)), - (2, HexEscape()), - (4, CharacterClass()), - (1, Gen.Constant("."))); - } - - /// - /// Single-character atoms only — the subset the quantifier-length properties need, where the generated length - /// is the repetition count. Hexadecimal escapes are dropped so that a quantifier can never be mistaken - /// for a continuation of the escape's digits. - /// - private static Gen SingleCharacterAtom() { - return Gen.Frequency((4, Gen.Elements(LiteralAlphabet.ToCharArray()).Select(character => character.ToString())), - (2, Gen.Elements(EscapedLiterals)), - (2, Gen.Elements(Shorthands)), - (2, CharacterClass()), - (1, Gen.Constant("."))); - } - - /// A \xHH or \uHHHH escape naming a letter or a digit, so the escaped character stays printable. - private static Gen HexEscape() { - return from character in Gen.Elements(AlphaNumericAlphabet.ToCharArray()) - from wide in Gen.Elements(false, true) - select wide - ? @"\u" + ((int)character).ToString("X4", CultureInfo.InvariantCulture) - : @"\x" + ((int)character).ToString("X2", CultureInfo.InvariantCulture); - } - - /// - /// A character class of one to three members, negated or not. A negated class draws from the restricted member - /// set (see ), so the negation always leaves characters to draw from. - /// - private static Gen CharacterClass() { - return from negated in Gen.Elements(false, true) - from count in Gen.Choose(1, 3) - from members in Gen.ArrayOf(ClassMember(negated), count) - select "[" + (negated ? "^" : string.Empty) + string.Concat(members) + "]"; - } - - /// One member of a character class: a single character, a range, a shorthand, or an escaped punctuation member. - private static Gen ClassMember(bool negatedClass) { - Gen single = Gen.Elements(AlphaNumericAlphabet.ToCharArray()).Select(character => character.ToString()); - Gen shorthand = Gen.Elements(negatedClass ? PositiveShorthands : Shorthands); - - if (negatedClass) { return Gen.Frequency((4, single), (3, ClassRange()), (2, shorthand)); } - - return Gen.Frequency((4, single), (3, ClassRange()), (2, shorthand), (2, Gen.Elements(ClassEscapedMembers))); - } - - /// A range whose endpoints come from the same alphabet, so the low endpoint never exceeds the high one. - private static Gen ClassRange() { - return from alphabet in Gen.Elements(RangeAlphabets) - from first in Gen.Choose(0, alphabet.Length - 1) - from second in Gen.Choose(0, alphabet.Length - 1) - select $"{alphabet[Math.Min(first, second)]}-{alphabet[Math.Max(first, second)]}"; - } - - /// - /// A quantifier for a single-character atom: nothing, ?, a bounded {n}/{n,m}, or an - /// unbounded */+/{n,}. An unbounded quantifier is safe here precisely because the atom - /// under it is one character wide. - /// - private static Gen LeafQuantifier() { - Gen bounded = from minimum in Gen.Choose(0, 2) - from extra in Gen.Choose(0, 2) - from exact in Gen.Elements(false, true) - select exact - ? "{" + Digits(minimum) + "}" - : "{" + Digits(minimum) + "," + Digits(minimum + extra) + "}"; - - Gen unbounded = Gen.OneOf(Gen.Elements("*", "+"), - Gen.Choose(0, 2).Select(minimum => "{" + Digits(minimum) + ",}")); - - return WithOptionalLazyMarker(Gen.Frequency((5, Gen.Constant(string.Empty)), - (2, Gen.Constant("?")), - (2, bounded), - (2, unbounded))); - } - - /// - /// A quantifier for a group: bounded only, and never above two repetitions. Both restrictions are about size — - /// an unbounded repeat of a group is the runaway case, and a large bounded one multiplies out just as fast. - /// - private static Gen GroupQuantifier() { - Gen bounded = from minimum in Gen.Choose(0, 1) - from extra in Gen.Choose(0, 1) - select "{" + Digits(minimum) + "," + Digits(minimum + extra) + "}"; - - return WithOptionalLazyMarker(Gen.Frequency((6, Gen.Constant(string.Empty)), - (2, Gen.Constant("?")), - (2, bounded))); - } - - /// - /// Occasionally makes a quantifier lazy. A lazy marker changes which match the engine prefers, never which - /// strings match, so it must leave the generated language untouched — worth quantifying over rather than - /// assuming. It is never appended to an absent quantifier, where a bare ? would be a quantifier of its own. - /// - private static Gen WithOptionalLazyMarker(Gen quantifiers) { - return from quantifier in quantifiers - from lazy in Gen.Choose(0, 3) - select quantifier.Length == 0 || lazy != 0 ? quantifier : quantifier + "?"; - } - - /// - /// A minimum for an unbounded quantifier, spread across the whole legal range rather than the small end alone. - /// The three pinned regions are where the count arithmetic can go wrong: buildable minimums, the ceiling - /// crossing, and the top of the int range where adding overflows. - /// - private static Gen UnboundedMinimum() { - return Gen.Frequency((4, Gen.Choose(0, UnboundedExtra)), - (2, Gen.Choose(GenerationLimit - UnboundedExtra, GenerationLimit + UnboundedExtra)), - (3, Gen.Choose(int.MaxValue - (2 * UnboundedExtra), int.MaxValue)), - (2, Gen.Choose(UnboundedExtra + 1, int.MaxValue))); - } - - /// A short literal word, the material of the alternation-reachability property's branches. - private static Gen Word() { - return from characters in Gen.ArrayOf(Gen.Elements(AlphaNumericAlphabet.ToCharArray()), 3) - from length in Gen.Choose(1, 3) - select new string(characters, 0, length); - } - - #endregion - - [Fact(DisplayName = "Round trip: every value generated from a supported pattern is fully matched by the real .NET engine.")] - public void EveryGeneratedValueIsMatchedByTheRealEngine() { - Prop.ForAll(SupportedPattern().ToArbitrary(), - pattern => { - // The oracle is built once per case and reused across the draws: it is the pattern that varies - // between cases, not between draws. - Regex oracle = Anchored(pattern, RegexOptions.None); - - return Expect.EveryDraw(Any.StringMatching(pattern), oracle.IsMatch); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An excluded value is never generated, and what is generated still matches the pattern.")] - public void AnExcludedValueIsNeverGenerated() { - Prop.ForAll(SupportedPattern().ToArbitrary(), - pattern => { - Regex oracle = Anchored(pattern, RegexOptions.None); - string existing = Any.StringMatching(pattern).Generate(); - - try { - // The exclusion is rejective: it removes a value without touching how the rest are built, - // so the round trip must still hold for every draw that comes back. - return Expect.EveryDraw(Any.StringMatching(pattern).DifferentFrom(existing), - value => !string.Equals(value, existing, StringComparison.Ordinal) && oracle.IsMatch(value)); - } catch (AnyGenerationException) { - // A pattern whose language the exclusion leaves nothing of — a single-word one, most - // often. The bounded redraw reports its exhausted budget rather than ever returning the - // excluded value, which is the invariant under test. - return true; - } - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Round trip under IgnoreCase: every value is matched by the very regex it was generated from.")] - public void IgnoreCaseValuesAreMatchedByTheSameRegex() { - Prop.ForAll(SupportedPattern().ToArbitrary(), - pattern => { - // The Regex overload exists so a test can reuse the object its production code validates with, - // so the oracle is that same pattern under that same option — not a case-folded rewrite of it. - Regex source = new(pattern, RegexOptions.IgnoreCase); - Regex oracle = Anchored(pattern, RegexOptions.IgnoreCase); - - return Expect.EveryDraw(Any.StringMatching(source), oracle.IsMatch); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Two contexts sharing a seed yield the same values, for every pattern and every seed.")] - public void TheSameSeedYieldsTheSameValues() { - Gen<(string Pattern, int Seed)> cases = - from pattern in SupportedPattern() - from seed in Generators.Seed() - select (Pattern: pattern, Seed: seed); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // A whole sequence, not a single draw: a generator that reseeded itself per value would still - // agree on the first one. - List first = Expect.Draws(Any.WithSeed(testCase.Seed).StringMatching(testCase.Pattern), 8); - List second = Expect.Draws(Any.WithSeed(testCase.Seed).StringMatching(testCase.Pattern), 8); - - return first.SequenceEqual(second); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A bounded quantifier keeps the length inside its bounds, whatever the bounds and the form.")] - public void BoundedQuantifiersKeepTheLengthInsideTheirBounds() { - Gen<(string Atom, int Min, int Max, int Form)> cases = - from atom in SingleCharacterAtom() - from bounds in Generators.OrderedPair(Gen.Choose(0, 4)) - from form in Gen.Choose(0, 2) - select (Atom: atom, Min: bounds.Min, Max: bounds.Max, Form: form); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // The three bounded forms are one rule with different bounds: '{n}' pins the count, '{n,m}' - // brackets it, and '?' is the fixed {0,1} case. Degenerate bounds are kept — '{0}' generating - // the empty string is a legitimate corner, not one to filter away. - (string Pattern, int Min, int Max) quantified = testCase.Form switch { - 0 => (testCase.Atom + "{" + Digits(testCase.Min) + "}", testCase.Min, testCase.Min), - 1 => (testCase.Atom + "{" + Digits(testCase.Min) + "," + Digits(testCase.Max) + "}", testCase.Min, testCase.Max), - _ => (testCase.Atom + "?", 0, 1) - }; - - return Expect.EveryDraw(Any.StringMatching(quantified.Pattern), - value => value.Length >= quantified.Min && value.Length <= quantified.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An unbounded quantifier draws its minimum plus 0 to 8 repetitions, whatever the minimum and the form.")] - public void UnboundedQuantifiersDrawTheMinimumPlusUpToEight() { - Gen<(string Atom, int Min, int Form)> cases = - from atom in SingleCharacterAtom() - from minimum in Gen.Choose(0, 4) - from form in Gen.Choose(0, 2) - select (Atom: atom, Min: minimum, Form: form); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // '*' is '{0,}' and '+' is '{1,}', so the three forms differ only in their minimum. The ceiling - // is the claim worth quantifying: an unbounded quantifier has to pick a spread, and the library - // promises the same bounded one it uses everywhere else. - (string Pattern, int Min) quantified = testCase.Form switch { - 0 => (testCase.Atom + "*", 0), - 1 => (testCase.Atom + "+", 1), - _ => (testCase.Atom + "{" + Digits(testCase.Min) + ",}", testCase.Min) - }; - - return Expect.EveryDraw(Any.StringMatching(quantified.Pattern), - value => value.Length >= quantified.Min - && value.Length <= quantified.Min + UnboundedExtra); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An unbounded quantifier never yields a value shorter than its minimum, whatever the minimum.")] - public void UnboundedQuantifiersNeverYieldAValueShorterThanTheirMinimum() { - Gen<(string Atom, int Min)> cases = - from atom in SingleCharacterAtom() - from minimum in UnboundedMinimum() - select (Atom: atom, Min: minimum); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // The companion property above pins the spread for minimums small enough to build. This one - // states the half that holds for EVERY minimum, including those no generation can satisfy: - // either a value of the promised length comes out, or the ceiling refuses — but a value - // SHORTER than the minimum is not an outcome. That is the class the int-arithmetic overflow - // fell into, where a minimum near int.MaxValue wrapped negative and yielded the empty string. - AnyPattern generator = Any.StringMatching(testCase.Atom + "{" + Digits(testCase.Min) + ",}"); - - for (int draw = 0; draw < MinimumHonouredDrawCount; draw++) { - try { - // The atom is one character wide, so the generated length IS the repetition count. - int length = generator.Generate().Length; - if (length < testCase.Min || length > (long)testCase.Min + UnboundedExtra) { return false; } - } catch (AnyGenerationException) { - // Overrunning the ceiling is the honest refusal for a minimum too large to build. - } - } - - return true; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Alternation reaches every declared branch and invents none, whatever the branches.")] - public void AlternationReachesEveryBranchAndInventsNone() { - Gen<(string[] Branches, int Seed)> cases = - from words in Gen.ArrayOf(Word(), 4) - from seed in Generators.Seed() - select (Branches: words.Distinct().ToArray(), Seed: seed); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // Branches are plain literal words, so a drawn value IS the branch that produced it. SetEquals - // then states both halves at once: no branch is dead, and nothing outside the declared set - // can come out. - AnyPattern generator = Any.WithSeed(testCase.Seed).StringMatching(string.Join("|", testCase.Branches)); - HashSet seen = [.. Expect.Draws(generator, BranchSampleCount)]; - - return seen.SetEquals(testCase.Branches); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "IgnorePatternWhitespace is refused as an argument error, whatever the pattern and the companion options.")] - public void IgnorePatternWhitespaceIsAnArgumentError() { - Gen<(string Pattern, RegexOptions Companion, int Seed)> cases = - from pattern in SupportedPattern() - from companion in Gen.Elements(CompanionOptions) - from seed in Generators.Seed() - select (Pattern: pattern, Companion: companion, Seed: seed); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // The Regex is built outside the assertion on purpose: only JustDummies' refusal is under test, - // never the .NET constructor's. The generated alphabets hold no whitespace and no '#', so this - // option cannot change whether the pattern compiles — only how JustDummies must answer. - Regex source = new(testCase.Pattern, RegexOptions.IgnorePatternWhitespace | testCase.Companion); - - return Expect.Throws(() => Any.StringMatching(source)) - && Expect.Throws(() => Any.WithSeed(testCase.Seed).StringMatching(source)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A null pattern is an argument error, on both overloads and on a seeded context.")] - public void ANullPatternIsAnArgumentError() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => Expect.Throws(() => Any.StringMatching((string)null!)) - && Expect.Throws(() => Any.StringMatching((Regex)null!)) - && Expect.Throws(() => Any.WithSeed(seed).StringMatching((string)null!)) - && Expect.Throws(() => Any.WithSeed(seed).StringMatching((Regex)null!))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A well-formed but unsupported construct is refused eagerly, alone and after any supported prefix.")] - public void UnsupportedConstructsAreRefusedAsUnsupported() { - Gen<(string Prefix, string Construct)> cases = - from prefix in SupportedPattern() - from construct in Gen.Elements(UnsupportedConstructs) - select (Prefix: prefix, Construct: construct); - - Prop.ForAll(cases.ToArbitrary(), - // Each construct opens on '(', '[' or '\', none of which a preceding supported pattern can absorb, - // so appending one to an arbitrary prefix reaches the same refusal from a different parser state: - // the verdict must not depend on the construct sitting at position zero. - testCase => Expect.Throws(() => Any.StringMatching(testCase.Construct)) - && Expect.Throws(() => Any.StringMatching(testCase.Prefix + testCase.Construct))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A malformed pattern is an argument error, never an unsupported-construct refusal.")] - public void MalformedPatternsAreArgumentErrors() { - Prop.ForAll(Gen.Elements(MalformedPatterns).ToArbitrary(), - // The real engine is asked first, so the property states the taxonomy rather than restating the - // list: a pattern .NET itself cannot compile is broken, and JustDummies must say so as an argument - // error. An UnsupportedRegexException here would be the wrong answer, and ThrowsMalformed rejects it. - pattern => !CompilesInTheRealEngine(pattern) && ThrowsMalformed(pattern)) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/PropertyTestSupport.cs b/JustDummies.PropertyTests/PropertyTestSupport.cs deleted file mode 100644 index b7b87c3a..00000000 --- a/JustDummies.PropertyTests/PropertyTestSupport.cs +++ /dev/null @@ -1,151 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Shared FsCheck generators for the property suite. They generate the constraints — the bounds, lengths, -/// counts and seeds a caller declares — while JustDummies generates the value that must satisfy them. That split is -/// the point of this project: the example-based suite pins a handful of hand-picked constraints -/// (Between(10, 20), WithLength(12)) and can only prove the generator right for those, whereas a -/// property quantifies over the whole constraint space and lets FsCheck shrink a failure down to its minimal -/// counter-example. -/// -/// -/// Drawing the constraints from FsCheck rather than from also breaks a circularity: the suite -/// no longer uses the component under test to decide what to test it with. -/// -internal static class Generators { - - #region Statics members declarations - - /// - /// Pairs two draws of into an ordered (min, max) tuple, so a bound pair is - /// always well-formed. Degenerate pairs (min == max) are deliberately kept: pinning a single value is a - /// legitimate — and historically fragile — corner of every interval generator. - /// - public static Gen<(T Min, T Max)> OrderedPair(Gen values, IComparer? comparer = null) { - IComparer order = comparer ?? Comparer.Default; - - return from first in values - from second in values - select order.Compare(first, second) <= 0 ? (first, second) : (second, first); - } - - /// - /// Mixes FsCheck's own draws with the edges an off-by-one hides behind. FsCheck's default numeric generator is - /// size-bounded and clusters around zero, so the extremes of the range would otherwise almost never be drawn — - /// exactly where an interval generator overflows or silently truncates. - /// - public static Gen WithEdges(Gen values, params T[] edges) { - return Gen.OneOf(values, Gen.Elements(edges)); - } - - /// Arbitrary s, biased towards the ends of the range. - public static Gen Int32() { - return WithEdges(ArbMap.Default.GeneratorFor(), int.MinValue, int.MinValue + 1, -1, 0, 1, int.MaxValue - 1, int.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range. - public static Gen Int64() { - return WithEdges(ArbMap.Default.GeneratorFor(), long.MinValue, long.MinValue + 1, -1, 0, 1, long.MaxValue - 1, long.MaxValue); - } - - /// Arbitrary finite s. NaN and the infinities are excluded: the library rejects them as argument errors. - public static Gen Double() { - return WithEdges(ArbMap.Default.GeneratorFor().Where(value => !double.IsNaN(value) && !double.IsInfinity(value)), - double.MinValue, -1d, 0d, 1d, double.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range. - public static Gen Decimal() { - return WithEdges(ArbMap.Default.GeneratorFor(), decimal.MinValue, -1m, 0m, 1m, decimal.MaxValue); - } - - /// A collection or string length: small enough to stay cheap, wide enough to cross the empty and single-element cases. - public static Gen Count(int max = 12) { - return Gen.Choose(0, max); - } - - /// An arbitrary seed, including the values a hand-written test would never pick. - public static Gen Seed() { - return Int32(); - } - - #endregion - -} - -/// -/// Assertion helpers usable from inside an FsCheck property, where the property's verdict is a returned -/// rather than a thrown assertion. -/// -internal static class Expect { - - #region Statics members declarations - - /// - /// Returns true when throws an exception assignable to - /// ; otherwise false. - /// - public static bool Throws(Action action) - where TException : Exception { - try { - action(); - - return false; - } catch (TException) { - return true; - } - } - - /// - /// Returns true when completes without throwing. The counterpart of - /// , for a property whose subject is that ordinary use of a generated value - /// stays uneventful — decimal arithmetic, which signals its overflow by throwing rather than by - /// saturating. - /// - public static bool DoesNotThrow(Action action) { - try { - action(); - - return true; - } catch (Exception) { - return false; - } - } - - /// - /// Draws values from and returns true when every - /// one of them satisfies . A generator is a recipe, not a value, so one draw per - /// FsCheck case would leave most of its randomness untested; a handful of draws per case multiplies the - /// coverage without making the property expensive. - /// - public static bool EveryDraw(IAny generator, Func invariant, int count = 8) { - for (int i = 0; i < count; i++) { - if (!invariant(generator.Generate())) { return false; } - } - - return true; - } - - /// - /// Materializes draws from , for the properties that - /// reason over a batch rather than over each value in isolation (reachability, distinctness, ...). - /// - public static List Draws(IAny generator, int count) { - List values = new(count); - for (int i = 0; i < count; i++) { - values.Add(generator.Generate()); - } - - return values; - } - - #endregion - -} diff --git a/JustDummies.PropertyTests/ScalarIntervalProperties.cs b/JustDummies.PropertyTests/ScalarIntervalProperties.cs deleted file mode 100644 index 38bdd299..00000000 --- a/JustDummies.PropertyTests/ScalarIntervalProperties.cs +++ /dev/null @@ -1,301 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the interval algebra of every integer width other than -/// : , , , -/// , , and . -/// They all ride the same ordinal interval engine, but each one supplies its own domain edges and its own -/// signed-or-unsigned mapping into ordinal space — and that mapping is exactly where an off-by-one at a domain -/// edge, or an overflow in the interval arithmetic, hides. Quantifying over the whole bound space of a width -/// reaches those corners; the hand-picked intervals of the example-based suite cannot. -/// -/// -/// The invariants are deliberately spread across the widths rather than repeated seven times over: each one is -/// proven on at least one signed and one unsigned width, with the narrowest pair (sbyte/byte, -/// where almost every bound is an edge) and the widest pair (long/ulong, where the interval -/// arithmetic runs out of room) getting the fullest treatment. -/// -[TestSubject(typeof(AnyInt64))] -public sealed class ScalarIntervalProperties { - - #region Statics members declarations - - // One generator per width, each built on the shared Generators.WithEdges so FsCheck's size-bounded draws — - // which cluster around zero — are mixed with the domain edges an off-by-one hides behind. `long` needs no - // local generator: Generators.Int64() is already part of the shared support. - - /// Arbitrary s, biased towards the ends of the range. - private static Gen SByte() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - sbyte.MinValue, sbyte.MinValue + 1, -1, 0, 1, sbyte.MaxValue - 1, sbyte.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range and the sign-bit boundary. - private static Gen Byte() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - byte.MinValue, 1, 127, 128, byte.MaxValue - 1, byte.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range. - private static Gen Int16() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - short.MinValue, short.MinValue + 1, -1, 0, 1, short.MaxValue - 1, short.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range and the sign-bit boundary. - private static Gen UInt16() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - ushort.MinValue, 1, 32767, 32768, ushort.MaxValue - 1, ushort.MaxValue); - } - - /// Arbitrary s, biased towards the ends of the range and the sign-bit boundary. - private static Gen UInt32() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - uint.MinValue, 1u, 0x8000_0000u, uint.MaxValue - 1u, uint.MaxValue); - } - - /// - /// Arbitrary s, biased towards the ends of the range and towards 2^63 — the point a - /// signed reinterpretation of the ordinal space would fold in two. - /// - private static Gen UInt64() { - return Generators.WithEdges(ArbMap.Default.GeneratorFor(), - ulong.MinValue, 1UL, 0x8000_0000_0000_0000UL, ulong.MaxValue - 1UL, ulong.MaxValue); - } - - #endregion - - [Fact(DisplayName = "SByte: Between contains — every draw falls within the declared inclusive bounds.")] - public void SByteBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(SByte()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.SByte().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "SByte: Between with equal bounds pins the value, for every value.")] - public void SByteBetweenWithEqualBoundsPins() { - Prop.ForAll(SByte().ToArbitrary(), - value => Expect.EveryDraw(Any.SByte().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "SByte: the inclusive bounds admit their own bound, on both sides.")] - public void SByteInclusiveBoundsAdmitTheirOwnBound() { - Prop.ForAll(SByte().ToArbitrary(), - bound => Expect.EveryDraw(Any.SByte().GreaterThanOrEqualTo(bound), value => value >= bound) - && Expect.EveryDraw(Any.SByte().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "SByte: GreaterThan and LessThan are strict, and conflict at the domain edge they cannot clear.")] - public void SByteStrictBoundsAreStrictAndConflictAtTheEdges() { - Prop.ForAll(SByte().ToArbitrary(), - bound => { - // There is no sbyte above sbyte.MaxValue nor below sbyte.MinValue: asking for one is a - // conflict declared at the call, never an interval that generates and then disappoints. - bool above = bound == sbyte.MaxValue - ? Expect.Throws(() => Any.SByte().GreaterThan(bound)) - : Expect.EveryDraw(Any.SByte().GreaterThan(bound), value => value > bound); - bool below = bound == sbyte.MinValue - ? Expect.Throws(() => Any.SByte().LessThan(bound)) - : Expect.EveryDraw(Any.SByte().LessThan(bound), value => value < bound); - - return above && below; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "SByte: Between never yields a value excluded by a subsequent Except.")] - public void SByteExceptRemovesTheValueFromTheInterval() { - Gen<((sbyte Min, sbyte Max) Bounds, sbyte Excluded)> cases = - from bounds in Generators.OrderedPair(SByte()) - from offset in Gen.Choose(0, bounds.Max - bounds.Min) - select (Bounds: bounds, Excluded: (sbyte)(bounds.Min + offset)); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // Excluding the single value of a pinned interval empties it: that is a conflict, not a draw. - if (testCase.Bounds.Min == testCase.Bounds.Max) { - return Expect.Throws( - () => Any.SByte().Between(testCase.Bounds.Min, testCase.Bounds.Max).Except(testCase.Excluded)); - } - - return Expect.EveryDraw(Any.SByte().Between(testCase.Bounds.Min, testCase.Bounds.Max).Except(testCase.Excluded), - value => value != testCase.Excluded - && value >= testCase.Bounds.Min - && value <= testCase.Bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Byte: Between contains — every draw falls within the declared inclusive bounds.")] - public void ByteBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Byte()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Byte().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Byte: Between with equal bounds pins the value, for every value.")] - public void ByteBetweenWithEqualBoundsPins() { - Prop.ForAll(Byte().ToArbitrary(), - value => Expect.EveryDraw(Any.Byte().Between(value, value), drawn => drawn == value)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Byte: the inclusive bounds admit their own bound, on both sides.")] - public void ByteInclusiveBoundsAdmitTheirOwnBound() { - Prop.ForAll(Byte().ToArbitrary(), - bound => Expect.EveryDraw(Any.Byte().GreaterThanOrEqualTo(bound), value => value >= bound) - && Expect.EveryDraw(Any.Byte().LessThanOrEqualTo(bound), value => value <= bound)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Byte: GreaterThan and LessThan are strict, and conflict at the domain edges — zero being the floor.")] - public void ByteStrictBoundsAreStrictAndConflictAtTheEdges() { - Prop.ForAll(Byte().ToArbitrary(), - bound => { - // The unsigned floor is zero, not a negative sentinel: LessThan(0) has nothing left to offer - // and must conflict rather than wrap around to byte.MaxValue. - bool above = bound == byte.MaxValue - ? Expect.Throws(() => Any.Byte().GreaterThan(bound)) - : Expect.EveryDraw(Any.Byte().GreaterThan(bound), value => value > bound); - bool below = bound == byte.MinValue - ? Expect.Throws(() => Any.Byte().LessThan(bound)) - : Expect.EveryDraw(Any.Byte().LessThan(bound), value => value < bound); - - return above && below; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Byte: Between never yields a value excluded by a subsequent Except.")] - public void ByteExceptRemovesTheValueFromTheInterval() { - Gen<((byte Min, byte Max) Bounds, byte Excluded)> cases = - from bounds in Generators.OrderedPair(Byte()) - from offset in Gen.Choose(0, bounds.Max - bounds.Min) - select (Bounds: bounds, Excluded: (byte)(bounds.Min + offset)); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // Excluding the single value of a pinned interval empties it: that is a conflict, not a draw. - if (testCase.Bounds.Min == testCase.Bounds.Max) { - return Expect.Throws( - () => Any.Byte().Between(testCase.Bounds.Min, testCase.Bounds.Max).Except(testCase.Excluded)); - } - - return Expect.EveryDraw(Any.Byte().Between(testCase.Bounds.Min, testCase.Bounds.Max).Except(testCase.Excluded), - value => value != testCase.Excluded - && value >= testCase.Bounds.Min - && value <= testCase.Bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int16: a generator is an immutable recipe — constraining it never narrows the original.")] - public void Int16ConstrainingNeverMutatesTheOriginal() { - Prop.ForAll(Generators.OrderedPair(Int16()).ToArbitrary(), - bounds => { - AnyInt16 original = Any.Int16().Between(bounds.Min, bounds.Max); - AnyInt16 narrowed = original.GreaterThanOrEqualTo(bounds.Max); - - return !ReferenceEquals(original, narrowed) - && Expect.EveryDraw(original, value => value >= bounds.Min && value <= bounds.Max) - && Expect.EveryDraw(narrowed, value => value == bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt16: a generator is an immutable recipe — constraining it never narrows the original.")] - public void UInt16ConstrainingNeverMutatesTheOriginal() { - Prop.ForAll(Generators.OrderedPair(UInt16()).ToArbitrary(), - bounds => { - AnyUInt16 original = Any.UInt16().Between(bounds.Min, bounds.Max); - AnyUInt16 narrowed = original.GreaterThanOrEqualTo(bounds.Max); - - return !ReferenceEquals(original, narrowed) - && Expect.EveryDraw(original, value => value >= bounds.Min && value <= bounds.Max) - && Expect.EveryDraw(narrowed, value => value == bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt32: crossed bounds are rejected — as Between arguments an argument error, as two constraints a conflict.")] - public void UInt32CrossedBoundsAreRejected() { - Prop.ForAll(Generators.OrderedPair(UInt32()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - // Argument validation precedes conflict checking: a swapped pair passed to a single - // call is an argument error, whereas the same emptiness spread over two calls is a - // constraint conflict. The two must not collapse into one another. - || (Expect.Throws(() => Any.UInt32().Between(bounds.Max, bounds.Min)) - && Expect.Throws( - () => Any.UInt32().GreaterThan(bounds.Max).LessThan(bounds.Min)))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt32: OneOf draws only from the supplied pool, whatever the pool.")] - public void UInt32OneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(UInt32()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.UInt32().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int64: Between contains — every draw falls within the bounds, across the whole 64-bit space.")] - public void Int64BetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Generators.Int64()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.Int64().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int64: crossed bounds are rejected — as Between arguments an argument error, as two constraints a conflict.")] - public void Int64CrossedBoundsAreRejected() { - Prop.ForAll(Generators.OrderedPair(Generators.Int64()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || (Expect.Throws(() => Any.Int64().Between(bounds.Max, bounds.Min)) - && Expect.Throws( - () => Any.Int64().GreaterThan(bounds.Max).LessThan(bounds.Min)))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Int64: OneOf draws only from the supplied pool, whatever the pool.")] - public void Int64OneOfStaysWithinItsPool() { - Gen pools = Gen.NonEmptyListOf(Generators.Int64()).Select(values => values.Distinct().ToArray()); - - Prop.ForAll(pools.ToArbitrary(), - pool => Expect.EveryDraw(Any.Int64().OneOf(pool), value => pool.Contains(value))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt64: Between contains — every draw falls within the bounds, up to the full unsigned range.")] - public void UInt64BetweenContainsEveryDraw() { - // The unsigned 64-bit domain is the one interval whose own size does not fit its own width: an interval - // spanning it cannot be sampled by "draw an index in [0, count)". Only quantified bounds reach that case. - Prop.ForAll(Generators.OrderedPair(UInt64()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.UInt64().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UInt64: Except never yields the excluded value, even on the unbounded full-width path.")] - public void UInt64ExceptHoldsOnTheFullWidthPath() { - // No interval is declared, so the specification still spans the whole domain and the exclusion has to be - // honoured by the full-width sampling path rather than by index arithmetic over a bounded range. - Prop.ForAll(UInt64().ToArbitrary(), - excluded => Expect.EveryDraw(Any.UInt64().Except(excluded), value => value != excluded)) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/SeedDeterminismProperties.cs b/JustDummies.PropertyTests/SeedDeterminismProperties.cs deleted file mode 100644 index 94ec45d1..00000000 --- a/JustDummies.PropertyTests/SeedDeterminismProperties.cs +++ /dev/null @@ -1,407 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the seeding surface: the isolated handed out by -/// , the ambient Any.UseSeed(...) scope, and the Any.Reproducibly(...) -/// runners. The example-based suite pins three hand-picked seeds — 12345, 777, 31415 — and can therefore only -/// prove reproducibility for those three numbers; these quantify over the seed itself, including the values a -/// hand-written test would never pick (zero, int.MinValue, int.MaxValue), so a seed that fails to -/// pin a run is found and shrunk to its minimal counter-example. -/// -/// -/// -/// Reproducibility is a claim about a whole run rather than about a single draw, so every property here -/// compares a batch: a mixed sequence of scalar, collection, nullable and pattern draws joined into -/// one string. The comparison fails as soon as any one draw shifts, which makes the batch both a sharper -/// probe than a single value and the reason the distinctness property at the end can rest on entropy rather -/// than on luck. -/// -/// -/// deliberately mirrors only the scalar factories, so the collection parts of a -/// context batch go through the static combinators over a context-derived element generator. That is not a -/// workaround but part of what is under test: a collection draws from its element generator's random source, -/// never from the ambient one. -/// -/// -/// Failure diagnostics are asserted by type and, for the reported seed, by the seed being quotable -/// from the message — never by the sentence around it, whose wording is a diagnostic concern rather than a -/// seeding one. -/// -/// -[TestSubject(typeof(AnyContext))] -public sealed class SeedDeterminismProperties { - - #region Statics members declarations - - /// - /// Draws a mixed batch from and joins it into a single string: every scalar family - /// the context exposes, plus a list, a set, a nullable and a pattern. Mirrors the Batch() helper of - /// the example-based suite minus the .NET 8+ types — this file also compiles on the .NET Framework floor, - /// where Int128 and Half do not exist. - /// - private static string ContextBatch(AnyContext any) { - int full = any.Int32().Generate(); - int bounded = any.Int32().Between(1, 1000).Generate(); - string free = any.String().Generate(); - string capped = any.String().NonEmpty().WithMaxLength(50).Generate(); - string shaped = any.String().StartingWith("ORD-").WithLength(12).Generate(); - long wide = any.Int64().Generate(); - double real = any.Double().Between(0d, 1000d).Generate(); - decimal exact = any.Decimal().Between(0m, 1000m).Generate(); - bool flag = any.Boolean().Generate(); - Guid id = any.Guid().Generate(); - char letter = any.Char().Generate(); - TimeSpan span = any.TimeSpan().Generate(); - DateTime instant = any.DateTime().Generate(); - // A context carries no collection factories of its own, and does not need any: the static combinators take - // the element generator's random source, so these two collections draw from the context all the same. - List list = Any.ListOf(any.Int32().Between(0, 9)).WithCount(4).Generate(); - HashSet set = Any.SetOf(any.Int32().Between(0, 99)).WithCount(3).Generate(); - int? maybe = any.Int32().Between(0, 9).OrNull().Generate(); - string coded = any.StringMatching(@"[A-Z]{3}-\d{4}").Generate(); - - return string.Join("|", full, bounded, free, capped, shaped, - wide, real, exact, flag, id, letter, - span.Ticks, instant.Ticks, - string.Join("-", list), string.Join("-", set.OrderBy(value => value)), - maybe?.ToString() ?? "null", coded); - } - - /// - /// The same batch drawn from the static entry points, for the mechanisms that pin the ambient context - /// instead of handing out a context object — Any.UseSeed(...) and Any.Reproducibly(...). It is - /// a second method rather than one parameterized over both because and the static - /// share a surface, not a type. - /// - private static string AmbientBatch() { - int full = Any.Int32().Generate(); - int bounded = Any.Int32().Between(1, 1000).Generate(); - string free = Any.String().Generate(); - string capped = Any.String().NonEmpty().WithMaxLength(50).Generate(); - string shaped = Any.String().StartingWith("ORD-").WithLength(12).Generate(); - long wide = Any.Int64().Generate(); - double real = Any.Double().Between(0d, 1000d).Generate(); - decimal exact = Any.Decimal().Between(0m, 1000m).Generate(); - bool flag = Any.Boolean().Generate(); - Guid id = Any.Guid().Generate(); - char letter = Any.Char().Generate(); - TimeSpan span = Any.TimeSpan().Generate(); - DateTime instant = Any.DateTime().Generate(); - - List list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4).Generate(); - HashSet set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3).Generate(); - int? maybe = Any.Int32().Between(0, 9).OrNull().Generate(); - string coded = Any.StringMatching(@"[A-Z]{3}-\d{4}").Generate(); - - return string.Join("|", full, bounded, free, capped, shaped, - wide, real, exact, flag, id, letter, - span.Ticks, instant.Ticks, - string.Join("-", list), string.Join("-", set.OrderBy(value => value)), - maybe?.ToString() ?? "null", coded); - } - - /// - /// A blank replay snippet: the empty string, and runs of the whitespace characters Trim() removes. - /// The example-based suite pins "" and three spaces; what the guard rejects is blankness, not those - /// two spellings. - /// - private static Gen BlankSnippet() { - return from length in Gen.Choose(0, 6) - from whitespace in Gen.Elements(' ', '\t', '\n', '\r', '\v', '\f') - select new string(whitespace, length); - } - - /// - /// Two seeds guaranteed to differ, drawn from the non-negative half of the seed space. The restriction is - /// deliberate and is about the BCL rather than about JustDummies: new Random(seed) derives its state - /// from the seed's absolute value, so s and -s are by design the very same generator. Quantifying - /// a distinctness claim over the whole range would therefore fail for a reason that has - /// nothing to do with the library under test. - /// - private static Gen<(int First, int Second)> DifferentSeeds() { - // Built from a base and a strictly positive delta rather than filtered for inequality, so no case is ever - // rejected and the halved bounds keep the sum inside int. - return from first in Gen.Choose(0, int.MaxValue / 2) - from delta in Gen.Choose(1, int.MaxValue / 2) - select (first, first + delta); - } - - /// - /// Returns true when throws itself - /// rather than a derived type. accepts a subclass, which would let - /// an satisfy the blank-snippet property whose whole point is that the - /// two rejections are told apart. - /// - private static bool ThrowsExactly(Action action) - where TException : Exception { - try { - action(); - - return false; - } catch (Exception exception) { - return exception.GetType() == typeof(TException); - } - } - - #endregion - - [Fact(DisplayName = "Two contexts created with the same seed replay the same batch, for every seed.")] - public void SameSeedContextsReplayTheSameBatch() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => ContextBatch(Any.WithSeed(seed)) == ContextBatch(Any.WithSeed(seed))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A context reports back the seed it was created with, for every seed.")] - public void ContextReportsItsSeed() { - // The round-trip is what makes a reported seed replayable at all: a context that silently normalized its - // seed would hand the reader a number that reproduces nothing. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => Any.WithSeed(seed).Seed == seed) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A context is isolated: ambient draws interleaved with its own never shift its sequence.")] - public void ContextIsIsolatedFromAmbientDraws() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - AnyContext quiet = Any.WithSeed(seed); - string firstQuiet = ContextBatch(quiet); - string secondQuiet = ContextBatch(quiet); - - // The same context again, this time with ambient draws before its first batch and between - // the two. A context owns its generator, so nothing the static entry points draw may - // advance it — not even by one value, which the second batch is there to catch. - AnyContext noisy = Any.WithSeed(seed); - Any.Guid().Generate(); - string firstNoisy = ContextBatch(noisy); - Any.String().Generate(); - Any.Int32().Generate(); - string secondNoisy = ContextBatch(noisy); - - return firstNoisy == firstQuiet && secondNoisy == secondQuiet; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Reproducibly replays the same batch for the same seed, for every seed.")] - public void ReproduciblyReplaysTheSameBatch() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - // None of the four Reproducibly overloads returns a value, so the batch leaves the body - // through a captured local. - string first = string.Empty; - string second = string.Empty; - - Any.Reproducibly(seed, () => { first = AmbientBatch(); }); - Any.Reproducibly(seed, () => { second = AmbientBatch(); }); - - return second == first; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UseSeed pins the ambient context, so the same seed replays the same batch.")] - public void UseSeedPinsTheAmbientContext() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - string first; - string second; - - using (Any.UseSeed(seed)) { first = AmbientBatch(); } - using (Any.UseSeed(seed)) { second = AmbientBatch(); } - - return second == first; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UseSeed and Reproducibly pin the same sequence, for every seed.")] - public void UseSeedAgreesWithReproducibly() { - // The scope form exists for a caller that cannot wrap what it pins in a delegate; it must be the same - // mechanism, not a second one that happens to agree on the seeds an example picked. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - string fromScope; - string fromRunner = string.Empty; - - using (Any.UseSeed(seed)) { fromScope = AmbientBatch(); } - Any.Reproducibly(seed, () => { fromRunner = AmbientBatch(); }); - - return fromScope == fromRunner; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UseSeed nests: an inner scope neither consumes nor resets the outer one, for every seed pair.")] - public void UseSeedScopesNest() { - Gen<(int Outer, int Inner)> seedPairs = from outer in Generators.Seed() - from inner in Generators.Seed() - select (outer, inner); - - Prop.ForAll(seedPairs.ToArbitrary(), - pair => { - string first; - string second; - using (Any.UseSeed(pair.Outer)) { - first = AmbientBatch(); - second = AmbientBatch(); - } - - // The same outer scope, interrupted by an inner one. The inner scope draws from its own - // generator, so the outer sequence must resume exactly where it was interrupted — including - // when the two seeds happen to be equal, where the inner scope still installs a generator - // of its own rather than sharing the outer one. - string restoredFirst; - string restoredSecond; - using (Any.UseSeed(pair.Outer)) { - restoredFirst = AmbientBatch(); - using (Any.UseSeed(pair.Inner)) { AmbientBatch(); } - restoredSecond = AmbientBatch(); - } - - return restoredFirst == first && restoredSecond == second; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Disposing a UseSeed scope twice is harmless and cannot unpin a later scope.")] - public void DisposingAScopeTwiceIsHarmless() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - IDisposable stale = Any.UseSeed(seed); - stale.Dispose(); - - string expected; - using (Any.UseSeed(seed)) { expected = AmbientBatch(); } - - string actual; - using (Any.UseSeed(seed)) { - // The second dispose of an already-closed handle must do nothing at all. Were it to run - // its restore again it would reinstate its own predecessor over the scope open right - // now, and the batch below would no longer be pinned — which is what makes this a - // stronger claim than merely "it does not throw". - stale.Dispose(); - actual = AmbientBatch(); - } - - return actual == expected; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Reproducibly reports the seed and rethrows the original exception instance, for every seed.")] - public void ReproduciblyReportsTheSeedAndRethrows() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - // An empty sentinel doubles as the never-reported case: it can contain no seed either way. - string reported = string.Empty; - InvalidOperationException boom = new("boom"); - // Explicitly typed: a throw-expression lambda converts to both Action and Func, so an - // inline one would make the overload ambiguous. - Action failing = () => throw boom; - Exception? caught = null; - - try { - Any.Reproducibly(seed, failing, message => reported = message); - } catch (Exception exception) { - caught = exception; - } - - // The exception must come back as the very instance the body threw — wrapping it would cost - // the test its real message — and the seed must be quotable from the report. What sentence - // carries it is a diagnostic concern, deliberately not asserted here. - return ReferenceEquals(caught, boom) && reported.Contains(seed.ToString()); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UseSeed rejects a null replay snippet, for every seed.")] - public void UseSeedRejectsANullSnippet() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => Expect.Throws(() => Any.UseSeed(seed, null!))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "UseSeed rejects a blank replay snippet, whatever the blank spelling.")] - public void UseSeedRejectsABlankSnippet() { - Gen<(int Seed, string Snippet)> cases = from seed in Generators.Seed() - from snippet in BlankSnippet() - select (seed, snippet); - - // Exactly ArgumentException, not merely something assignable to it: a blank snippet is not a null one, and - // the two guards must stay distinguishable. - Prop.ForAll(cases.ToArbitrary(), - testCase => ThrowsExactly(() => Any.UseSeed(testCase.Seed, testCase.Snippet))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Different seeds produce different batches — a statistical guard, not a theorem.")] - public void DifferentSeedsProduceDifferentBatches() { - // Nothing forbids two different seeds from landing on the same values, so this claim is probabilistic by - // nature. It is made robust by the batch rather than by weakening the assertion: a Guid, two full-range - // integers, three strings, a list, a set and a pattern would all have to coincide at once. What the property - // really watches for is the failure mode that would make them coincide systematically — a seed that ends up - // ignored, normalized away, or shared between contexts. - Prop.ForAll(DifferentSeeds().ToArbitrary(), - seeds => ContextBatch(Any.WithSeed(seeds.First)) != ContextBatch(Any.WithSeed(seeds.Second))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A source drawn from concurrently keeps generating, for every seed.")] - public void ConcurrentDrawsNeverCollapseTheSource() { - // Issue #310. The seed is the input space: nothing about the collapse depended on which seed was pinned, so - // pinning three by hand in the example suite could only ever prove it for those three. What is asserted is - // survival, not distribution — an unsynchronized Random whose indices converge under contention returns zero - // for ever, so every draw settles on the minimum of its range and the source never recovers. - // - // No claim is made here about WHICH values come out, or in what order: with a per-primitive lock two threads - // interleave inside a multi-draw Generate(), so neither the sequence nor the multiset of generated values is - // stable across threads. That is the documented contract, and asserting more would over-promise it. - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - int[] drawn = ConcurrentBurst(Any.WithSeed(seed)); - - return MostFrequent(drawn) < drawn.Length / 10; - }) - .QuickCheckThrowOnFailure(); - } - - #region Concurrency helpers - - private const int BurstThreads = 4; - private const int BurstDrawsPerThread = 1_500; - - /// - /// Draws from one context on every thread at once. Each worker writes its own slice, so the collection itself - /// adds no synchronization that could mask the source's. - /// - private static int[] ConcurrentBurst(AnyContext context) { - int[] drawn = new int[BurstThreads * BurstDrawsPerThread]; - Parallel.For(0, BurstThreads, new ParallelOptions { MaxDegreeOfParallelism = BurstThreads }, - worker => { - int offset = worker * BurstDrawsPerThread; - for (int index = 0; index < BurstDrawsPerThread; index++) { - drawn[offset + index] = context.Int32().Generate(); - } - }); - - return drawn; - } - - private static int MostFrequent(IEnumerable values) { - return values.GroupBy(value => value).Max(group => group.Count()); - } - - #endregion - -} diff --git a/JustDummies.PropertyTests/StringShapeProperties.cs b/JustDummies.PropertyTests/StringShapeProperties.cs deleted file mode 100644 index ccec6f68..00000000 --- a/JustDummies.PropertyTests/StringShapeProperties.cs +++ /dev/null @@ -1,457 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for 's shape algebra — lengths, anchored affixes, character -/// families, casing and exclusions. The example-based suite pins one length and one affix per invariant -/// (WithLength(10), StartingWith("ORD-")) and can only prove the layout right for those; these -/// quantify over the lengths and over the affix values themselves, so a filler budget that miscounts for -/// one length in a hundred, or a fragment check that lets one character through, is found and shrunk to its -/// minimal counter-example. -/// -/// -/// -/// Two of these properties are of a kind an example cannot express at all: the same call shape is legal or -/// illegal depending on the argument value. WithLength(n).StartingWith(prefix) holds exactly -/// when n leaves room for the prefix, and Numeric().StartingWith(prefix) holds exactly when -/// every character of the prefix is a digit. Both are written as a single property branching on that -/// relationship, so what gets tested is the boundary itself rather than a hand-picked point on either side. -/// -/// -/// Conflicts are asserted by type and at the fluent call that declares them, never on message text: -/// the messages are direction-aware — they name whichever side was declared first — so pinning them here -/// would test the wording instead of the algebra. -/// -/// -[TestSubject(typeof(AnyString))] -public sealed class StringShapeProperties { - - /// The alphabet an unconstrained generator draws from: ASCII letters and digits. - private const string DefaultAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - /// - /// The number of characters a string may reach above whatever its declared minimum is — the library's - /// unconstrained spread, mirrored here because the suite is black-box. - /// - private const int DefaultLengthSpread = 16; - - /// The digits alone, so an affix can be drawn from inside Numeric()'s own charset. - private const string DigitAlphabet = "0123456789"; - - /// - /// The source alphabet for a WithChars pool. It reaches beyond letters and digits — a custom pool is - /// precisely how a caller expresses an alphabet the named sets cannot — while staying free of surrogates, - /// which the pool rejects as an argument error. - /// - private const string PoolAlphabet = "ABCDEFabcdef0123456789-_.:/+*#@%&"; - - #region Statics members declarations - - /// - /// A non-empty affix of at most characters drawn from - /// . Affixes are drawn from an explicit alphabet rather than from arbitrary text - /// so that a charset conflict never fires by accident: the properties that probe the charset boundary declare - /// it deliberately, with an affix chosen for it. - /// - private static Gen Affix(string alphabet, int maxLength) { - return from characters in Gen.NonEmptyListOf(Gen.Elements(alphabet.ToCharArray())) - from length in Gen.Choose(1, maxLength) - select new string(characters.Take(length).ToArray()); - } - - /// A non-empty, duplicate-free character pool for . - private static Gen CharacterPool() { - return Gen.NonEmptyListOf(Gen.Elements(PoolAlphabet.ToCharArray())) - .Select(characters => new string(characters.Distinct().Take(12).ToArray())); - } - - /// - /// Applies one of the four character families by index — 0 Alpha, 1 Numeric, 2 - /// AlphaNumeric, 3 WithChars — so a property can quantify over the family itself instead of - /// restating the same invariant four times over. - /// - private static AnyString ApplyCharacterFamily(AnyString generator, int family, string pool) { - return family switch { - 0 => generator.Alpha(), - 1 => generator.Numeric(), - 2 => generator.AlphaNumeric(), - _ => generator.WithChars(pool) - }; - } - - /// Whether belongs to the alphabet the family selects. - private static bool AllowedByFamily(char character, int family, string pool) { - return family switch { - 0 => IsAsciiLetter(character), - 1 => IsAsciiDigit(character), - 2 => IsAsciiLetter(character) || IsAsciiDigit(character), - _ => pool.Contains(character) - }; - } - - /// Applies one of the two casings, so a property can quantify over the casing itself. - private static AnyString ApplyCasing(AnyString generator, bool upper) { - return upper ? generator.UpperCase() : generator.LowerCase(); - } - - /// Anchors at one end or the other, so a property can quantify over the end. - private static AnyString ApplyAffix(AnyString generator, bool asSuffix, string affix) { - return asSuffix ? generator.EndingWith(affix) : generator.StartingWith(affix); - } - - // char.IsAsciiLetter/IsAsciiDigit are .NET 7+, and this suite also runs on the netstandard2.0 asset from the - // net472 floor — so the two classifications the library itself uses are restated here. - - private static bool IsAsciiLetter(char character) { - return character is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; - } - - private static bool IsAsciiDigit(char character) { - return character is >= '0' and <= '9'; - } - - #endregion - - [Fact(DisplayName = "WithLength fixes the length exactly, for every length.")] - public void WithLengthFixesTheLengthExactly() { - Prop.ForAll(Generators.Count(40).ToArbitrary(), - length => Expect.EveryDraw(Any.String().WithLength(length), value => value.Length == length)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithMinLength is an inclusive floor: every draw is at least that long.")] - public void WithMinLengthIsAnInclusiveFloor() { - Prop.ForAll(Generators.Count(40).ToArbitrary(), - minimum => Expect.EveryDraw(Any.String().WithMinLength(minimum), value => value.Length >= minimum)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithMaxLength is an inclusive ceiling: every draw is at most that long.")] - public void WithMaxLengthIsAnInclusiveCeiling() { - Prop.ForAll(Generators.Count(40).ToArbitrary(), - maximum => Expect.EveryDraw(Any.String().WithMaxLength(maximum), value => value.Length <= maximum)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithMaxLength only caps: it never widens the draw beyond the unconstrained spread.")] - public void WithMaxLengthNeverWidensTheDraw() { - // ADR-0050: a maximum is a permission, not a size hint. It composes with the default spread instead of - // replacing it, so declaring a loose cap must keep yielding the small unconstrained string. The maxima - // generated here straddle the spread on both sides — that is where the old "maximum becomes the target" - // behaviour and this one disagree. - Prop.ForAll(Generators.WithEdges(Generators.Count(200), 0, 1, DefaultLengthSpread, DefaultLengthSpread + 1, 200).ToArbitrary(), - maximum => Expect.EveryDraw(Any.String().WithMaxLength(maximum), - value => value.Length <= Math.Min(maximum, DefaultLengthSpread))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A minimum, not a maximum, is what enlarges a string: the draw spans the spread above it.")] - public void WithMinLengthIsWhatEnlargesTheDraw() { - // The counterpart of the property above: since a maximum cannot widen the draw, a minimum is the only - // one-sided bound that can. Its draw stays within the spread above it, so asking for large strings costs - // exactly what was asked for and nothing more. - Prop.ForAll(Generators.WithEdges(Generators.Count(200), 0, 1, 200).ToArbitrary(), - minimum => Expect.EveryDraw(Any.String().WithMinLength(minimum), - value => value.Length >= minimum && value.Length <= minimum + DefaultLengthSpread)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithLengthBetween bounds the length inclusively, for every bound pair.")] - public void WithLengthBetweenIsAnInclusiveRange() { - Prop.ForAll(Generators.OrderedPair(Generators.Count(40)).ToArbitrary(), - bounds => Expect.EveryDraw(Any.String().WithLengthBetween(bounds.Min, bounds.Max), - value => value.Length >= bounds.Min && value.Length <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Crossed WithLengthBetween arguments are an argument error, never a silent swap.")] - public void CrossedLengthBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Generators.Count(40)).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || Expect.Throws(() => Any.String().WithLengthBetween(bounds.Max, bounds.Min))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "NonEmpty never yields the empty string, under every maximum length that leaves room.")] - public void NonEmptyNeverYieldsTheEmptyString() { - Prop.ForAll(Generators.Count(40).ToArbitrary(), - maximum => { - // NonEmpty is a minimum of one character, so capping the length at zero leaves nothing to - // draw: the pair is rejected at declaration, not at generation. - if (maximum == 0) { - return Expect.Throws(() => Any.String().NonEmpty().WithMaxLength(0)); - } - - return Expect.EveryDraw(Any.String().NonEmpty().WithMaxLength(maximum), - value => value.Length >= 1 && value.Length <= maximum); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "StartingWith anchors the prefix, whatever the prefix.")] - public void StartingWithAnchorsThePrefix() { - Prop.ForAll(Affix(DefaultAlphabet, 8).ToArbitrary(), - prefix => Expect.EveryDraw(Any.String().StartingWith(prefix), - value => value.StartsWith(prefix, StringComparison.Ordinal))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "EndingWith anchors the suffix, whatever the suffix.")] - public void EndingWithAnchorsTheSuffix() { - Prop.ForAll(Affix(DefaultAlphabet, 8).ToArbitrary(), - suffix => Expect.EveryDraw(Any.String().EndingWith(suffix), - value => value.EndsWith(suffix, StringComparison.Ordinal))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Containing embeds the value, whatever the value.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2249:Consider using String.Contains instead of String.IndexOf", - Justification = - "string.Contains(string, StringComparison) is not on the netstandard2.0 / net472 floor this suite runs " + - "against (ADR-0022); IndexOf with the same StringComparison.Ordinal carries the identical comparison and " + - "compiles on every leg. The rule is right on net10.0 only.")] - public void ContainingEmbedsTheValue() { - Prop.ForAll(Affix(DefaultAlphabet, 8).ToArbitrary(), - fragment => Expect.EveryDraw(Any.String().Containing(fragment), - // string.Contains(string, StringComparison) is not on the netstandard2.0 - // floor; IndexOf carries the same ordinal comparison. - value => value.IndexOf(fragment, StringComparison.Ordinal) >= 0)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Every character family draws only from its own alphabet, at every length.")] - public void CharacterFamiliesDrawOnlyFromTheirOwnAlphabet() { - Gen<(int Family, string Pool, int Length)> cases = - from family in Gen.Choose(0, 3) - from pool in CharacterPool() - from length in Generators.Count(20) - select (Family: family, Pool: pool, Length: length); - - Prop.ForAll(cases.ToArbitrary(), - testCase => Expect.EveryDraw(ApplyCharacterFamily(Any.String(), testCase.Family, testCase.Pool).WithLength(testCase.Length), - value => value.All(character => AllowedByFamily(character, testCase.Family, testCase.Pool)))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A casing constrains every cased character, at every length.")] - public void ACasingConstrainsEveryCasedCharacter() { - Gen<(bool Upper, int Length)> cases = - from upper in Gen.Elements(false, true) - from length in Generators.Count(20) - select (Upper: upper, Length: length); - - Prop.ForAll(cases.ToArbitrary(), - // A casing constrains the letters only: digits stay drawable under either of them. - testCase => Expect.EveryDraw(ApplyCasing(Any.String(), testCase.Upper).WithLength(testCase.Length), - value => value.All(character => testCase.Upper - ? !(character is >= 'a' and <= 'z') - : !(character is >= 'A' and <= 'Z')))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithLength and StartingWith hold together exactly when the length leaves room for the prefix.")] - public void ExactLengthAndPrefixHoldTogetherExactlyWhenThereIsRoom() { - Gen<(string Prefix, int Length)> cases = - from prefix in Affix(DefaultAlphabet, 8) - from length in Generators.Count(12) - select (Prefix: prefix, Length: length); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - // The boundary an example test can only sample: one character below it the pair is a - // declaration-time conflict, at it and above it the pair is generable — same call shape, - // legality decided by the argument values. - if (testCase.Length < testCase.Prefix.Length) { - return Expect.Throws( - () => Any.String().WithLength(testCase.Length).StartingWith(testCase.Prefix)); - } - - return Expect.EveryDraw(Any.String().WithLength(testCase.Length).StartingWith(testCase.Prefix), - value => value.Length == testCase.Length - && value.StartsWith(testCase.Prefix, StringComparison.Ordinal)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Numeric accepts a prefix exactly when every one of its characters is a digit.")] - public void NumericAcceptsAPrefixExactlyWhenEveryCharacterIsADigit() { - // Half the prefixes come from the digits themselves and half from the full default alphabet, so both sides of - // the boundary are reached often — Numeric().StartingWith("123") is valid where - // Numeric().StartingWith("ORD-") conflicts, on the argument value alone. - Gen prefixes = Gen.OneOf(Affix(DigitAlphabet, 4), Affix(DefaultAlphabet, 4)); - - Prop.ForAll(prefixes.ToArbitrary(), - prefix => prefix.All(IsAsciiDigit) - ? Expect.EveryDraw(Any.String().Numeric().StartingWith(prefix), - value => value.StartsWith(prefix, StringComparison.Ordinal) && value.All(IsAsciiDigit)) - : Expect.Throws(() => Any.String().Numeric().StartingWith(prefix))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Exclusions accumulate and never yield an excluded value, while preserving the declared shape.")] - public void ExclusionsNeverYieldAnExcludedValue() { - Prop.ForAll(Gen.Choose(3, 8).ToArbitrary(), - length => { - // The excluded values are drawn from the very generator they are then excluded from, so the - // exclusion is never vacuous. Three letters already allow 52^3 candidates, so removing a - // handful leaves the shape amply satisfiable: the redraw budget is not what is under test. - AnyString shaped = Any.String().Alpha().WithLength(length); - string[] excluded = Expect.Draws(shaped, 3).Distinct().ToArray(); - string banned = shaped.Generate(); - AnyString narrowed = shaped.Except(excluded).DifferentFrom(banned); - - return Expect.EveryDraw(narrowed, - value => value.Length == length - && value.All(IsAsciiLetter) - && !excluded.Contains(value) - && value != banned); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second character family conflicts unless it repeats the first, whichever two are combined.")] - public void ASecondCharacterFamilyConflictsUnlessItRepeatsTheFirst() { - Gen<(int First, int Second, string Pool)> cases = - from first in Gen.Choose(0, 3) - from second in Gen.Choose(0, 3) - from pool in CharacterPool() - select (First: first, Second: second, Pool: pool); - - Prop.ForAll(cases.ToArbitrary(), - // The pair may name the same family twice. Repeating it asks for the alphabet already in force, so - // it is a no-op and the alphabet still holds; naming a different family contradicts it. Both halves - // in one property, because the verdict follows the argument and not the call shape. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(ApplyCharacterFamily(ApplyCharacterFamily(Any.String(), testCase.First, testCase.Pool), - testCase.Second, testCase.Pool).NonEmpty(), - value => value.All(character => AllowedByFamily(character, testCase.First, testCase.Pool))) - : Expect.Throws( - () => ApplyCharacterFamily(ApplyCharacterFamily(Any.String(), testCase.First, testCase.Pool), - testCase.Second, testCase.Pool))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second casing conflicts unless it repeats the first, whichever two are combined.")] - public void ASecondCasingConflictsUnlessItRepeatsTheFirst() { - Gen<(bool First, bool Second)> cases = - from first in Gen.Elements(false, true) - from second in Gen.Elements(false, true) - select (First: first, Second: second); - - Prop.ForAll(cases.ToArbitrary(), - // Value-dependent legality: the same call is a no-op or a conflict depending on its argument, so - // the property branches on the value rather than on the call shape. Re-declaring the same casing - // asks for exactly the domain already in force; asking for the other one contradicts it. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(ApplyCasing(ApplyCasing(Any.String(), testCase.First), testCase.Second).NonEmpty(), - value => value.All(character => testCase.First ? !char.IsLower(character) : !char.IsUpper(character))) - : Expect.Throws( - () => ApplyCasing(ApplyCasing(Any.String(), testCase.First), testCase.Second))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second exact length conflicts unless it repeats the first, whatever the two lengths.")] - public void ASecondExactLengthConflictsUnlessItRepeatsTheFirst() { - Gen<(int First, int Second)> cases = - from first in Generators.Count(40) - from second in Generators.Count(40) - select (First: first, Second: second); - - Prop.ForAll(cases.ToArbitrary(), - // Repeating the same length is not a contradiction — the domain asked for is the one already in - // force — so it is a no-op, and the generator still produces exactly that length. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(Any.String().WithLength(testCase.First).WithLength(testCase.Second), - value => value.Length == testCase.First) - : Expect.Throws( - () => Any.String().WithLength(testCase.First).WithLength(testCase.Second))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second prefix or a second suffix conflicts unless it repeats the first, whatever the two values.")] - public void ASecondPrefixOrSuffixConflictsUnlessItRepeatsTheFirst() { - Gen<(bool AsSuffix, string First, string Second)> cases = - from asSuffix in Gen.Elements(false, true) - from first in Affix(DefaultAlphabet, 6) - from second in Affix(DefaultAlphabet, 6) - select (AsSuffix: asSuffix, First: first, Second: second); - - Prop.ForAll(cases.ToArbitrary(), - // Same rule, on the two affix slots: an identical re-declaration is a no-op and the affix still - // holds; a different value for the same slot is the contradiction. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(ApplyAffix(ApplyAffix(Any.String(), testCase.AsSuffix, testCase.First), - testCase.AsSuffix, testCase.Second), - value => testCase.AsSuffix ? value.EndsWith(testCase.First, StringComparison.Ordinal) - : value.StartsWith(testCase.First, StringComparison.Ordinal)) - : Expect.Throws( - () => ApplyAffix(ApplyAffix(Any.String(), testCase.AsSuffix, testCase.First), - testCase.AsSuffix, testCase.Second))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "OneOf on an already constrained generator narrows it, and conflicts only when the values leave nothing.")] - public void OneOfOnAConstrainedGeneratorNarrowsOrConflicts() { - Gen pools = Gen.NonEmptyListOf(Affix(DefaultAlphabet, 6)).Select(values => values.Distinct().ToArray()); - - Gen<(int Length, string[] Pool)> cases = - from length in Generators.Count(20) - from pool in pools - select (Length: length, Pool: pool); - - Prop.ForAll(cases.ToArbitrary(), - // The verdict follows the values, not the mere presence of a constraint: whatever the length and - // the pool, the generator survives exactly when some pooled value has that length, and every - // draw is then one of those values. - testCase => { - string[] surviving = testCase.Pool.Where(value => value.Length == testCase.Length).ToArray(); - - return surviving.Length == 0 - ? Expect.Throws( - () => Any.String().WithLength(testCase.Length).OneOf(testCase.Pool)) - : Expect.EveryDraw(Any.String().WithLength(testCase.Length).OneOf(testCase.Pool), - value => surviving.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - - /// - /// Quantified over a length constraint on purpose. Order is immaterial for every constraint the - /// constructive path accepts on its own, which is what this property covers; the one exception — a fragment - /// combination the layout budget rejects before any value set can reinterpret it as a filter — is a decision, - /// not an invariant, and belongs to the example suite that pins it. - /// - [Fact(DisplayName = "A length constraint and a value set reach the same domain whichever is declared first.")] - public void OneOfIsOrderIndependentWithALengthConstraint() { - Gen pools = Gen.NonEmptyListOf(Affix(DefaultAlphabet, 6)).Select(values => values.Distinct().ToArray()); - - Gen<(int Length, string[] Pool)> cases = - from length in Generators.Count(20) - from pool in pools - select (Length: length, Pool: pool); - - Prop.ForAll(cases.ToArbitrary(), - // Declaration order is a call-site accident; the domain it describes is not. Both orders conflict - // together, or both draw from the same surviving values — the verdict alone would not catch an - // order that survives with a different domain. - testCase => { - string[] surviving = testCase.Pool.Where(value => value.Length == testCase.Length).ToArray(); - - return surviving.Length == 0 - ? Expect.Throws(() => Any.String().OneOf(testCase.Pool).WithLength(testCase.Length)) - && Expect.Throws(() => Any.String().WithLength(testCase.Length).OneOf(testCase.Pool)) - : Expect.EveryDraw(Any.String().OneOf(testCase.Pool).WithLength(testCase.Length), value => surviving.Contains(value)) - && Expect.EveryDraw(Any.String().WithLength(testCase.Length).OneOf(testCase.Pool), value => surviving.Contains(value)); - }) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/TemporalProperties.cs b/JustDummies.PropertyTests/TemporalProperties.cs deleted file mode 100644 index 3b0f644a..00000000 --- a/JustDummies.PropertyTests/TemporalProperties.cs +++ /dev/null @@ -1,480 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the three temporal generators — , -/// and , including its offset dimension. Where the -/// example-based suite pins one anchor instant and a handful of hand-picked windows, these draw the instants, -/// the durations and the offsets themselves, over the whole ten-thousand-year domain and the whole ±14:00 -/// offset range, so a bound that overflows at the edge of the domain, or an offset that quietly pins itself to -/// one end of its range, is found and shrunk to its minimal counter-example. -/// -/// -/// Two traps shape almost every property here. The naming differs by type — and -/// say After/Before while says -/// GreaterThan/LessThan — and legality is value-dependent: Positive() on an interval -/// that lies below zero, After at the very top of the domain, or a WithOffset that no longer fits -/// the instant window already declared are conflicts rather than narrowings. The properties therefore decide -/// the expectation from the drawn value instead of assuming the call shape settles it. -/// -/// Instants are built from a drawn tick count rather than from FsCheck's own -/// arbitrary, so this file owns its domain and the distance it keeps from the edges that overflow. Bounds -/// are compared the way the library compares them — by ticks for (kind ignored) and -/// by for (rendering ignored). -/// -/// -[TestSubject(typeof(AnyDateTimeOffset))] -public sealed class TemporalProperties { - - #region Statics members declarations - - /// admits an offset in whole minutes within ±14:00; both offset constraints mirror that domain. - private const int MaxOffsetMinutes = 14 * 60; - - /// The narrowest offset range the variation property accepts, so the range always holds enough offsets for "it varies" to mean something. - private const int SpreadMinutes = 60; - - /// Draws taken when a property reasons over a batch rather than over each value in isolation. - private const int VariationDraws = 24; - - /// The widest window below the top of the domain the offset-tightening property opens — deliberately wider than ±14:00, so both sides of the legality line are drawn. - private const int CeilingWindowMinutes = 15 * 60; - - private static readonly DateTime AnchorInstant = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static readonly DateTimeOffset AnchorMoment = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); - - /// The number of ticks in the instant domain, shared by and — their maxima carry the same tick count. - private static readonly ulong DomainTicks = (ulong)DateTime.MaxValue.Ticks + 1UL; - - /// - /// A 64-bit word drawn over its whole range. FsCheck's own numeric generators are size-bounded and rarely - /// leave a hundred of zero, which for a tick count would mean "always within a microsecond of year one"; the - /// word is therefore assembled from three narrow draws, each spanning far less than a 32-bit range so the - /// span itself never has to be counted in 64 bits. - /// - private static Gen Bits64() { - return from high in Gen.Choose(-(1 << 21), (1 << 21) - 1) - from middle in Gen.Choose(0, (1 << 21) - 1) - from low in Gen.Choose(0, (1 << 21) - 1) - // Added rather than or-ed: the three fields occupy disjoint bit ranges, so the sum is the same - // word, without or-ing a sign-extended operand (CS0675) to carry `high`'s sign into bit 63. - select ((long)high << 42) + ((long)middle << 21) + low; - } - - /// - /// Arbitrary instants over the whole domain, its edges included and its - /// drawn rather than fixed: constraints compare by - /// and ignore the kind of the bounds they are handed, exactly as 's own operators do. - /// - private static Gen Instants() { - Gen anywhere = from bits in Bits64() - from kind in Gen.Choose(0, 2) - select new DateTime((long)((ulong)bits % DomainTicks), (DateTimeKind)kind); - - return Generators.WithEdges(anywhere, DateTime.MinValue, DateTime.MinValue.AddTicks(1), AnchorInstant, - DateTime.MaxValue.AddTicks(-1), DateTime.MaxValue); - } - - /// Arbitrary durations over the whole domain, biased towards zero and towards the edges an off-by-one hides behind. - private static Gen Durations() { - Gen anywhere = Bits64().Select(ticks => TimeSpan.FromTicks(ticks)); - - return Generators.WithEdges(anywhere, TimeSpan.MinValue, TimeSpan.FromTicks(long.MinValue + 1), TimeSpan.FromTicks(-1), - TimeSpan.Zero, TimeSpan.FromTicks(1), TimeSpan.FromTicks(long.MaxValue - 1), TimeSpan.MaxValue); - } - - /// - /// Arbitrary instants over the whole domain, expressed in UTC: a bound built at - /// the edge of the domain with a non-zero offset would overflow on construction, and the constraints compare - /// by anyway, so the offset of a bound carries no information. - /// - private static Gen Moments() { - Gen anywhere = Bits64().Select(bits => new DateTimeOffset((long)((ulong)bits % DomainTicks), TimeSpan.Zero)); - - return Generators.WithEdges(anywhere, DateTimeOffset.MinValue, DateTimeOffset.MinValue.AddTicks(1), AnchorMoment, - DateTimeOffset.MaxValue.AddTicks(-1), DateTimeOffset.MaxValue); - } - - /// A legal offset in whole minutes: anywhere within ±14:00, biased towards the ends of the range and towards UTC. - private static Gen OffsetMinutes() { - return Generators.WithEdges(Gen.Choose(-MaxOffsetMinutes, MaxOffsetMinutes), -MaxOffsetMinutes, -1, 0, 1, MaxOffsetMinutes); - } - - /// - /// Two legal whole-minute offsets that are strictly apart. This is the shape every "declared once" conflict - /// needs: re-declaring the same offset range is idempotent by design, so a property quantifying over - /// two independent draws would expect a conflict that never comes. - /// - private static Gen<(int Low, int High)> DistinctOffsetMinutes() { - return from low in Gen.Choose(-MaxOffsetMinutes, MaxOffsetMinutes - 1) - from high in Gen.Choose(low + 1, MaxOffsetMinutes) - select (Low: low, High: high); - } - - /// Turns a whole number of minutes into an offset without going through the double-based factories, so no rounding can creep between the draw and the call. - private static TimeSpan Minutes(int minutes) { - return TimeSpan.FromTicks(minutes * TimeSpan.TicksPerMinute); - } - - /// - /// Returns true when throws an — that - /// exact type — naming . The exactness is the point: - /// derives from , so a mere - /// assignability check could not tell a malformed offset from an out-of-range one, and the offset dimension - /// distinguishes the two deliberately. - /// - private static bool ThrowsArgumentExceptionNaming(Action action, string parameterName) { - try { - action(); - - return false; - } catch (ArgumentException exception) { - return exception.GetType() == typeof(ArgumentException) && exception.ParamName == parameterName; - } - } - - #endregion - - [Fact(DisplayName = "DateTime: Between contains — every draw falls within the declared inclusive instants.")] - public void DateTimeBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Instants()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.DateTime().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTime: Between with equal instants pins the value, for every instant.")] - public void DateTimeBetweenWithEqualBoundsPins() { - Prop.ForAll(Instants().ToArbitrary(), - instant => Expect.EveryDraw(Any.DateTime().Between(instant, instant), drawn => drawn.Ticks == instant.Ticks)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTime: After and Before are exclusive, and conflict at the edge of the domain.")] - public void DateTimeAfterAndBeforeAreExclusive() { - Prop.ForAll(Instants().ToArbitrary(), - instant => { - // No instant lies after DateTime.MaxValue, and none before DateTime.MinValue: there the - // exclusive bound empties the domain, and the library owes a conflict at the fluent call - // rather than a failure at Generate(). - bool after = instant == DateTime.MaxValue - ? Expect.Throws(() => Any.DateTime().After(instant)) - : Expect.EveryDraw(Any.DateTime().After(instant), value => value > instant); - bool before = instant == DateTime.MinValue - ? Expect.Throws(() => Any.DateTime().Before(instant)) - : Expect.EveryDraw(Any.DateTime().Before(instant), value => value < instant); - - return after && before; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTime: AfterOrEqualTo and BeforeOrEqualTo are inclusive, right up to the edge of the domain.")] - public void DateTimeInclusiveBoundsAreInclusive() { - Prop.ForAll(Instants().ToArbitrary(), - instant => Expect.EveryDraw(Any.DateTime().AfterOrEqualTo(instant), value => value >= instant) - && Expect.EveryDraw(Any.DateTime().BeforeOrEqualTo(instant), value => value <= instant)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTime: every generated value carries Utc kind, whatever kind the bounds carry.")] - public void DateTimeGeneratedValuesCarryUtcKind() { - Prop.ForAll(Instants().ToArbitrary(), - instant => Expect.EveryDraw(Any.DateTime(), value => value.Kind == DateTimeKind.Utc) - && Expect.EveryDraw(Any.DateTime().AfterOrEqualTo(instant), value => value.Kind == DateTimeKind.Utc) - && Expect.EveryDraw(Any.DateTime().BeforeOrEqualTo(instant), value => value.Kind == DateTimeKind.Utc)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTime: crossed Between instants are an argument error naming the start, never a silent swap.")] - public void DateTimeCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Instants()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || ThrowsArgumentExceptionNaming(() => Any.DateTime().Between(bounds.Max, bounds.Min), "start")) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: Between contains — every draw falls within the declared inclusive durations.")] - public void TimeSpanBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Durations()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.TimeSpan().Between(bounds.Min, bounds.Max), - value => value >= bounds.Min && value <= bounds.Max)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: GreaterThan and LessThan are exclusive, and conflict at the edge of the domain.")] - public void TimeSpanExclusiveBoundsAreExclusive() { - Prop.ForAll(Durations().ToArbitrary(), - duration => { - // The duration surface names its bounds GreaterThan/LessThan where the instant surface says - // After/Before; the invariant underneath is the same one. - bool greater = duration == TimeSpan.MaxValue - ? Expect.Throws(() => Any.TimeSpan().GreaterThan(duration)) - : Expect.EveryDraw(Any.TimeSpan().GreaterThan(duration), value => value > duration); - bool less = duration == TimeSpan.MinValue - ? Expect.Throws(() => Any.TimeSpan().LessThan(duration)) - : Expect.EveryDraw(Any.TimeSpan().LessThan(duration), value => value < duration); - - return greater && less; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: GreaterThanOrEqualTo and LessThanOrEqualTo are inclusive, right up to the edge of the domain.")] - public void TimeSpanInclusiveBoundsAreInclusive() { - Prop.ForAll(Durations().ToArbitrary(), - duration => Expect.EveryDraw(Any.TimeSpan().GreaterThanOrEqualTo(duration), value => value >= duration) - && Expect.EveryDraw(Any.TimeSpan().LessThanOrEqualTo(duration), value => value <= duration)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: Positive and Negative are strict about zero, and conflict with an interval lying on the wrong side of it.")] - public void TimeSpanPositiveAndNegativeAreStrictAboutZero() { - Prop.ForAll(Generators.OrderedPair(Durations()).ToArbitrary(), - bounds => { - AnyTimeSpan interval = Any.TimeSpan().Between(bounds.Min, bounds.Max); - - // Value-dependent legality: the very same call narrows an interval that still reaches past - // zero and empties one that does not, so the expectation is read off the drawn bounds. An - // interval touching zero from one side only is exactly the corner an example would miss. - bool positive = bounds.Max > TimeSpan.Zero - ? Expect.EveryDraw(interval.Positive(), - value => value > TimeSpan.Zero && value >= bounds.Min && value <= bounds.Max) - : Expect.Throws(() => interval.Positive()); - bool negative = bounds.Min < TimeSpan.Zero - ? Expect.EveryDraw(interval.Negative(), - value => value < TimeSpan.Zero && value >= bounds.Min && value <= bounds.Max) - : Expect.Throws(() => interval.Negative()); - - return positive && negative; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: Zero pins any interval that holds zero, and conflicts with every interval that does not.")] - public void TimeSpanZeroPinsTheIntervalsThatHoldIt() { - Prop.ForAll(Generators.OrderedPair(Durations()).ToArbitrary(), - bounds => { - AnyTimeSpan interval = Any.TimeSpan().Between(bounds.Min, bounds.Max); - - if (bounds.Min <= TimeSpan.Zero && bounds.Max >= TimeSpan.Zero) { - return Expect.EveryDraw(interval.Zero(), value => value == TimeSpan.Zero); - } - - return Expect.Throws(() => interval.Zero()); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: NonZero removes zero from any interval, and empties the one interval holding nothing else.")] - public void TimeSpanNonZeroExcludesZero() { - Prop.ForAll(Generators.OrderedPair(Durations()).ToArbitrary(), - bounds => { - AnyTimeSpan interval = Any.TimeSpan().Between(bounds.Min, bounds.Max); - - // Excluding zero from the interval pinned to zero leaves nothing to draw: that is a conflict - // at the fluent call, the duration counterpart of excluding the single value of a pinned - // integer interval. - if (bounds.Min == TimeSpan.Zero && bounds.Max == TimeSpan.Zero) { - return Expect.Throws(() => interval.NonZero()); - } - - return Expect.EveryDraw(interval.NonZero(), - value => value != TimeSpan.Zero && value >= bounds.Min && value <= bounds.Max); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "TimeSpan: crossed Between durations are an argument error naming the minimum, never a silent swap.")] - public void TimeSpanCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Durations()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || ThrowsArgumentExceptionNaming(() => Any.TimeSpan().Between(bounds.Max, bounds.Min), "minimum")) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTimeOffset: Between contains — every draw falls within the declared inclusive instants.")] - public void DateTimeOffsetBetweenContainsEveryDraw() { - Prop.ForAll(Generators.OrderedPair(Moments()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.DateTimeOffset().Between(bounds.Min, bounds.Max), - value => value.UtcTicks >= bounds.Min.UtcTicks && value.UtcTicks <= bounds.Max.UtcTicks)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTimeOffset: After and Before are exclusive, and conflict at the edge of the domain.")] - public void DateTimeOffsetAfterAndBeforeAreExclusive() { - Prop.ForAll(Moments().ToArbitrary(), - instant => { - bool after = instant == DateTimeOffset.MaxValue - ? Expect.Throws(() => Any.DateTimeOffset().After(instant)) - : Expect.EveryDraw(Any.DateTimeOffset().After(instant), value => value.UtcTicks > instant.UtcTicks); - bool before = instant == DateTimeOffset.MinValue - ? Expect.Throws(() => Any.DateTimeOffset().Before(instant)) - : Expect.EveryDraw(Any.DateTimeOffset().Before(instant), value => value.UtcTicks < instant.UtcTicks); - - return after && before; - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTimeOffset: AfterOrEqualTo and BeforeOrEqualTo are inclusive, right up to the edge of the domain.")] - public void DateTimeOffsetInclusiveBoundsAreInclusive() { - Prop.ForAll(Moments().ToArbitrary(), - instant => Expect.EveryDraw(Any.DateTimeOffset().AfterOrEqualTo(instant), value => value.UtcTicks >= instant.UtcTicks) - && Expect.EveryDraw(Any.DateTimeOffset().BeforeOrEqualTo(instant), value => value.UtcTicks <= instant.UtcTicks)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTimeOffset: with no offset constraint every draw carries the UTC offset, whatever the instant window.")] - public void DateTimeOffsetDefaultsToTheUtcOffset() { - Prop.ForAll(Generators.OrderedPair(Moments()).ToArbitrary(), - bounds => Expect.EveryDraw(Any.DateTimeOffset(), value => value.Offset == TimeSpan.Zero) - && Expect.EveryDraw(Any.DateTimeOffset().Between(bounds.Min, bounds.Max), value => value.Offset == TimeSpan.Zero)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "DateTimeOffset: crossed Between instants are an argument error naming the start, never a silent swap.")] - public void DateTimeOffsetCrossedBoundsAreAnArgumentError() { - Prop.ForAll(Generators.OrderedPair(Moments()).ToArbitrary(), - bounds => bounds.Min == bounds.Max - || ThrowsArgumentExceptionNaming(() => Any.DateTimeOffset().Between(bounds.Max, bounds.Min), "start")) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithOffset: every draw carries exactly the pinned offset, for every legal offset.")] - public void WithOffsetPinsTheOffset() { - Prop.ForAll(OffsetMinutes().ToArbitrary(), - minutes => { - TimeSpan offset = Minutes(minutes); - - // Reaching the assertion at all is half the property: the local ticks are the UTC ticks - // shifted by the offset, so without the instant range the library tightens on declaration, - // the extreme offsets would overflow inside Generate() long before the offset is compared. - return Expect.EveryDraw(Any.DateTimeOffset().WithOffset(offset), - value => value.Offset == offset - && value.UtcTicks + offset.Ticks >= DateTime.MinValue.Ticks - && value.UtcTicks + offset.Ticks <= DateTime.MaxValue.Ticks); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithOffset: an instant window with no room left for the offset conflicts; one with room keeps both.")] - public void WithOffsetTightensTheInstantWindow() { - Prop.ForAll((from floorMinutes in Gen.Choose(1, CeilingWindowMinutes) - from offsetMinutes in OffsetMinutes() - select (floorMinutes, offsetMinutes)).ToArbitrary(), - testCase => { - DateTimeOffset floor = DateTimeOffset.MaxValue.AddTicks(-testCase.floorMinutes * TimeSpan.TicksPerMinute); - TimeSpan offset = Minutes(testCase.offsetMinutes); - AnyDateTimeOffset windowed = Any.DateTimeOffset().After(floor); - - // Value-dependent legality again: the last hours of the domain can host a +02:00 offset but - // not a +14:00 one, and host every negative offset whatever the window — the window must be - // wider than the shift the offset applies to the local ticks. - if (testCase.floorMinutes <= testCase.offsetMinutes) { - return Expect.Throws(() => windowed.WithOffset(offset)); - } - - return Expect.EveryDraw(windowed.WithOffset(offset), - value => value.Offset == offset && value.UtcTicks > floor.UtcTicks); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithOffsetBetween: every draw's offset lies within the inclusive range, in whole minutes.")] - public void WithOffsetBetweenStaysWithinItsRange() { - Prop.ForAll(Generators.OrderedPair(OffsetMinutes()).ToArbitrary(), - bounds => { - TimeSpan minimum = Minutes(bounds.Min); - TimeSpan maximum = Minutes(bounds.Max); - - // The degenerate range is kept: a range collapsed onto one offset must pin it, not reject it. - return Expect.EveryDraw(Any.DateTimeOffset().WithOffsetBetween(minimum, maximum), - value => value.Offset >= minimum - && value.Offset <= maximum - && value.Offset.Ticks % TimeSpan.TicksPerMinute == 0); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithOffsetBetween: over enough draws the offset really varies — it is drawn, not pinned to an end of the range.")] - public void WithOffsetBetweenVariesTheOffset() { - Prop.ForAll((from low in Gen.Choose(-MaxOffsetMinutes, MaxOffsetMinutes - SpreadMinutes) - from high in Gen.Choose(low + SpreadMinutes, MaxOffsetMinutes) - select (low, high)).ToArbitrary(), - bounds => { - TimeSpan minimum = Minutes(bounds.low); - TimeSpan maximum = Minutes(bounds.high); - - // The range is drawn at least an hour wide, so it always offers more than sixty offsets: a - // generator that quietly pinned the offset to one end — the failure a single-draw assertion - // cannot see, since one end of the range satisfies the bounds perfectly — surfaces here. - List draws = Expect.Draws(Any.DateTimeOffset().WithOffsetBetween(minimum, maximum), VariationDraws); - - return draws.Select(value => value.Offset).Distinct().Count() > 1 - && draws.All(value => value.Offset >= minimum && value.Offset <= maximum); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Offsets: an offset carrying a sub-minute remainder is an argument error, wherever the remainder falls.")] - public void SubMinuteOffsetsAreAnArgumentError() { - Prop.ForAll((from minutes in Gen.Choose(-(MaxOffsetMinutes - 1), MaxOffsetMinutes - 1) - from remainder in Gen.Choose(1, (int)TimeSpan.TicksPerMinute - 1) - select TimeSpan.FromTicks(minutes * TimeSpan.TicksPerMinute + remainder)).ToArbitrary(), - offset => - // The drawn offsets stay inside ±14:00, so it is the whole-minute rule that fires and not - // the range rule — argument validation runs in that order, and the two are told apart by - // the exact exception type. - ThrowsArgumentExceptionNaming(() => Any.DateTimeOffset().WithOffset(offset), "offset") - && ThrowsArgumentExceptionNaming(() => Any.DateTimeOffset().WithOffsetBetween(offset, TimeSpan.Zero), "minimum") - && ThrowsArgumentExceptionNaming(() => Any.DateTimeOffset().WithOffsetBetween(TimeSpan.Zero, offset), "maximum")) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Offsets: a whole-minute offset beyond ±14:00 is out of range, however far beyond it lands.")] - public void OffsetsBeyondFourteenHoursAreOutOfRange() { - Prop.ForAll((from magnitude in Gen.Choose(MaxOffsetMinutes + 1, 10 * MaxOffsetMinutes) - from mirrored in Gen.Choose(0, 1) - select Minutes(mirrored == 0 ? magnitude : -magnitude)).ToArbitrary(), - // Whole minutes by construction, so the whole-minute rule passes and the range rule is the one - // under test. - offset => Expect.Throws(() => Any.DateTimeOffset().WithOffset(offset)) - && Expect.Throws(() => Any.DateTimeOffset().WithOffsetBetween(offset, offset))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithOffsetBetween: a crossed offset range is an argument error naming the minimum, never a silent swap.")] - public void CrossedOffsetRangeIsAnArgumentError() { - Prop.ForAll(DistinctOffsetMinutes().ToArbitrary(), - bounds => ThrowsArgumentExceptionNaming( - () => Any.DateTimeOffset().WithOffsetBetween(Minutes(bounds.High), Minutes(bounds.Low)), "minimum")) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Offsets: the offset dimension is declared once — a second, different offset constraint conflicts whichever form it takes.")] - public void TheOffsetDimensionIsDeclaredOnce() { - Prop.ForAll(DistinctOffsetMinutes().ToArbitrary(), - bounds => { - TimeSpan low = Minutes(bounds.Low); - TimeSpan high = Minutes(bounds.High); - - return Expect.Throws(() => Any.DateTimeOffset().WithOffset(low).WithOffset(high)) - && Expect.Throws(() => Any.DateTimeOffset().WithOffset(low).WithOffsetBetween(low, high)) - && Expect.Throws(() => Any.DateTimeOffset().WithOffsetBetween(low, high).WithOffset(high)) - // Re-declaring the very same range is idempotent, not a conflict: the dimension is - // declared once, which is not the same thing as called once. - && Expect.EveryDraw(Any.DateTimeOffset().WithOffset(low).WithOffset(low), value => value.Offset == low); - }) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.PropertyTests/UriProperties.cs b/JustDummies.PropertyTests/UriProperties.cs deleted file mode 100644 index 474f6b28..00000000 --- a/JustDummies.PropertyTests/UriProperties.cs +++ /dev/null @@ -1,584 +0,0 @@ -#region Usings declarations - -using FsCheck; -using FsCheck.Fluent; - -using JetBrains.Annotations; - -#endregion - -namespace JustDummies.PropertyTests; - -/// -/// Property-based tests for the family. The example-based suite pins one host -/// (api.example.com), one port (8443) and one segment count (3), and can only prove the -/// builder right for those; these quantify over the whole option space of each family — every host the library -/// accepts, every port in 1..65535, every small segment count, and every on/off combination of the optional -/// components — so a shape that renders an unparsable URI for one combination is found and shrunk to its minimal -/// counter-example rather than missed. -/// -/// -/// The family narrowings are a typed progression: a category error such as a port on a mailto or a fragment -/// on a WebSocket is a compile error, not an exception, so there is nothing here to assert about it. Only two -/// conflicts survive to run time — a second scheme constraint and a second path constraint — and both are proven -/// below over arbitrary arguments. -/// -[TestSubject(typeof(AnyUri))] -public sealed class UriProperties { - - private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; - private const string LowerAlphaNum = "abcdefghijklmnopqrstuvwxyz0123456789"; - private const string Unreserved = "abcdefghijklmnopqrstuvwxyz0123456789-_"; - - /// - /// Which path constraint a case declares. Unconstrained declares none at all — the third state a - /// nullable segment count cannot express, and the only one that leaves the segment count to the draw. - /// - private enum PathChoice { - - Unconstrained, - Root, - Exact - - } - - /// Which of the three WithUserInfo overloads a case calls. - private enum UserInfoChoice { - - Arbitrary, // WithUserInfo() — both parts drawn - UserOnly, // WithUserInfo(user) — user pinned, password drawn - UserAndPassword // WithUserInfo(user, password) — both parts pinned - - } - - #region Statics members declarations - - /// - /// The schemes the library is allowed to emit. file is deliberately absent: a file path does not - /// round-trip identically across target frameworks, so the unconstrained draw must never reach it. - /// - private static readonly HashSet EmittableSchemes = ["http", "https", "ws", "wss", "ftp", "mailto"]; - - /// - /// Arbitrary hosts the library accepts, drawn from the very alphabet UriSpec draws its own hosts from: a - /// DNS label, optionally followed by a second one. Staying inside that alphabet keeps every generated host on - /// the legal side of WithHost, so the property exercises the pinning rather than the validation. - /// - private static Gen Hosts() { - return from first in Labels() - from second in Gen.OneOf(Gen.Constant(string.Empty), Labels().Select(label => "." + label)) - select first + second; - } - - /// - /// A DNS-safe label: one letter, then up to seven letters or digits. Capping the tail matters — an unbounded - /// FsCheck list would eventually exceed the 63-character label ceiling and turn a pinning property into an - /// argument-validation one. - /// - private static Gen Labels() { - return from head in Gen.Elements(LowerLetters.ToCharArray()) - from tail in Gen.ListOf(Gen.Elements(LowerAlphaNum.ToCharArray())) - select head.ToString() + new string(tail.Take(7).ToArray()); - } - - /// - /// Arbitrary user-info parts and mailto local-parts: non-empty, starting with a letter or a digit, and drawn - /// from the unreserved characters RequireUserInfoPart accepts. - /// - private static Gen UnreservedParts() { - return from head in Gen.Elements(LowerAlphaNum.ToCharArray()) - from tail in Gen.ListOf(Gen.Elements(Unreserved.ToCharArray())) - select head.ToString() + new string(tail.Take(7).ToArray()); - } - - /// Arbitrary legal ports — the whole 1..65535 range, not the two or three a hand-written test would pick. - private static Gen Ports() { - return Gen.Choose(1, 65535); - } - - /// An option the caller may leave undeclared: null stands for "the call was never made". - private static Gen Optional(Gen values) - where T : struct { - return Gen.OneOf(Gen.Constant((T?)null), values.Select(value => (T?)value)); - } - - /// Counts the non-empty segments of a path, the way a reader counts the slashes. - private static int SegmentCount(string path) { - return path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Length; - } - - /// Applies one of the two web scheme pins — the pair that conflicts with itself in either order. - private static AnyWebUri PinScheme(AnyWebUri generator, bool secure) { - return secure ? generator.UsingHttps() : generator.UsingHttp(); - } - - /// Applies one of the two WebSocket scheme pins — the pair that conflicts with itself in either order. - private static AnyWebSocketUri PinScheme(AnyWebSocketUri generator, bool secure) { - return secure ? generator.UsingWss() : generator.UsingWs(); - } - - /// The scheme a pinned web generator must draw — the read side of . - private static string WebScheme(bool secure) { - return secure ? "https" : "http"; - } - - /// The scheme a pinned WebSocket generator must draw — the read side of . - private static string WebSocketScheme(bool secure) { - return secure ? "wss" : "ws"; - } - - /// Declares a path constraint: a segment count, or the root path when is null. - private static AnyWebUri PinPath(AnyWebUri generator, int? segments) { - return segments.HasValue ? generator.WithPathSegments(segments.Value) : generator.WithoutPath(); - } - - /// - /// Projects a path declaration onto a positive segment count for the relative leg, which has no - /// WithoutPath(). Injective on purpose — null and 0 are two different declarations and must - /// stay two different counts — and never zero, because a relative reference with no segment, query, fragment or - /// root is the empty string, which is not a valid URI reference. - /// - private static int RelativeSegments(int? declared) { - return declared is null ? 1 : declared.Value + 2; - } - - /// Declares on a web generator exactly the components the case asks for, and nothing else. - private static AnyWebUri WebGeneratorFor((string Host, int? Port, PathChoice Path, int Segments, bool? Secure, bool Query, bool Fragment) testCase) { - AnyWebUri generator = Any.Uri().Web().WithHost(testCase.Host); - if (testCase.Secure.HasValue) { generator = PinScheme(generator, testCase.Secure.Value); } - if (testCase.Port.HasValue) { generator = generator.WithPort(testCase.Port.Value); } - if (testCase.Path == PathChoice.Root) { generator = generator.WithoutPath(); } - if (testCase.Path == PathChoice.Exact) { generator = generator.WithPathSegments(testCase.Segments); } - if (testCase.Query) { generator = generator.WithQuery(); } - if (testCase.Fragment) { generator = generator.WithFragment(); } - - return generator; - } - - /// Whether one web draw carries the declared components — and, for the undeclared ones, nothing. - private static bool WebDrawCarries(Uri value, (string Host, int? Port, PathChoice Path, int Segments, bool? Secure, bool Query, bool Fragment) testCase) { - bool pathHolds = testCase.Path switch { - PathChoice.Root => value.AbsolutePath == "/", - PathChoice.Exact => SegmentCount(value.AbsolutePath) == testCase.Segments, - // An undeclared path draws 0 to 2 segments. - _ => SegmentCount(value.AbsolutePath) <= 2 - }; - - return value.IsAbsoluteUri - && (testCase.Secure.HasValue - ? value.Scheme == WebScheme(testCase.Secure.Value) - : value.Scheme is "http" or "https") - && value.Host == testCase.Host - && (!testCase.Port.HasValue || value.Port == testCase.Port.Value) - && pathHolds - && value.UserInfo.Length == 0 - && (value.Query.Length > 0) == testCase.Query - && (value.Fragment.Length > 0) == testCase.Fragment; - } - - /// Declares on a WebSocket generator exactly the components the case asks for. - private static AnyWebSocketUri WebSocketGeneratorFor((string Host, PathChoice Path, int Segments, bool? Secure, bool Query) testCase) { - AnyWebSocketUri generator = Any.Uri().WebSocket().WithHost(testCase.Host); - if (testCase.Secure.HasValue) { generator = PinScheme(generator, testCase.Secure.Value); } - if (testCase.Path == PathChoice.Root) { generator = generator.WithoutPath(); } - if (testCase.Path == PathChoice.Exact) { generator = generator.WithPathSegments(testCase.Segments); } - if (testCase.Query) { generator = generator.WithQuery(); } - - return generator; - } - - /// - /// Whether one WebSocket draw carries the declared components. Asserted on the rendered string rather than on - /// the parsed components: ws and wss are not authority-parsed identically on every framework, and the rendering - /// is what the library actually promises. - /// - private static bool WebSocketDrawCarries(Uri value, (string Host, PathChoice Path, int Segments, bool? Secure, bool Query) testCase) { - string rendered = value.OriginalString; - - return value.IsAbsoluteUri - && (testCase.Secure.HasValue - ? value.Scheme == WebSocketScheme(testCase.Secure.Value) - : value.Scheme is "ws" or "wss") - && rendered.StartsWith(value.Scheme + "://" + testCase.Host, StringComparison.Ordinal) - && rendered.Contains('?') == testCase.Query - && !rendered.Contains('#') - && !rendered.Contains('@'); - } - - /// Declares on a mailto generator whichever address parts the case pins. - private static AnyMailtoUri MailtoGeneratorFor((string Local, string Domain, bool PinLocal, bool PinDomain, bool Headers) testCase) { - AnyMailtoUri generator = Any.Uri().Mailto(); - if (testCase.PinLocal) { generator = generator.WithLocalPart(testCase.Local); } - if (testCase.PinDomain) { generator = generator.WithDomain(testCase.Domain); } - if (testCase.Headers) { generator = generator.WithHeaders(); } - - return generator; - } - - /// Whether one mailto draw renders local@domain, honouring whichever part the case pinned. - private static bool MailtoDrawRenders(Uri value, (string Local, string Domain, bool PinLocal, bool PinDomain, bool Headers) testCase) { - if (!value.IsAbsoluteUri || value.Scheme != "mailto") { return false; } - if (!value.OriginalString.StartsWith("mailto:", StringComparison.Ordinal)) { return false; } - - string address = value.OriginalString.Substring("mailto:".Length); - int headerStart = address.IndexOf('?'); - if ((headerStart >= 0) != testCase.Headers) { return false; } - if (headerStart >= 0) { address = address.Substring(0, headerStart); } - - string[] parts = address.Split('@'); - - return parts.Length == 2 - && parts[0].Length > 0 - && parts[1].Length > 0 - && (!testCase.PinLocal || parts[0] == testCase.Local) - && (!testCase.PinDomain || parts[1] == testCase.Domain); - } - - /// Declares on a relative generator exactly the path, query and fragment the case asks for. - private static AnyRelativeUri RelativeGeneratorFor((int? Segments, bool Rooted, bool Query, bool Fragment) testCase) { - AnyRelativeUri generator = Any.Uri().Relative(); - if (testCase.Rooted) { generator = generator.Rooted(); } - if (testCase.Segments.HasValue) { generator = generator.WithPathSegments(testCase.Segments.Value); } - if (testCase.Query) { generator = generator.WithQuery(); } - if (testCase.Fragment) { generator = generator.WithFragment(); } - - return generator; - } - - /// Whether one relative draw is a relative reference carrying exactly the declared path, query and fragment. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1870:Use a cached 'SearchValues' instance", - Justification = - "SearchValues arrived in .NET 8 and this suite also runs on the .NET Framework 4.7.2 support floor " + - "(ADR-0022, build/Net472TestFloor.props), where the type does not exist. The rule is right on net10.0 only; " + - "IndexOfAny over a two-character array carries the same meaning on both legs. Same downlevel wall as " + - "SYSLIB1045 and CA1510 (ADR-0058).")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1865:Use char overload", - Justification = - "string.StartsWith(char) is not on the .NET Framework 4.7.2 support floor this suite also runs on " + - "(ADR-0022): measured, the net472 leg rejects it with CS1503, cannot convert from 'char' to 'string'. " + - "The explicit StringComparison.Ordinal overload compiles on both legs and states the comparison it uses.")] - private static bool RelativeDrawCarries(Uri value, (int? Segments, bool Rooted, bool Query, bool Fragment) testCase) { - string reference = value.OriginalString; - int cut = reference.IndexOfAny(new[] { '?', '#' }); - int segments = SegmentCount(cut < 0 ? reference : reference.Substring(0, cut)); - - return !value.IsAbsoluteUri - && reference.Length > 0 - && (!testCase.Rooted || reference.StartsWith("/", StringComparison.Ordinal)) - && (testCase.Segments.HasValue ? segments == testCase.Segments.Value : segments <= 2) - && reference.Contains('?') == testCase.Query - && reference.Contains('#') == testCase.Fragment; - } - - #endregion - - [Fact(DisplayName = "Every unconstrained draw is a valid URI of an emittable family, whatever the seed.")] - public void UnconstrainedDrawsAreValidUris() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => Expect.EveryDraw(Any.WithSeed(seed).Uri(), - value => { - UriKind kind = value.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative; - - return value.OriginalString.Length > 0 - // Every component is ASCII by construction: an internationalized host - // would not round-trip identically across target frameworks. - && value.OriginalString.All(character => character < 128) - && (!value.IsAbsoluteUri || EmittableSchemes.Contains(value.Scheme)) - && Uri.TryCreate(value.OriginalString, kind, out _); - }, - 16)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "The unconstrained draw reaches all five families, whatever the seed.")] - public void UnconstrainedReachesEveryFamily() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - // One family in five per draw, so 120 draws leave a miss far below any rate that could make - // this flaky, while a hand-written test can only ever assert it for the one seed it picked. - HashSet seen = []; - foreach (Uri value in Expect.Draws(Any.WithSeed(seed).Uri(), 120)) { - seen.Add(value.IsAbsoluteUri ? value.Scheme : "relative"); - } - - return (seen.Contains("http") || seen.Contains("https")) - && (seen.Contains("ws") || seen.Contains("wss")) - && seen.Contains("ftp") - && seen.Contains("mailto") - && seen.Contains("relative"); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A web draw carries exactly the components declared on it, in every combination.")] - public void WebDrawsCarryTheDeclaredComponents() { - Gen<(string Host, int? Port, PathChoice Path, int Segments, bool? Secure, bool Query, bool Fragment)> cases = - from host in Hosts() - from port in Optional(Ports()) - from path in Gen.Elements(PathChoice.Unconstrained, PathChoice.Root, PathChoice.Exact) - from segments in Generators.Count(6) - from secure in Optional(Gen.Elements(true, false)) - from query in Gen.Elements(true, false) - from fragment in Gen.Elements(true, false) - select (host, port, path, segments, secure, query, fragment); - - Prop.ForAll(cases.ToArbitrary(), - testCase => Expect.EveryDraw(WebGeneratorFor(testCase), value => WebDrawCarries(value, testCase))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "Declared user-info reaches the URI, whichever of the three overloads declared it.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2692:\"IndexOf\" checks should not be for positive numbers", - Justification = - "0 is deliberately excluded. The check asserts that a user-info draw renders user:password with a NON-EMPTY " + - "user, so a colon at index 0 — an empty local part — must fail the property, which is exactly what > 0 says.")] - public void UserInfoShapesReachTheUri() { - Gen<(UserInfoChoice Choice, string User, string Password)> cases = - from choice in Gen.Elements(UserInfoChoice.Arbitrary, UserInfoChoice.UserOnly, UserInfoChoice.UserAndPassword) - from user in UnreservedParts() - from password in UnreservedParts() - select (choice, user, password); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - AnyWebUri generator = testCase.Choice switch { - UserInfoChoice.UserOnly => Any.Uri().Web().WithUserInfo(testCase.User), - UserInfoChoice.UserAndPassword => Any.Uri().Web().WithUserInfo(testCase.User, testCase.Password), - _ => Any.Uri().Web().WithUserInfo() - }; - - return Expect.EveryDraw(generator, - value => testCase.Choice switch { - // Only the user is pinned, so the password is merely required to be there. - UserInfoChoice.UserOnly => value.UserInfo.StartsWith(testCase.User + ":", StringComparison.Ordinal) - && value.UserInfo.Length > testCase.User.Length + 1, - UserInfoChoice.UserAndPassword => value.UserInfo == testCase.User + ":" + testCase.Password, - _ => value.UserInfo.IndexOf(':') > 0 - }); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A WebSocket draw uses ws or wss and never carries user-info or a fragment.")] - public void WebSocketDrawsAreWebSocketUris() { - Gen<(string Host, PathChoice Path, int Segments, bool? Secure, bool Query)> cases = - from host in Hosts() - from path in Gen.Elements(PathChoice.Unconstrained, PathChoice.Root, PathChoice.Exact) - from segments in Generators.Count(6) - from secure in Optional(Gen.Elements(true, false)) - from query in Gen.Elements(true, false) - select (host, path, segments, secure, query); - - Prop.ForAll(cases.ToArbitrary(), - testCase => Expect.EveryDraw(WebSocketGeneratorFor(testCase), value => WebSocketDrawCarries(value, testCase))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An FTP draw uses the ftp scheme with its user-info, and never a query or a fragment.")] - public void FtpDrawsAreFtpUris() { - Gen<(string Host, int? Port, PathChoice Path, int Segments, bool Credentials, string User, string Password)> cases = - from host in Hosts() - from port in Optional(Ports()) - from path in Gen.Elements(PathChoice.Unconstrained, PathChoice.Root, PathChoice.Exact) - from segments in Generators.Count(6) - from credentials in Gen.Elements(true, false) - from user in UnreservedParts() - from password in UnreservedParts() - select (host, port, path, segments, credentials, user, password); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - AnyFtpUri generator = Any.Uri().Ftp().WithHost(testCase.Host); - if (testCase.Port.HasValue) { generator = generator.WithPort(testCase.Port.Value); } - if (testCase.Path == PathChoice.Root) { generator = generator.WithoutPath(); } - if (testCase.Path == PathChoice.Exact) { generator = generator.WithPathSegments(testCase.Segments); } - if (testCase.Credentials) { generator = generator.WithUserInfo(testCase.User, testCase.Password); } - - return Expect.EveryDraw(generator, - value => value.IsAbsoluteUri - && value.Scheme == "ftp" - && value.Host == testCase.Host - && (!testCase.Port.HasValue || value.Port == testCase.Port.Value) - && value.UserInfo == (testCase.Credentials ? testCase.User + ":" + testCase.Password : string.Empty) - // An FTP URI has neither, and the builder does not even expose them. - && value.OriginalString.IndexOf('?') < 0 - && value.OriginalString.IndexOf('#') < 0); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A mailto draw renders local@domain, honouring whichever part is pinned.")] - public void MailtoDrawsRenderTheDeclaredAddress() { - Gen<(string Local, string Domain, bool PinLocal, bool PinDomain, bool Headers)> cases = - from local in UnreservedParts() - from domain in Hosts() - from pinLocal in Gen.Elements(true, false) - from pinDomain in Gen.Elements(true, false) - from headers in Gen.Elements(true, false) - select (local, domain, pinLocal, pinDomain, headers); - - Prop.ForAll(cases.ToArbitrary(), - testCase => Expect.EveryDraw(MailtoGeneratorFor(testCase), value => MailtoDrawRenders(value, testCase))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A relative draw is a relative reference carrying exactly the declared path, query and fragment.")] - public void RelativeDrawsAreRelativeReferences() { - Gen<(int? Segments, bool Rooted, bool Query, bool Fragment)> cases = - from segments in Optional(Generators.Count(6)) - from rooted in Gen.Elements(true, false) - from query in Gen.Elements(true, false) - from fragment in Gen.Elements(true, false) - select (segments, rooted, query, fragment); - - Prop.ForAll(cases.ToArbitrary(), - testCase => { - AnyRelativeUri generator = RelativeGeneratorFor(testCase); - - // An explicit zero-segment path with nothing else to carry it renders the empty string, which is - // not a valid reference: the one shape of this family that cannot generate. - if (testCase.Segments == 0 && !testCase.Rooted && !testCase.Query && !testCase.Fragment) { - return Expect.Throws(() => generator.Generate()); - } - - return Expect.EveryDraw(generator, value => RelativeDrawCarries(value, testCase)); - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An undeclared relative path never renders empty: a zero-segment draw is resolved to one segment.")] - public void UnconstrainedRelativeNeverRendersEmpty() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - // The draw picks 0, 1 or 2 segments; the 0 that would render the empty string is silently resolved - // to a single arbitrary segment, so the count never leaves 1..2 and generation never fails. - seed => Expect.EveryDraw(Any.WithSeed(seed).Uri().Relative(), - value => !value.IsAbsoluteUri - && value.OriginalString.Length > 0 - && SegmentCount(value.OriginalString) is 1 or 2, - 24)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "An explicit zero-segment relative path with nothing else fails at generation, carrying the seed.")] - public void EmptyRelativeFailsAtGenerationCarryingTheSeed() { - Prop.ForAll(Generators.Seed().ToArbitrary(), - seed => { - try { - Any.WithSeed(seed).Uri().Relative().WithPathSegments(0).Generate(); - - return false; - } catch (AnyGenerationException error) { - return error.Seed == seed; - } - }) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "WithPort pins the port over the whole legal range, and rejects anything outside it as an argument.")] - public void PortsArePinnedOrRejectedAsArguments() { - Gen candidates = Generators.WithEdges(Gen.OneOf(Ports(), Generators.Int32()), - -1, 0, 1, 65535, 65536, int.MinValue, int.MaxValue); - - Prop.ForAll(candidates.ToArbitrary(), - port => port is < 1 or > 65535 - ? Expect.Throws(() => Any.Uri().Web().WithPort(port)) - : Expect.EveryDraw(Any.Uri().Web().WithPort(port), value => value.Port == port)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "The argument-less WithPort yields an explicit, non-default port on every draw.")] - public void ArbitraryPortsAreExplicitAndNonDefault() { - Prop.ForAll(Hosts().ToArbitrary(), - host => Expect.EveryDraw(Any.Uri().Web().WithHost(host).WithPort(), - // Drawn above every default the library emits, so the port is always visible. - value => value.Port >= 1025 && value.Port <= 65535 && !value.IsDefaultPort)) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second scheme pin conflicts unless it repeats the first, whichever pair and whichever order.")] - public void SecondSchemePinConflictsUnlessItRepeatsTheFirst() { - Gen<(bool First, bool Second)> cases = from first in Gen.Elements(true, false) - from second in Gen.Elements(true, false) - select (first, second); - - Prop.ForAll(cases.ToArbitrary(), - // Pinning the same scheme twice asks for the scheme already in force, so it is a no-op and the - // generator still produces it; pinning the other one contradicts it. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(PinScheme(PinScheme(Any.Uri().Web(), testCase.First), testCase.Second), uri => uri.IsAbsoluteUri) - && Expect.EveryDraw(PinScheme(PinScheme(Any.Uri().WebSocket(), testCase.First), testCase.Second), uri => uri.IsAbsoluteUri) - : Expect.Throws( - () => PinScheme(PinScheme(Any.Uri().Web(), testCase.First), testCase.Second)) - && Expect.Throws( - () => PinScheme(PinScheme(Any.Uri().WebSocket(), testCase.First), testCase.Second))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A second path constraint conflicts unless it repeats the first, whichever pair and whichever segment counts.")] - public void SecondPathConstraintConflictsUnlessItRepeatsTheFirst() { - // null stands for WithoutPath(), a count for WithPathSegments(count): all four ordered pairs conflict. - Gen<(int? First, int? Second)> cases = from first in Optional(Generators.Count(6)) - from second in Optional(Generators.Count(6)) - select (first, second); - - Prop.ForAll(cases.ToArbitrary(), - // Repeating the SAME path declaration is a no-op; any other pair contradicts. The relative leg has - // no WithoutPath(), so it only ever exercises the doubled segment count. - testCase => testCase.First == testCase.Second - ? Expect.EveryDraw(PinPath(PinPath(Any.Uri().Web(), testCase.First), testCase.Second), uri => uri.IsAbsoluteUri) - // Shifted off zero: a relative reference with no segment, query, fragment or root - // is the empty string, which is not a valid URI reference — a pre-existing refusal - // this property is not about. - && Expect.EveryDraw(Any.Uri().Relative().WithPathSegments(RelativeSegments(testCase.First)).WithPathSegments(RelativeSegments(testCase.Second)), - uri => !uri.IsAbsoluteUri) - : Expect.Throws( - () => PinPath(PinPath(Any.Uri().Web(), testCase.First), testCase.Second)) - && Expect.Throws( - () => Any.Uri().Relative().WithPathSegments(RelativeSegments(testCase.First)).WithPathSegments(RelativeSegments(testCase.Second)))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A negative segment count is an argument error even when a path constraint already conflicts with it.")] - public void NegativeSegmentCountsAreArgumentErrorsBeforeConflicts() { - Gen negatives = Generators.WithEdges(Generators.Count(8).Select(offset => -1 - offset), -1, int.MinValue); - - Gen<(int? Declared, int Count)> cases = from declared in Optional(Generators.Count(6)) - from count in negatives - select (declared, count); - - Prop.ForAll(cases.ToArbitrary(), - // Argument validation runs before conflict checking, so the argument error wins over the conflict - // the second path constraint would otherwise raise. - testCase => Expect.Throws( - () => PinPath(Any.Uri().Web(), testCase.Declared).WithPathSegments(testCase.Count))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A host carrying a non-ASCII character is rejected as an argument, wherever the character sits.")] - public void NonAsciiHostsAreRejectedAsArguments() { - Gen cases = from host in Hosts() - from character in Gen.Elements('é', 'ü', 'ñ', 'ß', 'д', '中') - from index in Gen.Choose(0, host.Length) - select host.Insert(index, character.ToString()); - - Prop.ForAll(cases.ToArbitrary(), - // An internationalized host is refused at the call site, pointing at punycode — never silently - // accepted, because it would not round-trip identically across target frameworks. - spoiled => Expect.Throws(() => Any.Uri().Web().WithHost(spoiled))) - .QuickCheckThrowOnFailure(); - } - - [Fact(DisplayName = "A user-info part carrying a reserved character is rejected as an argument, wherever it sits.")] - public void ReservedUserInfoCharactersAreRejectedAsArguments() { - Gen cases = from part in UnreservedParts() - from character in Gen.Elements(':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '(', ')', '*', '+', ',', ';', '=', '%', ' ') - from index in Gen.Choose(0, part.Length) - select part.Insert(index, character.ToString()); - - Prop.ForAll(cases.ToArbitrary(), - spoiled => Expect.Throws(() => Any.Uri().Web().WithUserInfo(spoiled)) - && Expect.Throws(() => Any.Uri().Mailto().WithLocalPart(spoiled))) - .QuickCheckThrowOnFailure(); - } - -} diff --git a/JustDummies.UnitTests/AmbientSeedScopeTests.cs b/JustDummies.UnitTests/AmbientSeedScopeTests.cs deleted file mode 100644 index b06ad576..00000000 --- a/JustDummies.UnitTests/AmbientSeedScopeTests.cs +++ /dev/null @@ -1,271 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The scope form of reproducibility: a handle a caller opens and disposes itself, for a test-framework adapter -/// that observes a test through before/after hooks and therefore has no delegate to wrap (ADR-0035). The seed -/// behaviour must match Any.Reproducibly; what the scope adds is the replay snippet a -/// generation-failure diagnostic names, so a run pinned from outside the test body never advertises a call the -/// test does not contain. -/// -[TestSubject(typeof(Any))] -public sealed class AmbientSeedScopeTests { - - #region Statics members declarations - - private static (int, string) Batch() { - return (Any.Int32().Generate(), Any.String().NonEmpty().Generate()); - } - - #endregion - - [Fact(DisplayName = "UseSeed pins the ambient context, so the same seed yields the same values.")] - public void UseSeedPinsTheAmbientContext() { - (int, string) first; - (int, string) second; - - using (Any.UseSeed(1234)) { first = Batch(); } - using (Any.UseSeed(1234)) { second = Batch(); } - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "UseSeed with different seeds produces different sequences.")] - public void DifferentSeedsDiffer() { - (int, string) fromOne; - (int, string) fromTwo; - - using (Any.UseSeed(1)) { fromOne = Batch(); } - using (Any.UseSeed(2)) { fromTwo = Batch(); } - - Check.That(fromTwo).IsNotEqualTo(fromOne); - } - - [Fact(DisplayName = "UseSeed pins the same sequence as Reproducibly for the same seed.")] - public void UseSeedMatchesReproducibly() { - (int, string) fromScope; - (int, string) fromRunner = default; - - using (Any.UseSeed(4242)) { fromScope = Batch(); } - Any.Reproducibly(4242, () => { fromRunner = Batch(); }); - - Check.That(fromScope).IsEqualTo(fromRunner); - } - - [Fact(DisplayName = "Disposing the scope restores the previous context with its draw sequence intact.")] - public void DisposingRestoresThePreviousContext() { - (int, string) first; - (int, string) second; - (int, string) restoredFirst; - (int, string) restoredSecond; - - using (Any.UseSeed(7)) { - first = Batch(); - second = Batch(); - } - - using (Any.UseSeed(7)) { - restoredFirst = Batch(); - // A nested scope draws from its own generator; the outer one must neither be consumed nor reset by - // it, so the outer sequence resumes exactly where it was interrupted. - using (Any.UseSeed(99)) { Batch(); } - restoredSecond = Batch(); - } - - Check.That(restoredFirst).IsEqualTo(first); - Check.That(restoredSecond).IsEqualTo(second); - } - - [Fact(DisplayName = "Nested scopes pin the inner seed while they are open.")] - public void NestedScopesPinTheInnerSeed() { - (int, string) standalone; - (int, string) nested; - - using (Any.UseSeed(555)) { standalone = Batch(); } - - using (Any.UseSeed(111)) { - using (Any.UseSeed(555)) { nested = Batch(); } - } - - Check.That(nested).IsEqualTo(standalone); - } - - [Fact(DisplayName = "Disposing the scope twice is harmless.")] - public void DisposingTwiceIsHarmless() { - IDisposable scope = Any.UseSeed(31); - - scope.Dispose(); - - Check.ThatCode(() => scope.Dispose()).DoesNotThrow(); - } - - [Fact(DisplayName = "Disposing an outer scope out of order leaves the still-open inner scope's seed pinned.")] - public void OutOfOrderDisposalKeepsTheInnerScopePinned() { - // Reference: what seed 2 yields as the sole, top-of-stack scope. - (int, string) reference; - using (Any.UseSeed(2)) { reference = Batch(); } - - // Open two scopes, then dispose the OUTER first — the inner (seed 2) is still open, so the ambient - // context must still be pinned to seed 2. A blind restore-the-previous unpins it instead, leaving the - // still-open inner scope drawing from a fresh unseeded generator. - IDisposable outer = Any.UseSeed(1); - IDisposable inner = Any.UseSeed(2); - outer.Dispose(); - (int, string) whileInnerStillOpen = Batch(); - inner.Dispose(); - - Check.That(whileInnerStillOpen).IsEqualTo(reference); - } - - [Fact(DisplayName = "Out-of-order disposal does not leak a pinned seed to what runs next.")] - public void OutOfOrderDisposalDoesNotLeakASeed() { - // Reference: seed 1's sequence, to prove it is NOT what a later, unseeded draw replays. - (int, string) seedOneSequence; - using (Any.UseSeed(1)) { seedOneSequence = Batch(); } - - // Dispose the outer first, then the inner: a blind restore-the-previous now reinstates seed 1's frame, - // stranding it as the ambient context for whatever runs next. - IDisposable outer = Any.UseSeed(1); - IDisposable inner = Any.UseSeed(2); - outer.Dispose(); - inner.Dispose(); - - // Both scopes are closed, so the ambient context must be unseeded again — not pinned to seed 1. - (int, string) afterAllDisposed = Batch(); - - Check.That(afterAllDisposed).IsNotEqualTo(seedOneSequence); - } - - [Fact(DisplayName = "Disposing a middle scope out of order leaves the top scope and its live ancestors intact.")] - public void OutOfOrderMiddleDisposalPreservesTheStack() { - (int, string) seedThree; - using (Any.UseSeed(3)) { seedThree = Batch(); } - (int, string) seedOne; - using (Any.UseSeed(1)) { seedOne = Batch(); } - - IDisposable bottom = Any.UseSeed(1); - IDisposable middle = Any.UseSeed(2); - IDisposable top = Any.UseSeed(3); - - // Dispose the middle scope early: the top (seed 3) is untouched and stays pinned. - middle.Dispose(); - (int, string) topStillPinned = Batch(); - - // Now the top goes: it must skip the already-disposed middle and land on the bottom (seed 1). - top.Dispose(); - (int, string) afterTopDisposed = Batch(); - - bottom.Dispose(); - - Check.That(topStillPinned).IsEqualTo(seedThree); - Check.That(afterTopDisposed).IsEqualTo(seedOne); - } - - [Fact(DisplayName = "The scope does not leak across parallel execution contexts.")] - public async Task TheScopeDoesNotLeakAcrossExecutionContexts() { - (int, string) inside; - (int, string) outside = default; - - using (Any.UseSeed(2026)) { - inside = Batch(); - - // A task started inside the scope inherits it, so run the probe on a context that never saw it. - await Task.Run(() => { outside = Batch(); }, TestContext.Current.CancellationToken); - } - - // The probe drew from a context whose scope was never entered, so it cannot have replayed the pinned - // sequence. (An unseeded draw could coincide, but not across both components of the batch.) - Check.That(outside).IsNotEqualTo(inside); - } - - [Fact(DisplayName = "Without a replay snippet, a generation failure names Any.Reproducibly.")] - public void WithoutAnInstructionTheFailureNamesTheDelegateRunner() { - AnyGenerationException caught; - - using (Any.UseSeed(1234)) { - caught = Assert.Throws( - () => Any.Int32().As(_ => throw new InvalidOperationException("rejected")).Generate()); - } - - Check.That(caught.Seed).IsEqualTo(1234); - Check.That(caught.Message).Contains("The arbitrary values were seeded with 1234"); - Check.That(caught.Message).Contains("Any.Reproducibly(1234, ...)"); - } - - [Fact(DisplayName = "With a replay snippet, a generation failure names it instead of Any.Reproducibly.")] - public void WithAnInstructionTheFailureNamesIt() { - AnyGenerationException caught; - - using (Any.UseSeed(1234, "[Reproducible(Seed = 1234)]")) { - caught = Assert.Throws( - () => Any.Int32().As(_ => throw new InvalidOperationException("rejected")).Generate()); - } - - Check.That(caught.Message).Contains("The arbitrary values were seeded with 1234"); - Check.That(caught.Message).Contains("[Reproducible(Seed = 1234)]"); - // The whole point: the reader is never pointed at a call their test does not contain. - Check.That(caught.Message).Not.Contains("Any.Reproducibly"); - } - - [Fact(DisplayName = "The replay snippet also reaches the partial-replay guidance.")] - public void TheInstructionReachesThePartialReplayGuidance() { - AnyGenerationException caught; - - using (Any.UseSeed(777, "[Reproducible(Seed = 777)]")) { - IAny foreign = new ForeignAny(); - caught = Assert.Throws( - () => Any.Combine(Any.Int32(), foreign, (_, _) => throw new InvalidOperationException("rejected")).Generate()); - } - - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Contains("[Reproducible(Seed = 777)]"); - Check.That(caught.Message).Not.Contains("Any.Reproducibly"); - } - - [Fact(DisplayName = "The replay snippet is scoped: it does not outlive the scope that supplied it.")] - public void TheInstructionDoesNotOutliveItsScope() { - using (Any.UseSeed(1, "[Reproducible(Seed = 1)]")) { } - - AnyGenerationException caught; - using (Any.UseSeed(2)) { - caught = Assert.Throws( - () => Any.Int32().As(_ => throw new InvalidOperationException("rejected")).Generate()); - } - - Check.That(caught.Message).Contains("Any.Reproducibly(2, ...)"); - Check.That(caught.Message).Not.Contains("[Reproducible"); - } - - [Fact(DisplayName = "UseSeed rejects a null replay snippet.")] - public void UseSeedRejectsANullInstruction() { - Check.ThatCode(() => Any.UseSeed(1, null!)).Throws(); - } - - [Theory(DisplayName = "UseSeed rejects a blank replay snippet.")] - [InlineData("")] - [InlineData(" ")] - public void UseSeedRejectsABlankInstruction(string instruction) { - Check.ThatCode(() => Any.UseSeed(1, instruction)).Throws(); - } - - #region Nested types - - /// A generator carrying no random source, so a derivation over it cannot promise a full replay. - private sealed class ForeignAny : IAny { - - public int Generate() { - return 42; - } - - } - - #endregion - -} diff --git a/JustDummies.UnitTests/AnyCollectionTests.cs b/JustDummies.UnitTests/AnyCollectionTests.cs deleted file mode 100644 index b94bab7f..00000000 --- a/JustDummies.UnitTests/AnyCollectionTests.cs +++ /dev/null @@ -1,657 +0,0 @@ -#region Usings declarations - -using System.Globalization; -using System.Runtime.CompilerServices; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyCollectionTests { - - #region Statics members declarations - - private const int SampleCount = 200; - - private enum Suit { - - Clubs, - Diamonds, - Hearts, - Spades - - } - - #endregion - - [Fact(DisplayName = "ListOf: unconstrained draws vary in size, stay within 0..8, and hold elements from the item generator.")] - public void ListOfUnconstrained() { - HashSet sizes = []; - for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 9)).Generate(); - sizes.Add(list.Count); - Check.That(list.Count).IsGreaterOrEqualThan(0); - Check.That(list.Count).IsLessOrEqualThan(8); - Check.That(list).ContainsOnlyElementsThatMatch(value => value is >= 1 and <= 9); - } - Check.That(sizes.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "ListOf: the count family fixes, floors, caps and ranges the size.")] - public void ListOfCountFamily() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.ListOf(Any.Int32()).WithCount(5).Generate().Count).IsEqualTo(5); - Check.That(Any.ListOf(Any.Int32()).Empty().Generate().Count).IsEqualTo(0); - Check.That(Any.ListOf(Any.Int32()).NonEmpty().Generate().Count).IsStrictlyGreaterThan(0); - Check.That(Any.ListOf(Any.Int32()).WithMinCount(3).Generate().Count).IsGreaterOrEqualThan(3); - Check.That(Any.ListOf(Any.Int32()).WithMaxCount(2).Generate().Count).IsLessOrEqualThan(2); - - int ranged = Any.ListOf(Any.Int32()).WithCountBetween(4, 6).Generate().Count; - Check.That(ranged is >= 4 and <= 6).IsTrue(); - } - } - - [Fact(DisplayName = "ListOf: contradictory count constraints fail eagerly naming both sides.")] - public void ListOfCountConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.ListOf(Any.Int32()).WithCount(3).WithMinCount(5)); - Check.That(conflict.Message).Contains("WithCount(3)"); - - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithMinCount(5).WithMaxCount(3)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCount(2).WithCount(3)).Throws(); - } - - [Fact(DisplayName = "ListOf: count constraints validate their arguments.")] - public void ListOfCountValidation() { - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCount(-1)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithMinCount(-1)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCountBetween(6, 4)).Throws(); - Check.ThatCode(() => Any.ListOf(null!)).Throws(); - } - - [Fact(DisplayName = "A produced count is refused above the ceiling; the bound just below it is accepted.")] - public void ProducedCountsAreCeilinged() { - // ADR-0050, at the two coordinates a mis-written comparison would pass: the ceiling and the first value past it. - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCount(1_000_001)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithMinCount(1_000_001)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Int32()).WithCountBetween(1_000_001, 2_000_000)).Throws(); - - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCount(1_000_000)).DoesNotThrow(); - } - - [Fact(DisplayName = "An enormous count names the caller's parameter instead of exhausting memory.")] - public void AnEnormousCountNamesTheCallersParameter() { - // Regression: WithCount(int.MaxValue) used to fail on the allocation itself, and WithMaxCount(int.MaxValue) - // to grind for minutes filling a collection sized after the cap. Both are now decided at declaration. - ArgumentOutOfRangeException error = Assert.Throws(() => Any.ListOf(Any.Int32()).WithCount(int.MaxValue)); - - Check.That(error.ParamName).IsEqualTo("count"); - } - - [Fact(DisplayName = "A maximum accepts any non-negative count and still yields a small collection.")] - public void AMaximumIsACapNotASizeHint() { - Check.That(Any.ListOf(Any.Int32()).WithMaxCount(int.MaxValue).Generate().Count).IsStrictlyLessThan(9); - Check.That(Any.ArrayOf(Any.Int32()).WithMaxCount(4_000_000).Generate().Length).IsStrictlyLessThan(9); - } - - [Fact(DisplayName = "Distinct: a wide-domain distinct list holds only distinct elements.")] - public void DistinctOverAWideDomain() { - for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 1000)).WithCount(20).Distinct().Generate(); - Check.That(list.Count).IsEqualTo(20); - Check.That(new HashSet(list).Count).IsEqualTo(20); - } - } - - [Fact(DisplayName = "Distinct: a count beyond the element cardinality conflicts eagerly, naming the shortfall.")] - public void DistinctCardinalityConflictsEagerly() { - ConflictingAnyConstraintException fromBool = Assert.Throws( - () => Any.SetOf(Any.Boolean()).WithCount(3)); - Check.That(fromBool.Message).Contains("2 distinct value"); - - Check.ThatCode(() => Any.SetOf(Any.Enum()).WithMinCount(5)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Int32().Between(1, 3)).WithCount(5)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32().Between(1, 3)).WithCount(5).Distinct()).Throws(); - // Order-independent: turning distinct on after the count is set conflicts just the same. - Check.ThatCode(() => Any.ListOf(Any.Boolean()).WithCount(3).Distinct()).Throws(); - } - - [Fact(DisplayName = "Distinct: an unknowable small domain cannot be detected early, so a shortfall surfaces at generation.")] - public void DistinctFallbackThrowsAtGeneration() { - // '.As' erases the cardinality hint, so the conflict cannot be seen at declaration — the bounded dedup-draw - // fallback catches it while generating instead. - IAny opaque = Any.Int32().Between(1, 3).As(value => value); - - Check.ThatCode(() => Any.SetOf(opaque).WithCount(5).Generate()).Throws(); - } - - [Fact(DisplayName = "Distinct: over an element type without value equality the requirement is inert, and the collection holds repeats.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Composition", "JD028:Distinctness is declared over an element type that has no value equality", - Justification = - "The inert distinctness IS the subject. This pins the silent behaviour JD028 reports, which the library cannot report itself: from " + - "its side the requirement is met, because the draws really are pairwise unequal under the comparer it was given.")] - public void DistinctOverReferenceEqualityIsInert() { - // Percentage has no value equality, and '.As' builds a NEW instance per draw, so the default comparer can - // never call two of them equal. Six 'distinct' elements over a two-value domain therefore succeed — and hold - // repeats. The count is not statistical: six draws from a domain of two cannot show more than two values. - List percentages = Any.ListOf(Any.Int32().Between(1, 2).As(Percentage.Create)).Distinct().WithCount(6).Generate(); - - Check.That(percentages.Count).IsEqualTo(6); - Check.That(percentages.Select(percentage => percentage.Value).Distinct().Count()).IsStrictlyLessThan(3); - } - - [Fact(DisplayName = "SetOf: elements are always distinct and drawn from the item generator.")] - public void SetOfIsDistinct() { - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().Between(1, 500)).WithCount(10).Generate(); - Check.That(set.Count).IsEqualTo(10); - Check.That(set).ContainsOnlyElementsThatMatch(value => value is >= 1 and <= 500); - } - } - - [Fact(DisplayName = "SetOf: a comparer merges values, so cardinality is only an upper bound and the fallback still guards.")] - public void SetOfHonoursAComparer() { - IEqualityComparer modTen = new ModuloComparer(10); - - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(5).Generate(); - Check.That(set.Count).IsEqualTo(5); - List classes = set.Select(value => value % 10).ToList(); - Check.That(classes.Count).IsEqualTo(new HashSet(classes).Count); - } - - // Only ten residue classes exist, so twenty distinct-under-the-comparer elements are impossible; the raw - // cardinality (1000) hides that, so it can only be caught while drawing. - Check.ThatCode(() => Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(20).Generate()).Throws(); - } - - [Fact(DisplayName = "Containing: a required value is present, and a distinct duplicate requirement conflicts.")] - public void ContainingPlacesValues() { - for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 9)).WithCount(5).Containing(777).Generate(); - Check.That(list).Contains(777); - Check.That(list.Count).IsEqualTo(5); - } - - Check.ThatCode(() => Any.ListOf(Any.Int32()).WithCount(1).Containing(1).Containing(2)).Throws(); - - ConflictingAnyConstraintException duplicate = Assert.Throws( - () => Any.SetOf(Any.Int32()).Containing(7).Containing(7)); - Check.That(duplicate.Message).Contains("more than once"); - } - - [Fact(DisplayName = "Containing: a value drawn from a generator is forced into the collection.")] - public void ContainingFromAGenerator() { - for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 9)).NonEmpty().ContainingAny(Any.Int32().OneOf(4242)).Generate(); - Check.That(list).Contains(4242); - } - } - - [Fact(DisplayName = "Containing: a fixed value outside the element domain extends the effective cardinality (issue #188).")] - public void ContainingOutsideDomainExtendsCardinality() { - // The motivating case: {1, 2, 3} is satisfiable — 3 is supplied directly and lies outside the {1, 2} the - // generator can produce, so only two elements must be drawn from it. - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Generate(); - Check.That(set).Contains(1, 2, 3); - Check.That(set.Count).IsEqualTo(3); - } - - // The same reasoning holds for a distinct list and for several out-of-domain values at once. Dictionary keys - // run through the very same CollectionState path, so the correction reaches them too, now exercised directly - // through AnyDictionary.ContainingKey (see DictionaryContainingKeyOutsideDomainExtendsCardinality). - for (int i = 0; i < SampleCount; i++) { - HashSet list = [.. Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate()]; - Check.That(list).Contains(1, 2, 3); - - HashSet quad = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).Containing(4).WithCount(4).Generate(); - Check.That(quad).Contains(1, 2, 3, 4); - Check.That(quad.Count).IsEqualTo(4); - } - } - - [Fact(DisplayName = "Containing: a value already inside the element domain does not inflate the cardinality, so an impossible count still conflicts.")] - public void ContainingInsideDomainDoesNotInflate() { - // 1 is already producible by the generator, so it adds no capacity: three distinct values over {1, 2} remain - // impossible and must still fail eagerly, naming the shortfall. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3)); - Check.That(conflict.Message).Contains("2 distinct value"); - - // Mixed: 1 is inside the domain, 5 is outside — effective capacity is 2 + 1 = 3. Four is over the top; three - // is exactly reachable as {1, 2, 5}. - Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(4)).Throws(); - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(3).Generate(); - Check.That(set).Contains(1, 2, 5); - } - } - - [Fact(DisplayName = "Containing: the effective cardinality is order-independent across Distinct, Containing and the count.")] - public void EffectiveCardinalityIsOrderIndependent() { - // Every ordering of the same three constraints reaches the same verdict — accepted, because 3 is outside the - // domain — since Distinct() re-runs the whole validation on the accumulated state. - for (int i = 0; i < SampleCount; i++) { - Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(3).Distinct().Generate())).Contains(1, 2, 3); - Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(3).WithCount(3).Generate())).Contains(1, 2, 3); - Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate())).Contains(1, 2, 3); - } - - // And rejected whatever the order, because the contained value is inside the domain. - Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(1).Distinct()).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(1).WithCount(3)).Throws(); - Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3).Distinct()).Throws(); - } - - [Fact(DisplayName = "Containing: a comparer stricter than the default one does not turn a satisfiable spec into a conflict.")] - public void AComparerStricterThanTheDefaultDoesNotCauseAFalseConflict() { - // Regression: FixedOutsideCount asked the element generator's cardinality hint whether a pinned value was - // already inside its domain, and that hint answers under the DEFAULT comparer. Under reference equality the - // two Tag(1) below are two distinct values, so { pooled, pinned } is a legal two-element distinct list — but - // the hint reported the pinned one as already-inside, the effective domain stayed at one, and the declaration - // was refused for a specification the collection can satisfy. The comment defending it claimed a custom - // comparer "can only merge values, never create new ones"; a stricter one splits them instead. - Tag pooled = new(1); - Tag pinned = new(1); - - Check.That(pooled).IsEqualTo(pinned); // equal by value - Check.That(ReferenceEquals(pooled, pinned)).IsFalse(); // distinct by reference - - List list = Any.ListOf(Any.OneOf(pooled)) - .Distinct(new ReferenceComparer()) - .Containing(pinned) - .WithCount(2) - .Generate(); - - Check.That(list.Count).IsEqualTo(2); - Check.That(list.Count(element => ReferenceEquals(element, pinned))).IsEqualTo(1); - Check.That(list.Count(element => ReferenceEquals(element, pooled))).IsEqualTo(1); - } - - [Fact(DisplayName = "Containing: the default comparer still refuses a pinned value the element generator already covers.")] - public void TheDefaultComparerStillRefusesAnInDomainPinnedValue() { - // The other side of the same guard: relaxing the eager check under a CUSTOM comparer must not relax it when - // there is none. Without a comparer the hint answers under the very equality the collection will use, so a - // pinned value inside the domain does not extend it and the conflict is still caught at declaration. - Tag pooled = new(1); - Tag pinned = new(1); - - Check.ThatCode(() => Any.ListOf(Any.OneOf(pooled)).Distinct().Containing(pinned).WithCount(2)) - .Throws(); - } - - [Fact(DisplayName = "Containing: a near-maximum element cardinality plus an out-of-domain value does not overflow into a false conflict.")] - public void ContainingNearMaximumCardinalityDoesNotOverflow() { - // Between(0, long.MaxValue - 1) advertises long.MaxValue distinct values; the additive form base + extras - // would overflow to a negative and reject spuriously. The subtractive check stays correct. - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int64().Between(0, long.MaxValue - 1)).Containing(-1L).WithCount(3).Generate(); - Check.That(set).Contains(-1L); - Check.That(set.Count).IsEqualTo(3); - } - } - - [Fact(DisplayName = "ContainingAny stays conservative: no eager false conflict, and an opaque shortfall surfaces at generation.")] - public void ContainingAnyDefersToGeneration() { - // The generator drawn from can yield a value outside the element domain, so the request cannot be proven - // impossible at declaration — a wide ContainingAny makes it genuinely satisfiable. - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).ContainingAny(Any.Int32().GreaterThan(100)).WithCount(3).Generate(); - Check.That(set.Count).IsEqualTo(3); - Check.That(set).Contains(1, 2); - } - - // When every source draws from the same two-value domain, three distinct values are impossible — but the - // overlap is opaque, so it is caught while drawing (a replayable AnyGenerationException) rather than as a - // false eager conflict. - Check.ThatCode(() => Any.SetOf(Any.Boolean()).ContainingAny(Any.Boolean()).ContainingAny(Any.Boolean()).ContainingAny(Any.Boolean()).Generate()) - .Throws(); - } - - [Fact(DisplayName = "Containing under a merging comparer: an out-of-domain value is credited, and a comparer that merges it back is caught at generation.")] - public void ContainingUnderAMergingComparer() { - IEqualityComparer modTen = new ModuloComparer(10); - - // 15 is outside {1, 2, 3} and its residue class (5) is fresh too, so {1, 2, 3, 15} has four classes and - // generation succeeds. - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2, 3), modTen).Containing(15).WithCount(4).Generate(); - Check.That(set.Count).IsEqualTo(4); - } - - // 12 is outside {1, 2} by value, so it is still credited and the request is accepted eagerly — but 12 ≡ 2 - // (mod 10) collapses it back into the domain, so three distinct-under-the-comparer values are impossible and - // the shortfall surfaces while drawing, never as a false eager conflict. - Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2), modTen).Containing(12).WithCount(3).Generate()).Throws(); - } - - [Fact(DisplayName = "The eager perimeter reaches every finite generator: decimal, floating-point and 128-bit allow-lists gate distinct collections too.")] - public void FiniteScalarGeneratorsGateEagerly() { - // A finite allow-list or a narrow range over decimal, double, single or Int128 now advertises its cardinality, - // so a count beyond it conflicts at declaration — the same promise integers and enums already kept, held - // across the whole knowable perimeter rather than only part of it. - Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).WithCount(3)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).WithCount(3)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Single().OneOf(1f, 2f)).WithCount(3)).Throws(); -#if NET8_0_OR_GREATER - Check.ThatCode(() => Any.SetOf(Any.Int128().Between(1, 3)).WithCount(5)).Throws(); -#endif - - // Membership travels with cardinality: an out-of-domain contained value extends the effective domain... - for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(3m).WithCount(3).Generate(); - Check.That(set).Contains(1m, 2m, 3m); - } - - // ...while a contained value already inside it does not, so an impossible count still conflicts eagerly. - Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(1m).WithCount(3)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).Containing(2d).WithCount(3)).Throws(); - } - - [Fact(DisplayName = "A validated pin is a singleton domain: a distinct collection asking for more than one conflicts eagerly.")] - public void SingletonScalarDomainsGateEagerly() { - // Zero()/Between(x, x) pins the domain to a single value; asking a distinct collection for two is a fully - // knowable contradiction, so it must fail at declaration, not only while drawing. - Check.ThatCode(() => Any.SetOf(Any.Decimal().Zero()).WithCount(2)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Double().Between(1d, 1d)).WithCount(2)).Throws(); - Check.ThatCode(() => Any.SetOf(Any.Single().Zero()).WithCount(2)).Throws(); - - // The singleton still generates at count one, and an out-of-domain contained value extends it as usual. - for (int i = 0; i < SampleCount; i++) { - HashSet one = Any.SetOf(Any.Decimal().Zero()).WithCount(1).Generate(); - Check.That(one).ContainsExactly(0m); - - HashSet two = Any.SetOf(Any.Decimal().Zero()).Containing(5m).WithCount(2).Generate(); - Check.That(two).Contains(0m, 5m); - } - } - - [Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")] - public void ArrayOfProducesArrays() { - for (int i = 0; i < SampleCount; i++) { - int[] array = Any.ArrayOf(Any.Int32().Between(1, 100)).WithCount(6).Distinct().Generate(); - Check.That(array.Length).IsEqualTo(6); - Check.That(new HashSet(array).Count).IsEqualTo(6); - } - } - - [Fact(DisplayName = "SequenceOf: is fully materialized — enumerating twice yields the same elements without re-drawing.")] - public void SequenceOfIsMaterialized() { - IEnumerable sequence = Any.SequenceOf(Any.Int32()).WithCount(5).Generate(); - - List first = sequence.ToList(); - List second = sequence.ToList(); - - Check.That(first).ContainsExactly(second); - } - - [Fact(DisplayName = "DictionaryOf: builds unique-keyed dictionaries and gates the count by the key domain.")] - public void DictionaryOfBehaves() { - for (int i = 0; i < SampleCount; i++) { - Dictionary dictionary = Any.DictionaryOf(Any.Int32().Between(1, 1000), Any.String().NonEmpty()).WithCount(8).Generate(); - Check.That(dictionary.Count).IsEqualTo(8); - Check.That(dictionary.Values).ContainsOnlyElementsThatMatch(value => value.Length > 0); - } - - Check.ThatCode(() => Any.DictionaryOf(Any.Boolean(), Any.Int32()).WithCount(3)).Throws(); - Check.ThatCode(() => Any.DictionaryOf(null!, Any.Int32())).Throws(); - } - - [Fact(DisplayName = "ContainingKey: a key outside the key domain extends the effective cardinality (issue #225).")] - public void DictionaryContainingKeyOutsideDomainExtendsCardinality() { - // Mirrors ContainingOutsideDomainExtendsCardinality on the dictionary surface: {1, 2} is all the key - // generator can produce, and 3 is supplied directly from outside that domain, so a three-entry dictionary is - // satisfiable — the out-of-domain cardinality-credit path AnyDictionary could not exercise before. - for (int i = 0; i < SampleCount; i++) { - Dictionary dictionary = - Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.String().NonEmpty()).ContainingKey(3).WithCount(3).Generate(); - Check.That(dictionary.Keys).Contains(1, 2, 3); - Check.That(dictionary.Count).IsEqualTo(3); - Check.That(dictionary.Values).ContainsOnlyElementsThatMatch(value => value.Length > 0); - } - } - - [Fact(DisplayName = "ContainingKey: a within-domain key is present, an out-of-capacity one still conflicts eagerly.")] - public void DictionaryContainingKeyInsideDomainDoesNotInflate() { - // A within-domain fixed key is present and adds no capacity of its own. - for (int i = 0; i < SampleCount; i++) { - Dictionary dictionary = - Any.DictionaryOf(Any.Int32().Between(1, 9), Any.String().NonEmpty()).WithCount(5).ContainingKey(7).Generate(); - Check.That(dictionary.ContainsKey(7)).IsTrue(); - Check.That(dictionary.Count).IsEqualTo(5); - } - - // 1 is already producible, so three distinct keys over {1, 2} remain impossible and still fail eagerly, - // naming the shortfall — exactly as ContainingInsideDomainDoesNotInflate asserts for a set. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.String().NonEmpty()).ContainingKey(1).WithCount(3)); - Check.That(conflict.Message).Contains("2 distinct value"); - } - - [Fact(DisplayName = "ContainingAnyKey: a key drawn from a generator is forced into the dictionary; null is rejected (issue #287).")] - public void DictionaryContainingAnyKeyForcesADrawnKey() { - // The drawn key (4242) lies outside the key generator's own {1..9} domain, so it is supplied directly and - // extends the effective cardinality — the ContainingAny path, now reaching dictionary keys. - for (int i = 0; i < SampleCount; i++) { - Dictionary dictionary = - Any.DictionaryOf(Any.Int32().Between(1, 9), Any.String().NonEmpty()).NonEmpty().ContainingAnyKey(Any.Int32().OneOf(4242)).Generate(); - Check.That(dictionary.ContainsKey(4242)).IsTrue(); - } - - Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingAnyKey(null!)).Throws(); - } - - [Fact(DisplayName = "ContainingEntry: pins the value for a required key, extending cardinality out of domain (issue #288).")] - public void DictionaryContainingEntryPinsTheValue() { - for (int i = 0; i < SampleCount; i++) { - // Key 3 is outside the key domain {1, 2} (supplied directly, extends cardinality); value 99 is outside - // the value domain {1..9}, proving it is the pinned value rather than a generated one. - Dictionary dictionary = - Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.Int32().Between(1, 9)) - .ContainingEntry(3, 99) - .WithCount(3) - .Generate(); - Check.That(dictionary.Keys).Contains(1, 2, 3); - Check.That(dictionary[3]).IsEqualTo(99); - Check.That(dictionary.Count).IsEqualTo(3); - } - } - - [Fact(DisplayName = "ContainingEntry: pinning the same key twice — or an entry and a ContainingKey — conflicts.")] - public void DictionaryContainingEntryDuplicateKeyConflicts() { - Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingEntry(1, 10).ContainingEntry(1, 20)) - .Throws(); - - Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingKey(1).ContainingEntry(1, 20)) - .Throws(); - } - - [Fact(DisplayName = "PairOf and TripleOf assemble value tuples from constrained parts.")] - public void PairAndTriple() { - for (int i = 0; i < SampleCount; i++) { - (int first, string second) pair = Any.PairOf(Any.Int32().Positive(), Any.String().NonEmpty()).Generate(); - Check.That(pair.first).IsStrictlyGreaterThan(0); - Check.That(pair.second).IsNotEmpty(); - - (int a, int b, int c) triple = Any.TripleOf(Any.Int32().Between(1, 2), Any.Int32().Between(3, 4), Any.Int32().Between(5, 6)).Generate(); - Check.That(triple.a is 1 or 2).IsTrue(); - Check.That(triple.b is 3 or 4).IsTrue(); - Check.That(triple.c is 5 or 6).IsTrue(); - } - } - - [Fact(DisplayName = "Collections are reproducible when their element generator draws from a seeded context.")] - public void CollectionsAreReproducible() { - HashSet first = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6).Generate(); - HashSet second = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6).Generate(); - - Check.That(second.OrderBy(value => value)).ContainsExactly(first.OrderBy(value => value)); - - List listOne = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5).Generate(); - List listTwo = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5).Generate(); - Check.That(listTwo).ContainsExactly(listOne); - } - - [Fact(DisplayName = "Collections compose into value objects and aggregates through As and Combine.")] - public void CollectionsComposeThroughAsAndCombine() { - IAny> references = Any.ListOf(Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create)).WithCount(3); - - List list = references.Generate(); - Check.That(list.Count).IsEqualTo(3); - Check.That(list).ContainsOnlyElementsThatMatch(reference => reference.Value.StartsWith("ORD-")); - } - - [Fact(DisplayName = "Exhaustion over a foreign element generator qualifies the replay hint instead of promising a full replay of the elements.")] - public void ExhaustionOverAForeignElementGeneratorQualifiesTheHint() { - // A foreign IAny carries no IHasRandomSource, so the collection falls back to the ambient source for its count - // and layout while the foreign generator's own draws ignore that seed. The reported seed therefore cannot - // replay the elements, and the message must not claim it can. - AnyGenerationException caught = Assert.Throws( - () => Any.Reproducibly(2026, () => Any.SetOf(new ForeignPair()).WithCount(5).Generate(), _ => { })); - - Check.That(caught.Seed).IsEqualTo(2026); - Check.That(caught.Message).Contains("the element generator"); - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Contains("Any.Reproducibly(2026"); - // The faithful full-replay sentence must be gone — it is the false promise this fix removes. - Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); - } - - [Fact(DisplayName = "A derivation built over a foreign generator is qualified too: the discriminator is a null source, not the IHasRandomSource type.")] - public void ExhaustionOverAnAsDerivedForeignGeneratorQualifiesTheHint() { - // DerivedAny (from As) implements IHasRandomSource but propagates a null source when its operand is foreign, - // so its elements are as unreproducible as the foreign generator's. Keying on the type rather than the null - // source would misclassify this as faithful and keep over-promising. - IAny derivedOverForeign = new ForeignPair().As(value => value); - - AnyGenerationException caught = Assert.Throws( - () => Any.SetOf(derivedOverForeign).WithCount(5).Generate()); - - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); - } - - [Fact(DisplayName = "A foreign ContainingAny generator is qualified at its own site, and a fixed source is named as Any.WithSeed rather than Any.Reproducibly.")] - public void ExhaustionOverAForeignContainingAnyQualifiesAndNamesTheFixedSource() { - // The collection's own elements come from a fixed Any.WithSeed(...) context (faithful), but the ContainingAny - // draw is foreign. The twin exhaustion site must qualify the hint for that specific generator — and, because - // the collection's source is fixed, name Any.WithSeed, never the inapplicable Any.Reproducibly. - AnyContext seeded = Any.WithSeed(4242); - - AnyGenerationException caught = Assert.Throws( - () => Any.SetOf(seeded.Int32()).Containing(0).Containing(1).ContainingAny(new ForeignPair()).Generate()); - - Check.That(caught.Seed).IsEqualTo(4242); - Check.That(caught.Message).Contains("a ContainingAny(...) generator"); - Check.That(caught.Message).Contains("Any.WithSeed(4242)"); - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Not.Contains("Any.Reproducibly("); - } - - [Fact(DisplayName = "Exhaustion over a library element generator keeps the faithful full-replay hint unchanged.")] - public void ExhaustionOverALibraryElementGeneratorKeepsTheFaithfulHint() { - // A comparer collapses the effective domain below the requested count, so a library generator — whose draws do - // follow the reported seed — exhausts the bounded draw. Its message must stay the faithful one: the fix only - // touches the genuinely-foreign case. - IEqualityComparer modTen = new ModuloComparer(10); - - AnyGenerationException caught = Assert.Throws( - () => Any.Reproducibly(1234, () => Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(20).Generate(), _ => { })); - - Check.That(caught.Seed).IsEqualTo(1234); - Check.That(caught.Message).Contains("The arbitrary values were seeded with 1234"); - Check.That(caught.Message).Contains("Any.Reproducibly(1234"); - Check.That(caught.Message).Not.Contains("not reproducible from this seed alone"); - } - - [Fact(DisplayName = "Exhaustion over a Combine that mixes a foreign operand is qualified, even though a library operand supplies a non-null source.")] - public void ExhaustionOverACombineMixingAForeignOperandQualifiesTheHint() { - // Any.Combine keeps the library operand's non-null source (SourceOf(first) ?? SourceOf(second)), but the - // composed value follows the foreign draw, so the elements are not reproducible from the reported seed. The - // discriminator is full reproducibility, not merely a non-null source. - IAny mixed = Any.Combine(new ForeignPair(), Any.Int32(), (foreign, _) => foreign); - - AnyGenerationException caught = Assert.Throws( - () => Any.Reproducibly(777, () => Any.SetOf(mixed).WithCount(5).Generate(), _ => { })); - - Check.That(caught.Seed).IsEqualTo(777); - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); - } - - #region Nested types - - // A value-equal reference type: two Tag(1) are equal under the default comparer and distinct under reference - // equality. That is the whole point — it is the ordinary shape of a domain value object, and the pair it forms - // with ReferenceComparer is what makes a comparer STRICTER than the default one observable. - private sealed class Tag { - - public Tag(int value) { - Value = value; - } - - private int Value { get; } - - public override bool Equals(object? obj) { - return obj is Tag tag && tag.Value == Value; - } - - public override int GetHashCode() { - return Value; - } - - public override string ToString() { - return $"Tag({Value.ToString(CultureInfo.InvariantCulture)})"; - } - - } - - private sealed class ReferenceComparer : IEqualityComparer { - - // Stricter than EqualityComparer.Default: it splits value-equal instances rather than merging them. - public bool Equals(Tag? x, Tag? y) { - return ReferenceEquals(x, y); - } - - public int GetHashCode(Tag obj) { - return RuntimeHelpers.GetHashCode(obj); - } - - } - - private sealed class ModuloComparer : IEqualityComparer { - - private readonly int _modulus; - - public ModuloComparer(int modulus) { - _modulus = modulus; - } - - public bool Equals(int x, int y) { - return x % _modulus == y % _modulus; - } - - public int GetHashCode(int obj) { - return obj % _modulus; - } - - } - - private sealed class ForeignPair : IAny { - - private int _n; - - // Foreign on purpose: implements IAny but NOT IHasRandomSource, so it does not draw from the collection's - // reported source. It yields only two distinct values (0 and 1), driving a distinct collection past its budget. - public int Generate() { - return _n++ % 2; - } - - } - - #endregion - -} diff --git a/JustDummies.UnitTests/AnyContinuousTests.cs b/JustDummies.UnitTests/AnyContinuousTests.cs deleted file mode 100644 index 73c9e434..00000000 --- a/JustDummies.UnitTests/AnyContinuousTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The example-based half of the continuous generators' contract: conflict messages, the named domain -/// extremes, the exclusion families the property suite leaves alone, and the seeded regression for issue -/// #206. Containment, strictness, inclusiveness, sign handling and the rejection of non-finite arguments -/// hold for every bound and are quantified in JustDummies.PropertyTests (ADR-0040); the #206 -/// regression stays here because it pins the interval where the defect actually occurred. -/// -public sealed class AnyContinuousTests { - - private const int SampleCount = 200; - - [Fact(DisplayName = "An unconstrained draw survives ordinary arithmetic, on every continuous type.")] - public void UnconstrainedDrawsSurviveOrdinaryArithmetic() { - // Regression, ADR-0052. Measured before the ordinary-magnitude window existed: uniform sampling over a - // type's whole domain put 16.1 % of Positive() doubles where a single multiplication overflows to - // Infinity, and 17.1 % of decimals where the same multiplication throws OverflowException. Neither was a - // defect of the code under test — the dummy itself was breaking the Arrange. - for (int i = 0; i < SampleCount; i++) { - Check.That(IsFinite(Any.Double().Generate() * 1.2d)).IsTrue(); - Check.That(IsFinite(Any.Double().Positive().Generate() * 1.2d)).IsTrue(); - Check.That(IsFinite(Any.Single().Generate() * 1.2f)).IsTrue(); - Check.ThatCode(() => Any.Decimal().Generate() * 1.2m).DoesNotThrow(); - } - } - - /// - /// Finiteness, spelled the way the .NET Framework 4.7.2 floor leg understands: double.IsFinite arrived - /// with .NET Core 3.0, and this suite is built against the support floor too. - /// - private static bool IsFinite(double value) { - return !double.IsNaN(value) && !double.IsInfinity(value); - } - - [Fact(DisplayName = "A scale constraint still constrains: an unconstrained decimal has room for its fraction.")] - public void AScaleConstraintKeepsItsMeaning() { - // ADR-0052 restores what the old default emptied out. Near decimal.MaxValue a value has no fractional - // digits left, so WithScale(2) was satisfied by every draw and constrained none of them: 5000/5000 - // "honoured", every one of them a 29-digit integer. - bool anyFraction = false; - for (int i = 0; i < SampleCount; i++) { - decimal value = Any.Decimal().WithScale(2).Generate(); - - Check.That(value).IsEqualTo(Math.Round(value, 2)); - if (value != Math.Truncate(value)) { anyFraction = true; } - } - - Check.WithCustomMessage("No draw carried a fractional part, so WithScale(2) constrained nothing.") - .That(anyFraction) - .IsTrue(); - } - - [Fact(DisplayName = "A named magnitude is honoured; a merely permitted one is not targeted.")] - public void ANamedMagnitudeIsHonouredAndAPermittedOneIsNot() { - // The two named coordinates of the rule, at the extremes the property suite deliberately leaves to an - // example: asking for a magnitude and merely allowing one. - Check.That(Any.Double().Between(1e300d, 1e308d).Generate()).IsStrictlyGreaterThan(1e300d * 0.99d); - Check.That(Any.Double().GreaterThan(1e300d).Generate()).IsStrictlyGreaterThan(1e300d); - - Check.That(Math.Abs(Any.Double().Between(0d, double.MaxValue).Generate())).IsStrictlyLessThan(1.000001e6d); - } - - [Fact(DisplayName = "Double: sign constraints are strict, Zero pins, NonZero excludes.")] - public void DoubleSignFamily() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Double().Positive().Generate()).IsStrictlyGreaterThan(0d); - Check.That(Any.Double().Negative().Generate()).IsStrictlyLessThan(0d); - Check.That(Any.Double().NonZero().Generate()).IsNotEqualTo(0d); - } - Check.That(Any.Double().Zero().Generate()).IsEqualTo(0d); - Check.ThatCode(() => Any.Double().Zero().NonZero()).Throws(); - Check.ThatCode(() => Any.Double().Positive().Negative()).Throws(); - } - - [Fact(DisplayName = "Double: Between contains, GreaterThan is strict, and conflicts name both sides.")] - public void DoubleBounds() { - for (int i = 0; i < SampleCount; i++) { - double bounded = Any.Double().Between(1d, 2d).Generate(); - Check.That(bounded).IsGreaterOrEqualThan(1d); - Check.That(bounded).IsLessOrEqualThan(2d); - Check.That(Any.Double().GreaterThan(1d).LessThanOrEqualTo(2d).Generate()).IsStrictlyGreaterThan(1d); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Double().GreaterThan(100d).LessThan(10d)); - Check.That(conflict.Message).Contains("LessThan(10)"); - Check.That(conflict.Message).Contains("GreaterThan(100)"); - Check.ThatCode(() => Any.Double().GreaterThan(double.MaxValue)).Throws(); - } - - [Fact(DisplayName = "Double: OneOf stays within and Except/DifferentFrom never yield the excluded value.")] - public void DoubleSets() { - double[] allowed = [1.5d, 2.5d]; - for (int i = 0; i < SampleCount; i++) { - Check.That(allowed.Contains(Any.Double().OneOf(allowed).Generate())).IsTrue(); - Check.That(Any.Double().OneOf(allowed).Except(1.5d).Generate()).IsEqualTo(2.5d); - Check.That(Any.Double().OneOf(allowed).DifferentFrom(2.5d).Generate()).IsEqualTo(1.5d); - } - } - - [Fact(DisplayName = "Single: finite draws, strict signs, bounds contained, NaN rejected.")] - public void SingleBehaves() { - for (int i = 0; i < SampleCount; i++) { - float value = Any.Single().Generate(); - Check.That(float.IsNaN(value) || float.IsInfinity(value)).IsFalse(); - Check.That(Any.Single().Positive().Generate()).IsStrictlyGreaterThan(0f); - - float bounded = Any.Single().Between(1f, 2f).Generate(); - Check.That(bounded).IsGreaterOrEqualThan(1f); - Check.That(bounded).IsLessOrEqualThan(2f); - } - - Check.That(Any.Single().Zero().Generate()).IsEqualTo(0f); - Check.ThatCode(() => Any.Single().GreaterThan(float.NaN)).Throws(); - Check.ThatCode(() => Any.Single().GreaterThan(float.MaxValue)).Throws(); - Check.ThatCode(() => Any.Single().Positive().Negative()).Throws(); - } - - [Fact(DisplayName = "Decimal: strict signs, pinned zero, contained bounds, and strict GreaterThan via exclusion.")] - public void DecimalBehaves() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Decimal().Positive().Generate()).IsStrictlyGreaterThan(0m); - Check.That(Any.Decimal().Negative().Generate()).IsStrictlyLessThan(0m); - - decimal bounded = Any.Decimal().Between(1m, 2m).Generate(); - Check.That(bounded).IsGreaterOrEqualThan(1m); - Check.That(bounded).IsLessOrEqualThan(2m); - Check.That(Any.Decimal().Between(1m, 2m).GreaterThan(1m).Generate()).IsStrictlyGreaterThan(1m); - } - - Check.That(Any.Decimal().Zero().Generate()).IsEqualTo(0m); - Check.ThatCode(() => Any.Decimal().Zero().NonZero()).Throws(); - Check.ThatCode(() => Any.Decimal().Between(10m, 1m)).Throws(); - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Decimal().GreaterThan(100m).LessThan(10m)); - Check.That(conflict.Message).Contains("LessThan(10)"); - Check.That(conflict.Message).Contains("GreaterThan(100)"); - } - - [Fact(DisplayName = "Decimal: Between reaches both halves of a range, up to near the inclusive maximum.")] - public void DecimalBetweenReachesBothHalves() { - // Regression for #206: the fraction was built from three non-negative Random.Next() draws over - // the full 96-bit mantissa denominator, so each limb's top bit stayed zero, the fraction never - // crossed ~0.5, and every candidate fell in [min, mid). Seeded and deterministic — both halves, - // and a value near the inclusive maximum, must be observed. - const decimal min = 0m; - const decimal max = 100m; - const decimal mid = 50m; - - AnyContext any = Any.WithSeed(20260721); - - decimal lowest = decimal.MaxValue; - decimal highest = decimal.MinValue; - for (int i = 0; i < 5000; i++) { - decimal value = any.Decimal().Between(min, max).Generate(); - Check.That(value).IsGreaterOrEqualThan(min); - Check.That(value).IsLessOrEqualThan(max); - if (value < lowest) { lowest = value; } - if (value > highest) { highest = value; } - } - - Check.That(lowest).IsStrictlyLessThan(mid); // the lower half stays covered - Check.That(highest).IsStrictlyGreaterThan(mid); // the upper half — unreachable before the fix - Check.That(highest).IsStrictlyGreaterThan(99m); // and up to near the inclusive maximum - } - - [Fact(DisplayName = "Every continuous generator materializes its own value type through Generate().")] - public void MaterializesEachValueType() { - double d = Any.Double().Between(1d, 2d).Generate(); - float f = Any.Single().Between(1f, 2f).Generate(); - decimal m = Any.Decimal().Between(1m, 2m).Generate(); - - Check.That(d).IsGreaterOrEqualThan(1d); - Check.That(f).IsGreaterOrEqualThan(1f); - Check.That(m).IsGreaterOrEqualThan(1m); - } - -} diff --git a/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs b/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs deleted file mode 100644 index 252aa3d0..00000000 --- a/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Behaviour of the offset dimension — WithOffset pins it, -/// WithOffsetBetween draws it bounded, the default stays UTC, and values stay valid at the domain edges -/// because the instant is tightened before the offset is drawn. -/// -public sealed class AnyDateTimeOffsetOffsetTests { - - private const int SampleCount = 200; - - [Fact(DisplayName = "Offset: unconstrained, generated values carry UTC (zero) offset.")] - public void DefaultOffsetIsZero() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.DateTimeOffset().Generate().Offset).IsEqualTo(TimeSpan.Zero); - } - } - - [Fact(DisplayName = "WithOffset: every generated value carries the pinned offset.")] - public void WithOffsetPins() { - TimeSpan offset = TimeSpan.FromHours(2); - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.DateTimeOffset().WithOffset(offset).Generate().Offset).IsEqualTo(offset); - } - } - - [Fact(DisplayName = "WithOffsetBetween: offsets stay within the range, in whole minutes, and vary.")] - public void WithOffsetBetweenBounds() { - TimeSpan min = TimeSpan.FromHours(-5); - TimeSpan max = TimeSpan.FromHours(5); - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().WithOffsetBetween(min, max).Generate(); - Check.That(value.Offset >= min && value.Offset <= max).IsTrue(); - Check.That(value.Offset.Ticks % TimeSpan.TicksPerMinute).IsEqualTo(0L); - seen.Add(value.Offset); - } - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "WithOffset: stays valid and after the floor at the top of the domain.")] - public void WithOffsetValidNearMaxValue() { - DateTimeOffset floor = DateTimeOffset.MaxValue.AddDays(-1); - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().After(floor).WithOffset(TimeSpan.FromHours(14)).Generate(); - Check.That(value.Offset).IsEqualTo(TimeSpan.FromHours(14)); - Check.That(value.UtcTicks > floor.UtcTicks).IsTrue(); - } - } - - [Fact(DisplayName = "WithOffset: an instant window with no room for the offset conflicts eagerly.")] - public void WithOffsetImpossibleWindowConflicts() { - // The last 12h of the domain cannot host a +14h offset: the local ticks would overflow. - Check.ThatCode(() => Any.DateTimeOffset().After(DateTimeOffset.MaxValue.AddHours(-12)).WithOffset(TimeSpan.FromHours(14))) - .Throws(); - } - - [Fact(DisplayName = "WithOffset: arguments are validated (whole minutes, ±14:00, ordered range).")] - public void WithOffsetArguments() { - Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromSeconds(30))).Throws(); - Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(15))).Throws(); - Check.ThatCode(() => Any.DateTimeOffset().WithOffsetBetween(TimeSpan.FromHours(2), TimeSpan.FromHours(-2))).Throws(); - } - - [Fact(DisplayName = "WithOffset filters the OneOf pool instead of being ignored, in either order.")] - public void OneOfIsFilteredByTheDeclaredOffset() { - // ADR-0050 supersedes ADR-0037's accepted risk. A pooled value is still returned verbatim, offset included — - // rebuilding it from the instant would normalize the offset to UTC — but the offset dimension now decides - // WHICH pooled values may be drawn, rather than being silently dropped. The public contract of WithOffset says - // every generated value carries exactly that offset; it now does. - DateTimeOffset utc = new(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); - DateTimeOffset plusFive = new(2021, 1, 1, 0, 0, 0, TimeSpan.FromHours(5)); - - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.DateTimeOffset().WithOffset(TimeSpan.Zero).OneOf(utc, plusFive).Generate()).IsEqualTo(utc); - Check.That(Any.DateTimeOffset().OneOf(utc, plusFive).WithOffset(TimeSpan.Zero).Generate()).IsEqualTo(utc); - Check.That(Any.DateTimeOffset().OneOf(utc, plusFive).WithOffset(TimeSpan.FromHours(5)).Generate()).IsEqualTo(plusFive); - } - } - - [Fact(DisplayName = "WithOffset: an offset no pooled value carries is a conflict, in either order.")] - public void AnOffsetNoPooledValueCarriesConflicts() { - // The other half of the filter. Under the old behaviour both of these silently returned the UTC value, - // honouring neither the pool's offset nor the one the caller asked for. - DateTimeOffset utc = new(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); - TimeSpan requested = TimeSpan.FromHours(5); - - ConflictingAnyConstraintException afterPool = Assert.Throws( - () => Any.DateTimeOffset().OneOf(utc).WithOffset(requested)); - ConflictingAnyConstraintException beforePool = Assert.Throws( - () => Any.DateTimeOffset().WithOffset(requested).OneOf(utc)); - - Check.That(afterPool.Message).Contains("no pooled value carries an offset it admits"); - Check.That(beforePool.Message).Contains("no pooled value carries an offset it admits"); - } - - [Fact(DisplayName = "Without an offset constraint, OneOf still returns every pooled value with its own offset.")] - public void AnUnconstrainedOneOfKeepsEveryOffset() { - // The filter must only fire when an offset is actually declared: an unconstrained pool is unchanged, and that - // half of ADR-0037 stands. - DateTimeOffset utc = new(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); - DateTimeOffset plusFive = new(2021, 1, 1, 0, 0, 0, TimeSpan.FromHours(5)); - - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - seen.Add(Any.DateTimeOffset().OneOf(utc, plusFive).Generate().Offset); - } - - Check.That(seen).Contains(TimeSpan.Zero, TimeSpan.FromHours(5)); - } - - [Fact(DisplayName = "WithOffset: a second, different offset is rejected as already declared.")] - public void WithOffsetDeclaredOnce() { - Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(2)).WithOffset(TimeSpan.FromHours(3))) - .Throws(); - // The same offset twice is idempotent, not a conflict. - Check.That(Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(2)).WithOffset(TimeSpan.FromHours(2)).Generate().Offset) - .IsEqualTo(TimeSpan.FromHours(2)); - } - -} diff --git a/JustDummies.UnitTests/AnyEnumCombinationTests.cs b/JustDummies.UnitTests/AnyEnumCombinationTests.cs deleted file mode 100644 index 0bd9d5e5..00000000 --- a/JustDummies.UnitTests/AnyEnumCombinationTests.cs +++ /dev/null @@ -1,207 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The named cases of : the exact universe a given enum shape -/// yields, the conflict wording, and the boundary between the declared-members default and the opt-in. The -/// universal half — that every draw belongs to the universe whatever the constraints — lives in -/// JustDummies.PropertyTests. -/// -public sealed class AnyEnumCombinationTests { - - // Enough draws that an eight-value universe is exhausted with overwhelming probability, while a missing value is - // not attributed to bad luck. The assertions below are on the SET observed, so they are reachability claims. - private const int SampleCount = 2000; - - [Flags] - private enum Permissions { - - None = 0, - Read = 1, - Write = 2, - Exec = 4 - - } - - // No zero member: the empty combination is not a value this enum defines. - [Flags] - private enum Sides { - - Left = 1, - Right = 2 - - } - - // A declared composite: ReadWrite is already Read | Write, so it must not widen the universe. - [Flags] - private enum Access { - - Read = 1, - Write = 2, - ReadWrite = 3 - - } - - private enum OrderStatus { - - Draft, - Validated, - Cancelled - - } - - [Fact(DisplayName = "A [Flags] enum still draws only declared members until combinations are allowed.")] - public void FlagsEnumDrawsDeclaredMembersByDefault() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().Generate()); } - - // The contract the opt-in exists to leave untouched: the default never depends on the [Flags] attribute, so a - // combination is unreachable until the test asks for one. - Check.That(seen).IsOnlyMadeOf(Permissions.None, Permissions.Read, Permissions.Write, Permissions.Exec); - } - - [Fact(DisplayName = "AllowingCombinations: the universe is every combination, and the declared zero value.")] - public void CombinationsCoverTheWholeUniverse() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().AllowingCombinations().Generate()); } - - Check.That(seen.Count).IsEqualTo(8); - for (int bits = 0; bits <= 7; bits++) { Check.That(seen).Contains((Permissions)bits); } - } - - [Fact(DisplayName = "AllowingCombinations: an enum declaring no zero member never yields the empty combination.")] - public void CombinationsOmitZeroWhenItIsNotDeclared() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().AllowingCombinations().Generate()); } - - Check.That(seen).IsOnlyMadeOf(Sides.Left, Sides.Right, Sides.Left | Sides.Right); - } - - [Fact(DisplayName = "AllowingCombinations: a declared composite adds nothing — it already is a combination.")] - public void DeclaredCompositeDoesNotWidenTheUniverse() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().AllowingCombinations().Generate()); } - - Check.That(seen).IsOnlyMadeOf(Access.Read, Access.Write, Access.ReadWrite); - } - - [Fact(DisplayName = "AllowingCombinations: applying it to an enum that is not [Flags] conflicts, naming why.")] - public void CombinationsRequireAFlagsEnum() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Enum().AllowingCombinations()); - - Check.That(conflict.Message).Contains("AllowingCombinations()"); - Check.That(conflict.Message).Contains("OrderStatus"); - Check.That(conflict.Message).Contains("[Flags]"); - } - - [Fact(DisplayName = "AllowingCombinations: applying it twice is a no-op, not a conflict.")] - public void CombinationsAreIdempotent() { - AnyEnum generator = Any.Enum().AllowingCombinations().AllowingCombinations(); - - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); } - - // Idempotent, not cumulative: the universe is the same eight values a single application yields. - Check.That(seen.Count).IsEqualTo(8); - } - - [Fact(DisplayName = "OneOf: a combination is refused before the opt-in, and the message names the missing one.")] - public void OneOfRefusesACombinationBeforeTheOptIn() { - ArgumentException error = Assert.Throws( - () => Any.Enum().OneOf(Permissions.Read | Permissions.Write)); - - Check.That(error.Message).Contains("AllowingCombinations()"); - } - - [Fact(DisplayName = "OneOf: a combination is accepted once combinations are allowed.")] - public void OneOfAcceptsACombinationAfterTheOptIn() { - AnyEnum generator = Any.Enum() - .AllowingCombinations() - .OneOf(Permissions.Read | Permissions.Write, Permissions.Exec); - - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); } - - Check.That(seen).IsOnlyMadeOf(Permissions.Read | Permissions.Write, Permissions.Exec); - } - - [Fact(DisplayName = "Except: exclusions compare by equality, so a combination carrying an excluded bit survives.")] - public void ExclusionsCompareByEquality() { - AnyEnum generator = Any.Enum().AllowingCombinations().Except(Permissions.Read); - - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); } - - // Read itself is gone; Read | Write is a different value and stays drawable — Except is not a bit mask. - Check.That(seen).Not.Contains(Permissions.Read); - Check.That(seen).Contains(Permissions.Read | Permissions.Write); - Check.That(seen.Count).IsEqualTo(7); - } - - [Fact(DisplayName = "AllowingCombinations: the widened universe feeds the distinct-collection cardinality check.")] - public void CombinationsWidenTheCardinalityHint() { - // Eight distinct values exist, so eight are obtainable and nine conflict eagerly — the same check that caps a - // declared-members draw at four. - HashSet eight = Any.SetOf(Any.Enum().AllowingCombinations()).WithCount(8).Generate(); - Check.That(eight.Count).IsEqualTo(8); - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.SetOf(Any.Enum().AllowingCombinations()).WithCount(9).Generate()); - Check.That(conflict.Message).Contains("9"); - - ConflictingAnyConstraintException capped = Assert.Throws( - () => Any.SetOf(Any.Enum()).WithCount(5).Generate()); - Check.That(capped.Message).Contains("5"); - } - - [Fact(DisplayName = "AllowingCombinations: excluding the whole universe conflicts, naming both sides.")] - public void ExcludingTheWholeUniverseConflicts() { - Permissions[] everything = Enumerable.Range(0, 8).Select(bits => (Permissions)bits).ToArray(); - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Enum().AllowingCombinations().Except(everything)); - - Check.That(conflict.Message).Contains("Except("); - Check.That(conflict.Message).Contains("Permissions"); - } - - [Fact(DisplayName = "AllowingCombinations: an enum with too many members to enumerate is refused, naming the ceiling.")] - public void TooManyMembersIsRefused() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Enum().AllowingCombinations()); - - Check.That(conflict.Message).Contains("AllowingCombinations()"); - Check.That(conflict.Message).Contains("21"); - Check.That(conflict.Message).Contains("20"); - Check.That(conflict.Message).Contains("OneOf"); - } - - [Fact(DisplayName = "AllowingCombinations: a seeded context replays the same combinations.")] - public void CombinationsReplayUnderASeed() { - List Batch(int seed) { - AnyContext context = Any.WithSeed(seed); - AnyEnum generator = context.Enum().AllowingCombinations(); - - return Enumerable.Range(0, 20).Select(_ => generator.Generate()).ToList(); - } - - Check.That(Batch(4242)).ContainsExactly(Batch(4242)); - } - - // Twenty-one single-bit members: one past the ceiling AllowingCombinations() will enumerate. - [Flags] - private enum WideBits { - - B00 = 1 << 0, B01 = 1 << 1, B02 = 1 << 2, B03 = 1 << 3, B04 = 1 << 4, B05 = 1 << 5, B06 = 1 << 6, - B07 = 1 << 7, B08 = 1 << 8, B09 = 1 << 9, B10 = 1 << 10, B11 = 1 << 11, B12 = 1 << 12, B13 = 1 << 13, - B14 = 1 << 14, B15 = 1 << 15, B16 = 1 << 16, B17 = 1 << 17, B18 = 1 << 18, B19 = 1 << 19, B20 = 1 << 20 - - } - -} diff --git a/JustDummies.UnitTests/AnyInt32Tests.cs b/JustDummies.UnitTests/AnyInt32Tests.cs deleted file mode 100644 index fc7d45ec..00000000 --- a/JustDummies.UnitTests/AnyInt32Tests.cs +++ /dev/null @@ -1,149 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The example-based half of 's contract: what a conflict message must name, which -/// arguments are rejected outright, that the named domain extremes are generable, and that a bounded range -/// is actually reached. The invariants that hold for every bound — containment, strictness, -/// inclusiveness, exclusion, immutability — are quantified over generated bounds in -/// JustDummies.PropertyTests instead, and are deliberately not restated here (ADR-0040). -/// -[TestSubject(typeof(AnyInt32))] -public sealed class AnyInt32Tests { - - private const int SampleCount = 200; - - #region Statics members declarations - - private static IEnumerable Samples(IAny generator) { - for (int i = 0; i < SampleCount; i++) { - yield return generator.Generate(); - } - } - - #endregion - - [Fact(DisplayName = "An unconstrained Int32 generates without failing.")] - public void UnconstrainedGenerates() { - Check.ThatCode(() => Any.Int32().Generate()).DoesNotThrow(); - } - - [Fact(DisplayName = "Positive yields values strictly greater than zero.")] - public void PositiveIsStrictlyPositive() { - foreach (int value in Samples(Any.Int32().Positive())) { - Check.That(value).IsStrictlyGreaterThan(0); - } - } - - [Fact(DisplayName = "Negative yields values strictly less than zero.")] - public void NegativeIsStrictlyNegative() { - foreach (int value in Samples(Any.Int32().Negative())) { - Check.That(value).IsStrictlyLessThan(0); - } - } - - [Fact(DisplayName = "Zero yields exactly zero.")] - public void ZeroIsZero() { - Check.That(Any.Int32().Zero().Generate()).IsEqualTo(0); - } - - [Fact(DisplayName = "NonZero never yields zero.")] - public void NonZeroIsNeverZero() { - foreach (int value in Samples(Any.Int32().NonZero().Between(-2, 2))) { - Check.That(value).IsNotEqualTo(0); - } - } - - [Fact(DisplayName = "Between eventually reaches both inclusive bounds.")] - public void BetweenReachesItsBounds() { - HashSet seen = [.. Samples(Any.Int32().Between(1, 3))]; - - Check.That(seen.Contains(1)).IsTrue(); - Check.That(seen.Contains(3)).IsTrue(); - } - - [Fact(DisplayName = "The extreme bounds of the Int32 range are generable.")] - public void ExtremeBoundsAreGenerable() { - Check.That(Any.Int32().LessThanOrEqualTo(int.MinValue).Generate()).IsEqualTo(int.MinValue); - Check.That(Any.Int32().GreaterThanOrEqualTo(int.MaxValue).Generate()).IsEqualTo(int.MaxValue); - } - - [Fact(DisplayName = "DifferentFrom never yields the excluded value.")] - public void DifferentFromNeverYieldsTheValue() { - foreach (int value in Samples(Any.Int32().Between(7, 8).DifferentFrom(7))) { - Check.That(value).IsEqualTo(8); - } - } - - [Fact(DisplayName = "Positive then Negative conflicts, naming both constraints.")] - public void PositiveThenNegativeConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int32().Positive().Negative()); - - Check.That(conflict.Message).Contains("Negative()"); - Check.That(conflict.Message).Contains("Positive()"); - } - - [Fact(DisplayName = "GreaterThan then an impossible LessThan conflicts, naming both constraints.")] - public void CrossedBoundsConflict() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int32().GreaterThan(100).LessThan(10)); - - Check.That(conflict.Message).Contains("LessThan(10)"); - Check.That(conflict.Message).Contains("GreaterThan(100)"); - } - - [Fact(DisplayName = "Zero then NonZero conflicts: the pinned value is excluded.")] - public void ZeroThenNonZeroConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int32().Zero().NonZero()); - - Check.That(conflict.Message).Contains("NonZero()"); - Check.That(conflict.Message).Contains("Zero()"); - } - - [Fact(DisplayName = "GreaterThan int.MaxValue conflicts: no Int32 satisfies it.")] - public void GreaterThanMaxValueConflicts() { - Check.ThatCode(() => Any.Int32().GreaterThan(int.MaxValue)).Throws(); - } - - [Fact(DisplayName = "OneOf then a bound excluding every allowed value conflicts.")] - public void OneOfEmptiedByABoundConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int32().OneOf(1, 2).GreaterThan(5)); - - Check.That(conflict.Message).Contains("GreaterThan(5)"); - Check.That(conflict.Message).Contains("OneOf(1, 2)"); - } - - [Fact(DisplayName = "A second OneOf conflicts: the allow-list is declared once.")] - public void SecondOneOfConflicts() { - Check.ThatCode(() => Any.Int32().OneOf(1, 2).OneOf(3, 4)).Throws(); - } - - [Fact(DisplayName = "Except exhausting the whole interval conflicts.")] - public void ExceptExhaustingTheIntervalConflicts() { - Check.ThatCode(() => Any.Int32().Between(1, 2).Except(1, 2)).Throws(); - } - - [Fact(DisplayName = "Except exhausting the allow-list conflicts.")] - public void ExceptExhaustingTheAllowListConflicts() { - Check.ThatCode(() => Any.Int32().OneOf(1, 2).Except(1).Except(2)).Throws(); - } - - [Fact(DisplayName = "OneOf and Except reject null or empty value lists.")] - public void OneOfAndExceptRejectNullOrEmpty() { - Check.ThatCode(() => Any.Int32().OneOf()).Throws(); - Check.ThatCode(() => Any.Int32().OneOf(null!)).Throws(); - Check.ThatCode(() => Any.Int32().Except()).Throws(); - Check.ThatCode(() => Any.Int32().Except(null!)).Throws(); - } - -} diff --git a/JustDummies.UnitTests/AnyLatticeConstraintTests.cs b/JustDummies.UnitTests/AnyLatticeConstraintTests.cs deleted file mode 100644 index 350199d2..00000000 --- a/JustDummies.UnitTests/AnyLatticeConstraintTests.cs +++ /dev/null @@ -1,229 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Behaviour of the lattice constraints — MultipleOf on the integers, WithScale on -/// , and WithGranularity on the temporals: values are built directly on the grid in -/// one draw, the grid composes with bounds/exclusions/allow-lists, and an empty grid conflicts eagerly. -/// -public sealed class AnyLatticeConstraintTests { - - private const int SampleCount = 200; - - #region MultipleOf - - [Fact(DisplayName = "MultipleOf: every drawn value is a multiple of the step.")] - public void MultipleOfAlwaysDivisible() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Int32().MultipleOf(100).Generate() % 100).IsEqualTo(0); - Check.That(Any.Int64().Positive().MultipleOf(12L).Generate() % 12L).IsEqualTo(0L); - Check.That(Any.Byte().MultipleOf(5).Generate() % 5).IsEqualTo(0); - } - } - - [Fact(DisplayName = "MultipleOf: draws on the grid within the declared range and reaches both ends.")] - public void MultipleOfHonoursRangeAndReachesBounds() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - int value = Any.Int32().Between(0, 1000).MultipleOf(100).Generate(); - Check.That(value % 100).IsEqualTo(0); - Check.That(value >= 0 && value <= 1000).IsTrue(); - seen.Add(value); - } - - Check.That(seen.Contains(0)).IsTrue(); - Check.That(seen.Contains(1000)).IsTrue(); - } - - [Fact(DisplayName = "MultipleOf: negative multiples are drawn on the grid too.")] - public void MultipleOfHandlesNegativeGrid() { - for (int i = 0; i < SampleCount; i++) { - int value = Any.Int32().Between(-100, -1).MultipleOf(10).Generate(); - Check.That(value % 10).IsEqualTo(0); - Check.That(value >= -100 && value <= -10).IsTrue(); - } - } - - [Fact(DisplayName = "MultipleOf: composes with Except, never yielding an excluded grid point.")] - public void MultipleOfComposesWithExcept() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - int value = Any.Int32().Between(0, 30).MultipleOf(10).Except(10, 20).Generate(); - Check.That(value % 10).IsEqualTo(0); - Check.That(value != 10 && value != 20).IsTrue(); - seen.Add(value); - } - - Check.That(seen.SetEquals([0, 30])).IsTrue(); - } - - [Fact(DisplayName = "MultipleOf: filters a OneOf allow-list to its members on the grid.")] - public void MultipleOfFiltersAllowList() { - for (int i = 0; i < SampleCount; i++) { - int value = Any.Int32().OneOf(5, 10, 15, 20).MultipleOf(10).Generate(); - Check.That(value == 10 || value == 20).IsTrue(); - } - } - - [Fact(DisplayName = "MultipleOf: one is a no-op; zero and negatives are rejected.")] - public void MultipleOfArguments() { - for (int i = 0; i < SampleCount; i++) { Any.Int32().MultipleOf(1).Generate(); } // no-op: any value - - Check.ThatCode(() => Any.Int32().MultipleOf(0)).Throws(); - Check.ThatCode(() => Any.Int32().MultipleOf(-5)).Throws(); - Check.ThatCode(() => Any.Byte().MultipleOf(0)).Throws(); - } - - [Fact(DisplayName = "MultipleOf: an empty grid inside the range conflicts eagerly, naming the step.")] - public void MultipleOfEmptyGridConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int32().Between(1, 9).MultipleOf(10)); - Check.That(conflict.Message).Contains("MultipleOf(10)"); - - // The same emptiness, whichever order the two constraints arrive in. - Check.ThatCode(() => Any.Int32().MultipleOf(10).Between(1, 9)).Throws(); - } - - [Fact(DisplayName = "MultipleOf: a second, different step is rejected as already declared.")] - public void MultipleOfDeclaredOnce() { - Check.ThatCode(() => Any.Int32().MultipleOf(4).MultipleOf(6)).Throws(); - // The same step twice is idempotent, not a conflict. - Check.That(Any.Int32().Between(0, 100).MultipleOf(10).MultipleOf(10).Generate() % 10).IsEqualTo(0); - } - - [Fact(DisplayName = "MultipleOf: a distinct collection sees the grid cardinality.")] - public void MultipleOfFeedsCardinality() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Int32().Between(0, 20).MultipleOf(10).Generate()); } - - // Exactly the three grid points are reachable — the cardinality hint a distinct collection relies on. - Check.That(seen.SetEquals([0, 10, 20])).IsTrue(); - } - - #endregion - - #region WithScale - - [Fact(DisplayName = "WithScale: every drawn value lies on the 10^-scale grid.")] - public void WithScaleStaysOnGrid() { - for (int i = 0; i < SampleCount; i++) { - decimal amount = Any.Decimal().Between(0m, 1000m).WithScale(2).Generate(); - Check.That(amount).IsEqualTo(Math.Round(amount, 2, MidpointRounding.ToEven)); - Check.That(amount >= 0m && amount <= 1000m).IsTrue(); - - decimal whole = Any.Decimal().WithScale(0).Generate(); - Check.That(whole).IsEqualTo(Math.Round(whole, 0, MidpointRounding.ToEven)); - } - } - - [Fact(DisplayName = "WithScale: reaches both ends of a narrow grid.")] - public void WithScaleReachesBounds() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - decimal value = Any.Decimal().Between(0m, 1m).WithScale(1).Generate(); - Check.That(value).IsEqualTo(Math.Round(value, 1, MidpointRounding.ToEven)); - seen.Add(value); - } - - Check.That(seen.Contains(0m)).IsTrue(); - Check.That(seen.Contains(1m)).IsTrue(); - } - - [Fact(DisplayName = "WithScale: composes with Except on the grid.")] - public void WithScaleComposesWithExcept() { - for (int i = 0; i < SampleCount; i++) { - decimal value = Any.Decimal().Between(0m, 1m).WithScale(1).Except(0.5m).Generate(); - Check.That(value).IsEqualTo(Math.Round(value, 1, MidpointRounding.ToEven)); - Check.That(value).IsNotEqualTo(0.5m); - } - } - - [Fact(DisplayName = "WithScale: a scale outside [0, 28] is rejected.")] - public void WithScaleArguments() { - Check.ThatCode(() => Any.Decimal().WithScale(-1)).Throws(); - Check.ThatCode(() => Any.Decimal().WithScale(29)).Throws(); - Any.Decimal().WithScale(0).Generate(); - Any.Decimal().WithScale(28).Generate(); - } - - [Fact(DisplayName = "WithScale: a range containing no grid point conflicts eagerly.")] - public void WithScaleEmptyGridConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Decimal().Between(0.001m, 0.009m).WithScale(2)); - Check.That(conflict.Message).Contains("WithScale(2)"); - } - - #endregion - - #region WithGranularity - - [Fact(DisplayName = "WithGranularity: every drawn instant/duration lands on the grid.")] - public void WithGranularityStaysOnGrid() { - long quarterHour = TimeSpan.FromMinutes(15).Ticks; - long oneSecond = TimeSpan.FromSeconds(1).Ticks; - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.DateTime().WithGranularity(TimeSpan.FromMinutes(15)).Generate().Ticks % quarterHour).IsEqualTo(0L); - Check.That(Any.TimeSpan().WithGranularity(TimeSpan.FromSeconds(1)).Generate().Ticks % oneSecond).IsEqualTo(0L); - Check.That(Any.DateTimeOffset().WithGranularity(TimeSpan.FromSeconds(1)).Generate().UtcTicks % oneSecond).IsEqualTo(0L); - } - } - - [Fact(DisplayName = "WithGranularity: composes with a range and stays aligned within it.")] - public void WithGranularityHonoursRange() { - DateTime start = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); - DateTime end = new(2026, 1, 5, 0, 0, 0, DateTimeKind.Utc); - long day = TimeSpan.FromDays(1).Ticks; - for (int i = 0; i < SampleCount; i++) { - DateTime value = Any.DateTime().Between(start, end).WithGranularity(TimeSpan.FromDays(1)).Generate(); - Check.That(value.Ticks % day).IsEqualTo(0L); - Check.That(value >= start && value <= end).IsTrue(); - } - } - - [Fact(DisplayName = "WithGranularity: a non-positive granularity is rejected.")] - public void WithGranularityArguments() { - Check.ThatCode(() => Any.DateTime().WithGranularity(TimeSpan.Zero)).Throws(); - Check.ThatCode(() => Any.TimeSpan().WithGranularity(TimeSpan.FromTicks(-1))).Throws(); - } - - [Fact(DisplayName = "WithGranularity: a window with no aligned instant conflicts eagerly.")] - public void WithGranularityEmptyGridConflicts() { - DateTime start = new(2026, 1, 1, 0, 0, 1, DateTimeKind.Utc); - DateTime end = new(2026, 1, 1, 0, 0, 2, DateTimeKind.Utc); - - Check.ThatCode(() => Any.DateTime().Between(start, end).WithGranularity(TimeSpan.FromDays(1))) - .Throws(); - } - - #endregion - -#if NET8_0_OR_GREATER - #region Modern types (net8.0) - - [Fact(DisplayName = "MultipleOf: the 128-bit integers draw on the grid too.")] - public void MultipleOfWideIntegers() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Int128().Positive().MultipleOf((Int128)1000).Generate() % 1000 == (Int128)0).IsTrue(); - Check.That(Any.UInt128().MultipleOf((UInt128)7).Generate() % 7 == (UInt128)0).IsTrue(); - } - - Check.ThatCode(() => Any.Int128().Between((Int128)1, (Int128)9).MultipleOf((Int128)10)).Throws(); - } - - [Fact(DisplayName = "WithGranularity: TimeOnly aligns to the grid.")] - public void WithGranularityTimeOnly() { - long oneSecond = TimeSpan.FromSeconds(1).Ticks; - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.TimeOnly().WithGranularity(TimeSpan.FromSeconds(1)).Generate().Ticks % oneSecond).IsEqualTo(0L); - } - } - - #endregion -#endif - -} diff --git a/JustDummies.UnitTests/AnyModernTypeTests.cs b/JustDummies.UnitTests/AnyModernTypeTests.cs deleted file mode 100644 index a40a81a7..00000000 --- a/JustDummies.UnitTests/AnyModernTypeTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyModernTypeTests { - - private const int SampleCount = 200; - - private static readonly DateOnly AnchorDate = new(2026, 1, 1); - private static readonly TimeOnly AnchorTime = new(12, 0, 0); - - [Fact(DisplayName = "Half is untouched by the ordinary-magnitude window: its whole domain is already ordinary.")] - public void HalfIsUnaffectedByTheOrdinaryWindow() { - // ADR-0052. Half stops at 65 504, well inside the window, so clipping to a window wider than the domain - // changes nothing — the rule narrows where a type is extravagant and stays silent where it is not. It lives - // in this file rather than beside the other continuous examples because Half is a .NET 5+ type, absent from - // the .NET Framework 4.7.2 floor leg this file is excluded from. - for (int i = 0; i < SampleCount; i++) { - Check.That((double)Any.Half().Generate()).IsStrictlyLessThan(65_505d); - } - } - - [Fact(DisplayName = "DateOnly: Between is inclusive and reached; After/Before are exclusive; conflicts surface.")] - public void DateOnlyBehaves() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - DateOnly value = Any.DateOnly().Between(AnchorDate, AnchorDate.AddDays(2)).Generate(); - seen.Add(value); - Check.That(value >= AnchorDate && value <= AnchorDate.AddDays(2)).IsTrue(); - Check.That(Any.DateOnly().After(AnchorDate).Before(AnchorDate.AddDays(2)).Generate()).IsEqualTo(AnchorDate.AddDays(1)); - } - Check.That(seen.Contains(AnchorDate)).IsTrue(); - Check.That(seen.Contains(AnchorDate.AddDays(2))).IsTrue(); - - Check.ThatCode(() => Any.DateOnly().After(DateOnly.MaxValue)).Throws(); - Check.ThatCode(() => Any.DateOnly().Between(AnchorDate.AddDays(1), AnchorDate)).Throws(); - } - - [Fact(DisplayName = "DateOnly: OneOf/Except/DifferentFrom behave.")] - public void DateOnlySets() { - DateOnly[] allowed = [AnchorDate, AnchorDate.AddDays(7)]; - for (int i = 0; i < SampleCount; i++) { - Check.That(allowed.Contains(Any.DateOnly().OneOf(allowed).Generate())).IsTrue(); - Check.That(Any.DateOnly().OneOf(allowed).Except(AnchorDate).Generate()).IsEqualTo(AnchorDate.AddDays(7)); - Check.That(Any.DateOnly().OneOf(allowed).DifferentFrom(AnchorDate.AddDays(7)).Generate()).IsEqualTo(AnchorDate); - } - } - - [Fact(DisplayName = "TimeOnly: bounds behave and the exclusive window pins the middle tick.")] - public void TimeOnlyBehaves() { - for (int i = 0; i < SampleCount; i++) { - TimeOnly value = Any.TimeOnly().Between(AnchorTime, AnchorTime.Add(TimeSpan.FromMinutes(5))).Generate(); - Check.That(value >= AnchorTime && value <= AnchorTime.Add(TimeSpan.FromMinutes(5))).IsTrue(); - - TimeOnly middle = Any.TimeOnly().After(AnchorTime).Before(new TimeOnly(AnchorTime.Ticks + 2)).Generate(); - Check.That(middle.Ticks).IsEqualTo(AnchorTime.Ticks + 1); - } - - Check.ThatCode(() => Any.TimeOnly().After(TimeOnly.MaxValue)).Throws(); - } - - [Fact(DisplayName = "Int128: signs, pins, full-width variety, extremes and conflicts.")] - public void Int128Behaves() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - seen.Add(Any.Int128().Generate()); - Check.That(Any.Int128().Positive().Generate() > 0).IsTrue(); - Check.That(Any.Int128().Negative().Generate() < 0).IsTrue(); - - Int128 bounded = Any.Int128().Between(1, 3).Generate(); - Check.That(bounded >= 1 && bounded <= 3).IsTrue(); - } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - - Check.That(Any.Int128().Zero().Generate() == 0).IsTrue(); - Check.ThatCode(() => Any.Int128().GreaterThan(Int128.MaxValue)).Throws(); - Check.ThatCode(() => Any.Int128().Positive().Negative()).Throws(); - } - - [Fact(DisplayName = "UInt128: bounds, exclusivity and full-width variety.")] - public void UInt128Behaves() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - seen.Add(Any.UInt128().Generate()); - - UInt128 bounded = Any.UInt128().Between(1, 3).Generate(); - Check.That(bounded >= 1 && bounded <= 3).IsTrue(); - Check.That(Any.UInt128().GreaterThan(5).LessThanOrEqualTo(6).Generate() == 6).IsTrue(); - } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - - Check.That(Any.UInt128().Zero().Generate() == 0).IsTrue(); - Check.ThatCode(() => Any.UInt128().GreaterThan(UInt128.MaxValue)).Throws(); - } - - [Fact(DisplayName = "Half: finite draws, strict Positive, pinned Zero, contained bounds, argument checks.")] - public void HalfBehaves() { - for (int i = 0; i < SampleCount; i++) { - Half value = Any.Half().Generate(); - Check.That(Half.IsNaN(value) || Half.IsInfinity(value)).IsFalse(); - Check.That(Any.Half().Positive().Generate() > Half.Zero).IsTrue(); - - Half bounded = Any.Half().Between((Half)1f, (Half)2f).Generate(); - Check.That(bounded >= (Half)1f && bounded <= (Half)2f).IsTrue(); - } - - Check.That(Any.Half().Zero().Generate() == Half.Zero).IsTrue(); - Check.ThatCode(() => Any.Half().GreaterThan(Half.NaN)).Throws(); - Check.ThatCode(() => Any.Half().GreaterThan(Half.MaxValue)).Throws(); - Check.ThatCode(() => Any.Half().Positive().Negative()).Throws(); - } - -} diff --git a/JustDummies.UnitTests/AnyNullableTests.cs b/JustDummies.UnitTests/AnyNullableTests.cs deleted file mode 100644 index 8c2470ad..00000000 --- a/JustDummies.UnitTests/AnyNullableTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyNullableTests { - - #region Statics members declarations - - private const int SampleCount = 200; - - // Note: chaining OrNull twice (a nullable of a nullable) is a compile-time error — a Nullable is not a - // struct and not a class, so neither OrNull overload applies. That guard needs no runtime test. - - private static string Render(int? value) { - return value?.ToString() ?? "null"; - } - - #endregion - - [Fact(DisplayName = "OrNull on a value type yields both null and non-null; the non-null values honour the inner constraints.")] - public void ValueTypeOrNullYieldsBothCases() { - IAny generator = Any.Int32().Between(1, 100).OrNull(); - - int nulls = 0; - int nonNulls = 0; - for (int i = 0; i < SampleCount; i++) { - int? value = generator.Generate(); - if (value is null) { - nulls++; - } else { - nonNulls++; - Check.That(value.Value is >= 1 and <= 100).IsTrue(); - } - } - - Check.That(nulls).IsStrictlyGreaterThan(0); - Check.That(nonNulls).IsStrictlyGreaterThan(0); - } - - [Fact(DisplayName = "OrNull on a reference type yields both null and non-null values satisfying the inner constraints.")] - public void ReferenceTypeOrNullYieldsBothCases() { - IAny generator = Any.String().NonEmpty().OrNull(); - - bool sawNull = false; - bool sawNonNull = false; - for (int i = 0; i < SampleCount; i++) { - string? value = generator.Generate(); - if (value is null) { - sawNull = true; - } else { - sawNonNull = true; - Check.That(value).IsNotEmpty(); - } - } - - Check.That(sawNull).IsTrue(); - Check.That(sawNonNull).IsTrue(); - } - - [Fact(DisplayName = "OrNull is reproducible: two same-seed contexts replay the same null/value sequence.")] - public void OrNullIsReproducibleUnderASeed() { - IAny first = Any.WithSeed(123).Int32().OrNull(); - IAny second = Any.WithSeed(123).Int32().OrNull(); - - string sequenceOne = string.Join("|", Enumerable.Range(0, 30).Select(_ => Render(first.Generate()))); - string sequenceTwo = string.Join("|", Enumerable.Range(0, 30).Select(_ => Render(second.Generate()))); - - Check.That(sequenceTwo).IsEqualTo(sequenceOne); - // The sequence exercises both branches — otherwise the reproducibility guarantee would be vacuous. - Check.That(sequenceOne).Contains("null"); - } - - [Fact(DisplayName = "OrNull composes with As to produce an optional value object.")] - public void OrNullComposesWithAs() { - IAny generator = Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create).OrNull(); - - bool sawNull = false; - bool sawNonNull = false; - for (int i = 0; i < SampleCount; i++) { - OrderReference? reference = generator.Generate(); - if (reference is null) { - sawNull = true; - } else { - sawNonNull = true; - Check.That(reference.Value).StartsWith("ORD-"); - } - } - - Check.That(sawNull).IsTrue(); - Check.That(sawNonNull).IsTrue(); - } - - [Fact(DisplayName = "OrNull validates its argument on both the value-type and reference-type overloads.")] - public void OrNullValidatesItsArgument() { - Check.ThatCode(() => ((IAny)null!).OrNull()).Throws(); - Check.ThatCode(() => ((IAny)null!).OrNull()).Throws(); - } - -} diff --git a/JustDummies.UnitTests/AnyOneOfTests.cs b/JustDummies.UnitTests/AnyOneOfTests.cs deleted file mode 100644 index cd27aa38..00000000 --- a/JustDummies.UnitTests/AnyOneOfTests.cs +++ /dev/null @@ -1,309 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -[TestSubject(typeof(AnyOneOf<>))] -public sealed class AnyOneOfTests { - - private const int SampleCount = 200; - - #region Statics members declarations - - private static IEnumerable Samples(IAny generator) { - for (int i = 0; i < SampleCount; i++) { - yield return generator.Generate(); - } - } - - #endregion - - [Fact(DisplayName = "OneOf draws only the supplied values, including domain objects.")] - public void DrawsOnlyTheSuppliedValues() { - Percentage ten = Percentage.Create(10); - Percentage twenty = Percentage.Create(20); - Percentage thirty = Percentage.Create(30); - Percentage[] allowed = [ten, twenty, thirty]; - - foreach (Percentage value in Samples(Any.OneOf(ten, twenty, thirty))) { - Check.That(allowed.Contains(value)).IsTrue(); - } - } - - [Fact(DisplayName = "OneOf eventually reaches every supplied value.")] - public void ReachesEverySuppliedValue() { - HashSet seen = [.. Samples(Any.OneOf(1, 2, 3))]; - - Check.That(seen).Contains(1, 2, 3); - } - - [Fact(DisplayName = "A single value pins the generated value.")] - public void SingleValueIsPinned() { - foreach (int value in Samples(Any.OneOf(42))) { - Check.That(value).IsEqualTo(42); - } - } - - [Fact(DisplayName = "OneOf varies from draw to draw when the pool holds more than one value.")] - public void VariesAcrossDraws() { - HashSet seen = [.. Samples(Any.OneOf(1, 2, 3, 4))]; - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "Duplicate values are collapsed under the default comparer: both distinct values are still drawn, nothing else.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Constraints", "JD025:The same value is listed twice in a pool", - Justification = - "The duplicate IS the subject. This pins the collapsing JD025 reports: without it there is nothing to collapse and the test " + - "asserts nothing.")] - public void DuplicatesAreCollapsed() { - HashSet seen = [.. Samples(Any.OneOf(1, 1, 2))]; - - Check.That(seen).IsOnlyMadeOf(1, 2); - Check.That(seen).Contains(1, 2); - } - - [Fact(DisplayName = "OneOf is reproducible under a seed.")] - public void ReproducibleUnderASeed() { - string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate())); - string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate())); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "OneOf composes into a value object through As.")] - public void ComposesThroughAs() { - IAny generator = Any.OneOf("ORD-12345678", "ORD-87654321").As(OrderReference.Create); - - for (int i = 0; i < SampleCount; i++) { - OrderReference reference = generator.Generate(); - Check.That(reference.Value).StartsWith("ORD-"); - Check.That(reference.Value.Length).IsEqualTo(12); - } - } - - [Fact(DisplayName = "OrNull makes the pool generator null about half the time, otherwise a member of the pool.")] - public void OrNullIsSometimesNull() { - Percentage one = Percentage.Create(1); - Percentage two = Percentage.Create(2); - IAny generator = Any.WithSeed(20260721).OneOf(one, two).OrNull(); - - List values = []; - for (int i = 0; i < SampleCount; i++) { - values.Add(generator.Generate()); - } - - Check.That(values.Any(value => value is null)).IsTrue(); - Check.That(values.Where(value => value is not null)).IsOnlyMadeOf(one, two); - } - - [Fact(DisplayName = "A distinct set over OneOf is gated by the pool's cardinality, both ways.")] - public void CardinalityGatesDistinctCollections() { - // Two distinct values cannot fill a set of three: caught eagerly, like any cardinality conflict. - Check.ThatCode(() => Any.SetOf(Any.OneOf(1, 2)).WithCount(3)).Throws(); - - // Within the domain it fills the set with the requested distinct values. - HashSet set = Any.SetOf(Any.OneOf(1, 2, 3)).WithCount(3).Generate(); - Check.That(set.Count).IsEqualTo(3); - Check.That(set).IsOnlyMadeOf(1, 2, 3); - } - - [Fact(DisplayName = "Reference identity keeps equal-valued but distinct instances as separate pool members.")] - public void ReferenceIdentityKeepsDistinctInstancesDistinct() { - // Percentage has no value equality, so two instances of the same percentage are distinct under the default - // comparer — the pool's cardinality is two, and a set of two is fillable. - Percentage first = Percentage.Create(50); - Percentage second = Percentage.Create(50); - - HashSet set = Any.SetOf(Any.OneOf(first, second)).WithCount(2).Generate(); - - Check.That(set.Count).IsEqualTo(2); - Check.That(set).IsOnlyMadeOf(first, second); - } - - [Fact(DisplayName = "OneOf rejects empty, null, or null-containing pools as arguments — null goes through OrNull.")] - public void RejectsInvalidPools() { - Check.ThatCode(() => Any.OneOf()).Throws(); - Check.ThatCode(() => Any.OneOf((string[])null!)).Throws(); - Check.ThatCode(() => Any.OneOf("a", null!)).Throws(); - } - - [Fact(DisplayName = "The null-element message points the caller at OrNull().")] - public void NullElementMessagePointsAtOrNull() { - ArgumentException error = Assert.Throws(() => Any.OneOf("a", null!)); - - Check.That(error.Message).Contains("OrNull"); - } - - [Fact(DisplayName = "ElementOf draws only from the list it is given.")] - public void ElementOfDrawsFromTheList() { - IReadOnlyList pool = [1, 2, 3]; - - HashSet seen = [.. Samples(Any.ElementOf(pool))]; - - Check.That(seen).IsOnlyMadeOf(1, 2, 3); - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "ElementOf materializes a lazy sequence once, not once per draw.")] - public void ElementOfMaterializesTheSequenceOnce() { - int enumerations = 0; - - IEnumerable Source() { - enumerations++; - yield return 1; - yield return 2; - yield return 3; - } - - AnyOneOf generator = Any.ElementOf(Source()); - for (int i = 0; i < SampleCount; i++) { - generator.Generate(); - } - - Check.That(enumerations).IsEqualTo(1); - } - - [Fact(DisplayName = "ElementOf validates null, empty and null elements like OneOf, for both the list and the sequence overload.")] - public void ElementOfValidatesItsPool() { - Check.ThatCode(() => Any.ElementOf((IReadOnlyList)null!)).Throws(); - Check.ThatCode(() => Any.ElementOf((IEnumerable)null!)).Throws(); - Check.ThatCode(() => Any.ElementOf(new List())).Throws(); - Check.ThatCode(() => Any.ElementOf(Enumerable.Empty())).Throws(); - Check.ThatCode(() => Any.ElementOf(["a", null!])).Throws(); - } - - [Fact(DisplayName = "DifferentFrom removes a value from the pool — the idiom for drawing another element of a fixture.")] - public void DifferentFromRemovesTheValue() { - List orders = [Percentage.Create(10), Percentage.Create(20), Percentage.Create(30)]; - Percentage used = orders[1]; - - foreach (Percentage value in Samples(Any.ElementOf(orders).DifferentFrom(used))) { - Check.That(value).IsNotEqualTo(used); - Check.That(orders.Contains(value)).IsTrue(); - } - } - - [Fact(DisplayName = "Except removes every supplied value, and the exclusions accumulate across declarations.")] - public void ExceptRemovesEveryValue() { - foreach (int value in Samples(Any.OneOf(1, 2, 3, 4).Except(2, 3))) { - Check.That(new[] { 1, 4 }).Contains(value); - } - - foreach (int value in Samples(Any.OneOf(1, 2, 3, 4).Except(2).DifferentFrom(3).Except(4))) { - Check.That(value).IsEqualTo(1); - } - } - - [Fact(DisplayName = "A value that is not in the pool removes nothing.")] - public void ExcludingAnAbsentValueRemovesNothing() { - HashSet seen = [.. Samples(Any.OneOf(1, 2).DifferentFrom(99))]; - - Check.That(seen).IsOnlyMadeOf(1, 2); - Check.That(seen).Contains(1, 2); - } - - [Fact(DisplayName = "A held collection passed to OneOf is one pool member: the draw is the collection itself, not a value from it.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Usage", "JD013:A held collection is passed to Any.OneOf, making a pool of one", - Justification = - "The one-member pool IS the subject. This pins the behaviour JD013 reports: inference binds T to the collection, so the call is " + - "legal, the draw succeeds, and what comes back is the whole list.")] - public void AHeldCollectionPassedToOneOfIsOnePoolMember() { - IReadOnlyList held = new[] { 1, 2, 3 }; - - IReadOnlyList drawn = Any.OneOf(held).Generate(); - - Check.That(ReferenceEquals(drawn, held)).IsTrue(); - - // ElementOf is the overload that draws FROM the collection — the same argument, a different pool. - Check.That(held.Contains(Any.ElementOf(held).Generate())).IsTrue(); - } - - [Fact(DisplayName = "An exclusion that empties the pool conflicts at declaration, naming both sides.")] - public void AnExclusionEmptyingThePoolConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.OneOf(1, 2).Except(1, 2)); - - Check.That(conflict.Message).IsEqualTo("Cannot apply Except(...) because it forbids every value OneOf(...) allows."); - } - - [Fact(DisplayName = "The emptying conflict names the factory that declared the pool, on every entry point.")] - public void TheConflictNamesTheDeclaringFactory() { - // Each factory carries its own name into the pool, so a swapped or stale literal on any of the six entry - // points would send the caller looking for a declaration they never wrote. - static string Emptied(Func declare) { - return Assert.Throws(() => declare()).Message; - } - - const string fromOneOf = "Cannot apply DifferentFrom(...) because it forbids every value OneOf(...) allows."; - const string fromElement = "Cannot apply DifferentFrom(...) because it forbids every value ElementOf(...) allows."; - - Check.That(Emptied(() => Any.OneOf(7).DifferentFrom(7))).IsEqualTo(fromOneOf); - Check.That(Emptied(() => Any.WithSeed(1).OneOf(7).DifferentFrom(7))).IsEqualTo(fromOneOf); - - Check.That(Emptied(() => Any.ElementOf([7]).DifferentFrom(7))).IsEqualTo(fromElement); - Check.That(Emptied(() => Any.ElementOf(new List { 7 }.Select(value => value)).DifferentFrom(7))).IsEqualTo(fromElement); - Check.That(Emptied(() => Any.WithSeed(1).ElementOf([7]).DifferentFrom(7))).IsEqualTo(fromElement); - Check.That(Emptied(() => Any.WithSeed(1).ElementOf(new List { 7 }.Select(value => value)).DifferentFrom(7))).IsEqualTo(fromElement); - } - - [Fact(DisplayName = "An exclusion that leaves a declared value standing qualifies its claim instead of overstating it.")] - public void AnExclusionLeavingADeclaredValueQualifiesItsClaim() { - // DifferentFrom(2) does not forbid 1 — the first exclusion took that one — so it does not forbid *every* - // value the pool was declared with, only what the first one left. The message says exactly that. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.OneOf(1, 2).DifferentFrom(1).DifferentFrom(2)); - - Check.That(conflict.Message).IsEqualTo("Cannot apply DifferentFrom(...) because it forbids every value OneOf(...) allows that the exclusions already declared leave."); - } - - [Fact(DisplayName = "An exclusion covering the whole declared pool is not qualified away by an earlier one.")] - public void AnExclusionCoveringTheWholePoolIsNotQualifiedAway() { - // Except(1, 2) forbids both declared values, so dropping the earlier exclusion could not help and the - // message must not suggest it: the claim stays the plain one, as it is without any prior narrowing. - ConflictingAnyConstraintException afterAnother = Assert.Throws( - () => Any.OneOf(1, 2).DifferentFrom(1).Except(1, 2)); - ConflictingAnyConstraintException onItsOwn = Assert.Throws( - () => Any.OneOf(1, 2).Except(1, 2)); - - Check.That(afterAnother.Message).IsEqualTo("Cannot apply Except(...) because it forbids every value OneOf(...) allows."); - Check.That(afterAnother.Message).IsEqualTo(onItsOwn.Message); - } - - [Fact(DisplayName = "A distinct set over an excluded pool is gated by the surviving cardinality.")] - public void CardinalityFollowsTheFilteredPool() { - // Three values minus one leaves two: a set of three no longer fits, a set of two does and holds exactly - // the survivors. - Check.ThatCode(() => Any.SetOf(Any.OneOf(1, 2, 3).DifferentFrom(2)).WithCount(3)).Throws(); - - HashSet set = Any.SetOf(Any.OneOf(1, 2, 3).DifferentFrom(2)).WithCount(2).Generate(); - Check.That(set).IsOnlyMadeOf(1, 3); - } - - [Fact(DisplayName = "The exclusion arguments are validated as arguments, not as conflicts.")] - public void ExclusionArgumentsAreValidated() { - Check.ThatCode(() => Any.OneOf("a", "b").Except(null!)).Throws(); - Check.ThatCode(() => Any.OneOf("a", "b").Except()).Throws(); - Check.ThatCode(() => Any.OneOf("a", "b").Except("a", null!)).Throws(); - Check.ThatCode(() => Any.OneOf("a", "b").DifferentFrom(null!)).Throws(); - } - - [Fact(DisplayName = "A seeded context makes OneOf and ElementOf deterministic — the mirrored surface draws from the context's seed.")] - public void SeededContextIsDeterministic() { - List pool = [10, 20, 30, 40]; - - string oneOfFirst = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20)); - string oneOfSecond = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20)); - Check.That(oneOfSecond).IsEqualTo(oneOfFirst); - - string elementFirst = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20)); - string elementSecond = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20)); - Check.That(elementSecond).IsEqualTo(elementFirst); - } - -} diff --git a/JustDummies.UnitTests/AnyPatternTests.cs b/JustDummies.UnitTests/AnyPatternTests.cs deleted file mode 100644 index e1356a17..00000000 --- a/JustDummies.UnitTests/AnyPatternTests.cs +++ /dev/null @@ -1,556 +0,0 @@ -#region Usings declarations - -using System.Reflection; -using System.Text.RegularExpressions; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyPatternTests { - - #region Statics members declarations - - private const int SampleCount = 200; - - // The oracle: a generated value is correct iff the REAL .NET regex engine fully matches it. Anchoring with - // ^(?:...)$ turns the partial-match IsMatch into a whole-string test, so it catches both under-generation - // (too few characters) and over-generation (trailing junk), and handles top-level alternation correctly. - private static void AssertMatches(string value, string pattern, RegexOptions options = RegexOptions.None) { - Assert.True(Regex.IsMatch(value, "^(?:" + pattern + ")$", options), - $"generated value {Display(value)} is not matched by /{pattern}/"); - } - - private static string Display(string value) { - return "\"" + value.Replace("\t", "\\t").Replace("\n", "\\n").Replace("\r", "\\r") + "\""; - } - - // The oracle again, as a predicate: does the real .NET engine compile this pattern at all? Used to assert the - // rejection taxonomy — a pattern the engine accepts must never be reported as malformed (ArgumentException). - private static bool IsCompiledByTheRealEngine(string pattern) { - try { - _ = new Regex(pattern); - - return true; - } catch (ArgumentException) { - return false; - } - } - - #endregion - - [Theory(DisplayName = "Every generated value is fully matched by the real .NET regex engine.")] - [InlineData(@"\d{8}")] - [InlineData(@"^ORD-\d{8}$")] - [InlineData(@"[A-Z]{3}")] - [InlineData(@"[a-z]{2,5}")] - [InlineData(@"(EUR|USD|GBP)")] - [InlineData(@"[A-Za-z0-9_]+")] - [InlineData(@"\w{4}\d{2}")] - [InlineData(@"[^0-9]{3}")] - [InlineData(@"colou?r")] - [InlineData(@"a{2,4}b*c+")] - [InlineData(@"(ab|cd){2,3}")] - [InlineData(@"\d+\.\d{2}")] - [InlineData(@"[A-F0-9]{6}")] - [InlineData(@"(?:foo|bar)-\d+")] - [InlineData(@"(?\d{4})-(?\d{2})")] - [InlineData(@"(?'tag'\d{2})")] - [InlineData(@"(?<1>x)")] // explicitly-numbered group: a valid capture NUMBER, not an invalid name - [InlineData(@"(?'2'y)")] // ...same, quote form - [InlineData(@"(?<10>ab)")] // a multi-digit group number stays valid - [InlineData(@"(?xy)")] // a named group whose name merely contains digits stays valid - [InlineData(@"^a$|^b$")] - [InlineData(@"^^abc")] // a run of boundary anchors is a no-op, exactly as in the real engine - [InlineData(@"abc$$")] - [InlineData(@"^*abc")] // a quantifier on a zero-width anchor is a no-op too - [InlineData(@"abc$*")] - [InlineData(@"^{2}xy")] - [InlineData(@"^?abc$?")] - [InlineData(@"[-[x]]")] // a leading '-' is an ordinary hyphen, not a subtraction operator - [InlineData(@"[-[abc]]")] - [InlineData(@"[\d]{3}")] - [InlineData(@"[-a-z]{2}")] - [InlineData(@"[a-z-]{2}")] - [InlineData(@"[a-b-z]{4}")] - [InlineData(@"[]a]{3}")] - [InlineData(@"[^]]{3}")] - [InlineData(@"[\b]")] - [InlineData(@"[\1]")] - [InlineData(@"[\x30-\x39]{3}")] - [InlineData(@"(a|aa|aaa)")] - [InlineData(@"a+?b*?")] - [InlineData(@"\x41\x2DB")] - [InlineData(@"\a\t")] - [InlineData(@"\e")] - [InlineData(@"\cM")] - [InlineData(@"\0")] - [InlineData(@"\07")] - [InlineData(@"a{x}")] - [InlineData(@"{abc}")] - [InlineData(@"a{2,")] - [InlineData(@"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")] - [InlineData(@"[A-Z]{2}\d{2}[A-Z0-9]{10}")] - [InlineData(@"([01]\d|2[0-3]):[0-5]\d")] - [InlineData(@"\d+\.\d+\.\d+(-[a-z]+(\.\d+)?)?")] - [InlineData(@"\s")] - [InlineData(@".")] - [InlineData(@"")] - public void GeneratedValuesMatchTheRealEngine(string pattern) { - AnyContext context = Any.WithSeed(20260718); - AnyPattern generator = context.StringMatching(pattern); - - for (int i = 0; i < SampleCount; i++) { - AssertMatches(generator.Generate(), pattern); - } - } - - [Fact(DisplayName = "Generated values vary from draw to draw whenever the pattern leaves room.")] - public void GeneratedValuesVary() { - foreach (string pattern in new[] { @"\d{8}", @"[A-Z]{3}", @"(EUR|USD|GBP)", @"[A-Za-z0-9_]+", @"a{2,4}b*c+" }) { - AnyPattern generator = Any.WithSeed(4242).StringMatching(pattern); - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - } - - [Fact(DisplayName = "A fixed-shape pattern yields exactly that shape.")] - public void FixedShape() { - for (int i = 0; i < SampleCount; i++) { - string reference = Any.StringMatching(@"^ORD-\d{8}$").Generate(); - Check.That(reference.Length).IsEqualTo(12); - Check.That(reference).StartsWith("ORD-"); - Check.That(reference.Substring(4)).Matches("^[0-9]{8}$"); - } - } - - [Fact(DisplayName = "Alternation draws each branch and only declared branches.")] - public void Alternation() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - string value = Any.StringMatching("(EUR|USD|GBP)").Generate(); - Check.That(value == "EUR" || value == "USD" || value == "GBP").IsTrue(); - seen.Add(value); - } - - Check.That(seen).Contains("EUR", "USD", "GBP"); - } - - [Fact(DisplayName = "Character classes, ranges and negation stay within their set.")] - public void CharacterClasses() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.StringMatching("[A-Z]{3}").Generate()).Matches("^[A-Z]{3}$"); - Check.That(Any.StringMatching("[^0-9]{4}").Generate()).Matches("^[^0-9]{4}$"); - Check.That(Any.StringMatching(@"[\d]{5}").Generate()).Matches("^[0-9]{5}$"); - } - } - - [Fact(DisplayName = "Bounded quantifiers stay within their bounds; unbounded ones draw the minimum plus 0 to 8.")] - public void QuantifierBounds() { - HashSet starLengths = []; - HashSet plusLengths = []; - HashSet openLengths = []; - - for (int i = 0; i < SampleCount; i++) { - int bounded = (Any.StringMatching("a{2,4}").Generate()).Length; - Check.That(bounded is >= 2 and <= 4).IsTrue(); - - starLengths.Add((Any.StringMatching("a*").Generate()).Length); - plusLengths.Add((Any.StringMatching("a+").Generate()).Length); - openLengths.Add((Any.StringMatching("a{2,}").Generate()).Length); - } - - Check.That(starLengths.Min()).IsEqualTo(0); - Check.That(starLengths.Max()).IsEqualTo(8); // 0 + 0..8 - Check.That(plusLengths.Min()).IsEqualTo(1); - Check.That(plusLengths.Max()).IsEqualTo(9); // 1 + 0..8 - Check.That(openLengths.Min()).IsEqualTo(2); - Check.That(openLengths.Max()).IsEqualTo(10); // 2 + 0..8 - } - - [Fact(DisplayName = "Anchors are no-ops: the whole generated string is the match.")] - public void AnchorsAreNoOps() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.StringMatching("^abc$").Generate()).IsEqualTo("abc"); - } - } - - [Fact(DisplayName = "Repeated and quantified boundary anchors are no-ops, consistently for '^' and '$'.")] - public void RepeatedAndQuantifiedAnchorsAreNoOps() { - // '^^abc' was already accepted while the symmetric 'abc$$' was refused — an avoidable asymmetry, now closed. - // Repeating or quantifying a zero-width boundary assertion never changes which strings match, so all of - // these are no-ops, exactly as the real engine treats them. - Check.That(Any.StringMatching(@"^^abc$$").Generate()).IsEqualTo("abc"); - Check.That(Any.StringMatching(@"^*abc$*").Generate()).IsEqualTo("abc"); - Check.That(Any.StringMatching(@"^{2}abc").Generate()).IsEqualTo("abc"); - Check.That(Any.StringMatching(@"^?abc$?").Generate()).IsEqualTo("abc"); - } - - [Fact(DisplayName = "An unbounded quantifier whose minimum sits at int.MaxValue overruns the ceiling; it never yields a short value.")] - public void UnboundedQuantifierAtTheTopOfTheIntRangeNeverYieldsAShortValue() { - // Regression: the unbounded repetition count was computed as 'min + Next(0, 9)' in int arithmetic, so a - // minimum within 8 of int.MaxValue wrapped negative and the repetition loop wrote nothing — Generate() - // returned "" for a pattern demanding 2,147,483,647 characters, in 36 draws out of 40. A value the pattern - // does not match is the one outcome generation must never produce; overrunning the ceiling is the honest - // answer, and the same one 'a{100000,}' already gave. - AnyPattern generator = Any.StringMatching("a{2147483647,}"); - - for (int i = 0; i < 40; i++) { - AnyGenerationException caught = Assert.Throws(() => generator.Generate()); - Check.That(caught.Message).Contains("generation limit"); - } - } - - [Theory(DisplayName = "Across the whole overflow band, an unbounded minimum overruns the ceiling rather than wrapping negative.")] - [InlineData(2147483639)] // int.MaxValue - 8: the last minimum that could not wrap even before the fix - [InlineData(2147483640)] // int.MaxValue - 7: the first that could, and did — 3 empty strings out of 30 - [InlineData(2147483646)] - [InlineData(2147483647)] - public void EveryMinimumInTheOverflowBandOverrunsTheCeiling(int minimum) { - // The band is 'min > int.MaxValue - UnboundedExtra'. Pinned at both edges so a future change to - // UnboundedExtra cannot narrow the guard back to a subset of it without failing here. - AnyPattern generator = Any.WithSeed(1).StringMatching($"a{{{minimum},}}"); - - for (int i = 0; i < 12; i++) { - Assert.Throws(() => generator.Generate()); - } - } - - [Fact(DisplayName = "A pattern that overruns the generation ceiling fails with an honest message, naming no false cause.")] - public void OverLimitPatternFailsWithoutAFalseCause() { - // '(a{1000}){1000}' deterministically asks for 1,000,000 characters — every quantifier is bounded, there is - // no unbounded quantifier at all. The pattern parses fine (a resource ceiling is not a satisfiability - // conflict); generation is what overruns the limit, and the message must not blame a quantifier that is absent. - AnyPattern generator = Any.StringMatching(@"(a{1000}){1000}"); - - AnyGenerationException caught = Assert.Throws(() => generator.Generate()); - Check.That(caught.Message).Contains("generation limit"); - Check.That(caught.Message).Contains("bounded quantifiers"); // the real cause is offered - Check.That(caught.Message).Not.Contains("is expanding without bound"); // the old, false assertion is gone - } - - [Fact(DisplayName = "A negated class that excludes the whole printable universe is refused as unsupported, not malformed.")] - public void NegatedClassExcludingTheUniverseIsUnsupported() { - // Well-formed and regular — the real engine accepts both — but no printable-ASCII character survives the - // negation, so JustDummies cannot draw a value. That is a universe limit (unsupported), not a caller mistake - // (malformed): the pattern is not broken, JustDummies simply does not reach outside printable ASCII. - Check.ThatCode(() => Any.StringMatching(@"[^\x20-\x7E]")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"[^\s\S]")).Throws(); - - UnsupportedRegexException caught = Assert.Throws(() => Any.StringMatching(@"[^\x20-\x7E]")); - Check.That(caught.Message).Contains("printable ASCII"); - } - - [Theory(DisplayName = "A pattern the real .NET engine accepts is never rejected as malformed — it generates, or is refused as unsupported.")] - [InlineData(@"^^abc")] - [InlineData(@"abc$$")] - [InlineData(@"^*abc")] - [InlineData(@"abc$*")] - [InlineData(@"^{2}xy")] - [InlineData(@"^?a$?")] - [InlineData(@"[-[x]]")] - [InlineData(@"[a-[x]]")] // subtraction (member precedes): .NET-valid, refused as UNSUPPORTED, not malformed - [InlineData(@"[ab-[b]]")] - [InlineData(@"[^\x20-\x7E]")] // negated-empty: .NET-valid, refused as UNSUPPORTED - [InlineData(@"[^\s\S]")] - [InlineData(@"(a{1000}){1000}")] // over-limit: .NET-valid, fails at generation time, never as malformed - public void PatternsAcceptedByTheRealEngineAreNeverMalformed(string pattern) { - // The advertised taxonomy (see RegexParser's summary): ArgumentException == "the real engine rejects this - // pattern as malformed". So a pattern the real engine COMPILES must never surface here as ArgumentException — - // JustDummies must either generate a matching value, or refuse it as UnsupportedRegexException, or fail the - // generation itself. This guards the whole channel, not just the individual edges #210 corrected. - Assert.True(IsCompiledByTheRealEngine(pattern), $"test precondition: /{pattern}/ must be accepted by .NET"); - - try { - Any.WithSeed(1).StringMatching(pattern).Generate(); - } catch (ArgumentException e) { - Assert.Fail($"/{pattern}/ is accepted by the real engine but JustDummies rejected it as malformed: {e.Message}"); - } catch (UnsupportedRegexException) { - // acceptable: refused as unsupported (a construct or universe JustDummies declines), not as malformed - } catch (AnyGenerationException) { - // acceptable: a resource-limit overrun, not a verdict that the pattern is malformed - } - } - - [Fact(DisplayName = "A Regex with IgnoreCase generates either case.")] - public void IgnoreCaseHonoured() { - Regex pattern = new("^[a-z]{5}$", RegexOptions.IgnoreCase); - bool sawUpper = false; - AnyContext context = Any.WithSeed(99); - AnyPattern generator = context.StringMatching(pattern); - - for (int i = 0; i < SampleCount; i++) { - string value = generator.Generate(); - AssertMatches(value, "[a-z]{5}", RegexOptions.IgnoreCase); - if (value.Any(char.IsUpper)) { sawUpper = true; } - } - - Check.That(sawUpper).IsTrue(); - } - - [Fact(DisplayName = "A matching generator composes into a value object through As.")] - public void ComposesThroughAs() { - IAny generator = Any.StringMatching(@"^ORD-\d{8}$").As(OrderReference.Create); - - for (int i = 0; i < 50; i++) { - OrderReference reference = generator.Generate(); - Check.That(reference.Value).StartsWith("ORD-"); - Check.That(reference.Value.Length).IsEqualTo(12); - } - } - - [Fact(DisplayName = "Matching is reproducible under a seed.")] - public void ReproducibleUnderASeed() { - string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").Generate())); - string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").Generate())); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "DifferentFrom never yields the excluded value, and keeps the pattern's format.")] - public void DifferentFromNeverYieldsTheExcludedValue() { - const string pattern = @"^ORD-\d{8}$"; - const string existing = "ORD-12345678"; - - IAny generator = Any.WithSeed(20260728).StringMatching(pattern).DifferentFrom(existing); - - for (int i = 0; i < SampleCount; i++) { - string value = generator.Generate(); - Check.That(value).IsNotEqualTo(existing); - AssertMatches(value, pattern); - } - } - - [Fact(DisplayName = "Except removes every supplied value, and the exclusions accumulate across declarations.")] - public void ExceptRemovesEverySuppliedValue() { - // A four-word language with three words excluded: the surviving draw is forced, whichever declaration - // removed each word. - IAny generator = Any.WithSeed(20260728).StringMatching("^[ab]{2}$").Except("ab", "ba").DifferentFrom("bb"); - - for (int i = 0; i < SampleCount; i++) { - Check.That(generator.Generate()).IsEqualTo("aa"); - } - } - - [Fact(DisplayName = "An exclusion is bounded and rejective: it keeps the draw reproducible under a seed.")] - public void AnExclusionStaysReproducibleUnderASeed() { - string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").DifferentFrom("ABC-1234").Generate())); - string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").DifferentFrom("ABC-1234").Generate())); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "An exhausted exclusion budget reports the budget, never a claim that the pattern matches nothing else.")] - public void AnExhaustedExclusionBudgetReportsTheBudget() { - // "^[ab]$" really is a two-word language and both words are excluded — yet the generator only ever built - // and rejected candidates, so the message may claim the spent budget and nothing stronger. - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260728).StringMatching("^[ab]$").Except("a", "b").Generate()); - - Check.That(error.Message).Contains("10000 draws"); - Check.That(error.Message).Contains("exhausted budget rather than a proof"); - Check.That(error.Message).Not.Contains("the pattern has no other value"); - Check.That(error.Message).Contains("Loosen the exclusions or widen the pattern"); - } - - [Fact(DisplayName = "The exhaustion names the pattern, the excluded values and the seed that replays the run.")] - public void TheExhaustionCarriesTheSeed() { - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260728).StringMatching("^[ab]$").Except("a", "b").Generate()); - - Check.That(error.Message).Contains("\"^[ab]$\""); - Check.That(error.Message).Contains("\"a\", \"b\""); - Check.That(error.Message).Contains("Any.WithSeed(20260728)"); - Check.That(error.Seed).IsEqualTo(20260728); - } - - [Fact(DisplayName = "A value excluded twice is listed once: the exclusions collapse rather than accumulate.")] - public void RepeatedExclusionsCollapse() { - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260728).StringMatching("^[ab]$").Except("a").DifferentFrom("a").Except("a", "b").Generate()); - - Check.That(error.Message).Contains("excluding \"a\", \"b\":"); - Check.That(error.Message).Not.Contains("\"a\", \"a\""); - } - - [Fact(DisplayName = "A shape constraint stays refused: only the rejective pair is offered.")] - public void OnlyTheRejectivePairIsOffered() { - // Constructive constraints would mean building a value in the intersection of two regular languages, which - // the generator has no machinery for; the exclusion pair needs none, so it is the whole added surface. - string[] fluent = typeof(AnyPattern) - .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Where(method => method.ReturnType == typeof(AnyPattern) && !method.IsSpecialName) - .Select(method => method.Name) - .Distinct() - .OrderBy(name => name, StringComparer.Ordinal) - .ToArray(); - - Check.That(fluent).ContainsExactly("DifferentFrom", "Except"); - } - - [Fact(DisplayName = "The exclusion arguments are validated as arguments, not as conflicts.")] - public void ExclusionArgumentsAreValidated() { - Check.ThatCode(() => Any.StringMatching("a").Except(null!)).Throws(); - Check.ThatCode(() => Any.StringMatching("a").Except()).Throws(); - Check.ThatCode(() => Any.StringMatching("a").Except("a", null!)).Throws(); - Check.ThatCode(() => Any.StringMatching("a").DifferentFrom(null!)).Throws(); - } - - [Fact(DisplayName = "Non-regular constructs are refused eagerly with UnsupportedRegexException.")] - public void UnsupportedConstructsAreRefused() { - Check.ThatCode(() => Any.StringMatching(@"foo(?=bar)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"foo(?!bar)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(?<=x)y")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"\bword\b")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(\w+)\s\1")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"\p{L}+")).Throws(); - - UnsupportedRegexException caught = Assert.Throws(() => Any.StringMatching(@"a(?=b)")); - Check.That(caught.Message).Contains("lookahead"); - } - - [Fact(DisplayName = "Constructs whose language a plain walk cannot honour are refused, never mis-generated.")] - public void NotGeneratableConstructsAreRefused() { - // An atomic group commits to its first matching branch: (?>ab|a)b matches only "abb", so lowering it to - // a plain alternation could emit "ab" — refused instead. - Check.ThatCode(() => Any.StringMatching(@"(?>ab|a)b")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(?>a)")).Throws(); - - // A misplaced anchor makes the pattern unmatchable by any whole string. - Check.ThatCode(() => Any.StringMatching(@"a^")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"$a")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"x(^a)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(a$)x")).Throws(); - - // .NET class subtraction removes a nested class; parsing '-[' as members would generate outside the set. It - // is subtraction only when a base member precedes the '-[' — after a range ('[a-z-[aeiou]]') or a single - // member ('[a-[x]]', '[ab-[b]]'). A leading '-[' is an ordinary hyphen and IS accepted (see the oracle theory). - Check.ThatCode(() => Any.StringMatching(@"[a-z-[aeiou]]")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"[a-[x]]")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"[ab-[b]]")).Throws(); - - // IgnorePatternWhitespace changes how the pattern text itself is read: "^A B$" matches "AB", not "A B". - Check.ThatCode(() => Any.StringMatching(new Regex("^A B$", RegexOptions.IgnorePatternWhitespace))).Throws(); - Check.ThatCode(() => Any.WithSeed(1).StringMatching(new Regex("^A B$", RegexOptions.IgnorePatternWhitespace))).Throws(); - } - - [Fact(DisplayName = "A balancing group is refused as unsupported — both syntaxes, target defined or not.")] - public void BalancingGroupsAreRefused() { - // A balancing group '(?<-name>…)' / '(?…)' pops the capture stack — the backreference family, - // which is non-regular. Its language is not that of a plain named group: '(?y)?(?<-a>x)' matches only - // "yx" (the '-a' pop forces the optional 'a' group to have fired), yet lowering '(?<-a>x)' to an ordinary - // named group would emit "x". It is refused instead of mis-generated. .NET accepts these two target-defined - // patterns, so the refusal is a genuine "we decline what a plain walk cannot honour", not an echo of .NET. - Check.ThatCode(() => Any.StringMatching(@"(?y)?(?<-a>x)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(?y)?(?'-a'x)")).Throws(); // quote form - - // The '-' is refused even when the target group is undefined — where the real engine instead reports a - // malformed pattern. Distinguishing the two would need a table of captured groups the generator does not - // keep; the divergence is only in the error kind (both reject, neither mis-generates) and is accepted. - Check.ThatCode(() => Any.StringMatching(@"(?<-a>x)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(?'-a'x)")).Throws(); // quote form - Check.ThatCode(() => Any.StringMatching(@"(?z)")).Throws(); // name1-name2 form - - UnsupportedRegexException caught = Assert.Throws(() => Any.StringMatching(@"(?y)?(?<-a>x)")); - Check.That(caught.Message).Contains("balancing group"); - } - - [Fact(DisplayName = "Malformed patterns raise ArgumentException; a null pattern raises ArgumentNullException.")] - public void MalformedPatternsAreRejected() { - Check.ThatCode(() => Any.StringMatching(@"[a-")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(abc")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"a{3,1}")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"*abc")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"a\")).Throws(); - Check.ThatCode(() => Any.StringMatching((string)null!)).Throws(); - Check.ThatCode(() => Any.StringMatching((Regex)null!)).Throws(); - } - - [Fact(DisplayName = "Patterns the real engine rejects are rejected here too, never silently re-interpreted.")] - public void RealEngineRejectionsAreMirrored() { - // Each of these is refused by System.Text.RegularExpressions; accepting them would make the generator - // produce values for patterns no production code could ever carry. - Check.ThatCode(() => Any.StringMatching(@"a*+")).Throws(); // possessive: not a .NET construct - Check.ThatCode(() => Any.StringMatching(@"a**")).Throws(); // nested quantifier - Check.ThatCode(() => Any.StringMatching(@"a*??")).Throws(); // nested quantifier - Check.ThatCode(() => Any.StringMatching(@"[]")).Throws(); // unterminated class - Check.ThatCode(() => Any.StringMatching(@"\q")).Throws(); // unrecognized escape - Check.ThatCode(() => Any.StringMatching(@"\x4")).Throws(); // \x expects 2 hex digits - Check.ThatCode(() => Any.StringMatching(@"\c1")).Throws(); // \c expects a letter - Check.ThatCode(() => Any.StringMatching(@"{2}")).Throws(); // quantifier following nothing - Check.ThatCode(() => Any.StringMatching(@"(?<>a)")).Throws(); // empty group name - } - - [Fact(DisplayName = "An invalid group name is rejected as malformed, matching the real engine — both syntaxes.")] - public void InvalidGroupNamesAreRejected() { - // A name opening with a digit is an explicit capture NUMBER, which the real engine accepts only as a positive - // integer with no leading zero. '0' (reserved for the whole match), a leading zero, and a digit-then-letter - // name are all rejected — here as they are there. - Check.ThatCode(() => Any.StringMatching(@"(?<1a>x)")).Throws(); // digit then letter - Check.ThatCode(() => Any.StringMatching(@"(?<0>x)")).Throws(); // group 0 is reserved - Check.ThatCode(() => Any.StringMatching(@"(?<01>x)")).Throws(); // leading zero - Check.ThatCode(() => Any.StringMatching(@"(?'0'x)")).Throws(); // quote form, reserved - - // A non-numeric name must be word characters (letter, digit or underscore); a space or a dot is malformed. - Check.ThatCode(() => Any.StringMatching(@"(?x)")).Throws(); - Check.ThatCode(() => Any.StringMatching(@"(?'a b'x)")).Throws(); // quote form - Check.ThatCode(() => Any.StringMatching(@"(?x)")).Throws(); - } - - [Fact(DisplayName = "Escape sequences generate the real characters, not their letter.")] - public void EscapesGenerateTheRealCharacters() { - Check.That(Any.StringMatching(@"\a").Generate()).IsEqualTo("\a"); - Check.That(Any.StringMatching(@"\e").Generate()).IsEqualTo("\u001B"); - Check.That(Any.StringMatching(@"\x41").Generate()).IsEqualTo("A"); - Check.That(Any.StringMatching(@"\u0042").Generate()).IsEqualTo("B"); - Check.That(Any.StringMatching(@"\cA").Generate()).IsEqualTo("\u0001"); - Check.That(Any.StringMatching(@"\07").Generate()).IsEqualTo("\a"); - Check.That(Any.StringMatching(@"\0").Generate()).IsEqualTo("\0"); - } - - [Fact(DisplayName = "A brace that is not a well-formed quantifier is a literal, exactly as in the real engine.")] - public void BraceLiteralsGenerate() { - Check.That(Any.StringMatching(@"a{x}").Generate()).IsEqualTo("a{x}"); - Check.That(Any.StringMatching(@"{abc}").Generate()).IsEqualTo("{abc}"); - Check.That(Any.StringMatching(@"a{2,").Generate()).IsEqualTo("a{2,"); - } - - [Fact(DisplayName = "Nesting groups beyond the parser's depth ceiling fails cleanly instead of overflowing the stack.")] - public void DeepNestingFailsCleanly() { - string deep = new string('(', 300) + "a" + new string(')', 300); - - ArgumentException caught = Assert.Throws(() => Any.StringMatching(deep)); - Check.That(caught.Message).Contains("nested"); - } - - [Theory(DisplayName = "A class range ending at U+FFFF terminates promptly and yields a member, instead of hanging.")] - [InlineData(@"[\u0020-\uFFFF]")] // \uFFFF escape: drives the range's upper bound to the top of the char space... - [InlineData("[ -\uFFFF]")] // ...and a literal U+FFFF member does the same; both once wrapped the 16-bit loop. - public async Task ClassRangeEndingAtMaxCharTerminates(string pattern) { - // Generate off-thread and race a deadline: a loop that wraps a 16-bit char past U+FFFF loses the race and - // fails the test instead of hanging the whole suite (mirrors the AnyGuid carry-wraparound guard). - Task run = Task.Run(() => Any.StringMatching(pattern).Generate()); - Task first = await Task.WhenAny(run, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)); - Check.That(first == run).IsTrue(); - - // The generated value is a genuine member of the class — the real .NET engine is the oracle. - AssertMatches(await run, pattern); - } - - [Fact(DisplayName = "A nullable alternative under a quantifier never yields a value the pattern rejects (regression #335).")] - public void ANullableAlternativeUnderAQuantifierNeverYieldsAnUnmatchedValue() { - // #335: the structural generator picked the zero-width \S{0} branch and emitted "", but the real .NET - // engine refuses "" for this shape — an arcane, order- and form-dependent empty-match behaviour the - // generator cannot mirror. Every draw must match the pattern the value was generated from. - const string pattern = @"(?:r{1,2}|\S{0}){1,2}"; - IAny generator = Any.StringMatching(pattern); - - for (int i = 0; i < 1000; i++) { - AssertMatches(generator.Generate(), pattern); - } - } - -} diff --git a/JustDummies.UnitTests/AnySetTypeTests.cs b/JustDummies.UnitTests/AnySetTypeTests.cs deleted file mode 100644 index 61abe327..00000000 --- a/JustDummies.UnitTests/AnySetTypeTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnySetTypeTests { - - private const int SampleCount = 200; - - private enum OrderStatus { - - Draft, - Validated, - Cancelled - - } - - [Fact(DisplayName = "Boolean: unconstrained draws hit both values; pins pin; contradictory pins conflict.")] - public void BooleanBehaves() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Boolean().Generate()); } - Check.That(seen.Count).IsEqualTo(2); - - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Boolean().True().Generate()).IsTrue(); - Check.That(Any.Boolean().False().Generate()).IsFalse(); - Check.That(Any.Boolean().DifferentFrom(true).Generate()).IsFalse(); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Boolean().True().False()); - Check.That(conflict.Message).Contains("False()"); - Check.That(conflict.Message).Contains("True()"); - - bool value = Any.Boolean().True().Generate(); - Check.That(value).IsTrue(); - } - - [Fact(DisplayName = "Guid: unconstrained draws are non-empty, varied, and reproducible under a context seed.")] - public void GuidBehaves() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - Guid value = Any.Guid().Generate(); - seen.Add(value); - Check.That(value).IsNotEqualTo(Guid.Empty); - } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - - Check.That(Any.WithSeed(42).Guid().Generate()).IsEqualTo(Any.WithSeed(42).Guid().Generate()); - } - - [Fact(DisplayName = "Guid: Empty pins, NonEmpty excludes, and the pair conflicts in both orders.")] - public void GuidEmptyFamily() { - Check.That(Any.Guid().Empty().Generate()).IsEqualTo(Guid.Empty); - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Guid().NonEmpty().Generate()).IsNotEqualTo(Guid.Empty); - } - - Check.ThatCode(() => Any.Guid().Empty().NonEmpty()).Throws(); - Check.ThatCode(() => Any.Guid().NonEmpty().Empty()).Throws(); - } - - [Fact(DisplayName = "Guid: OneOf stays within, exhausting it conflicts, DifferentFrom never yields the value.")] - public void GuidSets() { - Guid first = Guid.NewGuid(); - Guid second = Guid.NewGuid(); - - for (int i = 0; i < SampleCount; i++) { - Guid value = Any.Guid().OneOf(first, second).Generate(); - Check.That(value == first || value == second).IsTrue(); - Check.That(Any.Guid().OneOf(first, second).DifferentFrom(first).Generate()).IsEqualTo(second); - } - - Check.ThatCode(() => Any.Guid().OneOf(first).Except(first)).Throws(); - } - - [Fact(DisplayName = "Guid: excluding all 256 last-byte variants of the drawn prefix escapes by carry, never hangs, and stays reproducible.")] - public async Task GuidExclusionByteWraparoundTerminates() { - const int seed = 20260718; - - // The first unconstrained draw under this seed fixes the 15-byte prefix the escape starts from; a - // second context with the same seed replays that same first draw, since Except() consumes no randomness. - Guid drawn = Any.WithSeed(seed).Guid().Generate(); - byte[] prefix = drawn.ToByteArray(); - - // Every identifier sharing that prefix and differing only in the last byte — the exact block the former - // last-byte-only walk cycled inside forever. - Guid[] block = new Guid[256]; - for (int last = 0; last < 256; last++) { - byte[] variant = (byte[])prefix.Clone(); - variant[15] = (byte)last; - block[last] = new Guid(variant); - } - - // Generate off-thread and race a deadline: a regression that reintroduces the unbounded loop loses the - // race and fails the test instead of hanging the whole suite. - Task run = Task.Run(() => Any.WithSeed(seed).Guid().Except(block).Generate()); - Task first = await Task.WhenAny(run, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)); - Check.That(first == run).IsTrue(); - - Guid escaped = await run; - Check.That(block.Contains(escaped)).IsFalse(); - Check.That(escaped).IsNotEqualTo(drawn); - - // Same seed and same exclusions yield the same escaped identifier. - Guid again = Any.WithSeed(seed).Guid().Except(block).Generate(); - Check.That(again).IsEqualTo(escaped); - } - - [Fact(DisplayName = "Enum: unconstrained draws yield only declared members and reach all of them.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2263:Prefer generic overload when type is known", - Justification = - "Enum.IsDefined(TEnum) arrived in .NET 5 and this suite also runs on the .NET Framework 4.7.2 " + - "support floor (ADR-0022, build/Net472TestFloor.props), where it does not exist. The non-generic overload " + - "is the only spelling that compiles on both legs; the reason is restated at the call site so a reader meets " + - "it there too.")] - public void EnumDrawsDeclaredMembers() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - OrderStatus value = Any.Enum().Generate(); - seen.Add(value); - // The non-generic overload on purpose: this suite also runs on the .NET Framework 4.7.2 support - // floor (ADR-0022, build/Net472TestFloor.props), where Enum.IsDefined(TEnum) does not exist. - // CA2263 suggests the generic one and is right on net10.0 only, so it is answered here rather than - // taken — the same downlevel trap as string.Contains(char) elsewhere in this repository. - Check.That(System.Enum.IsDefined(typeof(OrderStatus), value)).IsTrue(); - } - Check.That(seen.Count).IsEqualTo(3); - } - - [Fact(DisplayName = "Enum: OneOf restricts, Except removes, exhausting the pool conflicts.")] - public void EnumSets() { - for (int i = 0; i < SampleCount; i++) { - OrderStatus restricted = Any.Enum().OneOf(OrderStatus.Draft, OrderStatus.Validated).Generate(); - Check.That(restricted == OrderStatus.Draft || restricted == OrderStatus.Validated).IsTrue(); - Check.That(Any.Enum().Except(OrderStatus.Cancelled).Generate()).IsNotEqualTo(OrderStatus.Cancelled); - Check.That(Any.Enum().OneOf(OrderStatus.Draft, OrderStatus.Validated).DifferentFrom(OrderStatus.Draft).Generate()).IsEqualTo(OrderStatus.Validated); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Enum().Except(OrderStatus.Draft, OrderStatus.Validated, OrderStatus.Cancelled)); - Check.That(conflict.Message).Contains("Except("); - } - - [Fact(DisplayName = "Enum: OneOf rejects undeclared numeric values — the declared-members-only contract holds.")] - public void EnumOneOfRejectsUndeclaredValues() { - ArgumentException rejected = Assert.Throws( - () => Any.Enum().OneOf((OrderStatus)42)); - Check.That(rejected.Message).Contains("42"); - Check.That(rejected.Message).Contains("OrderStatus"); - - Check.ThatCode(() => Any.Enum().OneOf(OrderStatus.Draft, (OrderStatus)42)).Throws(); - } - - [Fact(DisplayName = "Char: the default pool is ASCII letters and digits; families narrow it.")] - public void CharPools() { - for (int i = 0; i < SampleCount; i++) { - char value = Any.Char().Generate(); - Check.That(value is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9').IsTrue(); - Check.That(Any.Char().Numeric().Generate() is >= '0' and <= '9').IsTrue(); - Check.That(Any.Char().Alpha().Generate() is >= 'A' and <= 'Z' or >= 'a' and <= 'z').IsTrue(); - Check.That(Any.Char().LowerCase().Generate() is >= 'A' and <= 'Z').IsFalse(); - Check.That(Any.Char().Alpha().UpperCase().Generate() is >= 'A' and <= 'Z').IsTrue(); - } - } - - [Fact(DisplayName = "Char: OneOf restricts, exclusions apply, and contradictions conflict.")] - public void CharSets() { - for (int i = 0; i < SampleCount; i++) { - char value = Any.Char().OneOf('a', 'b').Generate(); - Check.That(value == 'a' || value == 'b').IsTrue(); - Check.That(Any.Char().OneOf('a', 'b').DifferentFrom('a').Generate()).IsEqualTo('b'); - } - - Check.ThatCode(() => Any.Char().Numeric().Alpha()).Throws(); - Check.ThatCode(() => Any.Char().OneOf('a').Except('a')).Throws(); - Check.ThatCode(() => Any.Char().OneOf('a').Numeric()).Throws(); - } - -} diff --git a/JustDummies.UnitTests/AnySignedIntegerTests.cs b/JustDummies.UnitTests/AnySignedIntegerTests.cs deleted file mode 100644 index 7e876268..00000000 --- a/JustDummies.UnitTests/AnySignedIntegerTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnySignedIntegerTests { - - private const int SampleCount = 200; - - [Fact(DisplayName = "SByte: Positive and Negative are strict, and contradict each other.")] - public void SByteSignConstraints() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.SByte().Positive().Generate()).IsStrictlyGreaterThan((sbyte)0); - Check.That(Any.SByte().Negative().Generate()).IsStrictlyLessThan((sbyte)0); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.SByte().Positive().Negative()); - Check.That(conflict.Message).Contains("Negative()"); - Check.That(conflict.Message).Contains("Positive()"); - } - - [Fact(DisplayName = "SByte: Between is inclusive and reaches both bounds; extremes are generable.")] - public void SByteBounds() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.SByte().Between(-1, 1).Generate()); } - Check.That(seen.Contains(-1)).IsTrue(); - Check.That(seen.Contains(1)).IsTrue(); - - Check.That(Any.SByte().LessThanOrEqualTo(sbyte.MinValue).Generate()).IsEqualTo(sbyte.MinValue); - Check.That(Any.SByte().GreaterThanOrEqualTo(sbyte.MaxValue).Generate()).IsEqualTo(sbyte.MaxValue); - Check.ThatCode(() => Any.SByte().GreaterThan(sbyte.MaxValue)).Throws(); - } - - [Fact(DisplayName = "Int16: Zero pins, NonZero excludes, and the pair conflicts.")] - public void Int16ZeroFamily() { - Check.That(Any.Int16().Zero().Generate()).IsEqualTo((short)0); - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Int16().Between(-1, 1).NonZero().Generate()).IsNotEqualTo((short)0); - } - Check.ThatCode(() => Any.Int16().Zero().NonZero()).Throws(); - } - - [Fact(DisplayName = "Int16: GreaterThan and LessThan are exclusive bounds.")] - public void Int16ExclusiveBounds() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Int16().GreaterThan(10).LessThanOrEqualTo(12).Generate()).IsGreaterOrEqualThan((short)11); - Check.That(Any.Int16().LessThan(10).GreaterThanOrEqualTo(8).Generate()).IsLessOrEqualThan((short)9); - } - } - - [Fact(DisplayName = "Int64: full-range generation works and crossed bounds conflict naming both sides.")] - public void Int64RangeAndConflicts() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Int64().Generate()); } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Int64().GreaterThan(100L).LessThan(10L)); - Check.That(conflict.Message).Contains("LessThan(10)"); - Check.That(conflict.Message).Contains("GreaterThan(100)"); - } - - [Fact(DisplayName = "Int64: OneOf stays within the supplied values and Except never yields an excluded one.")] - public void Int64OneOfAndExcept() { - long[] allowed = [1L, 5L, 9L]; - for (int i = 0; i < SampleCount; i++) { - Check.That(allowed.Contains(Any.Int64().OneOf(allowed).Generate())).IsTrue(); - Check.That(Any.Int64().Between(1L, 3L).Except(2L).Generate()).IsNotEqualTo(2L); - Check.That(Any.Int64().Between(7L, 8L).DifferentFrom(7L).Generate()).IsEqualTo(8L); - } - } - - [Fact(DisplayName = "Int64: extremes are generable and arguments are validated.")] - public void Int64ExtremesAndArguments() { - Check.That(Any.Int64().LessThanOrEqualTo(long.MinValue).Generate()).IsEqualTo(long.MinValue); - Check.That(Any.Int64().GreaterThanOrEqualTo(long.MaxValue).Generate()).IsEqualTo(long.MaxValue); - Check.ThatCode(() => Any.Int64().GreaterThan(long.MaxValue)).Throws(); - Check.ThatCode(() => Any.Int64().Between(10L, 1L)).Throws(); - Check.ThatCode(() => Any.Int64().OneOf()).Throws(); - Check.ThatCode(() => Any.Int64().Except(null!)).Throws(); - } - - [Fact(DisplayName = "Every signed integer generator materializes its own value type through Generate().")] - public void MaterializesEachValueType() { - sbyte small = Any.SByte().Positive().Generate(); - short mid = Any.Int16().Negative().Generate(); - long wide = Any.Int64().Between(1L, 10L).Generate(); - - Check.That((int)small).IsStrictlyGreaterThan(0); - Check.That((int)mid).IsStrictlyLessThan(0); - Check.That(wide).IsGreaterOrEqualThan(1L); - } - -} diff --git a/JustDummies.UnitTests/AnyStringTests.cs b/JustDummies.UnitTests/AnyStringTests.cs deleted file mode 100644 index 14f841c7..00000000 --- a/JustDummies.UnitTests/AnyStringTests.cs +++ /dev/null @@ -1,508 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -[TestSubject(typeof(AnyString))] -public sealed class AnyStringTests { - - private const int SampleCount = 200; - - #region Statics members declarations - - private static IEnumerable Samples(IAny generator) { - for (int i = 0; i < SampleCount; i++) { - yield return generator.Generate(); - } - } - - #endregion - - [Fact(DisplayName = "An unconstrained String yields 0 to 16 ASCII letters and digits.")] - public void UnconstrainedYieldsShortAlphanumeric() { - foreach (string value in Samples(Any.String())) { - Check.That(value.Length).IsLessOrEqualThan(16); - Check.That(value.All(char.IsLetterOrDigit)).IsTrue(); - } - } - - [Fact(DisplayName = "NonEmpty yields at least one character.")] - public void NonEmptyHasAtLeastOneCharacter() { - foreach (string value in Samples(Any.String().NonEmpty())) { - Check.That(value.Length).IsStrictlyGreaterThan(0); - } - } - - [Fact(DisplayName = "WithLength yields exactly that many characters.")] - public void WithLengthIsExact() { - foreach (string value in Samples(Any.String().WithLength(10))) { - Check.That(value.Length).IsEqualTo(10); - } - } - - [Fact(DisplayName = "WithLength(0) yields the empty string.")] - public void WithLengthZeroIsEmpty() { - Check.That(Any.String().WithLength(0).Generate()).IsEqualTo(string.Empty); - } - - [Fact(DisplayName = "WithMinLength and WithMaxLength bound the length inclusively.")] - public void MinAndMaxLengthAreInclusiveBounds() { - foreach (string value in Samples(Any.String().WithMinLength(3).WithMaxLength(5))) { - Check.That(value.Length).IsGreaterOrEqualThan(3); - Check.That(value.Length).IsLessOrEqualThan(5); - } - } - - [Fact(DisplayName = "WithLengthBetween bounds the length inclusively and reaches its bounds.")] - public void WithLengthBetweenIsInclusive() { - HashSet lengths = []; - foreach (string value in Samples(Any.String().WithLengthBetween(2, 4))) { - lengths.Add(value.Length); - Check.That(value.Length).IsGreaterOrEqualThan(2); - Check.That(value.Length).IsLessOrEqualThan(4); - } - - Check.That(lengths.Contains(2)).IsTrue(); - Check.That(lengths.Contains(4)).IsTrue(); - } - - [Fact(DisplayName = "StartingWith anchors the prefix.")] - public void StartingWithAnchorsThePrefix() { - foreach (string value in Samples(Any.String().StartingWith("ORD-"))) { - Check.That(value).StartsWith("ORD-"); - } - } - - [Fact(DisplayName = "EndingWith anchors the suffix.")] - public void EndingWithAnchorsTheSuffix() { - foreach (string value in Samples(Any.String().EndingWith("-FR"))) { - Check.That(value).EndsWith("-FR"); - } - } - - [Fact(DisplayName = "Containing embeds the value.")] - public void ContainingEmbedsTheValue() { - foreach (string value in Samples(Any.String().Containing("ABC"))) { - Check.That(value).Contains("ABC"); - } - } - - [Fact(DisplayName = "Prefix, contained value, suffix and exact length hold together.")] - public void FragmentsAndExactLengthHoldTogether() { - foreach (string value in Samples(Any.String().StartingWith("ORD-").Containing("X").EndingWith("-FR").WithLength(12))) { - Check.That(value.Length).IsEqualTo(12); - Check.That(value).StartsWith("ORD-"); - Check.That(value).Contains("X"); - Check.That(value).EndsWith("-FR"); - } - } - - [Fact(DisplayName = "A fragment-only budget is generable: length equals the fragment sum.")] - public void FragmentsExactlyFillingTheLengthAreGenerable() { - Check.That(Any.String().StartingWith("AB").EndingWith("CD").WithLength(4).Generate()).IsEqualTo("ABCD"); - } - - [Fact(DisplayName = "Alpha yields ASCII letters only.")] - public void AlphaYieldsLettersOnly() { - foreach (string value in Samples(Any.String().Alpha().NonEmpty())) { - Check.That(value.All(character => character is >= 'A' and <= 'Z' or >= 'a' and <= 'z')).IsTrue(); - } - } - - [Fact(DisplayName = "Numeric yields ASCII digits only.")] - public void NumericYieldsDigitsOnly() { - foreach (string value in Samples(Any.String().Numeric().NonEmpty())) { - Check.That(value.All(character => character is >= '0' and <= '9')).IsTrue(); - } - } - - [Fact(DisplayName = "AlphaNumeric yields ASCII letters and digits only.")] - public void AlphaNumericYieldsLettersAndDigitsOnly() { - foreach (string value in Samples(Any.String().AlphaNumeric().NonEmpty())) { - Check.That(value.All(character => character is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9')).IsTrue(); - } - } - - [Fact(DisplayName = "LowerCase yields no uppercase letter; digits stay allowed.")] - public void LowerCaseForbidsUppercaseLetters() { - foreach (string value in Samples(Any.String().LowerCase().NonEmpty())) { - Check.That(value.Any(character => character is >= 'A' and <= 'Z')).IsFalse(); - } - } - - [Fact(DisplayName = "UpperCase yields no lowercase letter; fragments keep their own characters.")] - public void UpperCaseForbidsLowercaseLetters() { - foreach (string value in Samples(Any.String().UpperCase().StartingWith("ORD-").NonEmpty())) { - Check.That(value.Any(character => character is >= 'a' and <= 'z')).IsFalse(); - Check.That(value).StartsWith("ORD-"); - } - } - - [Fact(DisplayName = "A second WithLength conflicts: the exact length is declared once.")] - public void SecondWithLengthConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithLength(3).WithLength(5)); - - Check.That(conflict.Message).Contains("WithLength(5)"); - Check.That(conflict.Message).Contains("WithLength(3)"); - } - - [Fact(DisplayName = "A prefix longer than the exact length conflicts, naming both sides.")] - public void PrefixLongerThanExactLengthConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithLength(3).StartingWith("ORD-")); - - Check.That(conflict.Message).Contains("StartingWith(\"ORD-\")"); - Check.That(conflict.Message).Contains("WithLength(3)"); - Check.That(conflict.Message).Contains("4"); - } - - [Fact(DisplayName = "An exact length shorter than an already declared prefix conflicts, naming both sides.")] - public void ExactLengthShorterThanPrefixConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().StartingWith("ORD-").WithLength(3)); - - Check.That(conflict.Message).Contains("WithLength(3)"); - Check.That(conflict.Message).Contains("ORD-"); - Check.That(conflict.Message).Contains("4"); - } - - [Fact(DisplayName = "A numeric-only string cannot start with a non-numeric prefix.")] - public void NumericPrefixMismatchConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().Numeric().StartingWith("ORD-")); - - Check.That(conflict.Message).Contains("StartingWith(\"ORD-\")"); - Check.That(conflict.Message).Contains("Numeric()"); - } - - // A contained value is the one constraint the specification records per occurrence rather than in a named slot, - // so it is also the one whose name a message can lose without any other assertion noticing. - [Fact(DisplayName = "An allow-list that offers no value carrying the fragment names Containing as the culprit.")] - public void AllowListRejectedByAFragmentNamesContaining() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().Containing("ABC").OneOf("x", "y")); - - Check.That(conflict.Message).Contains("Containing(\"ABC\")"); - } - - [Fact(DisplayName = "Declaring the charset after an incompatible prefix conflicts too: order does not matter.")] - public void CharsetAfterIncompatiblePrefixConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().StartingWith("ORD-").Numeric()); - - Check.That(conflict.Message).Contains("Numeric()"); - Check.That(conflict.Message).Contains("ORD-"); - } - - [Fact(DisplayName = "A minimum length above the maximum conflicts.")] - public void MinAboveMaxConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithMinLength(10).WithMaxLength(3)); - - Check.That(conflict.Message).Contains("WithMaxLength(3)"); - Check.That(conflict.Message).Contains("WithMinLength(10)"); - } - - [Fact(DisplayName = "An exact length above an already declared maximum conflicts.")] - public void ExactAboveMaxConflicts() { - Check.ThatCode(() => Any.String().WithMaxLength(3).WithLength(5)).Throws(); - } - - [Fact(DisplayName = "LowerCase then UpperCase conflicts: one casing per generator.")] - public void LowerThenUpperCaseConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().LowerCase().UpperCase()); - - Check.That(conflict.Message).Contains("UpperCase()"); - Check.That(conflict.Message).Contains("LowerCase()"); - } - - [Fact(DisplayName = "Alpha then Numeric conflicts: one character family per generator.")] - public void AlphaThenNumericConflicts() { - Check.ThatCode(() => Any.String().Alpha().Numeric()).Throws(); - } - - [Fact(DisplayName = "A lowercase-only string cannot anchor an uppercase prefix.")] - public void LowerCaseUppercasePrefixConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().LowerCase().StartingWith("ORD-")); - - Check.That(conflict.Message).Contains("StartingWith(\"ORD-\")"); - Check.That(conflict.Message).Contains("LowerCase()"); - } - - [Fact(DisplayName = "A second StartingWith conflicts: the prefix is declared once.")] - public void SecondStartingWithConflicts() { - Check.ThatCode(() => Any.String().StartingWith("A").StartingWith("B")).Throws(); - } - - [Fact(DisplayName = "Fragments exceeding the maximum length conflict.")] - public void FragmentsExceedingMaxLengthConflict() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithMaxLength(5).StartingWith("ORD-").EndingWith("-FR")); - - Check.That(conflict.Message).Contains("EndingWith(\"-FR\")"); - Check.That(conflict.Message).Contains("7"); - } - - [Fact(DisplayName = "Length arguments are validated as arguments, not as conflicts.")] - public void LengthArgumentsAreValidated() { - Check.ThatCode(() => Any.String().WithLength(-1)).Throws(); - Check.ThatCode(() => Any.String().WithMinLength(-1)).Throws(); - Check.ThatCode(() => Any.String().WithMaxLength(-1)).Throws(); - Check.ThatCode(() => Any.String().WithLengthBetween(5, 3)).Throws(); - } - - [Fact(DisplayName = "A produced length is refused above the ceiling; the bound just below it is accepted.")] - public void ProducedLengthsAreCeilinged() { - // ADR-0050. The two coordinates that matter are the ceiling itself and the first value past it: a guard - // written with the wrong comparison passes every other length and fails exactly here. - Check.ThatCode(() => Any.String().WithLength(1_000_001)).Throws(); - Check.ThatCode(() => Any.String().WithMinLength(1_000_001)).Throws(); - Check.ThatCode(() => Any.String().WithLengthBetween(1_000_001, 2_000_000)).Throws(); - - Check.ThatCode(() => Any.String().WithLength(1_000_000)).DoesNotThrow(); - Check.ThatCode(() => Any.String().WithMinLength(1_000_000)).DoesNotThrow(); - } - - [Fact(DisplayName = "An enormous length names the caller's parameter instead of leaking an internal one.")] - public void AnEnormousLengthNamesTheCallersParameter() { - // Regression: WithLength(int.MaxValue) used to surface an ArgumentOutOfRangeException from inside the draw, - // naming System.Random's own 'maxValue' parameter after an arithmetic overflow — a message about internals - // for a mistake the caller made in the Arrange. - ArgumentOutOfRangeException error = Assert.Throws(() => Any.String().WithLength(int.MaxValue)); - - Check.That(error.ParamName).IsEqualTo("length"); - } - - [Fact(DisplayName = "A maximum accepts any non-negative length and still yields a small string.")] - public void AMaximumIsACapNotASizeHint() { - // Regression: WithMaxLength(int.MaxValue) used to return a string of about 130 MB, because a declared - // maximum replaced the default spread instead of composing with it. A maximum is a permission, so it is - // never ceilinged — and it never enlarges the draw either. - Check.That(Any.String().WithMaxLength(int.MaxValue).Generate().Length).IsStrictlyLessThan(17); - Check.That(Any.String().WithMaxLength(4_000_000).Generate().Length).IsStrictlyLessThan(17); - } - - [Fact(DisplayName = "Fragment arguments are validated as arguments, not as conflicts.")] - public void FragmentArgumentsAreValidated() { - Check.ThatCode(() => Any.String().StartingWith(null!)).Throws(); - Check.ThatCode(() => Any.String().StartingWith("")).Throws(); - Check.ThatCode(() => Any.String().EndingWith(null!)).Throws(); - Check.ThatCode(() => Any.String().Containing("")).Throws(); - } - - [Fact(DisplayName = "DifferentFrom never returns the excluded value.")] - public void DifferentFromNeverReturnsTheExcludedValue() { - foreach (string value in Samples(Any.String().WithLength(1).Alpha().DifferentFrom("A"))) { - Check.That(value).IsNotEqualTo("A"); - } - } - - [Fact(DisplayName = "Except excludes each listed value.")] - public void ExceptExcludesEachListedValue() { - string[] forbidden = { "A", "B", "C" }; - foreach (string value in Samples(Any.String().WithLength(1).Alpha().Except("A", "B", "C"))) { - Check.That(forbidden.Contains(value)).IsFalse(); - } - } - - [Fact(DisplayName = "An exclusion preserves the declared shape: only shape-matching survivors are drawn.")] - public void ExclusionPreservesTheDeclaredShape() { - foreach (string value in Samples(Any.String().StartingWith("ORD-").WithLength(5).DifferentFrom("ORD-A"))) { - Check.That(value).StartsWith("ORD-"); - Check.That(value.Length).IsEqualTo(5); - Check.That(value).IsNotEqualTo("ORD-A"); - } - } - - [Fact(DisplayName = "Exclusions accumulate across several declarations.")] - public void ExclusionsAccumulateAcrossDeclarations() { - foreach (string value in Samples(Any.String().WithLength(1).Alpha().Except("A", "B").DifferentFrom("C"))) { - Check.That(value is "A" or "B" or "C").IsFalse(); - } - } - - [Fact(DisplayName = "An over-tight exclusion fails at generation with a bounded, seed-bearing AnyGenerationException.")] - public void OverTightExclusionThrowsSeedBearingGenerationException() { - string[] everyLetter = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".Select(letter => letter.ToString()).ToArray(); - - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260721).String().WithLength(1).Alpha().Except(everyLetter).Generate()); - - Check.That(error.Seed).IsEqualTo(20260721); - Check.That(error.Message).Contains("Any.WithSeed(20260721)"); - } - - [Fact(DisplayName = "An exhausted exclusion budget reports the budget, never a claim that the shape is unsatisfiable.")] - public void ExhaustedExclusionBudgetDoesNotClaimUnsatisfiability() { - // The redraw is bounded at 10,000 draws, and the message concluded from an exhausted budget that "the - // exclusions leave the shape unsatisfiable". That does not follow: the failure probability is - // (excluded / domain) ^ 10000, so a shape keeping one value free in a few hundred thousand exhausts the - // budget most of the time and is still satisfiable. Here the domain really is empty — that is what makes the - // case deterministic — but the message must state what was established, the budget, not a proof it never ran. - string[] everyLetter = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".Select(letter => letter.ToString()).ToArray(); - - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260721).String().WithLength(1).Alpha().Except(everyLetter).Generate()); - - Check.That(error.Message).Contains("10000 draws"); - Check.That(error.Message).Contains("exhausted budget rather than a proof"); - Check.That(error.Message).Not.Contains("so the exclusions leave the shape unsatisfiable"); - // The actionable half stays: the caller still learns what to change. - Check.That(error.Message).Contains("Loosen the exclusions or widen the shape"); - } - - [Fact(DisplayName = "A seeded exclusion is reproducible: the same seed yields the same value.")] - public void SeededExclusionIsReproducible() { - string first = Any.WithSeed(4242).String().NonEmpty().Alpha().DifferentFrom("Q").Generate(); - string second = Any.WithSeed(4242).String().NonEmpty().Alpha().DifferentFrom("Q").Generate(); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "An exclusion composes with OneOf, removing the excluded value from the set.")] - public void AnExclusionComposesWithOneOf() { - Check.That(Any.String().DifferentFrom("x").OneOf("a", "x").Generate()).IsEqualTo("a"); - } - - [Fact(DisplayName = "Exclusion arguments are validated as arguments, not as conflicts.")] - public void ExclusionArgumentsAreValidated() { - Check.ThatCode(() => Any.String().DifferentFrom(null!)).Throws(); - Check.ThatCode(() => Any.String().Except(null!)).Throws(); - Check.ThatCode(() => Any.String().Except()).Throws(); - Check.ThatCode(() => Any.String().Except("a", null!)).Throws(); - } - - [Fact(DisplayName = "WithChars draws every character from the supplied pool.")] - public void WithCharsDrawsFromThePool() { - const string pool = "0123456789ABCDEF"; - foreach (string value in Samples(Any.String().WithChars(pool).NonEmpty())) { - Check.That(value.All(character => pool.Contains(character))).IsTrue(); - } - } - - [Fact(DisplayName = "WithChars reaches every character in the pool.")] - public void WithCharsReachesEveryCharacter() { - const string pool = "ACGT"; - HashSet seen = []; - foreach (string value in Samples(Any.String().WithChars(pool).WithLength(8))) { - foreach (char character in value) { seen.Add(character); } - } - - Check.That(pool.All(character => seen.Contains(character))).IsTrue(); - } - - [Fact(DisplayName = "WithChars reaches non-ASCII characters a named charset cannot.")] - public void WithCharsReachesNonAscii() { - const string pool = "àâäéèêëîïôùûüç"; - foreach (string value in Samples(Any.String().WithChars(pool).NonEmpty())) { - Check.That(value.All(character => pool.Contains(character))).IsTrue(); - } - } - - [Fact(DisplayName = "WithChars honours an exact length.")] - public void WithCharsHonoursExactLength() { - foreach (string value in Samples(Any.String().WithChars("xyz").WithLength(7))) { - Check.That(value.Length).IsEqualTo(7); - } - } - - [Fact(DisplayName = "WithChars collapses duplicate characters in the pool.")] - public void WithCharsCollapsesDuplicates() { - foreach (string value in Samples(Any.String().WithChars("aaabbb").WithLength(4))) { - Check.That(value.All(character => character is 'a' or 'b')).IsTrue(); - } - } - - [Fact(DisplayName = "WithChars combines with an exclusion over its own pool.")] - public void WithCharsCombinesWithExclusion() { - foreach (string value in Samples(Any.String().WithChars("ab").WithLength(1).DifferentFrom("a"))) { - Check.That(value).IsEqualTo("b"); - } - } - - [Fact(DisplayName = "A seeded WithChars draw is reproducible: the same seed yields the same value.")] - public void SeededWithCharsIsReproducible() { - string first = Any.WithSeed(4242).String().WithChars("αβγδεζ").WithLength(5).Generate(); - string second = Any.WithSeed(4242).String().WithChars("αβγδεζ").WithLength(5).Generate(); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "WithChars then a named charset conflicts: one character family per generator.")] - public void WithCharsThenNamedCharsetConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithChars("abc").Numeric()); - - Check.That(conflict.Message).Contains("Numeric()"); - Check.That(conflict.Message).Contains("WithChars(\"abc\")"); - } - - [Fact(DisplayName = "A named charset then WithChars conflicts: order does not matter.")] - public void NamedCharsetThenWithCharsConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().Alpha().WithChars("абвгд")); - - Check.That(conflict.Message).Contains("WithChars(\"абвгд\")"); - Check.That(conflict.Message).Contains("Alpha()"); - } - - [Fact(DisplayName = "WithChars then a casing conflicts: the pool is the whole character definition.")] - public void WithCharsThenCasingConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithChars("abc").LowerCase()); - - Check.That(conflict.Message).Contains("LowerCase()"); - Check.That(conflict.Message).Contains("WithChars(\"abc\")"); - } - - [Fact(DisplayName = "A casing then WithChars conflicts: order does not matter.")] - public void CasingThenWithCharsConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().UpperCase().WithChars("abc")); - - Check.That(conflict.Message).Contains("WithChars(\"abc\")"); - Check.That(conflict.Message).Contains("UpperCase()"); - } - - [Fact(DisplayName = "A WithChars pool cannot anchor a prefix with an outside character.")] - public void WithCharsPrefixOutsidePoolConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithChars("0123456789").StartingWith("ID-")); - - Check.That(conflict.Message).Contains("StartingWith(\"ID-\")"); - Check.That(conflict.Message).Contains("WithChars(\"0123456789\")"); - Check.That(conflict.Message).Contains("'I'"); - } - - [Fact(DisplayName = "Declaring WithChars after an incompatible fragment conflicts too: order does not matter.")] - public void WithCharsAfterIncompatibleFragmentConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().StartingWith("ID-").WithChars("0123456789")); - - Check.That(conflict.Message).Contains("WithChars(\"0123456789\")"); - Check.That(conflict.Message).Contains("ID-"); - Check.That(conflict.Message).Contains("'I'"); - } - - [Fact(DisplayName = "WithChars arguments are validated as arguments, not as conflicts.")] - public void WithCharsArgumentsAreValidated() { - Check.ThatCode(() => Any.String().WithChars(null!)).Throws(); - Check.ThatCode(() => Any.String().WithChars("")).Throws(); - } - - [Fact(DisplayName = "WithChars rejects a pool with an astral code point and points to OneOf.")] - public void WithCharsRejectsAstralPool() { - ArgumentException error = Assert.Throws(() => Any.String().WithChars("😀🎉")); - - Check.That(error.Message).Contains("OneOf"); - } - -} diff --git a/JustDummies.UnitTests/AnyStringValueSetTests.cs b/JustDummies.UnitTests/AnyStringValueSetTests.cs deleted file mode 100644 index 5293c6f7..00000000 --- a/JustDummies.UnitTests/AnyStringValueSetTests.cs +++ /dev/null @@ -1,369 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The value-set half of : OneOf(...) and how it composes with every other string -/// constraint. Split from , which owns the constructive shape. -/// -[TestSubject(typeof(AnyString))] -public sealed class AnyStringValueSetTests { - - private const int SampleCount = 200; - - #region Statics members declarations - - private static IEnumerable Samples(IAny generator) { - for (int i = 0; i < SampleCount; i++) { - yield return generator.Generate(); - } - } - - /// - /// Asserts that has been narrowed to exactly one value — every draw, not one. - /// A constraint that narrows a two-value set is only proven by the draws it makes impossible: checking a - /// single draw would still pass half the time with the narrowing gone. - /// - private static void NarrowsTo(string expected, IAny generator) { - foreach (string value in Samples(generator)) { - Check.That(value).IsEqualTo(expected); - } - } - - #endregion - - [Fact(DisplayName = "OneOf draws only the supplied values.")] - public void DrawsOnlyTheSuppliedValues() { - string[] allowed = ["Apple", "Microsoft", "Google"]; - foreach (string value in Samples(Any.String().OneOf(allowed))) { - Check.That(allowed.Contains(value)).IsTrue(); - } - } - - [Fact(DisplayName = "OneOf eventually reaches every supplied value.")] - public void ReachesEverySuppliedValue() { - HashSet seen = [.. Samples(Any.String().OneOf("EUR", "USD", "GBP"))]; - - Check.That(seen).Contains("EUR", "USD", "GBP"); - } - - [Fact(DisplayName = "A single value pins the generated string.")] - public void SingleValueIsPinned() { - foreach (string value in Samples(Any.String().OneOf("SOLE"))) { - Check.That(value).IsEqualTo("SOLE"); - } - } - - [Fact(DisplayName = "OneOf varies from draw to draw when the set holds more than one value.")] - public void VariesAcrossDraws() { - HashSet seen = [.. Samples(Any.String().OneOf("a", "b", "c", "d"))]; - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "Duplicate values are collapsed: both distinct values are still drawn, nothing else.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Constraints", "JD025:The same value is listed twice in a pool", - Justification = - "The duplicate IS the subject. This pins the collapsing JD025 reports: without it there is nothing to collapse and the test " + - "asserts nothing.")] - public void DuplicatesAreCollapsed() { - HashSet seen = [.. Samples(Any.String().OneOf("a", "a", "b"))]; - - Check.That(seen).IsOnlyMadeOf("a", "b"); - Check.That(seen).Contains("a", "b"); - } - - [Fact(DisplayName = "An empty string is a legitimate member of the set.")] - public void EmptyStringIsAllowed() { - Check.That(Any.String().OneOf("").Generate()).IsEqualTo(string.Empty); - } - - [Fact(DisplayName = "OneOf is reproducible under a seed.")] - public void ReproducibleUnderASeed() { - string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).String().OneOf("a", "b", "c", "d").Generate())); - string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).String().OneOf("a", "b", "c", "d").Generate())); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "OneOf composes into a value object through As.")] - public void ComposesThroughAs() { - IAny generator = Any.String().OneOf("ORD-12345678", "ORD-87654321").As(OrderReference.Create); - - for (int i = 0; i < SampleCount; i++) { - OrderReference reference = generator.Generate(); - Check.That(reference.Value).StartsWith("ORD-"); - Check.That(reference.Value.Length).IsEqualTo(12); - } - } - - [Fact(DisplayName = "OrNull makes the value set generator null about half the time, otherwise a member of the set.")] - public void OrNullIsSometimesNull() { - IAny generator = Any.WithSeed(20260721).String().OneOf("a", "b").OrNull(); - - List values = []; - for (int i = 0; i < SampleCount; i++) { - values.Add(generator.Generate()); - } - - Check.That(values.Any(value => value is null)).IsTrue(); - Check.That(values.Where(value => value is not null)).IsOnlyMadeOf("a", "b"); - } - - [Fact(DisplayName = "A distinct set over OneOf is gated by the set's cardinality, both ways.")] - public void CardinalityGatesDistinctCollections() { - // Two distinct values cannot fill a set of three: caught eagerly, like any cardinality conflict. - Check.ThatCode(() => Any.SetOf(Any.String().OneOf("a", "b")).WithCount(3)).Throws(); - - // Within the domain it fills the set with the requested distinct values. - HashSet set = Any.SetOf(Any.String().OneOf("a", "b", "c")).WithCount(3).Generate(); - Check.That(set.Count).IsEqualTo(3); - Check.That(set).IsOnlyMadeOf("a", "b", "c"); - } - - [Fact(DisplayName = "The advertised cardinality is the surviving pool, not the declared one.")] - public void CardinalityCountsTheSurvivingPool() { - // Only "abc" and "xyz" are three characters long, so the domain a distinct set may draw from holds two - // values — the shape narrowed the pool before the collection ever gated on it. - Check.ThatCode(() => Any.SetOf(Any.String().OneOf("abc", "de", "xyz").WithLength(3)).WithCount(3)) - .Throws(); - - HashSet set = Any.SetOf(Any.String().OneOf("abc", "de", "xyz").WithLength(3)).WithCount(2).Generate(); - Check.That(set).IsOnlyMadeOf("abc", "xyz"); - } - - [Fact(DisplayName = "A pinned value is counted as extending the set only when the set could not have drawn it.")] - public void APinnedValueExtendsTheDomainOnlyWhenItIsOutside() { - // "a" is one of the two values the generator can draw, so pinning it fills a slot the generator would have - // filled anyway: the domain is still two, and a set of three cannot be filled. - Check.ThatCode(() => Any.SetOf(Any.String().OneOf("a", "b")).Containing("a").WithCount(3)) - .Throws(); - - // "z" is a value the generator could never draw, so it occupies its own slot and the set of three fits. - HashSet set = Any.SetOf(Any.String().OneOf("a", "b")).Containing("z").WithCount(3).Generate(); - Check.That(set).IsOnlyMadeOf("a", "b", "z"); - } - - [Fact(DisplayName = "A distinct collection pinning a null is answered, not thrown at.")] - public void PinningANullIsAnsweredNotThrownAt() { - // Containing(null) is legal, if unlikely; asking a value set whether it could produce that null is a - // question with the answer "no" — a value set rejects a null element — not a boundary violation. The pool - // generator answers it that way, and so must this one. - Check.ThatCode(() => Any.SetOf(Any.String().OneOf("a", "b")).Containing(null!).WithCount(2)).DoesNotThrow(); - } - - [Fact(DisplayName = "A shape constraint narrows the value set instead of conflicting with it.")] - public void AShapeConstraintNarrowsTheSet() { - // The example the composable form exists for: "abc" satisfies both, so both hold at once. - foreach (string value in Samples(Any.String().OneOf("abc", "de").WithLength(3))) { - Check.That(value).IsEqualTo("abc"); - } - } - - [Fact(DisplayName = "Every string constraint composes with a value set, narrowing it to the values that satisfy it.")] - public void EveryConstraintNarrowsTheSet() { - // Each case pins the whole surviving domain, not one draw: a single draw from a two-value pool would still - // land on the expected value about half the time with the constraint's filter removed, which is a test that - // exercises the filter without asserting it. - NarrowsTo("ORD-1", Any.String().OneOf("ORD-1", "INV-1").StartingWith("ORD-")); - NarrowsTo("a-FR", Any.String().OneOf("a-FR", "a-BE").EndingWith("-FR")); - NarrowsTo("xxKEYxx", Any.String().OneOf("xxKEYxx", "nope").Containing("KEY")); - NarrowsTo("123", Any.String().OneOf("abc", "123").Numeric()); - NarrowsTo("abc", Any.String().OneOf("abc", "123").Alpha()); - NarrowsTo("abc", Any.String().OneOf("abc", "AB-1").AlphaNumeric()); - NarrowsTo("abc", Any.String().OneOf("abc", "ABC").LowerCase()); - NarrowsTo("ABC", Any.String().OneOf("abc", "ABC").UpperCase()); - NarrowsTo("aab", Any.String().OneOf("aab", "xyz").WithChars("ab")); - NarrowsTo("abc", Any.String().OneOf("", "abc").NonEmpty()); - NarrowsTo("ab", Any.String().OneOf("ab", "abcdef").WithMaxLength(3)); - NarrowsTo("abcdef", Any.String().OneOf("ab", "abcdef").WithMinLength(4)); - NarrowsTo("abcdef", Any.String().OneOf("ab", "abcdef").WithLengthBetween(4, 8)); - NarrowsTo("keep", Any.String().OneOf("keep", "drop").DifferentFrom("drop")); - NarrowsTo("keep", Any.String().OneOf("keep", "drop", "gone").Except("drop", "gone")); - } - - [Fact(DisplayName = "A pooled value satisfies Containing on its own terms, not through the constructive layout.")] - public void ContainedValuesAreCheckedNotLaidOut() { - // The constructive path lays fragments side by side, so it could never build "aba" from "ab" and "ba" — - // it would need four characters. Nothing is laid out here: the value was supplied and simply contains both. - Check.That(Any.String().OneOf("aba").Containing("ab").Containing("ba").Generate()).IsEqualTo("aba"); - } - - [Fact(DisplayName = "A value set and a shape reach the same verdict whichever order they are declared in.")] - public void OrderOfDeclarationDoesNotChangeTheVerdict() { - Check.That(Any.String().WithLength(3).OneOf("abc", "de").Generate()).IsEqualTo("abc"); - Check.That(Any.String().OneOf("abc", "de").WithLength(3).Generate()).IsEqualTo("abc"); - - Check.ThatCode(() => Any.String().WithLength(9).OneOf("abc", "de")).Throws(); - Check.ThatCode(() => Any.String().OneOf("abc", "de").WithLength(9)).Throws(); - } - - [Fact(DisplayName = "A constraint no supplied value satisfies names the value set and itself, and nothing else.")] - public void AnEmptyingConstraintNamesBothSides() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().OneOf("abc", "de").WithLength(9)); - - Check.That(conflict.Message).IsEqualTo("Cannot apply WithLength(9) because no value OneOf(\"abc\", \"de\") allows satisfies it."); - } - - [Fact(DisplayName = "A value set no declared constraint admits names that constraint and itself, and nothing else.")] - public void AnEmptyValueSetNamesTheConstraintThatRefusedIt() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithLength(9).OneOf("abc", "de")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply OneOf(\"abc\", \"de\") because WithLength(9) allows none of its values."); - } - - [Fact(DisplayName = "Several constraints that each refuse every value are all named.")] - public void EveryConstraintRefusingEveryValueIsNamed() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithLength(9).Numeric().OneOf("abc")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply OneOf(\"abc\") because WithLength(9), Numeric() allow none of its values."); - } - - [Fact(DisplayName = "When only the combination empties the set, no single constraint is blamed for it.")] - public void ACombinationBlamesNoSingleConstraint() { - // WithLength(3) admits "abc" and StartingWith("z") admits "zz": neither refuses every value, so naming - // either one would blame a constraint the caller could loosen without changing the verdict. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().WithLength(3).StartingWith("z").OneOf("abc", "zz")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply OneOf(\"abc\", \"zz\") because no value it offers satisfies the constraints already declared."); - } - - [Fact(DisplayName = "A constraint that would have accepted a value the others removed qualifies its claim.")] - public void AConstraintTheOthersOutranQualifiesItsClaim() { - // Numeric() does accept "12" — WithLength(3) is what took it away. Claiming that no value the set offers - // satisfies Numeric() would be false, so the message says only that nothing the other constraints left does. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().OneOf("abc", "12").WithLength(3).Numeric()); - - Check.That(conflict.Message).IsEqualTo("Cannot apply Numeric() because no value OneOf(\"abc\", \"12\") allows that the other constraints leave satisfies it."); - } - - [Fact(DisplayName = "A constraint that refuses every supplied value is not qualified away, whatever narrowed the set first.")] - public void AConstraintRefusingEveryValueIsNotQualifiedAway() { - // Numeric() refuses "abc" and "de" alike, so loosening WithLength(3) could not help and the message must - // not suggest it: the claim stays the plain one, identical to the verdict without any prior narrowing. - ConflictingAnyConstraintException narrowedFirst = Assert.Throws( - () => Any.String().OneOf("abc", "de").WithLength(3).Numeric()); - ConflictingAnyConstraintException onItsOwn = Assert.Throws( - () => Any.String().OneOf("abc", "de").Numeric()); - - Check.That(narrowedFirst.Message).IsEqualTo("Cannot apply Numeric() because no value OneOf(\"abc\", \"de\") allows satisfies it."); - Check.That(narrowedFirst.Message).IsEqualTo(onItsOwn.Message); - } - - [Fact(DisplayName = "A constraint declared in one call is blamed as one call, not as the bounds it sets.")] - public void ARangeIsBlamedAsTheCallTheCallerWrote() { - // WithLengthBetween sets two internal bounds under one name. Judged apart, each admits one of the two - // values and neither looks guilty; judged as the call the caller wrote — the only thing they can loosen — - // it is the sole culprit, and it is what the message names, in either declaration order. - ConflictingAnyConstraintException setLast = Assert.Throws( - () => Any.String().WithLengthBetween(2, 3).OneOf("a", "bbbb")); - ConflictingAnyConstraintException setFirst = Assert.Throws( - () => Any.String().OneOf("a", "bbbb").WithLengthBetween(2, 3)); - - Check.That(setLast.Message).IsEqualTo("Cannot apply OneOf(\"a\", \"bbbb\") because WithLengthBetween(2, 3) allows none of its values."); - // ... and, applied the other way round, it is not blamed on constraints that do not exist. - Check.That(setFirst.Message).IsEqualTo("Cannot apply WithLengthBetween(2, 3) because no value OneOf(\"a\", \"bbbb\") allows satisfies it."); - } - - [Fact(DisplayName = "A casing constraint judges a pooled value on its actual case, accents included.")] - public void CasingJudgesNonAsciiLettersToo() { - // The constructive filler is ASCII, but a supplied value is the caller's own text: 'É' is an uppercase - // letter, so LowerCase() must refuse it rather than wave it through and emit a value violating itself. - Check.ThatCode(() => Any.String().OneOf("É").LowerCase()).Throws(); - Check.ThatCode(() => Any.String().OneOf("é").UpperCase()).Throws(); - - Check.That(Any.String().OneOf("é", "É").LowerCase().Generate()).IsEqualTo("é"); - Check.That(Any.String().OneOf("é", "É").UpperCase().Generate()).IsEqualTo("É"); - } - - [Fact(DisplayName = "Constraints that contradict each other on their own terms are still refused before a value set is declared.")] - public void AContradictionIsRefusedBeforeTheValuesAreSeen() { - // Declared first, the set is the specification and the two fragments are merely checked against "aba". - Check.That(Any.String().OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba").Generate()).IsEqualTo("aba"); - - // Declared last, it arrives too late: laid out side by side those fragments need four characters, and that - // conflict is reported the moment it is declared — the generator cannot know a value set is coming, and - // deferring the refusal would cost every shaped string its eager conflict. - Check.ThatCode(() => Any.String().WithMaxLength(3).Containing("ab").Containing("ba").OneOf("aba")) - .Throws(); - } - - [Fact(DisplayName = "An exclusion that empties the value set conflicts at declaration, naming both sides.")] - public void AnExclusionEmptyingTheSetConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().OneOf("a", "b").Except("a", "b")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply Except(\"a\", \"b\") because no value OneOf(\"a\", \"b\") allows satisfies it."); - } - - [Fact(DisplayName = "A value set every exclusion covers conflicts, naming the exclusion that refused it.")] - public void AValueSetTheExclusionsRefuseNamesThem() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().DifferentFrom("x").OneOf("x")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply OneOf(\"x\") because DifferentFrom(\"x\") allows none of its values."); - } - - [Fact(DisplayName = "An exclusion on a value set never defers to a redraw: the surviving pool is drawn directly.")] - public void AnExclusionOnAValueSetIsResolvedEagerly() { - foreach (string value in Samples(Any.String().OneOf("keep", "drop").Except("drop"))) { - Check.That(value).IsEqualTo("keep"); - } - } - - [Fact(DisplayName = "Re-declaring the same value set is a no-op; a different one conflicts.")] - public void RedeclaringTheValueSet() { - Check.ThatCode(() => Any.String().OneOf("a", "b").OneOf("a", "b")).DoesNotThrow(); - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.String().OneOf("a", "b").OneOf("b", "c")); - - Check.That(conflict.Message).IsEqualTo("Cannot apply OneOf(\"b\", \"c\") because OneOf(\"a\", \"b\") is already defined."); - } - - [Fact(DisplayName = "OneOf rejects null, empty, or null-containing value lists as arguments.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3220:Method calls should not resolve ambiguously to overloads with \"params\"", - Justification = - "Passing a bare null to a params parameter is exactly what this test asserts about: OneOf(null!) must be " + - "refused with ArgumentNullException rather than read as an empty list. The ambiguity the rule warns about " + - "IS the input under test.")] - public void RejectsInvalidValueLists() { - Check.ThatCode(() => Any.String().OneOf()).Throws(); - Check.ThatCode(() => Any.String().OneOf(null!)).Throws(); - Check.ThatCode(() => Any.String().OneOf("a", null!)).Throws(); - } - - [Fact(DisplayName = "OneOf accepts a sequence, drawing only from its values.")] - public void AcceptsASequence() { - IEnumerable vendors = ["Apple", "Microsoft", "Google"]; - - HashSet seen = [.. Samples(Any.String().OneOf(vendors))]; - - Check.That(seen).IsOnlyMadeOf("Apple", "Microsoft", "Google"); - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "The sequence overload validates null, empty and null elements like the params one.")] - public void SequenceOverloadValidates() { - Check.ThatCode(() => Any.String().OneOf((IEnumerable)null!)).Throws(); - Check.ThatCode(() => Any.String().OneOf(Enumerable.Empty())).Throws(); - Check.ThatCode(() => Any.String().OneOf(new List { "a", null! })).Throws(); - } - - [Fact(DisplayName = "The sequence overload composes with the other constraints too.")] - public void SequenceOverloadComposes() { - Check.That(Any.String().NonEmpty().OneOf(new List { "", "a" }).Generate()).IsEqualTo("a"); - } - -} diff --git a/JustDummies.UnitTests/AnyTimeTests.cs b/JustDummies.UnitTests/AnyTimeTests.cs deleted file mode 100644 index 97c78f9e..00000000 --- a/JustDummies.UnitTests/AnyTimeTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyTimeTests { - - private const int SampleCount = 200; - - private static readonly DateTime Anchor = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - [Fact(DisplayName = "TimeSpan: Positive and Negative are strict against zero, and contradict each other.")] - public void TimeSpanSigns() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.TimeSpan().Positive().Generate() > TimeSpan.Zero).IsTrue(); - Check.That(Any.TimeSpan().Negative().Generate() < TimeSpan.Zero).IsTrue(); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.TimeSpan().Positive().Negative()); - Check.That(conflict.Message).Contains("Negative()"); - Check.That(conflict.Message).Contains("Positive()"); - } - - [Fact(DisplayName = "TimeSpan: Zero pins, Between is inclusive over a tiny tick window, GreaterThan is exclusive.")] - public void TimeSpanBounds() { - Check.That(Any.TimeSpan().Zero().Generate()).IsEqualTo(TimeSpan.Zero); - - HashSet ticks = []; - for (int i = 0; i < SampleCount; i++) { - TimeSpan value = Any.TimeSpan().Between(TimeSpan.FromTicks(1), TimeSpan.FromTicks(3)).Generate(); - ticks.Add(value.Ticks); - Check.That(value.Ticks).IsGreaterOrEqualThan(1L); - Check.That(value.Ticks).IsLessOrEqualThan(3L); - - Check.That(Any.TimeSpan().GreaterThan(TimeSpan.FromTicks(5)).LessThanOrEqualTo(TimeSpan.FromTicks(6)).Generate().Ticks).IsEqualTo(6L); - } - Check.That(ticks.Contains(1L)).IsTrue(); - Check.That(ticks.Contains(3L)).IsTrue(); - } - - [Fact(DisplayName = "DateTime: every generated value carries Utc kind.")] - public void DateTimeIsUtc() { - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.DateTime().Generate().Kind == DateTimeKind.Utc).IsTrue(); - Check.That(Any.DateTime().Between(Anchor, Anchor.AddDays(1)).Generate().Kind == DateTimeKind.Utc).IsTrue(); - } - } - - [Fact(DisplayName = "DateTime: After and Before are exclusive — a three-tick window pins the middle tick.")] - public void DateTimeExclusiveWindow() { - for (int i = 0; i < SampleCount; i++) { - DateTime value = Any.DateTime().After(Anchor).Before(Anchor.AddTicks(2)).Generate(); - Check.That(value.Ticks).IsEqualTo(Anchor.Ticks + 1); - } - } - - [Fact(DisplayName = "DateTime: an impossible After/Before pair conflicts naming both sides; crossed Between is an argument error.")] - public void DateTimeConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.DateTime().After(Anchor).Before(Anchor)); - Check.That(conflict.Message).Contains("Before("); - Check.That(conflict.Message).Contains("After("); - - Check.ThatCode(() => Any.DateTime().Between(Anchor.AddDays(1), Anchor)).Throws(); - Check.ThatCode(() => Any.DateTime().After(DateTime.MaxValue)).Throws(); - } - - [Fact(DisplayName = "DateTime: OneOf yields only supplied instants and DifferentFrom always picks the other of two.")] - public void DateTimeSets() { - DateTime[] allowed = [Anchor, Anchor.AddDays(1)]; - for (int i = 0; i < SampleCount; i++) { - Check.That(allowed.Contains(Any.DateTime().OneOf(allowed).Generate())).IsTrue(); - Check.That(Any.DateTime().Between(Anchor, Anchor.AddTicks(1)).DifferentFrom(Anchor).Generate().Ticks).IsEqualTo(Anchor.Ticks + 1); - } - } - - [Fact(DisplayName = "DateTimeOffset: generated values carry a zero offset, and comparisons work by instant.")] - public void DateTimeOffsetIsUtc() { - DateTimeOffset start = new(Anchor, TimeSpan.Zero); - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().Between(start, start.AddDays(1)).Generate(); - Check.That(value.Offset).IsEqualTo(TimeSpan.Zero); - } - - // A +02:00 bound constrains by UtcTicks: the exclusive three-tick window still pins the middle tick. - DateTimeOffset shifted = start.ToOffset(TimeSpan.FromHours(2)); - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().After(shifted).Before(shifted.AddTicks(2)).Generate(); - Check.That(value.UtcTicks).IsEqualTo(start.UtcTicks + 1); - } - } - - [Fact(DisplayName = "DateTimeOffset: OneOf returns the supplied values as given, offset included.")] - public void DateTimeOffsetOneOfPreservesOffsets() { - DateTimeOffset supplied = new(2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(2)); - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().OneOf(supplied).Generate(); - Check.That(value).IsEqualTo(supplied); - Check.That(value.Offset).IsEqualTo(TimeSpan.FromHours(2)); - } - } - - [Fact(DisplayName = "DateTimeOffset: Except excludes by instant.")] - public void DateTimeOffsetExceptByInstant() { - DateTimeOffset start = new(Anchor, TimeSpan.Zero); - for (int i = 0; i < SampleCount; i++) { - DateTimeOffset value = Any.DateTimeOffset().Between(start, start.AddTicks(1)).Except(start).Generate(); - Check.That(value.UtcTicks).IsEqualTo(start.UtcTicks + 1); - } - } - -} diff --git a/JustDummies.UnitTests/AnyUnsignedIntegerTests.cs b/JustDummies.UnitTests/AnyUnsignedIntegerTests.cs deleted file mode 100644 index 2ef47ac7..00000000 --- a/JustDummies.UnitTests/AnyUnsignedIntegerTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -public sealed class AnyUnsignedIntegerTests { - - private const int SampleCount = 200; - - [Fact(DisplayName = "Byte: Between is inclusive and reaches both bounds; extremes are generable.")] - public void ByteBounds() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { - byte value = Any.Byte().Between(1, 3).Generate(); - seen.Add(value); - Check.That((int)value).IsGreaterOrEqualThan(1); - Check.That((int)value).IsLessOrEqualThan(3); - } - Check.That(seen.Contains(1)).IsTrue(); - Check.That(seen.Contains(3)).IsTrue(); - - Check.That(Any.Byte().LessThanOrEqualTo(0).Generate()).IsEqualTo((byte)0); - Check.That(Any.Byte().GreaterThanOrEqualTo(byte.MaxValue).Generate()).IsEqualTo(byte.MaxValue); - } - - [Fact(DisplayName = "Byte: Zero pins, NonZero excludes, the pair conflicts, and GreaterThan(max) conflicts.")] - public void ByteZeroAndConflicts() { - Check.That(Any.Byte().Zero().Generate()).IsEqualTo((byte)0); - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Byte().Between(0, 1).NonZero().Generate()).IsEqualTo((byte)1); - } - Check.ThatCode(() => Any.Byte().Zero().NonZero()).Throws(); - Check.ThatCode(() => Any.Byte().GreaterThan(byte.MaxValue)).Throws(); - } - - [Fact(DisplayName = "UInt16 and UInt32: exclusive bounds behave and crossed bounds conflict.")] - public void MidWidthExclusiveBounds() { - for (int i = 0; i < SampleCount; i++) { - Check.That((int)Any.UInt16().GreaterThan(10).LessThanOrEqualTo(12).Generate()).IsGreaterOrEqualThan(11); - Check.That(Any.UInt32().LessThan(10u).GreaterThanOrEqualTo(8u).Generate()).IsLessOrEqualThan(9u); - } - - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.UInt32().GreaterThan(100u).LessThan(10u)); - Check.That(conflict.Message).Contains("LessThan(10)"); - Check.That(conflict.Message).Contains("GreaterThan(100)"); - } - - [Fact(DisplayName = "UInt64: the full-width sampling path yields varied values and honors exclusions.")] - public void UInt64FullWidth() { - HashSet seen = []; - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.UInt64().Generate()); } - Check.That(seen.Count).IsStrictlyGreaterThan(1); - - for (int i = 0; i < SampleCount; i++) { - Check.That(Any.UInt64().Between(0UL, 2UL).Except(1UL).Generate()).IsNotEqualTo(1UL); - } - } - - [Fact(DisplayName = "UInt64: extremes are generable and OneOf/Except behave.")] - public void UInt64ExtremesAndSets() { - Check.That(Any.UInt64().GreaterThanOrEqualTo(ulong.MaxValue).Generate()).IsEqualTo(ulong.MaxValue); - Check.ThatCode(() => Any.UInt64().GreaterThan(ulong.MaxValue)).Throws(); - - ulong[] allowed = [1UL, 5UL]; - for (int i = 0; i < SampleCount; i++) { - Check.That(allowed.Contains(Any.UInt64().OneOf(allowed).Generate())).IsTrue(); - Check.That(Any.UInt64().Between(7UL, 8UL).DifferentFrom(7UL).Generate()).IsEqualTo(8UL); - } - Check.ThatCode(() => Any.UInt64().Between(10UL, 1UL)).Throws(); - } - - [Fact(DisplayName = "Every unsigned integer generator materializes its own value type through Generate().")] - public void MaterializesEachValueType() { - byte tiny = Any.Byte().Between(1, 10).Generate(); - ushort mid = Any.UInt16().NonZero().Generate(); - uint wide = Any.UInt32().Between(1u, 10u).Generate(); - ulong huge = Any.UInt64().Between(1UL, 10UL).Generate(); - - Check.That((int)tiny).IsGreaterOrEqualThan(1); - Check.That((int)mid).IsStrictlyGreaterThan(0); - Check.That(wide).IsGreaterOrEqualThan(1u); - Check.That(huge).IsGreaterOrEqualThan(1UL); - } - -} diff --git a/JustDummies.UnitTests/AnyUriTests.cs b/JustDummies.UnitTests/AnyUriTests.cs deleted file mode 100644 index 2bdfdc6f..00000000 --- a/JustDummies.UnitTests/AnyUriTests.cs +++ /dev/null @@ -1,270 +0,0 @@ -#region Usings declarations - -using System; -using System.Collections.Generic; - -using JetBrains.Annotations; - -using NFluent; - -using Xunit; - -#endregion - -namespace JustDummies.UnitTests; - -[TestSubject(typeof(AnyUri))] -public sealed class AnyUriTests { - - private const int FuzzSeeds = 500; - private const int SampleCount = 300; - - #region Statics members declarations - - // Valid by construction: across many seeds a family generator never throws and yields the expected URI kind. - private static void GeneratesAcross(Func> build, UriKind? kind) { - for (int seed = 0; seed < FuzzSeeds; seed++) { - Uri value; - try { value = build(Any.WithSeed(seed)).Generate(); } - catch (Exception error) { Assert.Fail($"seed {seed}: {error.GetType().Name}: {error.Message}"); return; } - - if (kind.HasValue) { - Check.WithCustomMessage($"seed {seed}: '{value.OriginalString}'") - .That(value.IsAbsoluteUri).IsEqualTo(kind.Value == UriKind.Absolute); - } - } - } - - private static IAny Seeded(Func> build) { - return build(Any.WithSeed(20260723)); - } - - #endregion - - [Fact(DisplayName = "Every family is valid by construction: it never throws and yields the expected URI kind.")] - public void EveryFamilyGeneratesValidUris() { - GeneratesAcross(context => context.Uri(), null); - GeneratesAcross(context => context.Uri().Web(), UriKind.Absolute); - GeneratesAcross(context => context.Uri().WebSocket(), UriKind.Absolute); - GeneratesAcross(context => context.Uri().Ftp(), UriKind.Absolute); - GeneratesAcross(context => context.Uri().Mailto(), UriKind.Absolute); - GeneratesAcross(context => context.Uri().Relative(), UriKind.Relative); - } - - [Fact(DisplayName = "The unconstrained generator reaches every family.")] - public void UnconstrainedReachesEveryFamily() { - HashSet seen = []; - foreach (Uri value in Sample(Seeded(context => context.Uri()))) { - seen.Add(value.IsAbsoluteUri ? value.Scheme : "relative"); - } - - Check.That(seen.Contains("http") || seen.Contains("https")).IsTrue(); - Check.That(seen.Contains("ws") || seen.Contains("wss")).IsTrue(); - Check.That(seen.Contains("ftp")).IsTrue(); - Check.That(seen.Contains("mailto")).IsTrue(); - Check.That(seen.Contains("relative")).IsTrue(); - } - - [Fact(DisplayName = "Web reaches both http and https; each generated value is one of them.")] - public void WebReachesBothSchemes() { - HashSet seen = []; - foreach (Uri value in Sample(Seeded(context => context.Uri().Web()))) { - seen.Add(value.Scheme); - Check.That(value.Scheme is "http" or "https").IsTrue(); - } - - Check.That(seen.Contains("http")).IsTrue(); - Check.That(seen.Contains("https")).IsTrue(); - } - - [Fact(DisplayName = "WebSocket reaches both ws and wss.")] - public void WebSocketReachesBothSchemes() { - HashSet seen = []; - foreach (Uri value in Sample(Seeded(context => context.Uri().WebSocket()))) { - seen.Add(value.Scheme); - Check.That(value.Scheme is "ws" or "wss").IsTrue(); - } - - Check.That(seen.Contains("ws")).IsTrue(); - Check.That(seen.Contains("wss")).IsTrue(); - } - - [Fact(DisplayName = "UsingHttps pins the scheme to https.")] - public void UsingHttpsPinsTheScheme() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().UsingHttps()))) { - Check.That(value.Scheme).IsEqualTo("https"); - } - } - - [Fact(DisplayName = "WithHost pins the host.")] - public void WithHostPinsTheHost() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithHost("api.example.com")))) { - Check.That(value.Host).IsEqualTo("api.example.com"); - } - } - - [Fact(DisplayName = "WithPort pins the port.")] - public void WithPortPinsThePort() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithPort(8443)))) { - Check.That(value.Port).IsEqualTo(8443); - } - } - - [Fact(DisplayName = "WithoutPath renders the root path.")] - public void WithoutPathRendersRoot() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithoutPath()))) { - Check.That(value.AbsolutePath).IsEqualTo("/"); - } - } - - [Fact(DisplayName = "WithPathSegments renders exactly that many segments.")] - public void WithPathSegmentsRendersThatManySegments() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithHost("h.test").WithPathSegments(3)))) { - Check.That(value.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Length).IsEqualTo(3); - } - } - - [Fact(DisplayName = "WithUserInfo, WithQuery and WithFragment add their components.")] - public void OptionalComponentsAreIncluded() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithUserInfo().WithQuery().WithFragment()))) { - Check.That(value.UserInfo.Length).IsStrictlyGreaterThan(0); - Check.That(value.Query.Length).IsStrictlyGreaterThan(0); - Check.That(value.Fragment.Length).IsStrictlyGreaterThan(0); - } - } - - [Fact(DisplayName = "WithUserInfo(user, password) pins both parts.")] - public void WithUserInfoPinsBothParts() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Ftp().WithUserInfo("admin", "s3cret")))) { - Check.That(value.UserInfo).IsEqualTo("admin:s3cret"); - } - } - - [Fact(DisplayName = "Ftp yields the ftp scheme with user-info.")] - public void FtpYieldsFtpScheme() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Ftp().WithUserInfo()))) { - Check.That(value.Scheme).IsEqualTo("ftp"); - Check.That(value.UserInfo.Length).IsStrictlyGreaterThan(0); - } - } - - [Fact(DisplayName = "Mailto yields the mailto scheme and an address.")] - public void MailtoYieldsAnAddress() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Mailto()))) { - Check.That(value.Scheme).IsEqualTo("mailto"); - Check.That(value.OriginalString).StartsWith("mailto:"); - Check.That(value.OriginalString).Contains("@"); - } - } - - [Fact(DisplayName = "Mailto pins the local-part and domain.")] - public void MailtoPinsLocalPartAndDomain() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Mailto().WithLocalPart("john").WithDomain("example.test")))) { - Check.That(value.OriginalString).IsEqualTo("mailto:john@example.test"); - } - } - - [Fact(DisplayName = "Relative yields a relative reference (not absolute).")] - public void RelativeYieldsARelativeReference() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Relative()))) { - Check.That(value.IsAbsoluteUri).IsFalse(); - } - } - - [Fact(DisplayName = "Relative().Rooted() starts the path with a slash.")] - public void RootedRelativeStartsWithSlash() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Relative().Rooted().WithPathSegments(2)))) { - Check.That(value.OriginalString).StartsWith("/"); - } - } - - [Fact(DisplayName = "A second scheme pin conflicts, naming both sides.")] - public void SecondSchemePinConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Uri().Web().UsingHttp().UsingHttps()); - - Check.That(conflict.Message).Contains("UsingHttps()"); - Check.That(conflict.Message).Contains("UsingHttp()"); - } - - [Fact(DisplayName = "A second path constraint conflicts, naming both sides.")] - public void SecondPathConstraintConflicts() { - ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Uri().Web().WithoutPath().WithPathSegments(2)); - - Check.That(conflict.Message).Contains("WithPathSegments(2)"); - Check.That(conflict.Message).Contains("WithoutPath()"); - } - - [Fact(DisplayName = "WithHost rejects a non-ASCII (IDN) host, pointing to punycode.")] - public void WithHostRejectsIdnHost() { - ArgumentException error = Assert.Throws(() => Any.Uri().Web().WithHost("münchen.de")); - - Check.That(error.Message).Contains("punycode"); - } - - [Fact(DisplayName = "Host, user-info, port and segment-count arguments are validated as arguments.")] - public void ArgumentsAreValidated() { - Check.ThatCode(() => Any.Uri().Web().WithHost(null!)).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithHost("")).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithHost("bad host")).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithUserInfo("a:b")).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithPort(0)).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithPort(70000)).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithPathSegments(-1)).Throws(); - } - - [Fact(DisplayName = "WithUserInfo(user) keeps the user and draws an arbitrary password.")] - public void WithUserInfoPinsUserAndDrawsPassword() { - foreach (Uri value in Sample(Seeded(context => context.Uri().Web().WithUserInfo("bob")))) { - Check.That(value.UserInfo).StartsWith("bob:"); - Check.That(value.UserInfo.Length).IsStrictlyGreaterThan("bob:".Length); - } - } - - [Fact(DisplayName = "A relative URI with an explicit 0 segments and nothing else fails at generation with a seed.")] - public void EmptyRelativeFailsAtGeneration() { - AnyGenerationException error = Assert.Throws( - () => Any.WithSeed(20260723).Uri().Relative().WithPathSegments(0).Generate()); - - Check.That(error.Seed).IsEqualTo(20260723); - } - - [Fact(DisplayName = "A relative URI with 0 segments still generates when it carries a query.")] - public void ZeroSegmentRelativeWithQueryGenerates() { - Uri value = Any.WithSeed(1).Uri().Relative().WithPathSegments(0).WithQuery().Generate(); - - Check.That(value.IsAbsoluteUri).IsFalse(); - Check.That(value.OriginalString).StartsWith("?"); - } - - [Fact(DisplayName = "WithHost rejects a non-canonical shorthand-IPv4 host that would not round-trip.")] - public void WithHostRejectsNonCanonicalHost() { - ArgumentException error = Assert.Throws(() => Any.Uri().Web().WithHost("123")); - Check.That(error.Message).Contains("canonical"); - - Check.ThatCode(() => Any.Uri().Web().WithHost("1.2")).Throws(); - Check.ThatCode(() => Any.Uri().Web().WithHost("1.2.3.4")).DoesNotThrow(); - Check.ThatCode(() => Any.Uri().Web().WithHost("api.example.com")).DoesNotThrow(); - } - - [Fact(DisplayName = "A seeded URI draw is reproducible: the same seed yields the same value.")] - public void SeededDrawIsReproducible() { - Uri first = Any.WithSeed(4242).Uri().Web().WithQuery().Generate(); - Uri second = Any.WithSeed(4242).Uri().Web().WithQuery().Generate(); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "A URI generator composes like any other: through As into a domain value.")] - public void ComposesThroughAs() { - foreach (string scheme in Sample(Seeded(context => context.Uri().Web()).As(uri => uri.Scheme))) { - Check.That(scheme is "http" or "https").IsTrue(); - } - } - - private static IEnumerable Sample(IAny generator) { - for (int i = 0; i < SampleCount; i++) { yield return generator.Generate(); } - } - -} diff --git a/JustDummies.UnitTests/ArchitectureTests.cs b/JustDummies.UnitTests/ArchitectureTests.cs deleted file mode 100644 index 5b9cf42d..00000000 --- a/JustDummies.UnitTests/ArchitectureTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -#region Usings declarations - -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Guards the standalone boundary of the library: JustDummies is error-agnostic and must never gain a dependency on -/// FirstClassErrors (or any other project of this repository). If this test fails, the boundary was crossed — -/// see the ADR that records the decision before touching it. -/// -public sealed class ArchitectureTests { - - [Fact(DisplayName = "JustDummies references no FirstClassErrors assembly.")] - public void JustDummiesReferencesNoFirstClassErrorsAssembly() { - AssemblyName[] references = typeof(Any).Assembly.GetReferencedAssemblies(); - - foreach (AssemblyName reference in references) { - Check.That(reference.Name!.StartsWith("FirstClassErrors", StringComparison.Ordinal)).IsFalse(); - } - } - - [Fact(DisplayName = "JustDummies depends on nothing beyond the standard library.")] - public void JustDummiesDependsOnNothingBeyondTheStandardLibrary() { - AssemblyName[] references = typeof(Any).Assembly.GetReferencedAssemblies(); - - foreach (AssemblyName reference in references) { - // The exact facade split (System.Runtime, System.Threading, ...) varies with the SDK and build - // configuration, so the guard checks the intent — standard library only — not a fixed list. - bool standard = reference.Name is "netstandard" or "mscorlib" || reference.Name!.StartsWith("System.", StringComparison.Ordinal); - Check.WithCustomMessage($"Unexpected assembly reference: {reference.Name}").That(standard).IsTrue(); - } - } - -} diff --git a/JustDummies.UnitTests/CompositionTests.cs b/JustDummies.UnitTests/CompositionTests.cs deleted file mode 100644 index c5b2d8ec..00000000 --- a/JustDummies.UnitTests/CompositionTests.cs +++ /dev/null @@ -1,344 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -[TestSubject(typeof(AnyExtensions))] -public sealed class CompositionTests { - - #region Statics members declarations - - private static T Materialize(IAny generator) { - return generator.Generate(); - } - - #endregion - - [Fact(DisplayName = "As bridges a constrained string to a value object through its own factory.")] - public void AsBuildsAStringValueObject() { - IAny generator = Any.String() - .StartingWith("ORD-") - .WithLength(12) - .As(OrderReference.Create); - - OrderReference reference = generator.Generate(); - - Check.That(reference.Value).StartsWith("ORD-"); - Check.That(reference.Value.Length).IsEqualTo(12); - } - - [Fact(DisplayName = "As bridges a constrained integer to a value object through its own factory.")] - public void AsBuildsANumericValueObject() { - IAny generator = Any.Int32().Between(0, 100).As(Percentage.Create); - - Percentage percentage = generator.Generate(); - - Check.That(percentage.Value).IsGreaterOrEqualThan(0); - Check.That(percentage.Value).IsLessOrEqualThan(100); - } - - [Fact(DisplayName = "A factory rejecting the generated value surfaces as AnyGenerationException naming the value and the seed.")] - public void AsWrapsFactoryFailures() { - IAny tooWeaklyConstrained = Any.Int32().Between(200, 300).As(Percentage.Create); - - AnyGenerationException? caught = null; - Assert.Throws( - () => Any.Reproducibly(9876, () => { - try { - tooWeaklyConstrained.Generate(); - } catch (AnyGenerationException exception) { - caught = exception; - - throw; - } - }, _ => { })); - - Check.That(caught).IsNotNull(); - Check.That(caught!.Seed).IsEqualTo(9876); - Check.That(caught.Message).Contains("As(...)"); - Check.That(caught.Message).Contains("9876"); - Check.That(caught.InnerException).IsInstanceOf(); - } - - [Fact(DisplayName = "Combine assembles two constrained parts through a constructor lambda.")] - public void CombineAssemblesTwoParts() { - IAny generator = Any.Combine( - Any.String().NonEmpty().WithMaxLength(50), - Any.String().StartingWith("ORD-").WithLength(12), - (name, reference) => new Customer(name, OrderReference.Create(reference))); - - Customer customer = generator.Generate(); - - Check.That(customer.Name).IsNotEmpty(); - Check.That(customer.LastOrder.Value).StartsWith("ORD-"); - } - - [Fact(DisplayName = "Combine assembles three parts through a constructor lambda.")] - public void CombineAssemblesThreeParts() { - IAny generator = Any.Combine( - Any.String().WithLength(2).UpperCase(), - Any.Int32().Between(10, 99), - Any.String().WithLength(2).LowerCase(), - (head, middle, tail) => $"{head}{middle}{tail}"); - - string value = generator.Generate(); - - Check.That(value.Length).IsEqualTo(6); - } - - [Fact(DisplayName = "A composer failure surfaces as AnyGenerationException naming the generated values.")] - public void CombineWrapsComposerFailures() { - IAny generator = Any.Combine( - Any.Int32().Between(1, 3), - Any.Int32().Between(4, 6), - (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); - - AnyGenerationException caught = Assert.Throws(() => generator.Generate()); - - Check.That(caught.Message).Contains("Combine(...)"); - Check.That(caught.InnerException).IsInstanceOf(); - } - - [Fact(DisplayName = "A composer failure over ambient generators reports the Any.Reproducibly replay hint.")] - public void CombineOverAmbientGeneratorsReportsReproduciblyHint() { - IAny generator = Any.Combine( - Any.Int32().Between(1, 3), - Any.Int32().Between(4, 6), - (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); - - AnyGenerationException caught = Assert.Throws( - () => Any.Reproducibly(31415, () => generator.Generate(), _ => { })); - - Check.That(caught.Seed).IsEqualTo(31415); - Check.That(caught.Message).Contains("Any.Reproducibly(31415"); - Check.That(caught.Message).Not.Contains("Any.WithSeed("); - } - - [Fact(DisplayName = "A composer failure over an Any.WithSeed(...) context reports the WithSeed replay hint, not the inapplicable Any.Reproducibly instruction.")] - public void CombineOverFixedContextReportsWithSeedHint() { - AnyContext seeded = Any.WithSeed(4242); - - IAny generator = Any.Combine( - seeded.Int32().Between(1, 3), - seeded.Int32().Between(4, 6), - (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); - - AnyGenerationException caught = Assert.Throws(() => generator.Generate()); - - Check.That(caught.Seed).IsEqualTo(4242); - Check.That(caught.Message).Contains("Combine(...)"); - Check.That(caught.Message).Contains("Any.WithSeed(4242)"); - Check.That(caught.Message).Not.Contains("Any.Reproducibly("); - } - - [Fact(DisplayName = "A composer failure over a Combine mixing a foreign operand qualifies the replay hint, though a library operand supplies a nameable source.")] - public void CombineOverMixedForeignAndLibraryQualifiesTheHint() { - // The foreign operand has no source, but Any.Int32()'s ambient source survives the ?? collapse, so a naive - // "non-null source means faithful" rule would over-promise. The composed value depends on the foreign draw, so - // the hint must be qualified even though a seed can still be named. - IAny generator = Any.Combine( - new ForeignInt(), - Any.Int32().Between(1, 3), - (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); - - AnyGenerationException caught = Assert.Throws( - () => Any.Reproducibly(31415, () => generator.Generate(), _ => { })); - - Check.That(caught.Seed).IsEqualTo(31415); - Check.That(caught.Message).Contains("Combine(...)"); - Check.That(caught.Message).Contains("not reproducible from this seed alone"); - Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); - } - - [Fact(DisplayName = "A composer failure over a Combine mixing two different seeded sources does not promise a full replay from one seed.")] - public void CombineOverMixedSeededSourcesQualifiesTheHint() { - // The first operand draws from Any.WithSeed(4242); the second from the ambient source. The composed value - // depends on BOTH, so replaying WithSeed(4242) alone reproduces only the first — the hint must not promise a - // deterministic full replay from that one seed (issue #319). - IAny generator = Any.Combine( - Any.WithSeed(4242).Int32().Between(1, 3), - Any.Int32().Between(4, 6), - (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); - - AnyGenerationException caught = Assert.Throws(() => generator.Generate()); - - Check.That(caught.Message).Contains("Combine(...)"); - Check.WithCustomMessage($"The hint over-promised a full replay. Message: {caught.Message}") - .That(caught.Message).Not.Contains("already replays deterministically"); - Check.WithCustomMessage($"The hint did not qualify the replay promise. Message: {caught.Message}") - .That(caught.Message).Contains("not reproducible from this seed alone"); - } - - [Fact(DisplayName = "Combine composes four through eight parts, passing every constrained part to the lambda.")] - public void CombineSupportsHigherArities() { - IAny part = Any.Int32().Between(1, 9); - - for (int i = 0; i < 50; i++) { - int[] four = Any.Combine(part, part, part, part, (a, b, c, d) => new[] { a, b, c, d }).Generate(); - int[] five = Any.Combine(part, part, part, part, part, (a, b, c, d, e) => new[] { a, b, c, d, e }).Generate(); - int[] six = Any.Combine(part, part, part, part, part, part, (a, b, c, d, e, f) => new[] { a, b, c, d, e, f }).Generate(); - int[] seven = Any.Combine(part, part, part, part, part, part, part, (a, b, c, d, e, f, g) => new[] { a, b, c, d, e, f, g }).Generate(); - int[] eight = Any.Combine(part, part, part, part, part, part, part, part, (a, b, c, d, e, f, g, h) => new[] { a, b, c, d, e, f, g, h }).Generate(); - - Check.That(four.Length).IsEqualTo(4); - Check.That(five.Length).IsEqualTo(5); - Check.That(six.Length).IsEqualTo(6); - Check.That(seven.Length).IsEqualTo(7); - Check.That(eight.Length).IsEqualTo(8); - foreach (int[] parts in new[] { four, five, six, seven, eight }) { - Check.That(parts).ContainsOnlyElementsThatMatch(value => value is >= 1 and <= 9); - } - } - } - - [Fact(DisplayName = "Combine draws every operand before composing, including one the composer never reads.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Composition", "JD027:A Combine operand never reaches the composed value", - Justification = - "The ignored operand IS the subject. This pins the behaviour JD027 reports: the operand is generated in full — constraints built, " + - "conflict checks run — and then dropped, with nothing failing.")] - public void CombineDrawsAnOperandTheComposerIgnores() { - int drawnFirst = 0; - int drawnSecond = 0; - - IAny first = Any.Int32().As(value => { - drawnFirst++; - - return value; - }); - IAny second = Any.Int32().As(value => { - drawnSecond++; - - return value; - }); - - _ = Any.Combine(first, second, (a, b) => a).Generate(); - - Check.That(drawnFirst).IsEqualTo(1); - Check.That(drawnSecond).IsEqualTo(1); - } - - [Fact(DisplayName = "A higher-arity Combine validates its arguments and wraps composer failures.")] - public void HigherArityCombineValidatesAndWraps() { - Check.ThatCode(() => Any.Combine(Any.Int32(), Any.Int32(), Any.Int32(), Any.Int32(), (Func)null!)).Throws(); - Check.ThatCode(() => Any.Combine(Any.Int32(), Any.Int32(), Any.Int32(), null!, (a, b, c, d) => a)).Throws(); - - IAny failing = Any.Combine( - Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), - Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), Any.Int32().Between(1, 2), - (a, b, c, d, e, f, g, h) => throw new InvalidOperationException("rejected")); - - AnyGenerationException caught = Assert.Throws(() => failing.Generate()); - Check.That(caught.Message).Contains("Combine(...)"); - Check.That(caught.InnerException).IsInstanceOf(); - } - - [Fact(DisplayName = "A generated value whose ToString() throws does not break a succeeding As or Combine.")] - public void AThrowingToStringDoesNotBreakASucceedingDerivation() { - // Regression: the failure sentence handed to the derivation plumbing was an interpolated string, so rendering - // the generated value ran on EVERY draw — successful ones included. A domain object whose ToString() throws - // (state a fixture never set, most often) therefore killed a derivation that had nothing wrong with it: the - // factory below is never even reached. The sentence is a thunk now, so nothing renders unless it fails. - IAny derived = Any.ElementOf(new[] { new Unrenderable() }).As(_ => "built"); - - Check.That(derived.Generate()).IsEqualTo("built"); - - IAny combined = Any.Combine(Any.ElementOf(new[] { new Unrenderable() }), - Any.Int32().Between(1, 3), - (_, number) => "built " + number); - - Check.That(combined.Generate()).StartsWith("built "); - } - - [Fact(DisplayName = "A factory failure over a value whose ToString() throws still reports the wrapped diagnostic.")] - public void AThrowingToStringStillYieldsTheWrappedDiagnostic() { - // The other half: once the factory does fail, rendering the value is attempted — and must not replace the - // diagnostic being built with the ToString() failure. The message degrades to the type name; the caller still - // gets an AnyGenerationException naming As(...) and carrying the real cause. - IAny failing = Any.ElementOf(new[] { new Unrenderable() }) - .As(_ => throw new InvalidOperationException("rejected")); - - AnyGenerationException caught = Assert.Throws(() => failing.Generate()); - - Check.That(caught.Message).Contains("As(...)"); - Check.That(caught.Message).Contains(nameof(Unrenderable)); // the fallback rendering - Check.That(caught.Message).Not.Contains("ToString() exploded"); // never the renderer's own failure - Check.That(caught.InnerException).IsInstanceOf(); - } - - [Fact(DisplayName = "A collection constraint over a value whose ToString() throws reports the conflict, not the rendering.")] - public void AThrowingToStringDoesNotMaskACollectionConflict() { - // Display also renders values into constraint-conflict messages, built eagerly by design at declaration time. - // The same guard has to hold there: the caller must read the conflict, not the renderer's accident. - Unrenderable value = new(); - - ConflictingAnyConstraintException caught = Assert.Throws( - () => Any.ListOf(Any.ElementOf(new[] { value })).Distinct().Containing(value).Containing(value)); - - Check.That(caught.Message).Contains(nameof(Unrenderable)); - Check.That(caught.Message).Not.Contains("ToString() exploded"); - } - - [Fact(DisplayName = "Generic inference flows through IAny without relying on implicit conversions.")] - public void GenericInferenceFlowsThroughIAny() { - string text = Materialize(Any.String().NonEmpty().WithMaxLength(50)); - int value = Materialize(Any.Int32().Positive()); - - Check.That(text).IsNotEmpty(); - Check.That(value).IsStrictlyGreaterThan(0); - } - - [Fact(DisplayName = "As and Combine validate their arguments.")] - public void CompositionValidatesArguments() { - Check.ThatCode(() => Any.String().As(null!)).Throws(); - Check.ThatCode(() => AnyExtensions.As(null!, (string value) => value)).Throws(); - Check.ThatCode(() => Any.Combine(null!, Any.Int32(), (int a, int b) => a + b)).Throws(); - Check.ThatCode(() => Any.Combine(Any.Int32(), Any.Int32(), (Func)null!)).Throws(); - } - - [Fact(DisplayName = "A derived generator draws fresh values on every generation.")] - public void DerivedGeneratorsDrawFreshValues() { - IAny generator = Any.Int32().Between(0, 100).As(Percentage.Create); - - HashSet seen = []; - for (int i = 0; i < 100; i++) { - seen.Add(generator.Generate().Value); - } - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - #region Nested types - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3877:Exceptions should not be thrown from unexpected methods", - Justification = - "Throwing from ToString() IS the fixture. The test proves diagnostics survive a domain object whose rendering " + - "explodes, and that a successful draw never renders one — neither of which can be shown without a type that " + - "throws exactly here.")] - private sealed class Unrenderable { - - // A domain object whose ToString() throws: the ordinary shape of it is a renderer reaching for state the - // fixture never set. Diagnostics must survive it, and a successful draw must never trigger it at all. - public override string ToString() { - throw new InvalidOperationException("ToString() exploded"); - } - - } - - private sealed class ForeignInt : IAny { - - // Foreign on purpose: implements IAny but NOT IHasRandomSource, so it draws from no reported source and a - // Combine that includes it is not fully reproducible even when another operand carries one. - public int Generate() { - return 0; - } - - } - - #endregion - -} diff --git a/JustDummies.UnitTests/ConcurrentDrawTests.cs b/JustDummies.UnitTests/ConcurrentDrawTests.cs deleted file mode 100644 index 8652627d..00000000 --- a/JustDummies.UnitTests/ConcurrentDrawTests.cs +++ /dev/null @@ -1,234 +0,0 @@ -#region Usings declarations - -using System.Collections.Concurrent; -using System.Text.RegularExpressions; - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Drawing from one seeded source on several threads at once. The ambient source flows with the execution -/// context, so it reaches every thread a test spawns; a context from is shared by -/// whoever holds it. Both therefore hand the same source to concurrent callers, and the source must survive it. -/// -/// -/// -/// Regression for issue #310. The defect was not a loss of quality but a collapse: an unsynchronized -/// whose two internal indices converge under contention returns zero for ever, so every -/// generator settles on the minimum of its declared range — 0, "", , -/// — and stays there for the rest of the scope. Those are exactly the values most -/// likely to make an assertion pass for the wrong reason, and nothing throws. -/// -/// -/// These tests are statistical in the direction that matters least: once the draw is serialized, corruption is -/// impossible rather than unlikely, so they pass deterministically. Before the fix they failed with very high -/// probability but not certainty — the usual bargain for a concurrency regression. The parallelism is -/// deliberately oversubscribed relative to the core count to make the contention reliable. -/// -/// -[TestSubject(typeof(Any))] -public sealed class ConcurrentDrawTests { - - #region Statics members declarations - - private const int Threads = 8; - private const int DrawsPerThread = 10_000; - private const int TotalDraws = Threads * DrawsPerThread; - - /// Runs on every thread at once and collects everything it produced. - private static List Storm(Func draw) { - ConcurrentBag drawn = []; - Parallel.For(0, Threads, new ParallelOptions { MaxDegreeOfParallelism = Threads }, - _ => { - for (int index = 0; index < DrawsPerThread; index++) { drawn.Add(draw()); } - }); - - return drawn.ToList(); - } - - /// How many times the most frequent value came up — the collapse signal, not a distribution measure. - private static int MostFrequent(IEnumerable values) { - return values.GroupBy(value => value).Max(group => group.Count()); - } - - #endregion - - [Fact(DisplayName = "Concurrent draws from an ambient seed scope never collapse onto one value.")] - public void ConcurrentAmbientDrawsDoNotCollapse() { - List drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.Int32().Generate())); - - // A tenth of the draws sharing one value cannot happen by chance over the full Int32 range; it is the - // signature of a source that stopped generating. Far below what the defect produced (62% to 91%). - Check.WithCustomMessage($"{MostFrequent(drawn)} of {TotalDraws} draws returned the same value; the shared Random collapsed.") - .That(MostFrequent(drawn)) - .IsStrictlyLessThan(TotalDraws / 10); - } - - [Fact(DisplayName = "A seeded source is still usable for sequential draws taken after a concurrent burst.")] - public void ASeededSourceSurvivesAConcurrentBurst() { - List afterwards = []; - - Any.Reproducibly(310, () => { - Storm(() => Any.Int32().Generate()); - - // The heart of the regression: the defect was permanent. Once the indices had converged, every later - // draw on that source returned int.MinValue — including these, taken on one thread with no contention. - afterwards = Enumerable.Range(0, 20).Select(_ => Any.Int32().Generate()).ToList(); - }); - - Check.WithCustomMessage($"Sequential draws after the burst were all {afterwards[0]}; the source stayed dead.") - .That(afterwards.Distinct().Count()) - .IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "A concurrent burst does not poison the other generators of the same source.")] - public void AConcurrentBurstDoesNotPoisonSiblingGenerators() { - string text = string.Empty; - Guid guid = Guid.Empty; - - Any.Reproducibly(310, () => { - Storm(() => Any.Int32().Generate()); - - // The corruption lives in the source, not in the generator that triggered it, so unrelated generators - // resolved from it afterwards collapsed too — a string to "" and a Guid to Guid.Empty. - text = Any.String().NonEmpty().Generate(); - guid = Any.Guid().Generate(); - }); - - Check.WithCustomMessage("A non-empty string generator returned an empty string after a concurrent burst.") - .That(text).IsNotEmpty(); - Check.WithCustomMessage("The Guid generator returned Guid.Empty after a concurrent burst.") - .That(guid).IsNotEqualTo(Guid.Empty); - } - - [Fact(DisplayName = "Concurrent draws never collapse a bounded generator onto its lower bound.")] - public void ConcurrentBoundedDrawsDoNotCollapseOntoTheirLowerBound() { - List drawn = []; - - // A bounded range makes the failure mode legible: a dead source does not return a random value inside the - // interval, it returns the interval's minimum, which reads like a plausible dummy. - Any.Reproducibly(310, () => drawn = Storm(() => Any.Int32().Between(1_000, 9_999).Generate())); - - int atTheBound = drawn.Count(value => value == 1_000); - - Check.WithCustomMessage($"{atTheBound} of {TotalDraws} draws returned the lower bound 1000.") - .That(atTheBound) - .IsStrictlyLessThan(TotalDraws / 10); - } - - [Fact(DisplayName = "A context shared across threads stays usable after concurrent draws.")] - public void ASharedContextSurvivesConcurrentDraws() { - AnyContext context = Any.WithSeed(310); - - List drawn = Storm(() => context.Int32().Generate()); - List afterwards = Enumerable.Range(0, 20).Select(_ => context.Int32().Generate()).ToList(); - - Check.WithCustomMessage($"{MostFrequent(drawn)} of {TotalDraws} draws from a shared context returned the same value.") - .That(MostFrequent(drawn)) - .IsStrictlyLessThan(TotalDraws / 10); - Check.WithCustomMessage($"Sequential draws from the shared context after the burst were all {afterwards[0]}.") - .That(afterwards.Distinct().Count()) - .IsStrictlyGreaterThan(1); - } - - #region Composed draw paths - - // The lock lives at one choke point — SeededRandom — but every generator reaches it through a different path. - // The scalar cases above prove the choke point itself; these prove the paths with the most moving parts still - // route through it: the derived generators (As, Combine) that wrap a draw, the collection engine's fill and - // dedup-draw loops, and the regex context. Each collapses in its own way if the source dies, so each asserts - // the shape of its own non-collapse — and each was confirmed red before the fix by stripping the lock (#310). - // - // Paths whose per-draw work is a single light sample — OrNull's null/value coin, a bare Any.Double() — are - // deliberately absent. Corruption is reliably provoked only by NextBytes-heavy draws (an eight-byte ordinal - // fill, a regex drawing one choice per character); a lone Next(2) or NextDouble() never builds enough - // contention to corrupt the source within a bounded run, so a lock-stripped mutant does not make such a test - // fail. A test that cannot go red on the broken code would only manufacture false confidence, and the choke - // point those paths share is already pinned by the cases here. (Measured: OrNull and Double each missed 5/5 - // lock-stripped runs, while every case below tripped 5/5.) - - [Fact(DisplayName = "Concurrent draws through Combine never collapse either operand.")] - public void ConcurrentCombineDrawsDoNotCollapse() { - List<(int First, int Second)> drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.Combine(Any.Int32(), Any.Int32(), (first, second) => (first, second)).Generate())); - - Check.WithCustomMessage($"Combine's first operand collapsed: {MostFrequent(drawn.Select(pair => pair.First))} of {TotalDraws} identical.") - .That(MostFrequent(drawn.Select(pair => pair.First))) - .IsStrictlyLessThan(TotalDraws / 10); - Check.WithCustomMessage($"Combine's second operand collapsed: {MostFrequent(drawn.Select(pair => pair.Second))} of {TotalDraws} identical.") - .That(MostFrequent(drawn.Select(pair => pair.Second))) - .IsStrictlyLessThan(TotalDraws / 10); - } - - [Fact(DisplayName = "Concurrent draws through As keep the underlying draw healthy.")] - public void ConcurrentAsDrawsStayHealthy() { - // A pure projection carries no shared state, so any degeneration here is the library's own serialized draw - // collapsing, not a user-side race — the latter is the caller's responsibility, per the IAny contract. - List drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.Int32().As(value => (long)value * 2).Generate())); - - Check.WithCustomMessage($"{MostFrequent(drawn)} of {TotalDraws} As-projected values were identical; the underlying draw collapsed.") - .That(MostFrequent(drawn)) - .IsStrictlyLessThan(TotalDraws / 10); - } - - [Fact(DisplayName = "Concurrent draws through a list generator keep the right size and never collapse the elements.")] - public void ConcurrentListDrawsDoNotCollapse() { - List> drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.ListOf(Any.Int32()).WithCount(4).Generate())); - - List elements = drawn.SelectMany(list => list).ToList(); - - Check.WithCustomMessage("A fixed-count list came back the wrong size under concurrency.") - .That(drawn.All(list => list.Count == 4)).IsTrue(); - Check.WithCustomMessage($"{MostFrequent(elements)} of {elements.Count} list elements were identical; the element draw collapsed.") - .That(MostFrequent(elements)) - .IsStrictlyLessThan(elements.Count / 10); - } - - [Fact(DisplayName = "Concurrent draws through a distinct set generator stay valid.")] - public void ConcurrentSetDrawsStayValid() { - // The distinct path runs a bounded dedup-draw against a fresh HashSet per generation — the path most exposed - // to a dead source, which cannot supply the fresh values it needs and fails loudly rather than collapsing. - List> drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.SetOf(Any.Int32().Between(0, 100_000)).WithCount(5).Generate())); - - Check.WithCustomMessage("A distinct set came back the wrong size under concurrency.") - .That(drawn.All(set => set.Count == 5)).IsTrue(); - - List elements = drawn.SelectMany(set => set).ToList(); - Check.WithCustomMessage($"{MostFrequent(elements)} of {elements.Count} set elements were identical; the element draw collapsed.") - .That(MostFrequent(elements)) - .IsStrictlyLessThan(elements.Count / 5); - } - - [Fact(DisplayName = "Concurrent draws through a pattern generator match and never collapse.")] - public void ConcurrentPatternDrawsStayValid() { - Regex pattern = new("^[A-Z]{3}-[0-9]{4}$"); - List drawn = []; - - Any.Reproducibly(310, () => drawn = Storm(() => Any.StringMatching("^[A-Z]{3}-[0-9]{4}$").Generate())); - - Check.WithCustomMessage($"A pattern draw did not match under concurrency, e.g. \"{drawn.FirstOrDefault(value => !pattern.IsMatch(value))}\".") - .That(drawn.All(value => pattern.IsMatch(value))).IsTrue(); - // A dead regex context still matches (it picks the first choice every time — "AAA-0000"), so matching alone - // would not catch the collapse; the non-collapse assertion is what pins it. - Check.WithCustomMessage($"{MostFrequent(drawn)} of {TotalDraws} pattern draws were identical; generation collapsed.") - .That(MostFrequent(drawn)) - .IsStrictlyLessThan(TotalDraws / 10); - } - - #endregion - -} diff --git a/JustDummies.UnitTests/ConflictMessageProvenanceTests.cs b/JustDummies.UnitTests/ConflictMessageProvenanceTests.cs deleted file mode 100644 index 828aad9f..00000000 --- a/JustDummies.UnitTests/ConflictMessageProvenanceTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// A conflict message must name the constraint that actually caused the conflict. When an exclusion -/// (NonZero/Except/DifferentFrom) empties the domain, the interval engines used to name a -/// bound instead — or the constraint being applied — producing messages that were self-referential -/// ("Cannot apply Zero() because Zero() already pins the value to 0") or factually false -/// ("GreaterThanOrEqualTo(5) already pins the value to 5", which allows 5..MaxValue). Issue #312. -/// -/// -/// Message content is the example suite's job (ADR-0040): these pin the contract "name the excluding -/// constraint", not the exact prose. Each asserts that the offending exclusion appears in the message — the -/// information that was missing — which is red against the old engines and green once exclusions carry -/// provenance. The four interval engines (ordinal, wide, decimal, continuous) share one exhaustion path, so a -/// case per engine guards them all. -/// -[TestSubject(typeof(ConflictingAnyConstraintException))] -public sealed class ConflictMessageProvenanceTests { - - #region Statics members declarations - - private static string ConflictMessage(Action build) { - try { - build(); - } catch (ConflictingAnyConstraintException exception) { - return exception.Message; - } - - return ""; - } - - #endregion - - // ----- OrdinalIntervalSpec: integers (and, by sharing the engine, TimeSpan/DateTime/DateOnly/TimeOnly) ----- - - [Fact(DisplayName = "A pin emptied by NonZero names NonZero, not the pin itself.")] - public void PinEmptiedByNonZeroNamesTheExclusion() { - string message = ConflictMessage(() => Any.Byte().NonZero().Zero()); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("NonZero()"); - } - - [Fact(DisplayName = "A single-value bound emptied by NonZero names NonZero.")] - public void BoundEmptiedByNonZeroNamesTheExclusion() { - string message = ConflictMessage(() => Any.Byte().NonZero().LessThan(1)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("NonZero()"); - } - - [Fact(DisplayName = "A range pinned by two bounds and emptied by Except names Except, not a bound.")] - public void PinEmptiedByExceptNamesTheExclusionNotABound() { - string message = ConflictMessage(() => Any.Int32().Except(5).GreaterThanOrEqualTo(5).LessThanOrEqualTo(5)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("Except(5)"); - } - - [Fact(DisplayName = "A pin emptied by DifferentFrom names DifferentFrom.")] - public void PinEmptiedByDifferentFromNamesTheExclusion() { - string message = ConflictMessage(() => Any.Int32().DifferentFrom(-1).Negative().GreaterThan(-2)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("DifferentFrom(-1)"); - } - - [Fact(DisplayName = "A lattice emptied by Except names Except and the lattice, not the lattice alone.")] - public void LatticeEmptiedByExceptNamesBothTheExclusionAndTheLattice() { - string message = ConflictMessage(() => Any.Int32().MultipleOf(5).Except(0).Between(-4, 4)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("Except(0)"); - Check.WithCustomMessage($"The lattice was not named. Message: {message}").That(message).Contains("MultipleOf(5)"); - } - - [Fact(DisplayName = "An allow-list emptied by Except names Except, not just the allow-list.")] - public void AllowListEmptiedByExceptNamesTheExclusion() { - string message = ConflictMessage(() => Any.Int32().Except(1, 2).OneOf(1, 2)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("Except(1, 2)"); - } - - // ----- DecimalIntervalSpec ----- - - [Fact(DisplayName = "A decimal pin emptied by DifferentFrom names DifferentFrom.")] - public void DecimalPinEmptiedByDifferentFromNamesTheExclusion() { - string message = ConflictMessage(() => Any.Decimal().DifferentFrom(1m).Between(1m, 1m)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("DifferentFrom(1)"); - } - - // ----- ContinuousIntervalSpec: double (and, by sharing the engine, Single/Half) ----- - - [Fact(DisplayName = "A double pin emptied by DifferentFrom names DifferentFrom.")] - public void DoublePinEmptiedByDifferentFromNamesTheExclusion() { - string message = ConflictMessage(() => Any.Double().DifferentFrom(1d).Between(1d, 1d)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("DifferentFrom(1)"); - } - -#if NET8_0_OR_GREATER - // ----- WideIntervalSpec: Int128/UInt128 (net8.0 leg only) ----- - - [Fact(DisplayName = "An Int128 pin emptied by NonZero names NonZero.")] - public void Int128PinEmptiedByNonZeroNamesTheExclusion() { - string message = ConflictMessage(() => Any.Int128().NonZero().Between(System.Int128.Zero, System.Int128.Zero)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("NonZero()"); - } -#endif - - // ----- Correctness of the claim, not only the naming (surfaced by the exhaustive audit). ----- - - [Fact(DisplayName = "An allow-list narrowed by a bound before an exclusion empties it does not claim the exclusion forbids every allowed value.")] - public void AllowListNarrowedByABoundIsNotOverclaimed() { - // OneOf(1, 3) offers two values, but Between(0, 1) already drops 3; Except(1) then removes the only one - // that survived. Saying Except(1) forbids *every* value OneOf allows would be false — it never forbids 3 — - // so the claim must be qualified to the values the other constraints leave. - string message = ConflictMessage(() => Any.Int32().Except(1).Between(0, 1).OneOf(1, 3)); - - Check.WithCustomMessage($"The exclusion was not named. Message: {message}").That(message).Contains("Except(1)"); - Check.WithCustomMessage($"The message overclaims that Except(1) forbids every value OneOf allows. Message: {message}") - .That(message).Contains("that the other constraints leave"); - } - - [Fact(DisplayName = "When the applied exclusion is itself the sole cause, the message reads 'it forbids', not the constraint twice.")] - public void AnExclusionAppliedLastIsNotRepeatedOnBothSides() { - // Zero() pins the byte to 0; NonZero(), applied last, is itself the forbidder. Repeating "NonZero()" on - // both sides of "because" reads as circular, so the clause refers back to the applied constraint as "it". - string message = ConflictMessage(() => Any.Byte().Zero().NonZero()); - - Check.WithCustomMessage($"The applied constraint should be referred to as 'it'. Message: {message}").That(message).Contains("it forbids"); - Check.WithCustomMessage($"The applied constraint is echoed after 'because'. Message: {message}").That(message).Not.Contains("because NonZero()"); - } - - // ----- Regression guard: bound-vs-bound messages must stay correct and unchanged. ----- - - [Fact(DisplayName = "A bound-vs-bound conflict still names the opposing bound (unchanged).")] - public void BoundVersusBoundStillNamesBothSides() { - string message = ConflictMessage(() => Any.Int32().Between(1, 10).GreaterThan(50)); - - Check.That(message).Contains("Between(1, 10)"); - Check.That(message).Contains("GreaterThan(50)"); - } - -} diff --git a/JustDummies.UnitTests/ConstraintCallTests.cs b/JustDummies.UnitTests/ConstraintCallTests.cs deleted file mode 100644 index dbc92c06..00000000 --- a/JustDummies.UnitTests/ConstraintCallTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The rendering contract of : a constraint is quoted in diagnostics as the caller -/// spelled it. The expected spellings below are the ones the generators write by hand today -/// ("Zero()", $"Between({V(minimum)}, {V(maximum)})", "OneOf(...)"), pinned here so the -/// type can take that job over without moving a single character of any conflict message. -/// -/// -/// These belong to the example suite (ADR-0040): each asserts one named spelling, and the null cases have no -/// input space. The guards themselves are also held by -/// — reflected over every internal member — but a constraint's -/// own contract is worth reading locally. -/// -[TestSubject(typeof(ConstraintCall))] -public sealed class ConstraintCallTests { - - [Fact(DisplayName = "A constraint given no argument renders as an empty argument list.")] - public void RendersAConstraintWithoutArguments() { - ConstraintCall call = ConstraintCall.Of("Zero"); - - Check.That(call.ToString()).IsEqualTo("Zero()"); - } - - [Fact(DisplayName = "A constraint given one argument renders it between the parentheses.")] - public void RendersAConstraintWithOneArgument() { - ConstraintCall call = ConstraintCall.Of("MultipleOf", "5"); - - Check.That(call.ToString()).IsEqualTo("MultipleOf(5)"); - } - - [Fact(DisplayName = "A constraint given several arguments separates them with a comma and a space.")] - public void RendersSeveralArgumentsSeparatedByACommaAndASpace() { - ConstraintCall call = ConstraintCall.Of("Between", "0", "100"); - - Check.That(call.ToString()).IsEqualTo("Between(0, 100)"); - } - - // The migration path for the generators that pre-join a pool through their own Join helper: the joined text is - // one argument, and passing it through must not re-punctuate it. - [Fact(DisplayName = "An argument already carrying separators is rendered untouched.")] - public void KeepsAnAlreadyJoinedArgumentIntact() { - ConstraintCall call = ConstraintCall.Of("OneOf", "1, 2, 3"); - - Check.That(call.ToString()).IsEqualTo("OneOf(1, 2, 3)"); - } - - [Fact(DisplayName = "A constraint whose arguments cannot be rendered elides them with an ellipsis.")] - public void RendersElidedArgumentsAsAnEllipsis() { - ConstraintCall call = ConstraintCall.OfElided("OneOf"); - - Check.That(call.ToString()).IsEqualTo("OneOf(...)"); - } - - [Fact(DisplayName = "The declaring method's name reaches the rendering as written.")] - public void CarriesTheNameItWasGiven() { - ConstraintCall call = ConstraintCall.Of(nameof(Any.ElementOf)); - - Check.That(call.ToString()).IsEqualTo("ElementOf()"); - } - - // The specs compare the constraint being applied against the one already recorded to tell a harmless - // redeclaration from a conflict, so equality is over what the constraint reads as, not over identity. - [Fact(DisplayName = "Two constraints built apart but reading the same are equal.")] - public void EqualsAnotherConstraintThatReadsTheSame() { - ConstraintCall first = ConstraintCall.Of("Between", "0", "100"); - ConstraintCall second = ConstraintCall.Of("Between", "0", "100"); - - Check.That(first.Equals(second)).IsTrue(); - Check.That(first == second).IsTrue(); - Check.That(first != second).IsFalse(); - Check.That(first.GetHashCode()).IsEqualTo(second.GetHashCode()); - } - - [Fact(DisplayName = "The same name carrying different arguments is a different constraint.")] - public void DiffersFromTheSameNameWithOtherArguments() { - ConstraintCall first = ConstraintCall.Of("Between", "0", "100"); - ConstraintCall second = ConstraintCall.Of("Between", "5", "50"); - - Check.That(first.Equals(second)).IsFalse(); - Check.That(first != second).IsTrue(); - } - - [Fact(DisplayName = "Different names are different constraints, and the ellipsis is not an empty list.")] - public void DiffersFromAnotherName() { - Check.That(ConstraintCall.Of("Zero") == ConstraintCall.Of("NonZero")).IsFalse(); - Check.That(ConstraintCall.Of("OneOf") == ConstraintCall.OfElided("OneOf")).IsFalse(); - } - - [Fact(DisplayName = "Equality is ordinal, so casing tells two constraints apart.")] - public void ComparesOrdinally() { - Check.That(ConstraintCall.Of("zero") == ConstraintCall.Of("Zero")).IsFalse(); - } - - [Fact(DisplayName = "A constraint equals neither null nor a value of another type.")] - public void EqualsNeitherNullNorAnotherType() { - ConstraintCall call = ConstraintCall.Of("Zero"); - ConstraintCall? nothing = null; - object text = "Zero()"; - - Check.That(call.Equals(nothing)).IsFalse(); - Check.That(call.Equals(text)).IsFalse(); - Check.That(call == nothing).IsFalse(); - Check.That(call != nothing).IsTrue(); - } - - [Fact(DisplayName = "Two absent constraints compare equal, which is what an unset spec slot relies on.")] - public void TreatsTwoAbsentConstraintsAsEqual() { - ConstraintCall? absent = null; - - Check.That(absent == null).IsTrue(); - Check.That(absent != null).IsFalse(); - } - - [Fact(DisplayName = "Both factories reject a null name, and Of rejects a null argument array.")] - public void RejectsNullArguments() { - Check.ThatCode(() => ConstraintCall.Of(null!)).Throws(); - Check.ThatCode(() => ConstraintCall.Of("Between", (string[])null!)).Throws(); - Check.ThatCode(() => ConstraintCall.OfElided(null!)).Throws(); - } - -} diff --git a/JustDummies.UnitTests/ConstraintRedeclarationTests.cs b/JustDummies.UnitTests/ConstraintRedeclarationTests.cs deleted file mode 100644 index 3783a0aa..00000000 --- a/JustDummies.UnitTests/ConstraintRedeclarationTests.cs +++ /dev/null @@ -1,185 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The cross-type convention for re-declaring a constraint that is "declared once per generator". -/// -/// Declaring the same constraint twice is not a contradiction — the second declaration asks for exactly -/// what the first already guarantees — so it is a no-op. Declaring a different value for the same -/// once-only constraint is a contradiction and still fails at declaration time. The rule is one rule across -/// every generator family, which is why it is pinned here rather than scattered through each type's own tests. -/// -/// -/// This is a structural convention over a fixed table, not a property: the input space is "which constraint", -/// and the constraints are heterogeneous — there is nothing to generate. -/// -/// -public sealed class ConstraintRedeclarationTests { - - #region Statics members declarations - - private static readonly Guid Pinned = Guid.Parse("6f9619ff-8b86-d011-b42d-00c04fc964ff"); - - /// Every once-only constraint, declared twice with identical arguments. - private static IEnumerable<(string Label, Func Redeclare)> IdenticalRedeclarations() { - yield return ("String().WithLength(3)", () => Any.String().WithLength(3).WithLength(3)); - yield return ("String().StartingWith(\"a\")", () => Any.String().StartingWith("a").StartingWith("a")); - yield return ("String().EndingWith(\"z\")", () => Any.String().EndingWith("z").EndingWith("z")); - yield return ("String().Alpha()", () => Any.String().Alpha().Alpha()); - yield return ("String().Numeric()", () => Any.String().Numeric().Numeric()); - yield return ("String().AlphaNumeric()", () => Any.String().AlphaNumeric().AlphaNumeric()); - yield return ("String().WithChars(\"ab\")", () => Any.String().WithChars("ab").WithChars("ab")); - yield return ("String().LowerCase()", () => Any.String().LowerCase().LowerCase()); - yield return ("String().UpperCase()", () => Any.String().UpperCase().UpperCase()); - yield return ("String().OneOf(\"a\", \"b\")", () => Any.String().OneOf("a", "b").OneOf("a", "b")); - yield return ("Int32().OneOf(1, 2)", () => Any.Int32().OneOf(1, 2).OneOf(1, 2)); - yield return ("Int32().MultipleOf(3)", () => Any.Int32().MultipleOf(3).MultipleOf(3)); - yield return ("Int64().OneOf(1L)", () => Any.Int64().OneOf(1L).OneOf(1L)); - yield return ("Double().OneOf(1.5)", () => Any.Double().OneOf(1.5).OneOf(1.5)); - yield return ("Decimal().WithScale(2)", () => Any.Decimal().WithScale(2).WithScale(2)); - yield return ("Char().Alpha()", () => Any.Char().Alpha().Alpha()); - yield return ("Char().LowerCase()", () => Any.Char().LowerCase().LowerCase()); - yield return ("Guid().OneOf(pinned)", () => Any.Guid().OneOf(Pinned).OneOf(Pinned)); - yield return ("Uri().Web().WithPathSegments(2)", () => Any.Uri().Web().WithPathSegments(2).WithPathSegments(2)); - yield return ("Uri().Web().WithHost(\"a.example\")", () => Any.Uri().Web().WithHost("a.example").WithHost("a.example")); - yield return ("Uri().Web().WithPort(8080)", () => Any.Uri().Web().WithPort(8080).WithPort(8080)); - yield return ("Uri().Web().WithUserInfo(\"alice\")", () => Any.Uri().Web().WithUserInfo("alice").WithUserInfo("alice")); - yield return ("Uri().Mailto().WithDomain(\"a.example\")", () => Any.Uri().Mailto().WithDomain("a.example").WithDomain("a.example")); - yield return ("ListOf().WithCount(3)", () => Any.ListOf(Any.Int32()).WithCount(3).WithCount(3)); - yield return ("DateTimeOffset().WithOffset(zero)", () => Any.DateTimeOffset().WithOffset(TimeSpan.Zero).WithOffset(TimeSpan.Zero)); - } - - /// The same constraints declared twice with arguments that genuinely contradict. - private static IEnumerable<(string Label, Func Contradict)> ContradictoryRedeclarations() { - yield return ("String().WithLength(3).WithLength(5)", () => Any.String().WithLength(3).WithLength(5)); - yield return ("String().StartingWith(\"a\").StartingWith(\"b\")", () => Any.String().StartingWith("a").StartingWith("b")); - yield return ("String().Alpha().Numeric()", () => Any.String().Alpha().Numeric()); - yield return ("String().LowerCase().UpperCase()", () => Any.String().LowerCase().UpperCase()); - yield return ("String().OneOf(\"a\", \"b\").OneOf(\"c\", \"d\")", () => Any.String().OneOf("a", "b").OneOf("c", "d")); - yield return ("Int32().OneOf(1, 2).OneOf(3, 4)", () => Any.Int32().OneOf(1, 2).OneOf(3, 4)); - yield return ("Int32().MultipleOf(2).MultipleOf(3)", () => Any.Int32().MultipleOf(2).MultipleOf(3)); - yield return ("Decimal().WithScale(2).WithScale(4)", () => Any.Decimal().WithScale(2).WithScale(4)); - yield return ("Char().Alpha().Numeric()", () => Any.Char().Alpha().Numeric()); - yield return ("Uri().Web().WithPathSegments(2).WithPathSegments(3)", () => Any.Uri().Web().WithPathSegments(2).WithPathSegments(3)); - yield return ("Uri().Web().WithHost(a).WithHost(b)", () => Any.Uri().Web().WithHost("first.example").WithHost("second.example")); - yield return ("Uri().Web().WithPort(8080).WithPort(9090)", () => Any.Uri().Web().WithPort(8080).WithPort(9090)); - yield return ("Uri().Web().WithUserInfo(a).WithUserInfo(b)", () => Any.Uri().Web().WithUserInfo("alice").WithUserInfo("bob")); - yield return ("Uri().Ftp().WithPort().WithPort(21)", () => Any.Uri().Ftp().WithPort().WithPort(21)); - yield return ("Uri().Mailto().WithDomain(a).WithDomain(b)", () => Any.Uri().Mailto().WithDomain("a.example").WithDomain("b.example")); - yield return ("ListOf().WithCount(3).WithCount(4)", () => Any.ListOf(Any.Int32()).WithCount(3).WithCount(4)); - yield return ("DateTimeOffset().WithOffset(0h).WithOffset(1h)", () => Any.DateTimeOffset().WithOffset(TimeSpan.Zero).WithOffset(TimeSpan.FromHours(1))); - } - - #endregion - - [Fact(DisplayName = "Re-declaring a once-only constraint with identical arguments is a no-op, in every generator family.")] - public void IdenticalRedeclarationIsANoOp() { - // A constraint declared twice with the same argument is not a contradiction: the domain the second declaration - // asks for is exactly the one the first already produced. Refusing it made the fluent reject a specification it - // can satisfy — the one thing the eager check exists to avoid. - List refused = []; - foreach ((string label, Func redeclare) in IdenticalRedeclarations()) { - try { - redeclare(); - } catch (ConflictingAnyConstraintException) { - refused.Add(label); - } - } - - Check.WithCustomMessage($"identical re-declarations still refused: {string.Join(", ", refused)}") - .That(refused).IsEmpty(); - } - - [Fact(DisplayName = "Re-declaring a once-only constraint with a different argument is still a conflict.")] - public void ContradictoryRedeclarationStillConflicts() { - // The other half, and the reason the fix compares the rendered declaration rather than simply dropping the - // guard: tolerating an identical re-declaration must not tolerate a contradictory one. - List accepted = []; - foreach ((string label, Func contradict) in ContradictoryRedeclarations()) { - try { - contradict(); - accepted.Add(label); - } catch (ConflictingAnyConstraintException) { - // expected - } - } - - Check.WithCustomMessage($"contradictions silently accepted: {string.Join(", ", accepted)}") - .That(accepted).IsEmpty(); - } - - [Fact(DisplayName = "A second URI component pin names both sides instead of silently replacing the first.")] - public void ASecondComponentPinNamesBothSides() { - // Regression: WithHost, WithPort and WithUserInfo were plain setters — a second, different value replaced the - // first and the first vanished without a word, while WithPathSegments in the very same generator raised a - // conflict. A URI has one host, one port and one user-info, so a second declaration can never be honoured - // alongside the first; dropping it silently discards a constraint the caller wrote. - ConflictingAnyConstraintException host = Assert.Throws( - () => Any.Uri().Web().WithHost("first.example").WithHost("second.example")); - - Check.That(host.Message).Contains("WithHost(\"second.example\")"); - Check.That(host.Message).Contains("WithHost(\"first.example\")"); - - // The message names the PUBLIC call, so a mailto's WithDomain reads as WithDomain and not as the host setter - // it shares with the web families. - ConflictingAnyConstraintException domain = Assert.Throws( - () => Any.Uri().Mailto().WithDomain("a.example").WithDomain("b.example")); - - Check.That(domain.Message).Contains("WithDomain(\"b.example\")"); - Check.That(domain.Message).Not.Contains("WithHost"); - } - - [Fact(DisplayName = "A second, different Distinct comparer conflicts; the same one again is a no-op.")] - public void ASecondDistinctComparerConflicts() { - // One collection is distinct under one equality. Two different comparers cannot both be honoured, and the - // second was silently winning. - Check.ThatCode(() => Any.ListOf(Any.Int32()).Distinct(new ModuloComparer(10)).Distinct(new ModuloComparer(100))) - .Throws(); - - IEqualityComparer comparer = new ModuloComparer(10); - - Check.ThatCode(() => Any.ListOf(Any.Int32()).Distinct(comparer).Distinct(comparer)).DoesNotThrow(); - // Re-declaring distinctness without naming a comparer asks for the equality already in force. - Check.ThatCode(() => Any.ListOf(Any.Int32()).Distinct(comparer).Distinct()).DoesNotThrow(); - } - - [Fact(DisplayName = "A no-op re-declaration leaves the generator's domain untouched.")] - public void ANoOpRedeclarationDoesNotWidenTheDomain() { - // The no-op must be a no-op: returning `this` rather than rebuilding means the second declaration cannot - // loosen anything. Asserted on the observable domain, not on object identity. - for (int i = 0; i < 200; i++) { - Check.That(Any.String().WithLength(4).WithLength(4).Generate().Length).IsEqualTo(4); - Check.That(new[] { 7, 9 }).Contains(Any.Int32().OneOf(7, 9).OneOf(7, 9).Generate()); - Check.That(new[] { "a", "b" }).Contains(Any.String().OneOf("a", "b").OneOf("a", "b").Generate()); - Check.That(Any.ListOf(Any.Int32()).WithCount(3).WithCount(3).Generate().Count).IsEqualTo(3); - } - } - - #region Nested types - - private sealed class ModuloComparer : IEqualityComparer { - - private readonly int _modulus; - - public ModuloComparer(int modulus) { - _modulus = modulus; - } - - public bool Equals(int x, int y) { - return x % _modulus == y % _modulus; - } - - public int GetHashCode(int obj) { - return obj % _modulus; - } - - } - - #endregion - -} diff --git a/JustDummies.UnitTests/ContinuousExclusionNudgeTests.cs b/JustDummies.UnitTests/ContinuousExclusionNudgeTests.cs deleted file mode 100644 index fee90a4b..00000000 --- a/JustDummies.UnitTests/ContinuousExclusionNudgeTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -#region Usings declarations - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Regression coverage for issue #207. On the narrow (quantized) floating-point types an exclusion inside a tight -/// range must be honoured by the type-aware nudge — ascending or descending — instead of stalling on a sub-ulp -/// double step and exhausting the budget on a satisfiable specification. The identical scenarios on -/// guard the shared engine from the other side. -/// -public sealed class ContinuousExclusionNudgeTests { - - private const int SeedCount = 500; - - [Fact(DisplayName = "Half: an exclusion on the lower bound of a two-value range yields the surviving value for every seed.")] - public void HalfExclusionOnLowerBound() { - Half min = (Half)1f; - Half max = (Half)1.001f; // rounds to 1.0009765625: the next representable Half above 1.0 - Half survivor = max; - - for (int seed = 0; seed < SeedCount; seed++) { - Half value = Any.WithSeed(seed).Half().Between(min, max).DifferentFrom(min).Generate(); - Check.That(value == survivor).IsTrue(); - } - } - - [Fact(DisplayName = "Half: an exclusion on the upper bound descends to the surviving lower value for every seed.")] - public void HalfExclusionOnUpperBound() { - Half min = (Half)1f; - Half max = (Half)1.001f; - - for (int seed = 0; seed < SeedCount; seed++) { - Half value = Any.WithSeed(seed).Half().Between(min, max).DifferentFrom(max).Generate(); - Check.That(value == min).IsTrue(); - } - } - - [Fact(DisplayName = "Single: an exclusion inside a narrow range never yields the excluded value, either bound, for any seed.")] - public void SingleExclusionInsideNarrowRange() { - float min = 1f; - float max = MathF.BitIncrement(MathF.BitIncrement(1f)); // 1 + 2 ulp: three representable floats in range - - for (int seed = 0; seed < SeedCount; seed++) { - float lower = Any.WithSeed(seed).Single().Between(min, max).DifferentFrom(min).Generate(); - Check.That(lower).IsStrictlyGreaterThan(min); - Check.That(lower).IsLessOrEqualThan(max); - - float upper = Any.WithSeed(seed).Single().Between(min, max).DifferentFrom(max).Generate(); - Check.That(upper).IsStrictlyLessThan(max); - Check.That(upper).IsGreaterOrEqualThan(min); - } - } - - [Fact(DisplayName = "Double: an exclusion inside a narrow range never yields the excluded value, either bound, for any seed.")] - public void DoubleExclusionInsideNarrowRange() { - double min = 1d; - double max = Math.BitIncrement(Math.BitIncrement(1d)); // 1 + 2 ulp: three representable doubles in range - - for (int seed = 0; seed < SeedCount; seed++) { - double lower = Any.WithSeed(seed).Double().Between(min, max).DifferentFrom(min).Generate(); - Check.That(lower).IsStrictlyGreaterThan(min); - Check.That(lower).IsLessOrEqualThan(max); - - double upper = Any.WithSeed(seed).Double().Between(min, max).DifferentFrom(max).Generate(); - Check.That(upper).IsStrictlyLessThan(max); - Check.That(upper).IsGreaterOrEqualThan(min); - } - } - - [Fact(DisplayName = "A range whose every representable value is excluded fails with a seeded AnyGenerationException whose replay hint points at Any.WithSeed, not the inapplicable Any.Reproducibly.")] - public void ExhaustedRangeThrowsSeededGenerationException() { - Half min = (Half)1f; - Half max = (Half)1.001f; // exactly two representable Half values in [min, max] - - AnyGenerationException thrown = Assert.Throws( - () => Any.WithSeed(207).Half().Between(min, max).Except(min, max).Generate()); - - Check.That(thrown.Seed).IsEqualTo(207); - Check.That(thrown.Message).Contains("207"); - // The draw came from Any.WithSeed(207) — a fixed context that replays by itself — so the hint must name it, - // not the ambient Any.Reproducibly(...) instruction, which would not reproduce this run. - Check.That(thrown.Message).Contains("Any.WithSeed(207)"); - Check.That(thrown.Message).Not.Contains("Any.Reproducibly("); - } - - [Fact(DisplayName = "An exhausted nudge reports a local search, never a claim that the range holds no free value.")] - public void ExhaustedNudgeDoesNotClaimAnEmptyRange() { - // A range of 401 representable doubles whose 399 interior values are excluded: both bounds survive, so the - // range plainly holds free values. They sit further than the 128-step budget from a draw landing mid-range, - // so both walks give up — and the inner exception used to assert "No representable value in range remains - // after applying the exclusions", which nothing had established and which is false here. Seed 5 lands in that - // band on the first draw, so the case is pinned rather than statistical. - double min = 1d; - double max = 1d; - for (int step = 0; step < 400; step++) { max = Math.BitIncrement(max); } - - List excluded = []; - double value = Math.BitIncrement(min); - for (int step = 0; step < 399; step++) { - excluded.Add(value); - value = Math.BitIncrement(value); - } - - AnyGenerationException thrown = Assert.Throws( - () => Any.WithSeed(5).Double().Between(min, max).Except(excluded.ToArray()).Generate()); - - // The two bounds are free: the range is satisfiable, so any claim that it is empty would be a falsehood. - Check.That(excluded).Not.Contains(min); - Check.That(excluded).Not.Contains(max); - - string inner = thrown.InnerException!.Message; - Check.That(inner).Contains("128 steps"); - Check.That(inner).Contains("not examined"); - Check.That(inner).Not.Contains("No representable value in range remains"); - } - - [Fact(DisplayName = "The nudge stays reproducible: the same seed yields the same value across runs.")] - public void NudgeIsReproducibleForAGivenSeed() { - double min = 1d; - double max = Math.BitIncrement(Math.BitIncrement(1d)); - - for (int seed = 0; seed < 50; seed++) { - double first = Any.WithSeed(seed).Double().Between(min, max).DifferentFrom(min).Generate(); - double second = Any.WithSeed(seed).Double().Between(min, max).DifferentFrom(min).Generate(); - Check.That(second).IsEqualTo(first); - } - } - -} diff --git a/JustDummies.UnitTests/CrossEngineReachabilityTests.cs b/JustDummies.UnitTests/CrossEngineReachabilityTests.cs deleted file mode 100644 index cd2aaf4e..00000000 --- a/JustDummies.UnitTests/CrossEngineReachabilityTests.cs +++ /dev/null @@ -1,436 +0,0 @@ -#region Usings declarations - -using System.Diagnostics.CodeAnalysis; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// A single scenario battery run against every interval-backed builder through a small per-type adapter, -/// so an engine-level regression cannot hide in the one type-facade a hand-written test happens not to cover. -/// Adding a builder means adding one row to , not a new file. -/// -/// -/// -/// The suite closes the reachability blind spot the 2026-07-20 JustDummies architecture audit named (§9.2/§9.3, -/// issue #213): the existing tests assert membership (a generated value satisfies its constraints) but -/// never reachability (the whole declared domain is actually generable). Two shipped defects survived -/// that gap — never reaching the upper half of a range (#206) and the -/// AnySingle/AnyHalf exclusion nudge stalling on satisfiable specs (#207). Both are guarded here -/// structurally, across every engine at once, in addition to their dedicated regressions -/// ( and -/// , kept as focused, commented guards). -/// -/// -/// Every scenario is deterministic: one fixed seed, a fixed draw count large enough that a correct uniform -/// generator reaches the asserted region with overwhelming probability while a stuck one (half a range -/// unreachable) fails every time. No randomness leaks in, so the suite is a stable CI guard, never a flaky one. -/// -/// -[SuppressMessage("Blocker Code Smell", "S2699:Tests should include assertions", - Justification = - "Each theory is one line of dispatch to the per-type adapter; the NFluent Check.That and " + - "Assert.Throws calls live in the IntervalCase overrides. The rule does follow assertions into " + - "concrete helpers, but this call resolves statically to the abstract ReachabilityCase declaration, " + - "which has no body, so it cannot see past the virtual dispatch. Lifting the assertions into the " + - "test bodies would flatten the one-row-per-builder design and reduce the per-draw scenarios to a " + - "single aggregated boolean.")] -public sealed class CrossEngineReachabilityTests { - - #region Statics members declarations - - // Three consecutive representable values of each floating type: a tight domain where excluding one endpoint - // must still generate by nudging along the type's own ladder — the exact shape that stalled #207 on the - // quantized types. Computed once via the type-aware bit step so the values are genuinely adjacent. - private static readonly double DLo = 1.0d; - private static readonly double DMid = Math.BitIncrement(1.0d); - private static readonly double DHi = Math.BitIncrement(Math.BitIncrement(1.0d)); - - private static readonly float FLo = 1.0f; - private static readonly float FMid = MathF.BitIncrement(1.0f); - private static readonly float FHi = MathF.BitIncrement(MathF.BitIncrement(1.0f)); - - private static readonly Half HLo = (Half)1; - private static readonly Half HMid = NextHalf((Half)1); - private static readonly Half HHi = NextHalf(NextHalf((Half)1)); - - // Temporal anchors — the low end of each narrow (few-ordinal) domain and of each wide range. - private static readonly DateTime DtAnchor = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static readonly DateTimeOffset DtoAnchor = new(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); - private static readonly DateOnly DoAnchor = new(2000, 1, 1); - private static readonly TimeOnly ToAnchor = new(9, 0, 0); - - private static readonly Int128 WideInt128 = (Int128)ulong.MaxValue * 10; - private static readonly UInt128 WideUInt128 = (UInt128)ulong.MaxValue * 10; - - /// - /// One adapter per interval-backed builder — every discrete integer, the two 128-bit integers, the three - /// binary-floating types, , and the five temporal types. The non-interval scalars - /// (bool, guid, string, enum, char) are deliberately absent: they expose no user-bounded ordered range, so - /// "both halves / both endpoints" is undefined for them. - /// - private static readonly ReachabilityCase[] AllCases = { - Case("SByte", true, - c => c.SByte(), - (c, lo, hi) => c.SByte().Between(lo, hi), - (c, lo, hi, x) => c.SByte().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.SByte().OneOf(allow).Except(except), - v => v, sbyte.MinValue, sbyte.MaxValue, -100, 100, 1, 2, 3), - Case("Int16", true, - c => c.Int16(), - (c, lo, hi) => c.Int16().Between(lo, hi), - (c, lo, hi, x) => c.Int16().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Int16().OneOf(allow).Except(except), - v => v, short.MinValue, short.MaxValue, -30000, 30000, 1, 2, 3), - Case("Int32", true, - c => c.Int32(), - (c, lo, hi) => c.Int32().Between(lo, hi), - (c, lo, hi, x) => c.Int32().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Int32().OneOf(allow).Except(except), - v => v, int.MinValue, int.MaxValue, -1_000_000, 1_000_000, 1, 2, 3), - Case("Int64", true, - c => c.Int64(), - (c, lo, hi) => c.Int64().Between(lo, hi), - (c, lo, hi, x) => c.Int64().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Int64().OneOf(allow).Except(except), - v => v, long.MinValue, long.MaxValue, -1_000_000_000_000L, 1_000_000_000_000L, 1L, 2L, 3L), - Case("Byte", true, - c => c.Byte(), - (c, lo, hi) => c.Byte().Between(lo, hi), - (c, lo, hi, x) => c.Byte().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Byte().OneOf(allow).Except(except), - v => v, byte.MinValue, byte.MaxValue, 10, 240, 100, 101, 102), - Case("UInt16", true, - c => c.UInt16(), - (c, lo, hi) => c.UInt16().Between(lo, hi), - (c, lo, hi, x) => c.UInt16().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.UInt16().OneOf(allow).Except(except), - v => v, ushort.MinValue, ushort.MaxValue, 100, 60000, 1000, 1001, 1002), - Case("UInt32", true, - c => c.UInt32(), - (c, lo, hi) => c.UInt32().Between(lo, hi), - (c, lo, hi, x) => c.UInt32().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.UInt32().OneOf(allow).Except(except), - v => v, uint.MinValue, uint.MaxValue, 1000u, 4_000_000_000u, 100_000u, 100_001u, 100_002u), - Case("UInt64", true, - c => c.UInt64(), - (c, lo, hi) => c.UInt64().Between(lo, hi), - (c, lo, hi, x) => c.UInt64().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.UInt64().OneOf(allow).Except(except), - v => v, ulong.MinValue, ulong.MaxValue, 1000ul, 10_000_000_000_000_000_000ul, 1_000_000ul, 1_000_001ul, 1_000_002ul), - Case("Int128", true, - c => c.Int128(), - (c, lo, hi) => c.Int128().Between(lo, hi), - (c, lo, hi, x) => c.Int128().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Int128().OneOf(allow).Except(except), - v => (double)v, Int128.MinValue, Int128.MaxValue, -WideInt128, WideInt128, Int128.Zero, Int128.One, (Int128)2), - Case("UInt128", true, - c => c.UInt128(), - (c, lo, hi) => c.UInt128().Between(lo, hi), - (c, lo, hi, x) => c.UInt128().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.UInt128().OneOf(allow).Except(except), - v => (double)v, UInt128.MinValue, UInt128.MaxValue, UInt128.Zero, WideUInt128, UInt128.Zero, UInt128.One, (UInt128)2), - Case("Double", false, - c => c.Double(), - (c, lo, hi) => c.Double().Between(lo, hi), - (c, lo, hi, x) => c.Double().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Double().OneOf(allow).Except(except), - v => v, double.MinValue, double.MaxValue, -1_000_000d, 1_000_000d, DLo, DMid, DHi), - Case("Single", false, - c => c.Single(), - (c, lo, hi) => c.Single().Between(lo, hi), - (c, lo, hi, x) => c.Single().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Single().OneOf(allow).Except(except), - v => v, float.MinValue, float.MaxValue, -100_000f, 100_000f, FLo, FMid, FHi), - Case("Half", false, - c => c.Half(), - (c, lo, hi) => c.Half().Between(lo, hi), - (c, lo, hi, x) => c.Half().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Half().OneOf(allow).Except(except), - v => (double)v, Half.MinValue, Half.MaxValue, (Half)(-1000), (Half)1000, HLo, HMid, HHi), - Case("Decimal", false, - c => c.Decimal(), - (c, lo, hi) => c.Decimal().Between(lo, hi), - (c, lo, hi, x) => c.Decimal().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.Decimal().OneOf(allow).Except(except), - v => (double)v, decimal.MinValue, decimal.MaxValue, 0m, 1_000_000m, 1m, 2m, 3m), - Case("DateTime", true, - c => c.DateTime(), - (c, lo, hi) => c.DateTime().Between(lo, hi), - (c, lo, hi, x) => c.DateTime().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.DateTime().OneOf(allow).Except(except), - v => v.Ticks, DateTime.MinValue, DateTime.MaxValue, - DtAnchor, new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc), DtAnchor, DtAnchor.AddTicks(1), DtAnchor.AddTicks(2)), - Case("DateTimeOffset", true, - c => c.DateTimeOffset(), - (c, lo, hi) => c.DateTimeOffset().Between(lo, hi), - (c, lo, hi, x) => c.DateTimeOffset().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.DateTimeOffset().OneOf(allow).Except(except), - v => v.UtcTicks, DateTimeOffset.MinValue, DateTimeOffset.MaxValue, - DtoAnchor, new DateTimeOffset(2100, 1, 1, 0, 0, 0, TimeSpan.Zero), DtoAnchor, DtoAnchor.AddTicks(1), DtoAnchor.AddTicks(2)), - Case("DateOnly", true, - c => c.DateOnly(), - (c, lo, hi) => c.DateOnly().Between(lo, hi), - (c, lo, hi, x) => c.DateOnly().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.DateOnly().OneOf(allow).Except(except), - v => v.DayNumber, DateOnly.MinValue, DateOnly.MaxValue, - DoAnchor, new DateOnly(2100, 1, 1), DoAnchor, DoAnchor.AddDays(1), DoAnchor.AddDays(2)), - Case("TimeOnly", true, - c => c.TimeOnly(), - (c, lo, hi) => c.TimeOnly().Between(lo, hi), - (c, lo, hi, x) => c.TimeOnly().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.TimeOnly().OneOf(allow).Except(except), - v => v.Ticks, TimeOnly.MinValue, TimeOnly.MaxValue, - new TimeOnly(0, 0, 0), new TimeOnly(23, 59, 59), ToAnchor, ToAnchor.Add(TimeSpan.FromTicks(1)), ToAnchor.Add(TimeSpan.FromTicks(2))), - Case("TimeSpan", true, - c => c.TimeSpan(), - (c, lo, hi) => c.TimeSpan().Between(lo, hi), - (c, lo, hi, x) => c.TimeSpan().Between(lo, hi).DifferentFrom(x), - (c, allow, except) => c.TimeSpan().OneOf(allow).Except(except), - v => v.Ticks, TimeSpan.MinValue, TimeSpan.MaxValue, - TimeSpan.FromHours(-1), TimeSpan.FromHours(1), TimeSpan.Zero, TimeSpan.FromTicks(1), TimeSpan.FromTicks(2)), - }; - - private static readonly IReadOnlyDictionary ByName = AllCases.ToDictionary(one => one.Name); - - /// The builder names, one theory row each — a serializable key so every builder is an isolated test. - public static TheoryData CaseNames() { - TheoryData data = []; - foreach (ReachabilityCase one in AllCases) { data.Add(one.Name); } - - return data; - } - - private static IntervalCase Case(string name, bool exact, - Func> full, - Func> between, - Func> betweenDifferentFrom, - Func> oneOfExcept, - Func scale, - T domainMin, T domainMax, T wideLo, T wideHi, T na, T nb, T nc) { - return new IntervalCase(name, exact, full, between, betweenDifferentFrom, oneOfExcept, scale, - domainMin, domainMax, wideLo, wideHi, na, nb, nc); - } - - private static Half NextHalf(Half value) { - return BitConverter.Int16BitsToHalf((short)(BitConverter.HalfToInt16Bits(value) + 1)); - } - - #endregion - - [Theory(DisplayName = "Full range: an unconstrained generator reaches both halves of its domain.")] - [MemberData(nameof(CaseNames))] - public void FullRangeReachesBothHalvesOfTheDomain(string builder) { - ByName[builder].FullRangeReachesBothHalvesOfTheDomain(); - } - - [Theory(DisplayName = "Between: a wide range reaches both halves — neither half is silently unreachable.")] - [MemberData(nameof(CaseNames))] - public void WideBetweenReachesBothHalves(string builder) { - ByName[builder].WideBetweenReachesBothHalves(); - } - - [Theory(DisplayName = "Between: both inclusive bounds are reachable (exactly for discrete types, to within 1% for continuous ones).")] - [MemberData(nameof(CaseNames))] - public void BothInclusiveBoundsAreReachable(string builder) { - ByName[builder].BothInclusiveBoundsAreReachable(); - } - - [Theory(DisplayName = "Exclusion: a point excluded from a narrow range still generates and is never returned.")] - [MemberData(nameof(CaseNames))] - public void NarrowExclusionStillGenerates(string builder) { - ByName[builder].NarrowExclusionStillGenerates(); - } - - [Theory(DisplayName = "OneOf then Except: only the surviving allowed values are generated.")] - [MemberData(nameof(CaseNames))] - public void OneOfThenExceptYieldsOnlyTheSurvivors(string builder) { - ByName[builder].OneOfThenExceptYieldsOnlyTheSurvivors(); - } - - [Theory(DisplayName = "Conflict: contradictory constraints throw naming both sides.")] - [MemberData(nameof(CaseNames))] - public void ContradictoryConstraintsNameBothSides(string builder) { - ByName[builder].ContradictoryConstraintsNameBothSides(); - } - -} - -/// One interval-backed builder's participation in the shared scenario battery — the per-type adapter. -internal abstract class ReachabilityCase { - - protected ReachabilityCase(string name) { - Name = name; - } - - public string Name { get; } - - public sealed override string ToString() { - return Name; - } - - public abstract void FullRangeReachesBothHalvesOfTheDomain(); - - public abstract void WideBetweenReachesBothHalves(); - - public abstract void BothInclusiveBoundsAreReachable(); - - public abstract void NarrowExclusionStillGenerates(); - - public abstract void OneOfThenExceptYieldsOnlyTheSurvivors(); - - public abstract void ContradictoryConstraintsNameBothSides(); - -} - -/// -/// The generic adapter carrying a builder's value type , its wide and narrow test -/// domains, whether its endpoints are exactly reachable, a monotone projection onto for -/// half/bound detection, and the four uniformly-named chains the scenarios drive (Between, -/// Between + DifferentFrom, OneOf + Except, and the unconstrained builder). -/// -internal sealed class IntervalCase : ReachabilityCase { - - private const int Seed = 20260721; - private const int DistributionSamples = 4000; - private const int SetSamples = 200; - - #region Fields declarations - - private readonly Func> _betweenDifferentFrom; - private readonly Func> _between; - private readonly T _domainMax; - private readonly T _domainMin; - private readonly bool _endpointsExact; - private readonly Func> _full; - private readonly T _na; - private readonly T _nb; - private readonly T _nc; - private readonly Func> _oneOfExcept; - private readonly Func _scale; - private readonly T _wideHi; - private readonly T _wideLo; - - #endregion - - public IntervalCase(string name, bool endpointsExact, - Func> full, - Func> between, - Func> betweenDifferentFrom, - Func> oneOfExcept, - Func scale, - T domainMin, T domainMax, T wideLo, T wideHi, T na, T nb, T nc) - : base(name) { - _endpointsExact = endpointsExact; - _full = full; - _between = between; - _betweenDifferentFrom = betweenDifferentFrom; - _oneOfExcept = oneOfExcept; - _scale = scale; - _domainMin = domainMin; - _domainMax = domainMax; - _wideLo = wideLo; - _wideHi = wideHi; - _na = na; - _nb = nb; - _nc = nc; - } - - public override void FullRangeReachesBothHalvesOfTheDomain() { - (double min, double max, _) = Sample(_full(Any.WithSeed(Seed)), DistributionSamples); - double mid = _scale(_domainMin) / 2 + _scale(_domainMax) / 2; - - Check.That(min).IsStrictlyLessThan(mid); // the lower half of the domain is reached - Check.That(max).IsStrictlyGreaterThan(mid); // and so is the upper half - } - - public override void WideBetweenReachesBothHalves() { - (double min, double max, _) = Sample(_between(Any.WithSeed(Seed), _wideLo, _wideHi), DistributionSamples); - double lo = _scale(_wideLo); - double hi = _scale(_wideHi); - double mid = lo / 2 + hi / 2; - - Check.That(min).IsGreaterOrEqualThan(lo); // draws stay in range - Check.That(max).IsLessOrEqualThan(hi); - Check.That(min).IsStrictlyLessThan(mid); // the lower half is reached - Check.That(max).IsStrictlyGreaterThan(mid); // the upper half is reached — the #206 class of defect - } - - public override void BothInclusiveBoundsAreReachable() { - if (_endpointsExact) { - // Discrete types: a narrow three-value range must hand back both of its exact endpoints. - (_, _, HashSet seen) = Sample(_between(Any.WithSeed(Seed), _na, _nc), DistributionSamples); - Check.That(seen.Contains(_na)).IsTrue(); - Check.That(seen.Contains(_nc)).IsTrue(); - - return; - } - - // Continuous types: exact endpoints are a measure-zero target, so a draw must instead come within 1% of - // each inclusive bound of a wide range. A generator stuck below the midpoint (#206) never gets near the top. - (double min, double max, _) = Sample(_between(Any.WithSeed(Seed), _wideLo, _wideHi), DistributionSamples); - double lo = _scale(_wideLo); - double hi = _scale(_wideHi); - double range = hi - lo; - - Check.That(max).IsStrictlyGreaterThan(hi - range * 0.01d); - Check.That(min).IsStrictlyLessThan(lo + range * 0.01d); - } - - public override void NarrowExclusionStillGenerates() { - // Between(na, nc).DifferentFrom(na): on the quantized floating types this drives the type-aware nudge that - // stalled in #207; on the discrete types it drives the ordinal exclusion. Either way it must generate. - IAny generator = _betweenDifferentFrom(Any.WithSeed(Seed), _na, _nc, _na); - EqualityComparer equals = EqualityComparer.Default; - double lo = _scale(_na); - double hi = _scale(_nc); - - for (int i = 0; i < SetSamples; i++) { - T value = generator.Generate(); - Check.That(equals.Equals(value, _na)).IsFalse(); - Check.That(_scale(value)).IsGreaterOrEqualThan(lo); - Check.That(_scale(value)).IsLessOrEqualThan(hi); - } - } - - public override void OneOfThenExceptYieldsOnlyTheSurvivors() { - IAny generator = _oneOfExcept(Any.WithSeed(Seed), [_na, _nb, _nc], [_nb]); - EqualityComparer equals = EqualityComparer.Default; - - for (int i = 0; i < SetSamples; i++) { - T value = generator.Generate(); - Check.That(equals.Equals(value, _nb)).IsFalse(); - Check.That(equals.Equals(value, _na) || equals.Equals(value, _nc)).IsTrue(); - } - } - - public override void ContradictoryConstraintsNameBothSides() { - // OneOf(na).Except(na) empties the allow-list; the message must name both the allow-list and the exclusion. - // Asserting the method-name tokens keeps this independent of how each type renders its values. - ConflictingAnyConstraintException conflict = Assert.Throws( - () => _oneOfExcept(Any.WithSeed(Seed), [_na], [_na])); - - Check.That(conflict.Message).Contains("OneOf("); - Check.That(conflict.Message).Contains("Except("); - } - - private (double Min, double Max, HashSet Seen) Sample(IAny generator, int count) { - double min = double.PositiveInfinity; - double max = double.NegativeInfinity; - HashSet seen = []; - - for (int i = 0; i < count; i++) { - T value = generator.Generate(); - double scaled = _scale(value); - seen.Add(value); - if (scaled < min) { min = scaled; } - if (scaled > max) { max = scaled; } - } - - return (min, max, seen); - } - -} diff --git a/JustDummies.UnitTests/FactoryNamingConventionTests.cs b/JustDummies.UnitTests/FactoryNamingConventionTests.cs deleted file mode 100644 index 61c1b241..00000000 --- a/JustDummies.UnitTests/FactoryNamingConventionTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region Usings declarations - -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Locks the factory-naming rule recorded in ADR-0031: every parameterless, type-named scalar factory -/// on is named after the CLR type it produces — which is also the name of its -/// Any{ClrType} builder. This is the guard that would have caught the Bool/AnyBool -/// deviation before release. The mirror itself is guarded -/// separately by SurfaceParityTests. -/// -public sealed class FactoryNamingConventionTests { - - // The type-named scalar factories are exactly Any's public, static, non-generic, parameterless methods - // whose return type is a builder (implements IAny). StringMatching (parameters), Enum (generic), - // the collection/composition factories (generic, parameterized) and WithSeed/Reproducibly (not builders) - // fall out by construction, so no hand-maintained allow-list can drift out of sync with the surface. - private static IEnumerable ScalarFactories() { - return typeof(Any).GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(method => !method.IsGenericMethod - && method.GetParameters().Length == 0 - && ElementTypeOf(method.ReturnType) is not null); - } - - // The T of the single IAny a builder implements, or null when the type is not a builder. - private static Type? ElementTypeOf(Type builder) { - return builder.GetInterfaces() - .FirstOrDefault(candidate => candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(IAny<>)) - ?.GetGenericArguments()[0]; - } - - [Fact(DisplayName = "Every type-named scalar factory, and its builder, is named after the CLR type it produces.")] - public void FactoriesAreNamedAfterTheirClrType() { - List factories = ScalarFactories().ToList(); - - // Guards the reflection itself: were the query ever to match nothing, every assertion below would pass vacuously. - Check.That(factories.Count).IsStrictlyGreaterThan(15); - - foreach (MethodInfo factory in factories) { - Type builder = factory.ReturnType; - string clrName = ElementTypeOf(builder)!.Name; - - Check.WithCustomMessage($"Any.{factory.Name}() returns {builder.Name} (IAny<{clrName}>); the factory must be named '{clrName}', after the CLR type it produces.") - .That(factory.Name).IsEqualTo(clrName); - Check.WithCustomMessage($"The builder for {clrName} is named '{builder.Name}'; it must be 'Any{clrName}' to match the CLR type.") - .That(builder.Name).IsEqualTo("Any" + clrName); - } - } - -} diff --git a/JustDummies.UnitTests/JustDummies.UnitTests.csproj b/JustDummies.UnitTests/JustDummies.UnitTests.csproj deleted file mode 100644 index d957a576..00000000 --- a/JustDummies.UnitTests/JustDummies.UnitTests.csproj +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - enable - enable - false - - - $(NoWarn);CA1510 - - - $(NoWarn);SYSLIB1045 - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - <_Parameter1>RepositoryRoot - <_Parameter2>$(MSBuildThisFileDirectory).. - - - - - - - - - - - - - - - - - diff --git a/JustDummies.UnitTests/MaterializationTests.cs b/JustDummies.UnitTests/MaterializationTests.cs deleted file mode 100644 index d5fcc700..00000000 --- a/JustDummies.UnitTests/MaterializationTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -[TestSubject(typeof(Any))] -public sealed class MaterializationTests { - - #region Statics members declarations - - private static T Materialize(IAny generator) { - return generator.Generate(); - } - - #endregion - - [Fact(DisplayName = "Generate materializes a valid string value.")] - public void GenerateMaterializesAString() { - string value = Any.String().NonEmpty().Generate(); - - Check.That(value).IsNotEmpty(); - } - - [Fact(DisplayName = "Generate materializes a valid int value.")] - public void GenerateMaterializesAnInt() { - int value = Any.Int32().Positive().Generate(); - - Check.That(value).IsStrictlyGreaterThan(0); - } - - [Fact(DisplayName = "A materialized value flows into a method expecting the generated type.")] - public void GeneratedValueFlowsIntoACallSite() { - static int Measure(string text) { - return text.Length; - } - - int length = Measure(Any.String().WithLength(9).Generate()); - - Check.That(length).IsEqualTo(9); - } - - [Fact(DisplayName = "Each Generate call draws a fresh value.")] - public void EachGenerateDrawsAFreshValue() { - AnyInt32 generator = Any.Int32().Between(0, int.MaxValue); - - HashSet seen = []; - for (int i = 0; i < 20; i++) { - seen.Add(generator.Generate()); - } - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - - [Fact(DisplayName = "Generic inference flows through IAny, materializing without any implicit conversion.")] - public void GenericInferenceMaterializesThroughIAny() { - string text = Materialize(Any.String().NonEmpty()); - int value = Materialize(Any.Int32().Positive()); - - Check.That(text).IsNotEmpty(); - Check.That(value).IsStrictlyGreaterThan(0); - } - - [Fact(DisplayName = "Building a generator draws nothing at all, which is why a chain left unmaterialized is silent.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Usage", "JD006:The generator returned by a constraint is discarded", - Justification = - "The discarded generator IS the subject. This pins the behaviour JD006 reports: the arrange line reads like it did something, " + - "and drew nothing.")] - public void BuildingAGeneratorDrawsNothing() { - int draws = 0; - - Any.Int32().As(value => { - draws++; - - return value; - }); - - Check.That(draws).IsEqualTo(0); - } - - [Fact(DisplayName = "A generator interpolated into text renders its type name, never a value it could draw.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("JustDummies.Usage", "JD005:A generator is rendered as text instead of the value it would draw", - Justification = - "The rendered generator IS the subject. This pins the behaviour JD005 reports, and the deliberate absence of a ToString override " + - "that would mask it — an override returning a drawn value would make this test red, which is the point.")] - public void AGeneratorRendersAsItsTypeName() { - string rendered = $"{Any.Int32()}"; - - Check.That(rendered).IsEqualTo(typeof(AnyInt32).ToString()); - Check.That(int.TryParse(rendered, out int _)).IsFalse(); - } - -} diff --git a/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs b/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs deleted file mode 100644 index 3d6bb6d0..00000000 --- a/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs +++ /dev/null @@ -1,519 +0,0 @@ -#region Usings declarations - -using System.Collections; -using System.Linq.Expressions; -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The null-argument guard convention, enforced by reflection over the whole library surface: every -/// public or internal member (constructor or method) that takes a non-nullable reference-type -/// argument must reject null with an naming the offending parameter. -/// A caller — even another class of this assembly — is outside the class it calls, so the boundary validates -/// what crosses it; only what a validating member has already accepted is trusted inside. -/// -/// -/// -/// This is a single self-maintaining guard: it discovers members through reflection, so a new generator, -/// factory, or fluent method is held to the convention automatically, with nothing to add here. Value-type -/// and nullable (?) parameters are excluded by design — the former cannot be null, the latter -/// are deliberately optional. Exception types are excluded too: constructing an exception must never itself -/// throw while an error is being handled or logged — as are the types that exist only to build one, which -/// declare themselves with [BuiltOnTheFailurePath] (ADR-0064). -/// -/// -/// Testing the internal boundary (the Create factories and internal constructors the public API can -/// never route a null through) requires reaching internals, which is why JustDummies opens them to -/// this suite — see ADR-0045. The test uses .NET 6+ reflection nullability metadata, so it runs on the -/// modern leg only and is excluded from the net472 support-floor build. -/// -/// -public sealed class NullArgumentGuardConventionTests { - - private static readonly Assembly LibraryAssembly = typeof(Any).Assembly; - private static readonly NullabilityInfoContext Nullability = new(); - private static readonly List Samples = HarvestSamples(); - - [Fact(DisplayName = "Every public/internal member rejects a null non-nullable reference argument with ArgumentNullException.")] - public void EveryPublicOrInternalMemberGuardsItsNonNullableReferenceArguments() { - List violations = []; - List uncovered = []; - - foreach (Type declared in LibraryAssembly.GetTypes()) { - if (!IsInScope(declared)) { continue; } - - Type type = declared; - if (declared.IsGenericTypeDefinition) { - if (declared.IsAbstract) { continue; } // an abstract base (e.g. AnyCollection`3): covered through its concrete subclasses - if (!TryClose(declared, out type!)) { - uncovered.Add($"type {declared.Name}: could not close generic definition"); - continue; - } - } - - foreach (MemberDescriptor member in MembersOf(type)) { - ProcessMember(member, violations, uncovered); - } - } - - // A parameter the harness could not exercise is a hole in the convention's coverage, not a pass: it is - // reported alongside the missing guards so it is either given a sample here or covered by an explicit test, - // never silently skipped. - List failures = uncovered.Select(entry => $"[uncovered] {entry}") - .Concat(violations.Select(entry => $"[missing-guard] {entry}")) - .OrderBy(line => line, StringComparer.Ordinal) - .ToList(); - - Check.WithCustomMessage( - $"Null-argument guard convention — {violations.Count} member(s) missing a guard, " - + $"{uncovered.Count} parameter(s) the harness could not exercise:{Environment.NewLine}" - + string.Join(Environment.NewLine, failures)) - .That(failures) - .IsEmpty(); - } - - #region Member enumeration - - private readonly struct MemberDescriptor(MethodBase invoke, MethodBase classify, Type receiver) { - - public MethodBase Invoke { get; } = invoke; // closed and callable - public MethodBase Classify { get; } = classify; // open enough to read annotations and generic constraints - public Type Receiver { get; } = receiver; // the (closed) type an instance member is called on - - } - - private static IEnumerable MembersOf(Type type) { - return ConstructorsOf(type).Concat(MethodsOf(type)); - } - - private static IEnumerable ConstructorsOf(Type type) { - if (type is { IsAbstract: true } or { IsInterface: true }) { yield break; } - - foreach (ConstructorInfo ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - if (IsAccessible(ctor)) { yield return new MemberDescriptor(ctor, ctor, type); } - } - } - - private static IEnumerable MethodsOf(Type type) { - BindingFlags methodFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; - if (type.IsAbstract) { methodFlags |= BindingFlags.DeclaredOnly; } // instance methods of an abstract base are reached through its concrete subclasses - - foreach (MethodInfo method in type.GetMethods(methodFlags)) { - if (!IsReachableMethod(method)) { continue; } - - if (method.IsGenericMethodDefinition) { - if (!TryClose(method, out MethodInfo? closed)) { continue; } - yield return new MemberDescriptor(closed!, method, type); - } else { - yield return new MemberDescriptor(method, method, type); - } - } - } - - // The methods the harness can actually call: not object's own, not an accessor or operator, not compiler-generated, - // accessible, and not abstract. - private static bool IsReachableMethod(MethodInfo method) { - if (method.DeclaringType == typeof(object)) { return false; } - if (method.IsSpecialName) { return false; } // property/event accessors, operators - if (method.Name.Contains('<')) { return false; } // compiler-generated - if (!IsAccessible(method)) { return false; } - - return method is not { IsAbstract: true }; // no body to reach directly - } - - private static bool IsInScope(Type type) { - if (type.IsInterface || type.IsEnum) { return false; } - if (typeof(Delegate).IsAssignableFrom(type)) { return false; } - if (typeof(Exception).IsAssignableFrom(type)) { return false; } - // Types that exist only to build one of the library's exceptions are exempt for the same reason exception - // types are, and say so with the marker rather than being inferred (ADR-0064, widening ADR-0045). - if (type.GetCustomAttribute() is not null) { return false; } - if (type.Name.StartsWith('<')) { return false; } - if (type.GetCustomAttribute() is not null) { return false; } - - if (type.IsNested) { - return type.IsNestedPublic || type.IsNestedAssembly || type.IsNestedFamORAssem; - } - - return type.IsPublic || type.IsNotPublic; // top-level public or internal - } - - private static bool IsAccessible(MethodBase member) { - return member.IsPublic || member.IsAssembly || member.IsFamilyOrAssembly; // public / internal / protected internal - } - - #endregion - - #region Per-member verification - - private static void ProcessMember(MemberDescriptor member, List violations, List uncovered) { - ParameterInfo[] invokeParams = member.Invoke.GetParameters(); - ParameterInfo[] classifyParams = ClassificationParameters(member); - - // A validation helper that carries a caller-supplied name (RequireHost(host, parameterName)) reports that - // forwarded name, not its own — so for such members any ArgumentNullException, whatever its ParamName, honours - // the convention. Whether an exception is thrown at all is still checked strictly. - bool forwardsName = invokeParams.Any(parameter => parameter.Name is "parameterName" or "paramName"); - - for (int index = 0; index < invokeParams.Length; index++) { - ParameterInfo classify = classifyParams[index]; - if (!IsGuardTarget(classify)) { continue; } - - VerifyParameterGuard(member, invokeParams, index, classify, forwardsName, violations, uncovered); - } - } - - // One parameter, exercised on its own: every other argument is supplied, so whatever the member throws is a - // verdict about THIS parameter's guard and nothing else. - private static void VerifyParameterGuard(MemberDescriptor member, ParameterInfo[] invokeParams, int index, ParameterInfo classify, - bool forwardsName, List violations, List uncovered) { - string description = Describe(member, classify); - - if (!TryBuildInvocation(member, invokeParams, index, out object? instance, out object?[] arguments, out string? failure)) { - uncovered.Add($"{description}: could not build arguments — {failure}"); - - return; - } - - try { - if (member.Invoke is ConstructorInfo ctor) { ctor.Invoke(arguments); } else { member.Invoke.Invoke(instance, arguments); } - violations.Add($"{description}: expected ArgumentNullException, nothing was thrown"); - } catch (TargetInvocationException invocation) { - Exception? thrown = invocation.InnerException; - if (thrown is ArgumentNullException guard && (guard.ParamName == classify.Name || forwardsName)) { return; } - - string got = thrown is ArgumentNullException other - ? $"ArgumentNullException(ParamName=\"{other.ParamName}\")" - : thrown?.GetType().Name ?? "null"; - violations.Add($"{description}: expected ArgumentNullException(\"{classify.Name}\") but got {got}"); - } catch (Exception unexpected) { - uncovered.Add($"{description}: invocation failed — {Root(unexpected).GetType().Name}: {Root(unexpected).Message}"); - } - } - - // The receiver and the argument array to invoke with, `index` left null. Answers false when the harness has no - // sample for some other parameter — which is a coverage hole to report, not a missing guard. - private static bool TryBuildInvocation(MemberDescriptor member, ParameterInfo[] invokeParams, int index, - out object? instance, out object?[] arguments, out string? failure) { - try { - instance = member.Invoke.IsStatic || member.Invoke is ConstructorInfo ? null : Sample(member.Receiver); - arguments = new object?[invokeParams.Length]; - for (int j = 0; j < invokeParams.Length; j++) { - arguments[j] = j == index ? null : ArgumentFor(invokeParams[j]); - } - - failure = null; - - return true; - } catch (Exception build) { - instance = null; - arguments = []; - failure = Root(build).Message; - - return false; - } - } - - // Parameters to read annotations and generic constraints from: for a generic method, the open definition (which - // still carries `T` and its constraints); for a member of a constructed generic type, the definition member - // (whose parameters carry the nullable annotations the constructed copy elides). - private static ParameterInfo[] ClassificationParameters(MemberDescriptor member) { - if (member.Classify is MethodInfo { IsGenericMethodDefinition: true } definition) { return definition.GetParameters(); } - - Type? declaring = member.Invoke.DeclaringType; - if (declaring is { IsGenericType: true, IsGenericTypeDefinition: false }) { - try { - Type openDeclaring = declaring.GetGenericTypeDefinition(); - if (openDeclaring.Module.ResolveMember(member.Invoke.MetadataToken) is MethodBase open) { return open.GetParameters(); } - } catch { - // fall through to the constructed parameters - } - } - - return member.Invoke.GetParameters(); - } - - private static bool IsGuardTarget(ParameterInfo parameter) { - Type type = parameter.ParameterType; - if (type.IsByRef) { return false; } // out/ref/in - - if (type.IsGenericParameter) { - return (type.GenericParameterAttributes & GenericParameterAttributes.ReferenceTypeConstraint) != 0; - } - - if (type.IsValueType || type.IsPointer) { return false; } // includes Nullable, enums, structs - - try { - return Nullability.Create(parameter).ReadState == NullabilityState.NotNull; - } catch { - return true; // unknown annotation → treat as a target so any gap shows up as a visible violation - } - } - - private static string Describe(MemberDescriptor member, ParameterInfo parameter) { - string kind = member.Invoke is ConstructorInfo ? "ctor" : member.Invoke.Name; - string signature = string.Join(", ", member.Invoke.GetParameters().Select(p => $"{Readable(p.ParameterType)} {p.Name}")); - - return $"{Readable(member.Receiver)}.{kind}({signature}) [param '{parameter.Name}']"; - } - - #endregion - - #region Sample values - - private static object? ArgumentFor(ParameterInfo parameter) { - Type type = parameter.ParameterType; - if (type.IsByRef) { type = type.GetElementType()!; } - - if (type.IsValueType) { - return Nullable.GetUnderlyingType(type) is not null ? null : Activator.CreateInstance(type); - } - - // A nullable non-target reference can simply be null; a non-nullable one needs a real, valid value so the - // member reaches (and only reaches) the guard of the parameter under test. - try { - if (Nullability.Create(parameter).ReadState == NullabilityState.Nullable) { return null; } - } catch { - // fall through and build a value - } - - return Sample(type); - } - - private static object Sample(Type type) { - foreach (object candidate in Samples) { - if (type.IsInstanceOfType(candidate)) { return candidate; } - } - - if (type == typeof(string)) { return "sample"; } - - if (type.IsArray) { - Type element = type.GetElementType()!; - Array array = Array.CreateInstance(element, 1); - array.SetValue(Element(element), 0); - - return array; - } - - if (type.IsGenericType) { - Type definition = type.GetGenericTypeDefinition(); - Type[] arguments = type.GetGenericArguments(); - - if (definition == typeof(IAny<>)) { return CreateAny(arguments[0]); } - - if (definition == typeof(IEnumerable<>) || definition == typeof(IReadOnlyList<>) || definition == typeof(IReadOnlyCollection<>) - || definition == typeof(IList<>) || definition == typeof(ICollection<>) || definition == typeof(List<>)) { - Type listType = typeof(List<>).MakeGenericType(arguments[0]); - IList list = (IList)Activator.CreateInstance(listType)!; - list.Add(Element(arguments[0])); - - return list; - } - - if (definition == typeof(IReadOnlyDictionary<,>) || definition == typeof(IDictionary<,>) || definition == typeof(Dictionary<,>)) { - Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(arguments); - IDictionary dictionary = (IDictionary)Activator.CreateInstance(dictionaryType)!; - dictionary[Element(arguments[0])!] = Element(arguments[1]); - - return dictionary; - } - } - - if (typeof(Delegate).IsAssignableFrom(type)) { return CreateDelegate(type); } - - throw new NotSupportedException($"no sample available for {Readable(type)}"); - } - - private static object? Element(Type type) { - if (type == typeof(string)) { return "x"; } - if (type.IsValueType) { return Nullable.GetUnderlyingType(type) is not null ? null : Activator.CreateInstance(type); } - - return Sample(type); - } - - private static object CreateAny(Type valueType) { - MethodInfo oneOf = typeof(Any).GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(method => method is { Name: "OneOf", IsGenericMethodDefinition: true } - && method.GetParameters() is [{ ParameterType.IsArray: true }]) - .MakeGenericMethod(valueType); - - Array pool = Array.CreateInstance(valueType, 1); - pool.SetValue(Element(valueType), 0); - - return oneOf.Invoke(null, [pool])!; - } - - private static object CreateDelegate(Type delegateType) { - MethodInfo signature = delegateType.GetMethod("Invoke")!; - ParameterExpression[] inputs = signature.GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray(); - Type returnType = signature.ReturnType; - - Expression body = returnType == typeof(void) - ? Expression.Empty() - : Expression.Constant(Element(returnType), returnType); - - return Expression.Lambda(delegateType, body, inputs).Compile(); - } - - // A pool of live, valid internal instances (random sources, interval/string/uri specs, collection state, regex - // nodes, ...) harvested by walking the object graph of a handful of real generators. Reusing what the library - // itself builds is what lets the harness supply a valid `spec` when testing a `source` guard, without wiring up - // each engine type by hand. - private static List HarvestSamples() { - HashSet visited = new(ReferenceEqualityComparer.Instance); - Queue frontier = new(); - - void Seed(object? root) { - if (root is null or string) { return; } - if (root.GetType().IsValueType) { return; } // primitives, enums, structs are never harvested samples - if (visited.Add(root)) { frontier.Enqueue(root); } - } - - SeedContextRoots(Seed); - SeedRepresentativeGenerators(Seed); - - return WalkReachableObjects(frontier, Seed); - } - - // The context itself, the two random sources, and every parameterless generator AnyContext exposes. - private static void SeedContextRoots(Action seed) { - AnyContext context = new(0); - seed(context); - seed(new FixedRandomSource(0)); - seed(new SeededRandom(0)); - - foreach (MethodInfo factory in typeof(AnyContext).GetMethods(BindingFlags.Public | BindingFlags.Instance)) { - if (factory.DeclaringType == typeof(object)) { continue; } - if (factory.IsSpecialName || factory.GetParameters().Length != 0) { continue; } - if (factory.ReturnType == typeof(void) || factory.ReturnType == typeof(AnyContext) || factory.IsGenericMethodDefinition) { continue; } - try { seed(factory.Invoke(context, null)); } catch { /* best effort */ } - } - } - - // The shapes AnyContext's parameterless factories cannot reach: everything that needs an element generator, a - // type argument or a pattern to exist at all. - private static void SeedRepresentativeGenerators(Action seed) { - IAny strings = Any.String(); - void Try(Func build) { - try { seed(build()); } catch { /* best effort */ } - } - - // Collections closed on the representative element type the harness uses (string), plus a dictionary and an enum. - Try(() => Any.ListOf(strings)); - Try(() => Any.SetOf(strings)); - Try(() => Any.SequenceOf(strings)); - Try(() => Any.ArrayOf(strings)); - Try(() => Any.DictionaryOf(strings, strings)); - Try(() => Any.OneOf("a", "b", "c")); - Try(() => Any.Enum()); - Try(() => strings.As(value => value.Length)); - Try(() => strings.OrNull()); - - // One pattern per regex-node shape, so every RegexNode subtype is reachable through AnyPattern's tree. - Try(() => Any.StringMatching("abc")); // sequence - Try(() => Any.StringMatching("a|b|c")); // alternation - Try(() => Any.StringMatching("a{2,3}")); // repeat - Try(() => Any.StringMatching("[a-z]")); // characters - } - - // Breadth-first over what the seeded roots can reach, bounded by a budget so a cyclic or unexpectedly wide graph - // cannot hang the suite. - private static List WalkReachableObjects(Queue frontier, Action seed) { - List pool = []; - int budget = 0; - - while (frontier.Count > 0 && budget++ < 5_000) { - object current = frontier.Dequeue(); - pool.Add(current); - - SeedFields(current, seed); - SeedParameterlessSteps(current, seed); - } - - return pool; - } - - private static void SeedFields(object current, Action seed) { - for (Type? level = current.GetType(); level is not null && level != typeof(object); level = level.BaseType) { - foreach (FieldInfo field in level.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - seed(field.GetValue(current)); - } - } - } - - // Follow parameterless generator-producing steps too, so a sibling generator reachable only through a - // fluent call (e.g. the concrete URI generators returned by AnyUri) still enters the pool. - private static void SeedParameterlessSteps(object current, Action seed) { - if (current.GetType().Assembly != LibraryAssembly) { return; } - - foreach (MethodInfo step in current.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) { - if (step.DeclaringType == typeof(object) || step.IsSpecialName || step.IsGenericMethodDefinition) { continue; } - if (step.GetParameters().Length != 0 || step.ReturnType.Assembly != LibraryAssembly) { continue; } - try { seed(step.Invoke(current, null)); } catch { /* best effort */ } - } - } - - #endregion - - #region Helpers - - private static bool TryClose(Type definition, out Type? closed) { - try { - closed = definition.MakeGenericType(definition.GetGenericArguments().Select(Representative).ToArray()); - - return true; - } catch { - closed = null; - - return false; - } - } - - private static bool TryClose(MethodInfo definition, out MethodInfo? closed) { - try { - closed = definition.MakeGenericMethod(definition.GetGenericArguments().Select(Representative).ToArray()); - - return true; - } catch { - closed = null; - - return false; - } - } - - private static Type Representative(Type parameter) { - GenericParameterAttributes attributes = parameter.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask; - Type[] constraints = parameter.GetGenericParameterConstraints(); - - if ((attributes & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0) { - return constraints.Any(constraint => constraint == typeof(Enum) || constraint.BaseType == typeof(Enum)) ? typeof(DayOfWeek) : typeof(int); - } - - Type? baseConstraint = constraints.FirstOrDefault(constraint => constraint is { IsClass: true } && constraint != typeof(object)); - if (baseConstraint is not null) { return baseConstraint; } - - return constraints.All(constraint => !constraint.IsInterface || constraint.IsAssignableFrom(typeof(string))) ? typeof(string) : typeof(int); - } - - private static Exception Root(Exception exception) { - return exception is TargetInvocationException { InnerException: { } inner } ? inner : exception; - } - - private static string Readable(Type type) { - if (!type.IsGenericType) { return type.Name; } - - string name = type.Name; - int tick = name.IndexOf('`'); - if (tick >= 0) { name = name[..tick]; } - - return $"{name}<{string.Join(", ", type.GetGenericArguments().Select(Readable))}>"; - } - - #endregion - -} diff --git a/JustDummies.UnitTests/SeedReproducibilityTests.cs b/JustDummies.UnitTests/SeedReproducibilityTests.cs deleted file mode 100644 index e02c863a..00000000 --- a/JustDummies.UnitTests/SeedReproducibilityTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The example-based half of the reproducibility contract: what the failure report must say, that a -/// successful run stays silent, the asynchronous overloads, and the null-argument guards. That two runs -/// under the same seed agree — and that different seeds diverge — holds for every seed and is -/// quantified in JustDummies.PropertyTests instead of pinned to 12345, 777 and 31415 (ADR-0040). -/// -[TestSubject(typeof(Any))] -public sealed class SeedReproducibilityTests { - - #region Statics members declarations - - private static string Batch() { - int full = Any.Int32().Generate(); - int bounded = Any.Int32().Between(1, 1000).Generate(); - string free = Any.String().Generate(); - string capped = Any.String().NonEmpty().WithMaxLength(50).Generate(); - string shaped = Any.String().StartingWith("ORD-").WithLength(12).Generate(); - long wide = Any.Int64().Generate(); - ulong unsigned = Any.UInt64().Generate(); - double real = Any.Double().Between(0d, 1000d).Generate(); - decimal exact = Any.Decimal().Between(0m, 1000m).Generate(); - bool flag = Any.Boolean().Generate(); - Guid id = Any.Guid().Generate(); - char letter = Any.Char().Generate(); - TimeSpan span = Any.TimeSpan().Generate(); - DateTime instant = Any.DateTime().Generate(); -#if NET8_0_OR_GREATER - Int128 huge = Any.Int128().Generate(); - Half tiny = Any.Half().Generate(); -#endif - List list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4).Generate(); - HashSet set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3).Generate(); - int? maybe = Any.Int32().Between(0, 9).OrNull().Generate(); - string coded = Any.StringMatching(@"[A-Z]{3}-\d{4}").Generate(); - - return string.Join("|", full, bounded, free, capped, shaped, - wide, unsigned, real, exact, flag, id, letter, - span.Ticks, instant.Ticks, -#if NET8_0_OR_GREATER - huge, tiny, -#endif - string.Join("-", list), string.Join("-", set.OrderBy(value => value)), - maybe?.ToString() ?? "null", coded); - } - - #endregion - - [Fact(DisplayName = "Reproducibly reports the seed and rethrows the original exception on failure.")] - public void ReproduciblyReportsTheSeedAndRethrows() { - string? reported = null; - InvalidOperationException boom = new("boom"); - Action failing = () => throw boom; - - InvalidOperationException thrown = Assert.Throws( - () => Any.Reproducibly(4242, failing, message => reported = message)); - - Check.That(ReferenceEquals(thrown, boom)).IsTrue(); - Check.That(reported).IsNotNull(); - Check.That(reported!).Contains("4242"); - Check.That(reported!).Contains("Any.Reproducibly("); - } - - [Fact(DisplayName = "Reproducibly does not report when the body succeeds.")] - public void ReproduciblyIsSilentOnSuccess() { - bool reported = false; - - Any.Reproducibly(() => { Any.String().NonEmpty().Generate(); }, _ => reported = true); - - Check.That(reported).IsFalse(); - } - - [Fact(DisplayName = "Reproducibly without a seed reports a replayable seed on failure.")] - public void ReproduciblyWithoutSeedStillReportsAReplayableSeed() { - string? reported = null; - Action failing = () => throw new InvalidOperationException("x"); - - Assert.Throws( - () => Any.Reproducibly(failing, message => reported = message)); - - Check.That(reported).IsNotNull(); - Check.That(reported!).Contains("Any.Reproducibly("); - } - - [Fact(DisplayName = "The async ReproduciblyAsync reports the seed and rethrows on failure.")] - public async Task AsyncReproduciblyReportsTheSeedAndRethrows() { - string? reported = null; - - await Assert.ThrowsAsync( - () => Any.ReproduciblyAsync(7, async () => { - await Task.Yield(); - - throw new InvalidOperationException("boom"); - }, message => reported = message)); - - Check.That(reported).IsNotNull(); - Check.That(reported!).Contains("7"); - } - - [Fact(DisplayName = "The async ReproduciblyAsync with a given seed replays the same sequence of values.")] - public async Task AsyncReproduciblyWithASeedIsDeterministic() { - string first = string.Empty; - string second = string.Empty; - - await Any.ReproduciblyAsync(4321, async () => { - await Task.Yield(); - first = Batch(); - }); - await Any.ReproduciblyAsync(4321, async () => { - await Task.Yield(); - second = Batch(); - }); - - Check.That(second).IsEqualTo(first); - } - - [Fact(DisplayName = "Reproducibly and ReproduciblyAsync require a body.")] - public void ReproduciblyRequiresABody() { - Check.ThatCode(() => Any.Reproducibly((Action)null!)).Throws(); - Check.ThatCode(() => Any.ReproduciblyAsync((Func)null!)).Throws(); - } - - [Fact(DisplayName = "A report sink that throws does not mask the body's failure.")] - public void AThrowingReportSinkDoesNotMaskTheBodyFailure() { - InvalidOperationException boom = new("real body failure"); - Action throwingSink = _ => throw new Exception("sink failure"); - - // The seed report is a best-effort aid: a sink that throws must never replace the failure it exists to - // help diagnose. The body's exception — not the sink's — must reach the test runner, unchanged. - InvalidOperationException thrown = Assert.Throws( - () => Any.Reproducibly(4242, () => throw boom, throwingSink)); - - Check.That(ReferenceEquals(thrown, boom)).IsTrue(); - Check.That(thrown.Message).IsEqualTo("real body failure"); - } - - [Fact(DisplayName = "An async report sink that throws does not mask the body's failure.")] - public async Task AsyncAThrowingReportSinkDoesNotMaskTheBodyFailure() { - InvalidOperationException boom = new("real async failure"); - Action throwingSink = _ => throw new Exception("sink failure"); - - InvalidOperationException thrown = await Assert.ThrowsAsync( - () => Any.ReproduciblyAsync(7, async () => { - await Task.Yield(); - - throw boom; - }, throwingSink)); - - Check.That(ReferenceEquals(thrown, boom)).IsTrue(); - Check.That(thrown.Message).IsEqualTo("real async failure"); - } - - [Fact(DisplayName = "A report sink that throws falls back to the console so the seed still surfaces.")] - public void AThrowingReportSinkFallsBackToTheConsole() { - TextWriter original = Console.Error; - StringWriter captured = new(); - Console.SetError(captured); - try { - Assert.Throws( - () => Any.Reproducibly(6006, () => throw new InvalidOperationException("boom"), - _ => throw new Exception("sink down"))); - } finally { - Console.SetError(original); - } - - // A throwing sink must not lose the seed: it falls back to the default console sink so the run stays replayable. - Check.That(captured.ToString()).Contains("6006"); - } - - [Fact(DisplayName = "A report sink that succeeds is not also written to the console.")] - public void ASucceedingReportSinkIsNotDuplicatedToTheConsole() { - TextWriter original = Console.Error; - StringWriter captured = new(); - Console.SetError(captured); - string? reported = null; - try { - Assert.Throws( - () => Any.Reproducibly(5005, () => throw new InvalidOperationException("boom"), - message => reported = message)); - } finally { - Console.SetError(original); - } - - // A custom sink replaces console output; a working sink must not be duplicated to the console fallback. - Check.That(reported).IsNotNull(); - Check.That(reported!).Contains("5005"); - Check.That(captured.ToString()).Not.Contains("5005"); - } - -} diff --git a/JustDummies.UnitTests/SizeGuardConventionTests.cs b/JustDummies.UnitTests/SizeGuardConventionTests.cs deleted file mode 100644 index f3e4eee5..00000000 --- a/JustDummies.UnitTests/SizeGuardConventionTests.cs +++ /dev/null @@ -1,201 +0,0 @@ -#region Usings declarations - -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The size-guard convention, enforced by reflection over every generator the library exposes: a size a generator -/// must actually produce — an exact or minimum length or count — is refused above the ceiling, while a size -/// that only caps a draw accepts any non-negative value (ADR-0050). -/// -/// -/// -/// The rule this pins is a rule about how the API reads, so it is stated the way a reader states it: a method -/// whose name announces a maximum, or a parameter named maximum, declares a cap; everything else that -/// takes a length or a count declares something the generator has to materialize. -/// -/// -/// It is written as a convention rather than as one test per method for the reason ADR-0045 gives for the -/// null-argument guard: the defect it prevents is a new builder forgetting the rule, and only -/// reflection holds a member that does not exist yet. The overflow behind the original defect existed because -/// the same arithmetic had been made safe one line away and not at the second site — a rule applied by hand -/// is a rule applied unevenly. Discovery starts from the generator factories on , so a -/// collection shape added later is covered with nothing to add here. -/// -/// -public sealed class SizeGuardConventionTests { - - /// The largest size a generator will be asked to produce; mirrored from the library, which keeps it internal. - private const int MaxProducibleSize = 1_000_000; - - /// The parameter names the size constraints use across both surfaces. - private static readonly string[] SizeParameterNames = ["length", "count", "minimum", "maximum"]; - - [Fact(DisplayName = "Every size constraint ceilings what it must produce and leaves a pure cap uncapped.")] - public void EverySizeConstraintGuardsItsArgumentsToTheConvention() { - List violations = []; - List generators = Generators().ToList(); - - // An empty harvest would make every assertion below vacuously true — the classic way a convention test goes - // green by testing nothing. - Check.WithCustomMessage("No generator was harvested from Any's factories; the convention would assert nothing.") - .That(generators) - .Not.IsEmpty(); - - foreach (object generator in generators) { - foreach (MethodInfo method in SizeMethodsOf(generator.GetType())) { - foreach (ParameterInfo parameter in method.GetParameters()) { - Verify(generator, method, parameter, violations); - } - } - } - - Check.WithCustomMessage($"Size-guard convention — {violations.Count} deviation(s):{Environment.NewLine}" - + string.Join(Environment.NewLine, violations.OrderBy(line => line, StringComparer.Ordinal))) - .That(violations) - .IsEmpty(); - } - - #region Statics members declarations - - /// - /// One generator per shape that carries size constraints, obtained from 's own factories so - /// that a shape added later is picked up here automatically. Generic factories are closed over - /// , and the item generators a collection factory needs are supplied from the same source. - /// - private static IEnumerable Generators() { - List generators = [Any.String()]; - - foreach (MethodInfo factory in typeof(Any).GetMethods(BindingFlags.Public | BindingFlags.Static)) { - if (!factory.IsGenericMethodDefinition) { continue; } - if (factory.GetGenericArguments().Length is not (1 or 2)) { continue; } - if (!TryClose(factory, out MethodInfo closed)) { continue; } - if (!TryBuildArguments(closed, out object[] arguments)) { continue; } - - object? generator = closed.Invoke(null, arguments); - if (generator is not null && SizeMethodsOf(generator.GetType()).Any()) { generators.Add(generator); } - } - - return generators; - } - - /// - /// Closes a generic factory over , or reports that it cannot be: a factory constrained to - /// something else (Any.Enum<TEnum>) carries no size constraint, so skipping it costs no coverage. - /// - private static bool TryClose(MethodInfo factory, out MethodInfo closed) { - try { - closed = factory.MakeGenericMethod(Enumerable.Repeat(typeof(int), factory.GetGenericArguments().Length).ToArray()); - - return true; - } catch (ArgumentException) { - closed = null!; - - return false; - } - } - - /// - /// The arguments a closed factory needs, when every one of them is an item generator this test can supply. - /// A factory asking for anything else is skipped rather than guessed at — it carries no size constraint the - /// convention would reach. - /// - private static bool TryBuildArguments(MethodInfo factory, out object[] arguments) { - List built = []; - foreach (ParameterInfo parameter in factory.GetParameters()) { - if (parameter.ParameterType != typeof(IAny)) { - arguments = []; - - return false; - } - - built.Add(Any.Int32()); - } - - arguments = built.ToArray(); - - return true; - } - - /// - /// Every public method of a generator whose parameters are all sizes — the constraints this convention - /// governs. Inherited members are deliberately included: the collection shapes take - /// WithCount/WithMinCount/WithMaxCount from their shared base, so excluding them would - /// leave every shape but unchecked. - /// - private static IEnumerable SizeMethodsOf(Type type) { - return type.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(method => method.GetParameters().Length > 0 - && method.GetParameters().All(IsSizeParameter)); - } - - private static bool IsSizeParameter(ParameterInfo parameter) { - return parameter.ParameterType == typeof(int) - && parameter.Name is not null - && SizeParameterNames.Contains(parameter.Name, StringComparer.Ordinal); - } - - /// - /// Whether declares a pure cap: the parameter explicitly named maximum, - /// or the single size of a method that announces a maximum in its name. - /// - private static bool IsCap(MethodInfo method, ParameterInfo parameter) { - return parameter.Name == "maximum" || method.Name.StartsWith("WithMax", StringComparison.Ordinal); - } - - private static void Verify(object generator, MethodInfo method, ParameterInfo parameter, List violations) { - string member = $"{generator.GetType().Name}.{method.Name}(...) [param '{parameter.Name}']"; - - // A negative size is a caller mistake on every parameter, cap or not — the rule this one extends. - if (!Throws(generator, method, parameter, -1)) { - violations.Add($"{member}: a negative size was accepted; expected ArgumentOutOfRangeException."); - } - - if (IsCap(method, parameter)) { - // A cap costs nothing to honour, so it must stay declarable above the ceiling: refusing it here would - // reject a legitimate bound mirroring a storage limit. - if (Throws(generator, method, parameter, MaxProducibleSize + 1)) { - violations.Add($"{member}: a cap above the ceiling was refused; a maximum only narrows the draw and must accept any non-negative value."); - } - - return; - } - - if (!Throws(generator, method, parameter, MaxProducibleSize + 1)) { - violations.Add($"{member}: a produced size above the ceiling was accepted; expected ArgumentOutOfRangeException."); - } - - if (Throws(generator, method, parameter, MaxProducibleSize)) { - violations.Add($"{member}: the ceiling itself was refused; it is an inclusive bound."); - } - } - - /// - /// Invokes with on and the - /// neutral 0 everywhere else, reporting whether it threw . Zero is - /// what makes a two-bound call reach its own guards: the ordering check that would otherwise reject a crossed - /// pair runs after them, so the probe is the argument being judged. - /// - private static bool Throws(object generator, MethodInfo method, ParameterInfo probed, int probe) - where TException : Exception { - object[] arguments = method.GetParameters() - .Select(parameter => (object)(parameter.Position == probed.Position ? probe : 0)) - .ToArray(); - - try { - method.Invoke(generator, arguments); - - return false; - } catch (TargetInvocationException invocation) { - return invocation.InnerException is TException; - } - } - - #endregion - -} diff --git a/JustDummies.UnitTests/SurfaceParityTests.cs b/JustDummies.UnitTests/SurfaceParityTests.cs deleted file mode 100644 index 7d665167..00000000 --- a/JustDummies.UnitTests/SurfaceParityTests.cs +++ /dev/null @@ -1,226 +0,0 @@ -#region Usings declarations - -using System.Reflection; -using System.Threading.Tasks; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Structural guards over the library's two hand-mirrored surfaces. Both are pure reflection, so they add no -/// per-builder maintenance beyond the expectation table encoded here: -/// -/// -/// Mirror parity. Every scalar factory on the static entry point has an -/// identical instance counterpart on . A scalar factory added to one surface and -/// forgotten on the other would compile and pass every behavioral test, silently shipping a hole in the -/// deterministic surface. -/// -/// -/// Algebra parity. Each builder exposes exactly the constraint method set its family declares. A -/// renamed or missing constraint on one of the cloned numeric or temporal builders would otherwise slip -/// past the copy-paste discipline that keeps the duplication safe. -/// -/// -/// Composition and collection factories (Combine, ListOf, DictionaryOf, ...) are deliberately -/// not mirrored onto : they inherit the context through their operand sources, so -/// the mirror guard excludes them by construction (they take an operand). -/// -public sealed class SurfaceParityTests { - - #region Mirror parity: Any <-> AnyContext - - [Fact(DisplayName = "Every Any scalar factory has an identical AnyContext counterpart.")] - public void AnyAndAnyContextExposeTheSameScalarFactories() { - HashSet onAny = typeof(Any) - .GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Where(IsScalarFactory) - .Select(Signature) - .ToHashSet(); - - HashSet onContext = typeof(AnyContext) - .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Where(method => !method.IsSpecialName) // drops the Seed property getter - .Select(Signature) - .ToHashSet(); - - string[] onlyOnAny = onAny.Except(onContext).OrderBy(signature => signature, StringComparer.Ordinal).ToArray(); - string[] onlyOnContext = onContext.Except(onAny).OrderBy(signature => signature, StringComparer.Ordinal).ToArray(); - - Check.WithCustomMessage($"Scalar factories only on Any: [{string.Join(", ", onlyOnAny)}]; only on AnyContext: [{string.Join(", ", onlyOnContext)}].") - .That(onlyOnAny.Length + onlyOnContext.Length) - .IsEqualTo(0); - } - - // A scalar factory produces a generator from the context's own source: it returns a builder and takes no - // IAny<> operand. That excludes the composition/collection factories that live only on Any (Combine, ListOf, - // SetOf, DictionaryOf, PairOf, ...), as well as the three ways to control seeding — WithSeed (returns - // AnyContext), Reproducibly (returns void/Task) and UseSeed (returns IDisposable). None of those is a - // generator factory, and AnyContext is not meant to mirror them: it already *is* an explicit deterministic - // context, so pinning a seed on one would be meaningless. - private static bool IsScalarFactory(MethodInfo method) { - if (method.GetParameters().Any(parameter => IsAny(parameter.ParameterType))) { return false; } - - Type returnType = method.ReturnType; - - return returnType != typeof(AnyContext) - && returnType != typeof(void) - && returnType != typeof(IDisposable) - && !typeof(Task).IsAssignableFrom(returnType); - } - - private static bool IsAny(Type type) { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAny<>); - } - - // Name + generic arity + parameter types + return type, ignoring the static/instance distinction so the two - // surfaces line up. A drift in any of those four dimensions moves the signature and fails the guard. - private static string Signature(MethodInfo method) { - string parameters = string.Join(", ", method.GetParameters().Select(parameter => parameter.ParameterType.Name)); - - return $"{method.Name}`{method.GetGenericArguments().Length}({parameters}) -> {method.ReturnType.Name}"; - } - - #endregion - - #region Algebra parity: per-family constraint sets - - // The constraint vocabulary each family declares, encoded once as data. This table is the specification; the - // test compares it against what each builder actually exposes through reflection. Ordering here is irrelevant — - // the test compares sets. The lattice constraint splits what was once one signed-numeric family: only the - // integers carry MultipleOf, only Decimal carries WithScale, only the temporals carry WithGranularity. - - // Signed integers: the bound/sign vocabulary plus the integer lattice MultipleOf. - private static readonly string[] SignedIntegerAlgebra = [ - "Positive", "Negative", "Zero", "NonZero", - "GreaterThan", "GreaterThanOrEqualTo", "LessThan", "LessThanOrEqualTo", - "Between", "MultipleOf", "OneOf", "Except", "DifferentFrom" - ]; - - // Unsigned integers drop Positive/Negative (meaningless there — NonZero carries the intent); they keep MultipleOf. - private static readonly string[] UnsignedIntegerAlgebra = [ - "Zero", "NonZero", - "GreaterThan", "GreaterThanOrEqualTo", "LessThan", "LessThanOrEqualTo", - "Between", "MultipleOf", "OneOf", "Except", "DifferentFrom" - ]; - - // Binary floating-point carries the full signed vocabulary but no lattice: a grid of 10^-n over binary floats is a - // footgun (0.1 is not representable), so MultipleOf/WithScale are deliberately withheld. - private static readonly string[] FloatingPointAlgebra = [ - "Positive", "Negative", "Zero", "NonZero", - "GreaterThan", "GreaterThanOrEqualTo", "LessThan", "LessThanOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom" - ]; - - // Decimal is the signed vocabulary plus the decimal scale lattice WithScale. - private static readonly string[] DecimalAlgebra = [ - "Positive", "Negative", "Zero", "NonZero", - "GreaterThan", "GreaterThanOrEqualTo", "LessThan", "LessThanOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom", "WithScale" - ]; - - // TimeSpan is a signed magnitude with a temporal granularity lattice WithGranularity. - private static readonly string[] TimeSpanAlgebra = [ - "Positive", "Negative", "Zero", "NonZero", - "GreaterThan", "GreaterThanOrEqualTo", "LessThan", "LessThanOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom", "WithGranularity" - ]; - - // Instant-like builders rename the bound family to domain vocabulary, with identical inclusive/exclusive - // semantics, and carry no Positive/Negative/Zero (an instant has no sign). Conditioned like its only - // consumer — AnyDateOnly exists on .NET 8 and later — so the net472 leg does not carry a field it cannot use. -#if NET8_0_OR_GREATER - private static readonly string[] InstantAlgebra = [ - "After", "AfterOrEqualTo", "Before", "BeforeOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom" - ]; -#endif - - // Instants with sub-day tick precision also carry the temporal granularity lattice WithGranularity (DateOnly, - // already day-resolution, keeps the plain InstantAlgebra). - private static readonly string[] InstantWithGranularityAlgebra = [ - "After", "AfterOrEqualTo", "Before", "BeforeOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom", "WithGranularity" - ]; - - // AnyDateTimeOffset additionally exposes the offset dimension (WithOffset/WithOffsetBetween) — the only instant - // type carrying a second, offset dimension on top of the instant. - private static readonly string[] InstantWithGranularityAndOffsetAlgebra = [ - "After", "AfterOrEqualTo", "Before", "BeforeOrEqualTo", - "Between", "OneOf", "Except", "DifferentFrom", "WithGranularity", "WithOffset", "WithOffsetBetween" - ]; - - public static TheoryData Builders() { - TheoryData data = new(); - - // Signed integers carry MultipleOf; the binary floats do not; Decimal carries WithScale; TimeSpan (a signed - // magnitude) carries WithGranularity — the lattice constraint is what forks the former shared signed family. - data.Add(typeof(AnyInt32), SignedIntegerAlgebra); - data.Add(typeof(AnySByte), SignedIntegerAlgebra); - data.Add(typeof(AnyInt16), SignedIntegerAlgebra); - data.Add(typeof(AnyInt64), SignedIntegerAlgebra); - data.Add(typeof(AnyDouble), FloatingPointAlgebra); - data.Add(typeof(AnySingle), FloatingPointAlgebra); - data.Add(typeof(AnyDecimal), DecimalAlgebra); - data.Add(typeof(AnyTimeSpan), TimeSpanAlgebra); - - data.Add(typeof(AnyByte), UnsignedIntegerAlgebra); - data.Add(typeof(AnyUInt16), UnsignedIntegerAlgebra); - data.Add(typeof(AnyUInt32), UnsignedIntegerAlgebra); - data.Add(typeof(AnyUInt64), UnsignedIntegerAlgebra); - - data.Add(typeof(AnyDateTime), InstantWithGranularityAlgebra); - data.Add(typeof(AnyDateTimeOffset), InstantWithGranularityAndOffsetAlgebra); - - // The remaining scalar builders each carry their own deliberate set. - data.Add(typeof(AnyBoolean), new[] { "True", "False", "DifferentFrom" }); - data.Add(typeof(AnyGuid), new[] { "NonEmpty", "Empty", "OneOf", "Except", "DifferentFrom" }); - // AnyEnum adds AllowingCombinations, the opt-in widening the draw from the declared members to their - // combinations — meaningful only for a [Flags] enum, hence a constraint rather than a second factory. - data.Add(typeof(AnyEnum), new[] { "AllowingCombinations", "OneOf", "Except", "DifferentFrom" }); - data.Add(typeof(AnyChar), new[] { "Alpha", "AlphaNumeric", "Numeric", "UpperCase", "LowerCase", "OneOf", "Except", "DifferentFrom" }); - - // AnyString carries the exclusion pair Except/DifferentFrom (met by a bounded redraw, since strings are not - // ordinal-mapped) and, like every other family, a composable OneOf that returns the builder itself. - data.Add(typeof(AnyString), new[] { - "NonEmpty", "WithLength", "WithMinLength", "WithMaxLength", "WithLengthBetween", - "StartingWith", "EndingWith", "Containing", "Alpha", "AlphaNumeric", "Numeric", "WithChars", "UpperCase", "LowerCase", - "OneOf", "Except", "DifferentFrom" - }); - -#if NET8_0_OR_GREATER - data.Add(typeof(AnyInt128), SignedIntegerAlgebra); - data.Add(typeof(AnyHalf), FloatingPointAlgebra); - data.Add(typeof(AnyUInt128), UnsignedIntegerAlgebra); - data.Add(typeof(AnyDateOnly), InstantAlgebra); - data.Add(typeof(AnyTimeOnly), InstantWithGranularityAlgebra); -#endif - - return data; - } - - [Theory(DisplayName = "Each builder exposes exactly its family's constraint method set.")] - [MemberData(nameof(Builders))] - public void BuilderExposesExactlyItsFamilyAlgebra(Type builder, string[] expected) { - // A constraint method is fluent — it returns the builder itself. Generate() (returns the value) and the - // explicit interface members (not public) are excluded automatically. - HashSet actual = builder - .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Where(method => method.ReturnType == builder && !method.IsSpecialName) - .Select(method => method.Name) - .ToHashSet(); - - string[] missing = expected.Except(actual).OrderBy(name => name, StringComparer.Ordinal).ToArray(); - string[] unexpected = actual.Except(expected).OrderBy(name => name, StringComparer.Ordinal).ToArray(); - - Check.WithCustomMessage($"{builder.Name} — missing: [{string.Join(", ", missing)}]; unexpected: [{string.Join(", ", unexpected)}].") - .That(missing.Length + unexpected.Length) - .IsEqualTo(0); - } - - #endregion - -} diff --git a/JustDummies.UnitTests/TestValueObjects.cs b/JustDummies.UnitTests/TestValueObjects.cs deleted file mode 100644 index 4377bdb9..00000000 --- a/JustDummies.UnitTests/TestValueObjects.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace JustDummies.UnitTests; - -/// -/// A DDD-style value object with a format invariant, used to exercise the primitive-to-value-object bridge -/// (As): its factory is the single gatekeeper, exactly as in production code. -/// -public sealed class OrderReference { - - #region Statics members declarations - - public static OrderReference Create(string value) { - if (value is null) { throw new ArgumentNullException(nameof(value)); } - if (!value.StartsWith("ORD-", StringComparison.Ordinal)) { throw new ArgumentException("An order reference starts with 'ORD-'.", nameof(value)); } - if (value.Length != 12) { throw new ArgumentException("An order reference is exactly 12 characters long.", nameof(value)); } - - return new OrderReference(value); - } - - #endregion - - private OrderReference(string value) { - Value = value; - } - - public string Value { get; } - -} - -/// A numeric value object with a range invariant, for the numeric side of the bridge. -public sealed class Percentage { - - #region Statics members declarations - - public static Percentage Create(int value) { - if (value is < 0 or > 100) { throw new ArgumentOutOfRangeException(nameof(value), value, "A percentage lies between 0 and 100."); } - - return new Percentage(value); - } - - #endregion - - private Percentage(int value) { - Value = value; - } - - public int Value { get; } - -} - -/// A small aggregate assembled from constrained parts, for Any.Combine. -public sealed class Customer { - - public Customer(string name, OrderReference lastOrder) { - Name = name ?? throw new ArgumentNullException(nameof(name)); - LastOrder = lastOrder ?? throw new ArgumentNullException(nameof(lastOrder)); - } - - public string Name { get; } - public OrderReference LastOrder { get; } - -} diff --git a/JustDummies.UnitTests/ValueIdentityTests.cs b/JustDummies.UnitTests/ValueIdentityTests.cs deleted file mode 100644 index c2bcfe7d..00000000 --- a/JustDummies.UnitTests/ValueIdentityTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The value identity of the small values the failure-reporting path is built from. Each is immutable and -/// documented as a value, so each answers "is this the same one?" by what it holds rather than by which instance -/// it is — the answer a reference type gives by default, and gives silently. -/// -/// -/// Example-suite material (ADR-0040): each case pins one named pair, and there is no argument to quantify over. -/// has its own equality cases in ; this fixture -/// covers the two values built beside it. -/// -[TestSubject(typeof(ConstraintClaim))] -public sealed class ValueIdentityTests { - - #region Statics members declarations - - private static ConstraintCall Length(string bound) { - return ConstraintCall.Of("WithLength", bound); - } - - #endregion - - // The halves are asserted directly rather than only through ToString and equality: a getter carries no logic, so - // a mutation score says nothing about it, and Constraint is what the blame choice reads. - [Fact(DisplayName = "A claim on a constraint exposes the constraint, its rendering and its clause.")] - public void ClaimOnAConstraintExposesItsHalves() { - ConstraintCall length = Length("3"); - ConstraintClaim claim = ConstraintClaim.Of(length, "already fixes the length at 3"); - - Check.That(claim.Constraint).IsEqualTo(length); - Check.That(claim.Subject).IsEqualTo("WithLength(3)"); - Check.That(claim.Claims).IsEqualTo("already fixes the length at 3"); - Check.That(claim.ToString()).IsEqualTo("WithLength(3) already fixes the length at 3"); - } - - [Fact(DisplayName = "A claim on a phrase exposes the phrase and carries no constraint.")] - public void ClaimOnAPhraseCarriesNoConstraint() { - ConstraintClaim claim = ConstraintClaim.OfPhrase("the contained value \"ABC\"", "contains 'x', which it does not allow"); - - Check.That(claim.Constraint).IsNull(); - Check.That(claim.Subject).IsEqualTo("the contained value \"ABC\""); - Check.That(claim.Claims).IsEqualTo("contains 'x', which it does not allow"); - Check.That(claim.ToString()).IsEqualTo("the contained value \"ABC\" contains 'x', which it does not allow"); - } - - [Fact(DisplayName = "A replay exposes the seed it was given and the guidance naming it.")] - public void ReplayExposesItsSeedAndGuidance() { - FixedRandomSource source = new(7); - - Replay full = Replay.Of(source, 42); - Replay partial = Replay.PartialOf(source); - - Check.That(full.Seed).IsEqualTo(42); - Check.That(full.Guidance).Contains("42"); - // The seed is supplied, not read back from the source, so the two need not agree. - Check.That(partial.Seed).IsEqualTo(7); - Check.That(partial.Guidance).Contains("not reproducible from this seed alone"); - } - - // Rendered for a reader rather than for a message: nothing appends a replay whole — AnyGenerationException takes - // its guidance and its seed separately — so this exists for the debugger, which the type points at it. - [Fact(DisplayName = "A replay renders its seed and guidance rather than its type name.")] - public void ReplayRendersItself() { - FixedRandomSource source = new(7); - - string rendered = Replay.Of(source, 42).ToString(); - - Check.That(rendered).StartsWith("seed 42:"); - Check.That(rendered).Contains("Any.WithSeed(42)"); - Check.That(rendered).Not.Contains(nameof(Replay)); - } - - [Fact(DisplayName = "Two claims blaming the same constraint for the same thing are equal.")] - public void ClaimsWithTheSameConstraintAndClaimAreEqual() { - ConstraintClaim first = ConstraintClaim.Of(Length("3"), "already fixes the length at 3"); - ConstraintClaim second = ConstraintClaim.Of(Length("3"), "already fixes the length at 3"); - - Check.That(first.Equals(second)).IsTrue(); - Check.That(first == second).IsTrue(); - Check.That(first != second).IsFalse(); - Check.That(first.GetHashCode()).IsEqualTo(second.GetHashCode()); - } - - [Fact(DisplayName = "A claim differs when its constraint differs, and when its claim does.")] - public void ClaimsDifferOnEitherHalf() { - ConstraintClaim reference = ConstraintClaim.Of(Length("3"), "already fixes the length at 3"); - - Check.That(reference == ConstraintClaim.Of(Length("5"), "already fixes the length at 3")).IsFalse(); - Check.That(reference == ConstraintClaim.Of(Length("3"), "already caps the length at 3")).IsFalse(); - } - - // The blame choice turns on whether a claim's subject IS the constraint being applied, so a phrase that merely - // reads like one must not pass for it — which is what keeps the two apart here. - [Fact(DisplayName = "A phrase never equals a claim on the constraint it reads like.")] - public void APhraseIsNotTheConstraintItReadsLike() { - ConstraintClaim onAConstraint = ConstraintClaim.Of(Length("3"), "already fixes the length at 3"); - ConstraintClaim onAPhrase = ConstraintClaim.OfPhrase("WithLength(3)", "already fixes the length at 3"); - - Check.That(onAConstraint.ToString()).IsEqualTo(onAPhrase.ToString()); - Check.That(onAConstraint == onAPhrase).IsFalse(); - } - - [Fact(DisplayName = "A claim equals neither null nor a value of another type.")] - public void ClaimEqualsNeitherNullNorAnotherType() { - ConstraintClaim claim = ConstraintClaim.Of(Length("3"), "already fixes the length at 3"); - ConstraintClaim? nothing = null; - object text = "WithLength(3) already fixes the length at 3"; - - Check.That(claim.Equals(nothing)).IsFalse(); - Check.That(claim.Equals(text)).IsFalse(); - Check.That(claim == nothing).IsFalse(); - Check.That(claim != nothing).IsTrue(); - Check.That(nothing == null).IsTrue(); - } - - [Fact(DisplayName = "Two replays of the same run under the same seed are equal.")] - public void ReplaysOfTheSameRunAreEqual() { - FixedRandomSource source = new(7); - - Replay first = Replay.Of(source, 42); - Replay second = Replay.Of(source, 42); - - Check.That(first.Equals(second)).IsTrue(); - Check.That(first == second).IsTrue(); - Check.That(first.GetHashCode()).IsEqualTo(second.GetHashCode()); - } - - [Fact(DisplayName = "A replay differs when its seed differs.")] - public void ReplaysDifferOnTheirSeed() { - FixedRandomSource source = new(7); - - Check.That(Replay.Of(source, 42) == Replay.Of(source, 43)).IsFalse(); - Check.That(Replay.Of(source, 42) != Replay.Of(source, 43)).IsTrue(); - } - - // The seed alone does not settle it: the same seed replays a run in full or only in part depending on whether a - // foreign generator contributed values this source never drew. - [Fact(DisplayName = "A partial replay differs from a full one carrying the same seed.")] - public void APartialReplayIsNotAFullOne() { - FixedRandomSource source = new(7); - - Replay full = Replay.Of(source); - Replay partial = Replay.PartialOf(source); - - Check.That(full.Seed).IsEqualTo(partial.Seed); - Check.That(full == partial).IsFalse(); - } - - [Fact(DisplayName = "A replay equals neither null nor a value of another type.")] - public void ReplayEqualsNeitherNullNorAnotherType() { - FixedRandomSource source = new(7); - Replay replay = Replay.Of(source, 42); - Replay? nothing = null; - object text = "42"; - - Check.That(replay.Equals(nothing)).IsFalse(); - Check.That(replay.Equals(text)).IsFalse(); - Check.That(replay == nothing).IsFalse(); - Check.That(replay != nothing).IsTrue(); - Check.That(nothing == null).IsTrue(); - } - -} diff --git a/JustDummies.UnitTests/ValueObjectConventionTests.cs b/JustDummies.UnitTests/ValueObjectConventionTests.cs deleted file mode 100644 index cbde8225..00000000 --- a/JustDummies.UnitTests/ValueObjectConventionTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -#region Usings declarations - -using System.Diagnostics; -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// The value-object convention, enforced by reflection over the whole library: a type marked -/// [ValueObject] is sealed, immutable, renders itself, and carries the full identity set — -/// , both Equals overloads, GetHashCode, ToString behind a -/// , and ==/!=. -/// -/// -/// -/// This exists because the gap it closes is silent. A reference type compares by identity when nobody writes -/// another answer, and nothing complains: not the compiler, not a test, not a reviewer reading a type that -/// calls itself a value in its own remarks. Two of this library's values shipped that way, and only the third -/// had its equality — because code happened to compare it with ==, which forced the question. Nothing -/// forced it for the others. -/// -/// -/// The marker is what makes the rule enforceable without guessing: immutability alone would sweep in the -/// generators and the specifications, which are immutable recipes rather than values. Declaring a value is -/// therefore the decision, and this test is what holds it to it. -/// -/// -/// Structure is all reflection can settle, and it is the part that goes missing: whether two equal instances -/// really hash alike belongs to each type's own tests. What is checked here cannot be satisfied by accident. -/// -/// -public sealed class ValueObjectConventionTests { - - private static readonly Assembly LibraryAssembly = typeof(Any).Assembly; - - [Fact(DisplayName = "Every type declared a value object carries a full value identity.")] - public void EveryDeclaredValueObjectCarriesAValueIdentity() { - List values = LibraryAssembly.GetTypes() - .Where(type => type.GetCustomAttribute() is not null) - .OrderBy(type => type.Name, StringComparer.Ordinal) - .ToList(); - - // Guards the scan itself: a renamed attribute or a moved assembly would leave the enumeration empty and every - // assertion below would pass vacuously. Emptiness is the failure mode; the exact count is not pinned, so - // retiring a value never trips this instead of saying what really changed. - Check.WithCustomMessage("No type is marked [ValueObject]; the scan lost its target.") - .That(values).Not.IsEmpty(); - - List violations = []; - foreach (Type value in values) { - violations.AddRange(MissingFrom(value).Select(missing => $"{value.Name}: {missing}")); - } - - Check.WithCustomMessage( - $"Value-object convention — {violations.Count} missing member(s) or property(ies):{Environment.NewLine}" - + string.Join(Environment.NewLine, violations)) - .That(violations) - .IsEmpty(); - } - - #region Per-type verification - - private static IEnumerable MissingFrom(Type value) { - // A struct yields a zero-initialized instance through its parameterless constructor, bypassing every - // validating factory — which is why a value enforcing an invariant is a class in this repository. - if (value.IsValueType) { yield return "is a struct; a value enforcing an invariant is a class here"; } - - // An unsealed value cannot keep equality symmetric: a subclass compares unequal to its base under one - // direction of the comparison and equal under the other. - if (!value.IsValueType && !value.IsSealed) { yield return "is not sealed"; } - - if (!typeof(IEquatable<>).MakeGenericType(value).IsAssignableFrom(value)) { - yield return $"does not implement IEquatable<{value.Name}>"; - } - - if (!DeclaresMethod(value, nameof(Equals), typeof(object))) { yield return "does not override Equals(object)"; } - if (!DeclaresMethod(value, nameof(GetHashCode))) { yield return "does not override GetHashCode()"; } - - // A value that does not render itself shows a debugger its type name, which is the one thing the reader - // already knows. The attribute is what puts that rendering in front of them without expanding the instance. - if (!DeclaresMethod(value, nameof(ToString))) { yield return "does not override ToString()"; } - - DebuggerDisplayAttribute? display = value.GetCustomAttribute(); - if (display is null) { - yield return "does not carry [DebuggerDisplay]"; - } else if (display.Value?.Contains(nameof(ToString)) != true) { - yield return $"carries [DebuggerDisplay(\"{display.Value}\")] rather than forwarding to ToString()"; - } - - // The operator pair is the silent half of the contract: without it `a == b` compiles and compares references, - // where a missing Equals would at least be visible to anyone reading the type. - if (!DeclaresOperator(value, "op_Equality")) { yield return "does not define operator =="; } - if (!DeclaresOperator(value, "op_Inequality")) { yield return "does not define operator !="; } - - foreach (string mutable in MutableStateOf(value)) { yield return mutable; } - } - - private static IEnumerable MutableStateOf(Type value) { - foreach (FieldInfo field in value.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - if (!field.IsInitOnly) { yield return $"field '{field.Name}' is not readonly"; } - } - - foreach (PropertyInfo property in value.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - if (property.SetMethod is not null) { yield return $"property '{property.Name}' has a setter"; } - } - } - - private static bool DeclaresMethod(Type value, string name, params Type[] parameters) { - MethodInfo? declared = value.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, parameters, null); - - return declared is not null && declared.DeclaringType == value; - } - - private static bool DeclaresOperator(Type value, string name) { - return value.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Any(method => method.Name == name && method.GetParameters().Length == 2); - } - - #endregion - -} diff --git a/JustDummies.UnitTests/XmlDocCrefConventionTests.cs b/JustDummies.UnitTests/XmlDocCrefConventionTests.cs deleted file mode 100644 index ae7d3b11..00000000 --- a/JustDummies.UnitTests/XmlDocCrefConventionTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -#region Usings declarations - -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; - -using NFluent; - -#endregion - -namespace JustDummies.UnitTests; - -/// -/// Locks two spellings inside XML documentation cref attributes, both of which the compiler accepts in -/// silence. -/// -/// -/// A predefined type is written with its C# keyword (string, int), never with its CLR -/// name (String, Int32). Both bind to the same documentation ID, so the generated XML -/// is identical either way and the deviation drifts in unnoticed. It costs the source, not the -/// package: inside a generic argument written Action{String} reads as the -/// factory rather than as the CLR string. -/// -/// -/// Inside the two types that host the type-named factories, a cref meaning the BCL type is qualified -/// with its namespace. A cref carrying no parameter list accepts a method as a target, so a bare -/// DateTime written there binds to the factory instead of the type. Unlike the first rule this -/// one is not cosmetic: the wrong target ships resolved in the package XML, and every reader follows -/// it. Write Any.DateTime when the factory really is what is meant. -/// -/// -/// -/// -/// The first rule is a text scan, not reflection: by the time a cref reaches the generated XML the compiler -/// has resolved it to System.String and the spelling under test no longer exists. Reading the sources -/// of the sibling projects creates no assembly reference, so the standalone boundary -/// ArchitectureTests guards is untouched. -/// -public sealed class XmlDocCrefConventionTests { - - // The predefined types, which is to say exactly those whose CLR name has a C# keyword. Int128, UInt128 and - // Half have none, so their PascalCase spelling is the only one and they are legitimately absent here. - private static readonly IReadOnlyDictionary KeywordFor = new Dictionary(StringComparer.Ordinal) { - ["Boolean"] = "bool", - ["Byte"] = "byte", - ["SByte"] = "sbyte", - ["Char"] = "char", - ["Decimal"] = "decimal", - ["Double"] = "double", - ["Single"] = "float", - ["Int16"] = "short", - ["UInt16"] = "ushort", - ["Int32"] = "int", - ["UInt32"] = "uint", - ["Int64"] = "long", - ["UInt64"] = "ulong", - ["Object"] = "object", - ["String"] = "string", - ["Void"] = "void" - }; - - private static readonly Regex CrefAttribute = new("cref=\"([^\"]*)\"", RegexOptions.Compiled); - private static readonly Regex Identifier = new("[A-Za-z_][A-Za-z0-9_]*", RegexOptions.Compiled); - - // Any is partial across several files, so the hosts are found by what they declare rather than by name. The - // word boundary keeps AnyString, AnyContextTests and their like out. - private static readonly Regex FactoryHostDeclaration = new(@"\bclass\s+(Any|AnyContext)\b", RegexOptions.Compiled); - - [Fact(DisplayName = "Every cref spells a predefined type with its C# keyword, not its CLR name.")] - public void CrefsSpellPredefinedTypesWithTheirCSharpKeyword() { - List files = SourceFiles().ToList(); - - // Guards the scan itself: a moved root or a renamed project would leave the enumeration empty, and every - // assertion below would then pass vacuously. The thresholds are floors far under the real counts (126 - // files, ~1200 crefs), so ordinary growth or pruning never trips them. - Check.WithCustomMessage($"Only {files.Count} source file(s) found under the JustDummies projects; the scan lost its target.") - .That(files.Count).IsStrictlyGreaterThan(100); - - List offenders = []; - int scanned = 0; - - foreach (string file in files) { - foreach (Match cref in CrefAttribute.Matches(File.ReadAllText(file))) { - string reference = cref.Groups[1].Value; - scanned++; - - foreach (string clrName in ClrNamesIn(reference)) { - offenders.Add($"{Path.GetFileName(file)}: cref \"{reference}\" names {clrName}; write {KeywordFor[clrName]}."); - } - } - } - - // The same guard one level down: files found but no cref extracted would prove just as little. - Check.WithCustomMessage($"Only {scanned} cref(s) scanned across {files.Count} file(s); the extraction lost its target.") - .That(scanned).IsStrictlyGreaterThan(900); - - Check.WithCustomMessage($"{offenders.Count} cref(s) name a CLR type where C# has a keyword:{Environment.NewLine}{string.Join(Environment.NewLine, offenders)}") - .That(offenders).IsEmpty(); - } - - [Fact(DisplayName = "No cref inside Any or AnyContext is captured by a type-named factory.")] - public void CrefsInsideTheFactoryHostsNameTheTypeAndNotTheFactory() { - HashSet factories = TypeNamedFactories(); - List hosts = SourceFiles().Where(DeclaresAFactoryHost).ToList(); - - // Guard the two queries: an empty factory set or a lost host would make the loop below assert nothing. - // The floors sit under the real counts — 24 factories on net10, 19 on the netstandard2.0 asset the net472 - // floor loads (the modern generators are absent there), and six declaring files. - Check.WithCustomMessage($"Only {factories.Count} type-named factories found on Any; the reflection lost its target.") - .That(factories.Count).IsStrictlyGreaterThan(15); - Check.WithCustomMessage($"Only {hosts.Count} file(s) declare Any or AnyContext; the scan lost its target.") - .That(hosts.Count).IsStrictlyGreaterThan(4); - - List offenders = []; - - foreach (string host in hosts) { - foreach (Match cref in CrefAttribute.Matches(File.ReadAllText(host))) { - string reference = cref.Groups[1].Value; - string head = MemberPath(reference).Split('.')[0]; - - if (factories.Contains(head)) { - offenders.Add($"{Path.GetFileName(host)}: cref \"{reference}\" binds to the Any.{head}() factory, not to the type it names; qualify it (System.{head}), or write Any.{head} when the factory is what is meant."); - } - } - } - - Check.WithCustomMessage($"{offenders.Count} cref(s) bind to a factory instead of the type they name:{Environment.NewLine}{string.Join(Environment.NewLine, offenders)}") - .That(offenders).IsEmpty(); - } - - // Read off the surface rather than listed by hand: Any's public, static, non-generic, parameterless methods - // returning a builder. It is the same query FactoryNamingConventionTests uses to prove each one is named - // after the CLR type it produces — which is precisely what makes them collide with it here. - private static HashSet TypeNamedFactories() { - IEnumerable names = typeof(Any) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(method => !method.IsGenericMethod - && method.GetParameters().Length == 0 - && method.ReturnType.GetInterfaces().Any(candidate => candidate.IsGenericType - && candidate.GetGenericTypeDefinition() == typeof(IAny<>))) - .Select(method => method.Name); - - return new HashSet(names, StringComparer.Ordinal); - } - - private static bool DeclaresAFactoryHost(string file) { - return FactoryHostDeclaration.IsMatch(File.ReadAllText(file)); - } - - private static IEnumerable ClrNamesIn(string cref) { - // A parameter list and a generic-argument group are pure type positions: every identifier inside one - // names a type, so a CLR name found there is a violation whatever qualifies it — String and - // System.String alike. - foreach (Match identifier in Identifier.Matches(TypePositions(cref))) { - if (KeywordFor.ContainsKey(identifier.Value)) { yield return identifier.Value; } - } - - // Elsewhere only the LEADING segment of the member path is a type. Any.String is the factory method and - // must not be reported; String.Empty and System.String.Empty must. - string[] path = MemberPath(cref).Split('.'); - if (path.Length > 0 && KeywordFor.ContainsKey(path[0])) { yield return path[0]; } - if (path.Length > 1 && path[0] == "System" && KeywordFor.ContainsKey(path[1])) { yield return path[1]; } - } - - // Everything nested inside braces or parentheses, flattened; the delimiters become separators so that - // adjacent groups can never be read as one identifier. - private static string TypePositions(string cref) { - StringBuilder positions = new(); - int depth = 0; - - foreach (char character in cref) { - if (character is '{' or '(') { - depth++; - positions.Append(' '); - } else if (character is '}' or ')') { - depth--; - positions.Append(' '); - } else if (depth > 0) { - positions.Append(character); - } - } - - return positions.ToString(); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1870:Use a cached 'SearchValues' instance", - Justification = - "SearchValues arrived in .NET 8 and this suite also runs on the .NET Framework 4.7.2 support floor " + - "(ADR-0022), where the type does not exist. IndexOfAny over two characters, run once per cref in a " + - "convention test, is not the cost this rule exists to remove.")] - private static string MemberPath(string cref) { - int end = cref.IndexOfAny(new[] { '{', '(' }); - - return end < 0 ? cref : cref.Substring(0, end); - } - - private static IEnumerable SourceFiles() { - return Directory.EnumerateDirectories(RepositoryRoot(), "JustDummies*") - .SelectMany(project => Directory.EnumerateFiles(project, "*.cs", SearchOption.AllDirectories)) - .Where(file => !IsBuildOutput(file)); - } - - // obj/ carries generated sources (AssemblyInfo, global usings) and bin/ a copy of whatever was compiled; - // scanning either would double-count and, worse, report a file nobody can fix. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "Prose, not code. The line explains what obj/ and bin/ contain and why scanning them would double-count; " + - "the rule reads the slashes and the parenthetical as a commented-out statement.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3220:Method calls should not resolve ambiguously to overloads with \"params\"", - Justification = - "Two separators passed to Split's params overload, which is the only spelling that works on both target " + - "frameworks. Wrapping them in an explicit array to disambiguate would immediately trip S3878, which asks " + - "for that array to be removed again.")] - private static bool IsBuildOutput(string file) { - return file.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) - .Any(segment => segment is "bin" or "obj"); - } - - private static string RepositoryRoot() { - AssemblyMetadataAttribute root = typeof(XmlDocCrefConventionTests).Assembly - .GetCustomAttributes() - .Single(metadata => metadata.Key == "RepositoryRoot"); - - return Path.GetFullPath(root.Value!); - } - -} diff --git a/JustDummies.Xunit.UnitTests/ArchitectureTests.cs b/JustDummies.Xunit.UnitTests/ArchitectureTests.cs deleted file mode 100644 index 57895b35..00000000 --- a/JustDummies.Xunit.UnitTests/ArchitectureTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -#region Usings declarations - -using System.Reflection; - -using NFluent; - -#endregion - -namespace JustDummies.Xunit.UnitTests; - -/// -/// Guards the boundary of the companion package. JustDummies itself may depend on nothing beyond the standard -/// library (ADR-0011), which is precisely why the xUnit adapter is a separate package (ADR-0036): it exists to -/// carry the one dependency JustDummies cannot. What it must never carry is a FirstClassErrors dependency — the -/// error-agnostic promise applies to the whole JustDummies line, not just its core assembly. -/// -public sealed class ArchitectureTests { - - [Fact(DisplayName = "JustDummies.Xunit references no FirstClassErrors assembly.")] - public void JustDummiesXunitReferencesNoFirstClassErrorsAssembly() { - AssemblyName[] references = typeof(ReproducibleAttribute).Assembly.GetReferencedAssemblies(); - - foreach (AssemblyName reference in references) { - Check.WithCustomMessage($"Unexpected assembly reference: {reference.Name}") - .That(reference.Name!.StartsWith("FirstClassErrors", StringComparison.Ordinal)).IsFalse(); - } - } - - [Fact(DisplayName = "JustDummies.Xunit depends on nothing beyond the standard library, JustDummies and xUnit.")] - public void JustDummiesXunitDependsOnlyOnJustDummiesAndXunit() { - AssemblyName[] references = typeof(ReproducibleAttribute).Assembly.GetReferencedAssemblies(); - - foreach (AssemblyName reference in references) { - // The exact facade split varies with the SDK, so the guard checks the intent — the standard - // library, the library being adapted, and the framework it is adapted to — not a fixed list. - bool expected = reference.Name is "netstandard" or "mscorlib" or "JustDummies" - || reference.Name!.StartsWith("System.", StringComparison.Ordinal) - || reference.Name.StartsWith("xunit.", StringComparison.Ordinal); - - Check.WithCustomMessage($"Unexpected assembly reference: {reference.Name}").That(expected).IsTrue(); - } - } - -} diff --git a/JustDummies.Xunit.UnitTests/JustDummies.Xunit.UnitTests.csproj b/JustDummies.Xunit.UnitTests/JustDummies.Xunit.UnitTests.csproj deleted file mode 100644 index b65023ba..00000000 --- a/JustDummies.Xunit.UnitTests/JustDummies.Xunit.UnitTests.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - enable - enable - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - diff --git a/JustDummies.Xunit.UnitTests/ReproducibleAttributeHookTests.cs b/JustDummies.Xunit.UnitTests/ReproducibleAttributeHookTests.cs deleted file mode 100644 index 239b391d..00000000 --- a/JustDummies.Xunit.UnitTests/ReproducibleAttributeHookTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace JustDummies.Xunit.UnitTests; - -/// -/// The adapter's hooks, driven directly rather than through the xUnit pipeline. -/// -/// -/// -/// Everything the surrounding suite asserts is observed from inside a decorated test, which is the right -/// way to prove that pinning works — but it leaves the hooks' own defensive behaviour unobserved: the test never -/// sees After run, so it cannot tell whether the scope was closed or merely abandoned, nor what happens -/// when After runs without a matching Before. -/// -/// -/// Both hooks ignore their two parameters entirely, so they are passed as null here: naming a real -/// MethodInfo and a fake IXunitTest would suggest they matter. -/// -/// -[TestSubject(typeof(ReproducibleAttribute))] -public sealed class ReproducibleAttributeHookTests { - - #region Statics members declarations - - private static (int, string) Batch() { - return (Any.Int32().Generate(), Any.String().NonEmpty().Generate()); - } - - #endregion - - [Fact(DisplayName = "A declared seed is the seed the attribute reports.")] - public void ADeclaredSeedIsReadBack() { - // The property is what a reader sets and what the replay snippet echoes; nothing else asserted that setting - // it had any effect at all. - ReproducibleAttribute attribute = new() { Seed = 1234 }; - - Check.That(attribute.Seed).IsEqualTo(1234); - } - - [Fact(DisplayName = "An undeclared seed reads as zero.")] - public void AnUndeclaredSeedReadsAsZero() { - Check.That(new ReproducibleAttribute().Seed).IsEqualTo(0); - } - - [Fact(DisplayName = "The after-hook is a no-op when no scope was opened for it.")] - public void TheAfterHookToleratesAMissingScope() { - // xUnit is not obliged to have run Before — a failure in another hook can skip it — and the after-hook must - // survive that rather than take the whole test run down with a NullReferenceException. - ReproducibleAttribute attribute = new(); - - Check.ThatCode(() => attribute.After(null!, null!)).DoesNotThrow(); - } - - [Fact(DisplayName = "The after-hook closes the scope it opened, releasing the ambient source.")] - public void TheAfterHookClosesTheScope() { - // What seed 555 produces on its second draw. If the scope were left open, the draw after the hook would - // continue that sequence and match; a closed scope hands the ambient source back and it will not. - (int, string) secondOfTheSeededRun; - using (Any.UseSeed(555)) { - Batch(); - secondOfTheSeededRun = Batch(); - } - - ReproducibleAttribute attribute = new() { Seed = 555 }; - attribute.Before(null!, null!); - Batch(); - attribute.After(null!, null!); - - Check.That(Batch()).IsNotEqualTo(secondOfTheSeededRun); - } - - [Fact(DisplayName = "Nested scopes unwind in order, restoring the outer seed.")] - public void NestedScopesUnwindInOrder() { - // The method, class and assembly levels nest, and xUnit closes them in reverse. Closing the inner one must - // restore the outer seed rather than the unpinned source. - ReproducibleAttribute outer = new() { Seed = 111 }; - ReproducibleAttribute inner = new() { Seed = 222 }; - - (int, string) expectedSecondOfOuter; - using (Any.UseSeed(111)) { - Batch(); - expectedSecondOfOuter = Batch(); - } - - outer.Before(null!, null!); - try { - Batch(); - - inner.Before(null!, null!); - Batch(); - inner.After(null!, null!); - - Check.That(Batch()).IsEqualTo(expectedSecondOfOuter); - } finally { - outer.After(null!, null!); - } - } - -} diff --git a/JustDummies.Xunit.UnitTests/ReproducibleAttributeTests.cs b/JustDummies.Xunit.UnitTests/ReproducibleAttributeTests.cs deleted file mode 100644 index 967808ca..00000000 --- a/JustDummies.Xunit.UnitTests/ReproducibleAttributeTests.cs +++ /dev/null @@ -1,200 +0,0 @@ -#region Usings declarations - -using System.Collections.Concurrent; -using System.Reflection; - -using JetBrains.Annotations; - -using NFluent; - -using Xunit.v3; - -#endregion - -namespace JustDummies.Xunit.UnitTests; - -/// -/// The adapter is exercised through the real xUnit pipeline wherever a behaviour is observable from inside a -/// test: pinning, per-case seeds and scope closing all show up in the values a decorated test draws. Two things -/// cannot be observed that way — the outcome-dependent report, which needs a test that has already finished, and -/// the framework contract the adapter reads — so each gets its own guard: the reporting rule is decided by a seam -/// proved directly, and the contract is asserted from an after-hook, where a violation fails the test carrying it. -/// -[TestSubject(typeof(ReproducibleAttribute))] -public sealed class ReproducibleAttributeTests { - - #region Statics members declarations - - private static readonly ConcurrentDictionary DrawnByCase = new(); - - internal static (int, string) Batch() { - return (Any.Int32().Generate(), Any.String().NonEmpty().Generate()); - } - - #endregion - - [Fact(DisplayName = "A pinned seed yields the values that seed produces.")] - [Reproducible(Seed = 1234)] - public void APinnedSeedYieldsThatSeedsValues() { - (int, string) drawn = Batch(); - - (int, string) expected; - // The attribute pinned 1234 for this test; an explicit scope over the same seed must agree, which is - // only true if the attribute really pinned the ambient source the static Any entry points draw from. - using (Any.UseSeed(1234)) { expected = Batch(); } - - Check.That(drawn).IsEqualTo(expected); - } - - [Fact(DisplayName = "A pinned seed of zero is honoured, not treated as unset.")] - [Reproducible(Seed = 0)] - public void APinnedSeedOfZeroIsHonoured() { - (int, string) drawn = Batch(); - - (int, string) expected; - using (Any.UseSeed(0)) { expected = Batch(); } - - Check.That(drawn).IsEqualTo(expected); - } - - [Theory(DisplayName = "Each theory case draws its own seed, not one shared with its siblings.")] - [Reproducible] - [InlineData(1)] - [InlineData(2)] - public void EachTheoryCaseDrawsItsOwnSeed(int which) { - DrawnByCase[which] = Batch(); - - // Both cases run the same code under the same attribute instance. Sharing one seed would make their - // values identical; a seed drawn per case makes them differ. The check runs on whichever case lands - // second, so it does not depend on the order the two are executed in. - if (DrawnByCase.Count == 2) { - Check.That(DrawnByCase[1]).IsNotEqualTo(DrawnByCase[2]); - } - } - - [Fact(DisplayName = "The scope stays open for the whole test and restores after a nested one.")] - [Reproducible(Seed = 99)] - public void TheAttributeSeedSurvivesANestedScope() { - (int, string) first = Batch(); - - using (Any.UseSeed(11)) { Batch(); } - - (int, string) afterNesting = Batch(); - - (int, string) expectedFirst; - (int, string) expectedSecond; - using (Any.UseSeed(99)) { - expectedFirst = Batch(); - expectedSecond = Batch(); - } - - Check.That(first).IsEqualTo(expectedFirst); - Check.That(afterNesting).IsEqualTo(expectedSecond); - } - - [Fact(DisplayName = "A generation failure names the attribute, not the delegate runner.")] - [Reproducible(Seed = 2026)] - public void AGenerationFailureNamesTheAttribute() { - AnyGenerationException caught = Assert.Throws( - () => Any.Int32().As(_ => throw new InvalidOperationException("rejected")).Generate()); - - Check.That(caught.Seed).IsEqualTo(2026); - Check.That(caught.Message).Contains("[Reproducible(Seed = 2026)]"); - // The whole point of the replay snippet: this test contains no Any.Reproducibly call, so naming - // one would send the reader to a call that is not there. - Check.That(caught.Message).Not.Contains("Any.Reproducibly"); - } - - [Fact(DisplayName = "A failing test is told its seed and how to replay it.")] - public void AFailingTestIsToldItsSeed() { - string? report = ReproducibleAttribute.ReportFor(failed: true, seed: 1234); - - Check.That(report).IsNotNull(); - Check.That(report).Contains("seeded with 1234"); - Check.That(report).Contains("[Reproducible(Seed = 1234)]"); - Check.That(report).Not.Contains("Any.Reproducibly"); - } - - [Fact(DisplayName = "A passing test is told nothing.")] - public void APassingTestIsToldNothing() { - Check.That(ReproducibleAttribute.ReportFor(failed: false, seed: 1234)).IsNull(); - } - - [Fact(DisplayName = "xUnit still exposes a finished test's outcome to an after-hook.")] - [OutcomeContract] - public void TheFrameworkStillExposesTheOutcome() { - // The assertion lives in OutcomeContractAttribute.After: by the time it runs, this test has finished, - // which is the only moment the outcome exists. If a future xUnit stops populating it -- the contract - // the whole "report only on failure" rule rests on -- this test fails. - Check.That(true).IsTrue(); - } - - #region Nested types - - /// - /// Asserts, from the one place where a finished test's outcome exists, that the framework still reports it. - /// Throwing here fails the test that carries the attribute, which is exactly the signal wanted. - /// - [AttributeUsage(AttributeTargets.Method)] - private sealed class OutcomeContractAttribute : BeforeAfterTestAttribute { - - public override void After(MethodInfo methodUnderTest, IXunitTest test) { - TestResultState? state = TestContext.Current.TestState; - - Check.WithCustomMessage("xUnit no longer exposes a finished test's state to an after-hook; the Reproducible attribute cannot decide whether to report the seed.") - .That(state).IsNotNull(); - Check.WithCustomMessage("xUnit no longer reports a passing test as Passed; the failure-only rule cannot be trusted.") - .That(state!.Result).IsEqualTo(TestResult.Passed); - } - - } - - #endregion - -} - -/// -/// A class-level application: every test the class declares is reproducible without repeating the attribute, and -/// a method-level declaration overrides it for the test that carries one. -/// -[Reproducible(Seed = 7)] -public sealed class ClassLevelReproducibleTests { - - [Fact(DisplayName = "A class-level attribute pins every test the class declares.")] - public void AClassLevelAttributePinsEveryTest() { - (int, string) drawn = ReproducibleAttributeTests.Batch(); - - (int, string) expected; - using (Any.UseSeed(7)) { expected = ReproducibleAttributeTests.Batch(); } - - Check.That(drawn).IsEqualTo(expected); - } - - [Fact(DisplayName = "A method-level attribute wins over the class-level one.")] - [Reproducible(Seed = 4242)] - public void AMethodLevelAttributeWins() { - (int, string) drawn = ReproducibleAttributeTests.Batch(); - - (int, string) expected; - using (Any.UseSeed(4242)) { expected = ReproducibleAttributeTests.Batch(); } - - Check.That(drawn).IsEqualTo(expected); - } - -} - -/// -/// Without the attribute, nothing is pinned: two runs of the same draw differ. This is the arbitrary-by-default -/// behaviour the attribute is opt-in over, and the guard that a scope opened for another test never leaks here. -/// -public sealed class UndecoratedTests { - - [Fact(DisplayName = "Without the attribute the ambient source stays unpinned.")] - public void WithoutTheAttributeNothingIsPinned() { - (int, string) first = ReproducibleAttributeTests.Batch(); - (int, string) second = ReproducibleAttributeTests.Batch(); - - Check.That(second).IsNotEqualTo(first); - } - -} diff --git a/JustDummies.Xunit/JustDummies.Xunit.csproj b/JustDummies.Xunit/JustDummies.Xunit.csproj deleted file mode 100644 index 7f90419d..00000000 --- a/JustDummies.Xunit/JustDummies.Xunit.csproj +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - netstandard2.0 - enable - enable - latest - - - true - - - 0.1.0-dev - - - JustDummies.Xunit - Sylvain AURAT - Reefact - - - - The xUnit v3 companion of JustDummies: mark a test, a class or an assembly [Reproducible] and its arbitrary values are drawn from a pinned seed, reported only when the test fails. Removes the per-test Any.Reproducibly ceremony without changing how values are generated. - - - - testing;test-data;dummies;arbitrary;xunit;xunit-v3;deterministic;seed;reproducible - Apache-2.0 - false - - - https://justdummies.io - https://github.com/Reefact/first-class-errors.git - git - - © Reefact 2026 - - - icon.png - readme.md - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c581..00000000 --- a/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index d3165b76..00000000 --- a/JustDummies.Xunit/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,7 +0,0 @@ -#nullable enable -JustDummies.Xunit.ReproducibleAttribute -JustDummies.Xunit.ReproducibleAttribute.ReproducibleAttribute() -> void -JustDummies.Xunit.ReproducibleAttribute.Seed.get -> int -JustDummies.Xunit.ReproducibleAttribute.Seed.set -> void -override JustDummies.Xunit.ReproducibleAttribute.After(System.Reflection.MethodInfo! methodUnderTest, Xunit.v3.IXunitTest! test) -> void -override JustDummies.Xunit.ReproducibleAttribute.Before(System.Reflection.MethodInfo! methodUnderTest, Xunit.v3.IXunitTest! test) -> void diff --git a/JustDummies.Xunit/README.nuget.md b/JustDummies.Xunit/README.nuget.md deleted file mode 100644 index 19195af8..00000000 --- a/JustDummies.Xunit/README.nuget.md +++ /dev/null @@ -1,72 +0,0 @@ -# JustDummies.Xunit - -The [xUnit v3](https://xunit.net) companion of [JustDummies](https://www.nuget.org/packages/JustDummies). -Mark a test `[Reproducible]` and its arbitrary values are drawn from a pinned -seed — reported **only when the test fails**, so a red test names the exact seed -to replay while a green one stays silent. - -## Why - -`JustDummies` makes a run reproducible by wrapping the test body in a delegate: - - [Fact] - public void Order_reference_is_accepted() { - Any.Reproducibly(() => { - string reference = Any.String().StartingWith("ORD-").WithLength(12).Generate(); - // ... act, assert ... - }); - } - -That works on every test framework, and stays the portable form. This package -removes the ceremony for xUnit v3: - - [Fact, Reproducible] - public void Order_reference_is_accepted() { - string reference = Any.String().StartingWith("ORD-").WithLength(12).Generate(); - // ... act, assert ... - } - -Values still vary between runs — which is what surfaces a test secretly -depending on one — but a failure is now recoverable even though the body was -never wrapped in advance. - -## Replaying a failure - -A failing test writes its seed to the test output: - - [JustDummies] These arbitrary values were seeded with 1234. Reproduce this run with [Reproducible(Seed = 1234)]. - -Pin it to replay: - - [Fact, Reproducible(Seed = 1234)] - public void Order_reference_is_accepted() { /* ... */ } - -The same snippet is what a generation failure names, so a diagnostic never -points at a call the test does not contain. - -## Where it applies - -- **A test**: `[Fact, Reproducible]` or `[Theory, Reproducible]`. -- **A class**: `[Reproducible]` on the class covers every test it declares. -- **A whole suite**: `[assembly: Reproducible]`. - -The hooks run once per test *case*, so each case of a theory draws its own seed -rather than sharing one with its siblings. When several levels apply, the most -specific one wins for the duration of the test and the outer ones are restored -after it — an assembly-wide `[Reproducible]` can pin the suite while one test -replays a particular seed. - -## Notes - -- Values drawn from an explicit `Any.WithSeed(...)` context are unaffected: that - context is isolated by design and does not draw from the ambient source this - attribute pins. -- The seed is pinned through `Any.UseSeed(...)`, a public handle any test-framework - adapter can use — this package holds no privileged access to `JustDummies`. -- xUnit v3 only. On xUnit v2, NUnit, MSTest or anything else, use - `Any.Reproducibly(...)`: it is unaffected by this package and works everywhere. - -## Links - -- [Repository](https://github.com/Reefact/first-class-errors) -- [JustDummies](https://www.nuget.org/packages/JustDummies) diff --git a/JustDummies.Xunit/ReproducibleAttribute.cs b/JustDummies.Xunit/ReproducibleAttribute.cs deleted file mode 100644 index 010920e7..00000000 --- a/JustDummies.Xunit/ReproducibleAttribute.cs +++ /dev/null @@ -1,158 +0,0 @@ -#region Usings declarations - -using System.Reflection; - -using Xunit; -using Xunit.v3; - -#endregion - -namespace JustDummies.Xunit; - -/// -/// Makes a test's arbitrary values reproducible: the ambient context is pinned to a seed for -/// the duration of the test, and that seed is reported only when the test fails — so a red test names the -/// exact seed to replay while a green one stays silent. This is the declarative form of -/// Any.Reproducibly(() => { ... }): the values still vary between runs, which is what surfaces a test -/// secretly depending on one, but a failure is recoverable without the body having been wrapped in advance. -/// -/// -/// -/// Apply it next to [Fact] or [Theory], on a class to cover every test it declares, or on the -/// assembly to cover a whole suite. The hooks run once per test case, so each case of a theory gets its -/// own seed rather than sharing one with its siblings. When several levels apply, the most specific one wins -/// for the duration of the test and the outer ones are restored after it — so an assembly-wide -/// [Reproducible] can pin the suite while one test replays a particular seed. -/// -/// -/// Pin to replay a reported run. Left unset, a fresh seed is drawn for every test case. -/// -/// -/// -/// [Fact, Reproducible] -/// public void Order_reference_is_accepted() { -/// string reference = Any.String().StartingWith("ORD-").WithLength(12).Generate(); -/// // ... act, assert ... -/// } -/// -/// // Replay the seed a failing run reported: -/// [Fact, Reproducible(Seed = 1234)] -/// public void Order_reference_is_accepted() { /* ... */ } -/// -/// -/// -/// The seed reaches the test's output, which xUnit attaches to the failing test's result. Values drawn from an -/// explicit Any.WithSeed(...) context are unaffected: that context is isolated by design and does not -/// draw from the ambient source this attribute pins. -/// -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)] -public sealed class ReproducibleAttribute : BeforeAfterTestAttribute { - - #region Statics members declarations - - // The scopes opened for the current test, innermost first. An AsyncLocal rather than an instance field - // because one attribute instance serves every test it is applied to, including tests running in parallel; - // the ambient context this pins is itself AsyncLocal-backed, so the two flow together. A stack rather than - // a single slot because the method, class and assembly levels nest, and xUnit closes them in reverse. - private static readonly AsyncLocal Open = new(); - - #endregion - - #region Fields declarations - - // Nullable behind a non-nullable property: an attribute argument cannot be an int?, but the setter running - // at all is what distinguishes "Seed = 0" (a legitimate seed) from a seed that was never declared. - private int? _seed; - - #endregion - - /// - /// The seed to pin, to replay a run a previous failure reported. Left unset, every test case draws a fresh - /// seed — the arbitrary-by-default behaviour that surfaces a test depending on one particular value. - /// - public int Seed { - get => _seed ?? 0; - set => _seed = value; - } - - /// - public override void Before(MethodInfo methodUnderTest, IXunitTest test) { - int seed = _seed ?? NewSeed(); - - Open.Value = new Scope(Any.UseSeed(seed, ReplaySnippet(seed)), seed, Open.Value); - } - - /// - public override void After(MethodInfo methodUnderTest, IXunitTest test) { - Scope? scope = Open.Value; - if (scope is null) { return; } - - Open.Value = scope.Outer; - - try { - string? report = ReportFor(HasFailed(), scope.Seed); - if (report is not null) { TestContext.Current.TestOutputHelper?.WriteLine(report); } - } finally { - // Restoring the ambient context must happen even if reporting throws: a scope left open would pin - // the seed for whatever runs next in this execution context. - scope.Handle.Dispose(); - } - } - - /// - /// What the reader must write to replay the run — the attribute with its seed, not the delegate runner the - /// ambient source names by default, because a test carrying this attribute contains no such call. - /// - private static string ReplaySnippet(int seed) { - return $"[Reproducible(Seed = {seed})]"; - } - - /// - /// Whether the test that just ran failed. Read from the ambient test context, which carries the finished - /// test's outcome by the time the after-hook runs. A context that cannot be read is treated as a pass: a - /// spurious seed on a green test is noise, and silence is the safer default for a diagnostic aid. - /// - private static bool HasFailed() { - return TestContext.Current.TestState?.Result == TestResult.Failed; - } - - /// - /// What to tell the reader once the outcome is known: the seed and how to replay it when the test failed, - /// nothing at all when it passed. Kept apart from reading the outcome so the rule — report only on failure, - /// and name the attribute rather than the delegate runner — is verifiable without a failing test. - /// - internal static string? ReportFor(bool failed, int seed) { - return failed - ? $"[JustDummies] These arbitrary values were seeded with {seed}. Reproduce this run with {ReplaySnippet(seed)}." - : null; - } - - /// - /// A fresh seed per test case. Collision-tolerant by construction: the seed identifies a run to replay, it is - /// never asserted on, so two runs coinciding is harmless. - /// - private static int NewSeed() { - return Guid.NewGuid().GetHashCode(); - } - - #region Nested types - - /// One pinned ambient context and the one it displaced, so nested levels unwind in order. - private sealed class Scope { - - internal Scope(IDisposable handle, int seed, Scope? outer) { - Handle = handle; - Seed = seed; - Outer = outer; - } - - internal IDisposable Handle { get; } - internal int Seed { get; } - internal Scope? Outer { get; } - - } - - #endregion - -} diff --git a/JustDummies/Any.Choice.cs b/JustDummies/Any.Choice.cs deleted file mode 100644 index 22d87c48..00000000 --- a/JustDummies/Any.Choice.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Draws an arbitrary value from an explicit pool of caller-supplied , over the - /// ambient random context — the top-level choice combinator for a value whose domain is a closed set the test - /// does not assert on ("one of the configured currencies", "one of the states in this table"). The pool is the - /// whole shape of the specification — is opaque, so the returned generator offers no - /// type-specific constraint, only the exclusion pair Except/DifferentFrom every generator - /// carries — and it composes through As(...), OrNull(), Combine(...) and the collection - /// generators like any other. To draw from a pool already held as a collection, use - /// . - /// - /// - /// The draw is uniform and reproducible under a seed; duplicates collapse under - /// so no value is implicitly weighted, and the distinct count is - /// advertised so a distinct collection over the pool gates eagerly. A null element is rejected — make the - /// whole generator nullable with OrNull() instead of placing null in the pool. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public static AnyOneOf OneOf(params T[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(AmbientRandomSource.Instance, values, ConstraintCall.OfElided(nameof(OneOf))); - } - - /// - /// Draws an arbitrary value from an explicit pool held as a list — the - /// counterpart of , for a pool already materialized (a configuration list, a - /// fixture, a lookup table of domain objects). Same contract as : duplicates - /// collapse, the draw is uniform and reproducible under a seed, and Except/DifferentFrom narrow - /// the pool — Any.ElementOf(orders).DifferentFrom(theOneAlreadyUsed) is the idiom this overload exists - /// for. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public static AnyOneOf ElementOf(IReadOnlyList values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(AmbientRandomSource.Instance, values, ConstraintCall.OfElided(nameof(ElementOf))); - } - - /// - /// Draws an arbitrary value from an explicit pool held as a sequence — the - /// counterpart of (a LINQ result, values loaded at setup). The - /// sequence is materialized once, so a lazy query is never re-enumerated per draw. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public static AnyOneOf ElementOf(IEnumerable values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(AmbientRandomSource.Instance, values as IReadOnlyList ?? values.ToArray(), ConstraintCall.OfElided(nameof(ElementOf))); - } - -} diff --git a/JustDummies/Any.Collection.cs b/JustDummies/Any.Collection.cs deleted file mode 100644 index 88c18548..00000000 --- a/JustDummies/Any.Collection.cs +++ /dev/null @@ -1,123 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Starts an arbitrary generator over . Unconstrained, it yields - /// 0 to 8 elements; chain constraints to express what the surrounding code requires (NonEmpty(), - /// WithCount(...), Distinct(), Containing(...)). - /// - /// The generator each element is drawn from. - /// The element type. - /// A list generator to constrain fluently. - /// Thrown when is null. - public static AnyList ListOf(IAny item) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - - return new AnyList(AnyDerivation.SourceOf(item), CollectionState.Create(item, false, null)); - } - - /// - /// Starts an arbitrary array (T[]) generator over — same constraint surface as - /// , producing an array. - /// - /// The generator each element is drawn from. - /// The element type. - /// An array generator to constrain fluently. - /// Thrown when is null. - public static AnyArray ArrayOf(IAny item) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - - return new AnyArray(AnyDerivation.SourceOf(item), CollectionState.Create(item, false, null)); - } - - /// - /// Starts an arbitrary generator over — same constraint - /// surface as . The generated sequence is fully materialized, so it never re-draws when - /// enumerated more than once. - /// - /// The generator each element is drawn from. - /// The element type. - /// A sequence generator to constrain fluently. - /// Thrown when is null. - public static AnySequence SequenceOf(IAny item) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - - return new AnySequence(AnyDerivation.SourceOf(item), CollectionState.Create(item, false, null)); - } - - /// - /// Starts an arbitrary generator over — distinct by nature. - /// When the count exceeds the number of distinct values can produce, the conflict is - /// reported eagerly. - /// - /// The generator each element is drawn from. - /// The element type. - /// A set generator to constrain fluently. - /// Thrown when is null. - public static AnySet SetOf(IAny item) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - - return new AnySet(AnyDerivation.SourceOf(item), CollectionState.Create(item, true, null)); - } - - /// - /// Starts an arbitrary generator over , deduplicating - /// elements with — the same comparer the resulting set carries. - /// - /// The generator each element is drawn from. - /// The equality comparer deciding whether two elements are the same. - /// The element type. - /// A set generator to constrain fluently. - /// Thrown when or is null. - public static AnySet SetOf(IAny item, IEqualityComparer comparer) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } - - return new AnySet(AnyDerivation.SourceOf(item), CollectionState.Create(item, true, comparer)); - } - - /// - /// Starts an arbitrary generator drawing keys from - /// and values from . Keys are distinct by nature, so the key - /// generator's domain gates feasibility exactly as it does for . - /// - /// The generator each key is drawn from. - /// The generator each value is drawn from. - /// The key type. - /// The value type. - /// A dictionary generator to constrain fluently. - /// Thrown when or is null. - public static AnyDictionary DictionaryOf(IAny keys, IAny values) - where TKey : notnull { - if (keys is null) { throw new ArgumentNullException(nameof(keys)); } - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - RandomSource? source = AnyDerivation.SourceOf(keys) ?? AnyDerivation.SourceOf(values); - - return new AnyDictionary(source, CollectionState.Create(keys, true, null), values); - } - - /// - /// Starts an arbitrary generator whose keys are deduplicated with - /// — the same comparer the resulting dictionary carries. - /// - /// The generator each key is drawn from. - /// The generator each value is drawn from. - /// The equality comparer deciding whether two keys are the same. - /// The key type. - /// The value type. - /// A dictionary generator to constrain fluently. - /// Thrown when any argument is null. - public static AnyDictionary DictionaryOf(IAny keys, IAny values, IEqualityComparer keyComparer) - where TKey : notnull { - if (keys is null) { throw new ArgumentNullException(nameof(keys)); } - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (keyComparer is null) { throw new ArgumentNullException(nameof(keyComparer)); } - - RandomSource? source = AnyDerivation.SourceOf(keys) ?? AnyDerivation.SourceOf(values); - - return new AnyDictionary(source, CollectionState.Create(keys, true, keyComparer), values); - } - -} diff --git a/JustDummies/Any.Combine.cs b/JustDummies/Any.Combine.cs deleted file mode 100644 index e17b6cab..00000000 --- a/JustDummies/Any.Combine.cs +++ /dev/null @@ -1,352 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Composes two generators into one through a constructor lambda — the reflection-free way to assemble an - /// object from constrained parts. Each part draws from its own random context when the composed generator - /// generates. - /// - /// - /// - /// - /// IAny<Customer> customer = Any.Combine( - /// Any.String().NonEmpty().WithMaxLength(50), - /// Any.String().StartingWith("ORD-").WithLength(12), - /// (name, reference) => new Customer(name, OrderReference.Create(reference))); - /// - /// - /// - /// The generator of the first part. - /// The generator of the second part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - public static IAny Combine(IAny first, IAny second, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)})"); - }); - } - - /// - /// Composes three generators into one through a constructor lambda — see - /// . - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)})"); - }); - } - - /// - /// Composes four generators into one through a constructor lambda — see . - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The generator of the fourth part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the fourth part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, IAny fourth, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - T4 fourthValue = fourth.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)})"); - }); - } - - /// - /// Composes five generators into one through a constructor lambda — see . - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The generator of the fourth part. - /// The generator of the fifth part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the fourth part. - /// The type of the fifth part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, IAny fourth, IAny fifth, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } - if (fifth is null) { throw new ArgumentNullException(nameof(fifth)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - T4 fourthValue = fourth.Generate(); - T5 fifthValue = fifth.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)})"); - }); - } - - /// - /// Composes six generators into one through a constructor lambda — see . - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The generator of the fourth part. - /// The generator of the fifth part. - /// The generator of the sixth part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the fourth part. - /// The type of the fifth part. - /// The type of the sixth part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, IAny fourth, IAny fifth, IAny sixth, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } - if (fifth is null) { throw new ArgumentNullException(nameof(fifth)); } - if (sixth is null) { throw new ArgumentNullException(nameof(sixth)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - T4 fourthValue = fourth.Generate(); - T5 fifthValue = fifth.Generate(); - T6 sixthValue = sixth.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)})"); - }); - } - - /// - /// Composes seven generators into one through a constructor lambda — see . - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The generator of the fourth part. - /// The generator of the fifth part. - /// The generator of the sixth part. - /// The generator of the seventh part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the fourth part. - /// The type of the fifth part. - /// The type of the sixth part. - /// The type of the seventh part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, IAny fourth, IAny fifth, IAny sixth, IAny seventh, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } - if (fifth is null) { throw new ArgumentNullException(nameof(fifth)); } - if (sixth is null) { throw new ArgumentNullException(nameof(sixth)); } - if (seventh is null) { throw new ArgumentNullException(nameof(seventh)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth) ?? AnyDerivation.SourceOf(seventh); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source) && AnyDerivation.DrawsOnlyFrom(seventh, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - T4 fourthValue = fourth.Generate(); - T5 fifthValue = fifth.Generate(); - T6 sixthValue = sixth.Generate(); - T7 seventhValue = seventh.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)})"); - }); - } - - /// - /// Composes eight generators into one through a constructor lambda — see . - /// Eight is the ceiling; a constructor needing more parts is better assembled from intermediate value objects. - /// - /// The generator of the first part. - /// The generator of the second part. - /// The generator of the third part. - /// The generator of the fourth part. - /// The generator of the fifth part. - /// The generator of the sixth part. - /// The generator of the seventh part. - /// The generator of the eighth part. - /// The constructor lambda assembling the parts. - /// The type of the first part. - /// The type of the second part. - /// The type of the third part. - /// The type of the fourth part. - /// The type of the fifth part. - /// The type of the sixth part. - /// The type of the seventh part. - /// The type of the eighth part. - /// The type of the composed value. - /// A generator of the composed value. - /// Thrown when any argument is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "Heterogeneous composition needs one type parameter per part plus the result; the arity-8 ceiling is a deliberate ergonomic " + - "decision (ADR-0015), and nesting Combine calls to stay under three would bury the shape of the value being composed.")] - public static IAny Combine(IAny first, IAny second, IAny third, IAny fourth, IAny fifth, IAny sixth, IAny seventh, IAny eighth, Func compose) { - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - if (third is null) { throw new ArgumentNullException(nameof(third)); } - if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } - if (fifth is null) { throw new ArgumentNullException(nameof(fifth)); } - if (sixth is null) { throw new ArgumentNullException(nameof(sixth)); } - if (seventh is null) { throw new ArgumentNullException(nameof(seventh)); } - if (eighth is null) { throw new ArgumentNullException(nameof(eighth)); } - if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth) ?? AnyDerivation.SourceOf(seventh) ?? AnyDerivation.SourceOf(eighth); - bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source) && AnyDerivation.DrawsOnlyFrom(seventh, source) && AnyDerivation.DrawsOnlyFrom(eighth, source); - - return new DerivedAny(source, reproducible, () => { - T1 firstValue = first.Generate(); - T2 secondValue = second.Generate(); - T3 thirdValue = third.Generate(); - T4 fourthValue = fourth.Generate(); - T5 fifthValue = fifth.Generate(); - T6 sixthValue = sixth.Generate(); - T7 seventhValue = seventh.Generate(); - T8 eighthValue = eighth.Generate(); - - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue, eighthValue), source, reproducible, () => $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)}, {AnyDerivation.Display(eighthValue)})"); - }); - } - - /// - /// Composes two generators into a generator of the value tuple (, - /// ) — sugar over for the common case of - /// pairing two arbitrary values. - /// - /// The generator of the first component. - /// The generator of the second component. - /// The type of the first component. - /// The type of the second component. - /// A generator of the paired value. - /// Thrown when any argument is null. - public static IAny<(T1, T2)> PairOf(IAny first, IAny second) { - return Combine(first, second, (one, two) => (one, two)); - } - - /// - /// Composes three generators into a generator of the value tuple (, - /// , ) — sugar over - /// . - /// - /// The generator of the first component. - /// The generator of the second component. - /// The generator of the third component. - /// The type of the first component. - /// The type of the second component. - /// The type of the third component. - /// A generator of the tripled value. - /// Thrown when any argument is null. - public static IAny<(T1, T2, T3)> TripleOf(IAny first, IAny second, IAny third) { - return Combine(first, second, third, (one, two, three) => (one, two, three)); - } - -} diff --git a/JustDummies/Any.Pattern.cs b/JustDummies/Any.Pattern.cs deleted file mode 100644 index 64564380..00000000 --- a/JustDummies/Any.Pattern.cs +++ /dev/null @@ -1,62 +0,0 @@ -#region Usings declarations - -using System.Text.RegularExpressions; - -#endregion - -namespace JustDummies; - -public static partial class Any { - - /// - /// Starts a generator of arbitrary strings that match , drawing from the - /// ambient random context. The pattern is the whole shape of the specification — the returned generator carries - /// no further shape or length constraints; express those inside the pattern. It does carry the exclusion pair - /// Except/DifferentFrom, which rejects rather than constructs, and it composes through - /// As(...), OrNull(), Combine(...) and the collection generators. - /// - /// - /// Supported is the regular subset of the pattern language: literals and escapes (metacharacters, - /// control characters, \xHH, \uHHHH), the shorthands \d \D \w \W \s \S, character classes - /// (ranges, negation), the quantifiers ? * + {n} {n,} {n,m} (an unbounded quantifier draws its minimum - /// plus 0 to 8 repetitions), alternation, grouping (capturing, non-capturing and named), the dot, and the - /// anchors ^ $ at the start and end of the pattern or of a top-level alternation branch (no-ops there, - /// since a whole matching string is generated). Wherever the pattern leaves a character free, values are drawn - /// from printable ASCII (\s may also yield a tab); a character the pattern names explicitly is emitted as - /// written, control characters included. A well-formed but - /// non-regular or not-generatable construct — a lookaround, a backreference, a word boundary, a Unicode - /// category, an atomic group, a class subtraction, an anchor placed where it could never match — raises an - /// ; a malformed pattern raises an , - /// mirroring what the real engine rejects. - /// - /// The regular expression the generated strings must match. - /// A generator of strings matching the pattern. - /// Thrown when is null. - /// Thrown when is not a well-formed pattern. - /// Thrown when uses a construct outside the supported regular subset. - public static AnyPattern StringMatching(string pattern) { - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - - return AnyPattern.FromPattern(AmbientRandomSource.Instance, pattern, ignoreCase: false); - } - - /// - /// Starts a generator of arbitrary strings matching — the same contract as - /// , taking a compiled so a test can reuse the very - /// object its production code validates with. is honoured. - /// changes how the pattern text itself is read and is - /// rejected; the remaining options do not change which strings the pattern matches and are ignored. - /// - /// The regular expression the generated strings must match. - /// A generator of strings matching the pattern. - /// Thrown when is null. - /// Thrown when is not a well-formed pattern, or carries . - /// Thrown when uses a construct outside the supported regular subset. - public static AnyPattern StringMatching(Regex pattern) { - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - if ((pattern.Options & RegexOptions.IgnorePatternWhitespace) != 0) { throw new ArgumentException("RegexOptions.IgnorePatternWhitespace changes how the pattern text is read; pass the pattern without it (or with its whitespace and comments removed).", nameof(pattern)); } - - return AnyPattern.FromPattern(AmbientRandomSource.Instance, pattern.ToString(), (pattern.Options & RegexOptions.IgnoreCase) != 0); - } - -} diff --git a/JustDummies/Any.Primitive.cs b/JustDummies/Any.Primitive.cs deleted file mode 100644 index 41588ea8..00000000 --- a/JustDummies/Any.Primitive.cs +++ /dev/null @@ -1,241 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Starts an arbitrary generator drawing from the ambient random context. Unconstrained, - /// it yields a string of 0 to 16 ASCII letters and digits; chain constraints to express what the surrounding - /// code requires (NonEmpty(), WithLength(...), StartingWith(...), ...). - /// - /// A string generator to constrain fluently. - public static AnyString String() { - return new AnyString(AmbientRandomSource.Instance, StringSpec.Unconstrained); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context. Unconstrained, it - /// draws from the full range; chain constraints to express what the surrounding code - /// requires (Positive(), Between(...), ...). - /// - /// An integer generator to constrain fluently. - public static AnyInt32 Int32() { - return AnyInt32.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public static AnySByte SByte() { - return AnySByte.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public static AnyByte Byte() { - return AnyByte.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public static AnyInt16 Int16() { - return AnyInt16.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public static AnyUInt16 UInt16() { - return AnyUInt16.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public static AnyUInt32 UInt32() { - return AnyUInt32.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public static AnyInt64 Int64() { - return AnyInt64.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public static AnyUInt64 UInt64() { - return AnyUInt64.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained, negative durations included. Same constraint algebra as , less MultipleOf(...) and plus WithGranularity(...). - /// - /// A generator to constrain fluently. - public static AnyTimeSpan TimeSpan() { - return AnyTimeSpan.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// any representable instant unless constrained; generated values carry Utc kind. Same constraint algebra as - /// with the bounds renamed After(...)/Before(...): no sign or zero - /// constraint, no MultipleOf(...), plus WithGranularity(...). - /// - /// A generator to constrain fluently. - public static AnyDateTime DateTime() { - return AnyDateTime.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// any representable instant unless constrained; generated values carry a zero (UTC) offset. Same constraint - /// algebra as with the bounds renamed After(...)/Before(...): no sign or - /// zero constraint, no MultipleOf(...), plus WithGranularity(...) and WithOffset(...). - /// - /// A generator to constrain fluently. - public static AnyDateTimeOffset DateTimeOffset() { - return AnyDateTimeOffset.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// finite values only — NaN and infinities are never generated. Same constraint algebra as , less MultipleOf(...). - /// - /// A generator to constrain fluently. - public static AnyDouble Double() { - return AnyDouble.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// finite values only — NaN and infinities are never generated. Same constraint algebra as , less MultipleOf(...). - /// - /// A generator to constrain fluently. - public static AnySingle Single() { - return AnySingle.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context: - /// full range unless constrained. Same constraint algebra as , less - /// MultipleOf(...) and plus WithScale(...). - /// - /// A generator to constrain fluently. - public static AnyDecimal Decimal() { - return AnyDecimal.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — an even coin - /// flip unless pinned with True() or False(). - /// - /// A generator to constrain fluently. - public static AnyBoolean Boolean() { - return AnyBoolean.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — unlike - /// , reproducible inside an Any.Reproducibly(...) run, and for every - /// practical purpose never empty. - /// - /// A generator to constrain fluently. - public static AnyGuid Guid() { - return AnyGuid.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — - /// uniformly across the enum's declared members, never an undeclared numeric value. - /// - /// The enum type to draw values from. - /// A generator to constrain fluently. - /// Thrown when declares no members. - public static AnyEnum Enum() - where TEnum : struct, Enum { - return AnyEnum.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — ASCII letters - /// and digits unless constrained, mirroring 's character families. - /// - /// A generator to constrain fluently. - public static AnyChar Char() { - return AnyChar.Create(AmbientRandomSource.Instance); - } - -#if NET8_0_OR_GREATER - /// - /// Starts an arbitrary generator drawing from the ambient random context — any - /// representable date unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public static AnyDateOnly DateOnly() { - return AnyDateOnly.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — any - /// time of day unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public static AnyTimeOnly TimeOnly() { - return AnyTimeOnly.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — full - /// range unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public static AnyInt128 Int128() { - return AnyInt128.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — full - /// range unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public static AnyUInt128 UInt128() { - return AnyUInt128.Create(AmbientRandomSource.Instance); - } - - /// - /// Starts an arbitrary generator drawing from the ambient random context — finite - /// values only. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public static AnyHalf Half() { - return AnyHalf.Create(AmbientRandomSource.Instance); - } -#endif - -} diff --git a/JustDummies/Any.Reproducibility.cs b/JustDummies/Any.Reproducibility.cs deleted file mode 100644 index 6d8e83f4..00000000 --- a/JustDummies/Any.Reproducibility.cs +++ /dev/null @@ -1,224 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Creates an isolated, deterministic generation context: every generator created from it draws from a - /// dedicated source seeded with , independent of the ambient context. Two contexts - /// created with the same seed yield the same sequence of values. Prefer - /// inside tests — it keeps the arbitrary-by-default - /// behavior and reports the seed only when the test fails; reach for when you need an - /// explicit generator object, for example outside a test body. - /// - /// - /// A context is safe to draw from concurrently, but sharing one across threads costs the replay rather than - /// the values: interleaved draws make neither the sequence nor the multiset stable across runs. Keep a - /// context to one thread at a time, or give each unit of work its own scope. - /// - /// The seed pinning the context's value sequence. - /// A deterministic generation context. - public static AnyContext WithSeed(int seed) { - return new AnyContext(seed); - } - - /// - /// Pins the ambient random context to until the returned handle is disposed — the - /// scope form of , for a caller that cannot wrap the - /// code it pins in a delegate. A test-framework adapter is the case this exists for: it observes a test through - /// hooks that run before and after it, so it opens the scope in one and disposes it in the other. - /// - /// - /// - /// Inside a test body, prefer : it also reports the seed - /// when the body fails, which this handle does not — whoever opens the scope owns telling the reader which - /// seed to replay. Prefer when an explicit generator object fits better than an - /// ambient scope. - /// - /// - /// Like the ambient context itself, the scope flows with the current execution context, so it never leaks - /// across tests running in parallel. Scopes nest: disposing restores whatever was pinned before, and - /// disposing twice is harmless. Failing to dispose leaves the seed pinned for whatever runs next in the - /// same execution context. - /// - /// - /// Flowing with the execution context also means a scope opened around a parallel loop reaches every - /// worker, which is what makes this the seam for a reproducible parallel run: draws are safe under - /// concurrency but interleave, so one shared scope replays nothing, whereas a scope opened inside - /// the loop body gives each unit of work its own sequence and the whole run replays. - /// - /// - /// - /// const int runSeed = 20240501; // recorded by hand: keep it to replay, change it to explore - /// Parallel.For(0, 64, index => { - /// // a distinct, deterministic sub-seed per work item, floor-safe on netstandard2.0 (no System.HashCode) - /// using (Any.UseSeed(unchecked(runSeed * 397 ^ index))) { - /// sut.Handle(Any.String().NonEmpty().Generate()); - /// } - /// }); - /// - /// - /// - /// The seed pinning the ambient context's value sequence. - /// A handle that restores the previous ambient context when disposed. - public static IDisposable UseSeed(int seed) { - return AmbientRandomSource.UseSeed(seed); - } - - /// - /// Pins the ambient random context to and supplies the replay snippet — the - /// code a reader copies to replay this run — that generation-failure guidance will embed. This is the form a - /// test-framework adapter uses: the default snippet is Any.Reproducibly(seed, ...), which points at a - /// call a test pinned from outside its own body does not contain, where replaying means changing what the - /// adapter reads instead. - /// - /// - /// - /// A failure's guidance is one sentence embedding this snippet, so pass the code itself — an attribute with - /// its seed argument, a runner setting — not a sentence about it. It is quoted verbatim and validated only - /// for being non-blank: a badly phrased snippet degrades the very diagnostic it is meant to improve. - /// - /// - /// - /// using (Any.UseSeed(1234, "[Reproducible(Seed = 1234)]")) { /* ... */ } - /// // A generation failure then reads: - /// // The arbitrary values were seeded with 1234; reproduce this run with [Reproducible(Seed = 1234)]. - /// - /// - /// - /// Everything else matches : the scope flows with the execution context, nests, - /// and restores the previous ambient context when disposed. - /// - /// - /// The seed pinning the ambient context's value sequence. - /// The code a reader copies to replay this run, quoted verbatim into generation-failure guidance. - /// A handle that restores the previous ambient context when disposed. - /// Thrown when is null. - /// Thrown when is empty or white space. - public static IDisposable UseSeed(int seed, string replaySnippet) { - if (replaySnippet is null) { throw new ArgumentNullException(nameof(replaySnippet)); } - if (replaySnippet.Trim().Length == 0) { throw new ArgumentException("The replay snippet must be the code a reader copies to replay the run; pass a non-blank snippet, or use the overload without one to name Any.Reproducibly(seed, ...).", nameof(replaySnippet)); } - - return AmbientRandomSource.UseSeed(seed, replaySnippet); - } - - /// - /// Runs with the ambient random context pinned to a fresh seed and, if the body - /// throws, reports that seed before letting the exception propagate. This is how a test that draws on - /// stays reproducible: the values still vary between runs (which surfaces accidental - /// dependencies), yet a failure names the exact seed to replay. - /// - /// - /// - /// On failure the seed is written to (by default ), - /// with a message naming the Any.Reproducibly(seed, ...) call that reproduces the run. Pass your - /// test framework's output writer (for example xUnit's ITestOutputHelper.WriteLine) to route it - /// there instead. The original exception is rethrown unchanged, so the test still fails with its real - /// message. - /// - /// - /// Reproducing a run needs the same sequence of draws, so a body whose generation order depends on - /// non-deterministic external state is not fully replayable from the seed alone. - /// - /// - /// The test body to run under a reproducible random context. - /// The sink the seed is written to on failure. Defaults to when null. - /// Thrown when is null. - public static void Reproducibly(Action body, Action? report = null) { - Reproducibly(AmbientRandomSource.NewSeed(), body, report); - } - - /// - /// Replays with the ambient random context pinned to , so a - /// run first seen through the parameterless overload can - /// be reproduced exactly. If the body throws, the seed is reported before the exception propagates. - /// - /// The seed to replay — typically the one a previous failure reported. - /// The test body to run under the seeded random context. - /// The sink the seed is written to on failure. Defaults to when null. - /// Thrown when is null. - public static void Reproducibly(int seed, Action body, Action? report = null) { - if (body is null) { throw new ArgumentNullException(nameof(body)); } - - using (AmbientRandomSource.UseSeed(seed)) { - try { - body(); - } catch { - Report(report, seed); - - throw; - } - } - } - - /// - /// Asynchronous counterpart of : awaits - /// under a fresh seed and reports it if the body faults. - /// - /// - /// The returned task must be awaited. Dropping it silences the body's failures — the assertions run - /// on a continuation after the caller has already moved on, and a discarded fault never reaches the test - /// runner. Discarding it is a compile error (diagnostic JD002); passing an asynchronous body to the - /// synchronous instead is a compile error (JD001). - /// - /// The asynchronous test body to run under a reproducible random context. - /// The sink the seed is written to on failure. Defaults to when null. - /// A task that completes when completes, and faults with the body's exception. - /// Thrown when is null. - public static Task ReproduciblyAsync(Func body, Action? report = null) { - if (body is null) { throw new ArgumentNullException(nameof(body)); } - - return ReproduciblyAsync(AmbientRandomSource.NewSeed(), body, report); - } - - /// - /// Asynchronous counterpart of : awaits - /// under and reports it if the body faults. - /// - /// The seed to replay — typically the one a previous failure reported. - /// The asynchronous test body to run under the seeded random context. - /// The sink the seed is written to on failure. Defaults to when null. - /// A task that completes when completes. - /// Thrown when is null. - public static Task ReproduciblyAsync(int seed, Func body, Action? report = null) { - if (body is null) { throw new ArgumentNullException(nameof(body)); } - - return RunReproduciblyAsync(seed, body, report); - } - - // Kept separate from the public entry so the null-argument guard above throws synchronously at the call site, - // rather than being deferred into the returned task's fault — which a caller who forgets to await would miss. - private static async Task RunReproduciblyAsync(int seed, Func body, Action? report) { - using (AmbientRandomSource.UseSeed(seed)) { - try { - await body().ConfigureAwait(false); - } catch { - Report(report, seed); - - throw; - } - } - } - - private static void Report(Action? report, int seed) { - string message = $"[JustDummies] These arbitrary values were seeded with {seed}. Reproduce this run with Any.Reproducibly({seed}, ...)."; - - // The seed report is a best-effort diagnostic aid, called while an exception is already propagating: a - // caller-supplied sink that throws must never mask the failure the seed exists to help diagnose. Try the - // caller's sink first; if it throws, fall back to the default console sink so the seed still surfaces, and - // swallow even the fallback's failure so the body's exception always propagates unchanged. - if (report is not null && TryWrite(report, message)) { return; } - - TryWrite(Console.Error.WriteLine, message); - } - - private static bool TryWrite(Action sink, string message) { - try { - sink(message); - - return true; - } catch { - return false; - } - } - -} diff --git a/JustDummies/Any.Uri.cs b/JustDummies/Any.Uri.cs deleted file mode 100644 index 4b9e9ea3..00000000 --- a/JustDummies/Any.Uri.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace JustDummies; - -public static partial class Any { - - /// - /// Starts an arbitrary generator drawing from the ambient random context. - /// Unconstrained, it yields any valid URI from the safe space — an absolute web (http/https), - /// WebSocket (ws/wss), FTP or mailto URI, or a relative reference. Narrow it to a family - /// (Web(), WebSocket(), Ftp(), Mailto(), Relative()) to reach that family's - /// component constraints; each narrowing returns a builder exposing only that family's valid components. - /// - /// A URI generator to narrow fluently. - public static AnyUri Uri() { - return new AnyUri(AmbientRandomSource.Instance, UriSpec.Unconstrained); - } - -} diff --git a/JustDummies/Any.cs b/JustDummies/Any.cs deleted file mode 100644 index 8c525307..00000000 --- a/JustDummies/Any.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace JustDummies; - -/// -/// The entry point of the library: supplies arbitrary, valid values for the parts of a test that are not -/// under assertion — the dummies a test needs so its Arrange stops advertising values it never -/// checks. The constraints chained on a generator express what the surrounding code requires of the value (a -/// value object's invariant, a contract precondition), never what the test asserts: an explicit -/// call reads as "this is arbitrary" where a hand-picked literal reads as "this matters". -/// -/// -/// -/// Values are built to satisfy the declared constraints — the library never generates candidates and -/// filters them afterwards. Constraints that contradict each other fail at declaration time with a -/// naming both sides. -/// -/// -/// Every value is drawn from a pseudo-random source. By default that source is unseeded, so each run produces -/// fresh values — which surfaces a test that secretly depends on one. Wrap a value-sensitive test in -/// to make a failing run replayable: the source flows with -/// the current execution context, so it never leaks across tests running in parallel. For an explicit, -/// isolated deterministic context — for example outside a test body — use . -/// -/// -/// -/// // The reference format is the invariant; the exact value is irrelevant — so it is Any. -/// string reference = Any.String().StartingWith("ORD-").WithLength(12).Generate(); -/// -/// // Turn a constrained primitive into a value object, without reflection: -/// OrderReference order = Any.String().StartingWith("ORD-").WithLength(12) -/// .As(OrderReference.Create) -/// .Generate(); -/// -/// // Make a value-sensitive test replayable: the seed is reported on failure... -/// Any.Reproducibly(() => { /* arrange with Any, act, assert */ }); -/// // ...and replayed by passing it back: -/// Any.Reproducibly(1234, () => { /* ... */ }); -/// -/// -/// -// This file carries the façade's documentation and no member: every entry point lives in a sibling partial named -// after its family (Any.Primitive.cs, Any.Pattern.cs, Any.Uri.cs, Any.Choice.cs, Any.Collection.cs, Any.Combine.cs, -// Any.Reproducibility.cs). A family gets its own file as soon as its entry point returns a *narrowing builder* rather -// than a constrained scalar, however few members that leaves here — the file's weight is the surface it opens, not -// its line count. Adding a member to any of them means mirroring it on AnyContext; SurfaceParityTests enforces that. -public static partial class Any { } diff --git a/JustDummies/AnyArray.cs b/JustDummies/AnyArray.cs deleted file mode 100644 index b136ccdd..00000000 --- a/JustDummies/AnyArray.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary array (T[]) values over an element generator. Shares the collection -/// constraint surface () — count bounds and contained values — -/// and adds to require pairwise-distinct elements. -/// -/// The element type. -public sealed class AnyArray : AnyCollection> { - - internal AnyArray(RandomSource? source, CollectionState state) : base(source, state) { } - - /// Requires the elements to be pairwise distinct (default equality). - /// A new generator carrying the added constraint. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnyArray Distinct() { - return With(State.AsDistinct(null, ConstraintCall.Of(nameof(Distinct)))); - } - - /// Requires the elements to be pairwise distinct under . - /// The equality comparer deciding whether two elements are the same. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnyArray Distinct(IEqualityComparer comparer) { - if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } - - return With(State.AsDistinct(comparer, ConstraintCall.Of(nameof(Distinct), "comparer"))); - } - - private protected override AnyArray With(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return new AnyArray(SourceOrNull, state); - } - - private protected override T[] Build(List items) { - if (items is null) { throw new ArgumentNullException(nameof(items)); } - - return items.ToArray(); - } - -} diff --git a/JustDummies/AnyBoolean.cs b/JustDummies/AnyBoolean.cs deleted file mode 100644 index 2205ad67..00000000 --- a/JustDummies/AnyBoolean.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values. True() and False() pin the value — -/// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a -/// naming both sides, like every other generator. -/// -public sealed class AnyBoolean : IAny, IHasRandomSource, ICardinalityHint { - - /// How many values has: false and true, and nothing else. - private const int BooleanValueCount = 2; - - /// How many values a pin leaves producible — the one it fixed. - private const int PinnedCardinality = 1; - - #region Statics members declarations - - internal static AnyBoolean Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyBoolean(source, null, null); - } - - private static string V(bool value) { - return value ? "true" : "false"; - } - - #endregion - - #region Fields declarations - - private readonly bool? _pinned; - private readonly ConstraintCall? _pinnedConstraint; - private readonly RandomSource _source; - - #endregion - - private AnyBoolean(RandomSource source, bool? pinned, ConstraintCall? pinnedConstraint) { - _source = source; - _pinned = pinned; - _pinnedConstraint = pinnedConstraint; - } - - RandomSource? IHasRandomSource.Source => _source; - - // Two distinct values unless a pin has already fixed one of them. - long? ICardinalityHint.DistinctCardinality => _pinned is null ? BooleanValueCount : PinnedCardinality; - - // A pin narrows the domain to that single value; unpinned, both booleans are producible. - bool ICardinalityHint.Contains(bool value) => _pinned is not bool pinned || pinned == value; - - /// Pins the value to true. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyBoolean True() { - return Pin(true, ConstraintCall.Of(nameof(True))); - } - - /// Pins the value to false. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyBoolean False() { - return Pin(false, ConstraintCall.Of(nameof(False))); - } - - /// - /// Requires the value to differ from — which, for a boolean, pins it to the - /// opposite. The name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyBoolean DifferentFrom(bool value) { - return Pin(!value, ConstraintCall.Of(nameof(DifferentFrom), V(value))); - } - - /// - public bool Generate() { - return _pinned ?? _source.Current.Next(BooleanValueCount) == 0; - } - - private AnyBoolean Pin(bool value, ConstraintCall applying) { - if (_pinnedConstraint is not null && _pinned != value) { - throw ConflictingAnyConstraintException.AlreadyPinned(applying, _pinnedConstraint, V(_pinned!.Value)); - } - - return new AnyBoolean(_source, value, applying); - } - -} diff --git a/JustDummies/AnyByte.cs b/JustDummies/AnyByte.cs deleted file mode 100644 index 52eb4ad7..00000000 --- a/JustDummies/AnyByte.cs +++ /dev/null @@ -1,178 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyByte Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyByte(source, OrdinalIntervalSpec.Unconstrained("Byte", ordinal => V(Val(ordinal)), Ord(byte.MinValue), Ord(byte.MaxValue))); - } - - private static ulong Ord(byte value) { - return value; - } - - private static byte Val(ulong ordinal) { - return (byte)ordinal; - } - - private static string V(byte value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(byte[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyByte(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(byte value) => _spec.Contains(Ord(value)); - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte Zero() { - return new AnyByte(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte NonZero() { - return new AnyByte(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte GreaterThan(byte value) { - return new AnyByte(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte GreaterThanOrEqualTo(byte value) { - return new AnyByte(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte LessThan(byte value) { - return new AnyByte(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte LessThanOrEqualTo(byte value) { - return new AnyByte(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte Between(byte minimum, byte maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyByte(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte MultipleOf(byte value) { - if (value == 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyByte(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte OneOf(params byte[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyByte(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte Except(params byte[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyByte(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyByte DifferentFrom(byte value) { - return new AnyByte(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public byte Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyChar.cs b/JustDummies/AnyChar.cs deleted file mode 100644 index 99fed9ea..00000000 --- a/JustDummies/AnyChar.cs +++ /dev/null @@ -1,226 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values. Unconstrained, it draws from ASCII letters and -/// digits — the same readable default as 's filler — and the constraints mirror the -/// string character families: , , , -/// , , plus / / -/// . A combination that empties the pool fails eagerly with a -/// . -/// -public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyChar Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyChar(source, null, null, null, null, null, null, []); - } - - private static string V(char value) { - return $"'{value}'"; - } - - private static string Join(char[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly List _pool; - private readonly LetterCasing? _casing; - private readonly ConstraintCall? _casingConstraint; - private readonly CharacterSet? _charset; - private readonly ConstraintCall? _charsetConstraint; - private readonly IReadOnlyList _excluded; - private readonly RandomSource _source; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private AnyChar(RandomSource source, - CharacterSet? charset, ConstraintCall? charsetConstraint, - LetterCasing? casing, ConstraintCall? casingConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, - IReadOnlyList excluded) { - _source = source; - _charset = charset; - _charsetConstraint = charsetConstraint; - _casing = casing; - _casingConstraint = casingConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _excluded = excluded; - // Materialized once here — "constrain once, draw many": Generate never refilters the pool. The full - // constant pool is the unconstrained start; MatchesCharset narrows it, so no per-charset pre-narrowing - // is needed. - IEnumerable candidates = allowed ?? (IEnumerable)(CharacterPools.UpperLetters + CharacterPools.LowerLetters + CharacterPools.Digits); - _pool = candidates.Where(character => MatchesCharset(character) && MatchesCasing(character) && !excluded.Contains(character)).ToList(); - } - - RandomSource? IHasRandomSource.Source => _source; - - // The pool is materialized once at construction, so its size is the exact number of characters drawable. - long? ICardinalityHint.DistinctCardinality => _pool.Count; - - // The pool is the exact draw set, so membership is a direct pool lookup. - bool ICardinalityHint.Contains(char value) => _pool.Contains(value); - - /// Restricts the character to ASCII letters only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar Alpha() { - return WithCharset(CharacterSet.Alpha, ConstraintCall.Of(nameof(Alpha))); - } - - /// Restricts the character to ASCII digits only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar Numeric() { - return WithCharset(CharacterSet.Numeric, ConstraintCall.Of(nameof(Numeric))); - } - - /// Restricts the character to ASCII letters and digits only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar AlphaNumeric() { - return WithCharset(CharacterSet.AlphaNumeric, ConstraintCall.Of(nameof(AlphaNumeric))); - } - - /// Requires an alphabetic character to be lowercase. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar LowerCase() { - return WithCasing(LetterCasing.Lower, ConstraintCall.Of(nameof(LowerCase))); - } - - /// Requires an alphabetic character to be uppercase. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar UpperCase() { - return WithCasing(LetterCasing.Upper, ConstraintCall.Of(nameof(UpperCase))); - } - - /// Requires the character to be one of the supplied values. Declared once per generator. - /// The allowed characters; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar OneOf(params char[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(OneOf), Join(values)); - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == constraint) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(constraint, _allowedConstraint); } - - return Validated(new AnyChar(_source, _charset, _charsetConstraint, _casing, _casingConstraint, values.Distinct().ToArray(), constraint, _excluded), constraint); - } - - /// Requires the character to be none of the supplied values. - /// The forbidden characters. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar Except(params char[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values))); - } - - /// - /// Requires the character to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The character the generated character must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyChar DifferentFrom(char value) { - return WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), V(value))); - } - - /// - public char Generate() { - return _pool[_source.Current.Next(_pool.Count)]; - } - - private AnyChar WithCharset(CharacterSet charset, ConstraintCall applying) { - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_charsetConstraint == applying) { return this; } - if (_charsetConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _charsetConstraint); } - - return Validated(new AnyChar(_source, charset, applying, _casing, _casingConstraint, _allowed, _allowedConstraint, _excluded), applying); - } - - private AnyChar WithCasing(LetterCasing casing, ConstraintCall applying) { - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_casingConstraint == applying) { return this; } - if (_casingConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _casingConstraint); } - - return Validated(new AnyChar(_source, _charset, _charsetConstraint, casing, applying, _allowed, _allowedConstraint, _excluded), applying); - } - - private AnyChar WithExcluded(char[] values, ConstraintCall applying) { - List excluded = [.. _excluded, .. values]; - - return Validated(new AnyChar(_source, _charset, _charsetConstraint, _casing, _casingConstraint, _allowed, _allowedConstraint, excluded), applying); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private AnyChar Validated(AnyChar candidate, ConstraintCall applying) { - if (candidate._pool.Count > 0) { return candidate; } - - string pool = candidate._allowedConstraint is null - ? "no character remains in the pool the declared constraints allow" - : $"no character {candidate._allowedConstraint} allows satisfies the constraints already defined"; - - throw ConflictingAnyConstraintException.NoValueRemains(applying, pool); - } - - private bool MatchesCharset(char character) { - return _charset switch { - CharacterSet.Alpha => CharacterPools.IsAsciiLetter(character), - CharacterSet.Numeric => CharacterPools.IsAsciiDigit(character), - CharacterSet.AlphaNumeric => CharacterPools.IsAsciiLetter(character) || CharacterPools.IsAsciiDigit(character), - _ => true - }; - } - - private bool MatchesCasing(char character) { - return _casing switch { - LetterCasing.Lower => character is not (>= 'A' and <= 'Z'), - LetterCasing.Upper => character is not (>= 'a' and <= 'z'), - _ => true - }; - } - -} diff --git a/JustDummies/AnyCollection.cs b/JustDummies/AnyCollection.cs deleted file mode 100644 index 0c7d6d66..00000000 --- a/JustDummies/AnyCollection.cs +++ /dev/null @@ -1,133 +0,0 @@ -namespace JustDummies; - -/// -/// The shared fluent surface of the collection generators. Every collection — a , -/// an , an or an — carries a -/// count and, optionally, values it must contain; the concrete generators add only how the elements are shaped -/// (a set is always distinct) and what type returns. -/// -/// -/// The contract matches the scalar generators: constraints express what the surrounding code requires of -/// the collection, never what the test asserts; instances are immutable recipes, each method returning a new -/// generator; and a combination that cannot be satisfied fails eagerly with a -/// naming both sides. Unconstrained, a collection holds 0 to 8 -/// elements — chain when the surrounding code requires content. -/// -/// The element type. -/// The collection type produces. -/// The concrete generator type, so the fluent methods return it. -[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", - Justification = - "TItem and TResult are the element type and the collection type they build; TSelf is the CRTP self-type that lets every fluent " + - "method return the concrete generator instead of this base. Dropping it would make each chained call return AnyCollection and force " + - "a cast at every step.")] -public abstract class AnyCollection : IAny, IHasRandomSource - where TSelf : AnyCollection { - - #region Fields declarations - - private protected readonly RandomSource? SourceOrNull; - private protected readonly CollectionState State; - - #endregion - - private protected AnyCollection(RandomSource? source, CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - SourceOrNull = source; - State = state; - } - - RandomSource? IHasRandomSource.Source => SourceOrNull; - - /// Requires at least one element. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf NonEmpty() { - return With(CountConstraints.NonEmpty(State)); - } - - /// Fixes the collection to no elements. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf Empty() { - return With(CountConstraints.Empty(State)); - } - - /// Fixes the exact number of elements. Declared once per generator. - /// The exact number of elements. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf WithCount(int count) { - return With(CountConstraints.WithCount(State, count)); - } - - /// Requires at least elements. - /// The inclusive minimum number of elements. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf WithMinCount(int count) { - return With(CountConstraints.WithMinCount(State, count)); - } - - /// Requires at most elements. - /// The inclusive maximum number of elements. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf WithMaxCount(int count) { - return With(CountConstraints.WithMaxCount(State, count)); - } - - /// Requires a number of elements within the inclusive range [, ]. - /// The inclusive minimum number of elements. - /// The inclusive maximum number of elements. - /// A new generator carrying the added constraint. - /// Thrown when a bound is negative. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf WithCountBetween(int minimum, int maximum) { - return With(CountConstraints.WithCountBetween(State, minimum, maximum)); - } - - /// - /// Requires the collection to contain . May be declared several times; each required - /// value takes one element's room. In a distinct collection the required values must themselves be distinct. - /// - /// The value the generated collection must contain. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf Containing(TItem value) { - return With(State.WithContaining(value, ConstraintCall.Of(nameof(Containing), AnyDerivation.Display(value)))); - } - - /// - /// Requires the collection to contain a value drawn from at generation time — - /// useful to force a particular shape of element into an otherwise arbitrary collection. Named apart from - /// to keep the two cases legible: pins a concrete value - /// known now, whereas this method draws one from a generator when the collection is built. - /// - /// The generator whose drawn value the collection must contain. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when the constraint contradicts a constraint already declared. - public TSelf ContainingAny(IAny generator) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - return With(State.WithContaining(generator, ConstraintCall.Of(nameof(ContainingAny), ""))); - } - - /// - public TResult Generate() { - return Build(State.Materialize(SourceOrNull ?? AmbientRandomSource.Instance)); - } - - /// Wraps a new state in the concrete generator type. - private protected abstract TSelf With(CollectionState state); - - /// Converts the materialized elements into the concrete collection type. - private protected abstract TResult Build(List items); - -} diff --git a/JustDummies/AnyContext.cs b/JustDummies/AnyContext.cs deleted file mode 100644 index 74a171e4..00000000 --- a/JustDummies/AnyContext.cs +++ /dev/null @@ -1,368 +0,0 @@ -#region Usings declarations - -using System.Text.RegularExpressions; - -#endregion - -namespace JustDummies; - -/// -/// An isolated, deterministic generation context created by : every generator created -/// from it draws from a dedicated source seeded with , independent of the ambient context the -/// static entry points use. Two contexts created with the same seed yield the same sequence -/// of values. -/// -/// -/// -/// Inside a test, prefer wrapping the body in Any.Reproducibly(...): it keeps values arbitrary by -/// default and reports a replayable seed only when the test fails. A context is the explicit-object -/// alternative for when that scope does not fit — generating a deterministic dataset outside a test body, -/// for example. -/// -/// -/// A context owns a single pseudo-random generator, and it is safe to draw from concurrently. What -/// parallelism costs is the replay, not the values: the draws of two threads interleave, so neither the -/// sequence nor the multiset of values a context produces is stable across runs once it is shared. A context -/// used from one thread at a time replays exactly; to keep a parallel run reproducible, give each unit of -/// work its own scope with rather than sharing one context across threads. -/// -/// -public sealed class AnyContext { - - #region Fields declarations - - private readonly FixedRandomSource _source; - - #endregion - - internal AnyContext(int seed) { - Seed = seed; - _source = new FixedRandomSource(seed); - } - - /// The seed pinning this context's value sequence. - public int Seed { get; } - - /// - /// Starts an arbitrary generator drawing from this context — same fluent surface as - /// , deterministic under this context's seed. - /// - /// A string generator to constrain fluently. - public AnyString String() { - return new AnyString(_source, StringSpec.Unconstrained); - } - - /// - /// Starts a generator of arbitrary strings matching drawing from this context — - /// same fluent surface as , deterministic under this context's seed. - /// - /// The regular expression the generated strings must match. - /// A generator of strings matching the pattern. - /// Thrown when is null. - /// Thrown when is not a well-formed pattern. - /// Thrown when uses a construct outside the supported regular subset. - public AnyPattern StringMatching(string pattern) { - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - - return AnyPattern.FromPattern(_source, pattern, ignoreCase: false); - } - - /// - /// Starts a generator of arbitrary strings matching drawing from this context — - /// same fluent surface as , deterministic under this context's seed. - /// is honoured; is - /// rejected; the remaining options are ignored. - /// - /// The regular expression the generated strings must match. - /// A generator of strings matching the pattern. - /// Thrown when is null. - /// Thrown when is not a well-formed pattern, or carries . - /// Thrown when uses a construct outside the supported regular subset. - public AnyPattern StringMatching(Regex pattern) { - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - if ((pattern.Options & RegexOptions.IgnorePatternWhitespace) != 0) { throw new ArgumentException("RegexOptions.IgnorePatternWhitespace changes how the pattern text is read; pass the pattern without it (or with its whitespace and comments removed).", nameof(pattern)); } - - return AnyPattern.FromPattern(_source, pattern.ToString(), (pattern.Options & RegexOptions.IgnoreCase) != 0); - } - - /// - /// Starts an arbitrary generator drawing from this context — same fluent surface as - /// , deterministic under this context's seed. - /// - /// A URI generator to narrow fluently. - public AnyUri Uri() { - return new AnyUri(_source, UriSpec.Unconstrained); - } - - /// - /// Starts an arbitrary generator drawing from this context — same fluent surface as - /// , deterministic under this context's seed. - /// - /// An integer generator to constrain fluently. - public AnyInt32 Int32() { - return AnyInt32.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public AnySByte SByte() { - return AnySByte.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public AnyByte Byte() { - return AnyByte.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public AnyInt16 Int16() { - return AnyInt16.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public AnyUInt16 UInt16() { - return AnyUInt16.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public AnyUInt32 UInt32() { - return AnyUInt32.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as . - /// - /// A generator to constrain fluently. - public AnyInt64 Int64() { - return AnyInt64.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as , less Positive() - /// and Negative(), which an unsigned type cannot express. - /// - /// A generator to constrain fluently. - public AnyUInt64 UInt64() { - return AnyUInt64.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained, negative durations included. Same constraint algebra as , less MultipleOf(...) and plus WithGranularity(...). - /// - /// A generator to constrain fluently. - public AnyTimeSpan TimeSpan() { - return AnyTimeSpan.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// any representable instant unless constrained; generated values carry Utc kind. Same constraint algebra as - /// with the bounds renamed After(...)/Before(...): no sign or zero - /// constraint, no MultipleOf(...), plus WithGranularity(...). - /// - /// A generator to constrain fluently. - public AnyDateTime DateTime() { - return AnyDateTime.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// any representable instant unless constrained; generated values carry a zero (UTC) offset. Same constraint - /// algebra as with the bounds renamed After(...)/Before(...): no sign or - /// zero constraint, no MultipleOf(...), plus WithGranularity(...) and WithOffset(...). - /// - /// A generator to constrain fluently. - public AnyDateTimeOffset DateTimeOffset() { - return AnyDateTimeOffset.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// finite values only — NaN and infinities are never generated. Same constraint algebra as , less MultipleOf(...). - /// - /// A generator to constrain fluently. - public AnyDouble Double() { - return AnyDouble.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// finite values only — NaN and infinities are never generated. Same constraint algebra as , less MultipleOf(...). - /// - /// A generator to constrain fluently. - public AnySingle Single() { - return AnySingle.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: - /// full range unless constrained. Same constraint algebra as , less - /// MultipleOf(...) and plus WithScale(...). - /// - /// A generator to constrain fluently. - public AnyDecimal Decimal() { - return AnyDecimal.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — an even coin - /// flip unless pinned with True() or False(). - /// - /// A generator to constrain fluently. - public AnyBoolean Boolean() { - return AnyBoolean.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — unlike - /// , reproducible inside an Any.Reproducibly(...) run, and for every - /// practical purpose never empty. - /// - /// A generator to constrain fluently. - public AnyGuid Guid() { - return AnyGuid.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — - /// uniformly across the enum's declared members, never an undeclared numeric value. - /// - /// The enum type to draw values from. - /// A generator to constrain fluently. - /// Thrown when declares no members. - public AnyEnum Enum() - where TEnum : struct, Enum { - return AnyEnum.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — ASCII letters - /// and digits unless constrained, mirroring 's character families. - /// - /// A generator to constrain fluently. - public AnyChar Char() { - return AnyChar.Create(_source); - } - -#if NET8_0_OR_GREATER - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — any - /// representable date unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public AnyDateOnly DateOnly() { - return AnyDateOnly.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — any - /// time of day unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public AnyTimeOnly TimeOnly() { - return AnyTimeOnly.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — full - /// range unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public AnyInt128 Int128() { - return AnyInt128.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — full - /// range unless constrained. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public AnyUInt128 UInt128() { - return AnyUInt128.Create(_source); - } - - /// - /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — finite - /// values only. Net8.0 target only, like the type itself. - /// - /// A generator to constrain fluently. - public AnyHalf Half() { - return AnyHalf.Create(_source); - } -#endif - - /// - /// Draws an arbitrary value from an explicit pool of caller-supplied drawing from - /// this context — same surface as , deterministic under this context's seed. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public AnyOneOf OneOf(params T[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(_source, values, ConstraintCall.OfElided(nameof(OneOf))); - } - - /// - /// Draws an arbitrary value from an explicit pool held as a list drawing from this context — same surface as - /// , deterministic under this context's seed. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public AnyOneOf ElementOf(IReadOnlyList values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(_source, values, ConstraintCall.OfElided(nameof(ElementOf))); - } - - /// - /// Draws an arbitrary value from an explicit pool held as a sequence drawing from this context — same surface - /// as , deterministic under this context's seed. The sequence is - /// materialized once. - /// - /// The pool the generated value is drawn from; duplicates are ignored. - /// The type of the pooled values. - /// A generator drawing uniformly from . - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public AnyOneOf ElementOf(IEnumerable values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return AnyOneOf.FromPool(_source, values as IReadOnlyList ?? values.ToArray(), ConstraintCall.OfElided(nameof(ElementOf))); - } - -} diff --git a/JustDummies/AnyDateOnly.cs b/JustDummies/AnyDateOnly.cs deleted file mode 100644 index 6b515d70..00000000 --- a/JustDummies/AnyDateOnly.cs +++ /dev/null @@ -1,154 +0,0 @@ -#if NET8_0_OR_GREATER -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// Available on the net8.0 target only, like the type itself. There is deliberately no clock-relative -/// constraint: a reproducible test pins its reference dates explicitly with and -/// . -/// -public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyDateOnly Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyDateOnly(source, OrdinalIntervalSpec.Unconstrained("DateOnly", ordinal => V(Val(ordinal)), Ord(DateOnly.MinValue), Ord(DateOnly.MaxValue))); - } - - private static ulong Ord(DateOnly value) { - return (ulong)value.DayNumber; - } - - private static DateOnly Val(ulong ordinal) { - return DateOnly.FromDayNumber((int)ordinal); - } - - private static string V(DateOnly value) { - return value.ToString("O", CultureInfo.InvariantCulture); - } - - private static string Join(DateOnly[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyDateOnly(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(DateOnly value) => _spec.Contains(Ord(value)); - - /// Requires a date strictly after . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly After(DateOnly date) { - return new AnyDateOnly(_source, _spec.WithMinimumAbove(Ord(date), ConstraintCall.Of(nameof(After), V(date)))); - } - - /// Requires a date at or after . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly AfterOrEqualTo(DateOnly date) { - return new AnyDateOnly(_source, _spec.WithMinimum(Ord(date), ConstraintCall.Of(nameof(AfterOrEqualTo), V(date)))); - } - - /// Requires a date strictly before . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly Before(DateOnly date) { - return new AnyDateOnly(_source, _spec.WithMaximumBelow(Ord(date), ConstraintCall.Of(nameof(Before), V(date)))); - } - - /// Requires a date at or before . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly BeforeOrEqualTo(DateOnly date) { - return new AnyDateOnly(_source, _spec.WithMaximum(Ord(date), ConstraintCall.Of(nameof(BeforeOrEqualTo), V(date)))); - } - - /// Requires a date within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is after . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly Between(DateOnly start, DateOnly end) { - if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(start), V(end)); - - return new AnyDateOnly(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint)); - } - - /// Requires the date to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly OneOf(params DateOnly[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyDateOnly(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the date to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly Except(params DateOnly[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyDateOnly(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the date to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated date must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateOnly DifferentFrom(DateOnly value) { - return new AnyDateOnly(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public DateOnly Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} -#endif diff --git a/JustDummies/AnyDateTime.cs b/JustDummies/AnyDateTime.cs deleted file mode 100644 index 14607ff4..00000000 --- a/JustDummies/AnyDateTime.cs +++ /dev/null @@ -1,190 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as -/// : constraints express what the surrounding code requires of the value, never what the -/// test asserts; contradictory constraints fail eagerly with a -/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one -/// draw. -/// -/// -/// Generated values carry ; constraints compare by , -/// ignoring the of the supplied bounds — exactly as 's own -/// comparison operators do. Values supplied to are returned as given, Kind included. There is deliberately no clock-relative constraint (no "in the past/future"): a -/// reproducible test pins its reference instants explicitly with and . -/// -public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyDateTime Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyDateTime(source, OrdinalIntervalSpec.Unconstrained("DateTime", ordinal => V(Val(ordinal)), Ord(DateTime.MinValue), Ord(DateTime.MaxValue))); - } - - private static ulong Ord(DateTime value) { - return (ulong)value.Ticks; - } - - private static DateTime Val(ulong ordinal) { - return new DateTime((long)ordinal, DateTimeKind.Utc); - } - - private static string V(DateTime value) { - return value.ToString("O", CultureInfo.InvariantCulture); - } - - private static string Join(DateTime[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyDictionary? _allowedOriginals; - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyDateTime(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDictionary? allowedOriginals = null) { - _source = source; - _spec = spec; - _allowedOriginals = allowedOriginals; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(DateTime value) => _spec.Contains(Ord(value)); - - /// Requires an instant strictly after . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime After(DateTime instant) { - return new AnyDateTime(_source, _spec.WithMinimumAbove(Ord(instant), ConstraintCall.Of(nameof(After), V(instant))), _allowedOriginals); - } - - /// Requires an instant at or after . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime AfterOrEqualTo(DateTime instant) { - return new AnyDateTime(_source, _spec.WithMinimum(Ord(instant), ConstraintCall.Of(nameof(AfterOrEqualTo), V(instant))), _allowedOriginals); - } - - /// Requires an instant strictly before . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime Before(DateTime instant) { - return new AnyDateTime(_source, _spec.WithMaximumBelow(Ord(instant), ConstraintCall.Of(nameof(Before), V(instant))), _allowedOriginals); - } - - /// Requires an instant at or before . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime BeforeOrEqualTo(DateTime instant) { - return new AnyDateTime(_source, _spec.WithMaximum(Ord(instant), ConstraintCall.Of(nameof(BeforeOrEqualTo), V(instant))), _allowedOriginals); - } - - /// Requires an instant within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is after . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime Between(DateTime start, DateTime end) { - if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(start), V(end)); - - return new AnyDateTime(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint), _allowedOriginals); - } - - /// - /// Requires the instant to fall on a lattice of from - /// — a round instant (a whole second, a quarter-hour, a whole day), built on - /// the grid rather than snapped after the fact, so tick-precision values never surprise a serialization - /// round-trip. Declared once per generator. - /// - /// The lattice step; must be strictly positive. A granularity of one tick adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime WithGranularity(TimeSpan granularity) { - if (granularity <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(granularity), granularity, "The granularity must be strictly positive."); } - - string rendered = granularity.ToString("c", CultureInfo.InvariantCulture); - - return new AnyDateTime(_source, _spec.WithStep((ulong)granularity.Ticks, Ord(DateTime.MinValue), ConstraintCall.Of(nameof(WithGranularity), rendered)), _allowedOriginals); - } - - /// Requires the instant to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The condition reads the collection the body mutates. Where is lazily evaluated, so lifting the filter out would run each " + - "predicate against a snapshot taken before the additions it is meant to see, and let duplicates through.")] - public AnyDateTime OneOf(params DateTime[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - // Remember the supplied values by instant, so generation returns them as given: the ordinal space - // only carries the ticks, and rebuilding from it would silently normalize the Kind to Utc. - Dictionary originals = []; - foreach (DateTime value in values) { - if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); } - } - - return new AnyDateTime(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values))), originals); - } - - /// Requires the instant to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime Except(params DateTime[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyDateTime(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values))), _allowedOriginals); - } - - /// - /// Requires the instant to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated instant must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTime DifferentFrom(DateTime value) { - return new AnyDateTime(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value))), _allowedOriginals); - } - - /// - public DateTime Generate() { - ulong ordinal = _spec.GenerateOrdinal(_source.Current); - if (_allowedOriginals is not null && _allowedOriginals.TryGetValue(ordinal, out DateTime original)) { return original; } - - return Val(ordinal); - } - -} diff --git a/JustDummies/AnyDateTimeOffset.cs b/JustDummies/AnyDateTimeOffset.cs deleted file mode 100644 index f82ad617..00000000 --- a/JustDummies/AnyDateTimeOffset.cs +++ /dev/null @@ -1,332 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as -/// : constraints express what the surrounding code requires of the value, never what the -/// test asserts; contradictory constraints fail eagerly with a -/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one -/// draw. -/// -/// -/// Constraints compare by — the instant, not the local rendering — exactly -/// as 's own comparison operators do. Unconstrained, generated values carry offset -/// (UTC); / opt the offset -/// dimension into a fixed or bounded whole-minute value so offset-sensitive code can be exercised. Values supplied -/// to are returned as given, offset included. There is deliberately no clock-relative -/// constraint (no "in the past/future"): a reproducible test pins its reference instants explicitly with -/// and . -/// -public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, ICardinalityHint { - - // DateTimeOffset admits an offset in whole minutes within ±14:00. - private const int MaxOffsetMinutes = 14 * 60; - - #region Statics members declarations - - internal static AnyDateTimeOffset Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyDateTimeOffset(source, OrdinalIntervalSpec.Unconstrained("DateTimeOffset", ordinal => V(Val(ordinal)), Ord(DateTimeOffset.MinValue), Ord(DateTimeOffset.MaxValue)), null, null, 0, 0); - } - - private static ulong Ord(DateTimeOffset value) { - return (ulong)value.UtcTicks; - } - - private static DateTimeOffset Val(ulong ordinal) { - return new DateTimeOffset((long)ordinal, TimeSpan.Zero); - } - - private static string V(DateTimeOffset value) { - return value.ToString("O", CultureInfo.InvariantCulture); - } - - private static string Render(TimeSpan offset) { - return offset.ToString("c", CultureInfo.InvariantCulture); - } - - private static string Join(DateTimeOffset[] values) { - return string.Join(", ", values.Select(V)); - } - - /// Validates a supplied offset (whole minutes, within ±14:00) and returns it in whole minutes. - private static int ValidateOffset(TimeSpan offset, string parameterName) { - if (offset.Ticks % TimeSpan.TicksPerMinute != 0) { throw new ArgumentException("The offset must be a whole number of minutes.", parameterName); } - if (offset < TimeSpan.FromMinutes(-MaxOffsetMinutes) || offset > TimeSpan.FromMinutes(MaxOffsetMinutes)) { - throw new ArgumentOutOfRangeException(parameterName, offset, "The offset must be within ±14:00."); - } - - return (int)(offset.Ticks / TimeSpan.TicksPerMinute); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyDictionary? _allowedOriginals; - private readonly ConstraintCall? _offsetConstraint; - private readonly int _offsetMaxMinutes; - private readonly int _offsetMinMinutes; - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyDateTimeOffset(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDictionary? allowedOriginals, - ConstraintCall? offsetConstraint, int offsetMinMinutes, int offsetMaxMinutes) { - _source = source; - _spec = spec; - _allowedOriginals = allowedOriginals; - _offsetConstraint = offsetConstraint; - _offsetMinMinutes = offsetMinMinutes; - _offsetMaxMinutes = offsetMaxMinutes; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(DateTimeOffset value) => _spec.Contains(Ord(value)); - - /// Requires an instant strictly after . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset After(DateTimeOffset instant) { - return With(_spec.WithMinimumAbove(Ord(instant), ConstraintCall.Of(nameof(After), V(instant)))); - } - - /// Requires an instant at or after . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset AfterOrEqualTo(DateTimeOffset instant) { - return With(_spec.WithMinimum(Ord(instant), ConstraintCall.Of(nameof(AfterOrEqualTo), V(instant)))); - } - - /// Requires an instant strictly before . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset Before(DateTimeOffset instant) { - return With(_spec.WithMaximumBelow(Ord(instant), ConstraintCall.Of(nameof(Before), V(instant)))); - } - - /// Requires an instant at or before . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset BeforeOrEqualTo(DateTimeOffset instant) { - return With(_spec.WithMaximum(Ord(instant), ConstraintCall.Of(nameof(BeforeOrEqualTo), V(instant)))); - } - - /// Requires an instant within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is after . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset Between(DateTimeOffset start, DateTimeOffset end) { - if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(start), V(end)); - - return With(_spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint)); - } - - /// - /// Requires the instant to fall on a lattice of from - /// — a round instant (a whole second, a quarter-hour, a whole day), - /// built on the grid rather than snapped after the fact, so tick-precision values never surprise a - /// serialization round-trip. Declared once per generator. - /// - /// The lattice step; must be strictly positive. A granularity of one tick adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset WithGranularity(TimeSpan granularity) { - if (granularity <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(granularity), granularity, "The granularity must be strictly positive."); } - - string rendered = granularity.ToString("c", CultureInfo.InvariantCulture); - - return With(_spec.WithStep((ulong)granularity.Ticks, Ord(DateTimeOffset.MinValue), ConstraintCall.Of(nameof(WithGranularity), rendered))); - } - - /// - /// Pins the offset dimension to — every generated value carries exactly that offset, - /// rather than the default . The instant is tightened so the value stays valid at - /// the domain edges. Declared once per generator. - /// - /// The offset to pin; a whole number of minutes, within ±14:00. - /// A new generator carrying the added constraint. - /// Thrown when is not a whole number of minutes. - /// Thrown when is outside ±14:00. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset WithOffset(TimeSpan offset) { - int minutes = ValidateOffset(offset, nameof(offset)); - - return WithOffsetRange(minutes, minutes, ConstraintCall.Of(nameof(WithOffset), Render(offset))); - } - - /// - /// Draws the offset dimension from the inclusive range [, ] — - /// a bounded, whole-minute offset — so a test can exercise offset-sensitive logic while staying valid. The - /// instant is tightened so every offset in the range stays valid. Declared once per generator. - /// - /// The inclusive lower offset; a whole number of minutes, within ±14:00. - /// The inclusive upper offset; a whole number of minutes, within ±14:00. - /// A new generator carrying the added constraint. - /// Thrown when an offset is not a whole number of minutes, or is after . - /// Thrown when an offset is outside ±14:00. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset WithOffsetBetween(TimeSpan minimum, TimeSpan maximum) { - int min = ValidateOffset(minimum, nameof(minimum)); - int max = ValidateOffset(maximum, nameof(maximum)); - if (min > max) { throw new ArgumentException($"The minimum offset ({Render(minimum)}) must be at or before the maximum ({Render(maximum)}).", nameof(minimum)); } - - return WithOffsetRange(min, max, ConstraintCall.Of(nameof(WithOffsetBetween), Render(minimum), Render(maximum))); - } - - /// - /// Requires the instant to be one of the supplied values — returned as given, offset included. Declared once - /// per generator. - /// - /// The allowed values; duplicates (same instant) are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The condition reads the collection the body mutates. Where is lazily evaluated, so lifting the filter out would run each " + - "predicate against a snapshot taken before the additions it is meant to see, and let duplicates through.")] - public AnyDateTimeOffset OneOf(params DateTimeOffset[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - ConstraintCall applying = ConstraintCall.Of(nameof(OneOf), Join(values)); - // A pooled value is returned as given, offset included — rebuilding it from the ordinal would normalize the - // offset to UTC. So when an offset constraint is already in force, the pool is FILTERED by it rather than - // the constraint being ignored: a value carrying a different offset is one this generator must not produce. - DateTimeOffset[] admitted = _offsetConstraint is not null ? values.Where(SatisfiesDeclaredOffset).ToArray() : values; - if (admitted.Length == 0) { throw OffsetExcludesEveryPooledValue(applying, _offsetMinMinutes, _offsetMaxMinutes); } - - // Remember the supplied values by instant, so generation returns them as given: the ordinal space - // only carries the instant, and rebuilding from it would silently normalize the offset to UTC. - Dictionary originals = []; - foreach (DateTimeOffset value in admitted) { - if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); } - } - - return new AnyDateTimeOffset(_source, _spec.WithAllowed(admitted.Select(Ord).ToArray(), applying), originals, _offsetConstraint, _offsetMinMinutes, _offsetMaxMinutes); - } - - /// Requires the instant to be none of the supplied values (compared by instant). - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset Except(params DateTimeOffset[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return With(_spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the instant to differ from (compared by instant) — typically an existing - /// value the test already holds. Semantically equivalent to ; the name carries the intent - /// at the call site. - /// - /// The value the generated instant must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDateTimeOffset DifferentFrom(DateTimeOffset value) { - return With(_spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public DateTimeOffset Generate() { - SeededRandom random = _source.Current; - ulong ordinal = _spec.GenerateOrdinal(random); - if (_allowedOriginals is not null && _allowedOriginals.TryGetValue(ordinal, out DateTimeOffset original)) { return original; } - if (_offsetConstraint is null) { return Val(ordinal); } - - int minutes = _offsetMinMinutes == _offsetMaxMinutes - ? _offsetMinMinutes - : _offsetMinMinutes + random.Next(_offsetMaxMinutes - _offsetMinMinutes + 1); - TimeSpan offset = TimeSpan.FromMinutes(minutes); - - // The instant domain was tightened when the offset was declared, so the local ticks stay valid here. - return new DateTimeOffset((long)ordinal + offset.Ticks, offset); - } - - /// Carries the offset state forward onto a new spec — every instant constraint routes through here. - private AnyDateTimeOffset With(OrdinalIntervalSpec spec) { - return new AnyDateTimeOffset(_source, spec, _allowedOriginals, _offsetConstraint, _offsetMinMinutes, _offsetMaxMinutes); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "The flagged lines are prose, not disabled code: the heuristic reads an equation, a bracketed range or a semicolon inside an " + - "explanatory sentence as a statement. These comments carry the reasoning this codebase asks every comment to carry, so the " + - "finding is recorded rather than the comment deleted.")] - private AnyDateTimeOffset WithOffsetRange(int minMinutes, int maxMinutes, ConstraintCall applying) { - if (_offsetConstraint is not null) { - if (_offsetMinMinutes == minMinutes && _offsetMaxMinutes == maxMinutes) { return this; } - - throw ConflictingAnyConstraintException.AlreadyDefined(applying, _offsetConstraint); - } - - // Tighten the instant so local ticks = UtcTicks + offset stay in [0, MaxTicks] for every offset in the range; - // the offset can then be drawn independently, never producing an out-of-range DateTimeOffset. - long minOffsetTicks = minMinutes * TimeSpan.TicksPerMinute; - long maxOffsetTicks = maxMinutes * TimeSpan.TicksPerMinute; - ulong lowerUtc = (ulong)Math.Max(0L, -minOffsetTicks); - ulong upperUtc = (ulong)(DateTimeOffset.MaxValue.UtcTicks - Math.Max(0L, maxOffsetTicks)); - - OrdinalIntervalSpec spec = _spec.WithMinimum(lowerUtc, applying).WithMaximum(upperUtc, applying); - - // The mirror of OneOf's filter, so the two orders reach the same verdict: an offset declared AFTER a pool - // narrows that pool to the values it admits, and contradicts when it admits none. - if (_allowedOriginals is not null) { - Dictionary admitted = _allowedOriginals - .Where(entry => SatisfiesOffset(entry.Value, minMinutes, maxMinutes)) - .ToDictionary(entry => entry.Key, entry => entry.Value); - if (admitted.Count == 0) { throw OffsetExcludesEveryPooledValue(applying, minMinutes, maxMinutes); } - - return new AnyDateTimeOffset(_source, spec.NarrowingAllowed(admitted.Keys.ToArray(), applying), admitted, applying, minMinutes, maxMinutes); - } - - return new AnyDateTimeOffset(_source, spec, _allowedOriginals, applying, minMinutes, maxMinutes); - } - - /// Whether carries an offset the declared offset dimension admits. - private bool SatisfiesDeclaredOffset(DateTimeOffset value) { - return SatisfiesOffset(value, _offsetMinMinutes, _offsetMaxMinutes); - } - - private static bool SatisfiesOffset(DateTimeOffset value, int minMinutes, int maxMinutes) { - double minutes = value.Offset.TotalMinutes; - - return minutes >= minMinutes && minutes <= maxMinutes; - } - - // The range is passed in rather than read off the fields: when an offset is declared AFTER a pool, the fields - // still hold the previous (undeclared) state at the point the contradiction is detected. - private static ConflictingAnyConstraintException OffsetExcludesEveryPooledValue(ConstraintCall applying, int minMinutes, int maxMinutes) { - string admitted = minMinutes == maxMinutes - ? Render(TimeSpan.FromMinutes(minMinutes)) - : $"{Render(TimeSpan.FromMinutes(minMinutes))} to {Render(TimeSpan.FromMinutes(maxMinutes))}"; - - return new ConflictingAnyConstraintException($"Cannot apply {applying} because no pooled value carries an offset it admits ({admitted})."); - } - -} diff --git a/JustDummies/AnyDecimal.cs b/JustDummies/AnyDecimal.cs deleted file mode 100644 index 8f4e367d..00000000 --- a/JustDummies/AnyDecimal.cs +++ /dev/null @@ -1,189 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes. Exclusive bounds are expressed as the inclusive bound plus a point -/// exclusion, since has no next-representable-value ladder. -/// -public sealed class AnyDecimal : IAny, IHasRandomSource, ICardinalityHint { - - /// The fewest decimal places accepts — a whole number, with no fractional part. - private const int MinScale = 0; - - #region Statics members declarations - - internal static AnyDecimal Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyDecimal(source, DecimalIntervalSpec.Unconstrained("Decimal", V)); - } - - private static string V(decimal value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(decimal[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly DecimalIntervalSpec _spec; - - #endregion - - private AnyDecimal(RandomSource source, DecimalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(decimal value) => _spec.Contains(value); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal Positive() { - return new AnyDecimal(_source, _spec.WithMinimumAbove(0m, ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal Negative() { - return new AnyDecimal(_source, _spec.WithMaximumBelow(0m, ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal Zero() { - return new AnyDecimal(_source, _spec.WithMinimum(0m, ConstraintCall.Of(nameof(Zero))).WithMaximum(0m, ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal NonZero() { - return new AnyDecimal(_source, _spec.WithExcluded([0m], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than — the inclusive bound plus a point exclusion. - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal GreaterThan(decimal value) { - return new AnyDecimal(_source, _spec.WithMinimumAbove(value, ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal GreaterThanOrEqualTo(decimal value) { - return new AnyDecimal(_source, _spec.WithMinimum(value, ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than — the inclusive bound plus a point exclusion. - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal LessThan(decimal value) { - return new AnyDecimal(_source, _spec.WithMaximumBelow(value, ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal LessThanOrEqualTo(decimal value) { - return new AnyDecimal(_source, _spec.WithMaximum(value, ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal Between(decimal minimum, decimal maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyDecimal(_source, _spec.WithMinimum(minimum, constraint).WithMaximum(maximum, constraint)); - } - - /// - /// Requires the value to be expressible in decimal places — a multiple of - /// 10^- (a valid amount in cents is WithScale(2)), drawn directly on that grid. - /// A value lattice, not a representation contract: the drawn value lies on the grid but is not padded with - /// trailing zeros. Declared once per generator. - /// - /// The number of decimal places; in the inclusive range [0, 28]. - /// A new generator carrying the added constraint. - /// Thrown when is outside the range [0, 28]. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal WithScale(int scale) { - if (scale < MinScale || scale > DecimalIntervalSpec.MaxScale) { throw new ArgumentOutOfRangeException(nameof(scale), scale, $"The scale must be in the inclusive range [{MinScale}, {DecimalIntervalSpec.MaxScale}]."); } - - return new AnyDecimal(_source, _spec.WithScale(scale, ConstraintCall.Of(nameof(WithScale), scale.ToString(CultureInfo.InvariantCulture)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal OneOf(params decimal[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyDecimal(_source, _spec.WithAllowed(values, ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal Except(params decimal[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyDecimal(_source, _spec.WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDecimal DifferentFrom(decimal value) { - return new AnyDecimal(_source, _spec.WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public decimal Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyDerivation.cs b/JustDummies/AnyDerivation.cs deleted file mode 100644 index 5ce1d0cf..00000000 --- a/JustDummies/AnyDerivation.cs +++ /dev/null @@ -1,142 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A generator derived from other generators (As, Combine): it delegates generation to a closure -/// and carries, when known, the random context of the generators it derives from — so a failure inside the -/// derivation can still name the seed that replays the run. It also remembers whether every operand it draws from -/// is reproducible (): a single foreign operand leaves a non-null source to name -/// but makes the derived value unreproducible, which the seed reporting must not over-promise. -/// -/// The type of the generated values. -internal sealed class DerivedAny : IAny, IHasRandomSource, IReproducibilityHint { - - #region Fields declarations - - private readonly bool _drawsOnlyFromSource; - private readonly Func _generate; - private readonly RandomSource? _source; - - #endregion - - internal DerivedAny(RandomSource? source, bool drawsOnlyFromSource, Func generate) { - if (generate is null) { throw new ArgumentNullException(nameof(generate)); } - - _source = source; - _drawsOnlyFromSource = drawsOnlyFromSource; - _generate = generate; - } - - RandomSource? IHasRandomSource.Source => _source; - - bool IReproducibilityHint.DrawsOnlyFromSource => _drawsOnlyFromSource; - - /// - public T Generate() { - return _generate(); - } - -} - -/// Shared plumbing of the derived generators. -internal static class AnyDerivation { - - /// The random context of , when it is one of the library's own. - internal static RandomSource? SourceOf(IAny generator) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - return (generator as IHasRandomSource)?.Source; - } - - /// - /// Whether every value yields is replayable from the source it reports: true - /// for a library generator carrying a source, and for a derivation whose operands are all themselves - /// reproducible; false for a foreign generator (no source) or a derivation built over one. This is - /// stronger than being non-null — a Combine that mixes a foreign operand with a - /// library one keeps a non-null source to name, yet its composed value follows the foreign draw and cannot be - /// replayed from that seed. - /// - internal static bool IsReproducible(IAny generator) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - if (generator is IReproducibilityHint hint) { return hint.DrawsOnlyFromSource; } - - return SourceOf(generator) is not null; - } - - /// - /// Whether is reproducible and draws from - /// specifically — the per-operand condition for a Combine's full-replay promise. An operand that is - /// individually reproducible but draws from a different seeded source (a second - /// context, or the ambient source alongside a fixed one) leaves the reported seed covering only part of the run, - /// so naming it as a deterministic full replay would over-promise. When the operands do not all draw from the one - /// reported source, the hint is qualified instead — exactly as it is for a foreign operand. - /// - internal static bool DrawsOnlyFrom(IAny generator, RandomSource? source) { - return IsReproducible(generator) && ReferenceEquals(SourceOf(generator), source); - } - - /// - /// A conservative upper bound on the number of distinct values yields, when it - /// advertises one through ; null when the domain is unbounded or unknown. - /// - internal static long? CardinalityOf(IAny generator) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - return (generator as ICardinalityHint)?.DistinctCardinality; - } - - /// - /// Runs a user-supplied factory or composer and converts its failure into an - /// that names the generated value(s) and, when the random context is - /// known, the seed that replays the run. tells whether the derived value draws - /// only from that source: when it does not — a foreign operand contributes — the hint is qualified rather than - /// promising a full replay the seed cannot deliver. The library's own exceptions pass through untouched. - /// - /// is a thunk, not a string, because rendering the generated values is - /// only ever needed on the failing path: an eagerly interpolated message would run the caller's - /// ToString() — and allocate the whole sentence — on every successful draw, which is every draw a - /// test actually makes. - /// - /// - internal static T Invoke(Func invoke, RandomSource? source, bool reproducible, Func failure) { - if (invoke is null) { throw new ArgumentNullException(nameof(invoke)); } - if (failure is null) { throw new ArgumentNullException(nameof(failure)); } - - try { - return invoke(); - } catch (DummyException) { - throw; - } catch (Exception exception) { - throw AnyGenerationException.FactoryFailed(failure, exception, source, reproducible); - } - } - - /// - /// Renders a generated value for an exception message. A value's own ToString() is user code and may - /// throw — a domain object rendering state the fixture never set is the ordinary case. A renderer that let - /// that through would replace the diagnostic being built with an unrelated failure, hiding the constraint - /// conflict or factory rejection the caller needs to read, so a throwing rendering falls back to the type - /// name: the message loses a detail, never the report it was explaining. - /// - internal static string Display(object? value) { - switch (value) { - case null: return "null"; - case string text: return "\"" + text + "\""; - default: - try { - return value is IFormattable formattable - ? formattable.ToString(null, CultureInfo.InvariantCulture) - : value.ToString() ?? value.GetType().Name; - } catch (Exception) { - return value.GetType().Name; - } - } - } - -} diff --git a/JustDummies/AnyDictionary.cs b/JustDummies/AnyDictionary.cs deleted file mode 100644 index c9bc886c..00000000 --- a/JustDummies/AnyDictionary.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values over a key generator and a value -/// generator. The keys are distinct by nature — the count constraints bound the number of entries, and the key -/// generator's domain gates feasibility exactly as it does for a : too small a key domain -/// for the requested count fails eagerly with a , a genuine -/// shortfall surfaces at generation as an . -/// -/// The key type. -/// The value type. -public sealed class AnyDictionary : IAny>, IHasRandomSource - where TKey : notnull { - - #region Statics members declarations - - private static readonly IReadOnlyDictionary NoPinnedValues = new Dictionary(); - - #endregion - - #region Fields declarations - - private readonly CollectionState _keys; - private readonly IReadOnlyDictionary _pinnedValues; - private readonly RandomSource? _source; - private readonly IAny _values; - - #endregion - - internal AnyDictionary(RandomSource? source, CollectionState keys, IAny values) - : this(source, keys, values, NoPinnedValues) { - if (keys is null) { throw new ArgumentNullException(nameof(keys)); } - if (values is null) { throw new ArgumentNullException(nameof(values)); } - } - - private AnyDictionary(RandomSource? source, CollectionState keys, IAny values, IReadOnlyDictionary pinnedValues) { - _source = source; - _keys = keys; - _values = values; - _pinnedValues = pinnedValues; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Requires at least one entry. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary NonEmpty() { - return With(CountConstraints.NonEmpty(_keys)); - } - - /// Fixes the dictionary to no entries. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary Empty() { - return With(CountConstraints.Empty(_keys)); - } - - /// Fixes the exact number of entries. Declared once per generator. - /// The exact number of entries. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary WithCount(int count) { - return With(CountConstraints.WithCount(_keys, count)); - } - - /// Requires at least entries. - /// The inclusive minimum number of entries. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary WithMinCount(int count) { - return With(CountConstraints.WithMinCount(_keys, count)); - } - - /// Requires at most entries. - /// The inclusive maximum number of entries. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary WithMaxCount(int count) { - return With(CountConstraints.WithMaxCount(_keys, count)); - } - - /// Requires a number of entries within the inclusive range [, ]. - /// The inclusive minimum number of entries. - /// The inclusive maximum number of entries. - /// A new generator carrying the added constraint. - /// Thrown when a bound is negative. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary WithCountBetween(int minimum, int maximum) { - return With(CountConstraints.WithCountBetween(_keys, minimum, maximum)); - } - - /// - /// Requires the dictionary to contain an entry for . May be declared several times; - /// each required key takes one entry's room and the required keys must be distinct. A key outside the key - /// generator's domain extends the effective cardinality exactly as 's containment - /// does, so an otherwise impossible entry count becomes reachable; the entry's value is generated like any - /// other. Named ContainingKey rather than a bare Containing so the surface reads unambiguously - /// on a dictionary, whose elements are key/value pairs. - /// - /// The key the generated dictionary must contain. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary ContainingKey(TKey key) { - return With(_keys.WithContaining(key, ConstraintCall.Of(nameof(ContainingKey), AnyDerivation.Display(key)))); - } - - /// - /// Requires the dictionary to contain an entry whose key is drawn from at - /// generation time — the key analogue of a collection's ContainingAny. Named apart from - /// to keep the two cases legible: pins a concrete - /// key known now, whereas this draws one from a generator when the dictionary is built. The drawn key takes - /// one entry's room and its value is generated like any other. - /// - /// The generator whose drawn key the dictionary must contain. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary ContainingAnyKey(IAny generator) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - return With(_keys.WithContaining(generator, ConstraintCall.Of(nameof(ContainingAnyKey), ""))); - } - - /// - /// Requires the dictionary to contain the entry : the key - /// is forced in exactly as does (inheriting the out-of-domain cardinality - /// credit), and its value is pinned to instead of being drawn from the value - /// generator; the other entries stay arbitrary. Declaring two entries for the same key — or an entry and a - /// for it — conflicts, since the keys must be distinct. - /// - /// The key the generated dictionary must contain. - /// The value pinned to . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDictionary ContainingEntry(TKey key, TValue value) { - CollectionState keys = _keys.WithContaining(key, ConstraintCall.Of(nameof(ContainingEntry), AnyDerivation.Display(key), AnyDerivation.Display(value))); - - Dictionary pinned = new(_pinnedValues.Count + 1, _keys.Comparer); - foreach (KeyValuePair entry in _pinnedValues) { pinned[entry.Key] = entry.Value; } - pinned[key] = value; - - return new AnyDictionary(_source, keys, _values, pinned); - } - - /// - public Dictionary Generate() { - List keys = _keys.Materialize(_source ?? AmbientRandomSource.Instance); - Dictionary dictionary = new(keys.Count, _keys.Comparer); - foreach (TKey key in keys) { - dictionary[key] = _pinnedValues.ContainsKey(key) ? _pinnedValues[key] : _values.Generate(); - } - - return dictionary; - } - - private AnyDictionary With(CollectionState keys) { - return new AnyDictionary(_source, keys, _values, _pinnedValues); - } - -} diff --git a/JustDummies/AnyDouble.cs b/JustDummies/AnyDouble.cs deleted file mode 100644 index 1c69f2a6..00000000 --- a/JustDummies/AnyDouble.cs +++ /dev/null @@ -1,183 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. -/// -public sealed class AnyDouble : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyDouble Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyDouble(source, ContinuousIntervalSpec.Unconstrained("Double", V, value => value, ContinuousIntervalSpec.NextUp, -double.MaxValue, double.MaxValue)); - } - - private static string V(double value) { - return value.ToString("R", CultureInfo.InvariantCulture); - } - - private static string Join(double[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly ContinuousIntervalSpec _spec; - - #endregion - - private AnyDouble(RandomSource source, ContinuousIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(double value) => _spec.Contains(value); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble Positive() { - return new AnyDouble(_source, _spec.WithMinimumAbove(0d, ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble Negative() { - return new AnyDouble(_source, _spec.WithMaximumBelow(0d, ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble Zero() { - return new AnyDouble(_source, _spec.WithMinimum(0d, ConstraintCall.Of(nameof(Zero))).WithMaximum(0d, ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble NonZero() { - return new AnyDouble(_source, _spec.WithExcluded([0d], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble GreaterThan(double value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnyDouble(_source, _spec.WithMinimumAbove(value, ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble GreaterThanOrEqualTo(double value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnyDouble(_source, _spec.WithMinimum(value, ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble LessThan(double value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnyDouble(_source, _spec.WithMaximumBelow(value, ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble LessThanOrEqualTo(double value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnyDouble(_source, _spec.WithMaximum(value, ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when a bound is not finite or is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble Between(double minimum, double maximum) { - ContinuousIntervalSpec.EnsureFinite(minimum, nameof(minimum)); - ContinuousIntervalSpec.EnsureFinite(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyDouble(_source, _spec.WithMinimum(minimum, constraint).WithMaximum(maximum, constraint)); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble OneOf(params double[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (double value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } - - return new AnyDouble(_source, _spec.WithAllowed(values, ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble Except(params double[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (double value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } - - return new AnyDouble(_source, _spec.WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyDouble DifferentFrom(double value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnyDouble(_source, _spec.WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public double Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyEnum.cs b/JustDummies/AnyEnum.cs deleted file mode 100644 index 6bf2bf26..00000000 --- a/JustDummies/AnyEnum.cs +++ /dev/null @@ -1,295 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values, drawn uniformly from the enum's -/// declared members — never from undeclared numeric values. Constraints narrow the pool -/// (, , ), and a combination that empties it -/// fails eagerly with a naming both sides. -/// -/// -/// A [Flags] enum declares bits meant to be combined, so its valid values -/// are the combinations, not only the declared members: Read | Write is a legitimate value the type never -/// declares. The declared-members default holds for those enums too — it is the only default valid for both enum -/// families, and switching on the attribute would make the draw depend on a type's metadata rather than on what -/// the test wrote. Opt in explicitly with to widen the draw to every -/// combination. -/// -/// The enum type to draw values from. -public sealed class AnyEnum : IAny, IHasRandomSource, ICardinalityHint - where TEnum : struct, Enum { - - // The ceiling on the number of non-zero declared members AllowingCombinations() will enumerate. The universe is - // materialized so the draw is exactly uniform over the DISTINCT values and the cardinality hint stays exact - // (a per-member coin flip is neither: with a declared composite such as ReadWrite = Read | Write, several - // subsets collapse onto the same value). Enumeration is 2^k, so it needs a bound; beyond it the constraint is - // refused by name rather than silently degraded into a second, non-uniform regime. - private const int MaxCombinableMembers = 20; - - #region Statics members declarations - - // The declared-members set and the [Flags] marking of an enum type are process constants; cached once per closed - // generic type instead of reflecting on every Any.Enum() call. - private static readonly TEnum[] Declared = ((TEnum[])Enum.GetValues(typeof(TEnum))).Distinct().ToArray(); - private static readonly bool IsFlags = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false); - - // The combination universe is a process constant too, but far more expensive than Declared, so it is built on - // first use instead of on every closed generic type. A race computes it twice and stores the same set: benign. - private static TEnum[]? _combinations; - - internal static AnyEnum Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (Declared.Length == 0) { - throw AnyGenerationException.EnumDeclaresNoMembers(typeof(TEnum).Name); - } - - return new AnyEnum(source, Declared, false, null, null, []); - } - - /// - /// Every value obtained by OR-ing a non-empty subset of the declared members, plus the zero value when a zero - /// member is declared. Taking the declared members as the generating set — rather than the individual bits — - /// absorbs declared composites (ReadWrite = Read | Write contributes nothing new) without having to - /// decide which members "are" bits, and never invents the zero value for an enum that deliberately declares no - /// None. - /// - private static TEnum[] Combinations { - get { - if (_combinations is not null) { return _combinations; } - - ulong[] generators = Declared.Select(ToUInt64).Where(bits => bits != 0UL).ToArray(); - HashSet reachable = []; - foreach (ulong generator in generators) { - // Union of what was reachable, what becomes reachable by adding this generator to it, and the - // generator alone — the OR-closure, built without enumerating the 2^k subsets that collapse. - foreach (ulong existing in reachable.ToArray()) { reachable.Add(existing | generator); } - reachable.Add(generator); - } - - // The empty subset ORs to zero, but that value belongs to the universe only when the enum defines it. - if (Declared.Any(value => ToUInt64(value) == 0UL)) { reachable.Add(0UL); } - - _combinations = reachable.OrderBy(bits => bits).Select(ToEnum).ToArray(); - - return _combinations; - } - } - - /// The value's underlying bits, whatever the enum's underlying type — signed members included. - private static ulong ToUInt64(TEnum value) { - // Convert.ToUInt64 throws on a negative signed member, so each signed width is read at its own size and - // reinterpreted, exactly as the runtime stores it. - return Type.GetTypeCode(typeof(TEnum)) switch { - TypeCode.SByte => unchecked((ulong)Convert.ToSByte(value, CultureInfo.InvariantCulture)), - TypeCode.Int16 => unchecked((ulong)Convert.ToInt16(value, CultureInfo.InvariantCulture)), - TypeCode.Int32 => unchecked((ulong)Convert.ToInt32(value, CultureInfo.InvariantCulture)), - TypeCode.Int64 => unchecked((ulong)Convert.ToInt64(value, CultureInfo.InvariantCulture)), - _ => Convert.ToUInt64(value, CultureInfo.InvariantCulture) - }; - } - - private static TEnum ToEnum(ulong bits) { - return (TEnum)Enum.ToObject(typeof(TEnum), bits); - } - - private static string V(TEnum value) { - return value.ToString(); - } - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(TEnum[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly bool _combinable; - private readonly IReadOnlyList _excluded; - private readonly List _pool; - private readonly RandomSource _source; - private readonly IReadOnlyList _universe; - - #endregion - - private AnyEnum(RandomSource source, IReadOnlyList universe, bool combinable, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, IReadOnlyList excluded) { - _source = source; - _universe = universe; - _combinable = combinable; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _excluded = excluded; - // Materialized once here — "constrain once, draw many": Generate never refilters the pool. - _pool = (allowed ?? universe).Where(value => !excluded.Contains(value)).ToList(); - } - - RandomSource? IHasRandomSource.Source => _source; - - // The pool is materialized once at construction, so its size is the exact number of values drawable. - long? ICardinalityHint.DistinctCardinality => _pool.Count; - - // The pool is the exact draw set, so membership is a direct pool lookup. - bool ICardinalityHint.Contains(TEnum value) => _pool.Contains(value); - - /// - /// Widens the draw from the declared members to every combination of them — the values a - /// [Flags] enum is designed to hold. Without it, a flags dummy carries at - /// most one bit and a branch reading two never runs. - /// - /// - /// - /// The universe is every value obtained by OR-ing a non-empty subset of the declared members, plus the - /// zero value when a zero member is declared: { None = 0, Read = 1, Write = 2, Exec = 4 } yields the - /// eight values 07, while { Left = 1, Right = 2 } yields only 1, 2 and - /// 3 — never 0, which that enum does not define. A declared composite adds nothing: - /// ReadWrite = Read | Write is already the combination of the two. - /// - /// - /// and keep comparing by equality, here as - /// everywhere else: Except(Read) forbids the value Read and still allows - /// Read | Write. Applied after , this constraint changes nothing — an explicit - /// allow-list is a terminal enumeration of exact values, so declare it before OneOf when the - /// allow-list itself names combinations. - /// - /// - /// A new generator drawing from the combination universe. - /// - /// Thrown when is not declared [Flags], when it declares more non-zero - /// members than the enumerable ceiling, or when the constraint contradicts a constraint already declared. - /// - public AnyEnum AllowingCombinations() { - ConstraintCall constraint = ConstraintCall.Of(nameof(AllowingCombinations)); - if (_combinable) { return this; } - - if (!IsFlags) { - throw ConflictingAnyConstraintException.EnumIsNotFlags(constraint, typeof(TEnum).Name); - } - - int generators = Declared.Count(value => ToUInt64(value) != 0UL); - if (generators > MaxCombinableMembers) { - throw ConflictingAnyConstraintException.TooManyCombinableMembers(constraint, typeof(TEnum).Name, V(generators), V(MaxCombinableMembers)); - } - - return Validated(new AnyEnum(_source, Combinations, true, _allowed, _allowedConstraint, _excluded), constraint); - } - - /// Requires the value to be one of the supplied members. Declared once per generator. - /// - /// The allowed values; duplicates are ignored. Every value must belong to the generator's universe — the - /// declared members, or every combination of them once has been applied. - /// The generator never yields a value outside that universe, not even an explicitly supplied one. - /// - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a value outside the generator's universe. - /// Thrown when the constraint contradicts a constraint already declared. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop exists to name the FIRST offending element in the exception it throws. A Where clause discards which element failed, so " + - "the message would have to re-find it, turning one pass into two and one statement into three.")] - public AnyEnum OneOf(params TEnum[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (TEnum value in values) { - if (!_universe.Contains(value)) { throw new ArgumentException($"The value {value} {DescribeOutsideUniverse()}", nameof(values)); } - } - - ConstraintCall constraint = ConstraintCall.Of(nameof(OneOf), Join(values)); - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == constraint) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(constraint, _allowedConstraint); } - - return Validated(new AnyEnum(_source, _universe, _combinable, values.Distinct().ToArray(), constraint, _excluded), constraint); - } - - /// - /// Requires the value to be none of the supplied ones, compared by equality — under - /// too, so Except(Read) forbids Read and still allows - /// Read | Write. - /// - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyEnum Except(params TEnum[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyEnum DifferentFrom(TEnum value) { - return WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), V(value))); - } - - /// - public TEnum Generate() { - return _pool[_source.Current.Next(_pool.Count)]; - } - - /// - /// Why a supplied value is outside the universe — naming when the value is - /// a flag combination, since that is the constraint the caller is missing rather than a mistyped member. - /// - private string DescribeOutsideUniverse() { - string subject = $"is not a declared member of {typeof(TEnum).Name}: the generator only ever yields declared members."; - if (_combinable || !IsFlags) { return subject; } - - return $"{subject} Apply AllowingCombinations() first to draw combinations of them."; - } - - private AnyEnum WithExcluded(TEnum[] values, ConstraintCall applying) { - List excluded = [.. _excluded, .. values]; - - return Validated(new AnyEnum(_source, _universe, _combinable, _allowed, _allowedConstraint, excluded), applying); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private AnyEnum Validated(AnyEnum candidate, ConstraintCall applying) { - if (candidate._pool.Count > 0) { return candidate; } - - // Three exhausted pools, three different things to tell the reader: an allow-list that nothing survives, a - // flags enum whose combinations are all excluded, and a plain enum whose declared members are. - string pool; - if (candidate._allowedConstraint is not null) { pool = $"no value {candidate._allowedConstraint} allows remains available"; } - else if (candidate._combinable) { pool = $"no {typeof(TEnum).Name} combination remains available"; } - else { pool = $"no declared {typeof(TEnum).Name} member remains available"; } - - throw ConflictingAnyConstraintException.NoValueRemains(applying, pool); - } - -} diff --git a/JustDummies/AnyExtensions.cs b/JustDummies/AnyExtensions.cs deleted file mode 100644 index 923e80a0..00000000 --- a/JustDummies/AnyExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace JustDummies; - -/// -/// Composition over . These extensions are the bridge between constrained primitives and -/// domain types: a generator of raw values becomes a generator of value objects by going through the type's own -/// factory — no reflection, and the domain's validation stays the single gatekeeper. -/// -public static class AnyExtensions { - - /// - /// Derives a generator of by passing each generated - /// through — typically a value object's own - /// factory method, so the constraints declared upstream express the invariant that factory enforces. - /// - /// - /// - /// If the factory throws, the failure is wrapped in an naming the - /// generated value and, when known, the seed that replays the run — the usual cause is constraints weaker - /// than the invariant the factory enforces, and the fix is to tighten them. - /// - /// - /// - /// IAny<OrderReference> reference = Any.String() - /// .StartingWith("ORD-") - /// .WithLength(12) - /// .As(OrderReference.Create); - /// - /// - /// - /// The generator of the raw values. - /// The factory turning a raw value into a . - /// The type of the raw generated values. - /// The type the factory produces. - /// A generator of . - /// Thrown when or is null. - public static IAny As(this IAny generator, Func factory) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - if (factory is null) { throw new ArgumentNullException(nameof(factory)); } - - RandomSource? source = AnyDerivation.SourceOf(generator); - bool reproducible = AnyDerivation.IsReproducible(generator); - - return new DerivedAny(source, reproducible, () => { - TSource value = generator.Generate(); - - return AnyDerivation.Invoke(() => factory(value), source, reproducible, () => $"the factory passed to As(...) threw for the generated value {AnyDerivation.Display(value)}"); - }); - } - -} diff --git a/JustDummies/AnyFtpUri.cs b/JustDummies/AnyFtpUri.cs deleted file mode 100644 index 8b7a0978..00000000 --- a/JustDummies/AnyFtpUri.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace JustDummies; - -/// -/// A generator of arbitrary ftp URIs — the classic ftp://user:password@host/path shape of legacy -/// code. An FTP URI carries user-info but no query and no fragment, so this builder does not expose them. -/// -public sealed class AnyFtpUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyFtpUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Pins the host. Must be an ASCII host name (pass the punycode form for internationalized hosts). - /// Thrown when is null. - /// Thrown when is empty, non-ASCII or not a valid host name. - public AnyFtpUri WithHost(string host) { - return new AnyFtpUri(_source, _spec.WithHost(UriSpec.RequireHost(host, nameof(host)), UriSpec.Label(nameof(WithHost), host))); - } - - /// Includes arbitrary user:password user-info. - public AnyFtpUri WithUserInfo() { - return new AnyFtpUri(_source, _spec.WithUserInfo(null, null, UriSpec.Label(nameof(WithUserInfo)))); - } - - /// Includes user-info with the given and an arbitrary password. - /// Thrown when is null. - /// Thrown when contains a non-unreserved character. - public AnyFtpUri WithUserInfo(string user) { - return new AnyFtpUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), null, UriSpec.Label(nameof(WithUserInfo), user))); - } - - /// Includes the given and user-info. - /// Thrown when an argument is null. - /// Thrown when an argument contains a non-unreserved character. - public AnyFtpUri WithUserInfo(string user, string password) { - return new AnyFtpUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), UriSpec.RequireUserInfoPart(password, nameof(password)), UriSpec.Label(nameof(WithUserInfo), user, password))); - } - - /// Includes an arbitrary non-default port. - public AnyFtpUri WithPort() { - return new AnyFtpUri(_source, _spec.WithPort(null, UriSpec.Label(nameof(WithPort)))); - } - - /// Includes the given . - /// Thrown when is outside 1..65535. - public AnyFtpUri WithPort(int port) { - return new AnyFtpUri(_source, _spec.WithPort(UriSpec.RequirePort(port, nameof(port)), UriSpec.Label(nameof(WithPort), port))); - } - - /// Fixes the path to exactly segments. Declared once per generator. - /// Thrown when is negative. - /// Thrown when a path constraint is already declared. - public AnyFtpUri WithPathSegments(int count) { - return new AnyFtpUri(_source, _spec.WithPath(UriPathMode.Exact, UriSpec.RequireSegmentCount(count, nameof(count)), UriSpec.Label(nameof(WithPathSegments), count))); - } - - /// Renders the root path (/) with no segments. Declared once per generator. - /// Thrown when a path constraint is already declared. - public AnyFtpUri WithoutPath() { - return new AnyFtpUri(_source, _spec.WithPath(UriPathMode.Root, 0, ConstraintCall.Of(nameof(WithoutPath)))); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyGenerationException.cs b/JustDummies/AnyGenerationException.cs deleted file mode 100644 index 114a0132..00000000 --- a/JustDummies/AnyGenerationException.cs +++ /dev/null @@ -1,159 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// Thrown when a generation cannot be completed even though every declared constraint was accepted — most -/// commonly when a factory passed to or a composer passed to -/// rejects a generated value. Whenever the failing generator draws from -/// one of the library's random contexts, the message names the seed that replays the run and -/// carries it. -/// -/// -/// The library prefers detecting contradictions before generation — those throw -/// at declaration time. Reaching this exception therefore -/// usually means the constraints declared on the generator were weaker than the invariant the factory enforces; -/// the fix is to tighten the constraints so they express that invariant. -/// -public sealed class AnyGenerationException : DummyException { - - #region Statics members declarations - - /// - /// The bounded walk around the drawn candidate found nothing the exclusions allow: every representable value - /// within steps of it, in both directions, is excluded or out of bounds. - /// - internal static AnyGenerationException LocalSearchExhausted(string typeName, Replay replay, int budget) { - return NearTheCandidate(typeName, replay, - $"Every representable value within {budget.ToString(CultureInfo.InvariantCulture)} steps of the drawn candidate, in both directions, is excluded or out of bounds. Values further away were not examined, so this is an exhausted local search rather than an empty range."); - } - - /// - /// Snapping the drawn candidate onto the scale lattice could not leave an excluded point without leaving the - /// allowed range. - /// - internal static AnyGenerationException GridNudgeExhausted(string typeName, Replay replay) { - return NearTheCandidate(typeName, replay, "The grid nudge could not leave the excluded point within the allowed range."); - } - - /// - /// Nudging the drawn candidate away from an excluded point could not find a free value without leaving the - /// allowed range. - /// - internal static AnyGenerationException ExclusionNudgeExhausted(string typeName, Replay replay) { - return NearTheCandidate(typeName, replay, "The exclusion nudge could not leave the excluded point within the allowed range."); - } - - /// - /// Builds the exception for an enum with no member to draw from — nothing was constrained, the type simply - /// offers nothing. - /// - internal static AnyGenerationException EnumDeclaresNoMembers(string enumName) { - return new AnyGenerationException($"Cannot generate an arbitrary {enumName} value because the enum declares no members."); - } - - /// - /// Builds the exception for a relative URI whose every component was declared away — no path segment, no query, - /// no fragment, no root — leaving the empty string, which is not a valid URI reference. - /// - internal static AnyGenerationException EmptyRelativeReference(Replay replay) { - return new AnyGenerationException("A relative URI with exactly 0 path segments and no query, fragment or root is empty, which is not a valid URI reference. " + - $"Add a query, a fragment, Rooted(), or a positive segment count. {replay.Guidance}", - replay.Seed); - } - - /// - /// Builds the exception for a pattern whose expansion outgrew the generation ceiling, which exists so no - /// pattern can grow the buffer without bound. - /// - internal static AnyGenerationException PatternExceedsGenerationLimit(int limit) { - return new AnyGenerationException($"The pattern produced a string longer than the {limit}-character generation limit. This ceiling guards against runaway expansion; a pattern can reach it " + - "either through a nested unbounded quantifier (such as \"(a+)+\") or through bounded quantifiers whose product is very large (such as \"(a{1000}){1000}\")."); - } - - /// - /// Builds the exception for a pattern every draw of which the .NET engine refused to match — the generator and - /// the engine disagree about the same pattern, which only a degenerate empty-match shape provokes. - /// - internal static AnyGenerationException PatternVerificationFailed(string attempts) { - return new AnyGenerationException($"Generation failed: after {attempts} attempts, every value the pattern generator built was rejected by the .NET engine for the same pattern. " + - "This happens only for a degenerate pattern whose empty-match behaviour the generator cannot mirror; rewrite it with the supported subset, or generate the value another way."); - } - - /// - /// Builds the exception for a caller-supplied factory or composer that threw, naming what was being generated - /// and how to replay the run. - /// - /// - /// stays a thunk all the way in here, and is called once, on this path only: - /// rendering the generated values would run the caller's ToString() and allocate the sentence on every - /// successful draw otherwise — which is every draw a test actually makes. - /// - internal static AnyGenerationException FactoryFailed(Func failure, Exception cause, RandomSource? source, bool reproducible) { - // A derivation over a foreign generator carries no source to name, and then there is nothing to replay. - Replay? replay = null; - if (source is not null) { - replay = reproducible ? Replay.Of(source) : Replay.PartialOf(source); - } - - string message = $"Generation failed: {failure()} ({cause.GetType().Name}: {cause.Message})."; - if (replay is not null) { - message += $" {replay.Guidance}"; - } - - return new AnyGenerationException(message, replay?.Seed, cause); - } - - /// - /// Writes the sentence every near-the-candidate failure shares, and wraps as the - /// inner failure so the developer-facing detail travels with the exception rather than in its message. - /// - /// - /// Private on purpose, like the factories above are internal on purpose: it names the grammar of the message, - /// not a failure, so every caller is a named case. And nothing here guards its arguments — building an - /// exception must never throw, or the failure being reported is replaced by a failure about reporting it - /// (ADR-0045, which exempts exception types for exactly that reason). - /// - private static AnyGenerationException NearTheCandidate(string typeName, Replay replay, string diagnostic) { - return new AnyGenerationException($"Generation failed: no {typeName} value near the drawn candidate satisfies the exclusions. {replay.Guidance}", - replay.Seed, - new InvalidOperationException(diagnostic)); - } - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// A description of the failed generation. - public AnyGenerationException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class wrapping an underlying failure. - /// - /// A description of the failed generation. - /// The underlying failure. - public AnyGenerationException(string message, Exception innerException) : base(message, innerException) { } - - internal AnyGenerationException(string message, int? seed, Exception innerException) : base(message, innerException) { - Seed = seed; - } - - internal AnyGenerationException(string message, int? seed) : base(message) { - Seed = seed; - } - - /// - /// The seed of the random context the failing generation drew from, when it is known. Under the ambient context - /// (Any.Reproducibly(...)) pass it to Any.Reproducibly(seed, ...) to replay the run; a value drawn - /// from an Any.WithSeed(seed) context already replays deterministically on its own. The failure message - /// states which of the two applies. null when the failing generator does not draw from one of the - /// library's random contexts. - /// - public int? Seed { get; } - -} diff --git a/JustDummies/AnyGuid.cs b/JustDummies/AnyGuid.cs deleted file mode 100644 index 96ebebd5..00000000 --- a/JustDummies/AnyGuid.cs +++ /dev/null @@ -1,208 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values, drawn from the seedable source — unlike -/// , a generated identifier is reproducible inside an -/// Any.Reproducibly(...) run. An unconstrained draw is, for every practical purpose, never -/// ; chain to make that requirement explicit, or -/// to pin the empty identifier. Contradictory constraints fail eagerly with a -/// naming both sides. -/// -public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint { - - /// How many bytes a is made of — its 128 bits, which a draw fills whole. - private const int GuidByteCount = 16; - - /// How many values a pin leaves producible — the one it fixed. - private const int PinnedCardinality = 1; - - #region Statics members declarations - - internal static AnyGuid Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyGuid(source, null, null, null, null, []); - } - - private static string V(Guid value) { - return value.ToString("D"); - } - - private static string Join(Guid[] values) { - return string.Join(", ", values.Select(V)); - } - - // Increments the 16-byte buffer by one with carry, from the last byte down — the full-width successor of - // new Guid(bytes). Incrementing only the last byte (the former behaviour) wraps 255 back to 0 and can cycle - // forever when every last-byte variant of a prefix is excluded; propagating the carry into the higher bytes - // cannot, because it walks distinct values across the whole 128-bit space. - private static void Increment(byte[] bytes) { - for (int i = bytes.Length - 1; i >= 0; i--) { - if (++bytes[i] != 0) { return; } - } - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly List? _effectiveAllowed; - private readonly IReadOnlyList _excluded; - private readonly HashSet _excludedSet; - private readonly Guid? _pinned; - private readonly ConstraintCall? _pinnedConstraint; - private readonly RandomSource _source; - - #endregion - - private AnyGuid(RandomSource source, Guid? pinned, ConstraintCall? pinnedConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, IReadOnlyList excluded) { - _source = source; - _pinned = pinned; - _pinnedConstraint = pinnedConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _excluded = excluded; - // Materialized once here — "constrain once, draw many": Generate never refilters the allow-list, and - // the exclusion walk tests membership against a set rather than rescanning the list on every step. - _excludedSet = [.. excluded]; - _effectiveAllowed = allowed?.Where(value => !_excludedSet.Contains(value)).ToList(); - } - - RandomSource? IHasRandomSource.Source => _source; - - // Pinned to a single value, or bounded by an allow-list; otherwise the domain is effectively unbounded. - long? ICardinalityHint.DistinctCardinality => _pinned is not null ? PinnedCardinality : _effectiveAllowed?.Count; - - // Mirrors Generate: the pin, then the allow-list, then the full space minus the exclusions. - bool ICardinalityHint.Contains(Guid value) { - if (_pinned is Guid pinned) { return pinned == value; } - if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } - - return !_excluded.Contains(value); - } - - /// Requires an identifier different from . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyGuid NonEmpty() { - return WithExcluded([Guid.Empty], ConstraintCall.Of(nameof(NonEmpty))); - } - - /// Pins the identifier to . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyGuid Empty() { - return Validated(new AnyGuid(_source, Guid.Empty, ConstraintCall.Of(nameof(Empty)), _allowed, _allowedConstraint, _excluded), ConstraintCall.Of(nameof(Empty))); - } - - /// Requires the identifier to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyGuid OneOf(params Guid[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(OneOf), Join(values)); - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == constraint) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(constraint, _allowedConstraint); } - - return Validated(new AnyGuid(_source, _pinned, _pinnedConstraint, values.Distinct().ToArray(), constraint, _excluded), constraint); - } - - /// Requires the identifier to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyGuid Except(params Guid[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values))); - } - - /// - /// Requires the identifier to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated identifier must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyGuid DifferentFrom(Guid value) { - return WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), V(value))); - } - - /// - public Guid Generate() { - if (_pinned is Guid pinned) { return pinned; } - - SeededRandom random = _source.Current; - if (_effectiveAllowed is not null) { - return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; - } - - byte[] bytes = new byte[GuidByteCount]; - random.NextBytes(bytes); - Guid candidate = new(bytes); - // Colliding with an excluded identifier has probability |excluded| / 2^128 per draw. On a collision, - // walk the whole 128-bit value with carry — the full-width successor of the drawn bytes — off the - // excluded values. The exclusion set can never fill the 128-bit space, so the walk visits distinct - // values until it lands on an allowed one and terminates: the same deterministic escape - // OrdinalIntervalSpec and WideIntervalSpec already use for their 128-bit siblings. - while (_excludedSet.Contains(candidate)) { - Increment(bytes); - candidate = new(bytes); - } - - return candidate; - } - - private AnyGuid WithExcluded(Guid[] values, ConstraintCall applying) { - List excluded = [.. _excluded, .. values]; - - return Validated(new AnyGuid(_source, _pinned, _pinnedConstraint, _allowed, _allowedConstraint, excluded), applying); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private AnyGuid Validated(AnyGuid candidate, ConstraintCall applying) { - if (candidate._pinned is Guid pinned) { - if (candidate._excluded.Contains(pinned)) { - throw ConflictingAnyConstraintException.PinnedValueExcluded(applying, candidate._pinnedConstraint!, V(pinned)); - } - if (candidate._allowed is not null && !candidate._allowed.Contains(pinned)) { - throw ConflictingAnyConstraintException.PinnedValueNotAllowed(applying, candidate._pinnedConstraint!, V(pinned), candidate._allowedConstraint!); - } - - return candidate; - } - - if (candidate._effectiveAllowed is not null && candidate._effectiveAllowed.Count == 0) { - throw ConflictingAnyConstraintException.NoValueRemains(applying, $"no value {candidate._allowedConstraint} allows remains available"); - } - - return candidate; - } - -} diff --git a/JustDummies/AnyHalf.cs b/JustDummies/AnyHalf.cs deleted file mode 100644 index 9e889d11..00000000 --- a/JustDummies/AnyHalf.cs +++ /dev/null @@ -1,202 +0,0 @@ -#if NET8_0_OR_GREATER -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. Available on -/// the net8.0 target only, like the type itself. -/// -public sealed class AnyHalf : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyHalf Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyHalf(source, ContinuousIntervalSpec.Unconstrained("Half", value => V((Half)value), value => (double)(Half)value, value => NextUp((Half)value), -(double)Half.MaxValue, (double)Half.MaxValue)); - } - - private static string V(Half value) { - return value.ToString(null, CultureInfo.InvariantCulture); - } - - private static string Join(Half[] values) { - return string.Join(", ", values.Select(V)); - } - - /// The next representable half above — the exclusive-bound arithmetic. - private static double NextUp(Half value) { - short bits = BitConverter.HalfToInt16Bits(value); - if (bits >= 0) { bits++; } else if (bits == short.MinValue) { bits = 1; } else { bits--; } - - Half next = BitConverter.Int16BitsToHalf(bits); - - return Half.IsInfinity(next) ? double.PositiveInfinity : (double)next; - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly ContinuousIntervalSpec _spec; - - #endregion - - private AnyHalf(RandomSource source, ContinuousIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - // The allow-list holds the doubles the supplied halves widen to, so membership tests the same widening. - bool ICardinalityHint.Contains(Half value) => _spec.Contains((double)value); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf Positive() { - return new AnyHalf(_source, _spec.WithMinimumAbove(0d, ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf Negative() { - return new AnyHalf(_source, _spec.WithMaximumBelow(0d, ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf Zero() { - return new AnyHalf(_source, _spec.WithMinimum(0d, ConstraintCall.Of(nameof(Zero))).WithMaximum(0d, ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf NonZero() { - return new AnyHalf(_source, _spec.WithExcluded([0d], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf GreaterThan(Half value) { - ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); - - return new AnyHalf(_source, _spec.WithMinimumAbove((double)value, ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf GreaterThanOrEqualTo(Half value) { - ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); - - return new AnyHalf(_source, _spec.WithMinimum((double)value, ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf LessThan(Half value) { - ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); - - return new AnyHalf(_source, _spec.WithMaximumBelow((double)value, ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf LessThanOrEqualTo(Half value) { - ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); - - return new AnyHalf(_source, _spec.WithMaximum((double)value, ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when a bound is not finite or is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf Between(Half minimum, Half maximum) { - ContinuousIntervalSpec.EnsureFinite((double)minimum, nameof(minimum)); - ContinuousIntervalSpec.EnsureFinite((double)maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyHalf(_source, _spec.WithMinimum((double)minimum, constraint).WithMaximum((double)maximum, constraint)); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf OneOf(params Half[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (Half value in values) { ContinuousIntervalSpec.EnsureFinite((double)value, nameof(values)); } - - return new AnyHalf(_source, _spec.WithAllowed(values.Select(value => (double)value).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf Except(params Half[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (Half value in values) { ContinuousIntervalSpec.EnsureFinite((double)value, nameof(values)); } - - return new AnyHalf(_source, _spec.WithExcluded(values.Select(value => (double)value).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyHalf DifferentFrom(Half value) { - ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); - - return new AnyHalf(_source, _spec.WithExcluded([(double)value], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public Half Generate() { - return (Half)_spec.Generate(_source); - } - -} -#endif diff --git a/JustDummies/AnyInt128.cs b/JustDummies/AnyInt128.cs deleted file mode 100644 index 0bf9b281..00000000 --- a/JustDummies/AnyInt128.cs +++ /dev/null @@ -1,203 +0,0 @@ -#if NET8_0_OR_GREATER -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// Available on the net8.0 target only, like the type itself. -/// -public sealed class AnyInt128 : IAny, IHasRandomSource, ICardinalityHint { - - /// - /// The bit that tells a negative from a non-negative one. Flipping it maps a signed - /// value onto its order-preserving ordinal and back — the 128-bit twin of 's - /// 64-bit mapping. It is static readonly rather than const because C# has no constant of a - /// user-defined type such as . - /// - private static readonly UInt128 SignBit = UInt128.One << 127; - - #region Statics members declarations - - internal static AnyInt128 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyInt128(source, WideIntervalSpec.Unconstrained("Int128", ordinal => V(Val(ordinal)), Ord(Int128.MinValue), Ord(Int128.MaxValue))); - } - - private static UInt128 Ord(Int128 value) { - return unchecked((UInt128)value) ^ SignBit; - } - - private static Int128 Val(UInt128 ordinal) { - return unchecked((Int128)(ordinal ^ SignBit)); - } - - private static string V(Int128 value) { - return value.ToString(null, CultureInfo.InvariantCulture); - } - - private static string Join(Int128[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly WideIntervalSpec _spec; - - #endregion - - private AnyInt128(RandomSource source, WideIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(Int128 value) => _spec.Contains(Ord(value)); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 Positive() { - return new AnyInt128(_source, _spec.WithMinimum(Ord(1), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 Negative() { - return new AnyInt128(_source, _spec.WithMaximum(Ord(-1), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 Zero() { - return new AnyInt128(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 NonZero() { - return new AnyInt128(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 GreaterThan(Int128 value) { - return new AnyInt128(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 GreaterThanOrEqualTo(Int128 value) { - return new AnyInt128(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 LessThan(Int128 value) { - return new AnyInt128(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 LessThanOrEqualTo(Int128 value) { - return new AnyInt128(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 Between(Int128 minimum, Int128 maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyInt128(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 MultipleOf(Int128 value) { - if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyInt128(_source, _spec.WithStep((UInt128)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 OneOf(params Int128[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt128(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 Except(params Int128[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt128(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt128 DifferentFrom(Int128 value) { - return new AnyInt128(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public Int128 Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} -#endif diff --git a/JustDummies/AnyInt16.cs b/JustDummies/AnyInt16.cs deleted file mode 100644 index a83dc1cd..00000000 --- a/JustDummies/AnyInt16.cs +++ /dev/null @@ -1,192 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyInt16 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyInt16(source, OrdinalIntervalSpec.Unconstrained("Int16", ordinal => V(Val(ordinal)), Ord(short.MinValue), Ord(short.MaxValue))); - } - - private static ulong Ord(short value) { - return OrdinalMapping.FromInt64(value); - } - - private static short Val(ulong ordinal) { - return (short)OrdinalMapping.ToInt64(ordinal); - } - - private static string V(short value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(short[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyInt16(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(short value) => _spec.Contains(Ord(value)); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 Positive() { - return new AnyInt16(_source, _spec.WithMinimum(Ord(1), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 Negative() { - return new AnyInt16(_source, _spec.WithMaximum(Ord(-1), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 Zero() { - return new AnyInt16(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 NonZero() { - return new AnyInt16(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 GreaterThan(short value) { - return new AnyInt16(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 GreaterThanOrEqualTo(short value) { - return new AnyInt16(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 LessThan(short value) { - return new AnyInt16(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 LessThanOrEqualTo(short value) { - return new AnyInt16(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 Between(short minimum, short maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyInt16(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 MultipleOf(short value) { - if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyInt16(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 OneOf(params short[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt16(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 Except(params short[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt16(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt16 DifferentFrom(short value) { - return new AnyInt16(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public short Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyInt32.cs b/JustDummies/AnyInt32.cs deleted file mode 100644 index 25a1a1b8..00000000 --- a/JustDummies/AnyInt32.cs +++ /dev/null @@ -1,208 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values. Each constraint narrows the domain the value is -/// drawn from — the constraints express what the surrounding code requires of the value (an invariant, a -/// precondition), never what the test asserts. Constraints that contradict each other fail immediately with a -/// naming both sides, so an impossible Arrange reads as -/// the test defect it is. -/// -/// -/// -/// Instances are immutable recipes: every method returns a new generator, and the value is drawn only when -/// runs, from -/// the random context the generator was created with. Values are built to satisfy the constraints in -/// one draw — the library never generates candidates and retries. -/// -/// -/// -/// int quantity = Any.Int32().Positive().Generate(); -/// int percentage = Any.Int32().Between(0, 100).Generate(); -/// Any.Int32().GreaterThan(100).LessThan(10); // throws ConflictingAnyConstraintException -/// -/// -/// -public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyInt32 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyInt32(source, OrdinalIntervalSpec.Unconstrained("Int32", ordinal => V(Val(ordinal)), Ord(int.MinValue), Ord(int.MaxValue))); - } - - private static ulong Ord(int value) { - return OrdinalMapping.FromInt64(value); - } - - private static int Val(ulong ordinal) { - return (int)OrdinalMapping.ToInt64(ordinal); - } - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(int[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyInt32(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(int value) => _spec.Contains(Ord(value)); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 Positive() { - return new AnyInt32(_source, _spec.WithMinimum(Ord(1), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 Negative() { - return new AnyInt32(_source, _spec.WithMaximum(Ord(-1), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 Zero() { - return new AnyInt32(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 NonZero() { - return new AnyInt32(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 GreaterThan(int value) { - return new AnyInt32(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 GreaterThanOrEqualTo(int value) { - return new AnyInt32(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 LessThan(int value) { - return new AnyInt32(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 LessThanOrEqualTo(int value) { - return new AnyInt32(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 Between(int minimum, int maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyInt32(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 MultipleOf(int value) { - if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyInt32(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 OneOf(params int[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt32(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 Except(params int[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt32(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt32 DifferentFrom(int value) { - return new AnyInt32(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public int Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyInt64.cs b/JustDummies/AnyInt64.cs deleted file mode 100644 index a5192dfd..00000000 --- a/JustDummies/AnyInt64.cs +++ /dev/null @@ -1,192 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyInt64 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyInt64(source, OrdinalIntervalSpec.Unconstrained("Int64", ordinal => V(Val(ordinal)), Ord(long.MinValue), Ord(long.MaxValue))); - } - - private static ulong Ord(long value) { - return OrdinalMapping.FromInt64(value); - } - - private static long Val(ulong ordinal) { - return OrdinalMapping.ToInt64(ordinal); - } - - private static string V(long value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(long[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyInt64(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(long value) => _spec.Contains(Ord(value)); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 Positive() { - return new AnyInt64(_source, _spec.WithMinimum(Ord(1), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 Negative() { - return new AnyInt64(_source, _spec.WithMaximum(Ord(-1), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 Zero() { - return new AnyInt64(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 NonZero() { - return new AnyInt64(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 GreaterThan(long value) { - return new AnyInt64(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 GreaterThanOrEqualTo(long value) { - return new AnyInt64(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 LessThan(long value) { - return new AnyInt64(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 LessThanOrEqualTo(long value) { - return new AnyInt64(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 Between(long minimum, long maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyInt64(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 MultipleOf(long value) { - if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyInt64(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 OneOf(params long[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt64(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 Except(params long[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyInt64(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyInt64 DifferentFrom(long value) { - return new AnyInt64(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public long Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyList.cs b/JustDummies/AnyList.cs deleted file mode 100644 index a1121f00..00000000 --- a/JustDummies/AnyList.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values over an element generator. Shares the collection -/// constraint surface () — count bounds and contained values — -/// and adds to require pairwise-distinct elements. -/// -/// The element type. -public sealed class AnyList : AnyCollection, AnyList> { - - internal AnyList(RandomSource? source, CollectionState state) : base(source, state) { } - - /// Requires the elements to be pairwise distinct (default equality). - /// A new generator carrying the added constraint. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnyList Distinct() { - return With(State.AsDistinct(null, ConstraintCall.Of(nameof(Distinct)))); - } - - /// Requires the elements to be pairwise distinct under . - /// The equality comparer deciding whether two elements are the same. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnyList Distinct(IEqualityComparer comparer) { - if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } - - return With(State.AsDistinct(comparer, ConstraintCall.Of(nameof(Distinct), "comparer"))); - } - - private protected override AnyList With(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return new AnyList(SourceOrNull, state); - } - - private protected override List Build(List items) { - if (items is null) { throw new ArgumentNullException(nameof(items)); } - - return items; - } - -} diff --git a/JustDummies/AnyMailtoUri.cs b/JustDummies/AnyMailtoUri.cs deleted file mode 100644 index d5c63f26..00000000 --- a/JustDummies/AnyMailtoUri.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace JustDummies; - -/// -/// A generator of arbitrary mailto URIs — mailto:local@domain, optionally with headers. The -/// local-part and domain are drawn from ASCII characters, so the value is an arbitrary well-formed address, never -/// a realistic one (this library does not fabricate plausible data). A mailto URI is not authority-based, so this -/// builder exposes no host/port/user-info in the authority sense — only the address parts and headers. -/// -public sealed class AnyMailtoUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyMailtoUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Pins the local-part (the text before @). - /// Thrown when is null. - /// Thrown when contains a non-unreserved character. - public AnyMailtoUri WithLocalPart(string localPart) { - return new AnyMailtoUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(localPart, nameof(localPart)), null, UriSpec.Label(nameof(WithLocalPart), localPart))); - } - - /// Pins the domain (the text after @). Must be an ASCII host name. - /// Thrown when is null. - /// Thrown when is empty, non-ASCII or not a valid host name. - public AnyMailtoUri WithDomain(string domain) { - return new AnyMailtoUri(_source, _spec.WithHost(UriSpec.RequireHost(domain, nameof(domain)), UriSpec.Label(nameof(WithDomain), domain))); - } - - /// Includes an arbitrary header (e.g. ?subject=...). - public AnyMailtoUri WithHeaders() { - return new AnyMailtoUri(_source, _spec.WithQuery()); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyOneOf.cs b/JustDummies/AnyOneOf.cs deleted file mode 100644 index 172e954f..00000000 --- a/JustDummies/AnyOneOf.cs +++ /dev/null @@ -1,151 +0,0 @@ -namespace JustDummies; - -/// -/// A generator that draws an arbitrary value from an explicit, fixed pool supplied by the caller — the -/// dummy for a value whose domain is a closed set a test does not assert on (one of the currencies a context is -/// configured with, one of the orders already in a fixture, one of a handful of domain states). Unlike the typed -/// builders' OneOf, which narrows within a scalar's own domain, this draws from values the library -/// could never synthesize on its own. It still composes like any other generator — pipe it through As(...) -/// into a value object, make it optional with OrNull(), or fold it into Combine(...) and the -/// collection generators. -/// -/// -/// -/// Each draws one value uniformly from the pool, from the generator's random context — -/// so a run is reproducible under a seed, exactly like every other generator. Duplicate values are collapsed -/// under , so no value carries a heavier weight for being listed -/// twice, and the number of distinct values is the exact size of the domain a distinct collection -/// (SetOf, a dictionary's keys) gates against. -/// -/// -/// The pool is the whole shape of the specification: is opaque to the -/// library, so there is no type-specific constraint to offer. What it does expose is the type-agnostic -/// exclusion pair /, which every other generator carries — -/// they remove values from the pool rather than describing a shape, and removing everything is a -/// at declaration, like any other emptied domain. -/// -/// -/// A null element is rejected at construction: nullability is an orthogonal concern expressed by -/// OrNull(), never smuggled into the pool. -/// -/// -/// -/// Currency currency = Any.OneOf(eur, usd, gbp).Generate(); -/// Order order = Any.ElementOf(existingOrders).DifferentFrom(theOneAlreadyUsed).Generate(); -/// -/// -/// -/// The type of the pooled values. -public sealed class AnyOneOf : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - // Validates and deduplicates the caller's pool, then builds the generator. As an internal boundary it guards its - // own arguments per the null-argument convention (ADR-0045); the public factories additionally reject a null - // array first, under the caller-facing parameter name, before delegating here. The factory names itself so a - // later exclusion conflict can say which declaration it emptied. - internal static AnyOneOf FromPool(RandomSource source, IReadOnlyList values, ConstraintCall declaring) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (declaring is null) { throw new ArgumentNullException(nameof(declaring)); } - if (values.Count == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element; use OrNull() to make the whole generator nullable.", nameof(values)); } - - T[] distinct = values.Distinct().ToArray(); - - return new AnyOneOf(source, distinct, distinct, declaring); - } - - #endregion - - #region Fields declarations - - // The pool as declared, before any exclusion removed a value from it. It is what tells a conflict message - // whether the applied exclusion forbids the whole declared pool or merely the part earlier exclusions had left, - // so the message can make the stronger claim exactly when it is true. - private readonly IReadOnlyList _declared; - private readonly ConstraintCall _declaringConstraint; - private readonly RandomSource _source; - private readonly IReadOnlyList _values; - - #endregion - - private AnyOneOf(RandomSource source, IReadOnlyList values, IReadOnlyList declared, ConstraintCall declaringConstraint) { - _source = source; - _values = values; - _declared = declared; - _declaringConstraint = declaringConstraint; - } - - RandomSource? IHasRandomSource.Source => _source; - - // The pool is fixed and deduplicated at construction under the default comparer, and an exclusion filters it - // under that same comparer, so its count is the exact number of distinct values still drawable and membership is - // a direct lookup. The two answers do not survive a custom comparer equally. The count does: a pool of n values - // is at most n distinct under any comparer, so the advertised size stays a sound upper bound. Membership does - // not: a comparer stricter than the default one keeps apart values this lookup calls equal, so it may report a - // value as drawable that, under that comparer, the pool can never yield. A distinct collection carrying a custom - // comparer must therefore not consult membership — CollectionState.FixedOutsideCount is where that is enforced. - long? ICardinalityHint.DistinctCardinality => _values.Count; - - bool ICardinalityHint.Contains(T value) => _values.Contains(value); - - /// - /// Requires the generated value to be none of the supplied — they are removed from - /// the pool under , so the draw stays a single uniform pick over - /// what is left. May be declared several times; the exclusions accumulate. A value that is not in the pool - /// removes nothing. - /// - /// The values the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - /// Thrown when no pooled value is left once the excluded ones are removed. - public AnyOneOf Except(params T[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element.", nameof(values)); } - - return Excluding(values, ConstraintCall.OfElided(nameof(Except))); - } - - /// - /// Requires the generated value to differ from — typically a value the test already - /// holds, to exercise an inequality path while still drawing from the pool - /// (Any.ElementOf(orders).DifferentFrom(theOneAlreadyUsed)). Semantically equivalent to - /// ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when no pooled value is left once is removed. - public AnyOneOf DifferentFrom(T value) { - if (value is null) { throw new ArgumentNullException(nameof(value)); } - - return Excluding([value], ConstraintCall.OfElided(nameof(DifferentFrom))); - } - - /// - public T Generate() { - return _values[_source.Current.Next(_values.Count)]; - } - - private AnyOneOf Excluding(IReadOnlyList excluded, ConstraintCall applying) { - T[] survivors = _values.Where(value => !excluded.Contains(value)).ToArray(); - if (survivors.Length == 0) { - // The values themselves are never rendered: T is opaque, so its ToString is the caller's, not the - // library's, and could be anything. Naming the two declarations in play is what the caller needs. The - // claim is qualified only when this exclusion leaves some declared value standing — the emptiness then - // genuinely took the earlier exclusions too. When it forbids the whole declared pool, dropping the - // earlier ones could not help, and saying otherwise would send the caller at the wrong constraint. - string emptied = _declared.Any(value => !excluded.Contains(value)) - ? $"it forbids every value {_declaringConstraint} allows that the exclusions already declared leave" - : $"it forbids every value {_declaringConstraint} allows"; - - throw ConflictingAnyConstraintException.NoValueRemains(applying, emptied); - } - - return new AnyOneOf(_source, survivors, _declared, _declaringConstraint); - } - -} diff --git a/JustDummies/AnyPattern.cs b/JustDummies/AnyPattern.cs deleted file mode 100644 index 526a46b7..00000000 --- a/JustDummies/AnyPattern.cs +++ /dev/null @@ -1,245 +0,0 @@ -#region Usings declarations - -using System.Globalization; -using System.Text.RegularExpressions; - -#endregion - -namespace JustDummies; - -/// -/// A generator of arbitrary strings that match a regular expression — the dummy for a value whose format is -/// defined by a pattern (an order reference, a SKU, a currency code). The pattern is the whole shape of the -/// specification: unlike this generator exposes no further shape or length constraints — -/// express those inside the pattern instead. What it does expose is the exclusion pair and -/// , which every other generator carries. It also composes like any other generator: -/// pipe it through As(...) into a value object, make it optional with OrNull(), or fold it into -/// Combine(...) and the collection generators. -/// -/// -/// -/// The pattern is parsed once, when the generator is created; each then walks the -/// parsed tree, drawing every choice and repetition count from the generator's random context — so a run is -/// reproducible under a seed, exactly like every other generator. Wherever the pattern leaves a character -/// free, values are drawn from printable ASCII (\s may also yield a tab); a character the pattern -/// names explicitly is emitted as written, control characters included. Values are built directly rather than -/// generated-and-filtered. -/// -/// -/// A built value is then checked against the real .NET engine and, on the rare miss, redrawn. The structural -/// build mirrors the regular subset of the engine, but a few implementation-defined corners of empty-match -/// handling — a nullable alternative under a quantifier, whose emptiness the engine accepts or refuses -/// depending on branch order and form — cannot be mirrored structurally. Rather than model those corners, the -/// invariant "a generated value matches its pattern" is kept by construction: the check is the last word, so a -/// value the engine would reject is never returned. -/// -/// -/// A shape constraint is refused because it would mean building a value in the intersection of two regular -/// languages, which the library has no machinery for. An exclusion asks for nothing of the sort: it -/// never constructs, it rejects. The value is built from the pattern exactly as before and redrawn on a hit — -/// one more predicate inside a loop that already turns. That places it under the exception the library already -/// documents for strings: with no ordinal mapping to build the exclusion into, it is met by a bounded -/// redraw, so an exclusion tight enough to leave nothing surfaces at as a seed-bearing -/// rather than eagerly at declaration. -/// -/// -/// Only the regular subset of the pattern language is supported (see ); -/// a non-regular construct is refused eagerly with an rather than -/// silently mis-generated. -/// -/// -/// -/// string reference = Any.StringMatching(@"^ORD-\d{8}$").Generate(); -/// string other = Any.StringMatching(@"^ORD-\d{8}$").DifferentFrom(existing).Generate(); -/// IAny<OrderReference> any = Any.StringMatching(@"^ORD-\d{8}$").As(OrderReference.Create); -/// -/// -/// -public sealed class AnyPattern : IAny, IHasRandomSource { - - #region Statics members declarations - - // A nested unbounded quantifier can, in principle, expand super-linearly; this ceiling turns that into a clear - // AnyGenerationException instead of an out-of-memory. It is far above any realistic format-validation pattern. - private const int GenerationLimit = 65536; - - // The structural build occasionally produces a value the real engine rejects (see the class remarks). Each build - // is verified and redrawn on a miss; the cap turns a pattern the generator cannot satisfy at all into a clear - // error instead of an unbounded loop. A supported pattern matches on the first build save for these rare corners, - // where a valid value appears within a handful of draws, so the cap is never approached in practice. - private const int MatchAttemptLimit = 1000; - - // Bounded escape for exclusions, kept separate from the match budget above so the two failures never borrow each - // other's evidence: this one counts values the pattern produced and the engine accepted, which an exclusion then - // rejected. Mirrors the string generator's budget, and a genuinely emptied language fails fast against it. - private const int ExclusionRedrawBudget = 10_000; - - // A safety net against catastrophic backtracking while verifying a non-matching draw — a generated value matching - // its own pattern is near-instant, so this bites only a pathological pattern, which is treated as a miss. - private static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1); - - internal static AnyPattern FromPattern(RandomSource source, string pattern, bool ignoreCase) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - - // Parse first: it raises the specific ArgumentException / UnsupportedRegexException for an invalid or - // unsupported pattern. The verifier Regex is NOT built here — the Lazy field below defers it to the - // first actual need — so a pattern whose generation can never succeed (an unbounded quantifier with a - // minimum in the billions, say) never pays, or risks, compiling it. - RegexNode root = RegexParser.Parse(pattern, ignoreCase); - RegexOptions options = ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; - - return new AnyPattern(source, root, pattern, options); - } - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(IReadOnlyList values) { - return string.Join(", ", values.Select(value => $"\"{value}\"")); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList _excluded; - private readonly string _pattern; - private readonly RegexNode _root; - private readonly RandomSource _source; - - // Compiled at most once, on first need inside Generate() — see the FromPattern and Generate() remarks. - // Lazy's default thread-safety mode guarantees the factory runs exactly once even under concurrent - // Generate() calls on the same instance (see the "concurrent draws" test); no thread ever sees, or pays for, a - // second compilation. Anchored with ^(?:…)$ so it decides a full match, and honours only the option the - // generator itself honoured (IgnoreCase), never the rest of a passed Regex's. Shared, not rebuilt, when an - // exclusion derives a new generator: the pattern it verifies is unchanged. - private readonly Lazy _verifier; - - #endregion - - internal AnyPattern(RandomSource source, RegexNode root, string pattern, RegexOptions options) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (root is null) { throw new ArgumentNullException(nameof(root)); } - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - - _source = source; - _root = root; - _pattern = pattern; - _excluded = []; - _verifier = new Lazy(() => new Regex("^(?:" + pattern + ")$", options, MatchTimeout)); - } - - private AnyPattern(AnyPattern origin, IReadOnlyList excluded) { - _source = origin._source; - _root = origin._root; - _pattern = origin._pattern; - _verifier = origin._verifier; - _excluded = excluded; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// - /// Requires the generated value to be none of the supplied . May be declared several - /// times; the exclusions accumulate. The pattern still builds the value — an exclusion only rejects and - /// redraws — so a pattern whose language the exclusions leave nothing of surfaces at - /// as a seed-bearing , never as a declaration-time conflict: the library - /// does not enumerate a regular language to prove it empty. Comparison is ordinal, like string equality - /// itself, whether or not the pattern ignores case. - /// - /// The values the generated value must differ from; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - public AnyPattern Except(params string[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element.", nameof(values)); } - - return Excluding(values); - } - - /// - /// Requires the generated value to differ from — typically an existing value the - /// test already holds, to exercise an inequality path while keeping the format the pattern defines - /// (Any.StringMatching(@"^ORD-\d{8}$").DifferentFrom(existing)). Semantically equivalent to - /// ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is null. - public AnyPattern DifferentFrom(string value) { - if (value is null) { throw new ArgumentNullException(nameof(value)); } - - return Excluding([value]); - } - - /// - public string Generate() { - if (_excluded.Count == 0) { return BuildVerified(); } - - for (int collisions = 0;;) { - string candidate = BuildVerified(); - if (!_excluded.Contains(candidate, StringComparer.Ordinal)) { return candidate; } - if (++collisions >= ExclusionRedrawBudget) { throw Exhausted(); } - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method", - Justification = - "The loop body MUTATES the very list its condition reads: each accepted value is appended to `excluded`, " + - "so a later duplicate in `values` is rejected against the values already taken. A Where clause would read " + - "as a filter over a fixed collection, which is precisely what this is not.")] - private AnyPattern Excluding(IReadOnlyList values) { - List excluded = [.. _excluded]; - foreach (string value in values) { - if (!excluded.Contains(value, StringComparer.Ordinal)) { excluded.Add(value); } - } - - return new AnyPattern(this, excluded); - } - - /// Builds one value the .NET engine agrees matches the pattern, redrawing past the rare structural miss. - private string BuildVerified() { - for (int attempt = 0; attempt < MatchAttemptLimit; attempt++) { - RegexGenerationContext context = new(_source.Current, GenerationLimit); - _root.Append(context); - string value = context.Result(); - - try { - // _root.Append above already refuses, via AnyGenerationException, a pattern whose generation can - // never fit the ceiling — so a pattern like 'a{2147483647,}' never reaches this line, and _verifier - // is never compiled for it. That matters beyond avoiding needless work: compiling a Regex from a - // pattern with a quantifier bound that large has been observed to exhaust memory on at least one - // .NET regex engine implementation, and this class must never risk that for a pattern its own - // ceiling already refuses cleanly. - if (_verifier.Value.IsMatch(value)) { return value; } - } catch (RegexMatchTimeoutException) { - // Could not decide within the budget; treat as a miss and redraw rather than return it unverified. - } - } - - throw AnyGenerationException.PatternVerificationFailed(V(MatchAttemptLimit)); - } - - private AnyGenerationException Exhausted() { - // A pattern generator draws only from its own source, so the seed replays the run fully — never the partial hint. - Replay replay = Replay.Of(_source); - // The claim is the budget, not impossibility. The library builds values from the pattern; it never enumerates - // the language, so it cannot prove one holds no other value. Excluding both words of "^[ab]$" really does - // empty it — but a pattern with one free value in a few hundred thousand exhausts the same budget and is - // still satisfiable, so the message states what was established and no more. - string message = - $"Could not generate a value matching \"{_pattern}\" while excluding {Join(_excluded)}: no candidate " + - $"survived {V(ExclusionRedrawBudget)} draws. The redraw is bounded, so this is an exhausted budget rather " + - "than a proof that the pattern matches no other value — though the usual cause is a pattern the " + - "exclusions leave nothing of (excluding every word of a language with only a few). Loosen the exclusions " + - "or widen the pattern. " + - replay.Guidance; - - return new AnyGenerationException(message, replay.Seed); - } - -} diff --git a/JustDummies/AnyRelativeUri.cs b/JustDummies/AnyRelativeUri.cs deleted file mode 100644 index 96f21ea4..00000000 --- a/JustDummies/AnyRelativeUri.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace JustDummies; - -/// -/// A generator of arbitrary relative URI references — a path with an optional query and fragment, and no -/// scheme or authority (e.g. orders/42?page=2 or /a/b/c#top). A relative reference is well-formed on -/// its own; only its resolution against a base needs a base, not its validity. -/// returns a with false. -/// -public sealed class AnyRelativeUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyRelativeUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Starts the path with / (an absolute-path reference). - public AnyRelativeUri Rooted() { - return new AnyRelativeUri(_source, _spec.Rooted()); - } - - /// Fixes the path to exactly segments. Declared once per generator. - /// Thrown when is negative. - /// Thrown when a path constraint is already declared. - public AnyRelativeUri WithPathSegments(int count) { - return new AnyRelativeUri(_source, _spec.WithPath(UriPathMode.Exact, UriSpec.RequireSegmentCount(count, nameof(count)), UriSpec.Label(nameof(WithPathSegments), count))); - } - - /// Includes an arbitrary query string. - public AnyRelativeUri WithQuery() { - return new AnyRelativeUri(_source, _spec.WithQuery()); - } - - /// Includes an arbitrary fragment. - public AnyRelativeUri WithFragment() { - return new AnyRelativeUri(_source, _spec.WithFragment()); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnySByte.cs b/JustDummies/AnySByte.cs deleted file mode 100644 index c80f3404..00000000 --- a/JustDummies/AnySByte.cs +++ /dev/null @@ -1,192 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnySByte Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnySByte(source, OrdinalIntervalSpec.Unconstrained("SByte", ordinal => V(Val(ordinal)), Ord(sbyte.MinValue), Ord(sbyte.MaxValue))); - } - - private static ulong Ord(sbyte value) { - return OrdinalMapping.FromInt64(value); - } - - private static sbyte Val(ulong ordinal) { - return (sbyte)OrdinalMapping.ToInt64(ordinal); - } - - private static string V(sbyte value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(sbyte[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnySByte(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(sbyte value) => _spec.Contains(Ord(value)); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte Positive() { - return new AnySByte(_source, _spec.WithMinimum(Ord(1), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte Negative() { - return new AnySByte(_source, _spec.WithMaximum(Ord(-1), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte Zero() { - return new AnySByte(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte NonZero() { - return new AnySByte(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte GreaterThan(sbyte value) { - return new AnySByte(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte GreaterThanOrEqualTo(sbyte value) { - return new AnySByte(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte LessThan(sbyte value) { - return new AnySByte(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte LessThanOrEqualTo(sbyte value) { - return new AnySByte(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte Between(sbyte minimum, sbyte maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnySByte(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte MultipleOf(sbyte value) { - if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnySByte(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte OneOf(params sbyte[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnySByte(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte Except(params sbyte[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnySByte(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySByte DifferentFrom(sbyte value) { - return new AnySByte(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public sbyte Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnySequence.cs b/JustDummies/AnySequence.cs deleted file mode 100644 index 867423f9..00000000 --- a/JustDummies/AnySequence.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values over an element generator. The generated -/// sequence is fully materialized: it never defers work, so enumerating it twice yields the same elements -/// and never re-draws. Shares the collection constraint surface -/// () — count bounds and contained values — and adds -/// to require pairwise-distinct elements. -/// -/// -/// Materialize the sequence with , or use the generator -/// through . -/// -/// The element type. -public sealed class AnySequence : AnyCollection, AnySequence> { - - internal AnySequence(RandomSource? source, CollectionState state) : base(source, state) { } - - /// Requires the elements to be pairwise distinct (default equality). - /// A new generator carrying the added constraint. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnySequence Distinct() { - return With(State.AsDistinct(null, ConstraintCall.Of(nameof(Distinct)))); - } - - /// Requires the elements to be pairwise distinct under . - /// The equality comparer deciding whether two elements are the same. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when the constraint cannot be satisfied by the element generator's domain. - public AnySequence Distinct(IEqualityComparer comparer) { - if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } - - return With(State.AsDistinct(comparer, ConstraintCall.Of(nameof(Distinct), "comparer"))); - } - - private protected override AnySequence With(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return new AnySequence(SourceOrNull, state); - } - - private protected override IEnumerable Build(List items) { - if (items is null) { throw new ArgumentNullException(nameof(items)); } - - return items; - } - -} diff --git a/JustDummies/AnySet.cs b/JustDummies/AnySet.cs deleted file mode 100644 index 13cf3448..00000000 --- a/JustDummies/AnySet.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values over an element generator. A set is -/// distinct by nature, so it carries the collection constraint surface -/// () — count bounds and contained values — without a -/// Distinct() toggle. When the element generator advertises fewer distinct values than the requested -/// count, the contradiction is caught eagerly with a ; otherwise a -/// genuine shortfall surfaces at generation as an . -/// -/// The element type. -public sealed class AnySet : AnyCollection, AnySet> { - - internal AnySet(RandomSource? source, CollectionState state) : base(source, state) { } - - private protected override AnySet With(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return new AnySet(SourceOrNull, state); - } - - private protected override HashSet Build(List items) { - if (items is null) { throw new ArgumentNullException(nameof(items)); } - - // The state already deduplicated under the comparer; the set carries the same comparer so later lookups - // by the caller behave identically. - return new HashSet(items, State.Comparer); - } - -} diff --git a/JustDummies/AnySingle.cs b/JustDummies/AnySingle.cs deleted file mode 100644 index 9825abbb..00000000 --- a/JustDummies/AnySingle.cs +++ /dev/null @@ -1,194 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. -/// -public sealed class AnySingle : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnySingle Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnySingle(source, ContinuousIntervalSpec.Unconstrained("Single", value => V((float)value), value => (float)value, value => NextUp((float)value), -float.MaxValue, float.MaxValue)); - } - - private static string V(float value) { - return value.ToString("R", CultureInfo.InvariantCulture); - } - - private static string Join(float[] values) { - return string.Join(", ", values.Select(V)); - } - - /// The next representable float above — the exclusive-bound arithmetic. - private static double NextUp(float value) { - int bits = BitConverter.ToInt32(BitConverter.GetBytes(value), 0); - if (bits >= 0) { bits++; } else if (bits == int.MinValue) { bits = 1; } else { bits--; } - - float next = BitConverter.ToSingle(BitConverter.GetBytes(bits), 0); - - return float.IsInfinity(next) ? double.PositiveInfinity : next; - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly ContinuousIntervalSpec _spec; - - #endregion - - private AnySingle(RandomSource source, ContinuousIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - // The allow-list holds the doubles the supplied floats widen to, so membership tests the same widening. - bool ICardinalityHint.Contains(float value) => _spec.Contains((double)value); - - /// Requires a value strictly greater than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle Positive() { - return new AnySingle(_source, _spec.WithMinimumAbove(0d, ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a value strictly less than zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle Negative() { - return new AnySingle(_source, _spec.WithMaximumBelow(0d, ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the value to exactly zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle Zero() { - return new AnySingle(_source, _spec.WithMinimum(0d, ConstraintCall.Of(nameof(Zero))).WithMaximum(0d, ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle NonZero() { - return new AnySingle(_source, _spec.WithExcluded([0d], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle GreaterThan(float value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnySingle(_source, _spec.WithMinimumAbove(value, ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle GreaterThanOrEqualTo(float value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnySingle(_source, _spec.WithMinimum((double)value, ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle LessThan(float value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnySingle(_source, _spec.WithMaximumBelow(value, ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle LessThanOrEqualTo(float value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnySingle(_source, _spec.WithMaximum((double)value, ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when a bound is not finite or is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle Between(float minimum, float maximum) { - ContinuousIntervalSpec.EnsureFinite(minimum, nameof(minimum)); - ContinuousIntervalSpec.EnsureFinite(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnySingle(_source, _spec.WithMinimum((double)minimum, constraint).WithMaximum((double)maximum, constraint)); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle OneOf(params float[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (float value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } - - return new AnySingle(_source, _spec.WithAllowed(values.Select(value => (double)value).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a non-finite value. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle Except(params float[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - foreach (float value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } - - return new AnySingle(_source, _spec.WithExcluded(values.Select(value => (double)value).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is not finite. - /// Thrown when the constraint contradicts a constraint already declared. - public AnySingle DifferentFrom(float value) { - ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); - return new AnySingle(_source, _spec.WithExcluded([(double)value], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public float Generate() { - return (float)_spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyString.cs b/JustDummies/AnyString.cs deleted file mode 100644 index 1321272e..00000000 --- a/JustDummies/AnyString.cs +++ /dev/null @@ -1,373 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values. Each constraint narrows the shape of the -/// generated string — the constraints express what the surrounding code requires of the value (a value -/// object's format invariant, a contract precondition), never what the test asserts. Constraints that contradict -/// each other fail immediately with a naming both sides, so an -/// impossible Arrange reads as the test defect it is. -/// -/// -/// -/// Instances are immutable recipes: every method returns a new generator, and the value is drawn only when -/// runs, -/// from the random context the generator was created with. Strings are built to satisfy the -/// constraints — laid out as prefix + filler + contained values + filler + suffix — never generated -/// and filtered. That layout means fragments never overlap: the length budget they require is the plain sum -/// of their lengths. -/// -/// -/// Unconstrained, the generator yields 0 to 16 ASCII letters and digits; an unconstrained draw can therefore -/// be empty — chain when the surrounding code requires content. -/// -/// -/// is the one constraint that replaces the layout rather than shaping it: the -/// caller supplies the values, so the draw is a uniform pick from them and every other constraint narrows -/// that set instead of building a string. The constraints still fail at declaration when they contradict -/// each other — which is why a value set is best declared first: constraints that contradict each -/// other on their own terms are refused the moment they are declared, before any value set can reinterpret -/// them as a filter (see ). -/// -/// -/// -/// string code = Any.String().NonEmpty().WithMaxLength(50).StartingWith("ORD-").Generate(); -/// Any.String().WithLength(3).StartingWith("ORD-"); // throws ConflictingAnyConstraintException -/// Any.String().Numeric().StartingWith("ORD-"); // throws ConflictingAnyConstraintException -/// -/// -/// -public sealed class AnyString : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(string[] values) { - return string.Join(", ", values.Select(Q)); - } - - private static string Q(string value) { - return $"\"{value}\""; - } - - private static void RequireText(string value, string parameterName) { - if (value is null) { throw new ArgumentNullException(parameterName); } - if (value.Length == 0) { throw new ArgumentException("The value must not be empty.", parameterName); } - } - - private static void RequireNonNegative(int length, string parameterName) { - SizeGuard.RequireNonNegative(length, parameterName, "length"); - } - - private static void RequireProducible(int length, string parameterName) { - SizeGuard.RequireProducible(length, parameterName, "length"); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly StringSpec _spec; - - #endregion - - internal AnyString(RandomSource source, StringSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - // Only a value set (OneOf) makes the domain small and countable: it is then the exact surviving pool, so a - // distinct collection gates on it eagerly. A shaped string has no such bound — the specification answers null, - // and membership is never consulted on that path (the two answers travel together on one interface). - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - // A distinct collection may pin a null value — an unlikely but legal Containing(null) — and asking whether the - // generator could produce it is a question, not a boundary violation: the answer is simply no, since a value set - // rejects a null element at declaration. The specification's own guard stays the internal boundary (ADR-0045); - // this membership answer must not turn a pinned null into an exception the pool generator never raises. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "False positive on prose. The lines above are the explanation of this member's null handling; the rule " + - "reads the trailing \"(ADR-0045);\" of an English sentence as a statement. Nothing here is commented-out code.")] - bool ICardinalityHint.Contains(string value) => value is not null && _spec.Contains(value); - - /// Requires at least one character. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString NonEmpty() { - return new AnyString(_source, _spec.WithMinLength(1, ConstraintCall.Of(nameof(NonEmpty)))); - } - - /// Fixes the exact length. Declared once per generator. - /// The exact number of characters. - /// A new generator carrying the added constraint. - /// Thrown when is negative or exceeds 1000000, the largest length a generator is asked to produce. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString WithLength(int length) { - RequireProducible(length, nameof(length)); - - return new AnyString(_source, _spec.WithExactLength(length, ConstraintCall.Of(nameof(WithLength), V(length)))); - } - - /// - /// Requires at least characters. A minimum is the only one-sided length bound that - /// enlarges the generated string: the draw spans to plus - /// the default spread. - /// - /// The inclusive minimum number of characters. - /// A new generator carrying the added constraint. - /// Thrown when is negative or exceeds 1000000, the largest length a generator is asked to produce. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString WithMinLength(int length) { - RequireProducible(length, nameof(length)); - - return new AnyString(_source, _spec.WithMinLength(length, ConstraintCall.Of(nameof(WithMinLength), V(length)))); - } - - /// - /// Requires at most characters. A maximum only ever narrows the draw: it never - /// widens it beyond the default spread, so a loose cap still yields the small unconstrained string rather than - /// one sized after the cap. Any non-negative value is accepted, since nothing has to be produced to honour it. - /// - /// The inclusive maximum number of characters. - /// A new generator carrying the added constraint. - /// Thrown when is negative. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString WithMaxLength(int length) { - RequireNonNegative(length, nameof(length)); - - return new AnyString(_source, _spec.WithMaxLength(length, ConstraintCall.Of(nameof(WithMaxLength), V(length)))); - } - - /// - /// Requires a length within the inclusive range [, ]. - /// Equivalent to declaring the two bounds separately, and behaves as they do: the minimum sets the size, the - /// maximum only caps it. A range whose minimum is 0 therefore yields the default spread, not values spread - /// across the whole range — raise to ask for larger strings. - /// - /// The inclusive minimum number of characters. - /// The inclusive maximum number of characters. - /// A new generator carrying the added constraint. - /// Thrown when a bound is negative, or when exceeds 1000000, the largest length a generator is asked to produce. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString WithLengthBetween(int minimum, int maximum) { - RequireProducible(minimum, nameof(minimum)); - RequireNonNegative(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(WithLengthBetween), V(minimum), V(maximum)); - - return new AnyString(_source, _spec.WithMinLength(minimum, constraint).WithMaxLength(maximum, constraint)); - } - - /// Requires the string to start with . Declared once per generator. - /// The required prefix. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString StartingWith(string prefix) { - RequireText(prefix, nameof(prefix)); - - return new AnyString(_source, _spec.WithPrefix(prefix, ConstraintCall.Of(nameof(StartingWith), Q(prefix)))); - } - - /// Requires the string to end with . Declared once per generator. - /// The required suffix. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString EndingWith(string suffix) { - RequireText(suffix, nameof(suffix)); - - return new AnyString(_source, _spec.WithSuffix(suffix, ConstraintCall.Of(nameof(EndingWith), Q(suffix)))); - } - - /// - /// Requires the string to contain . May be declared several times; the contained - /// values are laid out side by side, without overlap, between the prefix and the suffix. - /// - /// The value the generated string must contain. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString Containing(string value) { - RequireText(value, nameof(value)); - - return new AnyString(_source, _spec.WithFragment(value, ConstraintCall.Of(nameof(Containing), Q(value)))); - } - - /// Restricts the string to ASCII letters only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString Alpha() { - return new AnyString(_source, _spec.WithCharset(CharacterSet.Alpha, ConstraintCall.Of(nameof(Alpha)))); - } - - /// Restricts the string to ASCII digits only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString Numeric() { - return new AnyString(_source, _spec.WithCharset(CharacterSet.Numeric, ConstraintCall.Of(nameof(Numeric)))); - } - - /// Restricts the string to ASCII letters and digits only. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString AlphaNumeric() { - return new AnyString(_source, _spec.WithCharset(CharacterSet.AlphaNumeric, ConstraintCall.Of(nameof(AlphaNumeric)))); - } - - /// - /// Restricts the string to the characters of an explicit — a custom alphabet, the - /// general form of //. Use it to reach - /// characters the named sets cannot, most notably non-ASCII text (accents, other scripts), without a - /// literal. Declared once per generator: it occupies the same - /// character-family slot as the named sets, and because the pool is the whole character definition it cannot - /// combine with / — put only the casing you want in the pool. - /// Any anchored fragment (prefix, suffix, contained value) must be drawn from the pool, otherwise the conflict - /// is reported at declaration naming both sides. Duplicate characters collapse and each distinct character is - /// equally likely. The pool is a sequence of UTF-16 code units and must stay within the Basic Multilingual - /// Plane: a surrogate — an emoji or other astral code point, which spans two units — is rejected, because it - /// would be drawn and split unit by unit; draw such values as whole strings with - /// instead. - /// - /// The characters the generated string is drawn from; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a surrogate (an astral code point). - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString WithChars(string pool) { - if (pool is null) { throw new ArgumentNullException(nameof(pool)); } - if (pool.Length == 0) { throw new ArgumentException("The character pool must not be empty.", nameof(pool)); } - if (pool.Any(char.IsSurrogate)) { throw new ArgumentException("The character pool must not contain a surrogate: an emoji or other astral code point spans two UTF-16 code units, which WithChars would draw and split independently. Draw such values as whole strings with OneOf(...) instead.", nameof(pool)); } - - string distinct = new(pool.Distinct().ToArray()); - - return new AnyString(_source, _spec.WithCharPool(distinct, ConstraintCall.Of(nameof(WithChars), Q(pool)))); - } - - /// Requires every alphabetic character to be lowercase. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString LowerCase() { - return new AnyString(_source, _spec.WithCasing(LetterCasing.Lower, ConstraintCall.Of(nameof(LowerCase)))); - } - - /// Requires every alphabetic character to be uppercase. Declared once per generator. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString UpperCase() { - return new AnyString(_source, _spec.WithCasing(LetterCasing.Upper, ConstraintCall.Of(nameof(UpperCase)))); - } - - /// - /// Requires the generated string to be none of the supplied . May be declared several - /// times; the exclusions accumulate. On a shaped string, and unlike the shape constraints, an exclusion - /// is met by a bounded redraw of the constructed layout, so one tight enough to leave the shape - /// unsatisfiable surfaces at as a seed-bearing - /// rather than as a declaration-time conflict. On a string drawn from a - /// value set () there is nothing to redraw: the excluded values are removed from - /// the set at once, and removing all of them is a conflict here and now. The empty string is a valid value to - /// exclude; a null element is not. - /// - /// The values the generated string must differ from; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - /// Thrown when a value set is in force and the exclusion leaves none of its values. - public AnyString Except(params string[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element.", nameof(values)); } - - return new AnyString(_source, _spec.WithExcluded(values, ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the generated string to differ from — typically an existing value the test - /// already holds, to exercise an inequality path while preserving the declared shape. Semantically equivalent to - /// , including when a value set is in force; the name carries the intent at the - /// call site. - /// - /// The value the generated string must differ from. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when a value set is in force and is the only value it leaves. - public AnyString DifferentFrom(string value) { - if (value is null) { throw new ArgumentNullException(nameof(value)); } - - return new AnyString(_source, _spec.WithExcluded([value], ConstraintCall.Of(nameof(DifferentFrom), Q(value)))); - } - - /// - /// Draws the string from an explicit, fixed set of instead of shaping one — the - /// dummy for a value whose domain is a closed list the test does not assert on (a currency code, a well-known - /// name). Declared once per generator, and composable like every other family's OneOf: the other - /// constraints keep their meaning and narrow the set rather than shaping a string, so - /// OneOf("abc", "de").WithLength(3) yields "abc". A constraint no supplied value satisfies is a - /// naming both sides, whichever order the two were declared - /// in. Duplicate values are collapsed; the generated string is one of the surviving values, drawn uniformly - /// and reproducibly under a seed. - /// - /// - /// Declare it first when the values are the point. Constraints that contradict each other on their own - /// terms are still refused the moment they are declared — the generator cannot know a value set is coming, and - /// deferring that refusal would cost every shaped string its eager conflict. So - /// OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba") yields "aba", while the same - /// constraints with OneOf last conflict on the layout budget before the values are ever seen: laid out - /// side by side those two fragments need four characters, even though the supplied value contains both in - /// three. - /// - /// The values the generated string is drawn from; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString OneOf(params string[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element; use OrNull() to make the whole generator nullable.", nameof(values)); } - - return new AnyString(_source, _spec.WithAllowed(values, ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// - /// Draws the string from an explicit, fixed set of — the - /// counterpart of , for a set already held as a - /// sequence (a list, a LINQ result, values loaded at test setup). Same contract: the set composes with the - /// other constraints, duplicates collapse, and the draw is uniform and reproducible under a seed. - /// - /// The values the generated string is drawn from; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty or contains a null element. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyString OneOf(IEnumerable values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - - return OneOf(values as string[] ?? values.ToArray()); - } - - /// - public string Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyTimeOnly.cs b/JustDummies/AnyTimeOnly.cs deleted file mode 100644 index 8e45f53e..00000000 --- a/JustDummies/AnyTimeOnly.cs +++ /dev/null @@ -1,172 +0,0 @@ -#if NET8_0_OR_GREATER -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// Available on the net8.0 target only, like the type itself. There is deliberately no clock-relative -/// constraint: a reproducible test pins its reference time of days explicitly with and -/// . -/// -public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyTimeOnly Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyTimeOnly(source, OrdinalIntervalSpec.Unconstrained("TimeOnly", ordinal => V(Val(ordinal)), Ord(TimeOnly.MinValue), Ord(TimeOnly.MaxValue))); - } - - private static ulong Ord(TimeOnly value) { - return (ulong)value.Ticks; - } - - private static TimeOnly Val(ulong ordinal) { - return new TimeOnly((long)ordinal); - } - - private static string V(TimeOnly value) { - return value.ToString("O", CultureInfo.InvariantCulture); - } - - private static string Join(TimeOnly[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyTimeOnly(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(TimeOnly value) => _spec.Contains(Ord(value)); - - /// Requires a time of day strictly after . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly After(TimeOnly time) { - return new AnyTimeOnly(_source, _spec.WithMinimumAbove(Ord(time), ConstraintCall.Of(nameof(After), V(time)))); - } - - /// Requires a time of day at or after . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly AfterOrEqualTo(TimeOnly time) { - return new AnyTimeOnly(_source, _spec.WithMinimum(Ord(time), ConstraintCall.Of(nameof(AfterOrEqualTo), V(time)))); - } - - /// Requires a time of day strictly before . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly Before(TimeOnly time) { - return new AnyTimeOnly(_source, _spec.WithMaximumBelow(Ord(time), ConstraintCall.Of(nameof(Before), V(time)))); - } - - /// Requires a time of day at or before . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly BeforeOrEqualTo(TimeOnly time) { - return new AnyTimeOnly(_source, _spec.WithMaximum(Ord(time), ConstraintCall.Of(nameof(BeforeOrEqualTo), V(time)))); - } - - /// Requires a time of day within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is after . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly Between(TimeOnly start, TimeOnly end) { - if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(start), V(end)); - - return new AnyTimeOnly(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint)); - } - - /// - /// Requires the time of day to fall on a lattice of from - /// — a round time of day (a whole second, a quarter-hour), built on the grid - /// rather than snapped after the fact, so tick-precision values never surprise a serialization round-trip. - /// Declared once per generator. - /// - /// The lattice step; must be strictly positive. A granularity of one tick adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly WithGranularity(TimeSpan granularity) { - if (granularity <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(granularity), granularity, "The granularity must be strictly positive."); } - - string rendered = granularity.ToString("c", CultureInfo.InvariantCulture); - - return new AnyTimeOnly(_source, _spec.WithStep((ulong)granularity.Ticks, Ord(TimeOnly.MinValue), ConstraintCall.Of(nameof(WithGranularity), rendered))); - } - - /// Requires the time of day to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly OneOf(params TimeOnly[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyTimeOnly(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the time of day to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly Except(params TimeOnly[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyTimeOnly(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the time of day to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated time of day must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeOnly DifferentFrom(TimeOnly value) { - return new AnyTimeOnly(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public TimeOnly Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} -#endif diff --git a/JustDummies/AnyTimeSpan.cs b/JustDummies/AnyTimeSpan.cs deleted file mode 100644 index e3c98df3..00000000 --- a/JustDummies/AnyTimeSpan.cs +++ /dev/null @@ -1,195 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as -/// : constraints express what the surrounding code requires of the value, never what the -/// test asserts; contradictory constraints fail eagerly with a -/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one -/// draw. Unconstrained, it draws from the full range, negative durations included. -/// -public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyTimeSpan Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyTimeSpan(source, OrdinalIntervalSpec.Unconstrained("TimeSpan", ordinal => V(Val(ordinal)), Ord(TimeSpan.MinValue), Ord(TimeSpan.MaxValue))); - } - - private static ulong Ord(TimeSpan value) { - return OrdinalMapping.FromInt64(value.Ticks); - } - - private static TimeSpan Val(ulong ordinal) { - return new TimeSpan(OrdinalMapping.ToInt64(ordinal)); - } - - private static string V(TimeSpan value) { - return value.ToString("c", CultureInfo.InvariantCulture); - } - - private static string Join(TimeSpan[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyTimeSpan(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(TimeSpan value) => _spec.Contains(Ord(value)); - - /// Requires a duration strictly greater than . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan Positive() { - return new AnyTimeSpan(_source, _spec.WithMinimumAbove(Ord(TimeSpan.Zero), ConstraintCall.Of(nameof(Positive)))); - } - - /// Requires a duration strictly less than . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan Negative() { - return new AnyTimeSpan(_source, _spec.WithMaximumBelow(Ord(TimeSpan.Zero), ConstraintCall.Of(nameof(Negative)))); - } - - /// Pins the duration to exactly . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan Zero() { - return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(TimeSpan.Zero), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(TimeSpan.Zero), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a duration different from . - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan NonZero() { - return new AnyTimeSpan(_source, _spec.WithExcluded([Ord(TimeSpan.Zero)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a duration strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan GreaterThan(TimeSpan value) { - return new AnyTimeSpan(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a duration greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan GreaterThanOrEqualTo(TimeSpan value) { - return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a duration strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan LessThan(TimeSpan value) { - return new AnyTimeSpan(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a duration less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan LessThanOrEqualTo(TimeSpan value) { - return new AnyTimeSpan(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a duration within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan Between(TimeSpan minimum, TimeSpan maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the duration to fall on a lattice of from — - /// a whole number of that granularity, built on the grid rather than snapped after the fact, so tick-precision - /// values never surprise a serialization round-trip. Declared once per generator. - /// - /// The lattice step; must be strictly positive. A granularity of one tick adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan WithGranularity(TimeSpan granularity) { - if (granularity <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(granularity), granularity, "The granularity must be strictly positive."); } - - string rendered = granularity.ToString("c", CultureInfo.InvariantCulture); - - return new AnyTimeSpan(_source, _spec.WithStep((ulong)granularity.Ticks, Ord(TimeSpan.Zero), ConstraintCall.Of(nameof(WithGranularity), rendered))); - } - - /// Requires the duration to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan OneOf(params TimeSpan[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyTimeSpan(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the duration to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan Except(params TimeSpan[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyTimeSpan(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the duration to differ from — typically an existing value the test - /// already holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated duration must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyTimeSpan DifferentFrom(TimeSpan value) { - return new AnyTimeSpan(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public TimeSpan Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyUInt128.cs b/JustDummies/AnyUInt128.cs deleted file mode 100644 index d070dad9..00000000 --- a/JustDummies/AnyUInt128.cs +++ /dev/null @@ -1,181 +0,0 @@ -#if NET8_0_OR_GREATER -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// Available on the net8.0 target only, like the type itself. -/// -public sealed class AnyUInt128 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyUInt128 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyUInt128(source, WideIntervalSpec.Unconstrained("UInt128", ordinal => V(Val(ordinal)), Ord(UInt128.MinValue), Ord(UInt128.MaxValue))); - } - - private static UInt128 Ord(UInt128 value) { - return value; - } - - private static UInt128 Val(UInt128 ordinal) { - return ordinal; - } - - private static string V(UInt128 value) { - return value.ToString(null, CultureInfo.InvariantCulture); - } - - private static string Join(UInt128[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly WideIntervalSpec _spec; - - #endregion - - private AnyUInt128(RandomSource source, WideIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(UInt128 value) => _spec.Contains(Ord(value)); - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 Zero() { - return new AnyUInt128(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 NonZero() { - return new AnyUInt128(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 GreaterThan(UInt128 value) { - return new AnyUInt128(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 GreaterThanOrEqualTo(UInt128 value) { - return new AnyUInt128(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 LessThan(UInt128 value) { - return new AnyUInt128(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 LessThanOrEqualTo(UInt128 value) { - return new AnyUInt128(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 Between(UInt128 minimum, UInt128 maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyUInt128(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 MultipleOf(UInt128 value) { - if (value == UInt128.Zero) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyUInt128(_source, _spec.WithStep(value, Ord(UInt128.Zero), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 OneOf(params UInt128[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt128(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 Except(params UInt128[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt128(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt128 DifferentFrom(UInt128 value) { - return new AnyUInt128(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public UInt128 Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} -#endif diff --git a/JustDummies/AnyUInt16.cs b/JustDummies/AnyUInt16.cs deleted file mode 100644 index b7e34a83..00000000 --- a/JustDummies/AnyUInt16.cs +++ /dev/null @@ -1,178 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyUInt16 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyUInt16(source, OrdinalIntervalSpec.Unconstrained("UInt16", ordinal => V(Val(ordinal)), Ord(ushort.MinValue), Ord(ushort.MaxValue))); - } - - private static ulong Ord(ushort value) { - return value; - } - - private static ushort Val(ulong ordinal) { - return (ushort)ordinal; - } - - private static string V(ushort value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(ushort[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyUInt16(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(ushort value) => _spec.Contains(Ord(value)); - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 Zero() { - return new AnyUInt16(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 NonZero() { - return new AnyUInt16(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 GreaterThan(ushort value) { - return new AnyUInt16(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 GreaterThanOrEqualTo(ushort value) { - return new AnyUInt16(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 LessThan(ushort value) { - return new AnyUInt16(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 LessThanOrEqualTo(ushort value) { - return new AnyUInt16(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 Between(ushort minimum, ushort maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyUInt16(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 MultipleOf(ushort value) { - if (value == 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyUInt16(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 OneOf(params ushort[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt16(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 Except(params ushort[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt16(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt16 DifferentFrom(ushort value) { - return new AnyUInt16(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public ushort Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyUInt32.cs b/JustDummies/AnyUInt32.cs deleted file mode 100644 index 63ad3352..00000000 --- a/JustDummies/AnyUInt32.cs +++ /dev/null @@ -1,178 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyUInt32 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyUInt32(source, OrdinalIntervalSpec.Unconstrained("UInt32", ordinal => V(Val(ordinal)), Ord(uint.MinValue), Ord(uint.MaxValue))); - } - - private static ulong Ord(uint value) { - return value; - } - - private static uint Val(ulong ordinal) { - return (uint)ordinal; - } - - private static string V(uint value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(uint[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyUInt32(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(uint value) => _spec.Contains(Ord(value)); - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 Zero() { - return new AnyUInt32(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 NonZero() { - return new AnyUInt32(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 GreaterThan(uint value) { - return new AnyUInt32(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 GreaterThanOrEqualTo(uint value) { - return new AnyUInt32(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 LessThan(uint value) { - return new AnyUInt32(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 LessThanOrEqualTo(uint value) { - return new AnyUInt32(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 Between(uint minimum, uint maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyUInt32(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 MultipleOf(uint value) { - if (value == 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyUInt32(_source, _spec.WithStep((ulong)value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 OneOf(params uint[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt32(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 Except(params uint[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt32(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt32 DifferentFrom(uint value) { - return new AnyUInt32(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public uint Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyUInt64.cs b/JustDummies/AnyUInt64.cs deleted file mode 100644 index 3a28b7b0..00000000 --- a/JustDummies/AnyUInt64.cs +++ /dev/null @@ -1,178 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A fluent generator of arbitrary values — the same contract as : -/// constraints express what the surrounding code requires of the value, never what the test asserts; -/// contradictory constraints fail eagerly with a naming both -/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. -/// -public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint { - - #region Statics members declarations - - internal static AnyUInt64 Create(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - return new AnyUInt64(source, OrdinalIntervalSpec.Unconstrained("UInt64", ordinal => V(Val(ordinal)), Ord(ulong.MinValue), Ord(ulong.MaxValue))); - } - - private static ulong Ord(ulong value) { - return value; - } - - private static ulong Val(ulong ordinal) { - return ordinal; - } - - private static string V(ulong value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Join(ulong[] values) { - return string.Join(", ", values.Select(V)); - } - - #endregion - - #region Fields declarations - - private readonly RandomSource _source; - private readonly OrdinalIntervalSpec _spec; - - #endregion - - private AnyUInt64(RandomSource source, OrdinalIntervalSpec spec) { - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - - bool ICardinalityHint.Contains(ulong value) => _spec.Contains(Ord(value)); - - /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 Zero() { - return new AnyUInt64(_source, _spec.WithMinimum(Ord(0), ConstraintCall.Of(nameof(Zero))).WithMaximum(Ord(0), ConstraintCall.Of(nameof(Zero)))); - } - - /// Requires a value different from zero. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 NonZero() { - return new AnyUInt64(_source, _spec.WithExcluded([Ord(0)], ConstraintCall.Of(nameof(NonZero)))); - } - - /// Requires a value strictly greater than . - /// The exclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 GreaterThan(ulong value) { - return new AnyUInt64(_source, _spec.WithMinimumAbove(Ord(value), ConstraintCall.Of(nameof(GreaterThan), V(value)))); - } - - /// Requires a value greater than or equal to . - /// The inclusive lower bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 GreaterThanOrEqualTo(ulong value) { - return new AnyUInt64(_source, _spec.WithMinimum(Ord(value), ConstraintCall.Of(nameof(GreaterThanOrEqualTo), V(value)))); - } - - /// Requires a value strictly less than . - /// The exclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 LessThan(ulong value) { - return new AnyUInt64(_source, _spec.WithMaximumBelow(Ord(value), ConstraintCall.Of(nameof(LessThan), V(value)))); - } - - /// Requires a value less than or equal to . - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 LessThanOrEqualTo(ulong value) { - return new AnyUInt64(_source, _spec.WithMaximum(Ord(value), ConstraintCall.Of(nameof(LessThanOrEqualTo), V(value)))); - } - - /// Requires a value within the inclusive range [, ]. - /// The inclusive lower bound. - /// The inclusive upper bound. - /// A new generator carrying the added constraint. - /// Thrown when is greater than . - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 Between(ulong minimum, ulong maximum) { - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(Between), V(minimum), V(maximum)); - - return new AnyUInt64(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); - } - - /// - /// Requires the value to be a multiple of — drawn directly on that lattice, so the - /// declared range keeps its meaning (unlike a post-hoc As(x => x * k) projection). Declared once per - /// generator. - /// - /// The lattice step; must be strictly positive. A value of 1 adds no constraint. - /// A new generator carrying the added constraint. - /// Thrown when is not strictly positive. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 MultipleOf(ulong value) { - if (value == 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "The multiple must be strictly positive."); } - - return new AnyUInt64(_source, _spec.WithStep(value, Ord(0), ConstraintCall.Of(nameof(MultipleOf), V(value)))); - } - - /// Requires the value to be one of the supplied values. Declared once per generator. - /// The allowed values; duplicates are ignored. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 OneOf(params ulong[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt64(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(OneOf), Join(values)))); - } - - /// Requires the value to be none of the supplied values. - /// The forbidden values. - /// A new generator carrying the added constraint. - /// Thrown when is null. - /// Thrown when is empty. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 Except(params ulong[] values) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - - return new AnyUInt64(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), ConstraintCall.Of(nameof(Except), Join(values)))); - } - - /// - /// Requires the value to differ from — typically an existing value the test already - /// holds. Semantically equivalent to ; the name carries the intent at the call site. - /// - /// The value the generated value must differ from. - /// A new generator carrying the added constraint. - /// Thrown when the constraint contradicts a constraint already declared. - public AnyUInt64 DifferentFrom(ulong value) { - return new AnyUInt64(_source, _spec.WithExcluded([Ord(value)], ConstraintCall.Of(nameof(DifferentFrom), V(value)))); - } - - /// - public ulong Generate() { - return Val(_spec.GenerateOrdinal(_source.Current)); - } - -} diff --git a/JustDummies/AnyUri.cs b/JustDummies/AnyUri.cs deleted file mode 100644 index ab576cc6..00000000 --- a/JustDummies/AnyUri.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace JustDummies; - -/// -/// A fluent generator of arbitrary yet valid values. Unconstrained, it spans the whole -/// safe URI space — an absolute web (http/https), WebSocket (ws/wss), FTP or mailto -/// URI, or a relative reference — drawn from the ambient random context. Narrow it to one family to reach -/// that family's constraints: each narrowing returns a family-specific builder that exposes only the components -/// that family actually has, so an impossible combination (a port on a mailto, a fragment on a WebSocket) cannot -/// even be written. -/// -/// -/// -/// Every component is drawn from ASCII-unreserved characters and the URI is assembled directly, so a value is -/// valid by construction — never generated then filtered — and a run is reproducible under a seed on every -/// target framework. Internationalized (IDN) hosts and the file scheme are deliberately outside the -/// unconstrained draw: an IDN host and a file path do not round-trip identically across frameworks, which -/// would break the determinism contract. -/// -/// -/// -/// Uri any = Any.Uri().Generate(); // any family, absolute or relative -/// Uri endpoint = Any.Uri().Web().UsingHttps().WithHost("api.example.com").Generate(); -/// Uri socket = Any.Uri().WebSocket().Generate(); // ws:// or wss:// -/// Uri relative = Any.Uri().Relative().Rooted().Generate(); // /a/b/c -/// -/// -/// -public sealed class AnyUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Narrows to a web URI: http or https, with the full authority surface. - /// A web-URI generator. - public AnyWebUri Web() { - return new AnyWebUri(_source, _spec.WithFamily(UriFamily.Web)); - } - - /// Narrows to a WebSocket URI: ws or wss (no user-info, no fragment — RFC 6455). - /// A WebSocket-URI generator. - public AnyWebSocketUri WebSocket() { - return new AnyWebSocketUri(_source, _spec.WithFamily(UriFamily.WebSocket)); - } - - /// Narrows to an ftp URI (authority with user-info, no query or fragment). - /// An FTP-URI generator. - public AnyFtpUri Ftp() { - return new AnyFtpUri(_source, _spec.WithFamily(UriFamily.Ftp)); - } - - /// Narrows to a mailto URI: local@domain, optionally with headers. - /// A mailto-URI generator. - public AnyMailtoUri Mailto() { - return new AnyMailtoUri(_source, _spec.WithFamily(UriFamily.Mailto)); - } - - /// Narrows to a relative reference: a path with an optional query and fragment, no scheme or authority. - /// A relative-URI generator. - public AnyRelativeUri Relative() { - return new AnyRelativeUri(_source, _spec.WithFamily(UriFamily.Relative)); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyWebSocketUri.cs b/JustDummies/AnyWebSocketUri.cs deleted file mode 100644 index 3654bca7..00000000 --- a/JustDummies/AnyWebSocketUri.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace JustDummies; - -/// -/// A generator of arbitrary ws/wss URIs. Per RFC 6455 a WebSocket URI carries no user-info and -/// no fragment, so — unlike — this builder does not expose them. Pin the TLS variant -/// with /; unpinned, the scheme is drawn from both. -/// -public sealed class AnyWebSocketUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyWebSocketUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Pins the scheme to ws. Declared once per generator. - public AnyWebSocketUri UsingWs() { - return new AnyWebSocketUri(_source, _spec.WithScheme("ws", ConstraintCall.Of(nameof(UsingWs)))); - } - - /// Pins the scheme to wss. Declared once per generator. - public AnyWebSocketUri UsingWss() { - return new AnyWebSocketUri(_source, _spec.WithScheme("wss", ConstraintCall.Of(nameof(UsingWss)))); - } - - /// Pins the host. Must be an ASCII host name (pass the punycode form for internationalized hosts). - /// Thrown when is null. - /// Thrown when is empty, non-ASCII or not a valid host name. - public AnyWebSocketUri WithHost(string host) { - return new AnyWebSocketUri(_source, _spec.WithHost(UriSpec.RequireHost(host, nameof(host)), UriSpec.Label(nameof(WithHost), host))); - } - - /// Includes an arbitrary non-default port. - public AnyWebSocketUri WithPort() { - return new AnyWebSocketUri(_source, _spec.WithPort(null, UriSpec.Label(nameof(WithPort)))); - } - - /// Includes the given . - /// Thrown when is outside 1..65535. - public AnyWebSocketUri WithPort(int port) { - return new AnyWebSocketUri(_source, _spec.WithPort(UriSpec.RequirePort(port, nameof(port)), UriSpec.Label(nameof(WithPort), port))); - } - - /// Fixes the path to exactly segments. Declared once per generator. - /// Thrown when is negative. - /// Thrown when a path constraint is already declared. - public AnyWebSocketUri WithPathSegments(int count) { - return new AnyWebSocketUri(_source, _spec.WithPath(UriPathMode.Exact, UriSpec.RequireSegmentCount(count, nameof(count)), UriSpec.Label(nameof(WithPathSegments), count))); - } - - /// Renders the root path (/) with no segments. Declared once per generator. - /// Thrown when a path constraint is already declared. - public AnyWebSocketUri WithoutPath() { - return new AnyWebSocketUri(_source, _spec.WithPath(UriPathMode.Root, 0, ConstraintCall.Of(nameof(WithoutPath)))); - } - - /// Includes an arbitrary query string. - public AnyWebSocketUri WithQuery() { - return new AnyWebSocketUri(_source, _spec.WithQuery()); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/AnyWebUri.cs b/JustDummies/AnyWebUri.cs deleted file mode 100644 index 1aa748d6..00000000 --- a/JustDummies/AnyWebUri.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace JustDummies; - -/// -/// A generator of arbitrary http/https URIs. Exposes the full authority surface — user-info, host, -/// port, path, query and fragment. Pin the TLS variant with /; -/// unpinned, the scheme is drawn from both. Every component is drawn from ASCII-unreserved characters, so a value -/// is valid by construction and reproducible under a seed. -/// -public sealed class AnyWebUri : IAny, IHasRandomSource { - - #region Fields declarations - - private readonly RandomSource _source; - private readonly UriSpec _spec; - - #endregion - - internal AnyWebUri(RandomSource source, UriSpec spec) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (spec is null) { throw new ArgumentNullException(nameof(spec)); } - _source = source; - _spec = spec; - } - - RandomSource? IHasRandomSource.Source => _source; - - /// Pins the scheme to http. Declared once per generator. - public AnyWebUri UsingHttp() { - return new AnyWebUri(_source, _spec.WithScheme("http", ConstraintCall.Of(nameof(UsingHttp)))); - } - - /// Pins the scheme to https. Declared once per generator. - public AnyWebUri UsingHttps() { - return new AnyWebUri(_source, _spec.WithScheme("https", ConstraintCall.Of(nameof(UsingHttps)))); - } - - /// Pins the host. Must be an ASCII host name (pass the punycode form for internationalized hosts). - /// Thrown when is null. - /// Thrown when is empty, non-ASCII or not a valid host name. - public AnyWebUri WithHost(string host) { - return new AnyWebUri(_source, _spec.WithHost(UriSpec.RequireHost(host, nameof(host)), UriSpec.Label(nameof(WithHost), host))); - } - - /// Includes arbitrary user:password user-info. - public AnyWebUri WithUserInfo() { - return new AnyWebUri(_source, _spec.WithUserInfo(null, null, UriSpec.Label(nameof(WithUserInfo)))); - } - - /// Includes user-info with the given and an arbitrary password. - /// Thrown when is null. - /// Thrown when contains a non-unreserved character. - public AnyWebUri WithUserInfo(string user) { - return new AnyWebUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), null, UriSpec.Label(nameof(WithUserInfo), user))); - } - - /// Includes the given and user-info. - /// Thrown when an argument is null. - /// Thrown when an argument contains a non-unreserved character. - public AnyWebUri WithUserInfo(string user, string password) { - return new AnyWebUri(_source, _spec.WithUserInfo(UriSpec.RequireUserInfoPart(user, nameof(user)), UriSpec.RequireUserInfoPart(password, nameof(password)), UriSpec.Label(nameof(WithUserInfo), user, password))); - } - - /// Includes an arbitrary non-default port. - public AnyWebUri WithPort() { - return new AnyWebUri(_source, _spec.WithPort(null, UriSpec.Label(nameof(WithPort)))); - } - - /// Includes the given . - /// Thrown when is outside 1..65535. - public AnyWebUri WithPort(int port) { - return new AnyWebUri(_source, _spec.WithPort(UriSpec.RequirePort(port, nameof(port)), UriSpec.Label(nameof(WithPort), port))); - } - - /// Fixes the path to exactly segments. Declared once per generator. - /// Thrown when is negative. - /// Thrown when a path constraint is already declared. - public AnyWebUri WithPathSegments(int count) { - return new AnyWebUri(_source, _spec.WithPath(UriPathMode.Exact, UriSpec.RequireSegmentCount(count, nameof(count)), UriSpec.Label(nameof(WithPathSegments), count))); - } - - /// Renders the root path (/) with no segments. Declared once per generator. - /// Thrown when a path constraint is already declared. - public AnyWebUri WithoutPath() { - return new AnyWebUri(_source, _spec.WithPath(UriPathMode.Root, 0, ConstraintCall.Of(nameof(WithoutPath)))); - } - - /// Includes an arbitrary query string. - public AnyWebUri WithQuery() { - return new AnyWebUri(_source, _spec.WithQuery()); - } - - /// Includes an arbitrary fragment. - public AnyWebUri WithFragment() { - return new AnyWebUri(_source, _spec.WithFragment()); - } - - /// - public Uri Generate() { - return _spec.Generate(_source); - } - -} diff --git a/JustDummies/BuiltOnTheFailurePathAttribute.cs b/JustDummies/BuiltOnTheFailurePathAttribute.cs deleted file mode 100644 index c368732d..00000000 --- a/JustDummies/BuiltOnTheFailurePathAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace JustDummies; - -/// -/// Marks a type that exists only to build one of this library's exceptions, and is therefore constructed while a -/// failure is being reported. -/// -/// -/// Such a type is exempt from the null-argument guard convention, for the same reason exception types themselves -/// are: a guard on this path throws while a failure is being reported, replacing that failure with a failure about -/// reporting it and losing the original. The exemption is declared here rather than inferred, so it applies only -/// where someone has said it should — the marker is the decision, and -/// NullArgumentGuardConventionTests reads it (ADR-0064, which widened ADR-0045's exemption from exception -/// types to this path). -/// -/// Nothing is given up by it: every argument on this path is non-nullable, so a caller that cannot prove a -/// value is CS8604 at build time. The contract moves from a runtime guard to the compiler, which is -/// where it belongs for a path that must never throw. -/// -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -internal sealed class BuiltOnTheFailurePathAttribute : Attribute { } diff --git a/JustDummies/CHANGELOG.md b/JustDummies/CHANGELOG.md deleted file mode 100644 index 849dce7a..00000000 --- a/JustDummies/CHANGELOG.md +++ /dev/null @@ -1,12 +0,0 @@ -# Changelog - -All notable, user-facing changes to **JustDummies** are documented here. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Releases are cut from the `dum` train (see [CONTRIBUTING.md](../CONTRIBUTING.md)). - -## [Unreleased] - -_No unreleased changes recorded yet. This section is drafted automatically from -merged pull requests — see [`.github/workflows/changelog.yml`](../.github/workflows/changelog.yml)._ diff --git a/JustDummies/CharacterPools.cs b/JustDummies/CharacterPools.cs deleted file mode 100644 index 15b9e945..00000000 --- a/JustDummies/CharacterPools.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace JustDummies; - -/// The character families a string or char generator can be restricted to. -internal enum CharacterSet { - - Alpha, - Numeric, - AlphaNumeric - -} - -/// The casing a string or char generator can impose on alphabetic characters. -internal enum LetterCasing { - - Lower, - Upper - -} - -/// -/// The ASCII pools and classification helpers shared by 's filler and -/// — one definition of "letters and digits", so the two generators can never drift -/// apart on what their default characters are. -/// -internal static class CharacterPools { - - internal const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - internal const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; - internal const string Digits = "0123456789"; - - internal static bool IsAsciiLetter(char character) { - return character is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; - } - - internal static bool IsAsciiDigit(char character) { - return character is >= '0' and <= '9'; - } - -} diff --git a/JustDummies/CollectionState.cs b/JustDummies/CollectionState.cs deleted file mode 100644 index 9380a807..00000000 --- a/JustDummies/CollectionState.cs +++ /dev/null @@ -1,302 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// The immutable specification behind every collection generator (, -/// , , and, for its keys, -/// ): the element generator, a shared , whether -/// the collection must be distinct, an optional equality comparer, and the values it must contain. Every mutation -/// returns a new state and validates the whole eagerly — so a collection generator that exists can always -/// produce, laid out directly rather than generated-then-filtered. -/// -/// -/// Distinctness follows the two-layer contract the library commits to: when the element generator advertises a -/// small cardinality that the requested count would exceed — counting only -/// the elements that must be drawn from that generator, since values pinned with Containing(...) outside -/// its domain (see ) are supplied directly and extend the effective domain — -/// the conflict is caught at declaration time (); otherwise the -/// count is drawn and the elements are filled by a bounded dedup-draw, and a genuine shortfall surfaces at -/// generation as an naming the seed to replay. -/// -/// The element type. -internal sealed class CollectionState { - - // The three numbers the exhaustion budget is built from. They bound how long a dedup-draw may keep colliding - // before it reports a shortfall, and nothing outside ExhaustionBudget reads them. - - /// How many consecutive collisions each value of a known finite domain is allowed to cost. - private const long CollisionsPerValue = 64L; - - /// The cardinality up to which the budget scales with the domain rather than with the requested count. - private const long ScalableCardinality = 1_000_000L; - - /// The floor the budget never drops below, whatever the domain and the count work out to. - private const long MinimumBudget = 10_000L; - - #region Statics members declarations - - internal static CollectionState Create(IAny item, bool distinct, IEqualityComparer? comparer) { - if (item is null) { throw new ArgumentNullException(nameof(item)); } - - return new CollectionState(item, AnyDerivation.CardinalityOf(item), CountSpec.Unconstrained, distinct, comparer, - Array.Empty(), Array.Empty>()); - } - - private static string Elements(int count) { - return count == 1 ? "1 element" : $"{count.ToString(CultureInfo.InvariantCulture)} elements"; - } - - private static IReadOnlyList Append(IReadOnlyList list, TItem value) { - List copy = [.. list, value]; - - return copy; - } - - private static void Shuffle(List items, SeededRandom random) { - // Fisher-Yates: contained values and filler are appended in a fixed order, so a shuffle keeps a dummy - // collection from advertising a positional invariant a caller might accidentally rely on. - for (int index = items.Count - 1; index > 0; index--) { - int swap = random.Next(index + 1); - (items[index], items[swap]) = (items[swap], items[index]); - } - } - - #endregion - - #region Fields declarations - - private readonly IEqualityComparer? _comparer; - private readonly CountSpec _count; - private readonly bool _distinct; - private readonly IReadOnlyList _fixedContaining; - private readonly IReadOnlyList> _generatedContaining; - private readonly IAny _item; - private readonly long? _itemCardinality; - - #endregion - - private CollectionState(IAny item, long? itemCardinality, CountSpec count, bool distinct, - IEqualityComparer? comparer, - IReadOnlyList fixedContaining, IReadOnlyList> generatedContaining) { - _item = item; - _itemCardinality = itemCardinality; - _count = count; - _distinct = distinct; - _comparer = comparer; - _fixedContaining = fixedContaining; - _generatedContaining = generatedContaining; - } - - /// The equality comparer distinct collections deduplicate with, or null for the default. - internal IEqualityComparer? Comparer => _comparer; - - /// Fixes the exact element count. - internal CollectionState WithExactCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return Rebuild(_count.WithExactCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); - } - - /// Tightens the minimum element count. - internal CollectionState WithMinCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return Rebuild(_count.WithMinCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); - } - - /// Tightens the maximum element count. - internal CollectionState WithMaxCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return Rebuild(_count.WithMaxCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); - } - - /// Requires the elements to be pairwise distinct, optionally under . - internal CollectionState AsDistinct(IEqualityComparer? comparer, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // One collection is distinct under ONE equality, so a second, different comparer cannot be honoured - // alongside the first: keeping the last one silently would drop a constraint the caller wrote. Declaring - // the same comparer again, or re-declaring distinctness without one, asks for the equality already in - // force and stays a no-op. - if (comparer is not null && _comparer is not null && !ReferenceEquals(comparer, _comparer)) { - throw ConflictingAnyConstraintException.ComparerAlreadyDefined(applying); - } - - return Rebuild(_count, true, comparer ?? _comparer, _fixedContaining, _generatedContaining, applying); - } - - /// Requires the collection to contain . - internal CollectionState WithContaining(T value, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return Rebuild(_count, _distinct, _comparer, Append(_fixedContaining, value), _generatedContaining, applying); - } - - /// Requires the collection to contain a value drawn from . - internal CollectionState WithContaining(IAny generator, ConstraintCall applying) { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return Rebuild(_count, _distinct, _comparer, _fixedContaining, Append(_generatedContaining, generator), applying); - } - - /// Builds one collection satisfying the whole specification — laid out directly, never generate-then-retry. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The condition reads the collection the body mutates. Where is lazily evaluated, so lifting the filter out would run each " + - "predicate against a snapshot taken before the additions it is meant to see, and let duplicates through.")] - internal List Materialize(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - SeededRandom random = source.Current; - int required = _fixedContaining.Count + _generatedContaining.Count; - int? cap = _distinct ? CardinalityCap() : null; - int count = _count.Resolve(random, required, cap); - - if (!_distinct) { - List items = new(Math.Max(count, required)); - items.AddRange(_fixedContaining); - foreach (IAny generator in _generatedContaining) { items.Add(generator.Generate()); } - while (items.Count < count) { items.Add(_item.Generate()); } - Shuffle(items, random); - - return items; - } - - HashSet seen = new(_comparer ?? EqualityComparer.Default); - List ordered = new(count); - foreach (T value in _fixedContaining) { - if (seen.Add(value)) { ordered.Add(value); } - } - foreach (IAny generator in _generatedContaining) { - T value = DrawFresh(generator, seen, source, count); - seen.Add(value); - ordered.Add(value); - } - FillDistinct(ordered, seen, source, count); - Shuffle(ordered, random); - - return ordered; - } - - private CollectionState Rebuild(CountSpec count, bool distinct, IEqualityComparer? comparer, - IReadOnlyList fixedContaining, IReadOnlyList> generatedContaining, ConstraintCall applying) { - CollectionState candidate = new(_item, _itemCardinality, count, distinct, comparer, fixedContaining, generatedContaining); - candidate.Validate(applying); - - return candidate; - } - - private void Validate(ConstraintCall applying) { - int required = _fixedContaining.Count + _generatedContaining.Count; - _count.EnsureFits(required, applying); - - if (!_distinct) { return; } - - IEqualityComparer comparer = _comparer ?? EqualityComparer.Default; - for (int left = 0; left < _fixedContaining.Count; left++) { - for (int right = left + 1; right < _fixedContaining.Count; right++) { - if (comparer.Equals(_fixedContaining[left], _fixedContaining[right])) { - throw ConflictingAnyConstraintException.DuplicateInDistinctCollection(applying, AnyDerivation.Display(_fixedContaining[left])); - } - } - } - - if (_itemCardinality is long cardinality) { - // Only the elements that must be drawn from the generator count against its cardinality: values pinned - // outside its domain occupy their own slots, and each opaque ContainingAny draw is credited as if it - // could land outside too (conservative — an unprovable overlap defers to the bounded draw rather than - // a false conflict). The subtractive form keeps the left side within int, so a near-long.MaxValue - // domain never overflows the comparison. - int need = Math.Max(_count.Floor, required); - int fromGenerator = need - FixedOutsideCount() - _generatedContaining.Count; - if (fromGenerator > cardinality) { - throw ConflictingAnyConstraintException.DistinctElementsExceedCardinality(applying, Elements(fromGenerator), cardinality.ToString(CultureInfo.InvariantCulture)); - } - } - } - - private int? CardinalityCap() { - // The effective ceiling is the generator's own cardinality plus the values pinned outside its domain, which - // fill their own slots without drawing on it — so a distinct collection over a small domain can still reach - // the size those extra values allow. Guard the cast: a domain wider than int cannot cap an int count anyway, - // and once the generator's cardinality is int-bounded the add stays within long. - if (_itemCardinality is not long cardinality || cardinality > int.MaxValue) { return null; } - - long effective = cardinality + FixedOutsideCount(); - - return effective <= int.MaxValue ? (int)effective : null; - } - - private int FixedOutsideCount() { - if (_fixedContaining.Count == 0) { return 0; } - // A fixed value the element generator could never produce extends the effective distinct domain; a value - // already inside it does not. The cardinality snapshot came from this same generator, so whenever the eager - // check runs it also answers membership (cardinality and membership are one interface). Both other branches - // are the same defensive fallback: treat every fixed value as outside, so the check can only defer to the - // bounded draw, never falsely reject. - // - // A custom comparer takes that fallback because the hint answers membership under the DEFAULT comparer, and a - // custom one can be stricter — reference equality over a type with value equality is the plain case. Two - // values the hint reports as one are then two values this collection keeps apart, so consulting it would call - // a pinned value already-inside when it genuinely extends the domain, and refuse a specification the - // collection can satisfy. The cardinality itself survives a comparer (a pool of n values is at most n - // distinct under any of them, so the bound only ever over-states); membership does not. - if (_comparer is not null || _item is not ICardinalityHint hint) { return _fixedContaining.Count; } - - return _fixedContaining.Count(value => !hint.Contains(value)); - } - - private T DrawFresh(IAny generator, HashSet seen, RandomSource source, int target) { - int budget = ExhaustionBudget(target); - for (int collisions = 0;;) { - T value = generator.Generate(); - if (!seen.Contains(value)) { return value; } - if (++collisions > budget) { throw Exhausted(source, seen.Count, target, "a ContainingAny(...) generator", generator); } - } - } - - private void FillDistinct(List ordered, HashSet seen, RandomSource source, int target) { - int budget = ExhaustionBudget(target); - int collisions = 0; - while (ordered.Count < target) { - T value = _item.Generate(); - if (seen.Add(value)) { - ordered.Add(value); - collisions = 0; - } else if (++collisions > budget) { - throw Exhausted(source, ordered.Count, target, "the element generator", _item); - } - } - } - - private int ExhaustionBudget(int target) { - // A known finite domain gets a coupon-collector-generous ceiling; an unknown or huge one gets a fixed - // floor that collisions only reach if the domain is unexpectedly small (for example a comparer that - // merges most values). Either way the fill is bounded — never an unbounded retry loop. - long cardinality = _itemCardinality ?? long.MaxValue; - long bounded = cardinality <= ScalableCardinality ? CollisionsPerValue * cardinality : CollisionsPerValue * target; - - return (int)Math.Min(Math.Max(bounded, MinimumBudget), int.MaxValue); - } - - private static AnyGenerationException Exhausted(RandomSource source, int reached, int target, string what, IAny culprit) { - // The reported seed reproduces the count and layout, but it reproduces the elements only when the failing - // generator's every draw follows that same source. A foreign IAny, a derivation built over one, or a Combine - // that mixes a foreign operand with a sourced one is not fully reproducible (AnyDerivation.IsReproducible is - // false) — even where it still carries a non-null source to name — so promising a full replay of its elements - // would be false: qualify the hint instead. - Replay replay = AnyDerivation.IsReproducible(culprit) - ? Replay.Of(source) - : Replay.PartialOf(source); - string message = $"Could not generate a distinct collection of {Elements(target)}: {what} produced only {reached} distinct value(s) before the draw budget was exhausted. Loosen the count or widen the element generator's domain. {replay.Guidance}"; - - return new AnyGenerationException(message, replay.Seed); - } - -} diff --git a/JustDummies/ConflictingAnyConstraintException.cs b/JustDummies/ConflictingAnyConstraintException.cs deleted file mode 100644 index e6dfdeba..00000000 --- a/JustDummies/ConflictingAnyConstraintException.cs +++ /dev/null @@ -1,182 +0,0 @@ -namespace JustDummies; - -/// -/// Thrown at the moment a constraint is declared when it cannot be satisfied together with the constraints -/// already declared on the same generator — for example -/// Any.String().WithLength(3).StartingWith("ORD-"), where the prefix alone already requires 4 -/// characters. Failing at declaration time, with a message that names both constraints, is a deliberate part of -/// the library's contract: a contradiction in a test's Arrange is a defect of the test, and it should -/// read as one — not surface later as a puzzling generation failure. -/// -public sealed class ConflictingAnyConstraintException : DummyException { - - #region Statics members declarations - - /// - /// Builds the exception for a constraint that no value of can satisfy — the - /// constraint is unsatisfiable on its own, before any other is considered. - /// - internal static ConflictingAnyConstraintException NoValueSatisfies(ConstraintCall applying, string typeName) { - return Sentence(applying, $"no {typeName} value satisfies it"); - } - - /// - /// Builds the exception for a constraint that leaves no value available once the constraints already declared - /// are taken together, naming what exhausted the domain. The counterpart of - /// : nothing survives the combination, rather than the constraint admitting - /// nothing by itself. - /// - internal static ConflictingAnyConstraintException NoValueRemains(ConstraintCall applying, string exhaustion) { - return Sentence(applying, exhaustion); - } - - /// - /// Builds the exception for a constraint that has already settled. - /// - internal static ConflictingAnyConstraintException AlreadyDefined(ConstraintCall applying, ConstraintCall existingConstraint) { - return Sentence(applying, $"{existingConstraint} is already defined"); - } - - /// - /// Builds the exception for two constraints that cannot hold together, blaming - /// — unless it is the one being applied, in which case - /// is blamed instead. - /// - /// - /// The choice is the whole point of this factory. A conflict always has two sides, and the message must name - /// the side the caller did NOT just write: telling someone that the constraint they are applying conflicts - /// with itself explains nothing. Every conflict between a fixed count or length and a bound is this shape, so - /// the rule is stated once here rather than re-derived at each throw site. - /// - internal static ConflictingAnyConstraintException Contradicts(ConstraintCall applying, ConstraintClaim culprit, ConstraintClaim otherwise) { - ConstraintClaim blamed = applying == culprit.Constraint ? otherwise : culprit; - - return Sentence(applying, blamed.ToString()); - } - - /// - /// Builds the exception for an allow-list none of whose values survives every constraint already declared, - /// naming what rejected them. The allow-list counterpart of - /// : the caller supplied the values, so the failure names what turned them all - /// away rather than what the domain could not produce. - /// - internal static ConflictingAnyConstraintException NoPooledValueSurvives(ConstraintCall applying, string exhaustion) { - return Sentence(applying, exhaustion); - } - - /// - /// Builds the exception for a constraint that contradicts a value already pinned. - /// - internal static ConflictingAnyConstraintException AlreadyPinned(ConstraintCall applying, ConstraintCall pinningConstraint, string value) { - return Sentence(applying, $"{pinningConstraint} already pins the value to {value}"); - } - - /// - /// Builds the exception for a pinned value the exclusions declared alongside it forbid. - /// - internal static ConflictingAnyConstraintException PinnedValueExcluded(ConstraintCall applying, ConstraintCall pinningConstraint, string value) { - return Sentence(applying, $"{pinningConstraint} already pins the value to {value}, which the exclusions forbid"); - } - - /// - /// Builds the exception for a pinned value the allow-list declared alongside it does not admit. - /// - internal static ConflictingAnyConstraintException PinnedValueNotAllowed(ConstraintCall applying, ConstraintCall pinningConstraint, string value, ConstraintCall allowingConstraint) { - return Sentence(applying, $"{pinningConstraint} already pins the value to {value}, which {allowingConstraint} does not allow"); - } - - /// - /// Builds the exception for combinations asked of an enum the runtime would not recognise them on. - /// - internal static ConflictingAnyConstraintException EnumIsNotFlags(ConstraintCall applying, string enumName) { - return Sentence(applying, $"{enumName} is not declared [Flags]: OR-ing its members would produce values the type does not define"); - } - - /// - /// Builds the exception for an enum with more combinable members than the library will enumerate. - /// - internal static ConflictingAnyConstraintException TooManyCombinableMembers(ConstraintCall applying, string enumName, string declared, string maximum) { - return Sentence(applying, $"{enumName} declares {declared} non-zero members, more than the {maximum} whose combinations can be enumerated. " + - "Draw from an explicit set with OneOf(...) instead"); - } - - /// - /// Builds the exception for elements required to be contained that cannot fit the capacity already declared. - /// - internal static ConflictingAnyConstraintException ContainedElementsDoNotFit(ConstraintCall applying, string required, string capacity) { - return Sentence(applying, $"{required} required to be contained cannot fit in a collection of at most {capacity}"); - } - - /// - /// Builds the exception for a second, different equality on a collection already required to be distinct. One - /// collection is distinct under one equality, so the two cannot both be honoured. - /// - internal static ConflictingAnyConstraintException ComparerAlreadyDefined(ConstraintCall applying) { - return Sentence(applying, $"a different comparer is already defined by an earlier {applying}"); - } - - /// - /// Builds the exception for a value required to be contained twice in a collection required to be distinct. - /// - internal static ConflictingAnyConstraintException DuplicateInDistinctCollection(ConstraintCall applying, string value) { - return Sentence(applying, $"a distinct collection cannot contain {value} more than once"); - } - - /// - /// Builds the exception for more distinct elements than the element generator has distinct values to give. - /// - internal static ConflictingAnyConstraintException DistinctElementsExceedCardinality(ConstraintCall applying, string required, string cardinality) { - return Sentence(applying, $"{required} required to be distinct exceed the {cardinality} distinct value(s) the element generator can produce"); - } - - /// - /// Builds the exception for a constraint that contradicts an upper bound already declared. - /// - internal static ConflictingAnyConstraintException AlreadyBoundedAbove(ConstraintCall applying, ConstraintCall existingConstraint, string bound) { - return Sentence(applying, $"{existingConstraint} already requires values less than or equal to {bound}"); - } - - /// - /// Builds the exception for a constraint that contradicts a lower bound already declared. - /// - internal static ConflictingAnyConstraintException AlreadyBoundedBelow(ConstraintCall applying, ConstraintCall existingConstraint, string bound) { - return Sentence(applying, $"{existingConstraint} already requires values greater than or equal to {bound}"); - } - - /// - /// Writes the conflict sentence, which every factory above funnels through so its shape exists in exactly one - /// place — it was written out at each throw site before, and had that many chances to drift. - /// - /// - /// Private on purpose. It names the grammar of the message, not a failure, so it is no one's factory: every - /// caller is a named case above, and a new case gets a name of its own rather than a free-form reason passed - /// through here. - /// - /// Nothing here guards its arguments, and that is the rule rather than an omission: building an exception - /// must never throw. A guard would replace the failure being reported with a failure about reporting it, - /// losing the original. ADR-0045 exempts exception types for exactly that reason, and the reflection - /// convention that enforces it skips them outright. The contract is the compiler's instead — these - /// parameters are non-nullable, so a caller that cannot prove a value is CS8604 at build time, which is - /// how the one nullable constraint name in the interval specs was found. - /// - /// - /// Interpolating a here calls its ToString, which reads back text - /// rendered when the constraint was declared rather than composing any. The rule above therefore holds - /// for the constraints too, by construction rather than by inspection. - /// - /// - /// The constraint being declared, as the caller spelled it. - /// Why it cannot be applied, written without a final period. - private static ConflictingAnyConstraintException Sentence(ConstraintCall applying, string reason) { - return new ConflictingAnyConstraintException($"Cannot apply {applying} because {reason}."); - } - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// A description naming the newly declared constraint and the declared constraint it conflicts with. - public ConflictingAnyConstraintException(string message) : base(message) { } - -} diff --git a/JustDummies/ConstraintCall.cs b/JustDummies/ConstraintCall.cs deleted file mode 100644 index a9aec59a..00000000 --- a/JustDummies/ConstraintCall.cs +++ /dev/null @@ -1,160 +0,0 @@ -#region Usings declarations - -using System.Diagnostics; - -#endregion - -namespace JustDummies; - -/// -/// A constraint as the caller spelled it — the declaring method's name and what it was given — rendered into the -/// form the diagnostics quote back: Zero(), Between(0, 100), OneOf(...). It is the unit a -/// conflict message names, so the punctuation that makes a constraint read as a call is written here once -/// instead of at every site that declares one. -/// -/// -/// -/// The two factories are the two things a generator can say about a constraint's arguments: it can render -/// them — none at all being the ordinary case of called with no argument — or it cannot, -/// and stands in for them. The second is a claim, not a shortcut: a pool of an -/// opaque T has arguments the library must not render, because their ToString belongs -/// to the caller and could be anything. -/// -/// -/// The rendering happens once, in the constructor, and only reads it back. That is -/// deliberate rather than an optimization: a constraint is quoted while a -/// is being built, and building an exception must never -/// throw (ADR-0045). Rendering when the constraint is declared — on the path that succeeds — leaves nothing -/// on the failing path that could fail in its turn. -/// -/// -/// Pass the name as nameof(...). It ties the message to the API it names, so renaming the method -/// carries its diagnostics along, and a misspelling stops being a string literal that compiles. -/// -/// -/// Two constraints are equal when they read the same, ordinally. That is not a convenience: a spec compares -/// the constraint being applied against the one it already recorded to tell a harmless redeclaration -/// (Between(0, 100) twice, which returns the spec untouched) from a real conflict -/// (Between(0, 100) then Between(5, 50)). == is defined for the same reason rather -/// than for symmetry — those comparisons are written with it, and a reference type without it would compare -/// identities in silence, turning every redeclaration into a conflict. -/// -/// -/// Nothing checks the rendered arguments for null, and that is the compiler's job rather than an -/// omission: the parameter is a non-nullable string[], so a caller that cannot prove an argument -/// non-null is CS8604 at build time, which this repository promotes to an error. A runtime guard would only -/// restate it, and could not be reached from C# without defeating the annotation it duplicates. -/// -/// -[DebuggerDisplay("{ToString()}")] -[ValueObject] -internal sealed class ConstraintCall : IEquatable { - - #region Statics members declarations - - /// - /// A constraint whose arguments are rendered, including the common case of a constraint that takes none — - /// which is this factory called with no (Zero(), Distinct()). - /// - /// - /// - /// takes rendered text — what the reader of a conflict message sees - /// between the parentheses — never a parameter name. Two shapes reach it. The ordinary one is a value the - /// declaring generator rendered itself (V(minimum), Join(values)), giving - /// Between(0, 100). The other is a word standing in for an argument that has no useful rendering, - /// written where it reads better than the ellipsis would give: - /// Distinct(comparer) for an equality, ContainingAny(<generator>) for a recipe. - /// - /// - /// Such a stand-in is passed as the literal it is, and nameof(...) does not belong here even when - /// the parameter it would name happens to spell it — Distinct(IEqualityComparer<T>) passes - /// "comparer", not nameof(comparer). The rule differs from 's on - /// purpose: a method name is a public symbol the message must follow through a rename, whereas a stand-in - /// is prose whose resemblance to a parameter is a coincidence of good naming. Tying it to the symbol would - /// let a rename local to one overload silently reword a user-facing message, and would leave the same - /// constraint reading differently across the generators declaring it as soon as two of them named their - /// parameter differently. <generator> is that same convention where no identifier could have - /// been mistaken for it. - /// - /// - /// The declaring method's name, passed as nameof(...). - /// The arguments, each already rendered by the declaring generator. - /// The constraint, rendered as name(argument, argument). - /// Thrown when or is null. - internal static ConstraintCall Of(string name, params string[] arguments) { - if (name is null) { throw new ArgumentNullException(nameof(name)); } - if (arguments is null) { throw new ArgumentNullException(nameof(arguments)); } - - return new ConstraintCall(name, string.Join(", ", arguments)); - } - - /// - /// A constraint carrying arguments the library cannot render, an ellipsis standing in for them — - /// OneOf(...), Except(...) over a pool whose element type is opaque to the library. - /// - /// The declaring method's name, passed as nameof(...). - /// The constraint, rendered as name(...). - /// Thrown when is null. - internal static ConstraintCall OfElided(string name) { - if (name is null) { throw new ArgumentNullException(nameof(name)); } - - return new ConstraintCall(name, "..."); - } - - #endregion - - /// - /// Determines whether two constraints read the same. - /// - /// The first constraint to compare. - /// The second constraint to compare. - /// true when both render the same text, or both are null; otherwise false. - public static bool operator ==(ConstraintCall? left, ConstraintCall? right) { - return Equals(left, right); - } - - /// - /// Determines whether two constraints read differently. - /// - /// The first constraint to compare. - /// The second constraint to compare. - /// true when they render different text, or exactly one is null; otherwise false. - public static bool operator !=(ConstraintCall? left, ConstraintCall? right) { - return !Equals(left, right); - } - - #region Fields declarations - - private readonly string _rendered; - - #endregion - - private ConstraintCall(string name, string arguments) { - _rendered = name + "(" + arguments + ")"; - } - - /// - /// Returns the constraint as the caller spelled it. Total by construction: the text was built when the - /// constraint was declared, so quoting one into a message cannot fail. - /// - /// The rendered constraint, such as Between(0, 100). - public override string ToString() { - return _rendered; - } - - /// - public bool Equals(ConstraintCall? other) { - return other is not null && string.Equals(_rendered, other._rendered, StringComparison.Ordinal); - } - - /// - public override bool Equals(object? obj) { - return obj is ConstraintCall other && Equals(other); - } - - /// - public override int GetHashCode() { - return StringComparer.Ordinal.GetHashCode(_rendered); - } - -} diff --git a/JustDummies/ConstraintClaim.cs b/JustDummies/ConstraintClaim.cs deleted file mode 100644 index 83b16990..00000000 --- a/JustDummies/ConstraintClaim.cs +++ /dev/null @@ -1,135 +0,0 @@ -#region Usings declarations - -using System.Diagnostics; - -#endregion - -namespace JustDummies; - -/// -/// A blamed subject and what it claims about the values it admits — WithLength(3) paired with "already -/// fixes the length at 3". The pair exists so a conflict can be reported against whichever of two subjects is -/// not the one being applied, without the reporting code taking four loose strings in an order nothing checks. -/// -/// -/// Immutable, and a class rather than a struct, like every value in this repository: a struct would expose a -/// parameterless constructor yielding a pair with no name and no claim. -/// -/// The subject is usually a constraint the caller wrote, and takes it as one. It is not -/// always: a shape's part can be blamed too — the contained value "ABC", the prefix "ORD-" — -/// and those are phrases the library composes, not calls anyone made. is that case, -/// named rather than smuggled through as a string, so the constraint slot keeps meaning a constraint. A -/// phrase carries no , which is what makes it never equal to the constraint being -/// applied — the comparison the blame choice turns on. -/// -/// -/// Two claims are equal when they blame the same subject for the same thing. Being a value with no identity -/// beyond what it holds, it says so rather than leaving the reference comparison a reader would get by -/// default — the same reason carries its own (ADR-0065). Nothing compares two -/// claims today; a value that answers the question wrongly the first time it is asked is worse than one that -/// answers it, so the answer is written now rather than when a caller needs it. -/// -/// -/// It carries no argument guard, and says so with : instances are -/// built at a throw site, as an argument to an exception factory, so a guard here would throw while a failure -/// is being reported and lose it (ADR-0064). The contract is the compiler's — the members are non-nullable -/// where a value is required, so a caller that cannot prove one is CS8604 at build time. Comparing and -/// hashing stay on that footing: neither composes anything, so neither can fail while a failure is reported. -/// -/// -[BuiltOnTheFailurePath] -[DebuggerDisplay("{ToString()}")] -[ValueObject] -internal sealed class ConstraintClaim : IEquatable { - - /// - /// The odd prime each field's hash is multiplied by before the next is folded in, so that two fields swapping - /// values do not collide. Its exact value carries no meaning beyond being odd and prime. - /// - private const int HashMultiplier = 397; - - #region Statics members declarations - - /// - /// Pairs with what it , written as a clause that - /// follows the constraint's name — "already caps the count at 3", not "caps". - /// - internal static ConstraintClaim Of(ConstraintCall constraint, string claims) { - return new ConstraintClaim(constraint.ToString(), constraint, claims); - } - - /// - /// Pairs a the library phrases itself — "the contained value "ABC"" — with - /// what it . For a part of a shape rather than a call the caller wrote. - /// - internal static ConstraintClaim OfPhrase(string subject, string claims) { - return new ConstraintClaim(subject, null, claims); - } - - #endregion - - /// Determines whether two claims blame the same subject for the same thing. - /// The first claim to compare. - /// The second claim to compare. - /// true when both hold the same subject and claim, or both are null; otherwise false. - public static bool operator ==(ConstraintClaim? left, ConstraintClaim? right) { - return Equals(left, right); - } - - /// Determines whether two claims differ in their subject or in what they claim. - /// The first claim to compare. - /// The second claim to compare. - /// true when they differ, or exactly one is null; otherwise false. - public static bool operator !=(ConstraintClaim? left, ConstraintClaim? right) { - return !Equals(left, right); - } - - private ConstraintClaim(string subject, ConstraintCall? constraint, string claims) { - Subject = subject; - Constraint = constraint; - Claims = claims; - } - - /// What the subject claims about the values it admits, as a clause following its name. - internal string Claims { get; } - - /// The constraint the subject is, when it is one; null for a phrase the library composed. - internal ConstraintCall? Constraint { get; } - - /// The subject as it reads in the message — a constraint as the caller spelled it, or a phrase. - internal string Subject { get; } - - /// The subject and its claim, as they read inside a conflict message. - public override string ToString() { - return $"{Subject} {Claims}"; - } - - /// - /// - /// The constraint is compared alongside the text, not merely implied by it: a claim whose subject is a - /// constraint and a phrase that happens to read the same are not the same value, because only the first can - /// be recognised as the constraint being applied. - /// - public bool Equals(ConstraintClaim? other) { - return other is not null - && string.Equals(Subject, other.Subject, StringComparison.Ordinal) - && string.Equals(Claims, other.Claims, StringComparison.Ordinal) - && Constraint == other.Constraint; - } - - /// - public override bool Equals(object? obj) { - return obj is ConstraintClaim other && Equals(other); - } - - /// - public override int GetHashCode() { - unchecked { - int hash = StringComparer.Ordinal.GetHashCode(Subject); - hash = (hash * HashMultiplier) ^ StringComparer.Ordinal.GetHashCode(Claims); - - return (hash * HashMultiplier) ^ (Constraint?.GetHashCode() ?? 0); - } - } - -} diff --git a/JustDummies/ContinuousIntervalSpec.cs b/JustDummies/ContinuousIntervalSpec.cs deleted file mode 100644 index 4b7321c3..00000000 --- a/JustDummies/ContinuousIntervalSpec.cs +++ /dev/null @@ -1,409 +0,0 @@ -#region Usings declarations - -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// The shared immutable engine behind the binary floating-point generators (, -/// , and Half on modern targets): an inclusive interval of finite doubles, an -/// optional allow-list, and point exclusions — each bound remembering the constraint that set it, so a conflict -/// message can name both sides. NaN and the infinities are never generated nor accepted: arbitrary test values -/// should cross invariants, not sabotage arithmetic. -/// -/// -/// -/// Narrower value types ride the double engine through a quantize step (for example -/// double → float): bounds are supplied already-representable in the narrow type, sampling happens in -/// double, and the drawn value is quantized then clamped back into the bounds. -/// -/// -/// Excluding a point from a continuum can only collide with a draw on a set of measure zero, but the engine -/// still guarantees the constraint: a colliding draw is nudged to the nearest non-excluded representable -/// value — a bounded deterministic walk along the type's own ladder, ascending then descending from the -/// original draw, not a retry loop. When neither walk finds a free value within its budget the generation -/// fails with an naming the seed. That is an exhausted local -/// search, not a proof that the range holds no free value, and the message says so: free values further than -/// the budget from the drawn candidate are never examined. -/// -/// -internal sealed class ContinuousIntervalSpec { - - private const int NudgeBudget = 128; - - #region Statics members declarations - - internal static ContinuousIntervalSpec Unconstrained(string typeName, Func render, Func quantize, Func nextUp, double domainMin, double domainMax) { - if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } - if (render is null) { throw new ArgumentNullException(nameof(render)); } - if (quantize is null) { throw new ArgumentNullException(nameof(quantize)); } - if (nextUp is null) { throw new ArgumentNullException(nameof(nextUp)); } - - return new ContinuousIntervalSpec(typeName, render, quantize, nextUp, domainMin, null, domainMax, null, null, null, []); - } - - /// Rejects NaN and the infinities — the shared argument guard of every floating-point generator. - internal static void EnsureFinite(double value, string parameterName) { - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - if (double.IsNaN(value) || double.IsInfinity(value)) { throw new ArgumentException("The value must be finite: NaN and infinities are never generated.", parameterName); } - } - - /// The next representable double above — the exclusive-bound arithmetic. - internal static double NextUp(double value) { - long bits = BitConverter.DoubleToInt64Bits(value); - if (bits >= 0L) { bits++; } else if (bits == long.MinValue) { bits = 1L; } else { bits--; } - - return BitConverter.Int64BitsToDouble(bits); - } - - /// The next representable double below . - internal static double NextDown(double value) { - return -NextUp(-value); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly List? _effectiveAllowed; - private readonly IReadOnlyList _excluded; - private readonly IReadOnlyList<(ConstraintCall Constraint, double[] Ordinals)> _exclusions; - private readonly Func _nextUp; - private readonly double _max; - private readonly ConstraintCall? _maxConstraint; - private readonly double _min; - private readonly ConstraintCall? _minConstraint; - private readonly Func _quantize; - private readonly Func _render; - private readonly string _typeName; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private ContinuousIntervalSpec(string typeName, Func render, Func quantize, Func nextUp, - double min, ConstraintCall? minConstraint, - double max, ConstraintCall? maxConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, - IReadOnlyList<(ConstraintCall Constraint, double[] Ordinals)> exclusions) { - _typeName = typeName; - _render = render; - _quantize = quantize; - _nextUp = nextUp; - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _exclusions = exclusions; - // The flat value set drives every draw-time decision; the provenance in _exclusions is consulted only - // when a conflict message must name the excluding constraint. Materialized once — "constrain once, draw many". - _excluded = exclusions.SelectMany(pair => pair.Ordinals).ToList(); - _effectiveAllowed = allowed?.Where(value => value >= min && value <= max && !IsExcluded(value)).ToList(); - } - - /// Tightens the lower bound; a looser bound than the current one is a no-op. - internal ContinuousIntervalSpec WithMinimum(double minimum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (double.IsInfinity(minimum)) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - if (minimum <= _min) { return this; } - - if (minimum > _max) { - if (_maxConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedAbove(applying, _maxConstraint, _render(_max)); - } - - return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions), applying); - } - - /// Tightens the upper bound; a looser bound than the current one is a no-op. - internal ContinuousIntervalSpec WithMaximum(double maximum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (double.IsInfinity(maximum)) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - if (maximum >= _max) { return this; } - - if (maximum < _min) { - if (_minConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedBelow(applying, _minConstraint, _render(_min)); - } - - return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _exclusions), applying); - } - - /// Tightens the lower bound to strictly above — via the type's next representable value. - internal ContinuousIntervalSpec WithMinimumAbove(double bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return WithMinimum(_nextUp(bound), applying); - } - - /// Tightens the upper bound to strictly below — via the type's next representable value. - internal ContinuousIntervalSpec WithMaximumBelow(double bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return WithMaximum(-_nextUp(-bound), applying); - } - - /// Restricts the domain to an explicit allow-list; declared once per generator. - internal ContinuousIntervalSpec WithAllowed(double[] values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == applying) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _allowedConstraint); } - - double[] distinct = values.Distinct().ToArray(); - - return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _exclusions), applying); - } - - /// Adds values the generator must never produce. - internal ContinuousIntervalSpec WithExcluded(double[] values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - // The applied constraint tags its own values, so a later exhaustion message can name the exclusion - // that actually emptied the domain rather than a bound that merely happens to border it. - List<(ConstraintCall Constraint, double[] Ordinals)> exclusions = [.. _exclusions, (applying, values)]; - - return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, exclusions), applying); - } - - /// - /// The number of distinct values the specification can produce — the allow-list size when one is set, 1 - /// for a validated pin (_min == _max, a singleton domain), and null otherwise: a floating-point - /// range is treated as a continuum (counting its representable values is a type-specific concern the shared - /// engine does not carry), so it stays outside the eager cardinality perimeter and a distinct collection over - /// it falls back to the bounded draw. Feeds . - /// - [SuppressMessage("Major Bug", "S1244:Floating point numbers should not be tested for equality", - Justification = - "Exact equality is the question, not an approximation of it: _min and _max are not measured " + - "quantities but the bounds the constraint chain validated, and the test asks whether they are the " + - "SAME representable value. A tolerance would answer a different question, and answer it wrongly: " + - "[1.0, 1.0 + 1e-12] holds millions of representable doubles, so reporting a cardinality of 1 would " + - "make ICardinalityHint promise a distinct collection of one element over a range that can serve many.")] - internal long? Cardinality { - get { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } - if (_min == _max) { return 1; } - - return null; - } - } - - /// - /// Whether is one the specification could produce — a member of the allow-list when - /// one is set, otherwise inside the interval and not excluded. Non-finite inputs fall outside the bounds and - /// so return false. Mirrors 's own domain. - /// - internal bool Contains(double value) { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } - - return value >= _min && value <= _max && !IsExcluded(value); - } - - /// Draws one value satisfying the whole specification. - [SuppressMessage("Major Bug", "S1244:Floating point numbers should not be tested for equality", - Justification = - "Exact equality detects the validated pin (the singleton domain Cardinality also reports) and " + - "returns the only value the bounds leave; IsSatisfiable already proved that value is not excluded, " + - "which is why this early return may skip the nudge walk. A tolerance would break both halves: it " + - "would collapse every draw of a merely narrow interval such as [1.0, 1.0 + 1e-12] onto its lower " + - "bound instead of sampling it, and it would take that exclusion-free shortcut for an interval whose " + - "lower bound IS excluded, returning a value the constraints forbid.")] - internal double Generate(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - SeededRandom random = source.Current; - - if (_effectiveAllowed is not null) { - return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; - } - - if (_min == _max) { return _min; } - - // Draw from the ordinary window rather than the declared interval (ADR-0052): the window only ever clips, - // and it steps aside entirely when it would leave the declared interval empty — a caller who asked for a - // magnitude gets it, a caller who merely permitted one does not. - double lower = Math.Max(_min, -OrdinaryMagnitude.AsDouble); - double upper = Math.Min(_max, OrdinaryMagnitude.AsDouble); - if (lower > upper) { - lower = _min; - upper = _max; - } - - // Sample around the midpoint so the span (max - min) never overflows to infinity on wide ranges. - double mid = lower / 2 + upper / 2; - double half = upper / 2 - lower / 2; - double candidate = Quantized(mid + (2 * random.NextDouble() - 1) * half); - - // A draw colliding with an excluded point (a measure-zero event) is nudged to the nearest - // non-excluded representable neighbour: ascending first, then descending from the original draw - // when the ascending walk leaves the bounds. Both walks step with the type-aware ladder (_nextUp), - // so on the narrow types a step lands on the next value of their own type instead of stalling on a - // sub-ulp double step that re-quantizes to the same value. - double? free = NudgeToFree(candidate, ascending: true) ?? NudgeToFree(candidate, ascending: false); - if (free is null) { - // The inner exception states what was actually established. Both walks are bounded, so their failure - // means the neighbourhood was exhausted — not that the range holds no free value, which nothing here - // examined. Reporting the stronger claim would send a caller looking for a contradiction that may not - // exist, and the shape that reaches this line (a wide range whose free values sit further than the - // budget from the draw) is precisely the one where it would not. - throw AnyGenerationException.LocalSearchExhausted(_typeName, Replay.Of(source, random.Seed), NudgeBudget); - } - - return free.Value; - } - - /// - /// Walks from along the type's representable ladder — ascending or descending — to the - /// nearest value the exclusions allow, staying within the bounds. Returns null when the walk reaches a - /// bound or spends its before finding one, so the caller can try the opposite - /// direction. Both directions returning null therefore means the neighbourhood is exhausted, which is - /// weaker than the range being empty: only the budget was searched. - /// - private double? NudgeToFree(double from, bool ascending) { - double candidate = from; - int budget = NudgeBudget; - while (IsExcluded(candidate)) { - // The type-aware next-up / next-down: -_nextUp(-x) mirrors the ascending step onto the descending ladder. - double next = ascending ? _nextUp(candidate) : -_nextUp(-candidate); - if (next < _min || next > _max || budget-- == 0) { return null; } - - candidate = Quantized(next); - } - - return candidate; - } - - private double Quantized(double value) { - double quantized = _quantize(value); - if (quantized < _min) { return _min; } - if (quantized > _max) { return _max; } - - return quantized; - } - - [SuppressMessage("Major Bug", "S1244:Floating point numbers should not be tested for equality", - Justification = - "Exclusion-list membership is exact by definition: DifferentFrom(x) forbids the value x, not a " + - "neighbourhood of it. Widening it to a tolerance would carve a band out of the continuum that no " + - "constraint asked for, and Generate's nudge walk would have to step clear of that band, turning a " + - "measure-zero collision into a systematic bias away from every excluded point. Equals(double) is " + - "'a == b || (IsNaN(a) && IsNaN(b))'; the NaN arm is unreachable because EnsureFinite rejects NaN at " + - "every entry point, so this is plain exact equality with a defensive tail.")] - private bool IsExcluded(double value) { - return _excluded.Any(excluded => value.Equals(excluded)); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private ContinuousIntervalSpec Validated(ContinuousIntervalSpec candidate, ConstraintCall applying) { - if (candidate.IsSatisfiable()) { return candidate; } - - throw ConflictingAnyConstraintException.NoValueRemains(applying, candidate.DescribeExhaustion(applying)); - } - - private bool IsSatisfiable() { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } - if (_min < _max) { return true; } - - return !IsExcluded(_min); - } - - private string DescribeExhaustion(ConstraintCall applying) { - IReadOnlyList culprits = ExcludingConstraintsInEffect(); - - if (_allowed is not null) { - if (culprits.Count == 0) { return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; } - - // Only the allow-list values the bounds still permit can be forbidden by an exclusion; if some allowed - // value was already dropped by a bound, the exclusions do not forbid "every" allowed value, so the claim - // is qualified rather than overstated. - string allowed = _allowed.All(WouldAllowIgnoringExclusions) - ? $"every value {_allowedConstraint} allows" - : $"every value {_allowedConstraint} allows that the other constraints leave"; - - return $"{Forbids(culprits, applying)} {allowed}"; - } - - if (culprits.Count == 0) { - string pinning = _minConstraint?.ToString() ?? _maxConstraint?.ToString() ?? "the declared bounds"; - - return $"{pinning} already pins the value to {_render(_min)}, which the exclusions forbid"; - } - - return $"{Forbids(culprits, applying)} {_render(_min)}, {PinningClause()}"; - } - - /// - /// The distinct exclusion constraints that actually caused the exhaustion — those forbidding at least one - /// value the interval and allow-list would otherwise permit. An exclusion whose values fall outside the - /// surviving domain never bit, so naming it would mislead; first-declared order is preserved. - /// - private IReadOnlyList ExcludingConstraintsInEffect() { - List names = []; - foreach ((ConstraintCall constraint, double[] values) in _exclusions) { - if (names.Contains(constraint)) { continue; } - if (values.Any(WouldAllowIgnoringExclusions)) { names.Add(constraint); } - } - - return names; - } - - /// Whether would be in the domain if no exclusion were applied. - private bool WouldAllowIgnoringExclusions(double value) { - if (_allowed is not null && !_allowed.Contains(value)) { return false; } - - return value >= _min && value <= _max; - } - - /// - /// The subject of the exhaustion clause. A single culprit that is the constraint being applied becomes "it", - /// so the message reads "Cannot apply DifferentFrom(1) because it forbids …" rather than repeating the - /// constraint on both sides of "because". - /// - private static string Forbids(IReadOnlyList names, ConstraintCall applying) { - if (names.Count == 1) { return names[0] == applying ? "it forbids" : $"{names[0]} forbids"; } - - return $"{string.Join(", ", names)} forbid"; - } - - /// Names the bounds that pinned the domain to its single value, for the "forbids X, the only value ... leaves" form. - private string PinningClause() { - List bounds = []; - if (_minConstraint is not null) { bounds.Add(_minConstraint); } - if (_maxConstraint is not null && _maxConstraint != _minConstraint) { bounds.Add(_maxConstraint); } - - if (bounds.Count == 0) { return "the only value the declared bounds leave"; } - if (bounds.Count == 1) { return $"the only value {bounds[0]} leaves"; } - - return $"the only value {string.Join(" and ", bounds)} leave"; - } - -} diff --git a/JustDummies/CountConstraints.cs b/JustDummies/CountConstraints.cs deleted file mode 100644 index 6e8ecb35..00000000 --- a/JustDummies/CountConstraints.cs +++ /dev/null @@ -1,102 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// The count-constraint facade shared by every collection generator, defined once over -/// . The builders -/// (a list, an array, a sequence, a set) and — whose keys run through -/// the very same — each expose NonEmpty/Empty/WithCount/ -/// WithMinCount/WithMaxCount/WithCountBetween by delegating here, so the argument -/// validation and the constraint labels that surface in a live -/// in exactly one place and cannot drift between the two surfaces. -/// -/// -/// Each method takes a state and returns the tightened state; the caller wraps that state back into its own -/// immutable generator. The labels (NonEmpty(), WithCount(3), ...) are part of the -/// user-facing conflict messages, so they are produced here rather than in the label-agnostic -/// , which only records whichever label its caller hands it. -/// -internal static class CountConstraints { - - #region Statics members declarations - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static void RequireNonNegative(int count, string parameterName) { - SizeGuard.RequireNonNegative(count, parameterName, "count"); - } - - private static void RequireProducible(int count, string parameterName) { - SizeGuard.RequireProducible(count, parameterName, "count"); - } - - #endregion - - /// Requires at least one element. - internal static CollectionState NonEmpty(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return state.WithMinCount(1, ConstraintCall.Of(nameof(NonEmpty))); - } - - /// Fixes the collection to no elements. - internal static CollectionState Empty(CollectionState state) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - - return state.WithExactCount(0, ConstraintCall.Of(nameof(Empty))); - } - - /// Fixes the exact number of elements. - internal static CollectionState WithCount(CollectionState state, int count) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - RequireProducible(count, nameof(count)); - - return state.WithExactCount(count, ConstraintCall.Of(nameof(WithCount), V(count))); - } - - /// - /// Requires at least elements. A minimum is the only one-sided count bound that - /// enlarges the generated collection. - /// - internal static CollectionState WithMinCount(CollectionState state, int count) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - RequireProducible(count, nameof(count)); - - return state.WithMinCount(count, ConstraintCall.Of(nameof(WithMinCount), V(count))); - } - - /// - /// Requires at most elements. A maximum only ever narrows the draw — it never widens - /// it beyond the default spread — so any non-negative value is accepted. - /// - internal static CollectionState WithMaxCount(CollectionState state, int count) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - RequireNonNegative(count, nameof(count)); - - return state.WithMaxCount(count, ConstraintCall.Of(nameof(WithMaxCount), V(count))); - } - - /// - /// Requires a number of elements within the inclusive range [, - /// ]. Equivalent to declaring the two bounds separately: the minimum sets the size, - /// the maximum only caps it. - /// - internal static CollectionState WithCountBetween(CollectionState state, int minimum, int maximum) { - if (state is null) { throw new ArgumentNullException(nameof(state)); } - RequireProducible(minimum, nameof(minimum)); - RequireNonNegative(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - ConstraintCall constraint = ConstraintCall.Of(nameof(WithCountBetween), V(minimum), V(maximum)); - - return state.WithMinCount(minimum, constraint).WithMaxCount(maximum, constraint); - } - -} diff --git a/JustDummies/CountSpec.cs b/JustDummies/CountSpec.cs deleted file mode 100644 index 779dc9b9..00000000 --- a/JustDummies/CountSpec.cs +++ /dev/null @@ -1,173 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// The immutable count specification shared by every collection generator (, -/// , ...): a lower bound, an optional upper bound and an optional exact count — each -/// remembering the constraint that set it, so a conflict message can name both sides. It is the collection-count -/// analogue of 's length bounds: every mutation returns a new specification and -/// cross-validates the whole eagerly, so a collection generator that exists can always produce a count. -/// -/// -/// Unconstrained, a collection draws between 0 and elements: an -/// unconstrained collection can therefore be empty — chain NonEmpty() when the surrounding code requires -/// content. The spread is deliberately smaller than 's (which is 16): a collection's -/// elements are themselves generated values, heavier than a string's characters, so a smaller default keeps a -/// dummy collection cheap while still exercising the multi-element path. -/// -/// That spread governs every draw, bounded or not (ADR-0050): a declared maximum composes with it rather than -/// replacing it, so an upper bound only narrows the draw and never widens it. Only a minimum, an exact count -/// or required elements enlarge a collection. -/// -/// -internal sealed class CountSpec { - - /// The number of extra elements an unconstrained collection may hold above its required minimum. - internal const int DefaultCountSpread = 8; - - #region Statics members declarations - - internal static readonly CountSpec Unconstrained = new(null, null, 0, null, null, null); - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Elements(int count) { - return count == 1 ? "1 element" : $"{V(count)} elements"; - } - - #endregion - - #region Fields declarations - - private readonly int? _exact; - private readonly ConstraintCall? _exactConstraint; - private readonly int? _max; - private readonly ConstraintCall? _maxConstraint; - private readonly int _min; - private readonly ConstraintCall? _minConstraint; - - #endregion - - private CountSpec(int? exact, ConstraintCall? exactConstraint, - int min, ConstraintCall? minConstraint, - int? max, ConstraintCall? maxConstraint) { - _exact = exact; - _exactConstraint = exactConstraint; - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - } - - /// The smallest count the specification allows — the exact count when pinned, otherwise the lower bound. - internal int Floor => _exact ?? _min; - - /// The largest count the specification allows, or null when the upper bound is left open. - internal int? Ceiling => _exact ?? _max; - - /// Fixes the exact count; declared once per generator. - internal CountSpec WithExactCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_exactConstraint == applying) { return this; } - if (_exactConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _exactConstraint); } - - return new CountSpec(count, applying, _min, _minConstraint, _max, _maxConstraint).Validated(applying); - } - - /// Tightens the minimum count; a looser bound than the current one is a no-op. - internal CountSpec WithMinCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (count <= _min) { return this; } - - return new CountSpec(_exact, _exactConstraint, count, applying, _max, _maxConstraint).Validated(applying); - } - - /// Tightens the maximum count; a looser bound than the current one is a no-op. - internal CountSpec WithMaxCount(int count, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_max is not null && count >= _max) { return this; } - - return new CountSpec(_exact, _exactConstraint, _min, _minConstraint, count, applying).Validated(applying); - } - - /// - /// Draws a count satisfying the specification. raises the floor to cover - /// elements the collection must contain (see ); lowers - /// the ceiling to the number of distinct values a distinct collection can hold. Both are already known to be - /// compatible with the declared bounds — the collection validates them eagerly before generation. - /// - internal int Resolve(SeededRandom random, int requiredMin, int? cap) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - if (_exact is int exact) { return exact; } - - int min = Math.Max(_min, requiredMin); - // A declared maximum composes with the default spread instead of replacing it (ADR-0050): it may only narrow - // the draw, never widen it, so a loose cap still yields the small unconstrained collection. Long arithmetic: a - // huge required minimum must saturate instead of overflowing past int.MaxValue. - long spreadCeiling = (long)min + DefaultCountSpread; - int max = (int)Math.Min(_max is int declared ? Math.Min(spreadCeiling, declared) : spreadCeiling, int.MaxValue); - if (cap is int ceiling && ceiling < max) { max = ceiling; } - if (max < min) { max = min; } - - return min == max ? min : random.NextInt32Inclusive(min, max); - } - - /// - /// Ensures the collection may hold the elements it must contain; throws naming the - /// upper bound that leaves no room. Symmetric wording, so the message reads whether the last constraint applied - /// was the count cap or the containment requirement. - /// - internal void EnsureFits(int required, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - int? cap = _exact ?? _max; - if (cap is int ceiling && required > ceiling) { - throw ConflictingAnyConstraintException.ContainedElementsDoNotFit(applying, Elements(required), Elements(ceiling)); - } - } - - private CountSpec Validated(ConstraintCall applying) { - if (_exact is int exact) { EnsureExactAgreesWithBounds(applying, exact); } - - if (_max is int max && _min > max) { - // Both bounds carry their constraint name: each is written as a pair by the constructor. And this branch - // needs _min > max, with max >= 0 because the entry points reject a negative count — so _min > 0, which - // only WithMinCount can produce, and it names the constraint as it sets the value. - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_maxConstraint!, $"already caps the count at {V(max)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Elements(_min)}")); - } - - return this; - } - - /// - /// Ensures a fixed count does not contradict a bound already applied; throws naming the bound it contradicts. - /// Symmetric wording, so the message reads whether the last constraint applied was the fixed count or the bound. - /// - private void EnsureExactAgreesWithBounds(ConstraintCall applying, int exact) { - if (exact < _min) { - // Same reasoning as above: exact >= 0 is guaranteed by the entry points, so exact < _min needs _min > 0 - // — a declared minimum, hence a named one — and a declared exact count carries its name too. - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the count at {V(exact)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Elements(_min)}")); - } - - if (_max is int cappedAt && exact > cappedAt) { - // Both values are declared here, and each was written as a pair with the constraint that declared it. - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the count at {V(exact)}"), - ConstraintClaim.Of(_maxConstraint!, $"already caps the count at {V(cappedAt)}")); - } - } - -} diff --git a/JustDummies/DecimalIntervalSpec.cs b/JustDummies/DecimalIntervalSpec.cs deleted file mode 100644 index fc41626f..00000000 --- a/JustDummies/DecimalIntervalSpec.cs +++ /dev/null @@ -1,479 +0,0 @@ -namespace JustDummies; - -/// -/// The immutable engine behind — the same algebra as -/// in arithmetic. has no -/// next-representable-value ladder, so exclusive bounds are expressed as an inclusive bound plus a point -/// exclusion, and a colliding draw is nudged by the smallest decimal increment within a bounded budget. An -/// optional scale lattice (set by WithScale) restricts the domain to the multiples of -/// 10^-scale — every value expressible in scale decimal places — by snapping the drawn candidate to -/// the grid, still in one constructive draw. -/// -internal sealed class DecimalIntervalSpec { - - private const int NoScale = -1; - private const int NudgeBudget = 128; - - /// The most decimal places a carries — the widest scale its 96-bit mantissa allows. - internal const int MaxScale = 28; - - /// How many bytes that mantissa spans: 96 bits, which reads back as three limbs. - private const int MantissaByteCount = 3 * sizeof(int); - - private static readonly decimal SmallestStep = 0.0000000000000000000000000001m; - private static readonly decimal MaxFraction = 7.9228162514264337593543950335m; - - #region Statics members declarations - - internal static DecimalIntervalSpec Unconstrained(string typeName, Func render) { - if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } - if (render is null) { throw new ArgumentNullException(nameof(render)); } - - return new DecimalIntervalSpec(typeName, render, decimal.MinValue, null, decimal.MaxValue, null, null, null, [], NoScale, null); - } - - /// Ten raised to as an exact ( in [0, 28]). - private static decimal Pow10(int power) { - decimal result = 1m; - for (int i = 0; i < power; i++) { result *= 10m; } - - return result; - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly decimal _ceiledMin; - private readonly List? _effectiveAllowed; - private readonly IReadOnlyList _excluded; - private readonly IReadOnlyList<(ConstraintCall Constraint, decimal[] Ordinals)> _exclusions; - private readonly int _excludedOnLattice; - private readonly decimal _flooredMax; - private readonly bool _latticeHasPoint; - private readonly decimal _max; - private readonly ConstraintCall? _maxConstraint; - private readonly decimal _min; - private readonly ConstraintCall? _minConstraint; - private readonly Func _render; - private readonly int _scale; - private readonly ConstraintCall? _scaleConstraint; - private readonly decimal _step; - private readonly string _typeName; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private DecimalIntervalSpec(string typeName, Func render, - decimal min, ConstraintCall? minConstraint, - decimal max, ConstraintCall? maxConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, - IReadOnlyList<(ConstraintCall Constraint, decimal[] Ordinals)> exclusions, - int scale, ConstraintCall? scaleConstraint) { - _typeName = typeName; - _render = render; - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _exclusions = exclusions; - _scale = scale; - _scaleConstraint = scaleConstraint; - // The flat value set drives every draw-time decision; the provenance in _exclusions is consulted only - // when a conflict message must name the excluding constraint. Materialized once — "constrain once, draw many". - _excluded = exclusions.SelectMany(pair => pair.Ordinals).ToList(); - // Lattice-derived state, materialized once — "constrain once, draw many". - if (scale >= 0) { - _step = 1m / Pow10(scale); - _ceiledMin = CeilToGrid(min, scale, _step); - _flooredMax = FloorToGrid(max, scale, _step); - _latticeHasPoint = _ceiledMin <= _flooredMax; - _excludedOnLattice = _excluded.Count(value => value >= min && value <= max && IsOnGrid(value, scale)); - } else { - _step = 0m; - _ceiledMin = min; - _flooredMax = max; - _latticeHasPoint = true; - _excludedOnLattice = 0; - } - // Materialized once here — "constrain once, draw many": Generate never refilters the allow-list. - _effectiveAllowed = allowed?.Where(value => value >= min && value <= max && !IsExcluded(value) && (scale < 0 || IsOnGrid(value, scale))).ToList(); - } - - /// Tightens the lower bound; a looser bound than the current one is a no-op. - internal DecimalIntervalSpec WithMinimum(decimal minimum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (minimum <= _min) { return this; } - - if (minimum > _max) { - if (_maxConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedAbove(applying, _maxConstraint, _render(_max)); - } - - return Validated(new DecimalIntervalSpec(_typeName, _render, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, _scale, _scaleConstraint), applying); - } - - /// Tightens the upper bound; a looser bound than the current one is a no-op. - internal DecimalIntervalSpec WithMaximum(decimal maximum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (maximum >= _max) { return this; } - - if (maximum < _min) { - if (_minConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedBelow(applying, _minConstraint, _render(_min)); - } - - return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _exclusions, _scale, _scaleConstraint), applying); - } - - /// Tightens the lower bound to strictly above — the inclusive bound plus a point exclusion. - internal DecimalIntervalSpec WithMinimumAbove(decimal bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return WithMinimum(bound, applying).WithExcluded([bound], applying); - } - - /// Tightens the upper bound to strictly below — the inclusive bound plus a point exclusion. - internal DecimalIntervalSpec WithMaximumBelow(decimal bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - return WithMaximum(bound, applying).WithExcluded([bound], applying); - } - - /// Restricts the domain to an explicit allow-list; declared once per generator. - internal DecimalIntervalSpec WithAllowed(decimal[] values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == applying) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _allowedConstraint); } - - decimal[] distinct = values.Distinct().ToArray(); - - return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _exclusions, _scale, _scaleConstraint), applying); - } - - /// Adds values the generator must never produce. - internal DecimalIntervalSpec WithExcluded(decimal[] values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - // The applied constraint tags its own values, so a later exhaustion message can name the exclusion - // that actually emptied the domain rather than a bound that merely happens to border it. - List<(ConstraintCall Constraint, decimal[] Ordinals)> exclusions = [.. _exclusions, (applying, values)]; - - return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, exclusions, _scale, _scaleConstraint), applying); - } - - /// - /// Restricts the domain to the multiples of 10^-scale — the values expressible in - /// decimal places. A value lattice, not a representation contract: the drawn value lies on the grid, but its - /// rendered form is not padded with trailing zeros. Declared once per generator. - /// - internal DecimalIntervalSpec WithScale(int scale, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_scale >= 0) { - if (_scale == scale) { return this; } - - // _scale and _scaleConstraint are written as a pair by the constructor and rethreaded as a pair by every - // rebuild, so a declared scale always carries the name of the constraint that declared it. - throw ConflictingAnyConstraintException.AlreadyDefined(applying, _scaleConstraint!); - } - - return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, scale, applying), applying); - } - - /// - /// The number of distinct values the specification can produce — the allow-list size when one is set; the number - /// of non-excluded grid points when a scale lattice is set and that count fits a ; 1 - /// for a validated pin; and null otherwise (a wider interval is a countable but - /// astronomically large domain, so it stays outside the eager cardinality perimeter and a distinct collection - /// over it falls back to the bounded draw). Feeds . - /// - internal long? Cardinality { - get { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } - if (_scale >= 0) { - long? points = LatticePointCount(); - - return points is null ? null : Math.Max(0, points.Value - _excludedOnLattice); - } - if (_min == _max) { return 1; } - - return null; - } - } - - /// - /// Whether is one the specification could produce — a member of the allow-list when - /// one is set, otherwise on the grid (when a scale lattice is set), inside the interval and not excluded. - /// Mirrors 's own domain. - /// - internal bool Contains(decimal value) { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } - if (_scale >= 0 && !IsOnGrid(value, _scale)) { return false; } - - return value >= _min && value <= _max && !IsExcluded(value); - } - - /// Draws one value satisfying the whole specification. - internal decimal Generate(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - - SeededRandom random = source.Current; - - if (_effectiveAllowed is not null) { - return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; - } - - if (_min == _max) { return _min; } - - // A uniform fraction in [0, 1] over the full 96-bit mantissa scale. NextBytes fills all three - // limbs — including each limb's top bit, which three non-negative Random.Next() draws would pin - // to zero, capping the fraction near 0.5 and leaving the upper half of every range unreachable. - byte[] mantissa = new byte[MantissaByteCount]; - random.NextBytes(mantissa); - decimal fraction = new decimal( - BitConverter.ToInt32(mantissa, 0), - BitConverter.ToInt32(mantissa, sizeof(int)), - BitConverter.ToInt32(mantissa, 2 * sizeof(int)), - false, MaxScale) / MaxFraction; - // Interpolate as a convex combination: min*(1 - fraction) + max*fraction stays within [min, max] for - // fraction in [0, 1], and no intermediate ever leaves the decimal range. The earlier midpoint form - // (mid ± half) overflowed on the full domain — it is symmetric, so max/2 rounds up and half = max/2 - min/2 - // doubles to just past decimal.MaxValue, throwing on an unconstrained Any.Decimal().Generate(). - // Draw from the ordinary window rather than the declared interval (ADR-0052): the window only ever clips, - // and it steps aside entirely when it would leave the declared interval empty. Without it an unconstrained - // decimal lands within a few decades of decimal.MaxValue, where a further multiplication throws - // OverflowException and a scale constraint has no fractional digits left to constrain. - decimal lower = Math.Max(_min, -OrdinaryMagnitude.AsDecimal); - decimal upper = Math.Min(_max, OrdinaryMagnitude.AsDecimal); - if (lower > upper) { - lower = _min; - upper = _max; - } - - decimal candidate = Clamped(lower * (1m - fraction) + upper * fraction); - - if (_scale >= 0) { - // Snap the draw onto the grid, then pull it inside the reachable grid window. A snapped point that - // collides with an exclusion is walked one grid step at a time — ascending first, then descending — - // a deterministic, bounded walk, not a retry loop. - decimal snapped = Math.Round(candidate, _scale, MidpointRounding.ToEven); - if (snapped < _ceiledMin) { snapped = _ceiledMin; } else if (snapped > _flooredMax) { snapped = _flooredMax; } - - decimal? free = NudgeOnGrid(snapped, true) ?? NudgeOnGrid(snapped, false); - if (free is null) { - throw AnyGenerationException.GridNudgeExhausted(_typeName, Replay.Of(source, random.Seed)); - } - - return free.Value; - } - - // A draw colliding with an excluded point is walked by the smallest decimal step — deterministic and - // bounded, not a retry loop. (At extreme magnitudes the step can vanish in rounding; the budget then - // fails the generation loudly instead of looping.) - int budget = NudgeBudget; - while (IsExcluded(candidate)) { - decimal next = Clamped(candidate + SmallestStep); - if (next == candidate || budget-- == 0) { - throw AnyGenerationException.ExclusionNudgeExhausted(_typeName, Replay.Of(source, random.Seed)); - } - - candidate = next; - } - - return candidate; - } - - /// - /// Walks from along the grid — ascending or descending by one step — to the nearest - /// value the exclusions allow, staying within the reachable grid window. Returns null when the walk - /// reaches the window edge before finding one, so the caller can try the opposite direction. - /// - private decimal? NudgeOnGrid(decimal from, bool ascending) { - decimal candidate = from; - int budget = NudgeBudget; - while (IsExcluded(candidate)) { - decimal next = ascending ? candidate + _step : candidate - _step; - if (next < _ceiledMin || next > _flooredMax || budget-- == 0) { return null; } - - candidate = next; - } - - return candidate; - } - - /// The number of grid points in [min, max], or null when that exceeds . - private long? LatticePointCount() { - if (!_latticeHasPoint) { return 0; } - - decimal maxCountable = _step * long.MaxValue; // _step is at most 1, so this never overflows - // The span itself can exceed the decimal range (an unconstrained WithScale spans MinValue..MaxValue). Only a - // straddling range risks that; when either half alone already outruns the countable span there are too many - // points, so short-circuit before forming a difference that would throw. - if (_ceiledMin < 0m && _flooredMax > 0m && (_flooredMax > maxCountable || -_ceiledMin > maxCountable)) { return null; } - - decimal span = _flooredMax - _ceiledMin; - if (span > maxCountable) { return null; } - - return (long)(span / _step) + 1; - } - - private static bool IsOnGrid(decimal value, int scale) { - return Math.Round(value, scale, MidpointRounding.ToEven) == value; - } - - /// The smallest grid point at or above . - private static decimal CeilToGrid(decimal value, int scale, decimal step) { - decimal rounded = Math.Round(value, scale, MidpointRounding.ToEven); - - return rounded >= value ? rounded : rounded + step; - } - - /// The largest grid point at or below . - private static decimal FloorToGrid(decimal value, int scale, decimal step) { - decimal rounded = Math.Round(value, scale, MidpointRounding.ToEven); - - return rounded <= value ? rounded : rounded - step; - } - - private decimal Clamped(decimal value) { - if (value < _min) { return _min; } - if (value > _max) { return _max; } - - return value; - } - - private bool IsExcluded(decimal value) { - return _excluded.Any(excluded => value == excluded); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private DecimalIntervalSpec Validated(DecimalIntervalSpec candidate, ConstraintCall applying) { - if (candidate.IsSatisfiable()) { return candidate; } - - throw ConflictingAnyConstraintException.NoValueRemains(applying, candidate.DescribeExhaustion(applying)); - } - - private bool IsSatisfiable() { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } - if (_scale >= 0) { - if (!_latticeHasPoint) { return false; } - - long? points = LatticePointCount(); - - return points is null || points.Value > _excludedOnLattice; - } - if (_min < _max) { return true; } - - return !IsExcluded(_min); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "The flagged lines are prose, not disabled code: the heuristic reads an equation, a bracketed range or a semicolon inside an " + - "explanatory sentence as a statement. These comments carry the reasoning this codebase asks every comment to carry, so the " + - "finding is recorded rather than the comment deleted.")] - private string DescribeExhaustion(ConstraintCall applying) { - IReadOnlyList culprits = ExcludingConstraintsInEffect(); - - if (_allowed is not null) { - if (culprits.Count == 0) { return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; } - - // Only the allow-list values the bounds and scale lattice still permit can be forbidden by an exclusion; - // if some allowed value was already dropped by a bound or the grid, the exclusions do not forbid "every" - // allowed value, so the claim is qualified rather than overstated. - string allowed = _allowed.All(WouldAllowIgnoringExclusions) - ? $"every value {_allowedConstraint} allows" - : $"every value {_allowedConstraint} allows that the other constraints leave"; - - return $"{Forbids(culprits, applying)} {allowed}"; - } - - if (_scale >= 0) { - if (!_latticeHasPoint || culprits.Count == 0) { return $"no {_typeName} value {_scaleConstraint} allows remains between {_render(_min)} and {_render(_max)}"; } - - return $"{Forbids(culprits, applying)} every {_scaleConstraint} value between {_render(_min)} and {_render(_max)}"; - } - - if (culprits.Count == 0) { - string pinning = _minConstraint?.ToString() ?? _maxConstraint?.ToString() ?? "the declared bounds"; - - return $"{pinning} already pins the value to {_render(_min)}, which the exclusions forbid"; - } - - return $"{Forbids(culprits, applying)} {_render(_min)}, {PinningClause()}"; - } - - /// - /// The distinct exclusion constraints that actually caused the exhaustion — those forbidding at least one - /// value the interval, scale lattice and allow-list would otherwise permit. An exclusion whose values fall - /// outside the surviving domain never bit, so naming it would mislead; first-declared order is preserved. - /// - private IReadOnlyList ExcludingConstraintsInEffect() { - List names = []; - foreach ((ConstraintCall constraint, decimal[] values) in _exclusions) { - if (names.Contains(constraint)) { continue; } - if (values.Any(WouldAllowIgnoringExclusions)) { names.Add(constraint); } - } - - return names; - } - - /// Whether would be in the domain if no exclusion were applied. - private bool WouldAllowIgnoringExclusions(decimal value) { - if (_allowed is not null && !_allowed.Contains(value)) { return false; } - if (_scale >= 0 && !IsOnGrid(value, _scale)) { return false; } - - return value >= _min && value <= _max; - } - - /// - /// The subject of the exhaustion clause. A single culprit that is the constraint being applied becomes "it", - /// so the message reads "Cannot apply DifferentFrom(1) because it forbids …" rather than repeating the - /// constraint on both sides of "because". - /// - private static string Forbids(IReadOnlyList names, ConstraintCall applying) { - if (names.Count == 1) { return names[0] == applying ? "it forbids" : $"{names[0]} forbids"; } - - return $"{string.Join(", ", names)} forbid"; - } - - /// Names the bounds that pinned the domain to its single value, for the "forbids X, the only value ... leaves" form. - private string PinningClause() { - List bounds = []; - if (_minConstraint is not null) { bounds.Add(_minConstraint); } - if (_maxConstraint is not null && _maxConstraint != _minConstraint) { bounds.Add(_maxConstraint); } - - if (bounds.Count == 0) { return "the only value the declared bounds leave"; } - if (bounds.Count == 1) { return $"the only value {bounds[0]} leaves"; } - - return $"the only value {string.Join(" and ", bounds)} leave"; - } - -} diff --git a/JustDummies/DummyException.cs b/JustDummies/DummyException.cs deleted file mode 100644 index 251aa14d..00000000 --- a/JustDummies/DummyException.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace JustDummies; - -/// -/// Base class of every exception the library throws on its own behalf, so a caller can catch "anything JustDummies -/// rejected" with a single clause. Concrete cases: -/// when two declared constraints cannot be satisfied together, -/// and when a generation fails even though the constraints were accepted. -/// -/// -/// Named after the dummies the library produces rather than after the entry point: an -/// Any-prefixed name reads in English as any exception whatsoever — the exact opposite of the -/// bounded, library-specific set this type denotes — which made a single-clause catch look like a catch-all. -/// -public abstract class DummyException : Exception { - - /// - /// Initializes a new instance of the class. - /// - /// A description of the failure. - protected DummyException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class wrapping an underlying failure. - /// - /// A description of the failure. - /// The underlying failure. - protected DummyException(string message, Exception innerException) : base(message, innerException) { } - -} diff --git a/JustDummies/IAny.cs b/JustDummies/IAny.cs deleted file mode 100644 index 01c2d981..00000000 --- a/JustDummies/IAny.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace JustDummies; - -/// -/// A recipe for an arbitrary value of type that satisfies the constraints declared on -/// it. This is the composition seam of the library: every generator — built-in or derived through -/// and — implements it, -/// so a constrained primitive, a value object built from one, and an object assembled from several all flow -/// through the same contract. -/// -/// -/// -/// A generator is an immutable recipe, not a value: each fluent constraint returns a new generator, and -/// randomness is drawn only when runs, from the random context the generator was -/// created with — the ambient context for the static entry points (see -/// ), or the isolated context of -/// . The same recipe can therefore be generated from several times, yielding a -/// fresh value each time. -/// -/// -/// is the single operation that materializes a value: the concrete generators expose -/// no implicit conversion to their generated type, so a value is produced only by an explicit -/// call — directly, or through the composition seams -/// and , which call -/// it internally. Generic inference likewise flows through this interface — Materialize(Any.String().NonEmpty()) -/// infers T = string. -/// -/// -/// The type of the generated values. -public interface IAny { - - /// - /// Produces one arbitrary value satisfying every constraint declared on this generator. - /// - /// - /// A built-in generator is safe to call concurrently: its draw on a random context is serialized, so no - /// amount of parallelism can corrupt the source or produce a value outside the declared constraints. That - /// covers the library's own draw only — a call may also run caller-supplied code (a factory passed to - /// , a composer passed to - /// , an element generator, a comparer) or a foreign - /// implementation, whose thread-safety is the caller's own responsibility. - /// Reproducibility is the separate cost of parallelism: concurrent draws interleave, so a seed replays a - /// run only while its draws are taken one at a time. To keep a parallel run reproducible, open a scope per - /// unit of work with and derive its seed from the run's own. - /// - /// A value that satisfies the declared constraints. - /// - /// Thrown when the value cannot be produced even though the declared constraints were accepted — for example - /// when a factory passed to rejects a generated value. - /// - T Generate(); - -} diff --git a/JustDummies/ICardinalityHint.cs b/JustDummies/ICardinalityHint.cs deleted file mode 100644 index 1cea6e77..00000000 --- a/JustDummies/ICardinalityHint.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace JustDummies; - -/// -/// Implemented by the library's own generators that draw from a small, countable domain, so a distinct -/// collection (, ListOf(...).Distinct(), a dictionary's keys) can tell — at -/// declaration time — whether a requested count, together with any values pinned through Containing(...), -/// can be satisfied from the effective domain, and fail eagerly with a -/// instead of only discovering it while drawing. -/// -/// -/// The two members travel together on purpose — that is the whole point of putting them on one interface: -/// answers "how many distinct values can the generator produce" (a -/// conservative upper bound), and answers "is this one of them". A distinct -/// collection needs both: the size to gate the count, and membership to tell a contained value that -/// extends the domain (one the generator could never draw) from one already inside it. Because they are a -/// single contract, a generator cannot advertise a cardinality without also answering membership — the compiler -/// keeps the promise, so no generator can drift out of the eager perimeter unnoticed. -/// -/// A generator whose domain is unbounded, effectively unbounded, or simply unknown (a foreign -/// , a derived generator) does not implement this interface; the collection then relies -/// on the bounded dedup-draw fallback, which surfaces a genuine shortfall as an -/// . -/// -/// -/// Both answers are given under , and only one of them survives a -/// collection carrying its own . does: -/// it is an upper bound, and no comparer can make a generator yield more distinct values than it has. -/// does not — a comparer stricter than the default one (reference equality over -/// a type with value equality) keeps apart values this membership calls the same, so a value it reports as -/// inside the domain may be one the collection would count as extending it. A collection carrying a custom -/// comparer therefore gates on the bound alone and treats every pinned value as outside: that can only defer -/// to the bounded dedup-draw, never refuse a specification the comparer makes satisfiable. -/// -/// -/// The element type. -internal interface ICardinalityHint { - - /// The number of distinct values the generator can produce, or null when that is unbounded or unknown. - long? DistinctCardinality { get; } - - /// Whether the generator, as constrained, could ever produce . - /// The candidate value. - /// true when is within the generator's domain; otherwise false. - bool Contains(T value); - -} diff --git a/JustDummies/JustDummies.csproj b/JustDummies/JustDummies.csproj deleted file mode 100644 index 0cfec5f4..00000000 --- a/JustDummies/JustDummies.csproj +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - netstandard2.0;net8.0 - enable - enable - latest - - - $(NoWarn);CA1510 - - - true - - - 0.1.0-dev - - - JustDummies - Sylvain AURAT - Reefact - - - - A fluent DSL for generating arbitrary yet valid test values: dummies. Constraints express the invariants a value must satisfy — never what the test asserts. Conflicting constraints fail fast with clear, actionable exceptions, and any sequential run is reproducible from a reported seed. - - - - testing;test-data;dummies;arbitrary;anonymous-values;fluent;deterministic;seed;value-objects;constraints - Apache-2.0 - false - - - https://justdummies.io - https://github.com/Reefact/first-class-errors.git - git - - © Reefact 2026 - - - icon.png - readme.md - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(TargetsForTfmSpecificContentInPackage);_AddAnalyzerToPackage - - - - - - - analyzers/dotnet/cs - - - - - diff --git a/JustDummies/NullableExtensions.cs b/JustDummies/NullableExtensions.cs deleted file mode 100644 index f3333b8f..00000000 --- a/JustDummies/NullableExtensions.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace JustDummies; - -/// -/// Makes a value-type generator optionally null: turns an -/// into an of that yields -/// null on an even coin flip and, otherwise, a value satisfying the constraints declared upstream — the -/// dummy for an optional value-type field (int?, DateTime?, Guid?, an enum, ...). -/// -public static class NullableExtensions { - - /// - /// How many equiprobable outcomes the null-versus-value draw picks between — two, which is what makes - /// null come up about half the time. Shared with - /// so the two siblings cannot drift to different rates. - /// - internal const int NullDrawOutcomes = 2; - - /// - /// Derives a generator that yields null about half the time and, otherwise, a value drawn from - /// — so a test exercises both the present and the absent case without pinning - /// either. Reproducible under a seed, like every other draw. - /// - /// - /// - /// The null-versus-value decision draws from the same random context as the wrapped generator, so an - /// Any.Reproducibly(...) run replays it exactly. A null draw does not consume a value from - /// the wrapped generator. - /// - /// - /// - /// int? discount = Any.Int32().Between(0, 100).OrNull().Generate(); - /// - /// - /// - /// The generator of the non-null values. - /// The underlying value type. - /// A generator of . - /// Thrown when is null. - public static IAny OrNull(this IAny generator) - where T : struct { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - RandomSource? source = AnyDerivation.SourceOf(generator); - bool reproducible = AnyDerivation.IsReproducible(generator); - - return new DerivedAny(source, reproducible, () => { - RandomSource working = source ?? AmbientRandomSource.Instance; - - return working.Current.Next(NullDrawOutcomes) == 0 ? (T?)null : generator.Generate(); - }); - } - -} - -/// -/// Makes a reference-type generator optionally null — the sibling of -/// for the reference-type case (a nullable string, or an optional -/// value object produced through As). It lives in its own class because a single overloaded -/// OrNull constrained once to struct and once to class would collide. -/// -public static class NullableReferenceExtensions { - - /// - /// Derives a generator that yields null about half the time and, otherwise, a value drawn from - /// — the dummy for an optional reference-type field. - /// - /// - /// The null-versus-value decision draws from the same random context as the wrapped generator, so a - /// reproducible run replays it exactly; a null draw does not consume a value from the wrapped generator. - /// - /// The generator of the non-null values. - /// The underlying reference type. - /// A generator that is sometimes null. - /// Thrown when is null. - public static IAny OrNull(this IAny generator) - where T : class { - if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - - RandomSource? source = AnyDerivation.SourceOf(generator); - bool reproducible = AnyDerivation.IsReproducible(generator); - - return new DerivedAny(source, reproducible, () => { - RandomSource working = source ?? AmbientRandomSource.Instance; - - return working.Current.Next(NullableExtensions.NullDrawOutcomes) == 0 ? (T?)null : generator.Generate(); - }); - } - -} diff --git a/JustDummies/OrdinalIntervalSpec.cs b/JustDummies/OrdinalIntervalSpec.cs deleted file mode 100644 index 783b2c7a..00000000 --- a/JustDummies/OrdinalIntervalSpec.cs +++ /dev/null @@ -1,473 +0,0 @@ -namespace JustDummies; - -/// -/// Order-preserving mappings between the discrete domains the generators expose and the unsigned 64-bit -/// ordinal space the shared interval engine works in. Every discrete type whose values fit 64 bits — -/// the integers, ticks-based time types, day numbers — maps onto [0, 2^64-1] so that one engine owns -/// bounds, exclusions, conflicts, and sampling for all of them. -/// -internal static class OrdinalMapping { - - private const ulong SignBit = 1UL << 63; - - /// Maps a signed 64-bit value to its ordinal: flips the sign bit, so ordering is preserved. - internal static ulong FromInt64(long value) { - return unchecked((ulong)value ^ SignBit); - } - - /// Maps an ordinal back to the signed 64-bit value it came from. - internal static long ToInt64(ulong ordinal) { - return unchecked((long)(ordinal ^ SignBit)); - } - -} - -/// -/// The shared immutable engine behind every discrete interval-shaped generator (integers, TimeSpan, -/// DateTime, ...): an inclusive interval of ordinals, an optional allow-list (OneOf), an -/// exclusion list, and an optional lattice (a step and an anchor, set by MultipleOf / a temporal -/// granularity) restricting the domain to values spaced a fixed distance apart — each bound remembering the -/// constraint that set it, so a conflict message can name both sides. Every mutation returns a new specification -/// and validates satisfiability eagerly: a generator that exists can always generate, in one draw, with no retry. -/// -/// -/// The engine is domain-agnostic: each public generator supplies its type's display name (for "no Int64 value -/// satisfies it" messages), a renderer turning an ordinal back into a displayable value, and the ordinal bounds -/// of its domain. The conflict logic therefore lives once, and a fix to a message or an edge case reaches every -/// discrete type at the same time. -/// -/// The lattice works because the ordinal map is affine: consecutive multiples of a step in value space stay a -/// constant step apart in ordinal space. The valid ordinals are therefore an arithmetic progression through a -/// known lattice ordinal (the anchor — the ordinal of the value 0, itself a multiple of every step), -/// found by striding from the first lattice point at or above the minimum. Sampling stays inside the drawn -/// window, so the wraparound at the ordinal-space edge is never crossed. -/// -/// -internal sealed class OrdinalIntervalSpec { - - #region Statics members declarations - - internal static OrdinalIntervalSpec Unconstrained(string typeName, Func render, ulong domainMin, ulong domainMax) { - if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } - if (render is null) { throw new ArgumentNullException(nameof(render)); } - - return new OrdinalIntervalSpec(typeName, render, domainMin, domainMax, - domainMin, null, domainMax, null, null, null, [], - 1UL, 0UL, null); - } - - /// Whether sits on the lattice anchored at with the given step. - private static bool IsOnLattice(ulong ordinal, ulong anchor, ulong step) { - ulong delta = ordinal >= anchor ? ordinal - anchor : anchor - ordinal; - - return delta % step == 0UL; - } - - /// - /// The smallest lattice ordinal at or above , staying within [min, max]. Returns - /// false when none exists (the stride steps past , or the domain top overflows). - /// - private static bool TryFirstLatticePoint(ulong min, ulong max, ulong anchor, ulong step, out ulong first) { - if (min >= anchor) { - ulong ahead = (min - anchor) % step; - if (ahead == 0UL) { - first = min; - } else { - first = min + (step - ahead); - if (first < min) { first = 0UL; return false; } // wrapped past the top of the ordinal domain - } - } else { - // The nearest lattice point at or above min is min plus its distance up to the anchor's phase. - first = min + (anchor - min) % step; - } - - return first <= max; - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly ulong _anchor; - private readonly ulong _domainMax; - private readonly ulong _domainMin; - private readonly List? _effectiveAllowed; - private readonly IReadOnlyList<(ConstraintCall Constraint, ulong[] Ordinals)> _exclusions; - private readonly List _excludedInRange; - private readonly List _excludedOnLattice; - private readonly ulong _latticeFirst; - private readonly bool _latticeHasPoint; - private readonly ulong _max; - private readonly ConstraintCall? _maxConstraint; - private readonly ulong _min; - private readonly ConstraintCall? _minConstraint; - private readonly Func _render; - private readonly ulong _step; - private readonly ConstraintCall? _stepConstraint; - private readonly string _typeName; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private OrdinalIntervalSpec(string typeName, Func render, ulong domainMin, ulong domainMax, - ulong min, ConstraintCall? minConstraint, - ulong max, ConstraintCall? maxConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, - IReadOnlyList<(ConstraintCall Constraint, ulong[] Ordinals)> exclusions, - ulong step, ulong anchor, ConstraintCall? stepConstraint) { - _typeName = typeName; - _render = render; - _domainMin = domainMin; - _domainMax = domainMax; - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _exclusions = exclusions; - _step = step; - _anchor = anchor; - _stepConstraint = stepConstraint; - // The flat ordinal set drives every hot-path decision; the provenance in _exclusions is consulted only - // when a conflict message must name the excluding constraint. Materialized once here — "constrain once, - // draw many": GenerateOrdinal never refilters or resorts. - ulong[] excluded = exclusions.SelectMany(pair => pair.Ordinals).ToArray(); - _excludedInRange = excluded.Where(value => value >= min && value <= max).Distinct().ToList(); - _excludedInRange.Sort(); - // Lattice-derived state, kept alongside so the hot path is a straight index-and-stride. - if (step > 1UL) { - _latticeHasPoint = TryFirstLatticePoint(min, max, anchor, step, out _latticeFirst); - _excludedOnLattice = _excludedInRange.Where(value => IsOnLattice(value, anchor, step)).ToList(); // stays sorted: filtered from a sorted list - } else { - _latticeHasPoint = true; - _latticeFirst = min; - _excludedOnLattice = _excludedInRange; - } - - if (allowed is not null) { - HashSet forbidden = [.. excluded]; - _effectiveAllowed = allowed.Where(value => value >= min && value <= max && !forbidden.Contains(value) && (step <= 1UL || IsOnLattice(value, anchor, step))).ToList(); - } - } - - /// Tightens the lower bound; a looser bound than the current one is a no-op. - internal OrdinalIntervalSpec WithMinimum(ulong minimum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (minimum <= _min) { return this; } - - if (minimum > _max) { - if (_maxConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedAbove(applying, _maxConstraint, _render(_max)); - } - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Tightens the lower bound to strictly above — the exclusive form of . - internal OrdinalIntervalSpec WithMinimumAbove(ulong bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (bound == _domainMax) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - return WithMinimum(bound + 1, applying); - } - - /// Tightens the upper bound; a looser bound than the current one is a no-op. - internal OrdinalIntervalSpec WithMaximum(ulong maximum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (maximum >= _max) { return this; } - - if (maximum < _min) { - if (_minConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedBelow(applying, _minConstraint, _render(_min)); - } - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Tightens the upper bound to strictly below — the exclusive form of . - internal OrdinalIntervalSpec WithMaximumBelow(ulong bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (bound == _domainMin) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - return WithMaximum(bound - 1, applying); - } - - /// Restricts the domain to an explicit allow-list; declared once per generator. - internal OrdinalIntervalSpec WithAllowed(ulong[] ordinals, ConstraintCall applying) { - if (ordinals is null) { throw new ArgumentNullException(nameof(ordinals)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == applying) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _allowedConstraint); } - - ulong[] distinct = ordinals.Distinct().ToArray(); - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// - /// Narrows an allow-list already in force to a subset of itself, keeping the constraint that declared it. - /// This is not a second declaration — the caller is removing values another constraint forbids — so it does - /// not trip the declared-once guard, and the original provenance stays the one a later conflict names. - /// - internal OrdinalIntervalSpec NarrowingAllowed(ulong[] kept, ConstraintCall applying) { - if (kept is null) { throw new ArgumentNullException(nameof(kept)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - ulong[] distinct = kept.Distinct().ToArray(); - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, distinct, _allowedConstraint ?? applying, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Adds values the generator must never produce. - internal OrdinalIntervalSpec WithExcluded(ulong[] ordinals, ConstraintCall applying) { - if (ordinals is null) { throw new ArgumentNullException(nameof(ordinals)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - // The applied constraint tags its own ordinals, so a later exhaustion message can name the exclusion - // that actually emptied the domain rather than a bound that merely happens to border it. - List<(ConstraintCall Constraint, ulong[] Ordinals)> exclusions = [.. _exclusions, (applying, ordinals)]; - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// - /// Restricts the domain to a lattice: values a multiple of away from - /// — a known lattice ordinal, the ordinal of the value 0. Declared once per - /// generator (a second, different lattice conflicts rather than silently intersecting). - /// - internal OrdinalIntervalSpec WithStep(ulong step, ulong anchor, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (step <= 1UL) { return this; } // every value is a multiple of one: a no-op, not a constraint - - if (_step > 1UL) { - if (_step == step && _anchor == anchor) { return this; } - - // _step and _stepConstraint are written as a pair by the constructor and rethreaded as a pair by every - // rebuild, so a declared step always carries the name of the constraint that declared it. - throw ConflictingAnyConstraintException.AlreadyDefined(applying, _stepConstraint!); - } - - return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, step, anchor, applying), applying); - } - - /// - /// The number of distinct values the specification can produce, or null when that exceeds - /// (a range too wide to ever conflict with a collection count). Feeds - /// , so a distinct collection over a narrow integer range can fail eagerly. - /// - internal long? Cardinality { - get { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } - if (_step > 1UL) { - if (!_latticeHasPoint) { return 0; } - - ulong onLattice = (_max - _latticeFirst) / _step + 1UL - (ulong)_excludedOnLattice.Count; - - return onLattice <= long.MaxValue ? (long)onLattice : null; - } - if (IsFullWidth()) { return null; } - - ulong count = _max - _min + 1UL - (ulong)_excludedInRange.Count; - - return count <= long.MaxValue ? (long)count : null; - } - } - - /// - /// Whether is a value the specification could produce — the exact domain - /// draws from: a member of the allow-list when one is set, otherwise on the - /// lattice (when one is set), inside the interval and not excluded. Feeds , - /// so a distinct collection can tell a contained value that extends the domain from one already inside it. - /// - internal bool Contains(ulong ordinal) { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(ordinal); } - if (_step > 1UL && !IsOnLattice(ordinal, _anchor, _step)) { return false; } - - return ordinal >= _min && ordinal <= _max && !_excludedInRange.Contains(ordinal); - } - - /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop body advances the very accumulator the condition tests — each iteration changes what the next one compares against — so " + - "the filter cannot be lifted out of the loop. A Where clause would evaluate every predicate against the value the accumulator " + - "held on entry and silently skip exclusions.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop body advances the very accumulator the condition tests — each iteration changes what the next one compares against — so " + - "the filter cannot be lifted out of the loop. A Where clause would evaluate every predicate against the value the accumulator " + - "held on entry and silently skip exclusions.")] - internal ulong GenerateOrdinal(SeededRandom random) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - - if (_effectiveAllowed is not null) { - return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; - } - - if (_step > 1UL) { - // The lattice caps the count below 2^64 (a step of two already halves the domain), so the - // full-width special case below never applies here. Draw an index over the surviving lattice - // points, then shift past any excluded lattice point at or below the drawn ordinal. - ulong latticeCount = (_max - _latticeFirst) / _step + 1UL; - ulong validCount = latticeCount - (ulong)_excludedOnLattice.Count; - ulong ordinal = _latticeFirst + (random.NextUInt64() % validCount) * _step; - foreach (ulong value in _excludedOnLattice) { - if (ordinal >= value) { ordinal += _step; } - } - - return ordinal; - } - - List excluded = _excludedInRange; - if (IsFullWidth()) { - // The interval spans the whole ordinal space, so its size does not fit a ulong and the index - // mapping below cannot run. Draw anywhere and, in the astronomically rare case the draw hits an - // excluded value, walk to the next free ordinal — a deterministic, bounded step, not a retry loop. - ulong candidate = random.NextUInt64(); - while (excluded.Contains(candidate)) { candidate = unchecked(candidate + 1UL); } - - return candidate; - } - - ulong validCountInRange = _max - _min + 1 - (ulong)excluded.Count; - ulong candidateOrdinal = _min + random.NextUInt64() % validCountInRange; - // Map the drawn index onto the k-th non-excluded ordinal of the interval: every excluded ordinal at - // or below the candidate shifts it up by one. Sorted ascending, so a single pass suffices. - foreach (ulong value in excluded) { - if (candidateOrdinal >= value) { candidateOrdinal++; } - } - - return candidateOrdinal; - } - - private bool IsFullWidth() { - return _min == ulong.MinValue && _max == ulong.MaxValue; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private OrdinalIntervalSpec Validated(OrdinalIntervalSpec candidate, ConstraintCall applying) { - if (candidate.IsSatisfiable()) { return candidate; } - - throw ConflictingAnyConstraintException.NoValueRemains(applying, candidate.DescribeExhaustion(applying)); - } - - private bool IsSatisfiable() { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } - if (_step > 1UL) { - if (!_latticeHasPoint) { return false; } - - return (_max - _latticeFirst) / _step + 1UL > (ulong)_excludedOnLattice.Count; - } - if (IsFullWidth()) { return true; } - - return _max - _min + 1 - (ulong)_excludedInRange.Count > 0; - } - - private string DescribeExhaustion(ConstraintCall applying) { - IReadOnlyList culprits = ExcludingConstraintsInEffect(); - - if (_allowed is not null) { - if (culprits.Count == 0) { return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; } - - // Only the allow-list values the bounds and lattice still permit can be forbidden by an exclusion; if - // some allowed value was already dropped by a bound or the lattice, the exclusions do not forbid - // "every" allowed value, so the claim is qualified rather than overstated. - string allowed = _allowed.All(WouldAllowIgnoringExclusions) - ? $"every value {_allowedConstraint} allows" - : $"every value {_allowedConstraint} allows that the other constraints leave"; - - return $"{Forbids(culprits, applying)} {allowed}"; - } - - if (_step > 1UL) { - if (!_latticeHasPoint || culprits.Count == 0) { return $"no {_typeName} value {_stepConstraint} allows remains between {_render(_min)} and {_render(_max)}"; } - - return $"{Forbids(culprits, applying)} every {_stepConstraint} value between {_render(_min)} and {_render(_max)}"; - } - - if (_min == _max) { - if (culprits.Count == 0) { - string pinning = _minConstraint?.ToString() ?? _maxConstraint?.ToString() ?? "the declared bounds"; - - return $"{pinning} already pins the value to {_render(_min)}"; - } - - return $"{Forbids(culprits, applying)} {_render(_min)}, {PinningClause()}"; - } - - if (culprits.Count == 0) { return $"no value remains between {_render(_min)} and {_render(_max)} once the excluded values are removed"; } - - return $"{Forbids(culprits, applying)} every value between {_render(_min)} and {_render(_max)}"; - } - - /// - /// The distinct exclusion constraints that actually caused the exhaustion — those forbidding at least one - /// value the interval, lattice and allow-list would otherwise permit. An exclusion whose values fall outside - /// the surviving domain never bit, so naming it would mislead; first-declared order is preserved. - /// - private IReadOnlyList ExcludingConstraintsInEffect() { - List names = []; - foreach ((ConstraintCall constraint, ulong[] ordinals) in _exclusions) { - if (names.Contains(constraint)) { continue; } - if (ordinals.Any(WouldAllowIgnoringExclusions)) { names.Add(constraint); } - } - - return names; - } - - /// Whether would be in the domain if no exclusion were applied. - private bool WouldAllowIgnoringExclusions(ulong ordinal) { - if (_allowed is not null && !_allowed.Contains(ordinal)) { return false; } - if (_step > 1UL && !IsOnLattice(ordinal, _anchor, _step)) { return false; } - - return ordinal >= _min && ordinal <= _max; - } - - /// - /// The subject of the exhaustion clause. A single culprit that is the constraint being applied becomes "it", - /// so the message reads "Cannot apply Except(1) because it forbids …" rather than repeating the constraint on - /// both sides of "because". - /// - private static string Forbids(IReadOnlyList names, ConstraintCall applying) { - if (names.Count == 1) { return names[0] == applying ? "it forbids" : $"{names[0]} forbids"; } - - return $"{string.Join(", ", names)} forbid"; - } - - /// Names the bounds that pinned the domain to its single value, for the "forbids X, the only value ... leaves" form. - private string PinningClause() { - List bounds = []; - if (_minConstraint is not null) { bounds.Add(_minConstraint); } - if (_maxConstraint is not null && _maxConstraint != _minConstraint) { bounds.Add(_maxConstraint); } - - if (bounds.Count == 0) { return "the only value the declared bounds leave"; } - if (bounds.Count == 1) { return $"the only value {bounds[0]} leaves"; } - - return $"the only value {string.Join(" and ", bounds)} leave"; - } - -} diff --git a/JustDummies/OrdinaryMagnitude.cs b/JustDummies/OrdinaryMagnitude.cs deleted file mode 100644 index c80fda1f..00000000 --- a/JustDummies/OrdinaryMagnitude.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace JustDummies; - -/// -/// The magnitude an arbitrary number stays within unless a declared constraint leaves no room for it — the -/// numeric counterpart of the small default spread the string and collection generators use, and the reason a -/// dummy number stays unremarkable (ADR-0052). -/// -/// -/// -/// A dummy exists to fill a slot whose content the test does not care about. A value drawn uniformly across a -/// floating-point type's whole domain is not that: almost every draw lands within a few decades of the -/// type's maximum, where the type stops behaving like arithmetic — a further multiplication overflows, -/// x + 1 == x, and a scale constraint has no fractional digits left to constrain. Such a value makes -/// the test fail for reasons that have nothing to do with what it asserts, and never visits the magnitudes -/// where real defects live. -/// -/// -/// The window clips a draw; it never widens one, and it never overrides a declared bound. A generator -/// whose declared interval lies entirely outside it — Between(1e300, 1e308) — draws from that interval -/// as declared, because the caller asked for that magnitude explicitly. A generator whose declared interval -/// merely permits large values — Between(0, double.MaxValue) — keeps drawing ordinary ones, -/// because permitting is not requesting. The same principle governs sizes under ADR-0050. -/// -/// -/// Both constants carry the same magnitude in the two arithmetics the numeric engines use. A type whose whole -/// domain is already ordinary — Half, which stops at 65 504 — is unaffected, since clipping to a -/// window wider than its domain changes nothing. -/// -/// -internal static class OrdinaryMagnitude { - - /// - /// The window's half-width for the binary floating-point engine. Large enough to look like a real quantity - /// and to exercise multi-digit formatting, small enough that any plausible further arithmetic — a rate, a - /// tax, a conversion factor — stays hundreds of decades away from overflow, and that a double keeps - /// about nine significant digits below the decimal point for a scale constraint to act on. - /// - internal const double AsDouble = 1_000_000d; - - /// The same magnitude for the decimal engine. - internal const decimal AsDecimal = 1_000_000m; - -} diff --git a/JustDummies/PublicAPI/net8.0/PublicAPI.Shipped.txt b/JustDummies/PublicAPI/net8.0/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c581..00000000 --- a/JustDummies/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/JustDummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/JustDummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt deleted file mode 100644 index af1ac833..00000000 --- a/JustDummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,502 +0,0 @@ -#nullable enable -JustDummies.Any -JustDummies.AnyArray -JustDummies.AnyArray.Distinct() -> JustDummies.AnyArray! -JustDummies.AnyArray.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnyArray! -JustDummies.AnyBoolean -JustDummies.AnyBoolean.DifferentFrom(bool value) -> JustDummies.AnyBoolean! -JustDummies.AnyBoolean.False() -> JustDummies.AnyBoolean! -JustDummies.AnyBoolean.Generate() -> bool -JustDummies.AnyBoolean.True() -> JustDummies.AnyBoolean! -JustDummies.AnyByte -JustDummies.AnyByte.Between(byte minimum, byte maximum) -> JustDummies.AnyByte! -JustDummies.AnyByte.DifferentFrom(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.Except(params byte[]! values) -> JustDummies.AnyByte! -JustDummies.AnyByte.Generate() -> byte -JustDummies.AnyByte.GreaterThan(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.GreaterThanOrEqualTo(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.LessThan(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.LessThanOrEqualTo(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.MultipleOf(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.NonZero() -> JustDummies.AnyByte! -JustDummies.AnyByte.OneOf(params byte[]! values) -> JustDummies.AnyByte! -JustDummies.AnyByte.Zero() -> JustDummies.AnyByte! -JustDummies.AnyChar -JustDummies.AnyChar.Alpha() -> JustDummies.AnyChar! -JustDummies.AnyChar.AlphaNumeric() -> JustDummies.AnyChar! -JustDummies.AnyChar.DifferentFrom(char value) -> JustDummies.AnyChar! -JustDummies.AnyChar.Except(params char[]! values) -> JustDummies.AnyChar! -JustDummies.AnyChar.Generate() -> char -JustDummies.AnyChar.LowerCase() -> JustDummies.AnyChar! -JustDummies.AnyChar.Numeric() -> JustDummies.AnyChar! -JustDummies.AnyChar.OneOf(params char[]! values) -> JustDummies.AnyChar! -JustDummies.AnyChar.UpperCase() -> JustDummies.AnyChar! -JustDummies.AnyCollection -JustDummies.AnyCollection.Containing(TItem value) -> TSelf! -JustDummies.AnyCollection.ContainingAny(JustDummies.IAny! generator) -> TSelf! -JustDummies.AnyCollection.Empty() -> TSelf! -JustDummies.AnyCollection.Generate() -> TResult -JustDummies.AnyCollection.NonEmpty() -> TSelf! -JustDummies.AnyCollection.WithCount(int count) -> TSelf! -JustDummies.AnyCollection.WithCountBetween(int minimum, int maximum) -> TSelf! -JustDummies.AnyCollection.WithMaxCount(int count) -> TSelf! -JustDummies.AnyCollection.WithMinCount(int count) -> TSelf! -JustDummies.AnyContext -JustDummies.AnyContext.Boolean() -> JustDummies.AnyBoolean! -JustDummies.AnyContext.Byte() -> JustDummies.AnyByte! -JustDummies.AnyContext.Char() -> JustDummies.AnyChar! -JustDummies.AnyContext.DateOnly() -> JustDummies.AnyDateOnly! -JustDummies.AnyContext.DateTime() -> JustDummies.AnyDateTime! -JustDummies.AnyContext.DateTimeOffset() -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyContext.Decimal() -> JustDummies.AnyDecimal! -JustDummies.AnyContext.Double() -> JustDummies.AnyDouble! -JustDummies.AnyContext.ElementOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.Enum() -> JustDummies.AnyEnum! -JustDummies.AnyContext.Guid() -> JustDummies.AnyGuid! -JustDummies.AnyContext.Half() -> JustDummies.AnyHalf! -JustDummies.AnyContext.Int128() -> JustDummies.AnyInt128! -JustDummies.AnyContext.Int16() -> JustDummies.AnyInt16! -JustDummies.AnyContext.Int32() -> JustDummies.AnyInt32! -JustDummies.AnyContext.Int64() -> JustDummies.AnyInt64! -JustDummies.AnyContext.OneOf(params T[]! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.SByte() -> JustDummies.AnySByte! -JustDummies.AnyContext.Seed.get -> int -JustDummies.AnyContext.Single() -> JustDummies.AnySingle! -JustDummies.AnyContext.String() -> JustDummies.AnyString! -JustDummies.AnyContext.StringMatching(string! pattern) -> JustDummies.AnyPattern! -JustDummies.AnyContext.StringMatching(System.Text.RegularExpressions.Regex! pattern) -> JustDummies.AnyPattern! -JustDummies.AnyContext.TimeOnly() -> JustDummies.AnyTimeOnly! -JustDummies.AnyContext.TimeSpan() -> JustDummies.AnyTimeSpan! -JustDummies.AnyContext.UInt128() -> JustDummies.AnyUInt128! -JustDummies.AnyContext.UInt16() -> JustDummies.AnyUInt16! -JustDummies.AnyContext.UInt32() -> JustDummies.AnyUInt32! -JustDummies.AnyContext.UInt64() -> JustDummies.AnyUInt64! -JustDummies.AnyContext.Uri() -> JustDummies.AnyUri! -JustDummies.AnyDateOnly -JustDummies.AnyDateOnly.After(System.DateOnly date) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.AfterOrEqualTo(System.DateOnly date) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.Before(System.DateOnly date) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.BeforeOrEqualTo(System.DateOnly date) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.Between(System.DateOnly start, System.DateOnly end) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.DifferentFrom(System.DateOnly value) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.Except(params System.DateOnly[]! values) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateOnly.Generate() -> System.DateOnly -JustDummies.AnyDateOnly.OneOf(params System.DateOnly[]! values) -> JustDummies.AnyDateOnly! -JustDummies.AnyDateTime -JustDummies.AnyDateTime.After(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.AfterOrEqualTo(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Before(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.BeforeOrEqualTo(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Between(System.DateTime start, System.DateTime end) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.DifferentFrom(System.DateTime value) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Except(params System.DateTime[]! values) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Generate() -> System.DateTime -JustDummies.AnyDateTime.OneOf(params System.DateTime[]! values) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTimeOffset -JustDummies.AnyDateTimeOffset.After(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.AfterOrEqualTo(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Before(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.BeforeOrEqualTo(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Between(System.DateTimeOffset start, System.DateTimeOffset end) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.DifferentFrom(System.DateTimeOffset value) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Except(params System.DateTimeOffset[]! values) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Generate() -> System.DateTimeOffset -JustDummies.AnyDateTimeOffset.OneOf(params System.DateTimeOffset[]! values) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithOffset(System.TimeSpan offset) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithOffsetBetween(System.TimeSpan minimum, System.TimeSpan maximum) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDecimal -JustDummies.AnyDecimal.Between(decimal minimum, decimal maximum) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.DifferentFrom(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Except(params decimal[]! values) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Generate() -> decimal -JustDummies.AnyDecimal.GreaterThan(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.GreaterThanOrEqualTo(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.LessThan(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.LessThanOrEqualTo(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Negative() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.NonZero() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.OneOf(params decimal[]! values) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Positive() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.WithScale(int scale) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Zero() -> JustDummies.AnyDecimal! -JustDummies.AnyDictionary -JustDummies.AnyDictionary.ContainingAnyKey(JustDummies.IAny! generator) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.ContainingEntry(TKey key, TValue value) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.ContainingKey(TKey key) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.Empty() -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary! -JustDummies.AnyDictionary.NonEmpty() -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithCountBetween(int minimum, int maximum) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithMaxCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithMinCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDouble -JustDummies.AnyDouble.Between(double minimum, double maximum) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.DifferentFrom(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Except(params double[]! values) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Generate() -> double -JustDummies.AnyDouble.GreaterThan(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.GreaterThanOrEqualTo(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.LessThan(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.LessThanOrEqualTo(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Negative() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.NonZero() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.OneOf(params double[]! values) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Positive() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Zero() -> JustDummies.AnyDouble! -JustDummies.AnyEnum -JustDummies.AnyEnum.AllowingCombinations() -> JustDummies.AnyEnum! -JustDummies.AnyEnum.DifferentFrom(TEnum value) -> JustDummies.AnyEnum! -JustDummies.AnyEnum.Except(params TEnum[]! values) -> JustDummies.AnyEnum! -JustDummies.AnyEnum.Generate() -> TEnum -JustDummies.AnyEnum.OneOf(params TEnum[]! values) -> JustDummies.AnyEnum! -JustDummies.AnyExtensions -JustDummies.AnyFtpUri -JustDummies.AnyFtpUri.Generate() -> System.Uri! -JustDummies.AnyFtpUri.WithHost(string! host) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithoutPath() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPathSegments(int count) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPort() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPort(int port) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo(string! user) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo(string! user, string! password) -> JustDummies.AnyFtpUri! -JustDummies.AnyGenerationException -JustDummies.AnyGenerationException.AnyGenerationException(string! message) -> void -JustDummies.AnyGenerationException.AnyGenerationException(string! message, System.Exception! innerException) -> void -JustDummies.AnyGenerationException.Seed.get -> int? -JustDummies.AnyGuid -JustDummies.AnyGuid.DifferentFrom(System.Guid value) -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Empty() -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Except(params System.Guid[]! values) -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Generate() -> System.Guid -JustDummies.AnyGuid.NonEmpty() -> JustDummies.AnyGuid! -JustDummies.AnyGuid.OneOf(params System.Guid[]! values) -> JustDummies.AnyGuid! -JustDummies.AnyHalf -JustDummies.AnyHalf.Between(System.Half minimum, System.Half maximum) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.DifferentFrom(System.Half value) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.Except(params System.Half[]! values) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.Generate() -> System.Half -JustDummies.AnyHalf.GreaterThan(System.Half value) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.GreaterThanOrEqualTo(System.Half value) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.LessThan(System.Half value) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.LessThanOrEqualTo(System.Half value) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.Negative() -> JustDummies.AnyHalf! -JustDummies.AnyHalf.NonZero() -> JustDummies.AnyHalf! -JustDummies.AnyHalf.OneOf(params System.Half[]! values) -> JustDummies.AnyHalf! -JustDummies.AnyHalf.Positive() -> JustDummies.AnyHalf! -JustDummies.AnyHalf.Zero() -> JustDummies.AnyHalf! -JustDummies.AnyInt128 -JustDummies.AnyInt128.Between(System.Int128 minimum, System.Int128 maximum) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.DifferentFrom(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.Except(params System.Int128[]! values) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.Generate() -> System.Int128 -JustDummies.AnyInt128.GreaterThan(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.GreaterThanOrEqualTo(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.LessThan(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.LessThanOrEqualTo(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.MultipleOf(System.Int128 value) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.Negative() -> JustDummies.AnyInt128! -JustDummies.AnyInt128.NonZero() -> JustDummies.AnyInt128! -JustDummies.AnyInt128.OneOf(params System.Int128[]! values) -> JustDummies.AnyInt128! -JustDummies.AnyInt128.Positive() -> JustDummies.AnyInt128! -JustDummies.AnyInt128.Zero() -> JustDummies.AnyInt128! -JustDummies.AnyInt16 -JustDummies.AnyInt16.Between(short minimum, short maximum) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.DifferentFrom(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Except(params short[]! values) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Generate() -> short -JustDummies.AnyInt16.GreaterThan(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.GreaterThanOrEqualTo(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.LessThan(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.LessThanOrEqualTo(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.MultipleOf(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Negative() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.NonZero() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.OneOf(params short[]! values) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Positive() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Zero() -> JustDummies.AnyInt16! -JustDummies.AnyInt32 -JustDummies.AnyInt32.Between(int minimum, int maximum) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.DifferentFrom(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Except(params int[]! values) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Generate() -> int -JustDummies.AnyInt32.GreaterThan(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.GreaterThanOrEqualTo(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.LessThan(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.LessThanOrEqualTo(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.MultipleOf(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Negative() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.NonZero() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.OneOf(params int[]! values) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Positive() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Zero() -> JustDummies.AnyInt32! -JustDummies.AnyInt64 -JustDummies.AnyInt64.Between(long minimum, long maximum) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.DifferentFrom(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Except(params long[]! values) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Generate() -> long -JustDummies.AnyInt64.GreaterThan(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.GreaterThanOrEqualTo(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.LessThan(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.LessThanOrEqualTo(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.MultipleOf(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Negative() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.NonZero() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.OneOf(params long[]! values) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Positive() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Zero() -> JustDummies.AnyInt64! -JustDummies.AnyList -JustDummies.AnyList.Distinct() -> JustDummies.AnyList! -JustDummies.AnyList.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnyList! -JustDummies.AnyMailtoUri -JustDummies.AnyMailtoUri.Generate() -> System.Uri! -JustDummies.AnyMailtoUri.WithDomain(string! domain) -> JustDummies.AnyMailtoUri! -JustDummies.AnyMailtoUri.WithHeaders() -> JustDummies.AnyMailtoUri! -JustDummies.AnyMailtoUri.WithLocalPart(string! localPart) -> JustDummies.AnyMailtoUri! -JustDummies.AnyOneOf -JustDummies.AnyOneOf.DifferentFrom(T value) -> JustDummies.AnyOneOf! -JustDummies.AnyOneOf.Except(params T[]! values) -> JustDummies.AnyOneOf! -JustDummies.AnyOneOf.Generate() -> T -JustDummies.AnyPattern -JustDummies.AnyPattern.DifferentFrom(string! value) -> JustDummies.AnyPattern! -JustDummies.AnyPattern.Except(params string![]! values) -> JustDummies.AnyPattern! -JustDummies.AnyPattern.Generate() -> string! -JustDummies.AnyRelativeUri -JustDummies.AnyRelativeUri.Generate() -> System.Uri! -JustDummies.AnyRelativeUri.Rooted() -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithFragment() -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithPathSegments(int count) -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithQuery() -> JustDummies.AnyRelativeUri! -JustDummies.AnySByte -JustDummies.AnySByte.Between(sbyte minimum, sbyte maximum) -> JustDummies.AnySByte! -JustDummies.AnySByte.DifferentFrom(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.Except(params sbyte[]! values) -> JustDummies.AnySByte! -JustDummies.AnySByte.Generate() -> sbyte -JustDummies.AnySByte.GreaterThan(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.GreaterThanOrEqualTo(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.LessThan(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.LessThanOrEqualTo(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.MultipleOf(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.Negative() -> JustDummies.AnySByte! -JustDummies.AnySByte.NonZero() -> JustDummies.AnySByte! -JustDummies.AnySByte.OneOf(params sbyte[]! values) -> JustDummies.AnySByte! -JustDummies.AnySByte.Positive() -> JustDummies.AnySByte! -JustDummies.AnySByte.Zero() -> JustDummies.AnySByte! -JustDummies.AnySequence -JustDummies.AnySequence.Distinct() -> JustDummies.AnySequence! -JustDummies.AnySequence.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnySequence! -JustDummies.AnySet -JustDummies.AnySingle -JustDummies.AnySingle.Between(float minimum, float maximum) -> JustDummies.AnySingle! -JustDummies.AnySingle.DifferentFrom(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.Except(params float[]! values) -> JustDummies.AnySingle! -JustDummies.AnySingle.Generate() -> float -JustDummies.AnySingle.GreaterThan(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.GreaterThanOrEqualTo(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.LessThan(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.LessThanOrEqualTo(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.Negative() -> JustDummies.AnySingle! -JustDummies.AnySingle.NonZero() -> JustDummies.AnySingle! -JustDummies.AnySingle.OneOf(params float[]! values) -> JustDummies.AnySingle! -JustDummies.AnySingle.Positive() -> JustDummies.AnySingle! -JustDummies.AnySingle.Zero() -> JustDummies.AnySingle! -JustDummies.AnyString -JustDummies.AnyString.Alpha() -> JustDummies.AnyString! -JustDummies.AnyString.AlphaNumeric() -> JustDummies.AnyString! -JustDummies.AnyString.Containing(string! value) -> JustDummies.AnyString! -JustDummies.AnyString.DifferentFrom(string! value) -> JustDummies.AnyString! -JustDummies.AnyString.EndingWith(string! suffix) -> JustDummies.AnyString! -JustDummies.AnyString.Except(params string![]! values) -> JustDummies.AnyString! -JustDummies.AnyString.Generate() -> string! -JustDummies.AnyString.LowerCase() -> JustDummies.AnyString! -JustDummies.AnyString.NonEmpty() -> JustDummies.AnyString! -JustDummies.AnyString.Numeric() -> JustDummies.AnyString! -JustDummies.AnyString.OneOf(params string![]! values) -> JustDummies.AnyString! -JustDummies.AnyString.OneOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyString! -JustDummies.AnyString.StartingWith(string! prefix) -> JustDummies.AnyString! -JustDummies.AnyString.UpperCase() -> JustDummies.AnyString! -JustDummies.AnyString.WithChars(string! pool) -> JustDummies.AnyString! -JustDummies.AnyString.WithLength(int length) -> JustDummies.AnyString! -JustDummies.AnyString.WithLengthBetween(int minimum, int maximum) -> JustDummies.AnyString! -JustDummies.AnyString.WithMaxLength(int length) -> JustDummies.AnyString! -JustDummies.AnyString.WithMinLength(int length) -> JustDummies.AnyString! -JustDummies.AnyTimeOnly -JustDummies.AnyTimeOnly.After(System.TimeOnly time) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.AfterOrEqualTo(System.TimeOnly time) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.Before(System.TimeOnly time) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.BeforeOrEqualTo(System.TimeOnly time) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.Between(System.TimeOnly start, System.TimeOnly end) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.DifferentFrom(System.TimeOnly value) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.Except(params System.TimeOnly[]! values) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.Generate() -> System.TimeOnly -JustDummies.AnyTimeOnly.OneOf(params System.TimeOnly[]! values) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeOnly.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyTimeOnly! -JustDummies.AnyTimeSpan -JustDummies.AnyTimeSpan.Between(System.TimeSpan minimum, System.TimeSpan maximum) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.DifferentFrom(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Except(params System.TimeSpan[]! values) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Generate() -> System.TimeSpan -JustDummies.AnyTimeSpan.GreaterThan(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.GreaterThanOrEqualTo(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.LessThan(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.LessThanOrEqualTo(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Negative() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.NonZero() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.OneOf(params System.TimeSpan[]! values) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Positive() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Zero() -> JustDummies.AnyTimeSpan! -JustDummies.AnyUInt128 -JustDummies.AnyUInt128.Between(System.UInt128 minimum, System.UInt128 maximum) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.DifferentFrom(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.Except(params System.UInt128[]! values) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.Generate() -> System.UInt128 -JustDummies.AnyUInt128.GreaterThan(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.GreaterThanOrEqualTo(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.LessThan(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.LessThanOrEqualTo(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.MultipleOf(System.UInt128 value) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.NonZero() -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.OneOf(params System.UInt128[]! values) -> JustDummies.AnyUInt128! -JustDummies.AnyUInt128.Zero() -> JustDummies.AnyUInt128! -JustDummies.AnyUInt16 -JustDummies.AnyUInt16.Between(ushort minimum, ushort maximum) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.DifferentFrom(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Except(params ushort[]! values) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Generate() -> ushort -JustDummies.AnyUInt16.GreaterThan(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.GreaterThanOrEqualTo(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.LessThan(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.LessThanOrEqualTo(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.MultipleOf(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.NonZero() -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.OneOf(params ushort[]! values) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Zero() -> JustDummies.AnyUInt16! -JustDummies.AnyUInt32 -JustDummies.AnyUInt32.Between(uint minimum, uint maximum) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.DifferentFrom(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Except(params uint[]! values) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Generate() -> uint -JustDummies.AnyUInt32.GreaterThan(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.GreaterThanOrEqualTo(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.LessThan(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.LessThanOrEqualTo(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.MultipleOf(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.NonZero() -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.OneOf(params uint[]! values) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Zero() -> JustDummies.AnyUInt32! -JustDummies.AnyUInt64 -JustDummies.AnyUInt64.Between(ulong minimum, ulong maximum) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.DifferentFrom(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Except(params ulong[]! values) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Generate() -> ulong -JustDummies.AnyUInt64.GreaterThan(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.GreaterThanOrEqualTo(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.LessThan(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.LessThanOrEqualTo(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.MultipleOf(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.NonZero() -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.OneOf(params ulong[]! values) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Zero() -> JustDummies.AnyUInt64! -JustDummies.AnyUri -JustDummies.AnyUri.Ftp() -> JustDummies.AnyFtpUri! -JustDummies.AnyUri.Generate() -> System.Uri! -JustDummies.AnyUri.Mailto() -> JustDummies.AnyMailtoUri! -JustDummies.AnyUri.Relative() -> JustDummies.AnyRelativeUri! -JustDummies.AnyUri.Web() -> JustDummies.AnyWebUri! -JustDummies.AnyUri.WebSocket() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri -JustDummies.AnyWebSocketUri.Generate() -> System.Uri! -JustDummies.AnyWebSocketUri.UsingWs() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.UsingWss() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithHost(string! host) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithoutPath() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPathSegments(int count) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPort() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPort(int port) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithQuery() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebUri -JustDummies.AnyWebUri.Generate() -> System.Uri! -JustDummies.AnyWebUri.UsingHttp() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.UsingHttps() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithFragment() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithHost(string! host) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithoutPath() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPathSegments(int count) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPort() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPort(int port) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithQuery() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo(string! user) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo(string! user, string! password) -> JustDummies.AnyWebUri! -JustDummies.ConflictingAnyConstraintException -JustDummies.ConflictingAnyConstraintException.ConflictingAnyConstraintException(string! message) -> void -JustDummies.DummyException -JustDummies.DummyException.DummyException(string! message) -> void -JustDummies.DummyException.DummyException(string! message, System.Exception! innerException) -> void -JustDummies.IAny -JustDummies.IAny.Generate() -> T -JustDummies.NullableExtensions -JustDummies.NullableReferenceExtensions -JustDummies.UnsupportedRegexException -JustDummies.UnsupportedRegexException.UnsupportedRegexException(string! message) -> void -static JustDummies.Any.ArrayOf(JustDummies.IAny! item) -> JustDummies.AnyArray! -static JustDummies.Any.Boolean() -> JustDummies.AnyBoolean! -static JustDummies.Any.Byte() -> JustDummies.AnyByte! -static JustDummies.Any.Char() -> JustDummies.AnyChar! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, JustDummies.IAny! seventh, JustDummies.IAny! eighth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, JustDummies.IAny! seventh, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.DateOnly() -> JustDummies.AnyDateOnly! -static JustDummies.Any.DateTime() -> JustDummies.AnyDateTime! -static JustDummies.Any.DateTimeOffset() -> JustDummies.AnyDateTimeOffset! -static JustDummies.Any.Decimal() -> JustDummies.AnyDecimal! -static JustDummies.Any.DictionaryOf(JustDummies.IAny! keys, JustDummies.IAny! values) -> JustDummies.AnyDictionary! -static JustDummies.Any.DictionaryOf(JustDummies.IAny! keys, JustDummies.IAny! values, System.Collections.Generic.IEqualityComparer! keyComparer) -> JustDummies.AnyDictionary! -static JustDummies.Any.Double() -> JustDummies.AnyDouble! -static JustDummies.Any.ElementOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.Enum() -> JustDummies.AnyEnum! -static JustDummies.Any.Guid() -> JustDummies.AnyGuid! -static JustDummies.Any.Half() -> JustDummies.AnyHalf! -static JustDummies.Any.Int128() -> JustDummies.AnyInt128! -static JustDummies.Any.Int16() -> JustDummies.AnyInt16! -static JustDummies.Any.Int32() -> JustDummies.AnyInt32! -static JustDummies.Any.Int64() -> JustDummies.AnyInt64! -static JustDummies.Any.ListOf(JustDummies.IAny! item) -> JustDummies.AnyList! -static JustDummies.Any.OneOf(params T[]! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.PairOf(JustDummies.IAny! first, JustDummies.IAny! second) -> JustDummies.IAny<(T1, T2)>! -static JustDummies.Any.Reproducibly(int seed, System.Action! body, System.Action? report = null) -> void -static JustDummies.Any.Reproducibly(System.Action! body, System.Action? report = null) -> void -static JustDummies.Any.ReproduciblyAsync(int seed, System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task! -static JustDummies.Any.ReproduciblyAsync(System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task! -static JustDummies.Any.SByte() -> JustDummies.AnySByte! -static JustDummies.Any.SequenceOf(JustDummies.IAny! item) -> JustDummies.AnySequence! -static JustDummies.Any.SetOf(JustDummies.IAny! item) -> JustDummies.AnySet! -static JustDummies.Any.SetOf(JustDummies.IAny! item, System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnySet! -static JustDummies.Any.Single() -> JustDummies.AnySingle! -static JustDummies.Any.String() -> JustDummies.AnyString! -static JustDummies.Any.StringMatching(string! pattern) -> JustDummies.AnyPattern! -static JustDummies.Any.StringMatching(System.Text.RegularExpressions.Regex! pattern) -> JustDummies.AnyPattern! -static JustDummies.Any.TimeOnly() -> JustDummies.AnyTimeOnly! -static JustDummies.Any.TimeSpan() -> JustDummies.AnyTimeSpan! -static JustDummies.Any.TripleOf(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third) -> JustDummies.IAny<(T1, T2, T3)>! -static JustDummies.Any.UInt128() -> JustDummies.AnyUInt128! -static JustDummies.Any.UInt16() -> JustDummies.AnyUInt16! -static JustDummies.Any.UInt32() -> JustDummies.AnyUInt32! -static JustDummies.Any.UInt64() -> JustDummies.AnyUInt64! -static JustDummies.Any.Uri() -> JustDummies.AnyUri! -static JustDummies.Any.UseSeed(int seed) -> System.IDisposable! -static JustDummies.Any.UseSeed(int seed, string! replaySnippet) -> System.IDisposable! -static JustDummies.Any.WithSeed(int seed) -> JustDummies.AnyContext! -static JustDummies.AnyExtensions.As(this JustDummies.IAny! generator, System.Func! factory) -> JustDummies.IAny! -static JustDummies.NullableExtensions.OrNull(this JustDummies.IAny! generator) -> JustDummies.IAny! -static JustDummies.NullableReferenceExtensions.OrNull(this JustDummies.IAny! generator) -> JustDummies.IAny! diff --git a/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c581..00000000 --- a/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 74305ac1..00000000 --- a/JustDummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,429 +0,0 @@ -#nullable enable -JustDummies.Any -JustDummies.AnyArray -JustDummies.AnyArray.Distinct() -> JustDummies.AnyArray! -JustDummies.AnyArray.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnyArray! -JustDummies.AnyBoolean -JustDummies.AnyBoolean.DifferentFrom(bool value) -> JustDummies.AnyBoolean! -JustDummies.AnyBoolean.False() -> JustDummies.AnyBoolean! -JustDummies.AnyBoolean.Generate() -> bool -JustDummies.AnyBoolean.True() -> JustDummies.AnyBoolean! -JustDummies.AnyByte -JustDummies.AnyByte.Between(byte minimum, byte maximum) -> JustDummies.AnyByte! -JustDummies.AnyByte.DifferentFrom(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.Except(params byte[]! values) -> JustDummies.AnyByte! -JustDummies.AnyByte.Generate() -> byte -JustDummies.AnyByte.GreaterThan(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.GreaterThanOrEqualTo(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.LessThan(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.LessThanOrEqualTo(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.MultipleOf(byte value) -> JustDummies.AnyByte! -JustDummies.AnyByte.NonZero() -> JustDummies.AnyByte! -JustDummies.AnyByte.OneOf(params byte[]! values) -> JustDummies.AnyByte! -JustDummies.AnyByte.Zero() -> JustDummies.AnyByte! -JustDummies.AnyChar -JustDummies.AnyChar.Alpha() -> JustDummies.AnyChar! -JustDummies.AnyChar.AlphaNumeric() -> JustDummies.AnyChar! -JustDummies.AnyChar.DifferentFrom(char value) -> JustDummies.AnyChar! -JustDummies.AnyChar.Except(params char[]! values) -> JustDummies.AnyChar! -JustDummies.AnyChar.Generate() -> char -JustDummies.AnyChar.LowerCase() -> JustDummies.AnyChar! -JustDummies.AnyChar.Numeric() -> JustDummies.AnyChar! -JustDummies.AnyChar.OneOf(params char[]! values) -> JustDummies.AnyChar! -JustDummies.AnyChar.UpperCase() -> JustDummies.AnyChar! -JustDummies.AnyCollection -JustDummies.AnyCollection.Containing(TItem value) -> TSelf! -JustDummies.AnyCollection.ContainingAny(JustDummies.IAny! generator) -> TSelf! -JustDummies.AnyCollection.Empty() -> TSelf! -JustDummies.AnyCollection.Generate() -> TResult -JustDummies.AnyCollection.NonEmpty() -> TSelf! -JustDummies.AnyCollection.WithCount(int count) -> TSelf! -JustDummies.AnyCollection.WithCountBetween(int minimum, int maximum) -> TSelf! -JustDummies.AnyCollection.WithMaxCount(int count) -> TSelf! -JustDummies.AnyCollection.WithMinCount(int count) -> TSelf! -JustDummies.AnyContext -JustDummies.AnyContext.Boolean() -> JustDummies.AnyBoolean! -JustDummies.AnyContext.Byte() -> JustDummies.AnyByte! -JustDummies.AnyContext.Char() -> JustDummies.AnyChar! -JustDummies.AnyContext.DateTime() -> JustDummies.AnyDateTime! -JustDummies.AnyContext.DateTimeOffset() -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyContext.Decimal() -> JustDummies.AnyDecimal! -JustDummies.AnyContext.Double() -> JustDummies.AnyDouble! -JustDummies.AnyContext.ElementOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.Enum() -> JustDummies.AnyEnum! -JustDummies.AnyContext.Guid() -> JustDummies.AnyGuid! -JustDummies.AnyContext.Int16() -> JustDummies.AnyInt16! -JustDummies.AnyContext.Int32() -> JustDummies.AnyInt32! -JustDummies.AnyContext.Int64() -> JustDummies.AnyInt64! -JustDummies.AnyContext.OneOf(params T[]! values) -> JustDummies.AnyOneOf! -JustDummies.AnyContext.SByte() -> JustDummies.AnySByte! -JustDummies.AnyContext.Seed.get -> int -JustDummies.AnyContext.Single() -> JustDummies.AnySingle! -JustDummies.AnyContext.String() -> JustDummies.AnyString! -JustDummies.AnyContext.StringMatching(string! pattern) -> JustDummies.AnyPattern! -JustDummies.AnyContext.StringMatching(System.Text.RegularExpressions.Regex! pattern) -> JustDummies.AnyPattern! -JustDummies.AnyContext.TimeSpan() -> JustDummies.AnyTimeSpan! -JustDummies.AnyContext.UInt16() -> JustDummies.AnyUInt16! -JustDummies.AnyContext.UInt32() -> JustDummies.AnyUInt32! -JustDummies.AnyContext.UInt64() -> JustDummies.AnyUInt64! -JustDummies.AnyContext.Uri() -> JustDummies.AnyUri! -JustDummies.AnyDateTime -JustDummies.AnyDateTime.After(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.AfterOrEqualTo(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Before(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.BeforeOrEqualTo(System.DateTime instant) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Between(System.DateTime start, System.DateTime end) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.DifferentFrom(System.DateTime value) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Except(params System.DateTime[]! values) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.Generate() -> System.DateTime -JustDummies.AnyDateTime.OneOf(params System.DateTime[]! values) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTime.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyDateTime! -JustDummies.AnyDateTimeOffset -JustDummies.AnyDateTimeOffset.After(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.AfterOrEqualTo(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Before(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.BeforeOrEqualTo(System.DateTimeOffset instant) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Between(System.DateTimeOffset start, System.DateTimeOffset end) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.DifferentFrom(System.DateTimeOffset value) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Except(params System.DateTimeOffset[]! values) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.Generate() -> System.DateTimeOffset -JustDummies.AnyDateTimeOffset.OneOf(params System.DateTimeOffset[]! values) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithOffset(System.TimeSpan offset) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDateTimeOffset.WithOffsetBetween(System.TimeSpan minimum, System.TimeSpan maximum) -> JustDummies.AnyDateTimeOffset! -JustDummies.AnyDecimal -JustDummies.AnyDecimal.Between(decimal minimum, decimal maximum) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.DifferentFrom(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Except(params decimal[]! values) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Generate() -> decimal -JustDummies.AnyDecimal.GreaterThan(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.GreaterThanOrEqualTo(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.LessThan(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.LessThanOrEqualTo(decimal value) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Negative() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.NonZero() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.OneOf(params decimal[]! values) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Positive() -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.WithScale(int scale) -> JustDummies.AnyDecimal! -JustDummies.AnyDecimal.Zero() -> JustDummies.AnyDecimal! -JustDummies.AnyDictionary -JustDummies.AnyDictionary.ContainingAnyKey(JustDummies.IAny! generator) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.ContainingEntry(TKey key, TValue value) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.ContainingKey(TKey key) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.Empty() -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary! -JustDummies.AnyDictionary.NonEmpty() -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithCountBetween(int minimum, int maximum) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithMaxCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDictionary.WithMinCount(int count) -> JustDummies.AnyDictionary! -JustDummies.AnyDouble -JustDummies.AnyDouble.Between(double minimum, double maximum) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.DifferentFrom(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Except(params double[]! values) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Generate() -> double -JustDummies.AnyDouble.GreaterThan(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.GreaterThanOrEqualTo(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.LessThan(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.LessThanOrEqualTo(double value) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Negative() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.NonZero() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.OneOf(params double[]! values) -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Positive() -> JustDummies.AnyDouble! -JustDummies.AnyDouble.Zero() -> JustDummies.AnyDouble! -JustDummies.AnyEnum -JustDummies.AnyEnum.AllowingCombinations() -> JustDummies.AnyEnum! -JustDummies.AnyEnum.DifferentFrom(TEnum value) -> JustDummies.AnyEnum! -JustDummies.AnyEnum.Except(params TEnum[]! values) -> JustDummies.AnyEnum! -JustDummies.AnyEnum.Generate() -> TEnum -JustDummies.AnyEnum.OneOf(params TEnum[]! values) -> JustDummies.AnyEnum! -JustDummies.AnyExtensions -JustDummies.AnyFtpUri -JustDummies.AnyFtpUri.Generate() -> System.Uri! -JustDummies.AnyFtpUri.WithHost(string! host) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithoutPath() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPathSegments(int count) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPort() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithPort(int port) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo() -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo(string! user) -> JustDummies.AnyFtpUri! -JustDummies.AnyFtpUri.WithUserInfo(string! user, string! password) -> JustDummies.AnyFtpUri! -JustDummies.AnyGenerationException -JustDummies.AnyGenerationException.AnyGenerationException(string! message) -> void -JustDummies.AnyGenerationException.AnyGenerationException(string! message, System.Exception! innerException) -> void -JustDummies.AnyGenerationException.Seed.get -> int? -JustDummies.AnyGuid -JustDummies.AnyGuid.DifferentFrom(System.Guid value) -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Empty() -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Except(params System.Guid[]! values) -> JustDummies.AnyGuid! -JustDummies.AnyGuid.Generate() -> System.Guid -JustDummies.AnyGuid.NonEmpty() -> JustDummies.AnyGuid! -JustDummies.AnyGuid.OneOf(params System.Guid[]! values) -> JustDummies.AnyGuid! -JustDummies.AnyInt16 -JustDummies.AnyInt16.Between(short minimum, short maximum) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.DifferentFrom(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Except(params short[]! values) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Generate() -> short -JustDummies.AnyInt16.GreaterThan(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.GreaterThanOrEqualTo(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.LessThan(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.LessThanOrEqualTo(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.MultipleOf(short value) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Negative() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.NonZero() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.OneOf(params short[]! values) -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Positive() -> JustDummies.AnyInt16! -JustDummies.AnyInt16.Zero() -> JustDummies.AnyInt16! -JustDummies.AnyInt32 -JustDummies.AnyInt32.Between(int minimum, int maximum) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.DifferentFrom(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Except(params int[]! values) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Generate() -> int -JustDummies.AnyInt32.GreaterThan(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.GreaterThanOrEqualTo(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.LessThan(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.LessThanOrEqualTo(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.MultipleOf(int value) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Negative() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.NonZero() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.OneOf(params int[]! values) -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Positive() -> JustDummies.AnyInt32! -JustDummies.AnyInt32.Zero() -> JustDummies.AnyInt32! -JustDummies.AnyInt64 -JustDummies.AnyInt64.Between(long minimum, long maximum) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.DifferentFrom(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Except(params long[]! values) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Generate() -> long -JustDummies.AnyInt64.GreaterThan(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.GreaterThanOrEqualTo(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.LessThan(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.LessThanOrEqualTo(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.MultipleOf(long value) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Negative() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.NonZero() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.OneOf(params long[]! values) -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Positive() -> JustDummies.AnyInt64! -JustDummies.AnyInt64.Zero() -> JustDummies.AnyInt64! -JustDummies.AnyList -JustDummies.AnyList.Distinct() -> JustDummies.AnyList! -JustDummies.AnyList.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnyList! -JustDummies.AnyMailtoUri -JustDummies.AnyMailtoUri.Generate() -> System.Uri! -JustDummies.AnyMailtoUri.WithDomain(string! domain) -> JustDummies.AnyMailtoUri! -JustDummies.AnyMailtoUri.WithHeaders() -> JustDummies.AnyMailtoUri! -JustDummies.AnyMailtoUri.WithLocalPart(string! localPart) -> JustDummies.AnyMailtoUri! -JustDummies.AnyOneOf -JustDummies.AnyOneOf.DifferentFrom(T value) -> JustDummies.AnyOneOf! -JustDummies.AnyOneOf.Except(params T[]! values) -> JustDummies.AnyOneOf! -JustDummies.AnyOneOf.Generate() -> T -JustDummies.AnyPattern -JustDummies.AnyPattern.DifferentFrom(string! value) -> JustDummies.AnyPattern! -JustDummies.AnyPattern.Except(params string![]! values) -> JustDummies.AnyPattern! -JustDummies.AnyPattern.Generate() -> string! -JustDummies.AnyRelativeUri -JustDummies.AnyRelativeUri.Generate() -> System.Uri! -JustDummies.AnyRelativeUri.Rooted() -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithFragment() -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithPathSegments(int count) -> JustDummies.AnyRelativeUri! -JustDummies.AnyRelativeUri.WithQuery() -> JustDummies.AnyRelativeUri! -JustDummies.AnySByte -JustDummies.AnySByte.Between(sbyte minimum, sbyte maximum) -> JustDummies.AnySByte! -JustDummies.AnySByte.DifferentFrom(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.Except(params sbyte[]! values) -> JustDummies.AnySByte! -JustDummies.AnySByte.Generate() -> sbyte -JustDummies.AnySByte.GreaterThan(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.GreaterThanOrEqualTo(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.LessThan(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.LessThanOrEqualTo(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.MultipleOf(sbyte value) -> JustDummies.AnySByte! -JustDummies.AnySByte.Negative() -> JustDummies.AnySByte! -JustDummies.AnySByte.NonZero() -> JustDummies.AnySByte! -JustDummies.AnySByte.OneOf(params sbyte[]! values) -> JustDummies.AnySByte! -JustDummies.AnySByte.Positive() -> JustDummies.AnySByte! -JustDummies.AnySByte.Zero() -> JustDummies.AnySByte! -JustDummies.AnySequence -JustDummies.AnySequence.Distinct() -> JustDummies.AnySequence! -JustDummies.AnySequence.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnySequence! -JustDummies.AnySet -JustDummies.AnySingle -JustDummies.AnySingle.Between(float minimum, float maximum) -> JustDummies.AnySingle! -JustDummies.AnySingle.DifferentFrom(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.Except(params float[]! values) -> JustDummies.AnySingle! -JustDummies.AnySingle.Generate() -> float -JustDummies.AnySingle.GreaterThan(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.GreaterThanOrEqualTo(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.LessThan(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.LessThanOrEqualTo(float value) -> JustDummies.AnySingle! -JustDummies.AnySingle.Negative() -> JustDummies.AnySingle! -JustDummies.AnySingle.NonZero() -> JustDummies.AnySingle! -JustDummies.AnySingle.OneOf(params float[]! values) -> JustDummies.AnySingle! -JustDummies.AnySingle.Positive() -> JustDummies.AnySingle! -JustDummies.AnySingle.Zero() -> JustDummies.AnySingle! -JustDummies.AnyString -JustDummies.AnyString.Alpha() -> JustDummies.AnyString! -JustDummies.AnyString.AlphaNumeric() -> JustDummies.AnyString! -JustDummies.AnyString.Containing(string! value) -> JustDummies.AnyString! -JustDummies.AnyString.DifferentFrom(string! value) -> JustDummies.AnyString! -JustDummies.AnyString.EndingWith(string! suffix) -> JustDummies.AnyString! -JustDummies.AnyString.Except(params string![]! values) -> JustDummies.AnyString! -JustDummies.AnyString.Generate() -> string! -JustDummies.AnyString.LowerCase() -> JustDummies.AnyString! -JustDummies.AnyString.NonEmpty() -> JustDummies.AnyString! -JustDummies.AnyString.Numeric() -> JustDummies.AnyString! -JustDummies.AnyString.OneOf(params string![]! values) -> JustDummies.AnyString! -JustDummies.AnyString.OneOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyString! -JustDummies.AnyString.StartingWith(string! prefix) -> JustDummies.AnyString! -JustDummies.AnyString.UpperCase() -> JustDummies.AnyString! -JustDummies.AnyString.WithChars(string! pool) -> JustDummies.AnyString! -JustDummies.AnyString.WithLength(int length) -> JustDummies.AnyString! -JustDummies.AnyString.WithLengthBetween(int minimum, int maximum) -> JustDummies.AnyString! -JustDummies.AnyString.WithMaxLength(int length) -> JustDummies.AnyString! -JustDummies.AnyString.WithMinLength(int length) -> JustDummies.AnyString! -JustDummies.AnyTimeSpan -JustDummies.AnyTimeSpan.Between(System.TimeSpan minimum, System.TimeSpan maximum) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.DifferentFrom(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Except(params System.TimeSpan[]! values) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Generate() -> System.TimeSpan -JustDummies.AnyTimeSpan.GreaterThan(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.GreaterThanOrEqualTo(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.LessThan(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.LessThanOrEqualTo(System.TimeSpan value) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Negative() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.NonZero() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.OneOf(params System.TimeSpan[]! values) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Positive() -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.WithGranularity(System.TimeSpan granularity) -> JustDummies.AnyTimeSpan! -JustDummies.AnyTimeSpan.Zero() -> JustDummies.AnyTimeSpan! -JustDummies.AnyUInt16 -JustDummies.AnyUInt16.Between(ushort minimum, ushort maximum) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.DifferentFrom(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Except(params ushort[]! values) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Generate() -> ushort -JustDummies.AnyUInt16.GreaterThan(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.GreaterThanOrEqualTo(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.LessThan(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.LessThanOrEqualTo(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.MultipleOf(ushort value) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.NonZero() -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.OneOf(params ushort[]! values) -> JustDummies.AnyUInt16! -JustDummies.AnyUInt16.Zero() -> JustDummies.AnyUInt16! -JustDummies.AnyUInt32 -JustDummies.AnyUInt32.Between(uint minimum, uint maximum) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.DifferentFrom(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Except(params uint[]! values) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Generate() -> uint -JustDummies.AnyUInt32.GreaterThan(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.GreaterThanOrEqualTo(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.LessThan(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.LessThanOrEqualTo(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.MultipleOf(uint value) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.NonZero() -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.OneOf(params uint[]! values) -> JustDummies.AnyUInt32! -JustDummies.AnyUInt32.Zero() -> JustDummies.AnyUInt32! -JustDummies.AnyUInt64 -JustDummies.AnyUInt64.Between(ulong minimum, ulong maximum) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.DifferentFrom(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Except(params ulong[]! values) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Generate() -> ulong -JustDummies.AnyUInt64.GreaterThan(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.GreaterThanOrEqualTo(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.LessThan(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.LessThanOrEqualTo(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.MultipleOf(ulong value) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.NonZero() -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.OneOf(params ulong[]! values) -> JustDummies.AnyUInt64! -JustDummies.AnyUInt64.Zero() -> JustDummies.AnyUInt64! -JustDummies.AnyUri -JustDummies.AnyUri.Ftp() -> JustDummies.AnyFtpUri! -JustDummies.AnyUri.Generate() -> System.Uri! -JustDummies.AnyUri.Mailto() -> JustDummies.AnyMailtoUri! -JustDummies.AnyUri.Relative() -> JustDummies.AnyRelativeUri! -JustDummies.AnyUri.Web() -> JustDummies.AnyWebUri! -JustDummies.AnyUri.WebSocket() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri -JustDummies.AnyWebSocketUri.Generate() -> System.Uri! -JustDummies.AnyWebSocketUri.UsingWs() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.UsingWss() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithHost(string! host) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithoutPath() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPathSegments(int count) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPort() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithPort(int port) -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebSocketUri.WithQuery() -> JustDummies.AnyWebSocketUri! -JustDummies.AnyWebUri -JustDummies.AnyWebUri.Generate() -> System.Uri! -JustDummies.AnyWebUri.UsingHttp() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.UsingHttps() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithFragment() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithHost(string! host) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithoutPath() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPathSegments(int count) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPort() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithPort(int port) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithQuery() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo() -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo(string! user) -> JustDummies.AnyWebUri! -JustDummies.AnyWebUri.WithUserInfo(string! user, string! password) -> JustDummies.AnyWebUri! -JustDummies.ConflictingAnyConstraintException -JustDummies.ConflictingAnyConstraintException.ConflictingAnyConstraintException(string! message) -> void -JustDummies.DummyException -JustDummies.DummyException.DummyException(string! message) -> void -JustDummies.DummyException.DummyException(string! message, System.Exception! innerException) -> void -JustDummies.IAny -JustDummies.IAny.Generate() -> T -JustDummies.NullableExtensions -JustDummies.NullableReferenceExtensions -JustDummies.UnsupportedRegexException -JustDummies.UnsupportedRegexException.UnsupportedRegexException(string! message) -> void -static JustDummies.Any.ArrayOf(JustDummies.IAny! item) -> JustDummies.AnyArray! -static JustDummies.Any.Boolean() -> JustDummies.AnyBoolean! -static JustDummies.Any.Byte() -> JustDummies.AnyByte! -static JustDummies.Any.Char() -> JustDummies.AnyChar! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, JustDummies.IAny! seventh, JustDummies.IAny! eighth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, JustDummies.IAny! seventh, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, JustDummies.IAny! sixth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, JustDummies.IAny! fifth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, JustDummies.IAny! fourth, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.Combine(JustDummies.IAny! first, JustDummies.IAny! second, System.Func! compose) -> JustDummies.IAny! -static JustDummies.Any.DateTime() -> JustDummies.AnyDateTime! -static JustDummies.Any.DateTimeOffset() -> JustDummies.AnyDateTimeOffset! -static JustDummies.Any.Decimal() -> JustDummies.AnyDecimal! -static JustDummies.Any.DictionaryOf(JustDummies.IAny! keys, JustDummies.IAny! values) -> JustDummies.AnyDictionary! -static JustDummies.Any.DictionaryOf(JustDummies.IAny! keys, JustDummies.IAny! values, System.Collections.Generic.IEqualityComparer! keyComparer) -> JustDummies.AnyDictionary! -static JustDummies.Any.Double() -> JustDummies.AnyDouble! -static JustDummies.Any.ElementOf(System.Collections.Generic.IEnumerable! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.Enum() -> JustDummies.AnyEnum! -static JustDummies.Any.Guid() -> JustDummies.AnyGuid! -static JustDummies.Any.Int16() -> JustDummies.AnyInt16! -static JustDummies.Any.Int32() -> JustDummies.AnyInt32! -static JustDummies.Any.Int64() -> JustDummies.AnyInt64! -static JustDummies.Any.ListOf(JustDummies.IAny! item) -> JustDummies.AnyList! -static JustDummies.Any.OneOf(params T[]! values) -> JustDummies.AnyOneOf! -static JustDummies.Any.PairOf(JustDummies.IAny! first, JustDummies.IAny! second) -> JustDummies.IAny<(T1, T2)>! -static JustDummies.Any.Reproducibly(int seed, System.Action! body, System.Action? report = null) -> void -static JustDummies.Any.Reproducibly(System.Action! body, System.Action? report = null) -> void -static JustDummies.Any.ReproduciblyAsync(int seed, System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task! -static JustDummies.Any.ReproduciblyAsync(System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task! -static JustDummies.Any.SByte() -> JustDummies.AnySByte! -static JustDummies.Any.SequenceOf(JustDummies.IAny! item) -> JustDummies.AnySequence! -static JustDummies.Any.SetOf(JustDummies.IAny! item) -> JustDummies.AnySet! -static JustDummies.Any.SetOf(JustDummies.IAny! item, System.Collections.Generic.IEqualityComparer! comparer) -> JustDummies.AnySet! -static JustDummies.Any.Single() -> JustDummies.AnySingle! -static JustDummies.Any.String() -> JustDummies.AnyString! -static JustDummies.Any.StringMatching(string! pattern) -> JustDummies.AnyPattern! -static JustDummies.Any.StringMatching(System.Text.RegularExpressions.Regex! pattern) -> JustDummies.AnyPattern! -static JustDummies.Any.TimeSpan() -> JustDummies.AnyTimeSpan! -static JustDummies.Any.TripleOf(JustDummies.IAny! first, JustDummies.IAny! second, JustDummies.IAny! third) -> JustDummies.IAny<(T1, T2, T3)>! -static JustDummies.Any.UInt16() -> JustDummies.AnyUInt16! -static JustDummies.Any.UInt32() -> JustDummies.AnyUInt32! -static JustDummies.Any.UInt64() -> JustDummies.AnyUInt64! -static JustDummies.Any.Uri() -> JustDummies.AnyUri! -static JustDummies.Any.UseSeed(int seed) -> System.IDisposable! -static JustDummies.Any.UseSeed(int seed, string! replaySnippet) -> System.IDisposable! -static JustDummies.Any.WithSeed(int seed) -> JustDummies.AnyContext! -static JustDummies.AnyExtensions.As(this JustDummies.IAny! generator, System.Func! factory) -> JustDummies.IAny! -static JustDummies.NullableExtensions.OrNull(this JustDummies.IAny! generator) -> JustDummies.IAny! -static JustDummies.NullableReferenceExtensions.OrNull(this JustDummies.IAny! generator) -> JustDummies.IAny! diff --git a/JustDummies/README.nuget.md b/JustDummies/README.nuget.md deleted file mode 100644 index 42d55561..00000000 --- a/JustDummies/README.nuget.md +++ /dev/null @@ -1,189 +0,0 @@ -# JustDummies - -A fluent DSL for generating arbitrary yet **valid** test values — *dummies*: values a -test needs but never asserts on. - -Website: [justdummies.io](https://justdummies.io) - -## The idea - -A test's `Arrange` is full of values the test does not check: an order reference, a -quantity, a label. A hand-picked literal reads as significant even when it is not. -`JustDummies` makes the incidental legible as incidental — and, when the value must cross -an invariant (a value object, a contract precondition), the constraints express *that -invariant*, never what the test asserts: - - string code = Any.String() - .NonEmpty() - .WithMaxLength(50) - .StartingWith("ORD-") - .Generate(); - -Read it as: *any* string that satisfies these constraints. The exact value does not -matter — and that is the point. - -## What's inside - -- **Fluent, typed generators** implementing `IAny`, materialized through - `.Generate()`, across the .NET simple types: `String`, `Char`, every integer - width (`SByte`/`Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Int64`/`UInt64`), - `Double`/`Single`/`Decimal` (finite values only — never NaN or infinities), - `Boolean`, `Guid`, `Enum` (declared members only — a `[Flags]` enum widens to - every combination with `AllowingCombinations()`), `TimeSpan`, `DateTime` (UTC) - and `DateTimeOffset`. On modern targets (`net8.0`) the surface extends to - `DateOnly`, `TimeOnly`, `Int128`, `UInt128` and `Half`; the package also targets - `netstandard2.0` and runs on **.NET Framework 4.7.2+**, .NET Core 2.0+ and .NET 5+ - for the widest reach — with the .NET Framework 4.7.2 floor exercised in CI, not - merely advertised. -- **Strings from a regex**: `Any.StringMatching(pattern)` generates arbitrary strings - that match a regular expression — the dummy for a format-validated value object. - Home-grown (zero dependencies) over the regular subset of the pattern language; a - non-regular construct (a lookaround, a backreference) is refused with a clear error - rather than a silently non-matching value. The pattern is the whole shape — express a - length or a prefix inside it, since building a value in the intersection of two regular - languages is not something the library does — but the exclusion pair is there: - `Any.StringMatching(@"^ORD-\d{8}$").DifferentFrom(existing)` never yields `existing`. -- **Custom alphabets**: `Any.String().WithChars("αβγδε")` draws the string from an - explicit character pool — the general form of the built-in `Alpha`/`Numeric`/ - `AlphaNumeric` sets, and the way to reach non-ASCII text (accents, Greek, Cyrillic, - CJK) without a `StringMatching` literal. It stays within the Basic Multilingual Plane - and rejects a surrogate: an emoji or other astral character is an atomic grapheme, not - a character family, so draw those as whole strings with `OneOf("😀", "🎉")` instead. - Anchored fragments must be drawn from the pool, or the conflict is reported at - declaration. -- **Strings from an explicit set**: `Any.String().OneOf("EUR", "USD", "GBP")` draws from - a fixed, closed list — the dummy for a value whose domain is a short enumeration (a - currency code, a well-known name). Composable like every other family's `OneOf`: the - other constraints narrow the set rather than shape a string, so - `Any.String().OneOf("abc", "de").WithLength(3)` yields `"abc"` and a constraint no - supplied value satisfies is a conflict naming both sides, whichever order the two were - declared in. Duplicates collapse, and the draw is uniform and reproducible under a - seed. -- **Any value from an explicit pool**: `Any.OneOf(eur, usd, gbp)` draws one value from a - caller-supplied set of arbitrary values or domain objects, and `Any.ElementOf(orders)` - does the same from a collection already held (a list, a LINQ result). This is the - seed-aware answer to "any of these" — replacing a hand-rolled - `pool[new Random().Next(...)]` that would ignore the seed and break `Reproducibly`. - Uniform like the string set: duplicates collapse under the default comparer, the pool's - distinct count gates distinct collections, and a `null` element is refused — make the - whole draw optional with `.OrNull()` instead. The element type is opaque to the - library, so the pool is the whole shape of the specification; what it does offer is the - exclusion pair, and `Any.ElementOf(orders).DifferentFrom(theOneAlreadyUsed)` is the - idiom for drawing *another* element of a fixture. -- **URIs by family**: `Any.Uri()` yields an arbitrary yet valid `System.Uri` — an - absolute web (`http`/`https`), WebSocket (`ws`/`wss`), FTP or mailto URI, or a relative - reference. Narrow it to a family and each returns a builder exposing only that family's - valid components, so an impossible combination cannot even be written (`Mailto()` has no - `WithPort`, `WebSocket()` no `WithUserInfo`): - `Any.Uri().Web().UsingHttps().WithHost("api.example.com")`. Every part is drawn from - ASCII-unreserved characters, so a value is valid by construction and reproducible across - frameworks; internationalized (IDN) hosts and the `file` scheme stay out of the default - draw to keep that determinism. -- **Domain vocabulary where it belongs**: dates constrain with - `After`/`Before`/`Between`, quantities with `Positive`/`Between`/`NonZero`, - identities with `NonEmpty`/`DifferentFrom` — and deliberately no clock-relative - constraints: a reproducible test pins its reference instants explicitly. -- **Values on a grid**: a quantity that must be a whole number of some unit takes - `MultipleOf` — `Any.Int32().Between(0, 100_000).MultipleOf(100)` for an amount in whole - euros held as cents — drawn *on* the grid so the declared range keeps its meaning, - instead of an `As(x => x * 100)` projection that silently distorts it. `Decimal` takes - `WithScale(n)`, a value expressible in `n` decimal places (`WithScale(2)` for a currency - amount) — a *value* lattice (a multiple of `10⁻ⁿ`), not a padded representation. The - temporal generators take `WithGranularity(TimeSpan)` — a round instant or duration - (`WithGranularity(TimeSpan.FromMinutes(15))`) — so tick-precision values never surprise a - serialization round-trip. Each is built in one draw, composes with the bounds and - exclusions, and conflicts eagerly when the range holds no grid point. -- **Offset-aware `DateTimeOffset`**: unconstrained, `Any.DateTimeOffset()` carries offset - `TimeSpan.Zero` (UTC); `WithOffset(TimeSpan)` pins a whole-minute offset (±14:00) and - `WithOffsetBetween(min, max)` draws a bounded one, so offset-sensitive code (local - rendering, offset arithmetic, "same instant, different offset") is actually exercised. The - instant is tightened first, so the value stays valid even at the edges of the range. Combined - with `OneOf(...)`, the declared offset selects which pooled values may be drawn — pooled values - keep their own offset rather than being rewritten — and an offset none of them carries is a - conflict, whichever of the two is declared first. -- **Values built to satisfy the constraints** — a scalar is constructed directly, - never generated-then-filtered. The one exception is excluding values from a string or a - pattern (`Any.String().DifferentFrom(...)`/`Except(...)`, - `Any.StringMatching(p).DifferentFrom(...)`): neither has an ordinal mapping to build the - exclusion into, so it is met by a **bounded** redraw — the same escape a *distinct* - collection uses to skip a duplicate, never an unbounded retry loop. An exclusion tight - enough to leave nothing surfaces at generation as a seed-bearing - `AnyGenerationException`, whose message reports the budget it spent rather than claiming - no value remains — the search is bounded, so it never established that. An exclusion on - an explicit value set needs no redraw at all: the domain is finite, so the values are - removed at declaration and emptying it is a conflict there. -- **Conflicting constraints fail fast** with a clear, actionable - `ConflictingAnyConstraintException` at the moment the conflicting constraint is - declared — for example `Any.String().WithLength(3).StartingWith("ORD-")`. -- **Dummies stay ordinary unless you ask for more.** An unconstrained `Any.Double()`, - `Single()` or `Decimal()` draws within a magnitude of a million, not across the type's - whole domain — so arithmetic on a dummy stays finite, `WithScale(2)` still has decimal - places to constrain, and the value sits where rounding and formatting defects actually - live. The window only ever *clips*: `Between(0, double.MaxValue)` permits a huge value - and still yields an ordinary one, while `Between(1e300, 1e308)` names a magnitude and - gets exactly it. `Half`, whose domain stops at 65 504, is unaffected. The integer - generators deliberately keep their full range — a large `int` is an ordinary `int`. -- **Dummies stay small unless you ask for more.** A bound is a permission, not a - request: `WithMaxLength`/`WithMaxCount` only ever *narrow* a draw, so - `Any.String().WithMaxLength(100_000)` still yields the short unconstrained string - rather than one sized after the cap. Only a minimum, an exact size or a required - fragment enlarges a value — `WithMinLength(90_000)` is how you ask for a large one. - `WithLengthBetween(a, b)` is exactly its two bounds declared separately, so a range - starting at zero reads as a limit, not as a request to spread across it. A size the - generator must actually produce is capped at 1 000 000: past that, - `WithLength`/`WithMinLength`/`WithCount`/`WithMinCount` raise an - `ArgumentOutOfRangeException` naming your own parameter, instead of hanging or - exhausting memory. A pure maximum is never capped — mirror a four-million-character - column limit if you like; it costs nothing to honour. -- **Composition without reflection**: `.As(factory)` turns a constrained primitive - into a domain value object; `Any.Combine(...)` assembles larger objects through - constructor lambdas — from two up to eight constrained parts. -- **Collections over any element generator**: `Any.ListOf(item)`, `ArrayOf`, - `SequenceOf`, `SetOf` and `DictionaryOf`, constrained with - `WithCount`/`NonEmpty`/`Distinct`/`Containing`. Ask a distinct collection for more - distinct elements than its effective domain — the element generator plus any values - pinned outside it with `Containing` — can supply, and it fails fast, just like any - other conflict, wherever that domain is countable; where it is not, the same - shortfall instead surfaces at generation as an `AnyGenerationException` naming the - seed to replay. `Any.PairOf`/`TripleOf` pair generators into value tuples. -- **Optional values**: `.OrNull()` turns any generator into one that is `null` about - half the time and otherwise a constrained value — the dummy for an optional field, - for value types (`int?`, `Guid?`, ...) and reference types alike. -- **Reproducible runs**: wrap a test in `Any.Reproducibly(...)` and a failing run - reports the seed to replay; `Any.WithSeed(seed)` gives an isolated, deterministic - context; `Any.UseSeed(seed)` pins the ambient one until the handle is disposed, for - a caller that has no body to wrap — a test-framework adapter driving the seed from - before/after hooks. Its second overload names what the reader must write to replay, - so a run pinned from outside the test body never points at a call the test does not - contain. Drawing from several threads at once is safe — values stay arbitrary and - well-formed — but concurrent draws interleave, so a seed replays a run only while its - draws are taken one at a time; open an `Any.UseSeed(...)` scope per unit of work to - keep a parallel run reproducible. - -## Example - - using JustDummies; - - OrderReference reference = Any.String() - .StartingWith("ORD-") - .WithLength(12) - .As(OrderReference.Create) - .Generate(); - -## What it is not - -No realistic fake data (names, emails, addresses), no object-graph auto-filling, no -reflection. Small, deterministic, explicit. - -And not a source of security material. Every draw comes from a seeded `System.Random`, -because a dummy is only worth generating if the seed a failing run reports replays it — -the very property that makes the sequence predictable to anyone who learns the seed. -Never draw a password, token, key, salt, nonce, or any identifier that has to be -unguessable from `Any.*`; reach for -`System.Security.Cryptography.RandomNumberGenerator` for those. - -## Documentation - -Full documentation on GitHub: - -https://github.com/Reefact/first-class-errors diff --git a/JustDummies/RandomSource.cs b/JustDummies/RandomSource.cs deleted file mode 100644 index 915d8fcc..00000000 --- a/JustDummies/RandomSource.cs +++ /dev/null @@ -1,362 +0,0 @@ -namespace JustDummies; - -/// -/// The random context a generator draws from when it generates: a pseudo-random generator paired with the seed -/// that created it, so any failure can name the seed that replays the run. Generators hold a -/// and resolve it at time — never at construction -/// time — which is what lets a recipe built outside an Any.Reproducibly(...) scope generate -/// deterministically inside one. -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", - Justification = - "The abstract class is the root of a closed, internal hierarchy and is deliberately not an interface: a class cannot be " + - "implemented from outside the assembly, keeps the option of adding shared state without breaking every subtype, and on the " + - "netstandard2.0 leg cannot be replaced by an interface with non-public members.")] -internal abstract class RandomSource { - - /// The seeded generator to draw from right now. Every draw goes through it, serialized on its own lock. - internal abstract SeededRandom Current { get; } - - /// - /// The reproduction guidance to append to a generation-failure message, phrased for this kind of source: one - /// sentence, which embeds a replay snippet — the code the reader copies. The two words are - /// a whole and its part, and they are never interchangeable: guidance is the sentence, a snippet is the - /// fragment it names. - /// - /// - /// Which snippet the sentence names depends on how the run was pinned, and getting that wrong is the whole - /// point of this method existing. The ambient source names Any.Reproducibly(seed, ...) — or whatever - /// snippet the opener of the current scope supplied, since a run pinned - /// by a test-framework adapter is replayed by changing what the adapter reads, not by adding a call the test - /// never had. A fixed Any.WithSeed(...) context replays deterministically on its own, so pinning the - /// ambient source would not apply. Naming a snippet the reader's code does not contain is exactly the - /// misleading diagnostic this method exists to avoid. - /// - internal abstract string ReplayGuidance(int seed); - - /// - /// The reproduction guidance for a failure whose seeded draws this source drove but whose result also depends on - /// a generator that does not draw from this source — a foreign , or a derivation built over - /// one (including a Combine that mixes a foreign operand with a sourced one). It names the same replay - /// mechanism as for the seeded part, but scopes the promise to it: the foreign values - /// are not reproducible from this seed alone, so claiming a full replay would be the misleading diagnostic the - /// seed reporting exists to avoid. - /// - internal abstract string PartialReplayGuidance(int seed); - -} - -/// -/// A pseudo-random generator that remembers the seed it was created from, and the only door to it: the -/// underlying is never handed out, so every draw goes through this type and is serialized -/// on its own lock. -/// -/// -/// -/// is not thread-safe, and a source reaches several threads by two ordinary routes: the -/// ambient state flows with the execution context into every task a test spawns, and an -/// is shared by whoever holds it. Left unsynchronized, concurrent draws converge the -/// generator's two internal indices and it returns zero for ever — so every generator settles on the -/// minimum of its declared range (0, "", ) and the source never -/// recovers. Silent, and exactly the values most likely to make an assertion pass for the wrong reason. -/// -/// -/// Keeping the private is what makes the guarantee hold: a synchronized subclass would -/// leak any member left un-overridden, whereas here a draw that bypasses the lock does not compile. An -/// uncontended lock leaves single-threaded sequences bit-identical, so a pinned seed replays exactly as -/// before, and the cost is immaterial on paths that are not hot loops. -/// -/// -/// What this does not buy is a value-level guarantee across threads: the lock is per primitive draw, so -/// two threads interleave inside a multi-draw generation (a string consumes one draw per character). Neither -/// the sequence nor the multiset of generated values is stable under parallelism — see -/// for the per-work-item scope that is. -/// -/// -internal sealed class SeededRandom { - - #region Fields declarations - - private readonly object _gate = new(); - private readonly Random _random; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Vulnerability", "S2245:Pseudorandom number generators (PRNGs) should not be used in security contexts", - Justification = - "S2245 is right that this generator is predictable; that predictability is the type's contract. A dummy is worth " + - "generating only if the seed a failing run reports replays it, and a seeded System.Random is the one BCL generator " + - "whose sequence a recorded seed reproduces. RandomNumberGenerator is seedless by design, so adopting it would delete " + - "Any.Reproducibly, Any.WithSeed and Any.UseSeed outright, along with the seed every generation failure reports. It " + - "would also break a checked contract: justdummies.yml compares the SEEDBATCH banner that tools/justdummies-check draws " + - "from CrossTfmSeed byte-for-byte between the lib/netstandard2.0 and lib/net8.0 assets. No draw in this solution is " + - "security material: SeededRandom is internal and reachable only through the Any.* test-data generators, and " + - "README.nuget.md tells consumers never to draw a secret, key, token or nonce from Any.*.")] - internal SeededRandom(int seed) { - Seed = seed; - _random = new Random(seed); - } - - internal int Seed { get; } - - /// Draws a non-negative below . - internal int Next(int maxExclusive) { - lock (_gate) { return _random.Next(maxExclusive); } - } - - /// Draws an in the half-open range [, ). - internal int Next(int minInclusive, int maxExclusive) { - lock (_gate) { return _random.Next(minInclusive, maxExclusive); } - } - - /// Fills with random bytes. - internal void NextBytes(byte[] buffer) { - if (buffer is null) { throw new ArgumentNullException(nameof(buffer)); } - - lock (_gate) { _random.NextBytes(buffer); } - } - - /// Draws a in the half-open range [0, 1). - internal double NextDouble() { - lock (_gate) { return _random.NextDouble(); } - } - -} - -/// -/// The default random context behind the static entry points. The state is stored in an -/// , so it flows with the current execution context and never leaks across tests -/// running in parallel. Outside an scope it lazily seeds itself with a fresh seed — every -/// run differs, which surfaces a test that secretly depends on a value — and that seed is remembered, so a -/// generation failure can still report it. Inside a scope (how Any.Reproducibly(...) pins a run) it is -/// deterministic. -/// -internal sealed class AmbientRandomSource : RandomSource { - - #region Statics members declarations - - internal static readonly AmbientRandomSource Instance = new(); - - private static readonly AsyncLocal State = new(); - - internal static int NewSeed() { - return Guid.NewGuid().GetHashCode(); - } - - internal static IDisposable UseSeed(int seed) { - return UseSeed(seed, null); - } - - internal static IDisposable UseSeed(int seed, string? replaySnippet) { - AmbientState frame = new(new SeededRandom(seed), replaySnippet, State.Value); - State.Value = frame; - - return new SeedScope(frame); - } - - #endregion - - private AmbientRandomSource() { } - - internal override SeededRandom Current { - get { - AmbientState? current = State.Value; - if (current is null) { - current = new AmbientState(new SeededRandom(NewSeed()), null, null); - State.Value = current; - } - - return current.Random; - } - } - - internal override string ReplayGuidance(int seed) { - return $"The arbitrary values were seeded with {seed}; reproduce this run with {ReplaySnippet(seed)}."; - } - - internal override string PartialReplayGuidance(int seed) { - return $"The seeded draws were made with {seed} ({ReplaySnippet(seed)}), but some values come from a generator that does not draw from this source, so they are not reproducible from this seed alone."; - } - - /// - /// The code the reader copies to replay the current run — the fragment the guidance sentence embeds, never the - /// sentence itself: the snippet the opener of the scope supplied, or the delegate runner when none was. Read - /// from the scope rather than fixed on the source, because the ambient source is pinned by several mechanisms - /// and each is replayed differently. - /// - private static string ReplaySnippet(int seed) { - return State.Value?.Snippet ?? $"Any.Reproducibly({seed}, ...)"; - } - - #region Nested types - - /// - /// One frame of the ambient seed stack a scope installs: the seeded generator, how to replay the run that uses - /// it, and the frame it was pushed on top of. The frames form a linked stack (each points at its - /// ) so a scope disposed out of order can be removed without stranding the ones still open - /// — see . tombstones a frame whose scope has closed but which is - /// not yet the top of the stack, so the top's later disposal can skip past it. - /// - private sealed class AmbientState { - - internal AmbientState(SeededRandom random, string? replaySnippet, AmbientState? parent) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - - Random = random; - Snippet = replaySnippet; - Parent = parent; - } - - internal SeededRandom Random { get; } - - /// - /// The replay snippet the opener of this scope supplied, if any — the fragment, never the whole guidance - /// sentence. Named Snippet rather than ReplaySnippet so it does not shadow the enclosing - /// , which reads it. - /// - internal string? Snippet { get; } - internal AmbientState? Parent { get; } - internal bool Disposed { get; set; } - - } - - /// - /// The handle returned by . Disposal is order-independent: it - /// tombstones its own frame, and only the frame that is currently the top of the stack rewrites the ambient - /// slot — walking past any tombstoned ancestors to the nearest frame whose scope is still open (or to - /// null when none is). So the documented "scopes nest, disposing restores whatever was pinned before" - /// holds even when scopes are disposed out of order: an outer scope closed early strands nothing, and no order - /// leaves a dead seed pinned for whatever runs next. Disposing twice is a no-op. - /// - private sealed class SeedScope : IDisposable { - - private readonly AmbientState _frame; - private bool _disposed; - - internal SeedScope(AmbientState frame) { - if (frame is null) { throw new ArgumentNullException(nameof(frame)); } - - _frame = frame; - } - - public void Dispose() { - if (_disposed) { return; } - - _disposed = true; - _frame.Disposed = true; - - // Only the current top owns the ambient slot; an out-of-order dispose of an inner frame just tombstones - // it and lets the top's own dispose skip it later. - if (ReferenceEquals(State.Value, _frame)) { - AmbientState? restored = _frame.Parent; - while (restored is { Disposed: true }) { restored = restored.Parent; } - State.Value = restored; - } - } - - } - - #endregion - -} - -/// -/// The isolated random context behind : one fixed, seeded generator owned by a single -/// . Unlike the ambient source it does not flow with the execution context — it is -/// deterministic by construction and belongs to whoever holds the context. -/// -internal sealed class FixedRandomSource : RandomSource { - - private readonly SeededRandom _random; - - internal FixedRandomSource(int seed) { - _random = new SeededRandom(seed); - } - - internal override SeededRandom Current => _random; - - internal override string ReplayGuidance(int seed) { - return $"The arbitrary values were drawn from Any.WithSeed({seed}), which already replays deterministically."; - } - - internal override string PartialReplayGuidance(int seed) { - return $"The seeded draws were made from Any.WithSeed({seed}), but some values come from a generator that does not draw from it, so they are not reproducible from this seed alone."; - } - -} - -/// -/// Implemented by the library's own generators so that derived generators (As, Combine) can -/// propagate the random context of their operands, and so that a generation failure can resolve the seed to -/// report. Foreign implementations simply do not carry one, and a derived generator -/// built over a foreign one carries null. -/// -internal interface IHasRandomSource { - - RandomSource? Source { get; } - -} - -/// -/// Implemented by derived generators (As, Combine) to report whether every operand they draw from is -/// itself reproducible. A single source-less (foreign) operand makes the derived value unreproducible even when -/// another operand supplies a non-null for the replay hint to name — so a -/// full-replay promise must be withheld. Generators that draw only from their own source do not implement this and -/// are treated as reproducible whenever they carry a source. -/// -internal interface IReproducibilityHint { - - bool DrawsOnlyFromSource { get; } - -} - -/// Uniform sampling helpers shared by the generators. -internal static class RandomSampling { - - /// - /// Draws a uniform value in the inclusive range [, - /// ]. Unlike the upper bound is reachable, - /// which matters for full-range and boundary draws. The draw maps 8 random bytes onto the range size; the - /// modulo bias is at most 2^-32 for the ranges an can express — irrelevant for arbitrary - /// test values. Deliberately NOT named NextInt64: on the net8.0 leg the framework's own - /// Random.NextInt64(long, long) instance method — whose upper bound is EXCLUSIVE — would win - /// overload resolution over a same-named extension and silently change the semantics. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "The flagged lines are prose, not disabled code. The heuristic reads an equation, a bracketed range or a semicolon inside an " + - "explanatory sentence as a statement. These comments carry the WHY this codebase asks for and deleting them would lose the " + - "reasoning, so the finding is recorded here instead.")] - internal static long NextInt64Inclusive(this SeededRandom random, long minInclusive, long maxInclusive) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - if (minInclusive > maxInclusive) { throw new ArgumentOutOfRangeException(nameof(maxInclusive), "The maximum must be greater than or equal to the minimum."); } - - ulong rangeSize = (ulong)(maxInclusive - minInclusive) + 1UL; - ulong draw = random.NextUInt64(); - - // rangeSize is 0 only when the range spans the full ulong width, which int-derived bounds never do; - // guard anyway so the helper stays correct if reused with wider bounds. - if (rangeSize == 0UL) { return unchecked((long)draw); } - - return minInclusive + (long)(draw % rangeSize); - } - - /// Draws a uniform in the inclusive range — see . - internal static int NextInt32Inclusive(this SeededRandom random, int minInclusive, int maxInclusive) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - - return (int)random.NextInt64Inclusive(minInclusive, maxInclusive); - } - - /// Draws 8 random bytes as a — the raw material of the ordinal sampling. - internal static ulong NextUInt64(this SeededRandom random) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - - byte[] bytes = new byte[sizeof(ulong)]; - random.NextBytes(bytes); - - return BitConverter.ToUInt64(bytes, 0); - } - - -} diff --git a/JustDummies/RegexAlphabet.cs b/JustDummies/RegexAlphabet.cs deleted file mode 100644 index 7b9dc3c6..00000000 --- a/JustDummies/RegexAlphabet.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace JustDummies; - -/// -/// The bounded, readable character universe the regex generator draws from wherever the pattern leaves a -/// character free — a shorthand (\d \w \s and their negations), the dot, a negated class. Those -/// positions resolve to printable ASCII (0x20–0x7E), with one deliberate exception: \s draws from a -/// readable pair that includes a tab. A character the pattern names explicitly — a literal, an escape, a -/// member of a positive class — is emitted exactly as written and may fall outside this universe, control -/// characters included. Restricting the free positions keeps generated dummies legible instead of scattering -/// arbitrary Unicode, and it keeps every generated character a genuine member of the class it stands for, so the -/// output always matches the source pattern. -/// -internal static class RegexAlphabet { - - #region Statics members declarations - - internal const char MinPrintable = ' '; // 0x20 - internal const char MaxPrintable = '~'; // 0x7E - - /// How far an ASCII letter's two cases sit apart: 'a' - 'A', the single bit that tells them apart. - private const int AsciiCaseDistance = 'a' - 'A'; - - /// Every printable ASCII character — the universe negated classes and the dot draw from. - internal static readonly char[] Printable = Range(MinPrintable, MaxPrintable); - - /// \d. - internal static readonly char[] Digit = Range('0', '9'); - - /// \D — printable non-digits. - internal static readonly char[] NonDigit = Where(character => !IsDigit(character)); - - /// \w. - internal static readonly char[] Word = Where(IsWord); - - /// \W — printable non-word characters. - internal static readonly char[] NonWord = Where(character => !IsWord(character)); - - /// \s — a readable pair; both are genuine whitespace, so either matches the source pattern. - internal static readonly char[] Whitespace = { ' ', '\t' }; - - /// \S — printable non-whitespace (space is the only printable whitespace, so this is 0x21–0x7E). - internal static readonly char[] NonWhitespace = Where(character => character != ' '); - - /// . — any character except a newline; every printable ASCII character qualifies. - internal static readonly char[] Dot = Printable; - - private static bool IsDigit(char character) { - return character is >= '0' and <= '9'; - } - - private static bool IsWord(char character) { - return character is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_'; - } - - /// The printable characters none of covers — the universe of a negated class [^…]. - internal static char[] Negate(ISet excluded) { - if (excluded is null) { throw new ArgumentNullException(nameof(excluded)); } - return Where(character => !excluded.Contains(character)); - } - - /// - /// together with its opposite-case twin when it is an ASCII letter — the - /// expansion applied under so a literal - /// or class member matches either case. - /// - internal static IEnumerable WithBothCases(char character) { - if (character is >= 'A' and <= 'Z') { return new[] { character, (char)(character + AsciiCaseDistance) }; } - if (character is >= 'a' and <= 'z') { return new[] { character, (char)(character - AsciiCaseDistance) }; } - - return new[] { character }; - } - - private static char[] Range(char low, char high) { - List characters = new(high - low + 1); - // Iterate an int, not a char: a high of U+FFFF would wrap a 16-bit char back to 0x0000 and loop forever. - // Every current caller passes a bounded high, so this is defense in depth against a future wide range. - for (int code = low; code <= high; code++) { characters.Add((char)code); } - - return characters.ToArray(); - } - - private static char[] Where(Func keep) { - List characters = new(Printable.Length); - characters.AddRange(Printable.Where(keep)); - - return characters.ToArray(); - } - - #endregion - -} diff --git a/JustDummies/RegexNode.cs b/JustDummies/RegexNode.cs deleted file mode 100644 index 9e82a8ab..00000000 --- a/JustDummies/RegexNode.cs +++ /dev/null @@ -1,172 +0,0 @@ -#region Usings declarations - -using System.Text; - -#endregion - -namespace JustDummies; - -/// -/// The carrier of a single generation: the seeded random generator to draw from, and the buffer the nodes write -/// into. enforces a hard length ceiling so no pattern can expand the buffer without bound — -/// whether through a nested unbounded quantifier ((a+)+ and the like) or through bounded quantifiers whose -/// product is very large ((a{1000}){1000}). The value is built directly, never generated then retried, but -/// the buffer is still guarded. -/// -internal sealed class RegexGenerationContext { - - #region Fields declarations - - private readonly StringBuilder _builder = new(); - private readonly int _limit; - - #endregion - - internal RegexGenerationContext(SeededRandom random, int limit) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - Random = random; - _limit = limit; - } - - internal SeededRandom Random { get; } - - internal void Append(char character) { - if (_builder.Length >= _limit) { - throw AnyGenerationException.PatternExceedsGenerationLimit(_limit); - } - - _builder.Append(character); - } - - internal string Result() { - return _builder.ToString(); - } - -} - -/// -/// A node of the parsed pattern tree. Generation is a direct recursive descent: each node writes the characters it -/// stands for into the , drawing counts and choices from the seeded random -/// generator — so the whole tree yields exactly one string that matches the pattern, in one pass. -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", - Justification = - "The abstract class is the root of a closed, internal hierarchy and is deliberately not an interface: a class cannot be " + - "implemented from outside the assembly, keeps the option of adding shared state without breaking every subtype, and on the " + - "netstandard2.0 leg cannot be replaced by an interface with non-public members.")] -internal abstract class RegexNode { - - internal abstract void Append(RegexGenerationContext context); - -} - -/// A terminal: one character drawn uniformly from a fixed set (a literal is the singleton case). -internal sealed class RegexCharacters : RegexNode { - - #region Fields declarations - - private readonly char[] _choices; - - #endregion - - internal RegexCharacters(char[] choices) { - if (choices is null) { throw new ArgumentNullException(nameof(choices)); } - _choices = choices; - } - - /// The characters this terminal can emit — empty when a class excludes the whole universe. - internal int Count => _choices.Length; - - internal override void Append(RegexGenerationContext context) { - if (context is null) { throw new ArgumentNullException(nameof(context)); } - context.Append(_choices[context.Random.Next(_choices.Length)]); - } - -} - -/// A concatenation: its children in order. -internal sealed class RegexSequence : RegexNode { - - #region Fields declarations - - private readonly RegexNode[] _parts; - - #endregion - - internal RegexSequence(RegexNode[] parts) { - if (parts is null) { throw new ArgumentNullException(nameof(parts)); } - _parts = parts; - } - - internal override void Append(RegexGenerationContext context) { - if (context is null) { throw new ArgumentNullException(nameof(context)); } - foreach (RegexNode part in _parts) { part.Append(context); } - } - -} - -/// An alternation: one branch, chosen uniformly. -internal sealed class RegexAlternation : RegexNode { - - #region Fields declarations - - private readonly RegexNode[] _branches; - - #endregion - - internal RegexAlternation(RegexNode[] branches) { - if (branches is null) { throw new ArgumentNullException(nameof(branches)); } - _branches = branches; - } - - internal override void Append(RegexGenerationContext context) { - if (context is null) { throw new ArgumentNullException(nameof(context)); } - _branches[context.Random.Next(_branches.Length)].Append(context); - } - -} - -/// -/// A quantifier: the child repeated between min and max times. An unbounded quantifier -/// (*, +, {n,}) has no max; generation then draws min plus 0 to -/// extra repetitions, the same bounded-spread default the rest of the library uses. -/// -internal sealed class RegexRepeat : RegexNode { - - #region Statics members declarations - - /// How many repetitions above the minimum an unbounded quantifier may add. - internal const int UnboundedExtra = 8; - - #endregion - - #region Fields declarations - - private readonly RegexNode _child; - private readonly int? _max; - private readonly int _min; - - #endregion - - internal RegexRepeat(RegexNode child, int min, int? max) { - if (child is null) { throw new ArgumentNullException(nameof(child)); } - _child = child; - _min = min; - _max = max; - } - - internal override void Append(RegexGenerationContext context) { - if (context is null) { throw new ArgumentNullException(nameof(context)); } - // The unbounded count is widened to long before the extra repetitions are added: a minimum within - // UnboundedExtra of int.MaxValue would otherwise wrap negative, and a negative count writes nothing at all — - // silently yielding a value the pattern does not match, which is the one outcome generation must never - // produce. Widened, such a count simply walks until the generation ceiling reports the overrun, exactly as - // any other minimum too large to fit does. - long count = _max is int max - ? context.Random.NextInt32Inclusive(_min, max) - : (long)_min + context.Random.Next(0, UnboundedExtra + 1); - - for (long repetition = 0; repetition < count; repetition++) { _child.Append(context); } - } - -} diff --git a/JustDummies/RegexParser.cs b/JustDummies/RegexParser.cs deleted file mode 100644 index 811c06c7..00000000 --- a/JustDummies/RegexParser.cs +++ /dev/null @@ -1,600 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// A recursive-descent parser turning the regular subset of a regex pattern into a -/// tree the generator can walk. Supported: literals; the escapes \t \n \r \f \v \a \e, hexadecimal -/// \xHH and \uHHHH, control \cX, octal \0nn and escaped punctuation; the shorthands -/// \d \D \w \W \s \S; character classes with ranges and negation; the quantifiers -/// ? * + {n} {n,} {n,m} (a lazy ? marker is accepted and ignored — it changes matching order, never -/// which strings match; possessive quantifiers do not exist in .NET and are rejected); alternation; grouping -/// (capturing, non-capturing and named — the name is validated as the real engine would, then ignored); the dot; -/// and the anchors ^ $ at the start and end of the pattern or of a top-level alternation branch — including -/// a run of them (^^, $$) or a quantified one (^*, $?), all no-ops there since a whole -/// matching string is generated; anywhere else they are refused, because the pattern could never be matched by a -/// whole generated string. A brace that does not form a well-formed quantifier is a literal, as in the real engine, -/// and groups may nest at most 256 levels deep. A well-formed but non-regular or out-of-scope construct — a -/// lookaround, a backreference, a balancing group (it pops the capture stack, the backreference family), a Unicode -/// category, a word boundary, an atomic group (its first-branch commit is not language-equivalent to plain -/// alternation), a class subtraction, or a negated class that excludes the whole printable-ASCII universe JustDummies -/// draws from — is refused with an -/// rather than silently mis-generated; a malformed pattern (including an -/// escape the real engine rejects) raises an . -/// -internal sealed class RegexParser { - - #region Statics members declarations - - private const int MaxGroupDepth = 256; - - /// How many hexadecimal digits a \xHH escape spells. - private const int HexEscapeDigits = 2; - - /// How many hexadecimal digits a \uHHHH escape spells. - private const int UnicodeEscapeDigits = 4; - - /// How many octal digits may follow the first one in a \0nn escape. - private const int MaxOctalTailDigits = 2; - - /// The base a \x or \u escape's digits accumulate in. - private const int HexBase = 16; - - /// The base a \0 escape's digits accumulate in. - private const int OctalBase = 8; - - /// How many digits precede 'A' in the hexadecimal alphabet, so 'A' reads back as ten. - private const int HexLetterOffset = 10; - - /// The control code \cA names — the alphabet's first letter maps to the first control character, not to the null one. - private const int FirstControlCode = 1; - - internal static RegexNode Parse(string pattern, bool ignoreCase) { - if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } - RegexParser parser = new(pattern, ignoreCase); - RegexNode root = parser.ParseAlternation(); - if (!parser.AtEnd) { - // The only character ParseSequence stops on without consuming is a ')' with no opener. - throw parser.Malformed(parser.Peek() == ')' ? "unbalanced closing parenthesis ')'" : $"unexpected character '{parser.Peek()}'"); - } - - return root; - } - - private static bool IsClassShorthand(char character) { - return character is 'd' or 'D' or 'w' or 'W' or 's' or 'S'; - } - - private static void AddClassShorthand(HashSet set, char shorthand) { - char[] members = shorthand switch { - 'd' => RegexAlphabet.Digit, - 'D' => RegexAlphabet.NonDigit, - 'w' => RegexAlphabet.Word, - 'W' => RegexAlphabet.NonWord, - 's' => RegexAlphabet.Whitespace, - _ => RegexAlphabet.NonWhitespace - }; - foreach (char member in members) { set.Add(member); } - } - - private static HashSet ExpandCase(HashSet set) { - HashSet expanded = []; - foreach (char character in set) { - foreach (char variant in RegexAlphabet.WithBothCases(character)) { expanded.Add(variant); } - } - - return expanded; - } - - private static bool IsHexDigit(char character) { - return character is (>= '0' and <= '9') or (>= 'a' and <= 'f') or (>= 'A' and <= 'F'); - } - - private static int HexValue(char character) { - return character <= '9' ? character - '0' : char.ToUpperInvariant(character) - 'A' + HexLetterOffset; - } - - #endregion - - #region Fields declarations - - private readonly bool _ignoreCase; - private readonly string _pattern; - private int _depth; - private int _index; - - #endregion - - private RegexParser(string pattern, bool ignoreCase) { - _pattern = pattern; - _ignoreCase = ignoreCase; - } - - private bool AtEnd => _index >= _pattern.Length; - - private char Peek() { - return _pattern[_index]; - } - - private char Next() { - return _pattern[_index++]; - } - - private char PeekAt(int offset) { - int at = _index + offset; - - return at < _pattern.Length ? _pattern[at] : '\0'; - } - - private bool Eat(char character) { - if (!AtEnd && _pattern[_index] == character) { - _index++; - - return true; - } - - return false; - } - - private RegexNode ParseAlternation() { - List branches = [ParseSequence()]; - while (Eat('|')) { branches.Add(ParseSequence()); } - - return branches.Count == 1 ? branches[0] : new RegexAlternation(branches.ToArray()); - } - - private RegexNode ParseSequence() { - List parts = []; - while (!AtEnd && Peek() != '|' && Peek() != ')') { - if (TryConsumeAnchor(atSequenceStart: parts.Count == 0)) { continue; } - - parts.Add(ParseQuantified()); - } - - return parts.Count == 1 ? parts[0] : new RegexSequence(parts.ToArray()); - } - - /// - /// Consumes a boundary anchor at the current position when one is there, answering whether it did. - /// - /// - /// Anchors are no-ops for a whole-string generator, but only where they are guaranteed to match: ^ at - /// the start and $ at the end of the pattern or of a top-level alternation branch. A run of them - /// (^^, $$) and a quantified one (^*, $?, ^{2}) are equally no-ops there — - /// the real engine accepts and matches all of these — so they are consumed and ignored. Anywhere else - /// (a^, $a, inside a group) the pattern can never be matched by a whole generated string, so it - /// is refused instead of silently mis-generated. - /// - private bool TryConsumeAnchor(bool atSequenceStart) { - if (Peek() == '^') { - if (_depth > 0 || !atSequenceStart) { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "an anchor '^' away from the start of the pattern or of a top-level alternation branch", _index); } - _index++; - SkipAnchorQuantifier(); - - return true; - } - - if (Peek() == '$') { - int position = _index; - _index++; - SkipAnchorQuantifier(); - // A '$' is a no-op only at the very end: what follows must be end-of-pattern, a branch bar, or - // another end-anchor '$'. Inside a group, or before anything else, it can never match a whole string. - if (_depth > 0 || (!AtEnd && Peek() != '|' && Peek() != '$')) { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "an anchor '$' away from the end of the pattern or of a top-level alternation branch", position); } - - return true; - } - - return false; - } - - /// - /// Consumes a quantifier applied to a boundary anchor (^*, $?, ^{2,3}, a lazy marker - /// included) when one is present. Repeating a zero-width no-op leaves it a no-op — the real engine accepts and - /// matches these — so the quantifier is read and discarded, never turned into a repeat node. - /// - private void SkipAnchorQuantifier() { - if (AtEnd) { return; } // an anchor at the very end carries no quantifier - if (TryReadQuantifier() is null) { return; } - if (!AtEnd && Peek() == '?') { _index++; } // a lazy marker on a no-op is immaterial - } - - private RegexNode ParseQuantified() { - RegexNode atom = ParseAtom(); - if (AtEnd) { return atom; } - - (int Min, int? Max)? quantifier = TryReadQuantifier(); - if (quantifier is null) { return atom; } - - // A trailing '?' makes the quantifier lazy — it changes which match is preferred, never which strings - // match, so it is accepted and ignored. Possessive quantifiers (a*+) do not exist in .NET; the '+' falls - // through to the nothing-to-repeat error below, mirroring the real engine's rejection. - if (!AtEnd && Peek() == '?') { _index++; } - - return new RegexRepeat(atom, quantifier.Value.Min, quantifier.Value.Max); - } - - private (int Min, int? Max)? TryReadQuantifier() { - switch (Peek()) { - case '*': _index++; return (0, null); - case '+': _index++; return (1, null); - case '?': _index++; return (0, 1); - case '{': return ReadBraceQuantifier(); - default: return null; - } - } - - /// - /// Reads a {n}, {n,} or {n,m} quantifier. A brace whose content is not one of those - /// forms is a literal in the real engine, so the position is restored and null returned — the - /// caller then reads the '{' as an ordinary character. An out-of-order {3,1} is rejected, as the real - /// engine rejects it. - /// - private (int Min, int? Max)? ReadBraceQuantifier() { - int start = _index; - _index++; // consume '{' - if (AtEnd || !char.IsDigit(Peek())) { - _index = start; - - return null; - } - - int min = ReadInteger(); - int? max; - if (Eat('}')) { - max = min; - } else if (Eat(',')) { - if (Eat('}')) { - max = null; - } else if (!AtEnd && char.IsDigit(Peek())) { - max = ReadInteger(); - if (!Eat('}')) { - _index = start; - - return null; - } - } else { - _index = start; - - return null; - } - } else { - _index = start; - - return null; - } - - if (max is int upper && upper < min) { throw Malformed($"quantifier {{{min},{upper}}} is out of order (the maximum is below the minimum)"); } - - return (min, max); - } - - private int ReadInteger() { - int start = _index; - while (!AtEnd && char.IsDigit(Peek())) { _index++; } - string digits = _pattern.Substring(start, _index - start); - if (!int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out int value)) { - throw Malformed($"quantifier bound '{digits}' is too large"); - } - - return value; - } - - private RegexNode ParseAtom() { - char character = Peek(); - switch (character) { - case '(': return ParseGroup(); - case '[': return ParseClass(); - case '\\': return ParseEscape(); - case '.': _index++; return new RegexCharacters(RegexAlphabet.Dot); - case '*': - case '+': - case '?': throw Malformed($"quantifier '{character}' has nothing to repeat"); - case '{': { - // A well-formed brace quantifier with no atom before it is an error, exactly as in the real - // engine; any other brace is a literal. - if (ReadBraceQuantifier() is not null) { throw Malformed("quantifier '{' has nothing to repeat"); } - _index++; - - return Literal('{'); - } - default: _index++; return Literal(character); - } - } - - private RegexNode ParseGroup() { - int position = _index; - _index++; // consume '(' - if (!AtEnd && Peek() == '?') { - _index++; // consume '?' - if (AtEnd) { throw Malformed("unterminated group '(?'"); } - - switch (Peek()) { - case ':': _index++; break; // non-capturing group - // An atomic group commits to the first branch that matches, so its language is NOT that of the - // plain alternation ('(?>ab|a)b' matches only "abb"); generating from it as if it were would - // yield non-matching values, so it is refused. - case '>': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "an atomic group '(?>…)'", position); - case '=': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a lookahead '(?=…)'", position); - case '!': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a negative lookahead '(?!…)'", position); - case '(': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a conditional group '(?(…)…)'", position); - case '#': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "an inline comment '(?#…)'", position); - case '<': - if (PeekAt(1) is '=' or '!') { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a lookbehind '(?<=…)' or '(?', position); // named group: the name never shapes generation, but it is validated - break; - case '\'': - _index++; // consume '\'' - SkipGroupName('\'', position); - break; - default: throw UnsupportedRegexException.OutsideRegularSubset(_pattern, $"a group option '(?{Peek()}…)'", position); - } - } - - _depth++; - if (_depth > MaxGroupDepth) { throw Malformed($"groups are nested deeper than {MaxGroupDepth} levels"); } - RegexNode inner = ParseAlternation(); - _depth--; - if (!Eat(')')) { throw Malformed("unbalanced opening parenthesis '('"); } - - return inner; - } - - /// - /// Consumes a named-group name up to its (> for - /// (?<name>…), a quote for (?'name'…)) and validates it as the real engine would, so - /// "accepted by JustDummies" stays aligned with "accepted by .NET". The name never shapes generation, but two - /// well-formed .NET constructs must not slip through as ordinary named groups: - /// - /// - /// a balancing group (?<name1-name2>…) / (?<-name2>…) — the - /// - pops the capture stack (the same family as a backreference), so it is non-regular and - /// refused with an . This fires even when the target group - /// is undefined — where the real engine instead reports a malformed pattern — because telling the - /// two apart would need a table of captured groups, which this generator deliberately does not keep. - /// The divergence is only in the error kind: both reject the pattern, neither mis-generates. - /// - /// - /// an invalid name — a name opening with a digit is an explicit capture number, valid - /// only as a positive integer with no leading zero (1, 10; not 0, 01 or - /// 1a); any other name must be word characters. Anything else raises an - /// , mirroring the real engine. - /// - /// - /// - private void SkipGroupName(char terminator, int position) { - int start = _index; - while (!AtEnd && Peek() != terminator) { _index++; } - if (_index == start) { throw Malformed("a group name must not be empty"); } - string name = _pattern.Substring(start, _index - start); - if (!Eat(terminator)) { throw Malformed($"unterminated group name (expected '{terminator}')"); } - ValidateGroupName(name, position); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop exists to name the FIRST offending element in the exception it throws. A Where clause discards which element failed, so " + - "the message would have to re-find it, turning one pass into two and one statement into three.")] - private void ValidateGroupName(string name, int position) { - // A '-' marks a balancing group; it manipulates the capture stack (the backreference family), so it is - // non-regular and refused here even when its target is undefined (see SkipGroupName for why the divergence - // from the real engine's malformed-pattern verdict on that case is accepted). - if (name.Contains('-')) { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a balancing group '(?…)'", position); } - - // A name opening with a digit is an explicit capture number: the real engine accepts it only as a positive - // integer with no leading zero, so '0', '01' and '1a' are refused while '1' and '10' pass. - if (name[0] is >= '0' and <= '9') { - bool validNumber = name[0] != '0' && name.All(character => character is >= '0' and <= '9'); - - if (!validNumber) { throw Malformed($"a group name starting with a digit must be a group number with no leading zero, but '{name}' is not"); } - - return; - } - - // Any other name must be word characters (letter, digit or underscore), exactly as the real engine requires. - foreach (char character in name) { - if (!char.IsLetterOrDigit(character) && character != '_') { throw Malformed($"the group name '{name}' contains '{character}', which is not a letter, digit or underscore"); } - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S125:Sections of code should not be commented out", - Justification = - "The flagged lines are prose, not disabled code: the heuristic reads an equation, a bracketed range or a semicolon inside an " + - "explanatory sentence as a statement. These comments carry the reasoning this codebase asks every comment to carry, so the " + - "finding is recorded rather than the comment deleted.")] - private RegexNode ParseEscape() { - int position = _index; - _index++; // consume '\' - if (AtEnd) { throw Malformed("a trailing '\\' escapes nothing"); } - - char escaped = Next(); - switch (escaped) { - case 'd': return new RegexCharacters(RegexAlphabet.Digit); - case 'D': return new RegexCharacters(RegexAlphabet.NonDigit); - case 'w': return new RegexCharacters(RegexAlphabet.Word); - case 'W': return new RegexCharacters(RegexAlphabet.NonWord); - case 's': return new RegexCharacters(RegexAlphabet.Whitespace); - case 'S': return new RegexCharacters(RegexAlphabet.NonWhitespace); - case 't': return Literal('\t'); - case 'n': return Literal('\n'); - case 'r': return Literal('\r'); - case 'f': return Literal('\f'); - case 'v': return Literal('\v'); - case 'a': return Literal('\a'); - case 'e': return Literal('\u001B'); - case 'x': return Literal(ReadHexEscape(HexEscapeDigits)); - case 'u': return Literal(ReadHexEscape(UnicodeEscapeDigits)); - case 'c': return Literal(ReadControlEscape()); - case '0': return Literal(ReadOctalTail(0)); - case 'b': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a word-boundary '\\b'", position); - case 'B': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a non-word-boundary '\\B'", position); - case 'A': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a start-of-string anchor '\\A'", position); - case 'G': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a contiguous-match anchor '\\G'", position); - case 'Z': - case 'z': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "an end-of-string anchor '\\" + escaped + "'", position); - case 'p': - case 'P': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a Unicode category '\\" + escaped + "{…}'", position); - case 'k': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a named backreference '\\k<…>'", position); - default: - if (escaped is >= '1' and <= '9') { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, $"a backreference '\\{escaped}'", position); } - // The real engine rejects unknown word-character escapes rather than reading them as literals; - // mirroring it keeps "accepted by JustDummies" aligned with "accepted by .NET". - if (char.IsLetterOrDigit(escaped)) { throw Malformed($"unrecognized escape sequence '\\{escaped}'"); } - - return Literal(escaped); // an escaped metacharacter (\. \* \( \\ …) or escaped punctuation - } - } - - private char ReadHexEscape(int digits) { - int value = 0; - for (int i = 0; i < digits; i++) { - if (AtEnd || !IsHexDigit(Peek())) { throw Malformed($"a '\\{(digits == HexEscapeDigits ? 'x' : 'u')}' escape expects exactly {digits} hexadecimal digits"); } - value = value * HexBase + HexValue(Next()); - } - - return (char)value; - } - - private char ReadControlEscape() { - if (AtEnd || Peek() is not ((>= 'A' and <= 'Z') or (>= 'a' and <= 'z'))) { throw Malformed("a '\\c' escape expects a letter (\\cA through \\cZ)"); } - - return (char)(char.ToUpperInvariant(Next()) - 'A' + FirstControlCode); - } - - private char ReadOctalTail(int firstDigit) { - int value = firstDigit; - for (int i = 0; i < MaxOctalTailDigits && !AtEnd && Peek() is >= '0' and <= '7'; i++) { value = value * OctalBase + (Next() - '0'); } - - return (char)value; - } - - private RegexNode ParseClass() { - int position = _index; - _index++; // consume '[' - bool negated = Eat('^'); - HashSet set = ReadClassMembers(); - - if (_ignoreCase) { set = ExpandCase(set); } - char[] choices = negated ? RegexAlphabet.Negate(set) : set.ToArray(); - // Only a negated class can end up empty — a plain class always holds the member it just read. Such a class - // is well-formed and regular (the real engine accepts it), but it excludes every character JustDummies draws - // from, so it is refused as unsupported (a universe limit), not as malformed. Routing it through Malformed - // would claim the caller wrote a broken pattern for one the real engine compiles. - if (choices.Length == 0) { - throw UnsupportedRegexException.EmptyNegatedClass(_pattern, position); - } - - return new RegexCharacters(choices); - } - - /// - /// Reads the class members up to the closing ], which it consumes. - /// - private HashSet ReadClassMembers() { - HashSet set = []; - bool first = true; - - while (true) { - if (AtEnd) { throw Malformed("unterminated character class '['"); } - if (Peek() == ']' && !first) { - _index++; - - break; - } - - first = false; - // .NET's class subtraction ([a-z-[aeiou]]) removes a nested class; parsing the '-[' as members would - // close the class early and generate values outside it, so the construct is refused. But '-[' is - // subtraction only when a base member precedes it: the real engine reads a leading '-' (as in '[-[x]]') - // as an ordinary hyphen, so it is refused here only once the set already holds a member. - if (Peek() == '-' && PeekAt(1) == '[' && set.Count > 0) { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a character-class subtraction '[…-[…]]'", _index); } - if (Peek() == '\\' && IsClassShorthand(PeekAt(1))) { - _index++; // consume '\' - AddClassShorthand(set, Next()); - - continue; - } - - AddClassMemberOrRange(set); - } - - return set; - } - - /// - /// Reads one class member and adds it, or the whole range it opens, to . - /// - private void AddClassMemberOrRange(HashSet set) { - char low = ReadClassChar(); - // A '-[' immediately after a member is subtraction as well ('[a-[x]]'): the real engine never reads it - // as a range whose upper bound is '[', so intercepting it here can never turn away a valid range. - if (!AtEnd && Peek() == '-' && PeekAt(1) == '[') { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a character-class subtraction '[…-[…]]'", _index); } - if (!AtEnd && Peek() == '-' && PeekAt(1) != ']' && PeekAt(1) != '\0') { - _index++; // consume '-' - char high = ReadClassChar(); - if (high < low) { throw Malformed($"character class range '{low}-{high}' is out of order"); } - // Iterate an int, not a char: a class range may legitimately end at U+FFFF (reachable via a - // literal or the \uFFFF escape), and incrementing a 16-bit char past it wraps to 0x0000 and never ends. - for (int code = low; code <= high; code++) { set.Add((char)code); } - } else { - set.Add(low); - } - } - - private char ReadClassChar() { - if (AtEnd) { throw Malformed("unterminated character class '['"); } - - char character = Next(); - if (character != '\\') { return character; } - if (AtEnd) { throw Malformed("a trailing '\\' escapes nothing"); } - - char escaped = Next(); - switch (escaped) { - case 't': return '\t'; - case 'n': return '\n'; - case 'r': return '\r'; - case 'f': return '\f'; - case 'v': return '\v'; - case 'a': return '\a'; - case 'e': return '\u001B'; - case 'b': return '\b'; // inside a class, \b is the backspace character, never a word boundary - case 'x': return ReadHexEscape(HexEscapeDigits); - case 'u': return ReadHexEscape(UnicodeEscapeDigits); - case 'c': return ReadControlEscape(); - case '0': return ReadOctalTail(0); - default: - if (IsClassShorthand(escaped)) { throw Malformed($"a shorthand '\\{escaped}' cannot be an endpoint of a character range"); } - // Inside a class a backslash-digit is an octal escape — backreferences cannot occur here. - if (escaped is >= '1' and <= '7') { return ReadOctalTail(escaped - '0'); } - if (escaped is 'p' or 'P') { throw UnsupportedRegexException.OutsideRegularSubset(_pattern, $"a Unicode category '\\{escaped}{{…}}'", _index - 2); } - if (char.IsLetterOrDigit(escaped)) { throw Malformed($"unrecognized escape sequence '\\{escaped}' in a character class"); } - - return escaped; // an escaped metacharacter or escaped punctuation - } - } - - private RegexNode Literal(char character) { - if (!_ignoreCase) { return new RegexCharacters(new[] { character }); } - - return new RegexCharacters(RegexAlphabet.WithBothCases(character).Distinct().ToArray()); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3928:Parameter names used into ArgumentException constructors should match an existing one", - Justification = - "pattern is the public parameter the consumer passed to Any.Pattern(...); this private factory only assembles the exception the " + - "parser throws on its behalf. Its own reason parameter names the diagnosis, not the argument at fault, so pointing the exception " + - "at it would send the caller to the wrong place.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", - Justification = - "Same reason as the S3928 suppression above: pattern is the public parameter the consumer passed to Any.Pattern(...), " + - "which is the argument they must fix. This private factory only assembles the exception on the parser's behalf.")] - private ArgumentException Malformed(string reason) { - return new ArgumentException($"The regular expression pattern \"{_pattern}\" is invalid: {reason} (at position {_index}).", "pattern"); - } - -} diff --git a/JustDummies/Replay.cs b/JustDummies/Replay.cs deleted file mode 100644 index c2af816d..00000000 --- a/JustDummies/Replay.cs +++ /dev/null @@ -1,128 +0,0 @@ -#region Usings declarations - -using System.Diagnostics; - -#endregion - -namespace JustDummies; - -/// -/// What a failed draw needs in order to be replayed: the seed of the run, and the sentence telling the reader how -/// to use it. The two are always derived from the same source and always travel together, so they are one value -/// rather than two arguments that a call site could pair wrongly. -/// -/// -/// A class rather than a struct, like every value object here: a struct carries a parameterless constructor that -/// would yield a seedless, guidance-less instance bypassing the factories below. -/// -/// Two replays are equal when they replay the same run the same way — the seed alone does not settle it, -/// since the same seed replays a run in full or only in part depending on what drew. Being a value with no -/// identity beyond what it holds, it says so rather than leaving the reference comparison a reader would get -/// by default (ADR-0065). -/// -/// -/// Built while a failure is being reported, so it guards nothing (ADR-0064): a guard on this path would throw -/// while a failure is being reported and lose the original. Its parameters are non-nullable instead, which -/// makes the contract the compiler's. Comparing and hashing keep that footing: neither composes anything, so -/// neither can fail while a failure is reported. -/// -/// -/// The seed is supplied rather than read back from the source, because the two are not always the same -/// thing: on the ambient source creates a state — and a fresh seed — when -/// none exists, so a caller that drew with a seed captured earlier must hand that seed over rather than let it -/// be resolved a second time. -/// -/// -[BuiltOnTheFailurePath] -[DebuggerDisplay("{ToString()}")] -[ValueObject] -internal sealed class Replay : IEquatable { - - /// - /// The odd prime the seed's hash is multiplied by before the guidance is folded in, so that the two fields - /// swapping values do not collide. Its exact value carries no meaning beyond being odd and prime. - /// - private const int HashMultiplier = 397; - - #region Statics members declarations - - /// - /// The run replays in full: every draw the failing generator made followed . - /// - internal static Replay Of(RandomSource source) { - return Of(source, source.Current.Seed); - } - - /// - /// The run replays in full, for a caller holding the seed it drew with — see the remark on the type about why - /// that seed is not read back from . - /// - internal static Replay Of(RandomSource source, int seed) { - return new Replay(seed, source.ReplayGuidance(seed)); - } - - /// - /// The run replays only in part: a foreign generator contributed values this source never drew, so promising a - /// full replay of them would be false. The seeded part still replays. - /// - internal static Replay PartialOf(RandomSource source) { - int seed = source.Current.Seed; - - return new Replay(seed, source.PartialReplayGuidance(seed)); - } - - #endregion - - /// Determines whether two replays replay the same run the same way. - /// The first replay to compare. - /// The second replay to compare. - /// true when both carry the same seed and guidance, or both are null; otherwise false. - public static bool operator ==(Replay? left, Replay? right) { - return Equals(left, right); - } - - /// Determines whether two replays differ in their seed or in what they promise to replay. - /// The first replay to compare. - /// The second replay to compare. - /// true when they differ, or exactly one is null; otherwise false. - public static bool operator !=(Replay? left, Replay? right) { - return !Equals(left, right); - } - - private Replay(int seed, string guidance) { - Seed = seed; - Guidance = guidance; - } - - /// The seed that replays the run, carried on the exception so a caller can read it without parsing prose. - internal int Seed { get; } - - /// The sentence naming the seed and scoping what it replays, appended to the failure message. - internal string Guidance { get; } - - /// - /// The seed and what it replays, as a reader needs them — the form - /// shows, since a value that renders as its own type name tells a debugger nothing. - /// - public override string ToString() { - return $"seed {Seed}: {Guidance}"; - } - - /// - public bool Equals(Replay? other) { - return other is not null && Seed == other.Seed && string.Equals(Guidance, other.Guidance, StringComparison.Ordinal); - } - - /// - public override bool Equals(object? obj) { - return obj is Replay other && Equals(other); - } - - /// - public override int GetHashCode() { - unchecked { - return (Seed * HashMultiplier) ^ StringComparer.Ordinal.GetHashCode(Guidance); - } - } - -} diff --git a/JustDummies/SizeGuard.cs b/JustDummies/SizeGuard.cs deleted file mode 100644 index 89d09f66..00000000 --- a/JustDummies/SizeGuard.cs +++ /dev/null @@ -1,89 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace JustDummies; - -/// -/// The argument validation every length and count constraint shares, defined once so the two surfaces — a string's -/// lengths and a collection's counts — cannot drift apart. It distinguishes the two kinds of size a caller can -/// declare, which ADR-0050 separates: a bound that only caps a draw, and a size the generator must actually -/// produce. -/// -/// -/// -/// A maximum is a permission, not a request: it narrows a draw and never widens it, so honouring one costs -/// nothing and any non-negative value is legal — a cap mirroring a storage limit far above -/// stays declarable and still yields small dummies. -/// -/// -/// An exact or minimum size is the opposite: it is what the generator must materialize, so it decides how much -/// memory and work a draw costs. Above the ceiling it is refused at declaration time, as an -/// naming the parameter the caller wrote — a single argument -/// unusable on its own is a caller mistake, not a contradiction between constraints and not a generation -/// failure, so it belongs to the same category as the negative size rejected right beside it rather than to -/// the library's own exception hierarchy. -/// -/// -internal static class SizeGuard { - - /// - /// The largest size a generator will be asked to produce. It sits in the gap between the legitimate and the - /// absurd: five orders of magnitude above the unconstrained spread, so ordinary use cannot approach it, and two - /// above the largest business limit a boundary test plausibly exercises, so such a test is never refused. A - /// value of this size still materializes in milliseconds — the ceiling therefore never turns a slow test into a - /// fast one, it turns a hang or an allocation failure into a diagnosable error. - /// - internal const int MaxProducibleSize = 1_000_000; - - #region Statics members declarations - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - /// - /// Validates a bound that only ever caps a draw. Any non-negative value is legal; the ceiling deliberately does - /// not apply, because nothing has to be produced to honour a maximum. - /// - /// The declared bound. - /// The name of the parameter the caller wrote. - /// How the size reads in a message — "length" or "count". - /// The validated value, so a caller can guard and pass in one expression. - internal static int RequireNonNegative(int value, string parameterName, string subject) { - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - if (subject is null) { throw new ArgumentNullException(nameof(subject)); } - if (value < 0) { throw new ArgumentOutOfRangeException(parameterName, value, $"The {subject} must not be negative."); } - - return value; - } - - /// - /// Validates a size the generator must actually produce: non-negative, and no larger than - /// . - /// - /// The declared size. - /// The name of the parameter the caller wrote. - /// How the size reads in a message — "length" or "count". - /// The validated value, so a caller can guard and pass in one expression. - /// - /// The ceiling applies to the size the caller states, not to the effective minimum that required fragments - /// (a prefix, a suffix, contained values) raise it to. The message can then name the parameter the caller - /// wrote, and a fragment large enough to matter is a literal the caller has already allocated — guarding it - /// here would report a size no argument of the call carries. - /// - internal static int RequireProducible(int value, string parameterName, string subject) { - RequireNonNegative(value, parameterName, subject); // guards both strings for this method too - if (value > MaxProducibleSize) { - throw new ArgumentOutOfRangeException(parameterName, value, - $"The {subject} must not exceed {V(MaxProducibleSize)}. Only a size the generator must actually produce is capped; a maximum, which only narrows the draw, accepts any non-negative value."); - } - - return value; - } - - #endregion - -} diff --git a/JustDummies/StringSpec.cs b/JustDummies/StringSpec.cs deleted file mode 100644 index 05611582..00000000 --- a/JustDummies/StringSpec.cs +++ /dev/null @@ -1,721 +0,0 @@ -#region Usings declarations - -using System.Globalization; -using System.Text; - -#endregion - -namespace JustDummies; - -/// -/// The immutable specification behind : length bounds, anchored fragments (prefix, -/// suffix, contained values), a character set, a letter casing, an optional allow-list (OneOf) and -/// excluded values — each remembering the constraint that set it, so a conflict message can name both sides. -/// Every mutation returns a new specification and cross-validates the whole eagerly: an -/// that exists can always generate — save for an exclusion tight enough to leave a shaped string -/// unsatisfiable, the one failure deferred to generation (see remarks). -/// -/// -/// -/// Without an allow-list the specification is constructive: a generated string is laid out as -/// prefix + filler + contained values + filler + suffix, without overlap analysis, so the length budget -/// the fragments require is the plain sum of their lengths. A combination that only a cleverer overlapping -/// layout could satisfy is reported as a conflict — a deliberate V1 simplification, kept explicit in the -/// conflict messages. -/// -/// -/// With an allow-list the specification is a filter instead: the caller supplied the values, so nothing -/// is laid out and every other constraint is answered by testing each pooled value. The layout budget -/// therefore does not apply — Containing("ab").Containing("ba") accepts the pooled "aba", which -/// the constructive path could not have built — and satisfiability is the plain question "does any pooled -/// value survive every declared constraint?", answered eagerly at declaration. Exclusions are eager too on -/// that path, since the domain is finite and enumerable. -/// -/// -/// Exclusions (DifferentFrom/Except) on a shaped string are the one constraint not met by -/// construction: strings are not ordinal-mapped, so an excluded value is avoided by a bounded redraw of -/// the constructive layout — expected collisions are ≈ 0 for any non-trivial shape, the same bounded escape a -/// distinct collection uses to skip a duplicate. An exclusion tight enough to leave the shape unsatisfiable -/// (for example excluding every character a single-character length allows) is therefore the one case that -/// surfaces at generation, as a seed-bearing , rather than eagerly at -/// declaration. -/// -/// -/// The default spread governs every draw, bounded or not (ADR-0050): a declared maximum composes with it -/// rather than replacing it, so an upper bound only narrows the draw and never widens it. Only a minimum, an -/// exact length or required fragments enlarge a string. -/// -/// -internal sealed class StringSpec { - - private const int DefaultLengthSpread = 16; - - // Bounded escape for exclusions: even the tightest realistic satisfiable shape — a single free character in a - // ~60-value pool with all but one value excluded — is found with overwhelming probability well within this many - // draws, while a genuinely unsatisfiable exclusion fails fast. Mirrors the fixed floor of the collection dedup draw. - private const int ExclusionRedrawBudget = 10_000; - - #region Statics members declarations - - internal static readonly StringSpec Unconstrained = new(null, null, 0, null, null, null, - null, null, null, null, [], - null, null, null, null, null, [], - null, null); - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static string Characters(int count) { - return count == 1 ? "1 character" : $"{V(count)} characters"; - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly LetterCasing? _casing; - private readonly ConstraintCall? _casingConstraint; - private readonly CharacterSet? _charset; - private readonly ConstraintCall? _charsetConstraint; - private readonly string? _customPool; - private readonly List? _effectiveAllowed; - private readonly int? _exactLength; - private readonly ConstraintCall? _exactConstraint; - private readonly IReadOnlyList _excluded; - private readonly IReadOnlyList<(ConstraintCall Constraint, string[] Values)> _exclusions; - private readonly IReadOnlyList<(string Fragment, ConstraintCall Constraint)> _fragments; - private readonly int? _maxLength; - private readonly ConstraintCall? _maxConstraint; - private readonly int _minLength; - private readonly ConstraintCall? _minConstraint; - private readonly string? _prefix; - private readonly ConstraintCall? _prefixConstraint; - private readonly string? _suffix; - private readonly ConstraintCall? _suffixConstraint; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private StringSpec(int? exactLength, ConstraintCall? exactConstraint, - int minLength, ConstraintCall? minConstraint, - int? maxLength, ConstraintCall? maxConstraint, - string? prefix, ConstraintCall? prefixConstraint, - string? suffix, ConstraintCall? suffixConstraint, - IReadOnlyList<(string Fragment, ConstraintCall Constraint)> fragments, - CharacterSet? charset, ConstraintCall? charsetConstraint, string? customPool, - LetterCasing? casing, ConstraintCall? casingConstraint, - IReadOnlyList<(ConstraintCall Constraint, string[] Values)> exclusions, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint) { - _exactLength = exactLength; - _exactConstraint = exactConstraint; - _minLength = minLength; - _minConstraint = minConstraint; - _maxLength = maxLength; - _maxConstraint = maxConstraint; - _prefix = prefix; - _prefixConstraint = prefixConstraint; - _suffix = suffix; - _suffixConstraint = suffixConstraint; - _fragments = fragments; - _charset = charset; - _charsetConstraint = charsetConstraint; - _customPool = customPool; - _casing = casing; - _casingConstraint = casingConstraint; - _exclusions = exclusions; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - // The flat, deduplicated value list drives the redraw and the exhaustion message; the provenance in - // _exclusions is consulted only when a conflict message must name the excluding constraint. Materialized - // once here — "constrain once, draw many" — in first-declared order. - _excluded = exclusions.SelectMany(pair => pair.Values).Distinct(StringComparer.Ordinal).ToList(); - // Same "constrain once, draw many" rule for the allow-list: the surviving pool is the exact domain the draw - // samples, the cardinality a distinct collection gates on, and the set a satisfiability check counts. - if (allowed is not null) { _effectiveAllowed = allowed.Where(Admits).ToList(); } - } - - /// Fixes the exact length; declared once per generator. - internal StringSpec WithExactLength(int length, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_exactConstraint == applying) { return this; } - if (_exactConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _exactConstraint); } - - StringSpec candidate = new(length, applying, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Tightens the minimum length; a looser bound than the current one is a no-op. - internal StringSpec WithMinLength(int length, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (length <= _minLength) { return this; } - - StringSpec candidate = new(_exactLength, _exactConstraint, length, applying, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Tightens the maximum length; a looser bound than the current one is a no-op. - internal StringSpec WithMaxLength(int length, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_maxLength is not null && length >= _maxLength) { return this; } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, length, applying, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Anchors a prefix; declared once per generator. - internal StringSpec WithPrefix(string prefix, ConstraintCall applying) { - if (prefix is null) { throw new ArgumentNullException(nameof(prefix)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_prefixConstraint == applying) { return this; } - if (_prefixConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _prefixConstraint); } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - prefix, applying, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Anchors a suffix; declared once per generator. - internal StringSpec WithSuffix(string suffix, ConstraintCall applying) { - if (suffix is null) { throw new ArgumentNullException(nameof(suffix)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_suffixConstraint == applying) { return this; } - if (_suffixConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _suffixConstraint); } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, suffix, applying, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Adds a value the generated string must contain. - internal StringSpec WithFragment(string fragment, ConstraintCall applying) { - if (fragment is null) { throw new ArgumentNullException(nameof(fragment)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - List<(string Fragment, ConstraintCall Constraint)> fragments = [.. _fragments, (fragment, applying)]; - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Restricts the character family; declared once per generator. - internal StringSpec WithCharset(CharacterSet charset, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_charsetConstraint == applying) { return this; } - if (_charsetConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _charsetConstraint); } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - charset, applying, _customPool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// - /// Restricts the filler to an explicit character pool — the general form of the named character sets. - /// Occupies the charset slot (declared once, and mutually exclusive with the named sets) and, because the - /// pool is the whole character definition, cannot combine with a casing. The pool is expected to be - /// distinct already. - /// - internal StringSpec WithCharPool(string pool, ConstraintCall applying) { - if (pool is null) { throw new ArgumentNullException(nameof(pool)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_charsetConstraint == applying) { return this; } - if (_charsetConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _charsetConstraint); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_casingConstraint == applying) { return this; } - if (_casingConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _casingConstraint); } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, applying, pool, _casing, _casingConstraint, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Imposes a letter casing; declared once per generator. - internal StringSpec WithCasing(LetterCasing casing, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_casingConstraint == applying) { return this; } - if (_casingConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _casingConstraint); } - // A custom pool and the constraint naming it are written together (WithCustomPool passes `applying, pool`), - // so a declared pool always carries its name. - if (_customPool is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _charsetConstraint!); } - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, casing, applying, _exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// Adds values the generated string must avoid; may be declared several times, the exclusions accumulate. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The condition reads the collection the body mutates. Where is lazily evaluated, so lifting the filter out would run each " + - "predicate against a snapshot taken before the additions it is meant to see, and let duplicates through.")] - internal StringSpec WithExcluded(IReadOnlyList values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - // The applied constraint tags its own values, so a conflict message can name the exclusion that actually - // emptied an allow-list rather than a shape constraint that merely borders it. - List<(ConstraintCall Constraint, string[] Values)> exclusions = [.. _exclusions, (applying, values.ToArray())]; - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, exclusions, - _allowed, _allowedConstraint); - - return candidate.Validated(applying, this); - } - - /// - /// Restricts the domain to an explicit allow-list; declared once per generator. From here on the specification - /// is a filter over the supplied values rather than a layout to build, so every other constraint — those - /// already declared and those declared later — narrows the pool instead of shaping a string. - /// - internal StringSpec WithAllowed(IReadOnlyList values, ConstraintCall applying) { - if (values is null) { throw new ArgumentNullException(nameof(values)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == applying) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _allowedConstraint); } - - string[] distinct = values.Distinct(StringComparer.Ordinal).ToArray(); - - StringSpec candidate = new(_exactLength, _exactConstraint, _minLength, _minConstraint, _maxLength, _maxConstraint, - _prefix, _prefixConstraint, _suffix, _suffixConstraint, _fragments, - _charset, _charsetConstraint, _customPool, _casing, _casingConstraint, _exclusions, - distinct, applying); - - return candidate.Validated(applying, this); - } - - /// - /// The number of distinct values the specification can produce, or null when no allow-list bounds it — - /// a shaped string draws from a domain too wide, and too dependent on the layout, to count. Feeds - /// , so a distinct collection over a pooled generator fails eagerly. - /// - internal long? Cardinality => _effectiveAllowed?.Count; - - /// - /// Whether is one the specification could produce — the exact pool - /// draws from when an allow-list is in force. Without one the answer is false - /// for every value: the two members travel together, and a shaped string - /// advertises no cardinality, so a distinct collection never consults membership on that path (it gates on the - /// bound alone, then falls back to the bounded dedup draw). Answering "outside" is also the side the interface - /// documents as safe — it can only defer, never refuse a satisfiable specification. - /// - internal bool Contains(string value) { - if (value is null) { throw new ArgumentNullException(nameof(value)); } - - return _effectiveAllowed is not null && _effectiveAllowed.Contains(value, StringComparer.Ordinal); - } - - /// - /// Builds one string satisfying the whole specification. With an allow-list the draw is a uniform pick from - /// the surviving pool — every constraint was already applied to it, so there is nothing to redraw. Without - /// one the string is laid out directly, never generate-then-retry; the one redraw is to skip an excluded - /// value, a bounded escape (expected collisions ≈ 0 for any non-trivial shape) whose exhausted budget is - /// reported as the spent budget it is, with the seed to replay. - /// - internal string Generate(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - SeededRandom random = source.Current; - if (_effectiveAllowed is not null) { return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; } - if (_excluded.Count == 0) { return BuildCandidate(random); } - - for (int collisions = 0;;) { - string candidate = BuildCandidate(random); - if (!_excluded.Contains(candidate, StringComparer.Ordinal)) { return candidate; } - if (++collisions >= ExclusionRedrawBudget) { throw Exhausted(source); } - } - } - - private string BuildCandidate(SeededRandom random) { - int required = RequiredLength(); - int effectiveMin = Math.Max(_minLength, required); - // A declared maximum composes with the default spread instead of replacing it (ADR-0050): it may only narrow - // the draw, never widen it, so a loose cap still yields the small unconstrained string. Long arithmetic: a - // huge required length must saturate instead of overflowing past int.MaxValue. - long spreadCeiling = (long)effectiveMin + DefaultLengthSpread; - int effectiveMax = (int)Math.Min(_maxLength is int declared ? Math.Min(spreadCeiling, declared) : spreadCeiling, int.MaxValue); - int length = _exactLength ?? random.NextInt32Inclusive(effectiveMin, effectiveMax); - - string pool = FillerPool(); - int fillerLength = length - required; - int before = random.NextInt32Inclusive(0, fillerLength); - int after = fillerLength - before; - - StringBuilder builder = new(length); - if (_prefix is not null) { builder.Append(_prefix); } - AppendFiller(builder, random, pool, before); - foreach ((string fragment, ConstraintCall _) in _fragments) { builder.Append(fragment); } - AppendFiller(builder, random, pool, after); - if (_suffix is not null) { builder.Append(_suffix); } - - return builder.ToString(); - } - - private AnyGenerationException Exhausted(RandomSource source) { - // A string generator draws only from its own source, so the seed replays the run fully — never the partial hint. - Replay replay = Replay.Of(source); - // The claim is the budget, not impossibility. The redraw is bounded, so an exhausted budget is overwhelming - // evidence that almost nothing survives the exclusions — and the usual cause really is a shape with no value - // left — but it is not a proof: a shape with one free value in a few hundred thousand exhausts the budget - // most of the time and is still satisfiable. Reporting "unsatisfiable" would send a caller hunting for a - // contradiction that need not exist. - string message = - $"Could not generate a string that satisfies the declared shape while excluding {DescribeExcluded()}: " + - $"no candidate survived {V(ExclusionRedrawBudget)} draws. The redraw is bounded, so this is an exhausted " + - "budget rather than a proof that no value remains — though the usual cause is a shape the exclusions " + - "leave nothing of (excluding every value a fixed short length allows). Loosen the exclusions or widen " + - "the shape. " + - replay.Guidance; - - return new AnyGenerationException(message, replay.Seed); - } - - private string DescribeExcluded() { - return string.Join(", ", _excluded.Select(value => $"\"{value}\"")); - } - - /// - /// Cross-validates the whole specification. The layout checks belong to the constructive path only: once an - /// allow-list is in force nothing is laid out, so the single satisfiability question is whether any pooled - /// value survives every declared constraint. is the specification this one was - /// derived from — it tells a conflict message which side was already narrowed and which is the new one. - /// - private StringSpec Validated(ConstraintCall applying, StringSpec previous) { - ValidateLengthBounds(applying); - if (_allowed is null) { - ValidateFragmentBudget(applying); - ValidateFragmentCharacters(applying); - - return this; - } - - ValidateAllowedSurvives(applying, previous); - - return this; - } - - private void ValidateLengthBounds(ConstraintCall applying) { - if (_exactLength is int exact) { ValidateExactAgainstBounds(applying, exact); } - - // Each bound is written as a pair with the constraint that set it. And this branch needs _minLength > max, - // with max >= 0 because AnyString.WithMaxLength rejects a negative length — so _minLength > 0, which only - // WithMinLength can produce, and it names the constraint as it sets the value. - if (_maxLength is int max && _minLength > max) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_maxConstraint!, $"already caps the length at {V(max)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Characters(_minLength)}")); - } - } - - /// - /// Validates a fixed length against a bound already applied; throws naming the bound it contradicts. Symmetric - /// wording, so the message reads whether the last constraint applied was the fixed length or the bound. - /// - private void ValidateExactAgainstBounds(ConstraintCall applying, int exact) { - // Same reasoning: exact >= 0 is guaranteed by the entry points, so exact < _minLength needs _minLength > 0 — - // a declared minimum, hence a named one — and a declared exact length carries its name too. - if (exact < _minLength) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the length at {V(exact)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Characters(_minLength)}")); - } - - if (_maxLength is int cappedAt && exact > cappedAt) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the length at {V(exact)}"), - ConstraintClaim.Of(_maxConstraint!, $"already caps the length at {V(cappedAt)}")); - } - } - - private void ValidateFragmentBudget(ConstraintCall applying) { - int required = RequiredLength(); - if (required == 0) { return; } - - (string description, bool several) = DescribeFragments(); - string requires = several ? "require" : "requires"; - - if (_exactLength is int exact && required > exact) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"allows only {Characters(exact)} while {description} {requires} {V(required)}"), - ConstraintClaim.OfPhrase(description, $"already {requires} {Characters(required)}")); - } - - if (_maxLength is int max && required > max) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_maxConstraint!, $"allows at most {Characters(max)} while {description} {requires} {V(required)}"), - ConstraintClaim.OfPhrase(description, $"already {requires} {Characters(required)}")); - } - } - - private void ValidateFragmentCharacters(ConstraintCall applying) { - foreach ((string kind, string fragment) in Fragments()) { - // A character can only be disallowed by a declared pool or a declared character set, and either is - // written together with the constraint that named it: an offending character proves _charsetConstraint. - char? offendingCharacter = FirstDisallowedCharacter(fragment); - if (offendingCharacter is char outside) { - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_charsetConstraint!, $"does not allow its character '{outside}'"), - ConstraintClaim.OfPhrase($"the {kind} \"{fragment}\"", $"contains '{outside}', which it does not allow")); - } - - if (_casing is LetterCasing casing) { - char? offending = FirstAgainstCasing(fragment, casing); - if (offending is char against) { - string caseName = casing == LetterCasing.Lower ? "uppercase" : "lowercase"; - throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_casingConstraint!, $"forbids its {caseName} letter '{against}'"), - ConstraintClaim.OfPhrase($"the {kind} \"{fragment}\"", $"contains the {caseName} letter '{against}'")); - } - } - } - } - - /// - /// Fails when no pooled value survives every declared constraint, with a message naming exactly the two sides - /// in play and claiming only what the surviving pools establish. - /// - private void ValidateAllowedSurvives(ConstraintCall applying, StringSpec previous) { - if (_effectiveAllowed!.Count > 0) { return; } - - throw ConflictingAnyConstraintException.NoPooledValueSurvives(applying, DescribeEmptyPool(applying, previous)); - } - - private string DescribeEmptyPool(ConstraintCall applying, StringSpec previous) { - // The allow-list is the constraint being applied: the values are new, and the constraints already declared - // are the other side. Name those that reject every single value — the ones the caller must loosen — and stay - // generic when it took a combination of them, since no individual constraint is then the culprit. - if (previous._allowed is null) { - IReadOnlyList culprits = previous.ConstraintsRejectingAll(_allowed!); - if (culprits.Count == 0) { return "no value it offers satisfies the constraints already declared"; } - if (culprits.Count == 1) { return $"{culprits[0]} allows none of its values"; } - - return $"{string.Join(", ", culprits)} allow none of its values"; - } - - // The allow-list was already in force: it is the other side, and the constraint being applied is what - // emptied it. Qualify only when the applied constraint is not the whole story — it admits some value the - // allow-list declared, so the emptiness genuinely took the other constraints too. When it admits none of - // them, loosening the others cannot help, and the qualified form would send the caller at the wrong - // constraint, so the plain claim is both true and the useful one. - return _allowed!.Any(AdmittedBy(applying)) - ? $"no value {previous._allowedConstraint} allows that the other constraints leave satisfies it" - : $"no value {previous._allowedConstraint} allows satisfies it"; - } - - /// - /// The declared constraints that reject every value of , in declaration - /// order. A constraint some value satisfies is not a culprit — naming it would blame a constraint the caller - /// could loosen without changing the verdict. - /// - private IReadOnlyList ConstraintsRejectingAll(IReadOnlyList values) { - List culprits = []; - foreach ((ConstraintCall constraint, Func admits) in DeclaredConstraints()) { - if (!values.Any(admits)) { culprits.Add(constraint); } - } - - return culprits; - } - - /// - /// The test a value must pass to satisfy alone. A constraint the - /// specification does not carry admits everything, which keeps a message that cannot identify its own - /// applied constraint on the weaker, still-true claim rather than the stronger one. - /// - private Func AdmittedBy(ConstraintCall constraint) { - Func[] tests = DeclaredConstraints() - .Where(entry => entry.Constraint == constraint) - .Select(entry => entry.Admits) - .ToArray(); - - return value => tests.All(test => test(value)); - } - - /// - /// Every declared constraint paired with the test a value must pass to satisfy it — the single definition of - /// what the specification demands of a value it did not build. It drives the pool filter, the culprit search - /// and the blame qualification, so the three can never drift apart. - /// - /// - /// Entries are grouped by the constraint as the caller wrote it, and a group's tests are conjoined. - /// One call can set two internal bounds — WithLengthBetween(2, 3) sets both, under one name — and the - /// caller can only loosen the call: judging its halves separately would let a constraint that alone rejects - /// every value escape the blame, because each half on its own admits one. - /// - private IEnumerable<(ConstraintCall Constraint, Func Admits)> DeclaredConstraints() { - return Declarations() - .GroupBy(entry => entry.Constraint) - .Select(group => { - Func[] tests = group.Select(entry => entry.Admits).ToArray(); - - return (group.Key, (Func)(value => tests.All(test => test(value)))); - }); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2249:Consider using String.Contains instead of String.IndexOf", - Justification = - "string.Contains(string, StringComparison) does not exist on netstandard2.0, which this library targets " + - "(ADR-0022). IndexOf with StringComparison.Ordinal is the same comparison and the only spelling that " + - "compiles on the shipped asset. Same downlevel wall as CA1510 (ADR-0058).")] - private IEnumerable<(ConstraintCall Constraint, Func Admits)> Declarations() { - if (_exactLength is int exact) { yield return (_exactConstraint!, value => value.Length == exact); } - if (_minLength > 0) { yield return (_minConstraint!, value => value.Length >= _minLength); } - if (_maxLength is int max) { yield return (_maxConstraint!, value => value.Length <= max); } - if (_prefix is not null) { yield return (_prefixConstraint!, value => value.StartsWith(_prefix, StringComparison.Ordinal)); } - if (_suffix is not null) { yield return (_suffixConstraint!, value => value.EndsWith(_suffix, StringComparison.Ordinal)); } - foreach ((string fragment, ConstraintCall constraint) in _fragments) { - yield return (constraint, value => value.IndexOf(fragment, StringComparison.Ordinal) >= 0); - } - if (_charsetConstraint is not null) { yield return (_charsetConstraint, value => FirstDisallowedCharacter(value) is null); } - if (_casing is LetterCasing casing) { yield return (_casingConstraint!, value => FirstAgainstCasing(value, casing) is null); } - foreach ((ConstraintCall constraint, string[] excluded) in _exclusions) { - yield return (constraint, value => !excluded.Contains(value, StringComparer.Ordinal)); - } - } - - /// Whether satisfies every declared constraint — the allow-list filter. - private bool Admits(string value) { - foreach ((ConstraintCall _, Func admits) in DeclaredConstraints()) { - if (!admits(value)) { return false; } - } - - return true; - } - - private IEnumerable<(string Kind, string Fragment)> Fragments() { - if (_prefix is not null) { yield return ("prefix", _prefix); } - foreach ((string fragment, ConstraintCall _) in _fragments) { yield return ("contained value", fragment); } - if (_suffix is not null) { yield return ("suffix", _suffix); } - } - - private (string Description, bool Several) DescribeFragments() { - List parts = []; - if (_prefix is not null) { parts.Add($"the prefix \"{_prefix}\""); } - foreach ((string fragment, ConstraintCall _) in _fragments) { parts.Add($"the contained value \"{fragment}\""); } - if (_suffix is not null) { parts.Add($"the suffix \"{_suffix}\""); } - - return (string.Join(" and ", parts), parts.Count > 1); - } - - private int RequiredLength() { - int required = (_prefix?.Length ?? 0) + (_suffix?.Length ?? 0); - foreach ((string fragment, ConstraintCall _) in _fragments) { required += fragment.Length; } - - return required; - } - - private char? FirstDisallowedCharacter(string fragment) { - if (_customPool is not null) { - foreach (char character in fragment) { - if (_customPool.IndexOf(character) < 0) { return character; } - } - - return null; - } - - return _charset is CharacterSet charset ? FirstOutsideCharset(fragment, charset) : null; - } - - private static char? FirstOutsideCharset(string fragment, CharacterSet charset) { - foreach (char character in fragment) { - bool allowed = charset switch { - CharacterSet.Alpha => CharacterPools.IsAsciiLetter(character), - CharacterSet.Numeric => CharacterPools.IsAsciiDigit(character), - CharacterSet.AlphaNumeric => CharacterPools.IsAsciiLetter(character) || CharacterPools.IsAsciiDigit(character), - _ => true - }; - if (!allowed) { return character; } - } - - return null; - } - - /// - /// The first character of the declared casing forbids. The test is the Unicode - /// one, not an ASCII range: the constructive filler is ASCII, but an anchored fragment and a pooled value are - /// the caller's own text, so an accented or non-Latin letter must be judged on its actual case rather than - /// waved through — the constraint says "every alphabetic character", and a generator must not emit a value - /// that violates the constraint it was given. - /// - private static char? FirstAgainstCasing(string fragment, LetterCasing casing) { - foreach (char character in fragment) { - if (casing == LetterCasing.Lower && char.IsUpper(character)) { return character; } - if (casing == LetterCasing.Upper && char.IsLower(character)) { return character; } - } - - return null; - } - - private static void AppendFiller(StringBuilder builder, SeededRandom random, string pool, int count) { - for (int i = 0; i < count; i++) { - builder.Append(pool[random.Next(pool.Length)]); - } - } - - private string FillerPool() { - if (_customPool is not null) { return _customPool; } - - string letters = _casing switch { - LetterCasing.Lower => CharacterPools.LowerLetters, - LetterCasing.Upper => CharacterPools.UpperLetters, - _ => CharacterPools.UpperLetters + CharacterPools.LowerLetters - }; - - return _charset switch { - CharacterSet.Alpha => letters, - CharacterSet.Numeric => CharacterPools.Digits, - _ => letters + CharacterPools.Digits - }; - } - -} diff --git a/JustDummies/UnsupportedRegexException.cs b/JustDummies/UnsupportedRegexException.cs deleted file mode 100644 index d7eee349..00000000 --- a/JustDummies/UnsupportedRegexException.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace JustDummies; - -/// -/// Thrown when a pattern passed to is well-formed but uses a construct -/// outside the regular subset the library generates from — a lookahead or lookbehind, a backreference, a -/// balancing group, a Unicode category, a word boundary. These constructs are either not regular (so no finite generator can honour -/// them) or deliberately out of scope; the library refuses to guess rather than silently emit a value that does -/// not actually match. A syntactically malformed pattern is a caller mistake and surfaces as an -/// instead. -/// -public sealed class UnsupportedRegexException : DummyException { - - #region Statics members declarations - - /// - /// Builds the exception for a construct outside the regular subset the library generates from — a lookaround, a - /// backreference, a balancing group, a Unicode category, a word boundary. A grammar limit: the pattern is - /// well-formed, and the construct is one no finite generator can honour or one deliberately out of scope. - /// - internal static UnsupportedRegexException OutsideRegularSubset(string pattern, string construct, int position) { - return Sentence(pattern, construct, position, - "It builds values from the regular subset of the pattern language; lookarounds, backreferences, word boundaries and Unicode categories are outside it. " + - "Express the requirement with the supported subset, or generate the value another way."); - } - - /// - /// Builds the exception for a negated character class that excludes the whole alphabet the library draws from. - /// - /// - /// A universe limit rather than a grammar limit, which is why it has a name of its own: the class is regular and - /// the real engine compiles it, so refusing it as malformed would claim the caller wrote a broken pattern. What - /// it excludes is every character JustDummies can produce, and the remedy is about that range rather than about - /// the supported subset. - /// - internal static UnsupportedRegexException EmptyNegatedClass(string pattern, int position) { - return Sentence(pattern, "a negated character class that excludes every character JustDummies can generate (printable ASCII U+0020 to U+007E)", position, - "It draws values from printable ASCII; express the requirement with characters inside that range, or generate the value another way."); - } - - /// - /// Writes the refusal sentence both factories share, naming the construct, where it occurs, and what to do - /// instead. - /// - /// - /// Private on purpose: it names the grammar of the message, not a failure, so every caller is a named case - /// above. Nothing here guards its arguments — building an exception must never throw, or the failure being - /// reported is replaced by a failure about reporting it (ADR-0045, which exempts exception types for exactly - /// that reason). - /// - private static UnsupportedRegexException Sentence(string pattern, string construct, int position, string remedy) { - return new UnsupportedRegexException($"The regular expression pattern \"{pattern}\" uses {construct} at position {position}, which JustDummies cannot generate from. {remedy}"); - } - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// A description naming the unsupported construct and where it occurs. - public UnsupportedRegexException(string message) : base(message) { } - -} diff --git a/JustDummies/UriSpec.cs b/JustDummies/UriSpec.cs deleted file mode 100644 index ea1e55a1..00000000 --- a/JustDummies/UriSpec.cs +++ /dev/null @@ -1,508 +0,0 @@ -#region Usings declarations - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; - -#endregion - -namespace JustDummies; - -/// The URI families the builder can produce, one per distinct valid shape. -internal enum UriFamily { - - Web, // http / https — full authority: host, userinfo, port, path, query, fragment - WebSocket, // ws / wss — authority without userinfo or fragment (RFC 6455) - Ftp, // ftp — authority without query or fragment - Mailto, // mailto — local@domain (+ optional headers) - Relative // no scheme — path (+ optional query, fragment) - -} - -/// How the path component is drawn. -internal enum UriPathMode { - - Auto, // 0 to 2 arbitrary segments - Root, // no segments (an authority family still renders "/") - Exact // a fixed number of segments - -} - -/// -/// The immutable specification behind every AnyUri family builder. It records the constrained family and -/// scheme plus the per-component pins, and assembles a directly — every component is -/// drawn from ASCII-unreserved characters, so a generated URI is valid by construction (never a throw, never a -/// retry) and reproducible under a seed on every target framework. Contradictory constraints fail eagerly with a -/// naming both sides; an invalid pinned component fails as an -/// argument at the call site. -/// -internal sealed class UriSpec { - - #region Constants - - private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; - private const string LowerAlphaNum = "abcdefghijklmnopqrstuvwxyz0123456789"; - private const string Unreserved = "abcdefghijklmnopqrstuvwxyz0123456789-._~"; - private const int MinDynamicPort = 1025; // above every default we emit (http 80, https 443, ftp 21, ws 80, wss 443) - - /// The lowest port an authority may carry; zero is reserved and never appears in a URI. - private const int MinPort = 1; - - /// The highest port an authority may carry — a port number is sixteen bits wide. - private const int MaxPort = 65535; - - /// The highest code point that is still ASCII; above it a host is internationalized (IDN). - private const int MaxAsciiCodePoint = 127; - - /// How many dot-separated octets a dotted-quad IPv4 literal is written with. - private const int Ipv4OctetCount = 4; - - /// The most digits a canonical IPv4 octet spells — the width of "255". - private const int MaxOctetDigits = 3; - - /// The highest value an IPv4 octet holds — an octet is one byte. - private const int MaxOctetValue = 255; - - /// How many schemes the Web and WebSocket families draw between: http/https, and ws/wss. - private const int SchemesPerFamily = 2; - - // The lengths and counts below decide how a generated URI LOOKS. Nothing in the grammar requires these particular - // numbers — they are chosen to read as a plausible URI in a failing test's output without burying it, and they are - // gathered here because each one is otherwise invisible at its call site, spelled as a bare pair of bounds passed - // to Draw. Changing one changes the look of every URI the library generates. - - /// The shortest user, password or mailto subject drawn. - private const int MinUserInfoLength = 3; - - /// The longest user, password or mailto subject drawn. - private const int MaxUserInfoLength = 8; - - /// The shortest path segment, fragment, or standalone relative reference drawn. - private const int MinTokenLength = 1; - - /// The longest path segment, fragment, or standalone relative reference drawn. - private const int MaxTokenLength = 8; - - /// The shortest key or value inside a generated query pair. - private const int MinQueryTokenLength = 1; - - /// The longest key or value inside a generated query pair. - private const int MaxQueryTokenLength = 6; - - /// The shortest top-level domain drawn. - private const int MinTldLength = 2; - - /// The longest top-level domain drawn. - private const int MaxTldLength = 4; - - /// The longest tail drawn after a DNS label's mandatory leading letter, so a label spans one to eight characters. - private const int MaxLabelTailLength = 7; - - /// The most segments an unconstrained () path draws; it may also draw none. - private const int MaxAutoPathSegments = 2; - - /// The fewest key/value pairs a generated query string carries. - private const int MinQueryPairs = 1; - - /// The most key/value pairs a generated query string carries. - private const int MaxQueryPairs = 2; - - #endregion - - #region Statics members declarations - - internal static readonly UriSpec Unconstrained = new(null, null, null, null, - false, null, null, - false, null, - UriPathMode.Auto, 0, - false, false, false); - - private static readonly UriFamily[] DefaultFamilies = { UriFamily.Web, UriFamily.WebSocket, UriFamily.Ftp, UriFamily.Mailto, UriFamily.Relative }; - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - // Renders the PUBLIC call that pinned a component, so a conflict message names what the caller wrote rather - // than the internal setter it reached. The method name is supplied because one spec setter backs several - // public spellings — a mailto's WithDomain and a web URI's WithHost both pin the host. What is left here once - // ConstraintCall owns the punctuation is the rendering a URI needs: a component is quoted, because an empty or - // space-bearing host is exactly the argument a caller has to see verbatim to recognize it. - internal static ConstraintCall Label(string method) { - if (method is null) { throw new ArgumentNullException(nameof(method)); } - - return ConstraintCall.Of(method); - } - - internal static ConstraintCall Label(string method, int value) { - if (method is null) { throw new ArgumentNullException(nameof(method)); } - - return ConstraintCall.Of(method, V(value)); - } - - internal static ConstraintCall Label(string method, string value) { - if (method is null) { throw new ArgumentNullException(nameof(method)); } - if (value is null) { throw new ArgumentNullException(nameof(value)); } - - return ConstraintCall.Of(method, Quoted(value)); - } - - internal static ConstraintCall Label(string method, string first, string second) { - if (method is null) { throw new ArgumentNullException(nameof(method)); } - if (first is null) { throw new ArgumentNullException(nameof(first)); } - if (second is null) { throw new ArgumentNullException(nameof(second)); } - - return ConstraintCall.Of(method, Quoted(first), Quoted(second)); - } - - private static string Quoted(string value) { - return "\"" + value + "\""; - } - - #endregion - - #region Fields declarations - - private readonly UriFamily? _family; - private readonly string? _scheme; // pinned concrete scheme within the family (e.g. "https") - private readonly ConstraintCall? _schemeConstraint; // the call that pinned it, for a conflict message - private readonly string? _host; // pinned host / mailto domain - private readonly ConstraintCall? _hostConstraint; // the call that pinned it, for a conflict message - private readonly ConstraintCall? _userInfoConstraint; - private readonly ConstraintCall? _portConstraint; - private readonly bool _hasUserInfo; - private readonly string? _user; - private readonly string? _password; - private readonly bool _hasPort; - private readonly int? _port; - private readonly UriPathMode _pathMode; - private readonly ConstraintCall? _pathConstraint; - private readonly int _pathSegments; - private readonly bool _hasQuery; - private readonly bool _hasFragment; - private readonly bool _rooted; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private UriSpec(UriFamily? family, string? scheme, ConstraintCall? schemeConstraint, string? host, - bool hasUserInfo, string? user, string? password, - bool hasPort, int? port, - UriPathMode pathMode, int pathSegments, - bool hasQuery, bool hasFragment, bool rooted, - ConstraintCall? pathConstraint = null, ConstraintCall? hostConstraint = null, - ConstraintCall? userInfoConstraint = null, ConstraintCall? portConstraint = null) { - _family = family; - _scheme = scheme; - _schemeConstraint = schemeConstraint; - _host = host; - _hasUserInfo = hasUserInfo; - _user = user; - _password = password; - _hasPort = hasPort; - _port = port; - _pathMode = pathMode; - _pathConstraint = pathConstraint; - _hostConstraint = hostConstraint; - _userInfoConstraint = userInfoConstraint; - _portConstraint = portConstraint; - _pathSegments = pathSegments; - _hasQuery = hasQuery; - _hasFragment = hasFragment; - _rooted = rooted; - } - - #region Family narrowing - - internal UriSpec WithFamily(UriFamily family) { - return new UriSpec(family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, _hasFragment, _rooted, _pathConstraint, _hostConstraint, _userInfoConstraint, _portConstraint); - } - - internal UriSpec WithScheme(string scheme, ConstraintCall applying) { - if (scheme is null) { throw new ArgumentNullException(nameof(scheme)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_schemeConstraint == applying) { return this; } - if (_schemeConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _schemeConstraint); } - - return new UriSpec(_family, scheme, applying, _host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, _hasFragment, _rooted, _pathConstraint, _hostConstraint, _userInfoConstraint, _portConstraint); - } - - #endregion - - #region Component pins - - // A URI has ONE host, ONE user-info and ONE port, so a second declaration of any of them can never be - // satisfied alongside the first. Each is therefore declared once, exactly like the scheme and the path: - // silently keeping the last value would drop a constraint the caller wrote, which is the one outcome the - // eager check exists to prevent. Repeating the SAME declaration stays a no-op. - internal UriSpec WithHost(string host, ConstraintCall applying) { - if (host is null) { throw new ArgumentNullException(nameof(host)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_hostConstraint == applying) { return this; } - if (_hostConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _hostConstraint); } - - return new UriSpec(_family, _scheme, _schemeConstraint, host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, _hasFragment, _rooted, _pathConstraint, applying, _userInfoConstraint, _portConstraint); - } - - internal UriSpec WithUserInfo(string? user, string? password, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_userInfoConstraint == applying) { return this; } - if (_userInfoConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _userInfoConstraint); } - - return new UriSpec(_family, _scheme, _schemeConstraint, _host, true, user, password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, _hasFragment, _rooted, _pathConstraint, _hostConstraint, applying, _portConstraint); - } - - internal UriSpec WithPort(int? port, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (_portConstraint == applying) { return this; } - if (_portConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _portConstraint); } - - return new UriSpec(_family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - true, port, _pathMode, _pathSegments, _hasQuery, _hasFragment, _rooted, _pathConstraint, _hostConstraint, _userInfoConstraint, applying); - } - - internal UriSpec WithPath(UriPathMode mode, int segments, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_pathConstraint == applying) { return this; } - if (_pathConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _pathConstraint); } - - return new UriSpec(_family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - _hasPort, _port, mode, segments, _hasQuery, _hasFragment, _rooted, applying, _hostConstraint, _userInfoConstraint, _portConstraint); - } - - internal UriSpec WithQuery() { - return new UriSpec(_family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, true, _hasFragment, _rooted, _pathConstraint, _hostConstraint, _userInfoConstraint, _portConstraint); - } - - internal UriSpec WithFragment() { - return new UriSpec(_family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, true, _rooted, _pathConstraint, _hostConstraint, _userInfoConstraint, _portConstraint); - } - - internal UriSpec Rooted() { - return new UriSpec(_family, _scheme, _schemeConstraint, _host, _hasUserInfo, _user, _password, - _hasPort, _port, _pathMode, _pathSegments, _hasQuery, _hasFragment, true, _pathConstraint); - } - - #endregion - - #region Generation - - internal Uri Generate(RandomSource source) { - if (source is null) { throw new ArgumentNullException(nameof(source)); } - SeededRandom random = source.Current; - UriFamily family = _family ?? DefaultFamilies[random.Next(DefaultFamilies.Length)]; - - return family == UriFamily.Relative - ? new Uri(BuildRelative(source), UriKind.Relative) - : new Uri(BuildAbsolute(family, random), UriKind.Absolute); - } - - private string BuildAbsolute(UriFamily family, SeededRandom random) { - string scheme = ResolveScheme(family, random); - StringBuilder builder = new(); - builder.Append(scheme).Append(':'); - - if (family == UriFamily.Mailto) { - builder.Append(_user ?? Draw(random, LowerAlphaNum, MinUserInfoLength, MaxUserInfoLength)); - builder.Append('@'); - builder.Append(_host ?? Host(random)); - if (_hasQuery) { builder.Append("?subject=").Append(Draw(random, LowerAlphaNum, MinUserInfoLength, MaxUserInfoLength)); } - - return builder.ToString(); - } - - builder.Append("//"); - if (AllowsUserInfo(family) && _hasUserInfo) { - builder.Append(_user ?? Draw(random, LowerAlphaNum, MinUserInfoLength, MaxUserInfoLength)); - builder.Append(':').Append(_password ?? Draw(random, LowerAlphaNum, MinUserInfoLength, MaxUserInfoLength)); - builder.Append('@'); - } - - builder.Append(_host ?? Host(random)); - if (_hasPort) { builder.Append(':').Append(V(_port ?? random.Next(MinDynamicPort, MaxPort + 1))); } - - builder.Append(Path(random, leadingSlash: true)); - if (AllowsQuery(family) && _hasQuery) { builder.Append(Query(random)); } - if (AllowsFragment(family) && _hasFragment) { builder.Append('#').Append(Draw(random, LowerAlphaNum, MinTokenLength, MaxTokenLength)); } - - return builder.ToString(); - } - - private string BuildRelative(RandomSource source) { - SeededRandom random = source.Current; - StringBuilder builder = new(); - builder.Append(Path(random, leadingSlash: _rooted)); - if (_hasQuery) { builder.Append(Query(random)); } - if (_hasFragment) { builder.Append('#').Append(Draw(random, LowerAlphaNum, MinTokenLength, MaxTokenLength)); } - - string result = builder.ToString(); - if (result.Length > 0) { return result; } - - // The reference rendered empty, which is not a valid URI. An unconstrained (Auto) path incidentally drew zero - // segments — resolve it to an arbitrary segment. An explicit WithPathSegments(0) with no query, fragment or - // root asked for the empty reference, which cannot generate: surface it with the seed to replay, like the - // library's other unsatisfiable specs. - if (_pathMode == UriPathMode.Exact) { - throw AnyGenerationException.EmptyRelativeReference(Replay.Of(source)); - } - - return Draw(random, LowerAlphaNum, MinTokenLength, MaxTokenLength); - } - - private string ResolveScheme(UriFamily family, SeededRandom random) { - if (_scheme is not null) { return _scheme; } - - return family switch { - UriFamily.Web => random.Next(SchemesPerFamily) == 0 ? "http" : "https", - UriFamily.WebSocket => random.Next(SchemesPerFamily) == 0 ? "ws" : "wss", - UriFamily.Ftp => "ftp", - UriFamily.Mailto => "mailto", - _ => throw new InvalidOperationException("Relative URIs have no scheme.") - }; - } - - private string Path(SeededRandom random, bool leadingSlash) { - int count = _pathMode switch { - UriPathMode.Root => 0, - UriPathMode.Exact => _pathSegments, - _ => random.Next(MaxAutoPathSegments + 1) - }; - - if (count == 0) { return leadingSlash ? "/" : string.Empty; } - - StringBuilder builder = new(); - for (int i = 0; i < count; i++) { - if (leadingSlash || i > 0) { builder.Append('/'); } - builder.Append(Draw(random, LowerAlphaNum, MinTokenLength, MaxTokenLength)); - } - - return builder.ToString(); - } - - private static string Query(SeededRandom random) { - int pairs = random.Next(MinQueryPairs, MaxQueryPairs + 1); - StringBuilder builder = new("?"); - for (int i = 0; i < pairs; i++) { - if (i > 0) { builder.Append('&'); } - builder.Append(Draw(random, LowerAlphaNum, MinQueryTokenLength, MaxQueryTokenLength)).Append('=').Append(Draw(random, LowerAlphaNum, MinQueryTokenLength, MaxQueryTokenLength)); - } - - return builder.ToString(); - } - - private static string Host(SeededRandom random) { - return Label(random) + "." + Draw(random, LowerLetters, MinTldLength, MaxTldLength); - } - - private static string Label(SeededRandom random) { - // A DNS-safe label: starts with a letter, then letters/digits — no leading digit, no hyphen edges. - return LowerLetters[random.Next(LowerLetters.Length)].ToString() + Draw(random, LowerAlphaNum, 0, MaxLabelTailLength); - } - - private static string Draw(SeededRandom random, string pool, int min, int max) { - int length = min == max ? min : random.Next(min, max + 1); - StringBuilder builder = new(length); - for (int i = 0; i < length; i++) { builder.Append(pool[random.Next(pool.Length)]); } - - return builder.ToString(); - } - - private static bool AllowsUserInfo(UriFamily family) { - return family is UriFamily.Web or UriFamily.Ftp; - } - - private static bool AllowsQuery(UriFamily family) { - return family is UriFamily.Web or UriFamily.WebSocket; - } - - private static bool AllowsFragment(UriFamily family) { - return family is UriFamily.Web; - } - - #endregion - - #region Argument validation helpers (shared by the public builders) - - internal static string RequireHost(string host, string parameterName) { - if (host is null) { throw new ArgumentNullException(parameterName); } - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - if (host.Length == 0) { throw new ArgumentException("The host must not be empty.", parameterName); } - if (host.Any(character => character > MaxAsciiCodePoint)) { - throw new ArgumentException("The host must be ASCII: an internationalized (IDN) host would not round-trip identically across target frameworks. Pass the punycode form instead (e.g. \"xn--mnchen-3ya.de\").", parameterName); - } - if (Uri.CheckHostName(host) == UriHostNameType.Unknown) { - throw new ArgumentException($"\"{host}\" is not a valid host name.", parameterName); - } - - // A host of only digits and dots is interpreted as an IPv4 literal by System.Uri, and its shorthand forms - // ("1" -> "0.0.0.1") parse differently across target frameworks — the same determinism hazard as an IDN host. - // Reject any such host that is not already a canonical four-octet dotted-quad, with a framework-independent - // check (never through System.Uri, whose parsing is the very thing that differs). - if (host.All(character => character is >= '0' and <= '9' or '.') && !IsCanonicalIpv4(host)) { - throw new ArgumentException($"The host \"{host}\" looks like a shorthand IPv4 literal, which System.Uri parses differently across target frameworks. Pass a DNS host name, or a canonical dotted-quad such as \"1.2.3.4\".", parameterName); - } - - return host; - } - - private static bool IsCanonicalIpv4(string host) { - string[] parts = host.Split('.'); - if (parts.Length != Ipv4OctetCount) { return false; } - foreach (string part in parts) { - if (part.Length is 0 or > MaxOctetDigits) { return false; } - if (part.Length > 1 && part[0] == '0') { return false; } // a leading zero is a non-canonical (octal-ish) octet - if (!int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out int octet) || octet > MaxOctetValue) { return false; } - } - - return true; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop exists to name the FIRST offending element in the exception it throws. A Where clause discards which element failed, so " + - "the message would have to re-find it, turning one pass into two and one statement into three.")] - internal static string RequireUserInfoPart(string value, string parameterName) { - if (value is null) { throw new ArgumentNullException(parameterName); } - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - foreach (char character in value) { - if (Unreserved.IndexOf(char.ToLowerInvariant(character)) < 0) { - throw new ArgumentException($"The user-info part may only contain unreserved characters (letters, digits, '-', '.', '_', '~'); '{character}' is not allowed.", parameterName); - } - } - - return value; - } - - internal static int RequirePort(int port, string parameterName) { - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - if (port is < MinPort or > MaxPort) { throw new ArgumentOutOfRangeException(parameterName, port, $"The port must be between {V(MinPort)} and {V(MaxPort)}."); } - - return port; - } - - internal static int RequireSegmentCount(int count, string parameterName) { - if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } - if (count < 0) { throw new ArgumentOutOfRangeException(parameterName, count, "The segment count must not be negative."); } - - return count; - } - - #endregion - -} diff --git a/JustDummies/ValueObjectAttribute.cs b/JustDummies/ValueObjectAttribute.cs deleted file mode 100644 index 2d418ceb..00000000 --- a/JustDummies/ValueObjectAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace JustDummies; - -/// -/// Marks a type whose instances are values: two of them holding the same thing are the same one, and nothing -/// about which instance you hold matters. -/// -/// -/// A reference type answers "is this the same one?" by identity unless somebody writes another answer, and it -/// answers silently — no compiler warning, no failing test, just a comparison that quietly means something else. -/// The marker turns that into a contract ValueObjectConventionTests enforces by reflection: a marked type -/// is sealed, immutable, and carries the full set — , both -/// Equals overloads, GetHashCode, and the ==/!= pair, whose absence is the silent -/// case since it degrades to reference comparison rather than failing to compile. -/// -/// It is a declaration, not a detection. Immutability alone does not make a value: the generators and the -/// specifications are immutable too, yet two identically constrained generators are two recipes, not one -/// value, and comparing them would claim a meaning they do not have. Only a type that says it is a value is -/// held to the contract. -/// -/// -/// Marking a struct is rejected by the same convention rather than by usage rules alone: a struct exposes a -/// parameterless constructor that yields an instance bypassing every validating factory, which is why a value -/// enforcing an invariant is a class throughout this repository. -/// -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -internal sealed class ValueObjectAttribute : Attribute { } diff --git a/JustDummies/WideIntervalSpec.cs b/JustDummies/WideIntervalSpec.cs deleted file mode 100644 index c4f7feae..00000000 --- a/JustDummies/WideIntervalSpec.cs +++ /dev/null @@ -1,420 +0,0 @@ -#if NET8_0_OR_GREATER -namespace JustDummies; - -/// -/// The 128-bit sibling of , backing and -/// : their ordinal space exceeds 64 bits, so the same algebra — descriptor-tracked -/// inclusive bounds, allow-list, exclusions, an optional lattice (MultipleOf), eager conflicts, one-draw -/// generation — runs over ordinals. Net8-only, like the types it serves. -/// -internal sealed class WideIntervalSpec { - - #region Statics members declarations - - internal static WideIntervalSpec Unconstrained(string typeName, Func render, UInt128 domainMin, UInt128 domainMax) { - if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } - if (render is null) { throw new ArgumentNullException(nameof(render)); } - - return new WideIntervalSpec(typeName, render, domainMin, domainMax, domainMin, null, domainMax, null, null, null, [], UInt128.One, UInt128.Zero, null); - } - - private static UInt128 NextUInt128(SeededRandom random) { - return new UInt128(random.NextUInt64(), random.NextUInt64()); - } - - /// Whether sits on the lattice anchored at with the given step. - private static bool IsOnLattice(UInt128 ordinal, UInt128 anchor, UInt128 step) { - UInt128 delta = ordinal >= anchor ? ordinal - anchor : anchor - ordinal; - - return delta % step == UInt128.Zero; - } - - /// - /// The smallest lattice ordinal at or above , staying within [min, max]. Returns - /// false when none exists (the stride steps past , or the domain top overflows). - /// - private static bool TryFirstLatticePoint(UInt128 min, UInt128 max, UInt128 anchor, UInt128 step, out UInt128 first) { - if (min >= anchor) { - UInt128 ahead = (min - anchor) % step; - if (ahead == UInt128.Zero) { - first = min; - } else { - first = min + (step - ahead); - if (first < min) { first = UInt128.Zero; return false; } // wrapped past the top of the ordinal domain - } - } else { - first = min + (anchor - min) % step; - } - - return first <= max; - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly ConstraintCall? _allowedConstraint; - private readonly UInt128 _anchor; - private readonly UInt128 _domainMax; - private readonly List? _effectiveAllowed; - private readonly List _excludedInRange; - private readonly List _excludedOnLattice; - private readonly UInt128 _domainMin; - private readonly IReadOnlyList<(ConstraintCall Constraint, UInt128[] Ordinals)> _exclusions; - private readonly UInt128 _latticeFirst; - private readonly bool _latticeHasPoint; - private readonly UInt128 _max; - private readonly ConstraintCall? _maxConstraint; - private readonly UInt128 _min; - private readonly ConstraintCall? _minConstraint; - private readonly Func _render; - private readonly UInt128 _step; - private readonly ConstraintCall? _stepConstraint; - private readonly string _typeName; - - #endregion - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", - Justification = - "This private constructor carries the engine's whole immutable state: the 'constrain once, draw many' design rebuilds the spec on " + - "every With* call, so every field has to be threaded through it. A parameter object would only rename the same list, and the " + - "constructor is private — no caller ever writes this argument list.")] - private WideIntervalSpec(string typeName, Func render, UInt128 domainMin, UInt128 domainMax, - UInt128 min, ConstraintCall? minConstraint, - UInt128 max, ConstraintCall? maxConstraint, - IReadOnlyList? allowed, ConstraintCall? allowedConstraint, - IReadOnlyList<(ConstraintCall Constraint, UInt128[] Ordinals)> exclusions, - UInt128 step, UInt128 anchor, ConstraintCall? stepConstraint) { - _typeName = typeName; - _render = render; - _domainMin = domainMin; - _domainMax = domainMax; - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _exclusions = exclusions; - _step = step; - _anchor = anchor; - _stepConstraint = stepConstraint; - // The flat ordinal set drives every hot-path decision; the provenance in _exclusions is consulted only - // when a conflict message must name the excluding constraint. Materialized once here — "constrain once, - // draw many": GenerateOrdinal never refilters or resorts. - UInt128[] excluded = exclusions.SelectMany(pair => pair.Ordinals).ToArray(); - _excludedInRange = excluded.Where(value => value >= min && value <= max).Distinct().ToList(); - _excludedInRange.Sort(); - if (step > UInt128.One) { - _latticeHasPoint = TryFirstLatticePoint(min, max, anchor, step, out _latticeFirst); - _excludedOnLattice = _excludedInRange.Where(value => IsOnLattice(value, anchor, step)).ToList(); // stays sorted: filtered from a sorted list - } else { - _latticeHasPoint = true; - _latticeFirst = min; - _excludedOnLattice = _excludedInRange; - } - - if (allowed is not null) { - HashSet forbidden = [.. excluded]; - _effectiveAllowed = allowed.Where(value => value >= min && value <= max && !forbidden.Contains(value) && (step <= UInt128.One || IsOnLattice(value, anchor, step))).ToList(); - } - } - - /// Tightens the lower bound; a looser bound than the current one is a no-op. - internal WideIntervalSpec WithMinimum(UInt128 minimum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (minimum <= _min) { return this; } - - if (minimum > _max) { - if (_maxConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedAbove(applying, _maxConstraint, _render(_max)); - } - - return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Tightens the lower bound to strictly above — the exclusive form of . - internal WideIntervalSpec WithMinimumAbove(UInt128 bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (bound == _domainMax) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - return WithMinimum(bound + 1, applying); - } - - /// Tightens the upper bound; a looser bound than the current one is a no-op. - internal WideIntervalSpec WithMaximum(UInt128 maximum, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (maximum >= _max) { return this; } - - if (maximum < _min) { - if (_minConstraint is null) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - throw ConflictingAnyConstraintException.AlreadyBoundedBelow(applying, _minConstraint, _render(_min)); - } - - return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Tightens the upper bound to strictly below — the exclusive form of . - internal WideIntervalSpec WithMaximumBelow(UInt128 bound, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (bound == _domainMin) { throw ConflictingAnyConstraintException.NoValueSatisfies(applying, _typeName); } - - return WithMaximum(bound - 1, applying); - } - - /// Restricts the domain to an explicit allow-list; declared once per generator. - internal WideIntervalSpec WithAllowed(UInt128[] ordinals, ConstraintCall applying) { - if (ordinals is null) { throw new ArgumentNullException(nameof(ordinals)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - // Re-declaring the SAME constraint is not a contradiction, so it is a no-op rather than a - // conflict: the second declaration asks for exactly what the first already guarantees. - if (_allowedConstraint == applying) { return this; } - if (_allowedConstraint is not null) { throw ConflictingAnyConstraintException.AlreadyDefined(applying, _allowedConstraint); } - - UInt128[] distinct = ordinals.Distinct().ToArray(); - - return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// Adds values the generator must never produce. - internal WideIntervalSpec WithExcluded(UInt128[] ordinals, ConstraintCall applying) { - if (ordinals is null) { throw new ArgumentNullException(nameof(ordinals)); } - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - - // The applied constraint tags its own ordinals, so a later exhaustion message can name the exclusion - // that actually emptied the domain rather than a bound that merely happens to border it. - List<(ConstraintCall Constraint, UInt128[] Ordinals)> exclusions = [.. _exclusions, (applying, ordinals)]; - - return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, exclusions, _step, _anchor, _stepConstraint), applying); - } - - /// - /// Restricts the domain to a lattice: values a multiple of away from - /// — a known lattice ordinal, the ordinal of the value 0. Declared once per - /// generator. - /// - internal WideIntervalSpec WithStep(UInt128 step, UInt128 anchor, ConstraintCall applying) { - if (applying is null) { throw new ArgumentNullException(nameof(applying)); } - if (step <= UInt128.One) { return this; } // every value is a multiple of one: a no-op, not a constraint - - if (_step > UInt128.One) { - if (_step == step && _anchor == anchor) { return this; } - - // _step and _stepConstraint are written as a pair by the constructor and rethreaded as a pair by every - // rebuild, so a declared step always carries the name of the constraint that declared it. - throw ConflictingAnyConstraintException.AlreadyDefined(applying, _stepConstraint!); - } - - return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, _exclusions, step, anchor, applying), applying); - } - - /// - /// The number of distinct values the specification can produce, or null when the interval is full-width - /// or wider than (a range too vast to ever conflict with a collection count). - /// Feeds , so a distinct collection over a narrow 128-bit range or allow-list - /// can fail eagerly. - /// - internal long? Cardinality { - get { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } - if (_step > UInt128.One) { - if (!_latticeHasPoint) { return 0; } - - UInt128 onLattice = (_max - _latticeFirst) / _step + 1 - (UInt128)_excludedOnLattice.Count; - - return onLattice <= (UInt128)long.MaxValue ? (long)onLattice : null; - } - if (IsFullWidth()) { return null; } - - UInt128 count = _max - _min + 1 - (UInt128)_excludedInRange.Count; - - return count <= (UInt128)long.MaxValue ? (long)count : null; - } - } - - /// - /// Whether is a value the specification could produce — the exact domain - /// draws from. Feeds . - /// - internal bool Contains(UInt128 ordinal) { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(ordinal); } - if (_step > UInt128.One && !IsOnLattice(ordinal, _anchor, _step)) { return false; } - - return ordinal >= _min && ordinal <= _max && !_excludedInRange.Contains(ordinal); - } - - /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop body advances the very accumulator the condition tests — each iteration changes what the next one compares against — so " + - "the filter cannot be lifted out of the loop. A Where clause would evaluate every predicate against the value the accumulator " + - "held on entry and silently skip exclusions.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with LINQ expressions", - Justification = - "The loop body advances the very accumulator the condition tests — each iteration changes what the next one compares against — so " + - "the filter cannot be lifted out of the loop. A Where clause would evaluate every predicate against the value the accumulator " + - "held on entry and silently skip exclusions.")] - internal UInt128 GenerateOrdinal(SeededRandom random) { - if (random is null) { throw new ArgumentNullException(nameof(random)); } - - if (_effectiveAllowed is not null) { - return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; - } - - if (_step > UInt128.One) { - // The lattice caps the count below the full 128-bit width, so the full-width special case never - // applies here. Draw an index over the surviving lattice points, then shift past any excluded - // lattice point at or below the drawn ordinal. - UInt128 latticeCount = (_max - _latticeFirst) / _step + 1; - UInt128 validCount = latticeCount - (UInt128)_excludedOnLattice.Count; - UInt128 ordinal = _latticeFirst + NextUInt128(random) % validCount * _step; - foreach (UInt128 value in _excludedOnLattice) { - if (ordinal >= value) { ordinal += _step; } - } - - return ordinal; - } - - List excluded = _excludedInRange; - if (IsFullWidth()) { - // Same escape as OrdinalIntervalSpec: the full 128-bit space has no representable size, so draw - // anywhere and walk off an excluded value deterministically. - UInt128 candidate = NextUInt128(random); - while (excluded.Contains(candidate)) { candidate = unchecked(candidate + 1); } - - return candidate; - } - - UInt128 size = _max - _min + 1 - (UInt128)excluded.Count; - UInt128 candidateOrdinal = _min + NextUInt128(random) % size; - foreach (UInt128 value in excluded) { - if (candidateOrdinal >= value) { candidateOrdinal++; } - } - - return candidateOrdinal; - } - - private bool IsFullWidth() { - return _min == UInt128.MinValue && _max == UInt128.MaxValue; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S2325:Methods and properties that do not access instance data should be static", - Justification = - "Validated is the uniform validation hook of the fluent builders: every With* method routes its candidate through it, and all " + - "seven engines declare it with the same signature. It reads the CANDIDATE's state rather than this instance's — which is what " + - "the rule notices — but that is a builder validating its own successor, not an oversight. Making it static across seven types " + - "would break a family resemblance the reader relies on, for no measurable gain on a path that runs once per declared " + - "constraint.")] - private WideIntervalSpec Validated(WideIntervalSpec candidate, ConstraintCall applying) { - if (candidate.IsSatisfiable()) { return candidate; } - - throw ConflictingAnyConstraintException.NoValueRemains(applying, candidate.DescribeExhaustion(applying)); - } - - private bool IsSatisfiable() { - if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } - if (_step > UInt128.One) { - if (!_latticeHasPoint) { return false; } - - return (_max - _latticeFirst) / _step + 1 > (UInt128)_excludedOnLattice.Count; - } - if (IsFullWidth()) { return true; } - - return _max - _min + 1 - (UInt128)_excludedInRange.Count > 0; - } - - private string DescribeExhaustion(ConstraintCall applying) { - IReadOnlyList culprits = ExcludingConstraintsInEffect(); - - if (_allowed is not null) { - if (culprits.Count == 0) { return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; } - - // Only the allow-list values the bounds and lattice still permit can be forbidden by an exclusion; if - // some allowed value was already dropped by a bound or the lattice, the exclusions do not forbid - // "every" allowed value, so the claim is qualified rather than overstated. - string allowed = _allowed.All(WouldAllowIgnoringExclusions) - ? $"every value {_allowedConstraint} allows" - : $"every value {_allowedConstraint} allows that the other constraints leave"; - - return $"{Forbids(culprits, applying)} {allowed}"; - } - - if (_step > UInt128.One) { - if (!_latticeHasPoint || culprits.Count == 0) { return $"no {_typeName} value {_stepConstraint} allows remains between {_render(_min)} and {_render(_max)}"; } - - return $"{Forbids(culprits, applying)} every {_stepConstraint} value between {_render(_min)} and {_render(_max)}"; - } - - if (_min == _max) { - if (culprits.Count == 0) { - string pinning = _minConstraint?.ToString() ?? _maxConstraint?.ToString() ?? "the declared bounds"; - - return $"{pinning} already pins the value to {_render(_min)}"; - } - - return $"{Forbids(culprits, applying)} {_render(_min)}, {PinningClause()}"; - } - - if (culprits.Count == 0) { return $"no value remains between {_render(_min)} and {_render(_max)} once the excluded values are removed"; } - - return $"{Forbids(culprits, applying)} every value between {_render(_min)} and {_render(_max)}"; - } - - /// - /// The distinct exclusion constraints that actually caused the exhaustion — those forbidding at least one - /// value the interval, lattice and allow-list would otherwise permit. An exclusion whose values fall outside - /// the surviving domain never bit, so naming it would mislead; first-declared order is preserved. - /// - private IReadOnlyList ExcludingConstraintsInEffect() { - List names = []; - foreach ((ConstraintCall constraint, UInt128[] ordinals) in _exclusions) { - if (names.Contains(constraint)) { continue; } - if (ordinals.Any(WouldAllowIgnoringExclusions)) { names.Add(constraint); } - } - - return names; - } - - /// Whether would be in the domain if no exclusion were applied. - private bool WouldAllowIgnoringExclusions(UInt128 ordinal) { - if (_allowed is not null && !_allowed.Contains(ordinal)) { return false; } - if (_step > UInt128.One && !IsOnLattice(ordinal, _anchor, _step)) { return false; } - - return ordinal >= _min && ordinal <= _max; - } - - /// - /// The subject of the exhaustion clause. A single culprit that is the constraint being applied becomes "it", - /// so the message reads "Cannot apply Except(1) because it forbids …" rather than repeating the constraint on - /// both sides of "because". - /// - private static string Forbids(IReadOnlyList names, ConstraintCall applying) { - if (names.Count == 1) { return names[0] == applying ? "it forbids" : $"{names[0]} forbids"; } - - return $"{string.Join(", ", names)} forbid"; - } - - /// Names the bounds that pinned the domain to its single value, for the "forbids X, the only value ... leaves" form. - private string PinningClause() { - List bounds = []; - if (_minConstraint is not null) { bounds.Add(_minConstraint); } - if (_maxConstraint is not null && _maxConstraint != _minConstraint) { bounds.Add(_maxConstraint); } - - if (bounds.Count == 0) { return "the only value the declared bounds leave"; } - if (bounds.Count == 1) { return $"the only value {bounds[0]} leaves"; } - - return $"the only value {string.Join(" and ", bounds)} leave"; - } - -} -#endif diff --git a/build/PublicApiBaseline.props b/build/PublicApiBaseline.props index 3343f1cb..7c286a20 100644 --- a/build/PublicApiBaseline.props +++ b/build/PublicApiBaseline.props @@ -10,7 +10,7 @@ Only the SHIPPING libraries import it — FirstClassErrors, FirstClassErrors.Testing, - FirstClassErrors.RequestBinder (the `lib` train) and JustDummies (the `dum` train). Tools, workers, the CLI + FirstClassErrors.RequestBinder (the `lib` train). Tools, workers, the CLI and every test project stay out: PublicApiAnalyzers would otherwise flag their entire public surface as undeclared, and they carry no compatibility promise. @@ -40,9 +40,9 @@ @@ -62,7 +62,7 @@ return type, a renamed member) via RS0016/RS0017 and package validation. RS0026/RS0027 are a different thing: opinionated API-DESIGN rules ("do not spread optional parameters across overloads"). They fire pervasively on the library's deliberate, central fluent surfaces — Outcome.Then/Recover/Finally/Try/ - Create/Error and JustDummies' Any.Reproducibly — every one of which carries an optional callback across a + Create/Error — every one of which carries an optional callback across a family of overloads by design (and shipped that way in lib-v0.1.0-preview.1). Enforcing them here would demand a breaking redesign of the signature APIs, which is out of scope for installing a baseline and is the maintainer's call (it overlaps the pre-v1 Outcome API-symmetry review). Disabled deliberately; the diff --git a/build/stryker/justdummies-analyzers.json b/build/stryker/justdummies-analyzers.json deleted file mode 100644 index dea7fbc6..00000000 --- a/build/stryker/justdummies-analyzers.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "stryker-config": { - "solution": "FirstClassErrors.sln", - "project": "JustDummies.Analyzers.csproj", - "test-projects": [ - "JustDummies.Analyzers.UnitTests/JustDummies.Analyzers.UnitTests.csproj" - ], - "test-runner": "mtp", - "coverage-analysis": "off", - "reporters": [ - "html", - "json" - ], - "thresholds": { - "high": 100, - "low": 60, - "break": 0 - } - } -} diff --git a/build/stryker/justdummies-xunit.json b/build/stryker/justdummies-xunit.json deleted file mode 100644 index b5844574..00000000 --- a/build/stryker/justdummies-xunit.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "stryker-config": { - "solution": "FirstClassErrors.sln", - "project": "JustDummies.Xunit.csproj", - "test-projects": [ - "JustDummies.Xunit.UnitTests/JustDummies.Xunit.UnitTests.csproj" - ], - "test-runner": "mtp", - "coverage-analysis": "off", - "reporters": [ - "html", - "json" - ], - "thresholds": { - "high": 100, - "low": 90, - "break": 80 - } - } -} diff --git a/build/stryker/justdummies.json b/build/stryker/justdummies.json deleted file mode 100644 index 6f13fb57..00000000 --- a/build/stryker/justdummies.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "stryker-config": { - "solution": "FirstClassErrors.sln", - "project": "JustDummies.csproj", - "test-projects": [ - "JustDummies.UnitTests/JustDummies.UnitTests.csproj" - ], - "test-runner": "mtp", - "coverage-analysis": "off", - "concurrency": 4, - "reporters": [ - "html", - "json" - ], - "thresholds": { - "high": 95, - "low": 85, - "break": 0 - } - } -} diff --git a/doc/handwritten/for-maintainers/README.fr.md b/doc/handwritten/for-maintainers/README.fr.md index 010189fc..062a49f3 100644 --- a/doc/handwritten/for-maintainers/README.fr.md +++ b/doc/handwritten/for-maintainers/README.fr.md @@ -34,31 +34,6 @@ La checklist pour ajouter un nouveau paquet versionné indépendamment : l'uniqu imposés par GitHub et la tooling (trigger de tag, options de choix, scopes du commit-lint, packaging). Aussi en [anglais](AddingAReleaseTrain.en.md). -### [Écrire les tests de JustDummies](WritingJustDummiesTests.fr.md) - -Où placer un nouveau test pour `JustDummies` — suite par l'exemple ou suite par -propriétés — et comment l'écrire pour qu'il prouve quelque chose. Une seule -question tranche : l'assertion a-t-elle un espace d'entrée ? La frontière -elle-même est enregistrée dans -[l'ADR-0040](adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md) ; -cette page l'applique. Aussi en [anglais](WritingJustDummiesTests.en.md). - -### [Tool JustDummies (`dum`) — spécification](specifications/justdummies-tool.fr.md) - -La spécification complète de `dum`, le scaffolder en ligne de commande de -JustDummies, qui écrit un generator nommé et composable pour un type du code du -développeur — un moteur chargeable par un hôte Roslyn, plus une CLI mince par-dessus. -Elle est implémentable telle quelle : le squelette émis, les règles de résolution des -paramètres et les décisions qui les portent ont chacun été vérifiés contre la source -de la bibliothèque, et les affirmations centrales ont été mesurées. Elle est aussi -**autonome** : elle inline chaque fait sur la bibliothèque dont elle dépend, énonce -ses exigences envers le dépôt hôte en exigences plutôt qu'en chemins, et porte ses -dix enregistrements de décision au format ADR complet dans son -propre §15 — tenus là, plutôt qu'entrés dans la base ci-dessus, parce que le dépôt -qui devrait les accueillir n'existe pas encore. Le tout survit au déménagement de -JustDummies dans son propre dépôt. Pas encore construit. Aussi en -[anglais](specifications/justdummies-tool.md). - ### [Registres de décision d'architecture (ADR)](adr/README.md) Des enregistrements datés des décisions importantes — leur contexte, l'option diff --git a/doc/handwritten/for-maintainers/README.md b/doc/handwritten/for-maintainers/README.md index 71dd27fb..0bfa1817 100644 --- a/doc/handwritten/for-maintainers/README.md +++ b/doc/handwritten/for-maintainers/README.md @@ -32,29 +32,6 @@ edit in [`tools/trains.sh`](../../../tools/trains.sh) and the static edits GitHu tooling force (tag trigger, choice options, commit-lint scopes, packing). Also in [French](AddingAReleaseTrain.fr.md). -### [Writing JustDummies tests](WritingJustDummiesTests.en.md) - -Where a new test for `JustDummies` belongs — the example suite or the property -suite — and how to write it so it proves something. One question decides it: -does the assertion have an input space? The boundary itself is recorded in -[ADR-0040](adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md); -this page applies it. Also in [French](WritingJustDummiesTests.fr.md). - -### [JustDummies tool (`dum`) — specification](specifications/justdummies-tool.md) - -The complete specification of `dum`, the JustDummies command-line scaffolder that -writes a named, composable generator for a type from the developer's own code — -an engine loadable by a Roslyn host, plus a thin CLI over it. It is implementable -as written: the emitted skeleton, the parameter-resolution rules and the decisions -behind them were each checked against the library's source, and the central claims -were measured. It is also **self-contained**: it inlines every library fact it -relies on, states its host-repository requirements as requirements rather than -paths, and carries all ten of its architectural decision records in full ADR format -in its own §15 — held there, rather than entered into the base above, because the -repository that should hold them does not exist yet. All of which survives -JustDummies moving to its own repository. Not yet built. Also in -[French](specifications/justdummies-tool.fr.md). - ### [Architecture Decision Records](adr/README.md) Dated records of significant decisions — their context, the option chosen, and diff --git a/doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md b/doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md deleted file mode 100644 index 81806e10..00000000 --- a/doc/handwritten/for-maintainers/WritingJustDummiesTests.en.md +++ /dev/null @@ -1,128 +0,0 @@ -# Writing JustDummies tests - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](WritingJustDummiesTests.fr.md) - -> Where a new test for `JustDummies` belongs, and how to write it. The boundary -> between the two suites is recorded in -> [ADR-0040](adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md); -> this page is how to apply it. - -## The two suites - -| Project | Owns | Style | -|---|---|---| -| `JustDummies.PropertyTests` | Invariants that hold for **every** legal constraint argument | FsCheck properties over generated constraints | -| `JustDummies.UnitTests` | Contracts whose subject is a **specific, named case** | xUnit + NFluent examples | - -Both run on `net10.0` and on the .NET Framework 4.7.2 floor, so both prove their -half against the `netstandard2.0` asset consumers actually load. - -## The one question to ask - -> *Does my assertion have an input space?* - -Something a caller could have passed differently — a bound, a length, a count, a -pool, a seed, a pattern, an offset — and the assertion should still hold? - -* **Yes** → it is a property. Generate that input and quantify over it. -* **No** → it is an example. Pin it and assert it directly. - -That is the whole rule. Everything below is it applied. - -### Goes in the property suite - -* **Containment and strictness.** `Between(min, max)` contains; `GreaterThan` is - strict; `GreaterThanOrEqualTo` admits its own bound. The bound is the input space. -* **Shape.** `WithLength(n)` produces exactly `n`; `StartingWith(prefix)` actually - starts with it; `WithCount(n)` yields exactly `n` elements. -* **Grids.** `MultipleOf`, `WithScale`, `WithGranularity` — the step is the input - space, and the anchor differs per type, which is where these go wrong. -* **Round trips.** A value generated from a pattern is matched by the real engine; - a generated URI parses and carries the shape asked for. -* **Determinism.** Two contexts on the same seed agree — for *every* seed, not for - 12345. -* **Composition.** `As`, `OrNull`, `Combine`, the explicit pools: the composed - value carries each part's constraint, whatever the parts were constrained to. -* **Value-dependent legality.** When the same call is legal or illegal depending on - its argument, a property is the only honest way to state it — branch on the - value, never on the call shape. - -### Stays in the example suite - -* **Message content.** A conflict must name *both* offending constraints. The - wording is direction-aware; assert it on a pinned case, and assert the exception - **type** anywhere else. -* **Null and blank arguments.** `null` has no input space. -* **Named domain extremes.** `int.MinValue`, `byte.MaxValue`, an empty pool — a - specific coordinate, cheaper and clearer pinned than quantified. -* **Reachability.** That a bounded range is actually reached, that both branches of - a coin flip are observed. These are statistical, not universal — pin a seed. -* **Structural conventions.** The `Any` ↔ `AnyContext` mirror, factory naming, the - standalone assembly boundary. Reflection over a fixed expectation table; there is - no input to generate. -* **Dated regressions.** A defect that actually occurred, pinned at the coordinates - where it occurred. A property covering the same ground does **not** retire it — - the specificity is the value. Reference the issue in a comment. - -## Adding a feature - -1. Write the example tests first — they are how you discover the shape, and they - own the conflict messages your new constraint must produce. -2. Then ask the question above of each invariant you wrote. Anything that holds for - every argument moves to a property, and the example that pinned one argument goes - away with it. -3. If your constraint interacts with an existing one, the interaction is almost - always a property: it has two input spaces. - -## Fixing a defect - -1. Pin the defect as an example at the coordinates where it was found, with the - issue number in a comment. That is the regression, and it stays forever. -2. Then ask whether the defect had an input space the example does not cover. - Issue #206 did — the decimal midpoint bug was found on one interval and lived on - all of them. Add the property too. -3. Both land. The regression proves the exact case; the property proves the class. - -## Writing the property - -Use the shared helpers in `PropertyTestSupport.cs`: - -* `Generators.OrderedPair(values)` — a well-formed `(min, max)`, degenerate pairs - included. Pinned intervals are a historically fragile corner; do not filter them - out, branch on them. -* `Generators.WithEdges(values, edges)` — FsCheck's numeric generators are - size-bounded and cluster near zero, so the domain ends would otherwise almost - never be drawn. That is exactly where an off-by-one lives. -* `Expect.EveryDraw(generator, invariant)` — a generator is a recipe, not a value, - so one draw per case tests almost none of its randomness. Eight draws per case, - over a hundred cases, is the default. -* `Expect.Draws(generator, count)` — when the property reasons over a batch - (distinctness, reachability) rather than over each value alone. - -Rules that keep a property honest: - -* **Assert exception types, never message text.** Messages are direction-aware and - will change; that assertion belongs to an example. -* **Know when the exception is thrown.** Conflicts throw at the fluent *call*, not - at `Generate()`. Argument validation precedes conflict checking and wins when both - would apply. -* **Guard the degenerate corners** your generated arguments can produce — an empty - interval, a pinned interval, a zero count, an exhausted pool. Either keep the - generator off them or branch in the predicate. -* **Pin a seed for anything statistical.** "Both halves are reached", "null is - eventually drawn" are probabilistic. Under a pinned seed they are deterministic; - without one they flake. Say so in a comment. -* **Keep it fast.** A hundred cases times eight draws is already eight hundred - draws. Cap lengths and counts in the tens, not the thousands. - -## Before you push - -* `dotnet test JustDummies.PropertyTests` and `dotnet test JustDummies.UnitTests`. -* The floor legs, which CI runs and a plain `dotnet test` does not: - `dotnet build JustDummies.PropertyTests -c Release -f net472 -p:EnableNet472Floor=true`. - Anything using .NET 8+ API belongs in `ModernTypeInvariantProperties.cs`, which the - project file excludes from that leg. -* **Check your property can fail.** A property that returns `true` for the wrong - reason passes silently and proves nothing. Break it on purpose once — invert the - comparison, drop a bound — confirm it goes red, then put it back. This is the only - cheap defence against a suite that is green because it asserts nothing. diff --git a/doc/handwritten/for-maintainers/WritingJustDummiesTests.fr.md b/doc/handwritten/for-maintainers/WritingJustDummiesTests.fr.md deleted file mode 100644 index 998fbbd6..00000000 --- a/doc/handwritten/for-maintainers/WritingJustDummiesTests.fr.md +++ /dev/null @@ -1,139 +0,0 @@ -# Écrire les tests de JustDummies - -🌍 🇬🇧 [English](WritingJustDummiesTests.en.md) · 🇫🇷 Français (ce fichier) - -> Où placer un nouveau test pour `JustDummies`, et comment l'écrire. La frontière -> entre les deux suites est enregistrée dans -> [l'ADR-0040](adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md) ; -> cette page explique comment l'appliquer. - -## Les deux suites - -| Projet | Porte | Style | -|---|---|---| -| `JustDummies.PropertyTests` | Les invariants vrais pour **tout** argument de contrainte légal | Propriétés FsCheck sur contraintes générées | -| `JustDummies.UnitTests` | Les contrats dont le sujet est un **cas spécifique et nommé** | Exemples xUnit + NFluent | - -Les deux tournent sur `net10.0` et sur le plancher .NET Framework 4.7.2 : chacune -prouve donc sa moitié contre l'asset `netstandard2.0` que les consommateurs -chargent réellement. - -## L'unique question à se poser - -> *Mon assertion a-t-elle un espace d'entrée ?* - -Quelque chose que l'appelant aurait pu passer autrement — une borne, une longueur, -une cardinalité, un vivier, une graine, un motif, un décalage — sans que -l'assertion cesse d'être vraie ? - -* **Oui** → c'est une propriété. Générez cette entrée et quantifiez dessus. -* **Non** → c'est un exemple. Figez-le et affirmez-le directement. - -C'est toute la règle. Tout ce qui suit n'en est que l'application. - -### Va dans la suite par propriétés - -* **Contenance et stricture.** `Between(min, max)` contient ; `GreaterThan` est - strict ; `GreaterThanOrEqualTo` admet sa propre borne. La borne est l'espace - d'entrée. -* **Forme.** `WithLength(n)` produit exactement `n` ; `StartingWith(prefix)` - commence effectivement par lui ; `WithCount(n)` rend exactement `n` éléments. -* **Grilles.** `MultipleOf`, `WithScale`, `WithGranularity` — le pas est l'espace - d'entrée, et l'ancre diffère selon le type : c'est là que ça dérape. -* **Allers-retours.** Une valeur générée depuis un motif est reconnue par le vrai - moteur ; une URI générée s'analyse et porte la forme demandée. -* **Déterminisme.** Deux contextes de même graine concordent — pour *toute* graine, - pas pour 12345. -* **Composition.** `As`, `OrNull`, `Combine`, les viviers explicites : la valeur - composée porte la contrainte de chaque partie, quelles qu'aient été ces - contraintes. -* **Légalité dépendante de la valeur.** Quand le même appel est légal ou illégal - selon son argument, une propriété est la seule façon honnête de l'énoncer — se - ramifier sur la valeur, jamais sur la forme de l'appel. - -### Reste dans la suite par l'exemple - -* **Contenu des messages.** Un conflit doit nommer *les deux* contraintes fautives. - La formulation est sensible au sens d'application : affirmez-la sur un cas figé, - et ailleurs affirmez le **type** de l'exception. -* **Arguments nuls ou vides.** `null` n'a pas d'espace d'entrée. -* **Extrêmes nommés du domaine.** `int.MinValue`, `byte.MaxValue`, un vivier vide — - une coordonnée précise, plus claire et moins chère figée que quantifiée. -* **Atteignabilité.** Qu'une plage bornée soit effectivement atteinte, que les deux - branches d'un tirage soient observées. C'est statistique, non universel : figez - une graine. -* **Conventions structurelles.** Le miroir `Any` ↔ `AnyContext`, le nommage des - fabriques, la frontière d'assemblage autonome. De la réflexion sur une table - d'attentes fixe ; il n'y a rien à générer. -* **Régressions datées.** Un défaut qui a réellement eu lieu, figé aux coordonnées - où il a eu lieu. Une propriété couvrant le même terrain ne la retire **pas** — la - spécificité *est* la valeur. Référencez l'issue en commentaire. - -## Ajouter une fonctionnalité - -1. Écrivez d'abord les tests par l'exemple : c'est ainsi qu'on découvre la forme, et - ce sont eux qui portent les messages de conflit que votre nouvelle contrainte doit - produire. -2. Posez ensuite la question ci-dessus à chaque invariant écrit. Tout ce qui vaut - pour tout argument devient une propriété, et l'exemple qui en figeait un seul - disparaît avec. -3. Si votre contrainte interagit avec une contrainte existante, l'interaction est - presque toujours une propriété : elle a deux espaces d'entrée. - -## Corriger un défaut - -1. Figez le défaut en exemple, aux coordonnées où il a été trouvé, avec le numéro - d'issue en commentaire. C'est la régression, et elle reste pour toujours. -2. Demandez-vous ensuite si le défaut avait un espace d'entrée que l'exemple ne - couvre pas. L'issue #206 en avait un : le bug du milieu d'intervalle décimal a été - trouvé sur un intervalle et vivait sur tous. Ajoutez aussi la propriété. -3. Les deux atterrissent. La régression prouve le cas exact ; la propriété prouve la - classe. - -## Écrire la propriété - -Utilisez les helpers partagés de `PropertyTestSupport.cs` : - -* `Generators.OrderedPair(values)` — un `(min, max)` bien formé, paires dégénérées - comprises. Les intervalles épinglés sont un coin historiquement fragile : ne les - filtrez pas, ramifiez-vous dessus. -* `Generators.WithEdges(values, edges)` — les générateurs numériques de FsCheck sont - bornés en taille et se massent près de zéro : sans cela les extrémités du domaine - ne seraient pour ainsi dire jamais tirées. C'est précisément là que vit un décalage - d'une unité. -* `Expect.EveryDraw(generator, invariant)` — un générateur est une recette, pas une - valeur : un seul tirage par cas ne teste presque rien de son aléa. Huit tirages par - cas, sur cent cas, est la valeur par défaut. -* `Expect.Draws(generator, count)` — quand la propriété raisonne sur un lot - (distinction, atteignabilité) plutôt que sur chaque valeur isolément. - -Les règles qui gardent une propriété honnête : - -* **Affirmez les types d'exception, jamais le texte des messages.** Les messages sont - sensibles au sens d'application et changeront ; cette assertion appartient à un - exemple. -* **Sachez quand l'exception est levée.** Les conflits sont levés à *l'appel* fluide, - pas à `Generate()`. La validation des arguments précède la détection de conflit et - l'emporte quand les deux s'appliqueraient. -* **Gardez les coins dégénérés** que vos arguments générés peuvent produire : - intervalle vide, intervalle épinglé, cardinalité nulle, vivier épuisé. Soit le - générateur les évite, soit le prédicat s'y ramifie. -* **Figez une graine pour tout ce qui est statistique.** « Les deux moitiés sont - atteintes », « `null` finit par être tiré » sont probabilistes. Sous graine figée - elles sont déterministes ; sans elle, elles deviennent instables. Dites-le en - commentaire. -* **Restez rapide.** Cent cas fois huit tirages font déjà huit cents tirages. Plafonnez - longueurs et cardinalités dans les dizaines, pas les milliers. - -## Avant de pousser - -* `dotnet test JustDummies.PropertyTests` et `dotnet test JustDummies.UnitTests`. -* Les segments du plancher, que la CI exécute et qu'un `dotnet test` ordinaire ignore : - `dotnet build JustDummies.PropertyTests -c Release -f net472 -p:EnableNet472Floor=true`. - Tout ce qui utilise une API .NET 8+ appartient à `ModernTypeInvariantProperties.cs`, - que le fichier projet exclut de ce segment. -* **Vérifiez que votre propriété peut échouer.** Une propriété qui rend `true` pour la - mauvaise raison passe en silence et ne prouve rien. Cassez-la volontairement une fois - — inversez la comparaison, retirez une borne — constatez qu'elle rougit, puis - remettez-la. C'est la seule défense bon marché contre une suite verte parce qu'elle - n'affirme rien. diff --git a/doc/handwritten/for-maintainers/specifications/justdummies-tool.fr.md b/doc/handwritten/for-maintainers/specifications/justdummies-tool.fr.md deleted file mode 100644 index 235063ec..00000000 --- a/doc/handwritten/for-maintainers/specifications/justdummies-tool.fr.md +++ /dev/null @@ -1,2253 +0,0 @@ -# Tool JustDummies (`dum`) — spécification v1.0 - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](justdummies-tool.md) - -**Statut :** spécification, prête à implémenter. Rien n'est encore construit. -**Remplace :** la pré-spécification de travail 0.1 (jamais commitée) - ---- - -## 0. Comment lire ce document - -Cette spécification est **autonome à dessein**. JustDummies a vocation à rejoindre son propre -dépôt avant que le tool soit construit, donc rien ici ne peut dépendre d'une lecture à l'intérieur -de `Reefact/first-class-errors`. - -* **§1–§9, c'est le produit.** Ce que le tool fait, ce qu'il émet, et pourquoi. Lire le §2 - d'abord : onze décisions portent tout le reste. Le §5 est la partie difficile et la seule qui - comporte un vrai risque de conception. -* **§10–§12, c'est la construction.** Deux projets, le contrat entre eux, et le plan de tests. -* **§13, c'est le contrat de portabilité.** Tout ce dont le tool a besoin *de son dépôt hôte*, - énoncé en exigences plutôt qu'en chemins. Si JustDummies a déménagé, commencer ici. -* **§14, c'est la référence.** Chaque fait sur la bibliothèque JustDummies dont dépend cette - spécification, inliné, avec la commande pour le redériver. Rien dans les §1–§12 n'exige de lire - la source de la bibliothèque pour être vérifié. -* **§15, c'est le raisonnement.** Dix enregistrements de décision au format ADR de ce dépôt, tenus - dans la spécification parce que le dépôt qui devrait les accueillir n'existe pas encore. À lire - quand on veut savoir *pourquoi*, ou quand on est tenté de revenir sur une décision du §2. -* **§16, c'est la frontière de la v1.0.** Ce qui est reporté, et ce qui a été abandonné net. -* **§17, ce sont les preuves.** Le squelette émis du §4.1 a été compilé et exécuté contre la vraie - bibliothèque, et les deux affirmations contestées ont été mesurées. Le §17.2 dit comment tout - rejouer. - -Tout dans ce document est **décidé**, sauf ce qui figure au §16 (reporté) ou ce qui est -explicitement marqué ouvert. Aucune question ouverte ne bloque l'implémentation. - ---- - -## 1. Ce qu'est `dum` - -`dum` est un **scaffolder**, pas un générateur de code. - -À partir d'un type du code du développeur, il écrit **un fichier C#, une fois**, contenant un -generator nommé et composable pour ce type. Dès que le fichier est écrit il appartient au -développeur : il le lit, le modifie, le commite, et ne relance jamais le tool dessus. - -```console -$ cd Shop.Tests -$ dum generate Order -✓ AnyOrder.cs -``` - -```csharp -Order order = new AnyOrder() - .WithStatus(OrderStatus.Pending) - .Generate(); -``` - -La distinction avec un *générateur* est toute la position produit, et elle règle d'un coup -l'essentiel de la conception : - -* il n'y a pas de dérive, parce qu'il n'y a rien à maintenir synchronisé — le fichier est celui du - développeur, pas celui du tool ; -* il n'y a donc **ni verbe `check`, ni source generator, ni scénario de régénération** ; -* le tool a le droit de laisser le fichier **inachevé**, parce que l'achever est la moitié du - marché qui revient au développeur. - -La proposition de valeur reste distincte de celle de la bibliothèque : la **bibliothèque** rend les -valeurs valides ; le **tool** rend le test concis. - -### 1.1 Les règles de conception auxquelles ce document répond - -1. **Extrêmement simple à utiliser.** L'invocation nominale tient en un verbe et un nom de type, - depuis le répertoire où le fichier atterrira, sans fichier de configuration et sans option. -2. **Bon marché aux deux bouts.** Rien à configurer avant le premier usage ; rien à configurer à - chaque usage. -3. **Générer tout ce qui peut l'être, et rien de plus.** Là où le tool ne peut pas savoir, il le - dit dans le fichier et dans la console, et rend la main sur le squelette. -4. **Le nommage est figé en v1.0.** `Order` devient `AnyOrder`, point. Le renommage - (`OrderFactory`, un préfixe personnalisé) est pour la v1.1+ et le §16 en réserve la forme pour - que la v1.0 ne la bloque pas. - ---- - -## 2. Décisions - -Ce sont les décisions porteuses. Les onze sont couvertes par les dix enregistrements de décision du -§15 — contexte, argument, alternatives écartées, conséquences ; D5 et D6 en partagent un. Cette -table en est l'index ; elle ne porte aucun argument propre. - -| # | Décision | Pourquoi, en une ligne | -|---|---|---| -| **D1** | Scaffolder une fois ; le fichier appartient au développeur. | Supprime d'un coup la dérive, le `check` et la question du source generator. | -| **D2** | Le type émis implémente `IAny` et est **immuable**. | Composabilité, et réarmement des analyzers `JustDummies.Usage` sur le type émis. | -| **D3** | Le fichier émis n'est **pas** marqué comme code généré. | Les 27 analyzers exemptent le code généré ; le marquer rendrait le fichier aveugle. | -| **D4** | Ne jamais émettre un membre non résolu dans la compilation cible. | Une règle couvre le clivage de TFM, la baseline d'API publique, l'écart de version et l'arithmétique non signée. | -| **D5** | Lire les clauses de garde du constructeur pour amorcer chaque generator. | Sans cela le code émis produit des valeurs que le constructeur rejette. | -| **D6** | Un paramètre non résolu est émis comme **erreur de compilation**. | Le développeur est déjà dans le fichier ; un soulignement rouge est le signal le moins cher. | -| **D7** | Le generator émis tire du contexte **ambiant** et ne détient aucun état. | La résolution au tirage rend la garantie du §8.2 gratuite ; un état capturé exigerait une règle de cycle de vie. | -| **D8** | Le type émis vit dans le **namespace du type cible**. | Zéro friction au site d'appel — et la cause unique du risque de masquage du §7. | -| **D9** | Le tool ne prend **aucune dépendance sur le package JustDummies**. | Résolution par nom de métadonnée, comme les analyzers — l'écart de version devient structurellement impossible. | -| **D10** | Ne jamais émettre `.OrNull()`. | Un dummy aléatoirement `null` est précisément l'instabilité que la bibliothèque existe pour supprimer. | -| **D11** | Le **moteur de scaffolding est une bibliothèque séparée** au plancher Roslyn ; la CLI est une coquille. | Le second consommateur plausible du moteur est un refactoring IDE, qui n'est pas une CLI et ne peut pas charger un assembly `net8.0`. | - ---- - -## 3. Surface de commande - -Le tool est distribué comme .NET tool dont la commande est **`dum`**. - -```console -dotnet tool install --global JustDummies.Cli -dum generate [...] [options] -``` - -`generate` est le seul verbe de la v1.0. - -| Option | Défaut | Signification | -|---|---|---| -| `--project ` | l'unique `*.csproj` du répertoire courant | Projet dont la compilation est analysée. | -| `--output ` | le répertoire courant | Où le fichier est écrit. | -| `--namespace ` | le namespace du type cible (D8) | Namespace du type émis. | -| `--force` | inactif | Écrase un fichier existant. | -| `--dry-run` | inactif | Affiche le fichier sur stdout ; n'écrit rien. | - -C'est toute la surface. Pas de fichier de configuration, pas de `init`, pas de `list`, pas de -`--all`, et — par D1 — pas de `check`. Le §16 liste ce qui est délibérément reporté. - -### 3.1 Où le tool est lancé - -Depuis le **projet de test**, parce que c'est là que le fichier va. Le projet de test référence le -projet de production, donc `Order` est atteignable depuis sa compilation, et le défaut de -`--output` place `AnyOrder.cs` à côté des tests qui l'utilisent. - -Résolution de `--project` : si exactement un `*.csproj` se trouve dans le répertoire courant, il -est retenu ; s'il n'y en a aucun ou plusieurs, échec avec un message nommant les candidats et -pointant `--project`. - -### 3.2 Résolution du type cible - -`Order` est cherché, dans l'ordre : - -1. par nom de métadonnée complet, si l'argument contient un `.` (`Shop.Domain.Order`) ; -2. par nom simple parmi les types source de la compilation et les assemblies référencées. - -Un type **imbriqué** s'écrit comme un développeur le taperait — `dum generate Order.Line` — et le -moteur le traduit pour la recherche, où le séparateur est `+` et non `.` -(`Shop.Domain.Order+Line`). Passer la forme pointée telle quelle à une recherche par nom de -métadonnée ne renvoie rien, ce qui signalerait comme absent un type bien réel. Le generator émis est -un type de premier niveau dans le namespace englobant, nommé d'après le seul type imbriqué : -`AnyLine`. - -Zéro correspondance → erreur, avec les noms les plus proches par distance d'édition. Plus d'une → -erreur, avec les noms complets, en demandant lequel. Les deux sortent en `1`. - ---- - -## 4. Le fichier émis - -### 4.1 Exemple complet - -Cet exemple n'est pas une esquisse : il a été compilé et exécuté contre la vraie bibliothèque -(§17). - -Source analysée : - -```csharp -namespace Shop.Domain; - -public sealed class Order { - - public Order(OrderReference reference, Customer customer, int quantity, - OrderStatus status, IReadOnlyList tags, DateTime placedAt) { - if (reference is null) { throw new ArgumentNullException(nameof(reference)); } - if (quantity <= 0) { throw new ArgumentOutOfRangeException(nameof(quantity)); } - ... - } - -} - -public sealed class OrderReference { - - public static OrderReference Create(string value) { - if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException(...); } - ... - } - -} -``` - -`dum generate Order`, avec `AnyCustomer` déjà scaffoldé dans le projet, émet : - -```csharp -// Scaffolded by dum (JustDummies). This file is yours: read it, edit it, commit it. -// `dum generate Order --force` overwrites it. This type is partial, so members you add in a -// neighbouring file survive. - -using System; -using System.Collections.Generic; - -using JustDummies; - -namespace Shop.Domain; - -/// -/// A generator of arbitrary values. It draws from the ambient random -/// context, so a reproducibility scope pins it; to draw from an isolated -/// Any.WithSeed(...) context, pass that context's generators through the -/// With… overloads. -/// -public sealed partial class AnyOrder : IAny { - - private readonly IAny _reference; - private readonly IAny _customer; - private readonly IAny _quantity; - private readonly IAny _status; - private readonly IAny> _tags; - private readonly IAny _placedAt; - - /// Creates the generator with a default recipe for every constructor parameter. - public AnyOrder() - : this(reference: Any.String().NonEmpty().As(OrderReference.Create), - customer: new AnyCustomer(), - quantity: Any.Int32().Positive(), - status: Any.Enum(), - tags: Any.ListOf(Any.String().NonEmpty()), - placedAt: Any.DateTime()) { } - - private AnyOrder(IAny reference, - IAny customer, - IAny quantity, - IAny status, - IAny> tags, - IAny placedAt) { - _reference = reference; - _customer = customer; - _quantity = quantity; - _status = status; - _tags = tags; - _placedAt = placedAt; - } - - /// Pins reference to a fixed value. - public AnyOrder WithReference(OrderReference value) { - return WithReference(new FixedValue(value)); - } - - /// Draws reference from . - public AnyOrder WithReference(IAny generator) { - return new AnyOrder(generator, _customer, _quantity, _status, _tags, _placedAt); - } - - // ... une telle paire par paramètre ... - - /// Produces one arbitrary . - public Order Generate() { - return new Order(_reference.Generate(), - _customer.Generate(), - _quantity.Generate(), - _status.Generate(), - _tags.Generate(), - _placedAt.Generate()); - } - - private sealed class FixedValue : IAny { - - private readonly TValue _value; - - public FixedValue(TValue value) { - _value = value; - } - - public TValue Generate() { - return _value; - } - - } - -} -``` - -### 4.2 Règles de forme - -* `public sealed partial class Any{Type} : IAny<{Type}>`. `partial` pour que les membres propres du - développeur vivent dans un fichier voisin et survivent à un `--force`. -* Un `private readonly IAny _param;` par paramètre du constructeur, dans l'ordre de - déclaration. -* Un **constructeur public sans paramètre** portant la recette inférée, écrit avec des arguments - nommés pour que le lecteur associe chaque expression à son paramètre sans compter. -* Un **constructeur privé complet** réalisant la copie. -* Par paramètre, **deux** surcharges `With{Param}` retournant une nouvelle instance : - `With{Param}(TParam value)` et `With{Param}(IAny generator)`. - La surcharge par valeur est l'ergonomique ; la surcharge par generator est ce qui maintient la - composition possible, et c'est pourquoi passer `Any.String().StartingWith("ORD-")` ne devient pas - une erreur `JD011`/`JD012`. -* `public {Type} Generate()` appelant le constructeur avec le `Generate()` de chaque champ. -* Le helper privé imbriqué `FixedValue`. Justification : il accepte `null` (ce que - `Any.OneOf(value)` refuse) et ne consomme aucun tirage de la source ambiante, donc épingler un - paramètre ne décale pas les valeurs tirées pour les autres (§14.5). Il est imbriqué et privé, - donc un nombre quelconque de fichiers scaffoldés coexistent. *(Si `Any.Fixed(value)` est un - jour ajouté à la bibliothèque, le helper pourra disparaître — voir §15.)* -* Casse de `With{Param}` : le nom du paramètre, première lettre en majuscule, culture invariante. - Un paramètre nommé `_id` ou `@class` est normalisé en retirant le `_`/`@` de tête. - -**Le cas dégénéré a sa propre forme.** Un constructeur sans paramètre (§5.1) fait s'effondrer tout -ce qui précède : un seul constructeur public sans paramètre, aucun champ, aucun constructeur privé, -aucune méthode `With`, aucun helper `FixedValue`, et `Generate()` retournant `new {Type}()`. Émettre -les deux constructeurs sans condition leur donnerait la même signature et échouerait en `CS0111` — -vérifié. Le résultat vaut quand même d'être généré : `Any{Type}` est un `IAny`, donc il se -compose dans `Any.ListOf(...)`, `Any.Combine(...)` et le reste, ce qu'un simple `new {Type}()` ne -fait pas. - -### 4.3 Règles d'en-tête - -Exactement trois lignes de commentaire, comme ci-dessus. **Aucun horodatage et aucune version du -tool** : les deux feraient dépendre le contenu d'autre chose que du type analysé, si bien que tout -scaffold suivant une montée de version produirait un diff parasite. Le déterminisme est une -exigence dure (§8.1). - -### 4.4 Niveau de langage - -Le code émis n'utilise aucune construction plus récente que **C# 7.3** : pas de `var` (cela se lit -mieux dans un squelette), pas de `new` typé par la cible, pas de records, pas d'expressions -`switch`, pas de namespace à portée de fichier sauf si le fichier du type cible en utilise déjà un. -Le fichier atterrit dans le projet du développeur et doit compiler au `LangVersion` de ce projet. - -La seule exception est la forme du namespace, copiée sur le style de déclaration du type cible pour -que le fichier émis ressemble à ses voisins. - ---- - -## 5. Résolution — comment un paramètre devient un generator - -Pour chaque paramètre, le moteur produit une expression de type `IAny`, ou échoue et marque -le paramètre non résolu. - -### 5.1 Choix du constructeur - -1. Constructeurs d'instance publics, le plus de paramètres d'abord ; égalité départagée par l'ordre - source. La signature retenue est toujours affichée (§6). -2. Si le type n'a **aucun** constructeur accessible mais expose une fabrique statique reconnue - (§5.4) retournant lui-même, cette fabrique est utilisée et `Generate()` l'appelle. -3. Un constructeur sans paramètre donne un `AnyOrder` valide et trivial, sans méthode `With`. -4. Les records positionnels fonctionnent sans traitement particulier — leur constructeur primaire - est un constructeur public ordinaire. Les membres `init` et `required` sont **hors périmètre** - (§16). -5. Un constructeur ayant un paramètre `ref` ou `out` n'est **pas éligible** : `Generate()` passe des - arguments par valeur, et un tel site d'appel échoue en `CS1620` — vérifié. L'ignorer et - considérer le candidat suivant ; s'il n'en reste aucun, le type est non résolu (§7). `in` - convient, un argument par valeur s'y lie. - -### 5.2 La table de base - -Chaque entrée est soumise à D4 : le membre n'est émis que s'il se résout dans la compilation. - -| Type du paramètre | Émission | -|---|---| -| `string` | `Any.String().NonEmpty()` | -| `bool` | `Any.Boolean()` | -| `sbyte` `byte` `short` `ushort` `int` `uint` `long` `ulong` | `Any.SByte()` … `Any.UInt64()` | -| `float` `double` `decimal` | `Any.Single()` / `Any.Double()` / `Any.Decimal()` | -| `char` | `Any.Char()` | -| `Guid` | `Any.Guid().NonEmpty()` | -| `DateTime` `DateTimeOffset` `TimeSpan` | `Any.DateTime()` / `Any.DateTimeOffset()` / `Any.TimeSpan()` | -| `DateOnly` `TimeOnly` `Int128` `UInt128` `Half` | la fabrique correspondante — **asset `net8.0` uniquement**, D4 tranche | -| tout `enum E` | `Any.Enum()` | -| `Uri` | `Any.Uri().Web()` | -| `T[]` | `Any.ArrayOf()` | -| `List` `IReadOnlyList` `IList` `ICollection` `IReadOnlyCollection` | `Any.ListOf()` | -| `IEnumerable` | `Any.SequenceOf()` | -| `HashSet` `ISet` | `Any.SetOf()` | -| `Dictionary` `IDictionary` `IReadOnlyDictionary` | `Any.DictionaryOf(, )` | -| `T?` où `T` est un type référence | le generator de `T` inchangé — **jamais** `.OrNull()` (D10) | -| `T?` où `T` est un type valeur | `.As(value => (T?)value)` — **jamais** `.OrNull()` (D10) | -| un type ayant un `AnyT` scaffoldé dans la compilation | `new AnyT()` (§5.4) | -| un type ayant une fabrique statique reconnue à un paramètre | `.As(T.Create)` (§5.4) | -| tout le reste | non résolu (§5.5) | - -Trois remarques sur la table. - -**`Any.String().NonEmpty()`, pas `Any.String()`.** Sans contrainte, `Any.String()` produit *0 à 16* -lettres et chiffres ASCII (§14.5) — il peut retourner la chaîne vide. Un paramètre de constructeur -de type `string` dans un type métier est massivement requis non vide, et un défaut qui échoue -environ une fois sur dix-sept (mesuré : §17) est exactement l'instabilité que la bibliothèque existe -pour supprimer. Même raisonnement pour `Any.Guid().NonEmpty()`. - -**Les collections reposent sur la covariance — les types valeur, non.** `IAny` est -covariante, donc `Any.ListOf(...)`, de type `IAny>`, est directement affectable à un champ -de type `IAny>` ; aucun adaptateur n'est nécessaire pour les lignes d'interface, et -il en va de même pour `HashSet`/`ISet` et `Dictionary`/`IReadOnlyDictionary`. - -La variance en C# ne s'applique qu'aux conversions de **référence**, d'où la différence entre les -deux lignes nullables. `IAny` est un `IAny` et ne demande rien ; `IAny` -n'est **pas** un `IAny`, donc un paramètre `int?` exige le saut explicite -`.As(value => (int?)value)`. S'y tromper est la façon la plus probable de produire une table qui ne -compile pas — les lignes réservées à `net8.0` sont elles aussi des types valeur. - -**Les generators d'éléments récursent.** `IReadOnlyList` résout son élément par cette -même table, et devient donc `Any.ListOf(new AnyOrderLine())` quand `AnyOrderLine` existe. La -récursion est limitée à une profondeur de 3 et protégée contre les cycles ; dépasser l'une ou -l'autre rend le paramètre non résolu. - -### 5.3 Clauses de garde - -C'est la fonctionnalité qui justifie de construire le tool plutôt que de faire un template. - -Quand le corps du constructeur (ou de la fabrique) est **disponible en source** — ce qui est le cas -pour tout type de la solution du développeur, et ne l'est pas pour un type venant d'un package -NuGet — le moteur lit ses clauses de garde de tête et resserre le generator en conséquence. - -Une instruction n'est une garde que si **toutes** les conditions suivantes tiennent. La règle est -délibérément conservatrice, à l'image des analyzers de la bibliothèque, qui préfèrent sous-signaler -plutôt que se tromper : - -* c'est un `if` dont le corps lève inconditionnellement, sans `else` ; -* elle apparaît avant la première affectation à un champ ou une propriété ; -* sa condition mentionne **exactement un** paramètre et ne contient ni `&&` ni `||` ; -* tout autre opérande est une constante de compilation. - -L'ensemble reconnu est clos : - -| Condition qui lève | Contrainte ajoutée | -|---|---| -| `p is null`, `p == null` | aucune — le generator ne retourne jamais `null` de toute façon | -| `string.IsNullOrEmpty(p)`, `string.IsNullOrWhiteSpace(p)`, `p.Length == 0`, `p.Length < 1` | `.NonEmpty()` | -| `p.Length > N` | `.WithMaxLength(N)` | -| `p.Length < N` | `.WithMinLength(N)` | -| `p.Length != N` | `.WithLength(N)` | -| `p <= 0` ; ou `p < 1` sur un type **intégral** | `.Positive()` | -| `p < 0` | `.GreaterThanOrEqualTo(0)` | -| `p >= 0` | `.Negative()` | -| `p == 0` | `.NonZero()` | -| `p > N` | `.LessThanOrEqualTo(N)` | -| `p < N` | `.GreaterThanOrEqualTo(N)` | -| `p == Guid.Empty` | `.NonEmpty()` | -| `!Enum.IsDefined(typeof(E), p)` | aucune — `Any.Enum()` ne tire déjà que des membres déclarés | - -`.NonEmpty()` couvre `IsNullOrWhiteSpace` aussi bien que `IsNullOrEmpty`, parce qu'un -`Any.String()` non contraint ne tire que des lettres et chiffres ASCII : un tirage non vide ne peut -jamais être blanc (§14.5). - -**Une garde de taille sur un paramètre collection relève de la famille `Count`, pas de la famille -`Length`.** Un generator de collection expose `NonEmpty`, `WithCount`, `WithMinCount` et -`WithMaxCount`, et aucun `WithLength` (§14.3). Donc `p.Length > N` sur un `T[]`, ou `p.Count > N` -sur une `List`, devient `.WithMaxCount(N)` ; `p.Count != N` devient `.WithCount(N)`. Lire une -telle garde contre la famille des chaînes émettrait un membre qui ne se résout pas, et D4 -l'abandonnerait **silencieusement** — une vraie contrainte perdue sans laisser de trace. -`.NonEmpty()` est le seul membre qui s'écrit pareil des deux côtés. - -Les contraintes reconnues **se composent quand elles bornent des choses différentes, et sont -abandonnées quand elles se heurtent**. Deux gardes posant une borne inférieure et une borne -supérieure sont complémentaires — `.NonEmpty()` avec `.WithMaxLength(10)`, ou -`.GreaterThanOrEqualTo(0)` avec `.LessThanOrEqualTo(100)` — et les deux sont conservées. C'est -l'idiome d'intervalle borné ordinaire, écrit en deux gardes consécutives ; l'écarter rendrait la -lecture des gardes inutile pour le cas qu'elle rencontre le plus souvent. Les deux compositions ont -été vérifiées contre la bibliothèque (§17). - -Deux gardes posant *la même* borne sont inconciliables : les deux sont abandonnées et le paramètre -est signalé `guards not combined`. Une borne inférieure au-dessus d'une borne supérieure aussi — la -bibliothèque rejette cette chaîne par `ConflictingAnyConstraintException`, et `JD023` la signale à -la compilation (§17), mais le moteur ne doit pas l'émettre pour autant. Aucune garde reconnue ne -produit de contrainte de jeu de caractères ni de motif, donc ces axes ne se présentent jamais. - -**Les gardes regex ne sont délibérément pas lues.** `!Regex.IsMatch(p, "…")` a tout de la garde -idéale à traduire : la bibliothèque a `Any.StringMatching(...)`, et le motif est là, littéral. Elle -est hors de l'ensemble pour la v1.0, pour une raison qui se généralise. - -La bibliothèque construit ses valeurs à partir du sous-ensemble *régulier* du langage des motifs — -lookarounds, backreferences, limites de mot et catégories Unicode sont en dehors, et un motif qui en -utilise lève `UnsupportedRegexException`. Quatre motifs de validation réalistes sur cinq ont été -rejetés à l'essai (§17) ; lookaheads et limites de mot sont le vocabulaire ordinaire d'un validateur -écrit à la main. - -Pire, le rejet a lieu à la **construction**, pas au `Generate()`. Le constructeur sans paramètre -émis exécute toute la recette dans son initialiseur, donc `new AnyOrder()` lèverait avant que le -moindre `.WithReference(...)` ne puisse surcharger. Le type généré serait inutilisable, pas -seulement imprécis, et aucun appel que le développeur pourrait écrire ne le rattraperait — vérifié -(§17). - -Et le moteur ne peut pas le savoir à l'avance. D9 lui interdit de référencer la bibliothèque, donc -il ne peut pas demander à son parser si un motif est supporté, et réimplémenter ce contrôle -dupliquerait un parser qu'il ne voit pas et en dériverait. - -D'où une règle qui mérite d'être énoncée pour elle-même, puisque la ligne motif est la seule à -l'avoir jamais enfreinte : **le moteur n'émet jamais une expression dont la validité dépend d'une -valeur qu'il ne peut pas contrôler.** Toutes les autres lignes émettent un membre que D4 résout, -avec un argument qui est une constante de compilation du bon type. Lire les gardes regex est un -candidat v1.1 (§16) et suppose la question du sous-ensemble tranchée d'abord. - -Quand deux lignes apparient une même condition, **la plus spécifique gagne**. `p < 1` sur un type -intégral relève de la ligne `.Positive()` ; sur `decimal`, `double` ou `float`, de la ligne -`.GreaterThanOrEqualTo(N)`, parce que `.Positive()` admettrait les valeurs entre zéro et un que la -garde rejette. C'est un tirage rare pour un `decimal` par ailleurs non contraint — mesuré à un sur -cinq mille — et fréquent dès que le paramètre porte une autre borne (§17). Exactement le profil d'un -défaut qui survit à un test superficiel. - -**Où les contraintes s'attachent.** Une contrainte dérivée d'une garde appartient au generator du -type propre du paramètre, *avant* toute conversion ou composition. Un paramètre `int?` gardé par -`p <= 0` émet `Any.Int32().Positive().As(value => (int?)value)`, pas l'inverse ; un paramètre de -fabrique gardé dans le corps de celle-ci émet -`Any.String().NonEmpty().As(OrderReference.Create)`. Le saut `.As` vient toujours en dernier, parce -que c'est l'étape qui change le type. - -Chaque contrainte ci-dessus reste soumise à D4. `.Positive()` sur un paramètre `uint` ne se résout -pas (§14.3) et est ignorée. - -La lecture des gardes est aussi ce qui rend la composition par fabrique correcte plutôt que -nominale : `OrderReference.Create` garde sur `IsNullOrWhiteSpace`, donc le tool émet -`Any.String().NonEmpty().As(OrderReference.Create)` — une chaîne qui fonctionne — au lieu de -`Any.String().As(OrderReference.Create)`, mesurée levant `AnyGenerationException` **594 fois sur -10 000 tirages**, et 557 lors d'une reprise indépendante — environ une fois sur dix-sept, ce que -prédit un tirage non contraint sur les dix-sept longueurs de 0 à 16 (§17). - -Cette seule mesure est la raison d'être de cette section ; D5 + D6 en expose l'argument et les -alternatives pesées contre lui. - -### 5.4 Composition - -**Un generator scaffoldé l'emporte.** Si la compilation contient un type nommé `Any{T}` implémentant -`IAny` avec un constructeur public sans paramètre, le moteur émet `new Any{T}()`. C'est ainsi que -les agrégats se composent en cascade, et cela fonctionne que ce type ait été scaffoldé plus tôt ou -écrit à la main. - -**Sinon, une fabrique statique.** Une méthode qualifie si elle est `public static`, retourne le type -du paramètre, prend exactement un paramètre, et se nomme `Create`, `From`, `Of` ou `Parse`. Si -plusieurs qualifient, `Create` gagne ; s'il en reste plusieurs, le paramètre est non résolu et la -console nomme les candidates. L'émission est `.As(T.Create)`, -avec le §5.3 appliqué au corps de la fabrique elle-même. - -Convention, pas attribut, pas configuration : un attribut supposerait de toucher au code de -production du développeur pour plaire à un outil de test, et un fichier de configuration casserait -la règle de conception 2. - -### 5.5 Paramètres non résolus - -L'argument du paramètre dans le constructeur public devient un identifiant qui n'existe pas : - -```csharp - public AnyOrder() - : this(reference: Any.String().NonEmpty().As(OrderReference.Create), - // TODO(dum): no generator inferred for 'Customer customer'. - // Scaffold one: dum generate Customer - // or write one here, or delete this argument and always pass .WithCustomer(...). - customer: TODO_supply_a_generator_for_customer, - quantity: Any.Int32().Positive(), - ... -``` - -Le fichier ne compile pas tant que le développeur n'a pas agi. C'est le but (D6). Le message du -compilateur lui-même — *« The name 'TODO_supply_a_generator_for_customer' does not exist in the -current context »* — est l'instruction, et il apparaît dans l'IDE, dans la liste d'erreurs et en -intégration continue. - -Les deux alternatives ont été écartées : une expression `throw` compile et reporte l'échec au premier -run de test, et omettre le paramètre rend `AnyOrder` silencieusement inutilisable. Le développeur -lance le tool et ouvre le fichier dans la même minute ; un soulignement rouge à la ligne exacte lui -coûte dix secondes, un échec à l'exécution une semaine plus tard lui coûte bien davantage. - ---- - -## 6. Sortie console - -Le récapitulatif console n'est pas décoratif : c'est le mécanisme qui maintient le tool honnête sur -ce qu'il a inféré et ce qu'il a deviné. - -L'exécution ci-dessous porte sur le même `Order` qu'au §4.1, mais *avant* que `AnyCustomer` ne soit -scaffoldé — d'où l'unique paramètre resté ouvert. Scaffolder `Customer` puis relancer avec `--force` -le referme, et ce deux-temps est la façon prévue de traverser un graphe d'agrégats. - -```console -$ dum generate Order - -Analyzing Shop.Domain.Order - constructor Order(OrderReference, Customer, int, OrderStatus, IReadOnlyList, DateTime) - - reference OrderReference Any.String().NonEmpty().As(OrderReference.Create) factory, guard - customer Customer — TODO - quantity int Any.Int32().Positive() guard - status OrderStatus Any.Enum() - tags IReadOnlyList Any.ListOf(Any.String().NonEmpty()) - placedAt DateTime Any.DateTime() - -✓ AnyOrder.cs — 5 of 6 parameters inferred, 1 TODO. - The file will not compile until you resolve it. That is deliberate. -``` - -La colonne de droite porte la provenance de chaque expression : vide pour la table de base, `guard` -quand le §5.3 l'a resserrée, `factory` quand le §5.4 l'a composée, `AnyX` quand un generator -scaffoldé a été réutilisé, `guards not combined` pour le cas de conflit du §5.3, `no source` quand le -corps du constructeur était indisponible et qu'aucune garde n'a pu être lue, `unread guards` quand -le corps lève d'une façon que l'ensemble reconnu n'a pas appariée, et `unavailable` quand le -generator existe dans la bibliothèque mais pas dans l'asset que ce projet résout. - -Cette dernière valeur compte plus qu'il n'y paraît. Sans elle, la dégradation de D4 est -indiscernable d'une simple ignorance du tool : un paramètre `DateOnly` sur un projet downlevel se -lirait « non inféré », alors que la vérité est « inféré, mais `Any.DateOnly()` n'existe pas ici — -change de cible, ou écris-le toi-même ». Un mot transforme une impasse en instruction. - -**La provenance est une donnée, pas une sortie.** Le moteur la retourne dans son modèle de résultat -(§10.3) ; la CLI la rend. C'est ce qui rend le récapitulatif testable sans console. - -`--dry-run` affiche le même récapitulatif sur stderr et le fichier sur stdout. - ---- - -## 7. Modes d'échec et codes de sortie - -| Situation | Sortie | Comportement | -|---|---|---| -| Fichier écrit, tout inféré | `0` | — | -| Fichier écrit, un ou plusieurs TODO | `0` | L'écriture a réussi ; le build du développeur signale le reste. | -| `--dry-run` | `0` | Rien n'est écrit. | -| Type introuvable / ambigu | `1` | Candidats listés. | -| Fichier de sortie existant, sans `--force` | `1` | Nomme le fichier, suggère `--force`, avertit que les éditions seront perdues. | -| Aucun / plusieurs projets trouvés | `1` | Candidats listés, `--project` suggéré. | -| Le projet ne charge pas ou n'est pas restauré | `1` | Le diagnostic MSBuild, tel quel. | -| Le projet ne référence pas JustDummies | `1` | Rien ne peut être résolu (D4) ; le dit et suggère le package. | -| `Any{Type}` masque un type `JustDummies.Any*` | `0` | **Avertissement**, puis génération. | - -Cette dernière ligne mérite sa note, et le contrôle derrière est plus étroit qu'il n'y paraît. La -bibliothèque déclare 40 noms de types publics `Any*`, mais **8 sont génériques** — `AnyList`, -`AnySet`, `AnyArray`, `AnySequence`, `AnyDictionary`, `AnyOneOf`, `AnyEnum`, -`AnyCollection<…>`. L'arité fait partie de l'identité d'un type en C#, donc un `AnySet` scaffoldé -(arité 0) et le `AnySet` de la bibliothèque **coexistent sans rien masquer** — vérifié. Un type -métier nommé `Set`, `List` ou `Sequence` est une fausse alerte. - -Le vrai ensemble de collision, ce sont les **32 noms non génériques** (§14.2) : `AnyString`, -`AnyGuid`, `AnyUri`, `AnyPattern`, `AnyChar`, `AnyBoolean`, `AnyDateTime`, `AnyContext`, -`AnyDecimal`, `AnyInt32`, … Un type métier nommé `Pattern`, `Context` ou `Uri` scaffolde vers un nom -qui, dans son propre namespace, **masque silencieusement le type de la bibliothèque** pour tous les -fichiers de ce namespace : C# résout le namespace englobant avant tout `using`. Cela compile ; c'est -simplement faux plus tard — vérifié. Le tool avertit, nomme les deux types, et génère quand même ; -sous la règle de conception 4 le renommage est l'affaire du développeur, et la v1.1 lui en donne le -levier. - -Le contrôle doit donc comparer l'arité, pas seulement le nom. Avertir sur les 40 crierait au loup -sur les huit qui ne peuvent pas entrer en collision. - -Plusieurs arguments de type (`dum generate Order Customer Invoice`) sont traités indépendamment ; le -code de sortie est le pire d'entre eux, et un échec n'empêche pas l'écriture des autres. - ---- - -## 8. Garanties - -### 8.1 Déterminisme - -Le même type analysé contre la même compilation produit un fichier **identique à l'octet près**, sur -n'importe quelle machine, sous n'importe quelle version du tool qui résout les mêmes membres. Rien -qui dépende du temps, du chemin, de la culture ou d'un ordre de hachage n'entre dans la sortie : -aucun horodatage, aucune version de tool, aucun chemin absolu, et toute énumération parcourue par -l'émetteur est ordonnée par déclaration. - -Cela compte même sans verbe `check` : c'est ce qui rend un nouveau scaffold relisible comme un diff. - -### 8.2 Reproductibilité - -Le generator émis tire du contexte **ambiant**, parce que toutes les expressions qu'il émet viennent -de la façade statique `Any`, et que la source ambiante résout la frame `AsyncLocal` courante **au -moment du tirage**, pas à la construction (§14.5). Donc : - -```csharp -AnyOrder recipe = new AnyOrder(); // construit hors du scope -Any.Reproducibly(() => { - Order order = recipe.Generate(); // toujours épinglé par le seed du scope -}); -``` - -est reproductible, tout comme le cas ordinaire où les deux se produisent dans le scope. Cela a été -vérifié (§17). - -**`Any.WithSeed(seed)` est hors périmètre (D7).** Un `AnyContext` porte sa propre source aléatoire -fixe et n'est pas affecté par le scope ambiant, donc un generator construit à partir de `Any.*` ne -peut pas y tirer. Un développeur sur `WithSeed` fournit les generators de ce contexte paramètre par -paramètre via la surcharge `.With{Param}(IAny)`, et la doc XML émise le dit en une phrase -(§4.1). Le raisonnement, et les alternatives pesées contre lui, sont dans D7. - -L'émetteur ne produit jamais d'état statique, donc `JD009` et `JD020` n'ont rien sur quoi se -déclencher. - -### 8.3 Aucune réflexion dans le code émis - -Le fichier émis ne contient aucune réflexion — ce sont des appels de constructeur et des chaînes -fluides. La revendication *« no reflection »* de la bibliothèque porte sur ce qui s'exécute dans le -test du développeur, et elle tient. - -Le **tool lui-même** est un programme de build et n'est soumis à aucune contrainte de ce genre ; il -utilise Roslyn, qui n'est de toute façon pas de la réflexion. Les deux questions sont indépendantes. - ---- - -## 9. Non-objectifs de la v1.0 - -Nommés explicitement pour ne pas être pris pour des oublis. - -* **Données réalistes.** Le tool hérite du périmètre de la bibliothèque : arbitraire-mais-valide, - jamais plausible. Ni noms, ni emails, ni adresses. -* **Remplissage automatique de graphe d'objets.** La composition est d'un saut, via `Any{T}` ou une - fabrique à un paramètre, limitée à une profondeur de 3. Au-delà, le développeur l'écrit. -* **Les invariants que le tool ne peut pas voir.** Le §5.3 lit un ensemble clos d'idiomes de garde. - Là où le constructeur lève d'une façon que l'ensemble n'apparie pas — règle inter-paramètres, - condition arithmétique, garde regex (§5.3) — le paramètre obtient le generator neutre et le - récapitulatif le marque `unread guards`, pour que le développeur sache où regarder. Là où la - validation est entièrement déléguée à un helper (`Guard.Against.Null(p)`), il n'y a aucun `throw` - à voir dans le corps, et le - tool ne peut pas distinguer ce paramètre d'un paramètre non contraint. Dans aucun des deux cas il - ne devine. -* **L'aller-retour.** Le tool ne relit jamais un fichier qu'il a écrit. -* **Membres `init` / `required`, construction par propriétés.** Constructeur et fabrique statique - uniquement. -* **Tout ce qui relève de `--all`.** Arguments de type explicites seulement. - ---- - -## 10. Architecture - -### 10.1 Deux projets - -| Projet | TFM | Rôle | -|---|---|---| -| `JustDummies.GenAny` | `netstandard2.0`, épinglé au plancher Roslyn (§13.2) | Le moteur. Résolution, lecture des gardes, composition, émission. | -| `JustDummies.Cli` | `net8.0`, `RollForward=Major` | La coquille. Commandes, chargement du projet, IO fichier, console. | - -Sur le nom : le moteur existant du tool frère de ce dépôt s'appelle `GenDoc` — un nom de -**fonction**, pas un nom de pattern (`GenDoc` génère de la documentation). `GenAny` le suit -exactement : il génère les types `AnyX`, et `Any` est le nom central de la bibliothèque -(`Any.String()`, `IAny`, `AnyOrder`). « Scaffolder » a été écarté comme nom de projet — il -nomme un rôle générique plutôt qu'un produit, et tous les frameworks en ont un. Le mot survit dans -la prose, où il décrit un **comportement** (§1) ; le projet est nommé d'après ce qu'il **produit**. - -### 10.2 La frontière - -**`JustDummies.GenAny` possède** la table de résolution (§5.2), la lecture des gardes (§5.3), la -composition et la reconnaissance de fabriques (§5.4), l'émetteur (§11.2) et la fonction de nommage -(§11.3). -Il dépend de `Microsoft.CodeAnalysis.CSharp` **uniquement** — pas de `Workspaces`, dont il n'a pas -besoin : la lecture des gardes veut un arbre syntaxique et un modèle sémantique, et l'émission est -de la construction de chaînes. - -**Il ne fait aucune IO, n'écrit sur aucune console, et ne touche jamais MSBuild.** Ces trois -contraintes sont ce qui le maintient chargeable dans un hôte Roslyn. - -**`JustDummies.Cli` possède** les définitions de commandes et de settings Spectre, la découverte de -projet, `MSBuildLocator` / `MSBuildWorkspace`, l'écriture de fichiers, la gestion de `--force` / -`--dry-run`, le rendu du récapitulatif console, et les codes de sortie du §7. - -### 10.3 Le contrat entre les deux - -Un point d'entrée, taillé pour que le futur consommateur IDE puisse l'appeler tel quel : - -* **Entrée** — une `Compilation`, l'`ITypeSymbol` cible, et un enregistrement d'options portant la - surcharge de namespace et le motif de nommage du type (§16). -* **Sortie** — un modèle de résultat, jamais une chaîne nue : - * le nom de fichier et le texte source complet ; - * une ligne par paramètre : nom, type affiché, expression émise (ou aucune), et provenance (§6) ; - * les avertissements, comme le cas de masquage `Any*` du §7 ; - * un indicateur « contient au moins un TODO » ; - * **l'échec comme donnée, pas comme exception** — un type cible qui ne résout vers rien ou vers - plusieurs candidats revient comme un résultat portant cette liste de candidats, de sorte que la - CLI le projette sur les codes de sortie du §7 sans rien attraper. Le §11.1 place la résolution - du type dans le moteur, donc le modèle doit porter cela ou la frontière fuit des exceptions. - -La CLI rend ce modèle ; un code refactoring appliquerait le texte source et ignorerait le reste. -Rien dans le modèle n'est une chaîne destinée à une console. - -### 10.4 Packaging - -`JustDummies.Cli` est packagé comme le .NET tool (`PackAsTool`, `ToolCommandName=dum`, -`PackageId=JustDummies.Cli`). `JustDummies.GenAny` n'est **pas publié comme package propre** en -v1.0 : il voyage dans le package du tool comme dépendance managée ordinaire, exactement comme le -dépôt frère publie son moteur `GenDoc`. Le publier plus tard, quand un consommateur IDE existera, -est une décision purement additive. - -Conséquence : aucun des deux projets ne porte de promesse de compatibilité d'API publique, donc -aucun ne prend de baseline d'API publique (§13.4). - -**D9 s'applique aux deux.** Aucun des deux ne référence le package ni le projet `JustDummies`. -Chaque symbole JustDummies est résolu par nom de métadonnée contre la compilation du développeur, -exactement comme le font les analyzers de la bibliothèque. L'écart de version entre le tool et la -bibliothèque est donc structurellement impossible, et le package du tool ne doit déclarer aucune -dépendance `JustDummies` (§13.6). - ---- - -## 11. Notes d'implémentation - -### 11.1 Chaîne de traitement - -1. `MSBuildLocator.RegisterDefaults()` — **avant de toucher au moindre type de workspace Roslyn**. - Charger `MSBuildWorkspace` d'abord est la façon classique dont cela échoue, avec une - `FileNotFoundException` sur `Microsoft.Build` qui ne nomme rien d'utile. (CLI uniquement.) -2. `MSBuildWorkspace.Create()`, ouvrir le projet, prendre sa `Compilation`. Les diagnostics du - workspace sont remontés, pas avalés. (CLI uniquement.) -3. Passer la `Compilation` au moteur. Tout ce qui suit est `JustDummies.GenAny`. -4. Résoudre `JustDummies.Any`, ``JustDummies.IAny`1`` et `JustDummies.AnyExtensions` par nom de - métadonnée. Absents → le moteur le signale et la CLI sort en `1` (§7). -5. Résoudre le type cible (§3.2), choisir le constructeur (§5.1). -6. Par paramètre : table de base (§5.2) → gardes (§5.3) → composition (§5.4) → non résolu (§5.5). - Tout membre candidat est cherché dans la compilation avant d'être retenu (D4). -7. Émettre dans le modèle de résultat (§10.3). -8. La CLI écrit le fichier et rend le récapitulatif. - -### 11.2 Émetteur - -Un simple constructeur de chaînes sur un modèle ordonné, pas `SyntaxFactory`. La sortie doit être -lisible et correspondre à une mise en page écrite à la main — déclarations de champs alignées, types -explicites, accolades — et l'espacement normalisé par `SyntaxFactory` ne produit pas cela. -L'émetteur étant couvert par des tests à fichier de référence (§12), l'argument de fragilité en -faveur d'une API syntaxique ne s'applique pas. - -### 11.3 Nommage - -Faire passer le nom du type émis par **une seule** fonction, -`TypeNaming.GeneratorNameFor(ITypeSymbol, NamingOptions)`. La v1.1 (§16) devient alors une -modification de cette fonction plus une liaison d'options, pas un balayage. En v1.0 -`NamingOptions` ne porte qu'un motif fixe, `Any{Type}`. - ---- - -## 12. Plan de tests - -**Moteur — `JustDummies.GenAny.UnitTests`** (le gros) : - -* **Tests unitaires du résolveur.** Construire une `CSharpCompilation` en mémoire avec une référence - sur le `JustDummies.dll` construit, et asserter la chaîne d'expression émise par paramètre. - Rapide, sans MSBuild. Couvrir chaque ligne du §5.2, chaque ligne du §5.3, les deux chemins du - §5.4 et le repli du §5.5. Inclure le cas non signé (`p <= 0` sur un `uint`), le cas nullable de - type valeur, les deux issues de composition du §5.3 (bornes complémentaires conservées, même borne - abandonnée), une garde de taille sur un paramètre **collection** (qui doit atteindre - `WithMaxCount`, jamais `WithMaxLength`), et `p < 1` sur un paramètre intégral puis sur un - `decimal` — les deux lignes qui ne diffèrent que par le type du paramètre. Ajouter un cas - négatif : un constructeur gardé par `!Regex.IsMatch(...)` ne doit produire **aucune** contrainte - de motif, pour que l'exclusion du §5.3 ne puisse pas être défaite par inadvertance. -* **Fichiers de référence de l'émetteur.** Un fichier approuvé par forme représentative : aucun - paramètre, un paramètre, six paramètres, un TODO, une collision de nom, un record positionnel, - une cible à fabrique statique. Le fichier sans paramètre épingle la forme dégénérée du §4.2 — - émettre les deux constructeurs sans condition y donne un `CS0111`. Le fichier de collision doit - utiliser un nom de bibliothèque **non générique** (`Pattern`, `Context`, `Uri`), puisqu'un nom - générique ne peut pas entrer en collision (§7). -* **Tests de compilation de la sortie.** Chaque fichier de référence est compilé contre - `JustDummies.dll` **avec les analyzers JustDummies branchés**, et la compilation ne doit produire - aucune erreur `CS*` ni aucun diagnostic `JD*`. C'est le contrôle que D3 rend possible : le fichier - n'étant pas marqué comme code généré, les analyzers tournent réellement dessus. Le harnais doit - inclure un **fichier de contrôle avec une violation connue**, dont on asserte qu'elle se - déclenche — sinon « aucun diagnostic » ne se distingue pas de « analyzers non chargés » (§17.2). -* **Le test sur le code du dépôt.** Scaffolder les **vrais types du dépôt hôte**, compiler les - résultats et générer une valeur depuis chacun. Le raisonnement est consigné dans la décision - « faire tourner les analyzers sur notre propre code » (§13.7) : une règle et le snippet qui la - teste, écrits par le même auteur, partagent la même idée fausse et passent ensemble ; du code - écrit pour d'autres raisons, non. `ErrorCode.Create` du dépôt actuel est le cas canonique — il - garde sur `IsNullOrWhiteSpace`, donc sans le §5.3 le code scaffoldé échoue environ une fois sur - seize, ce qu'aucun fichier de référence ne révélerait. Dans un dépôt dépourvu de tels types, - prendre n'importe quel value object validant doté d'une fabrique statique. -* **Test de sélection d'asset.** Scaffolder contre un consommateur de l'asset `netstandard2.0` et un - consommateur de l'asset `net8.0` pour un type ayant un paramètre `DateOnly`, et asserter que le - premier produit un TODO **marqué `unavailable`** — pas seulement un TODO — et le second - `Any.DateOnly()`. C'est la preuve exécutable de D4 (§13.8). - -**Coquille — `JustDummies.Cli.UnitTests`** : découverte de projet, gestion des options, codes de -sortie du §7, et rendu du récapitulatif depuis un modèle de résultat figé. - ---- - -## 13. Ce que le dépôt hôte doit fournir - -JustDummies a vocation à rejoindre son propre dépôt avant que ce tool soit construit. Cette section -énonce chaque dépendance envers l'hôte comme une **exigence**, avec la réalisation actuelle en -exemple. Si la bibliothèque a déménagé, rétablir tout cela là-bas ; ne pas construire le tool contre -l'infrastructure d'un autre dépôt. - -### 13.1 Versions de packages épinglées - -Pour les dépendances du tool. Nouvelles pour le tool : -`Microsoft.CodeAnalysis.Workspaces.MSBuild` et `Microsoft.Build.Locator` (CLI uniquement). Déjà -présentes pour la bibliothèque et ses analyzers : `Microsoft.CodeAnalysis.CSharp` et -`Spectre.Console.Cli`. *Réalisation actuelle : gestion centralisée des packages dans -`Directory.Packages.props`.* - -### 13.2 Une propriété de plancher Roslyn - -`JustDummies.GenAny` doit compiler contre la **même -version minimale de Roslyn que le package d'analyzers**, et ne pas flotter au-dessus — un assembly -chargé par le compilateur d'un consommateur échoue silencieusement (`CS8032`) sur un hôte plus -ancien s'il a été construit contre un Roslyn plus récent. *Réalisation actuelle : -`RoslynFloorVersion` = `4.8.0`, posée une fois dans `Directory.Build.props` et appliquée avec -`VersionOverride`.* La CLI n'est **pas** liée par cela : elle héberge son propre compilateur. - -### 13.3 Imbrication dans la solution - -Si l'hôte utilise un `.sln`, ajouter les deux projets et les -deux projets de test à son `GlobalSection(NestedProjects)`, sous les dossiers de solution source et -tests. Un projet absent de cette section apparaît en vrac à la racine de la solution au lieu d'être -groupé avec ses frères. Cela a été manqué puis corrigé après coup à plusieurs reprises ; le vérifier -à chaque ajout de `.csproj`. - -### 13.4 Exclusion de la baseline d'API publique - -Ni `JustDummies.GenAny` ni `JustDummies.Cli` -n'adhèrent à la baseline d'API publique : les outils ne portent aucune promesse de compatibilité, et -l'analyzer signalerait toute leur surface comme non déclarée. *Réalisation actuelle : seules les -bibliothèques publiées importent `build/PublicApiBaseline.props`.* - -### 13.5 Tests de mutation - -Si l'hôte mesure la mutation sur les projets dont le code est publié ou -s'exécute, les deux projets qualifient. Donner à chacun sa propre configuration — le moteur est la -cible à forte valeur, la coquille non — et les enregistrer avec les autres. *Réalisation actuelle : -un JSON par projet sous `build/stryker/`, piloté par un flux dédié, consultatif par pull request et -imposé par un balayage hebdomadaire.* - -### 13.6 Un train de publication pour le tool - -Distinct de celui de la bibliothèque. Le tool ne -versionne pas en lockstep avec la bibliothèque (D9), donc il ne doit pas monter sur son train. -L'étape de packaging du train doit asserter que le `.nupkg` produit ne déclare **aucune dépendance -`JustDummies`** — la forme exécutable de D9. *Réalisation actuelle : `tools/packaging/pack.sh` avec -un train par famille de packages et une assertion « standalone » déjà écrite pour le train de la -bibliothèque.* - -### 13.7 Les analyzers doivent pouvoir tourner sur le code de l'hôte - -Pour que le test sur le code -du dépôt (§12) puisse exister. *Réalisation actuelle : le projet d'analyzers est branché sur les -suites du dépôt lui-même, décision prise après avoir constaté que la suite unitaire des analyzers -n'attrapait pas cinq règles fausses que le passage sur du vrai code a attrapées immédiatement.* - -### 13.8 Deux TFM consommateurs pour la bibliothèque packagée - -Pour que le -test de sélection d'asset (§12) puisse exister : un consommateur en `net8.0` (résout l'asset -`net8.0`) et un en dessous (résout `netstandard2.0`). *Réalisation actuelle : un projet isolé hors -solution, multi-ciblé, consommant le `.nupkg` packagé depuis un flux local.* - -### 13.9 Framework de tests - -*Réalisation actuelle : `xunit.v3`, `NFluent`, `Verify.XunitV3` pour -les fichiers de référence, `NSubstitute`.* Tout équivalent convient ; les tests à fichier de -référence ont besoin d'une bibliothèque de snapshots. - -### 13.10 Conventions de commit, de branche et de pull request - -Et un processus ADR pour le §15. -*Réalisation actuelle : Conventional Commits avec une liste close de types et de scopes, imposée par -un hook et par la CI ; ADR sous `doc/handwritten/for-maintainers/adr/` où un agent rédige en -`Proposed` et le mainteneur accepte.* - ---- - -## 14. Faits sur la bibliothèque dont dépend cette spécification - -Tout ce qui suit a été lu dans la source de la bibliothèque. C'est inliné pour que ce document -puisse être implémenté sans ouvrir la bibliothèque, et pour qu'un lecteur futur puisse repérer -quelles affirmations sont porteuses. Le §14.7 donne la commande pour redériver chaque bloc. - -### 14.1 Identité du package et frameworks cibles - -* `PackageId` **`JustDummies`**, `TargetFrameworks` **`netstandard2.0;net8.0`**, `Nullable` activé, - `LangVersion` latest. -* Les deux assets divergent : la branche `net8.0` porte en plus `DateOnly`, `TimeOnly`, `Int128`, - `UInt128` et `Half`, gardés par `#if NET8_0_OR_GREATER`. Un consommateur sous `net8.0` résout - l'asset `netstandard2.0` et ne les voit pas. C'est le fait que D4 existe pour absorber. -* Les analyzers voyagent **dans** ce package, sous `analyzers/dotnet/cs`, donc tout consommateur les - reçoit automatiquement. C'est pour cela que le fichier émis est analysé du tout (D3). -* Un package compagnon adapte la bibliothèque à xUnit v3 (`[Reproducible]`) ; le tool n'interagit - pas avec lui. - -### 14.2 Points d'entrée - -`JustDummies.Any` est une façade statique, répartie en fichiers partiels par famille. L'ensemble -complet des fabriques, toutes tirant du contexte aléatoire ambiant : - -* **Primitifs** — `String()`, `Boolean()`, `Char()`, `Guid()`, - `SByte()`, `Byte()`, `Int16()`, `UInt16()`, `Int32()`, `UInt32()`, `Int64()`, `UInt64()`, - `Single()`, `Double()`, `Decimal()`, - `TimeSpan()`, `DateTime()`, `DateTimeOffset()`, - `Enum() where TEnum : struct, Enum`. -* **Asset `net8.0` uniquement** — `DateOnly()`, `TimeOnly()`, `Int128()`, `UInt128()`, `Half()`. -* **Motif** — `StringMatching(string)`, `StringMatching(Regex)`. -* **URI** — `Uri()`, puis un sélecteur de famille : `.Web()`, `.Ftp()`, `.Mailto()`, `.Relative()`, - `.WebSocket()`. -* **Choix** — `OneOf(params T[])`, `ElementOf(IReadOnlyList)`, - `ElementOf(IEnumerable)`. -* **Collections** — `ListOf`, `ArrayOf`, `SequenceOf`, `SetOf` (avec comparateur - optionnel), `DictionaryOf` (avec comparateur de clés optionnel). -* **Composition** — `Combine` en arités 2 à 8, `PairOf`, `TripleOf`. -* **Reproductibilité** — `WithSeed(int)`, `UseSeed(int)`, `UseSeed(int, string)`, - `Reproducibly(...)`, `ReproduciblyAsync(...)`. - -Attention aux pièges de nommage : c'est **`Any.Boolean()`**, pas `Any.Bool()` ; et `double` se -projette sur **`Any.Double()`**, pas `Any.Decimal()`. - -`AnyContext`, retourné par `Any.WithSeed(int)`, reflète les primitifs, le motif, l'URI et les points -d'entrée de choix comme méthodes **d'instance** tirant de sa propre source fixe. Il ne reflète -**pas** les points d'entrée de collection ni de composition. D7 le met hors périmètre. - -La bibliothèque déclare **40 noms de types publics `Any*`** — 38 generators plus `AnyContext` et -`AnyGenerationException`. **8 sont génériques et 32 ne le sont pas**, et seuls les non génériques -peuvent être masqués par un `Any{Type}` scaffoldé ; c'est cet ensemble de 32 noms que -l'avertissement du §7 interroge. (`AnyCollection<…>`, la base abstraite des generators de -collection, est facile à manquer au comptage : elle est déclarée `public abstract class`, pas -`public sealed class`.) - -### 14.3 Surfaces de contraintes utilisées par l'émetteur - -| Famille de generator | Surface de contraintes disponible pour l'émetteur | -|---|---| -| `AnyString` | `NonEmpty`, `WithMinLength`, `WithMaxLength`, `WithLength`, `WithLengthBetween`, `StartingWith`, `EndingWith`, `Containing`, `Alpha`, `Numeric`, `AlphaNumeric`, `UpperCase`, `LowerCase`, `WithChars`, `OneOf`, `Except`, `DifferentFrom` | -| Entiers signés (`SByte`, `Int16`, `Int32`, `Int64`) | `Positive`, `Negative`, `NonZero`, `Zero`, `Between`, `GreaterThan(OrEqualTo)`, `LessThan(OrEqualTo)`, `MultipleOf`, `OneOf`, `Except`, `DifferentFrom` | -| **Entiers non signés** (`Byte`, `UInt16`, `UInt32`, `UInt64`) | les mêmes **moins `Positive` et `Negative`**, qu'un type non signé ne peut pas exprimer | -| `AnyDouble`, `AnySingle` | comme les entiers signés, moins `MultipleOf` | -| `AnyDecimal` | comme les entiers signés, moins `MultipleOf`, plus `WithScale` | -| `AnyGuid` | `NonEmpty`, `Empty`, `OneOf`, `Except`, `DifferentFrom` | -| `AnyBoolean` | `True`, `False`, `DifferentFrom` | -| `AnyEnum` | `AllowingCombinations`, `OneOf`, `Except`, `DifferentFrom` | -| Temporels (`DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`) | `After(OrEqualTo)`, `Before(OrEqualTo)`, `Between`, `WithGranularity`, `OneOf`, `Except`, `DifferentFrom` | -| `AnyTimeSpan` | style temporel plus `Positive`, `Negative`, `NonZero`, `Zero` | -| Collections | `Empty`, `NonEmpty`, `WithCount`, `WithCountBetween`, `WithMinCount`, `WithMaxCount`, `Containing`, `ContainingAny` | - -Deux lignes mordent. La ligne **non signée** est pourquoi D4 doit filtrer `.Positive()` plutôt que -laisser l'émetteur supposer une algèbre numérique uniforme. La ligne **collections** est pourquoi -une garde de taille doit atteindre la famille `Count` : il n'y a pas de `WithLength` sur un -generator de collection, donc lire une telle garde contre la famille des chaînes émet un membre qui -ne se résout jamais (§5.3). - -La v1.0 n'utilise que les contraintes de taille, de signe et de borne. Les familles jeu de -caractères et motif sont listées parce que le §16 pourrait y revenir, pas parce que l'émetteur s'en -sert aujourd'hui. - -### 14.4 Seams de composition - -* `AnyExtensions.As(this IAny, Func)` → `IAny`. - Un groupe de méthodes comme `OrderReference.Create` s'y lie directement. Quand la fabrique rejette - la valeur générée, l'appel lève `AnyGenerationException`. -* `Any.Combine` (arités 2 à 8) → `IAny`. -* Les generators de collection dérivent d'une base commune implémentant `IAny` : - `ListOf` → `List`, `ArrayOf` → `T[]`, `SequenceOf` → `IEnumerable`, `SetOf` → `HashSet`, - `DictionaryOf` → `Dictionary`. -* `NullableExtensions.OrNull()` existe en deux formes, une pour les types valeur et une pour les - types référence annotés. **D10 interdit d'émettre l'une ou l'autre.** - -### 14.5 Invariants sémantiques dont dépend le code émis - -Ces cinq-là sont ceux qui casseraient silencieusement le code émis s'ils changeaient. Chacun est -exercé au §17. - -1. **La source ambiante se résout au moment du tirage.** Chaque fabrique `Any.*` capture une source - ambiante singleton, et cette source lit la frame `AsyncLocal` courante à l'intérieur de - `Generate()`, pas à la construction. C'est pour cela qu'une recette construite hors d'un scope de - reproductibilité y rejoue quand même (§8.2). -2. **`IAny` est covariante.** D'où l'absence d'adaptateur pour les lignes d'interface de - collection du §5.2 — et la nécessité d'un pour la ligne nullable de type valeur. -3. **Les generators sont des recettes immuables.** Chaque contrainte fluide retourne une nouvelle - instance. D2 en hérite. -4. **`Any.String()` non contraint tire 0 à 16 lettres et chiffres ASCII.** Il peut retourner la - chaîne vide ; il ne peut jamais retourner du blanc. Les deux moitiés comptent pour les §5.2 et - §5.3. -5. **`Any.OneOf(value)` exige au moins une valeur, rejette les éléments `null`, et consomme un - tirage.** Ces trois raisons sont pourquoi le §4.2 émet un `FixedValue` privé à la place. - -### 14.6 Inventaire des analyzers - -28 identifiants de diagnostic sur 27 classes d'analyzer — `JD023` et `JD024` en partagent une. - -| Plage | Catégorie | Sévérités | -|---|---|---| -| `JD001`–`JD004` | Reproducibility | toutes **Error** | -| `JD005` | Usage | **Error** | -| `JD006` | Usage | Warning | -| `JD007`–`JD010` | Reproducibility | Warning | -| `JD011` | Usage | **Désactivé par défaut** | -| `JD012`–`JD013` | Usage | Warning | -| `JD014`–`JD017` | Constraints | Warning | -| `JD018` | Reproducibility | Warning | -| `JD019` | Reproducibility | **Désactivé par défaut** | -| `JD020` | Reproducibility | Info | -| `JD021` | Reproducibility | Warning | -| `JD022` | Reproducibility | Info | -| `JD023` | Constraints | Warning | -| `JD024` | Constraints | Info | -| `JD025`–`JD026` | Constraints | Warning | -| `JD027`–`JD028` | Composition | Warning | - -Trois faits à leur sujet pilotent des décisions de ce document : - -* **Les 27 appellent `ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)`** — d'où D3. -* **Les règles `Usage` matchent tout type implémentant `IAny`**, pas une liste de generators - intégrés — d'où le second bénéfice de D2. -* **Les règles `Reproducibility` matchent les chaînes enracinées sur la façade statique `Any`**, et - répondent délibérément « non » pour un generator atteint par un local, un champ ou un paramètre. - `new AnyOrder().Generate()` leur est donc invisible ; c'est une limite connue et acceptée, pas un - défaut que le tool peut corriger. - -### 14.7 Comment redériver ces faits - -Depuis la racine du dépôt de la bibliothèque : - -```console -# 14.1 identité du package et clivage de TFM -grep -n "TargetFrameworks\|PackageId\|analyzers/dotnet/cs" JustDummies/JustDummies.csproj -grep -n "#if NET8_0_OR_GREATER" JustDummies/Any.Primitive.cs - -# 14.2 points d'entrée, et le miroir AnyContext -grep -hn "public static" JustDummies/Any.*.cs -grep -n "public " JustDummies/AnyContext.cs -# Les noms de types AVEC leur arité. `abstract` compte — AnyCollection n'est pas sealed, et un -# motif n'acceptant que `sealed` sous-compte d'une unité. L'arité est ce dont le contrôle de -# masquage du §7 a besoin : 8 noms génériques ne peuvent pas entrer en collision, les 32 autres si. -grep -rhoP "^public (?:sealed |abstract )?class \KAny\w+(?:<[^>]*>)?" JustDummies/*.cs | sort -u - -# 14.3 surfaces de contraintes -grep -oP "public AnyInt32 \K\w+(?=\()" JustDummies/AnyInt32.cs | sort -u -grep -oP "public AnyUInt32 \K\w+(?=\()" JustDummies/AnyUInt32.cs | sort -u # noter : ni Positive ni Negative - -# 14.4 seams de composition -grep -n "public static" JustDummies/AnyExtensions.cs JustDummies/NullableExtensions.cs - -# 14.5 invariants — les docs XML les énoncent tous les cinq -sed -n '1,60p' JustDummies/IAny.cs -grep -n "AmbientRandomSource.Instance" JustDummies/Any.Primitive.cs | head -3 - -# 14.6 inventaire des analyzers et exemption du code généré -cat JustDummies.Analyzers/AnalyzerReleases.Unshipped.md -grep -rlc "ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)" JustDummies.Analyzers/*.cs | wc -l -``` - -Les chemins sont ceux du dépôt actuel ; les ajuster si la bibliothèque a déménagé. - ---- - -## 15. Enregistrements de décision - -Les onze décisions du §2 sont architecturales : un mainteneur futur remettrait chacune en question, -et chacune tiendrait inchangée même si l'implémentation était entièrement réécrite. **Dix -enregistrements** les couvrent — D5 et D6 en partagent un. Dans le cours normal des choses, elles -entreraient dans la base ADR d'un dépôt en `Proposed`, y recevraient un numéro, et seraient -acceptées par le mainteneur. - -**Elles sont tenues à l'intérieur de cette spécification, parce que le dépôt qui devrait les -accueillir n'existe pas encore.** JustDummies a vocation à quitter `Reefact/first-class-errors` -avant que ce tool soit construit, et ces enregistrements décrivent un outil qui vivra dans ce -nouveau dépôt. Les faire entrer dans la base actuelle leur attribuerait des numéros — les poignées -stables sur lesquelles toute la base est bâtie — qu'il faudrait abandonner ou réécrire au -déménagement, et laisserait le journal de ce dépôt porter des décisions sur du code qu'il ne -contient plus. - -Les garder ici ne coûte rien et rapporte deux choses. Le raisonnement reste attaché à la -spécification qu'il justifie, donc l'historique de décision voyage comme un seul artefact plutôt -que comme un document plus huit fichiers que quelqu'un doit penser à emporter. Et chaque -enregistrement suit le format ADR de ce dépôt, section par section, de sorte que l'admission est -mécanique : soulever l'enregistrement dans la base ADR du dépôt de destination, lui y attribuer son -numéro, conserver sa date `Proposed:`, et remplacer l'enregistrement ici par un lien. - -D'ici là ce sont des brouillons. Aucun statut n'est basculé dans ce document ; le mainteneur les -accepte dans la base qui les portera. - -Trois de ces enregistrements ont été écrits après la table des décisions, et le pourquoi mérite -d'être gardé. D7, D8 et D10 ont chacune été jugées trop petites d'abord — une limite de périmètre -déjà programmée pour être revisitée, un défaut de namespace avec surcharge, une règle sur une -méthode de la bibliothèque. La taille était à chaque fois la mauvaise mesure ; le test est de savoir -si la décision survit à l'implémentation, et les trois y survivent. - -Surtout, chacune s'est révélée porter ailleurs dans ce document une conséquence qui se lit comme -accidentelle tant que le raisonnement n'est pas écrit. D10 est la raison pour laquelle le §5.2 porte -une conversion explicite pour les nullables de type valeur. D8 est la **cause unique** du risque de -masquage du §7. D7 est la raison pour laquelle le type émis n'a besoin d'aucune règle de cycle de -vie, et pour laquelle deux analyzers de seeding n'ont rien à y signaler. Un enregistrement qui -empêche un nettoyage plausible de réintroduire un défaut mérite sa place quelle que soit sa taille, -et aucune de ces trois conséquences ne s'explique d'elle-même là où elle atterrit. - ---- - -### D1 — Scaffolder le generator une fois et confier le fichier au développeur - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -Le tool écrit un fichier C#, contenant un generator pour un type du code du développeur, dans le -projet du développeur lui-même. Trois formes existent pour un tel outil, toutes utilisées par des -outils réels : un source generator Roslyn produisant le fichier dans la sortie intermédiaire du -build ; un fichier écrit une fois dans l'arbre des sources ; et un fichier écrit dans l'arbre des -sources accompagné d'une commande de vérification qui échoue quand il ne correspond plus à ce que -l'outil produirait aujourd'hui. - -Un fichier dans l'arbre des sources peut se désynchroniser, silencieusement, du type dont il a été -dérivé quand le constructeur de ce type change. - -La bibliothèque que le tool sert affiche l'absence de magie dans son positionnement : pas de -réflexion, pas de remplissage de graphe d'objets, et sa propre description est « small, -deterministic, explicit ». - -Le tool ne peut pas inférer tous les paramètres de constructeur. Certains portent des invariants -exprimés d'une façon qu'aucun ensemble clos de règles ne peut lire (§9), donc un fichier scaffoldé -est censé être incomplet pour certains types. - -La sortie d'un source generator n'est pas éditable par le développeur et n'apparaît pas en revue de -code. Un fichier dans l'arbre des sources est les deux. - -#### Décision - -Le tool écrit chaque fichier de generator une fois et en transfère la propriété au développeur, qui -peut l'éditer librement et à qui il n'est jamais demandé de le régénérer. - -#### Justification - -La dérive est la seule objection sérieuse à l'écriture dans l'arbre des sources, et elle n'existe -que tant que le tool revendique la propriété du fichier. Une fois la propriété transférée, « le -fichier ne correspond plus à ce que le tool produirait » cesse d'être un défaut et devient l'état -attendu d'un fichier que le développeur a édité — ce que le tool lui demande précisément de faire. -L'objection se dissout au lieu d'être atténuée. - -Ce transfert est aussi ce qui rend un fichier incomplet acceptable. Un outil qui possède sa sortie -doit produire quelque chose de complet ou échouer ; un outil qui remet un squelette peut s'arrêter -où sa connaissance s'arrête et le dire, ce qui est la position honnête étant donné que certains -invariants sont illisibles. D5 et D6 dépendent de ce point réglé d'abord. - -L'éditabilité et la visibilité en revue servent une bibliothèque dont l'argument de vente est que -rien ne se passe dans le dos du développeur. Un generator qu'il peut lire, parcourir au débogueur -et modifier est cohérent avec ce positionnement ; un generator matérialisé par le compilateur ne -l'est pas. - -Retirer la propriété retire avec elle toute une classe de machinerie : pas de verbe de -vérification, pas de protocole de régénération, pas de détection de dérive, pas de règles sur les -régions éditables à la main. Pour un outil dont la première règle de conception est d'être trivial -à adopter, la machinerie non construite vaut plus que les garanties qu'elle aurait offertes. - -#### Alternatives considérées - -##### Un source generator Roslyn - -Considéré parce qu'il rend la dérive structurellement impossible : il rejoue à chaque build, donc -sa sortie ne peut pas retarder sur le type. - -Écarté parce qu'il abandonne tout ce que l'existence réelle du fichier apporte. Le développeur ne -peut pas l'éditer, ne peut pas compléter les paramètres que le tool n'a pas su inférer, et les -relecteurs ne le voient jamais. Il n'a par ailleurs aucun moyen utile de laisser du travail -inachevé, donc le cas du paramètre non résolu devrait faire échouer le build sans offrir au -développeur d'endroit où agir. - -##### Un fichier écrit plus un verbe de vérification - -Considéré parce que c'est la réponse standard à la dérive pour les artefacts générés commités, et -qu'elle s'intègre proprement en intégration continue. - -Écarté parce que vérification et édition s'excluent. Une commande qui échoue dès que le fichier -diffère d'une génération fraîche interdit exactement l'édition que ce tool existe pour inviter. -Garder les deux supposerait d'encoder quelles régions appartiennent au tool et lesquelles au -développeur — plus de machinerie que la fonctionnalité entière n'en vaut. - -#### Conséquences - -**Positives.** Le tool a un verbe et aucun protocole. Le fichier scaffoldé est du code ordinaire : -relisible, débogable, éditable. Le chemin du paramètre non résolu de D6 devient disponible. - -**Négatives.** Un generator peut retarder sur son type. Ajouter un paramètre de constructeur casse -la compilation du generator, ce qui fait remonter le problème ; changer l'invariant d'un paramètre, -non — le generator continue de produire des valeurs que le constructeur rejette désormais, et seul -un test en échec le révèle. - -**Risques.** Un développeur peut s'attendre à ce que la régénération préserve ses éditions. Atténué -par l'en-tête émis, qui indique que la régénération écrase et que le type est `partial` donc que -les fichiers voisins survivent, et par `--force` exigé pour écraser tout court. - -#### Actions de suivi - -* Énoncer la position « ce fichier est le tien » en évidence dans la documentation utilisateur du - tool : elle inverse l'attente installée par la plupart des outils de scaffolding. - -#### Références - -* §1, §3, §4.3 de cette spécification. - ---- - -### D2 — Faire du generator émis un `IAny` de plein droit - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -`IAny` est le seam de composition de la bibliothèque : `As`, `Combine`, les generators de -collection et ceux de choix le consomment et le produisent tous (§14.4). - -L'interface est documentée comme une recette immuable, et tous les generators de la bibliothèque -l'honorent — chaque contrainte fluide retourne une nouvelle instance (§14.5). - -La catégorie `Usage` des analyzers reconnaît un generator comme l'interface `IAny` elle-même ou -tout type qui l'implémente, plutôt que comme une liste fixe de types intégrés (§14.6). - -Le type émis expose une méthode fluide par paramètre de constructeur, ce qui lui donne la forme -d'un builder. Les builders de l'écosystème mutent conventionnellement et retournent `this`. - -#### Décision - -Le type émis implémente `IAny` et est immuable, chaque méthode `With` retournant une nouvelle -instance. - -#### Justification - -Implémenter le seam est ce qui fait fonctionner les agrégats imbriqués sans code supplémentaire. Un -generator émis est directement utilisable comme generator d'élément, comme opérande de `Combine` ou -comme source de `As` ; sans l'interface, soit le tool émettrait des adaptateurs, soit le -développeur les écrirait. - -Le second bénéfice est moins évident et vaut autant : les analyzers `Usage` s'appuient sur -l'interface, donc un type émis qui l'implémente est couvert par eux exactement comme un generator -intégré. Cette couverture compte plus ici qu'ailleurs, parce que le fichier émis est celui que le -développeur édite (D3), souvent en découvrant cette API. - -L'immuabilité n'est pas une préférence de style mais le contrat documenté du seam. Un `With` mutant -ferait du type émis le seul generator mutable de l'écosystème, et se comporterait de façon -surprenante : deux generators dérivés d'une base partagée interféreraient. Le coût est une -allocation par appel à `With`, sur un chemin de code qui n'est pas chaud. - -#### Alternatives considérées - -##### Un builder mutant retournant `this` - -Considéré parce que c'est la forme conventionnelle du builder et qu'il alloue moins. - -Écarté parce qu'il contredit le contrat documenté de l'interface qu'il implémenterait, et parce que -dériver deux generators d'une base partagée les corromprait silencieusement tous les deux. - -##### Un type ordinaire exposant `Generate`, n'implémentant pas `IAny` - -Considéré parce qu'il garde le fichier émis exempt de toute interface de bibliothèque. - -Écarté parce qu'il abandonne les deux bénéfices d'un coup : aucune composition avec les seams de la -bibliothèque, et aucune couverture d'analyzer sur le fichier qui en a le plus besoin. - -#### Conséquences - -**Positives.** La composition avec tous les seams de la bibliothèque est gratuite. Quatre règles -d'analyzer s'étendent au type émis sans rien coûter. - -**Négatives.** Une allocation par appel à `With`. Le constructeur privé complet grossit avec le -nombre de paramètres, donc le fichier émis est verbeux pour les constructeurs larges. - -**Risques.** Si la bibliothèque relâchait un jour le contrat d'immuabilité, la forme émise serait -plus stricte que nécessaire — inoffensif, et aucune action ne serait requise. - -#### Actions de suivi - -* Aucune. - -#### Références - -* §4.2, §14.4, §14.5, §14.6 de cette spécification. - ---- - -### D3 — Laisser le fichier scaffoldé ouvert aux analyzers JustDummies - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -Les analyzers voyagent dans le package de la bibliothèque, donc tout consommateur de celle-ci les -reçoit automatiquement (§14.1). - -Les 27 exemptent le code généré (§14.6). Roslyn classe un fichier comme généré quand il se nomme -`*.g.cs` ou `*.generated.cs`, ou quand il s'ouvre sur un commentaire d'en-tête auto-generated. - -L'exemption a été mesurée. Un fichier contenant exactement deux violations — un avertissement -`JD006` et une erreur `JD005` — a été compilé deux fois, en ne changeant que sa première ligne : -sans l'en-tête, les deux ont été remontées et le build a échoué ; avec `// `, -aucune n'a été remontée et le build a réussi (§17). - -Le fichier scaffoldé est celui que le développeur édite (D1), et il peut sortir du tool incomplet -(D6). - -La seule façon pour l'émetteur de produire une chaîne que la bibliothèque rejette à l'exécution est -deux contraintes dérivées de gardes atterrissant sur le même axe (§5.3). `JD015` et `JD023` -détectent exactement cette classe de chaîne insatisfiable. - -La convention de l'écosystème est de marquer les fichiers générés, principalement pour que les -analyzers de style ne se déclenchent pas sur du code écrit par une machine. - -#### Décision - -Le fichier scaffoldé ne porte aucun marqueur de code généré, de sorte que les analyzers JustDummies -l'analysent comme ils analysent du code écrit à la main. - -#### Justification - -L'exemption est totale, et la mesure montre à quel point elle s'applique discrètement : une erreur -de compilation est devenue du silence sur un changement d'une ligne. Marquer le fichier en ferait -le seul fichier du projet de test du développeur hors du filet de sécurité de la bibliothèque. - -Ce serait aussi le pire fichier à exempter. C'est celui que le développeur va éditer, avec une API -qu'il découvre peut-être, dans un fichier que le tool vient de lui demander de compléter. - -La couverture sert en outre de filet aux erreurs de l'émetteur lui-même. La règle du même axe du -§5.3 élimine le cas de la chaîne conflictuelle par construction, mais un défaut dans cette règle ne -remonterait autrement que comme une exception à l'exécution ; avec le fichier analysé, il remonte -dans l'éditeur. - -La raison conventionnelle du marquage — épargner les règles de style au code écrit par une machine -— ne s'applique pas à un fichier qui, par D1, n'appartient pas à une machine. C'est le code du -développeur dès l'instant où il est écrit, et il doit répondre des mêmes règles que ses voisins. - -#### Alternatives considérées - -##### Marquer le fichier d'un en-tête auto-generated - -Considéré parce que c'est la convention de l'écosystème, et parce que cela épargnerait à un premier -scaffold les analyzers de style propres au développeur. - -Écarté parce que cela désactive tout diagnostic JustDummies sur ce fichier, ce qui est l'inverse de -ce dont a besoin un fichier sur le point d'être édité à la main contre une API peu familière. La -mesure rend le coût concret : un diagnostic de sévérité erreur disparaît sans laisser de trace. - -##### Nommer le fichier `*.g.cs` - -Considéré comme une variante plus légère de la même idée. - -Écarté pour la même raison, plus une autre : le nom affirme une propriété machine que D1 nie. - -#### Conséquences - -**Positives.** Le fichier scaffoldé est couvert par les mêmes diagnostics que le code qui l'entoure, -et les erreurs de l'émetteur remontent à l'édition plutôt qu'à l'exécution. - -**Négatives.** Les analyzers et règles de style propres au développeur se déclenchent aussi dessus, -donc un premier scaffold peut demander une passe de formatage pour rejoindre le style maison. -L'émetteur limite cela en écrivant des types explicites et une mise en page conventionnelle, mais -il ne peut pas coller à toutes les configurations. - -**Risques.** Un changement futur de l'émetteur pourrait introduire un diagnostic dans tous les -fichiers scaffoldés d'un coup. Atténué par les tests de compilation de la sortie (§12), qui -échouent sur tout diagnostic `JD`. - -#### Actions de suivi - -* Conserver le fichier de contrôle dans le test de compilation de la sortie. Sans une violation - connue dont on asserte le déclenchement, le test ne distingue pas « aucun diagnostic » de « les - analyzers n'ont jamais été chargés » et devient silencieusement inopérant — le piège dans lequel - la vérification de cette spécification est tombée au premier essai (§17.2). - -#### Références - -* §2, §5.3, §14.6, §17 de cette spécification. - ---- - -### D4 — N'émettre que des membres résolus dans la compilation cible - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -La bibliothèque publie deux assets divergents. Le moderne porte cinq points d'entrée de generator -qui n'existent pas sur celui de bas niveau, parce que les types de framework sous-jacents n'y -existent pas (§14.1). - -Les generators d'entiers non signés n'exposent ni contrainte `Positive` ni `Negative`, un type non -signé ne pouvant exprimer ni l'une ni l'autre (§14.3). - -Le tool ne détient aucune référence sur la bibliothèque (D9), donc il ne peut pas voir l'API de -celle-ci à sa propre compilation. - -La compilation du développeur fait autorité sur ce qui est réellement disponible dans son projet : -son framework cible choisit l'asset, et sa version de package choisit la surface. - -Un membre émis mais absent est une erreur de compilation dans le projet du développeur, imputée au -tool. - -#### Décision - -Le moteur n'émet un membre JustDummies qu'après avoir résolu ce membre dans la compilation du -développeur. - -#### Justification - -L'alternative est une table, à l'intérieur du tool, de ce qui existe par version de bibliothèque et -par framework cible. Elle demanderait un entretien à chaque publication de la bibliothèque, serait -fausse pour toute version antérieure au tool, et encoderait des faits que la compilation connaît -déjà exactement. - -La résolution remplace quatre cas particuliers indépendants par une règle : le clivage d'assets, la -surface numérique non signée, le tool plus ancien ou plus récent que la bibliothèque, et la -découverte des generators du développeur. Aucun n'a à être nommé où que ce soit dans l'émetteur. - -Le mode d'échec qu'elle produit est le bon. Un membre non résoluble transforme le paramètre en -paramètre non résolu (D6) — un état que le tool traite et signale déjà — plutôt qu'en une émission -que le développeur rencontre comme une erreur de compilation qu'il n'a pas causée et ne peut pas -interpréter. - -Elle rend aussi gratuite la garantie d'API publique au lieu d'en faire une contrainte à imposer : -tout ce qui est résoluble dans la compilation fait par construction partie de la surface publique -publiée, donc le tool ne peut pas émettre contre un membre interne ni hors de la baseline de -compatibilité. - -#### Alternatives considérées - -##### Une table de membres codée en dur par version de bibliothèque - -Considérée parce qu'elle est plus simple, ne demande aucune recherche de symbole, et rend la -connaissance de l'émetteur explicite et relisible. - -Écartée parce qu'elle est inmaintenable au fil des versions et tout simplement fausse pour toute -version publiée après le tool. - -##### Référencer la bibliothèque et émettre contre ses types de compilation - -Considérée parce qu'elle laisserait le compilateur vérifier l'usage que l'émetteur fait de l'API, -supprimant le mode d'échec « faute de frappe silencieuse » que D9 accepte. - -Écartée parce qu'elle contredit D9, et parce qu'elle répondrait de toute façon à la mauvaise -question : la version que le tool référence n'est pas celle du projet du développeur. - -#### Conséquences - -**Positives.** Le tool est correct contre n'importe quelle version de bibliothèque et n'importe quel -framework cible, sans détenir la moindre connaissance par version. - -**Négatives.** La dégradation est discrète par nature : un membre qui ne se résout pas n'apparaît -simplement pas dans l'émission, et sans un signalement délibéré le développeur ne peut pas -distinguer un paramètre que le tool n'a pas su inférer d'un paramètre dont le generator existe mais -n'est pas disponible ici. - -**Risques.** Un défaut de résolution — chercher un mauvais nom de métadonnée — dégraderait tout en -TODO d'un coup, ce qui se lit comme un tool qui ne marche pas plutôt que comme un bug. Atténué par -le test de sélection d'asset (§12), qui asserte le cas présent et le cas absent. - -#### Actions de suivi - -* Le §6 porte la valeur de provenance `unavailable` pour cette raison. Conserver un test qui - l'asserte : sans lui, la dégradation que cette décision accepte redevient invisible et l'exigence - se dégrade en commentaire. - -#### Références - -* §5.2, §5.3, §6, §14.1, §14.3 de cette spécification. - ---- - -### D5 + D6 — Amorcer les generators sur les gardes du constructeur, et laisser le reste en erreur de compilation - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -Les generators non contraints tirent tout leur domaine : celui des chaînes produit de zéro à seize -caractères, donc il peut retourner la chaîne vide, et celui des entiers tire tout l'intervalle, -négatifs compris (§14.5). - -Les constructeurs métier rejettent couramment une partie de ce domaine. - -Cela a été mesuré sur une vraie fabrique validante de ce dépôt : un generator de chaînes non -contraint composé dessus a levé 594 fois sur 10 000 tirages, et 557 lors d'une reprise -indépendante — environ une fois sur dix-sept, le taux que prédit un tirage non contraint sur les -longueurs de 0 à 16 (§17). - -Les clauses de garde en tête de constructeur sont l'idiome de validation dominant dans le code que -ce tool vise. - -Le tool dispose du corps du constructeur en source pour tout type de la solution du développeur, et -n'en dispose pas pour un type venant d'un package. - -Certains invariants ne sont pas exprimés comme des gardes du tout — validation déléguée à une -méthode auxiliaire, à une bibliothèque de gardes, ou règle portant sur deux paramètres. - -Le développeur lance le tool et ouvre le fichier obtenu dans la même minute. - -#### Décision - -Le moteur dérive les contraintes d'un ensemble clos de clauses de garde de constructeur reconnues, -et émet un identifiant inexistant pour tout paramètre dont il ne peut pas inférer le generator. - -#### Justification - -Sans lecture des gardes, la sortie par défaut du tool n'est pas seulement imprécise, elle est -nuisible : elle fabrique, dans la suite de tests du développeur, l'échec intermittent que la -bibliothèque existe pour éliminer. Un échec sur dix-sept est pire que pas d'outil du tout, parce qu'il -discrédite la bibliothèque à l'instant du premier usage. - -Un ensemble clos et syntaxique borne le risque. Lire des gardes n'est pas inférer une intention ; -chaque forme reconnue se projette sur exactement une contrainte, et tout ce qui est hors de -l'ensemble est ignoré. L'appariement conservateur — un paramètre, aucune composition booléenne, des -opérandes constants — sous-signale plutôt qu'il ne se trompe, ce qui est le bon biais ici : une -contrainte manquante donne une valeur que le constructeur peut rejeter et un échec visible, tandis -qu'une contrainte fausse donne une valeur qui exerce mal le test en silence. - -Pour les paramètres qui restent non résolus, une erreur de compilation est le signal le moins cher -disponible. Le développeur est dans le fichier, venant de lancer le tool ; le compilateur nomme le -paramètre dans son propre message, et ce message atteint aussi bien l'éditeur, la liste d'erreurs -que l'intégration continue. Un signal délivré plus tard coûte plus, et un signal jamais délivré -coûte le plus. - -Publier un fichier qui ne compile pas n'est défendable qu'à cause de D1. Un outil qui possède sa -sortie ne le pourrait pas ; un outil qui remet un squelette le peut, et énoncer le manque -franchement est plus honnête qu'un fichier qui compile et échoue plus tard. - -#### Alternatives considérées - -##### Des generators neutres, tout le resserrement laissé au développeur - -Considérée parce qu'elle fait que le tool n'affirme rien qu'il ne puisse prouver, ce qui est -séduisant pour une bibliothèque bâtie sur la précision. - -Écartée sur la mesure. La sortie par défaut échouerait par intermittence pour la plupart des -constructeurs validants, ce qui est le mode d'échec le plus coûteux disponible et celui que la -bibliothèque a été construite pour supprimer. - -##### Une exception à l'exécution pour les paramètres non résolus - -Considérée parce que le fichier compile alors, ce qui est plus avenant à première vue. - -Écartée parce qu'elle reporte le signal au-delà du moment où le développeur regarde le fichier, et -convertit un manque de scaffolding en un test en échec dont la cause est une ligne qu'il n'a jamais -lue. - -##### Omettre du recipe le paramètre non résolu - -Considérée parce que c'est la plus élégante des trois : le generator exigerait simplement du -développeur qu'il fournisse ce paramètre. - -Écartée parce qu'elle est silencieuse. Le generator devient partiellement utilisable sans le dire, -et le manque remonte comme un null ou un défaut au fond d'un test. - -##### Un fichier de déclaration associant des types à leur construction - -Considérée parce qu'elle permettrait au développeur d'enseigner le tool une fois pour toutes, -couvrant des invariants qu'aucune garde n'exprime, et rendrait la composition correcte pour les -value objects en général plutôt que pour les seuls gardés. - -Écartée pour la première version parce qu'elle convertit le tool en système de conventions, ce qui -contredit la règle de conception voulant que rien ne soit configuré avant le premier usage. Laissée -ouverte au §16. - -#### Conséquences - -**Positives.** Le défaut émis fonctionne pour l'idiome de validation dominant. Les paramètres non -résolus sont impossibles à manquer. - -**Négatives.** Un fichier scaffoldé peut ne pas compiler tant qu'il n'est pas édité, ce qui -surprendra quiconque attend d'un scaffolding qu'il produise du code fonctionnel. Les invariants hors -de l'ensemble reconnu donnent toujours des valeurs que le constructeur rejette. - -**Risques.** L'ensemble reconnu peut apparier une garde dont il se méprend sur le sens, produisant -une contrainte fausse plutôt qu'absente — le seul résultat pire que de ne rien inférer. Atténué par -les conditions d'appariement conservatrices et la règle de conflit sur le même axe ; le test sur le -code du dépôt (§12) est le contrôle le plus susceptible de l'attraper, parce qu'il fait tourner -l'émetteur sur du code écrit pour d'autres raisons. - -#### Actions de suivi - -* Tout ajout à l'ensemble de gardes reconnues demande un cas dans la suite du résolveur et, quand - c'est possible, une occurrence dans le test sur le code du dépôt. - -#### Références - -* §5.3, §5.5, §9, §14.5, §17 de cette spécification. - ---- - -### D7 — Tirer du contexte ambiant et ne détenir aucun état - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -La bibliothèque offre deux mécanismes de reproductibilité. Le contexte **ambiant** est épinglé par -un scope (`Any.UseSeed`, `Any.Reproducibly`) et suit le contexte d'exécution ; le contexte **isolé** -est créé par `Any.WithSeed` et porte sa propre source aléatoire fixe, insensible à tout scope. - -Chaque fabrique statique `Any.*` capture l'objet source ambiant, et cette source résout la frame -`AsyncLocal` courante **au moment du `Generate()`**, pas à la construction du generator (§14.5). - -`AnyContext` reflète les points d'entrée primitifs, motif, URI et choix comme méthodes d'instance. -Il ne reflète **pas** les points d'entrée de collection ni de composition (§14.2). - -Le type émis porte une surcharge `With{Param}(IAny)` pour chaque paramètre (D2). Il est -construit une fois et peut être générateur de plusieurs valeurs, possiblement dans des scopes -différents. - -Deux analyzers, `JD009` et `JD020`, signalent les tirages depuis un initialiseur statique et les -contextes statiques partagés. Le fichier émis est analysé comme du code écrit à la main (D3). - -#### Décision - -Le generator émis construit sa recette à partir de la seule façade statique `Any`, sans détenir de -source aléatoire, de seed ni d'état statique propre. - -#### Justification - -La résolution au moment du tirage est ce qui rend cela gratuit. Une recette construite hors d'un -scope de reproductibilité et générée dedans reste épinglée par ce scope : le type émis n'a donc -besoin d'aucune règle de cycle de vie — on le construit là où ça se lit le mieux, on le génère là où -le seed compte. Toute conception capturant une source à la construction devrait spécifier ce cycle -de vie, et dire ce qui arrive quand le generator survit au scope qui l'a vu naître. - -Ne détenir aucun état statique est ce qui laisse `JD009` et `JD020` sans rien à signaler. Le fichier -émis étant analysé, un émetteur qui mettrait quoi que ce soit en cache statique serait signalé dans -le build du développeur et non dans le nôtre — le diagnostic serait juste, et le tool serait le -fautif. - -Supporter le contexte isolé signifierait un second constructeur et un second chemin de recette à -travers `AnyContext`. Ce chemin ne pourrait pas exprimer toutes les lignes du §5.2, puisque -`AnyContext` ne reflète aucun point d'entrée de collection ni de composition : la surface serait -plus grande *et* moins capable. Le cas est déjà couvert sans rien ajouter : un développeur sur -`WithSeed` passe les generators de ce contexte paramètre par paramètre, via la surcharge que D2 -fournit déjà. - -#### Alternatives considérées - -##### Capturer un seed à la construction - -Considérée parce qu'un generator qui possède son seed est autonome et manifestement reproductible, -sans rien d'ambiant à raisonner. - -Écartée parce qu'elle duplique un mécanisme que la bibliothèque possède déjà, et parce que deux -generators de ce type dans un même test tireraient de séquences indépendantes — aucun seed unique -rapporté par un test en échec ne pourrait alors rejouer l'exécution dans son ensemble, ce qui est -précisément la propriété que la reproductibilité de la bibliothèque existe pour offrir. - -##### Un second constructeur prenant un `AnyContext` - -Considérée parce qu'elle referme le manque pour un développeur travaillant avec `Any.WithSeed`, qui -est une façon supportée d'utiliser la bibliothèque. - -Écartée pour la v1.0 parce que `AnyContext` ne reflète qu'une partie de la façade — le second chemin -ne saurait pas résoudre les paramètres collection ni composés — et parce que la surcharge par -paramètre couvre déjà le cas sans coût de surface. Laissée ouverte au §16. - -#### Conséquences - -**Positives.** Aucune règle de cycle de vie, aucun état statique. La garantie de reproductibilité du -§8.2 vient gratuitement, et les deux analyzers de seeding n'ont rien sur quoi se déclencher. - -**Négatives.** Un développeur utilisant `Any.WithSeed` ne peut pas confier le contexte entier au -generator et doit fournir les generators paramètre par paramètre, ce qui est verbeux pour un -constructeur large. - -**Risques.** Un futur émetteur qui mémoïserait quoi que ce soit — generator en cache, instance -partagée — casserait d'un coup la garantie de reproductibilité et la propreté vis-à-vis des -analyzers. Le test de compilation de la sortie attrape la seconde ; seul un test de reproductibilité -attrape la première, et c'est celui qu'on oublie. - -#### Actions de suivi - -* Conserver un test assertant qu'une recette construite **hors** d'un scope y rejoue dedans. C'est - la forme exécutable de cette décision ; le §17 consigne l'exécution manuelle qu'il doit remplacer. - -#### Références - -* §8.2, §14.2, §14.5, §16 de cette spécification ; D2 et D3 de cette section. - ---- - -### D8 — Émettre le generator dans le namespace du type cible - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -Le fichier scaffoldé est écrit dans le projet de test du développeur, mais le type qu'il génère vit -dans le projet de production. - -Un test qui utilise `Order` importe déjà le namespace d'`Order`. - -C# résout un nom de type simple dans le **namespace englobant avant toute directive `using`**, donc -un type déclaré dans un namespace l'emporte sur un type importé de même nom et de même arité. - -La bibliothèque déclare 32 noms de types publics `Any*` non génériques (§14.2) ; un generator -scaffoldé dont le nom correspond à l'un d'eux, dans un namespace où la bibliothèque est importée, le -masque. - -Le tool offre `--namespace` comme surcharge par invocation (§3), et le motif de nommage de la v1.1 -(§16) change le nom du type émis mais pas son namespace. - -Le moteur détient une `Compilation` et aucune connaissance MSBuild : il ignore le namespace racine du -projet et sa convention dossier-vers-namespace (D11). - -#### Décision - -Le generator émis est déclaré dans le namespace du type qu'il génère, sauf indication contraire de -`--namespace`. - -#### Justification - -C'est le seul choix qui ne coûte rien au site d'appel. Un test important déjà le namespace métier -écrit `new AnyOrder()` et s'arrête là ; tout autre namespace ajoute un import à chaque fichier de -test qui touche au generator. C'est une friction payée à chaque usage, et la règle de conception 2 la -tarife cher — un outil trop pénible à chaque appel ne vaut pas d'être adopté. - -C'est aussi le seul choix que le moteur peut faire avec ce qu'il détient. Le namespace qu'un IDE -inférerait — celui qu'implique le dossier de sortie — exige le namespace racine du projet et sa -convention de dossiers, c'est-à-dire exactement la connaissance MSBuild que D11 tient hors du moteur. - -Le coût est réel et assumé les yeux ouverts : **cette décision, et elle seule, crée le risque de -masquage du §7.** Un generator dans un namespace dédié ne pourrait jamais masquer un type de la -bibliothèque, parce que le `using` du développeur concourrait alors à armes égales au lieu de perdre -d'office contre une déclaration englobante. Le risque est borné — 32 noms, un contrôle conscient de -l'arité, un avertissement nommant les deux types — et rare. Échanger une collision rare et signalée -contre une friction à chaque usage est le bon sens de l'échange. - -#### Alternatives considérées - -##### Un namespace dédié aux helpers générés - -Considérée parce qu'elle supprime entièrement le risque de masquage et garde les helpers de test -visiblement à part du code métier, ce que certains codebases exigent au titre du découpage en -couches. - -Écartée parce qu'elle facture un import à chaque fichier de test, définitivement, pour éviter un -risque qui touche une poignée de noms de types et s'annonce quand il survient. `--namespace` donne -cette disposition à qui la veut, par invocation, sans l'imposer à tout le monde. - -##### Le namespace impliqué par le dossier de sortie - -Considérée parce que c'est ce que fait un IDE quand on ajoute un fichier, donc ce à quoi un -développeur s'attend. - -Écartée parce que le dériver exige le namespace racine du projet et la convention -dossier-vers-namespace. Le moteur ne les porte pas (D11), donc la CLI devrait les découvrir et les -transmettre, élargissant le contrat du §10.3 pour aboutir à un résultat moins bon que le namespace -propre du type cible. - -#### Conséquences - -**Positives.** Aucune friction au site d'appel. Le moteur n'a besoin d'aucune connaissance du projet. -La déclaration de namespace émise est copiée sur le fichier du type cible, donc le fichier scaffoldé -ressemble à ses voisins dans la forme comme dans le nom (§4.4). - -**Négatives.** Un helper de test est déclaré dans un namespace de production, ce que certains -codebases jugeront discutable au nom du découpage ; `--namespace` est la réponse, et il faut le -donner à chaque invocation. Et cette décision est la cause unique du risque du §7. - -**Risques.** Un développeur scaffoldant un type portant l'un des 32 noms non génériques de la -bibliothèque obtient un masquage silencieux s'il ignore l'avertissement. Atténué par l'avertissement -qui nomme les deux types, et par le motif de nommage de la v1.1 qui offre un renommage sans exiger -de changer de namespace. - -#### Actions de suivi - -* Le contrôle de masquage doit être conscient de l'arité (§7). Avertir sur les huit noms génériques, - qui ne peuvent pas entrer en collision, entraînerait les développeurs à ignorer le seul - avertissement qui compte. - -#### Références - -* §3, §4.4, §7, §14.2, §16 de cette spécification ; D11 de cette section. - ---- - -### D9 — Ne donner au scaffolder aucune dépendance sur le package JustDummies - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -Le tool émet du code qui appelle l'API de la bibliothèque, mais n'appelle jamais cette API -lui-même. - -Si le tool référençait la bibliothèque, le projet du développeur en détiendrait deux versions : -celle contre laquelle le tool a été construit et celle que le projet référence réellement. - -Les analyzers de la bibliothèque résolvent déjà chaque symbole de celle-ci par nom de métadonnée -contre la compilation du consommateur, sans référencer aucun assembly de la bibliothèque ; une -règle dont le type est absent de la compilation se tait simplement. - -Le dépôt hôte publie des familles de packages sur des trains de publication, chaque train publiant -ses membres à une version unique. - -#### Décision - -Ni le moteur ni la CLI ne référencent le package ou le projet JustDummies ; chaque symbole -JustDummies est résolu par nom de métadonnée contre la compilation du développeur. - -#### Justification - -La question de correction du tool n'est jamais « qu'offre la version de bibliothèque contre -laquelle j'ai été construit » mais « qu'offre la version de bibliothèque de ce projet ». Une -référence répond à la première en laissant croire qu'elle répond à la seconde, ce qui est -exactement ainsi qu'un outil se met à émettre du code qui ne compile pas chez quelqu'un d'autre. - -Conjuguée à D4, l'absence de référence rend l'écart de version structurellement impossible plutôt -que seulement testé. Il n'y a aucun couple de versions à tester, parce que le tool ne détient -aucune version de la bibliothèque. - -Les analyzers de la bibliothèque fonctionnent déjà ainsi, ce qui démontre que le motif suffit pour -exactement ce travail : des symboles résolus par nom, un silence gracieux quand un type est absent. - -Cela découple aussi les trains de publication. Le tool sort quand le tool change et la bibliothèque -quand la bibliothèque change, et aucun ne force la publication de l'autre. - -#### Alternatives considérées - -##### Référencer la bibliothèque et versionner les deux en lockstep - -Considérée parce qu'elle laisse le compilateur vérifier l'usage que l'émetteur fait de l'API, et -parce qu'un numéro de version identique est une histoire de compatibilité évidente à présenter aux -utilisateurs. - -Écartée parce que le lockstep ne garantit que la correspondance du tool avec la bibliothèque publiée -en même temps que lui, pas avec celle du projet du développeur — le seul cas qui compte — et parce -qu'elle forcerait une publication du tool à chaque publication de la bibliothèque. - -#### Conséquences - -**Positives.** Aucune matrice de versions, aucune question de compatibilité à gérer, et des cadences -de publication indépendantes. - -**Négatives.** La connaissance que l'émetteur a de l'API s'exprime en chaînes, donc un nom de membre -mal orthographié n'est pas une erreur de compilation dans le tool. Il remonte comme un membre non -résolu, que D4 transforme en TODO — une sortie fausse mais silencieuse. - -**Risques.** Ce mode d'échec silencieux est le vrai coût de cette décision. Atténué par les tests de -compilation de la sortie et le test sur le code du dépôt (§12), qui exercent les expressions émises -contre une vraie compilation, où un membre mal orthographié apparaît en TODO à une place qui aurait -dû porter une valeur. - -#### Actions de suivi - -* Le package du tool doit asserter au moment du packaging qu'il ne déclare aucune dépendance - JustDummies (§13.6) — la forme exécutable de cette décision. - -#### Références - -* §10.4, §13.6, §14.2 de cette spécification. - ---- - -### D10 — Ne jamais tirer null pour un paramètre nullable - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -La bibliothèque expose `OrNull` sous deux formes — une pour les types valeur, une pour les types -référence annotés — chacune retournant un generator qui produit `null` une partie du temps (§14.4). - -Un paramètre de constructeur déclaré `string?` ou `int?` énonce que null est *permis*. Il n'énonce -pas qu'un test particulier a l'intention d'exercer le chemin null. - -Le principe affiché de la bibliothèque est que les contraintes expriment les invariants qu'une -valeur doit satisfaire, jamais ce que le test asserte. - -Le type émis porte une surcharge `With{Param}(IAny)` pour chaque paramètre (D2), donc un -développeur peut fournir n'importe quel generator, y compris nullable, sur un paramètre choisi dans -un test choisi. - -La variance en C# ne franchit pas les types valeur, donc un paramètre nullable de type valeur exige -une conversion explicite quand le generator sous-jacent est utilisé. `OrNull` n'en exigerait -aucune, puisqu'il retourne déjà le type de generator nullable (§5.2). - -Un test qui n'échoue que sur certaines exécutions est le mode d'échec que la bibliothèque existe -pour supprimer. - -#### Décision - -L'émetteur n'applique jamais `OrNull`, de sorte qu'un paramètre nullable tire une valeur de son -type sous-jacent et que le développeur consent à null explicitement. - -#### Justification - -La nullabilité dans une signature est une permission, pas une intention. La lire comme une -intention fait décider au tool, à la place du développeur et au hasard, quelles exécutions -exercent le chemin null — si bien qu'un test écrit pour le chemin ordinaire échoue sur les -exécutions qui tirent null, pour une raison étrangère à tout ce qu'il asserte. C'est l'échec -intermittent que D5 existe pour empêcher, atteint par l'autre bout. - -Le consentement est déjà bon marché et précis. La surcharge par generator de D2 permet au -développeur de demander null au paramètre exact et dans le test exact où cela compte, c'est-à-dire -là où cette décision appartient : le test qui veut le chemin null le dit, et aucun autre test n'en -souffre. - -Refuser ici applique aussi à un défaut la règle propre à la bibliothèque sur les contraintes. -Émettre `OrNull` encoderait ce qu'un test pourrait asserter plutôt que ce que la valeur doit -satisfaire, ce qui est la distinction sur laquelle la bibliothèque est bâtie. - -#### Alternatives considérées - -##### Émettre `OrNull` pour tout paramètre nullable - -Considérée parce que c'est la lecture fidèle du type déclaré, qu'elle ne demande aucun cas -particulier, et que — pour les nullables de type valeur — elle est plus courte que la conversion que -cette décision impose. - -Écartée parce que la fidélité à la signature coûte le déterminisme : environ la moitié des valeurs -générées seraient null sans que le test l'ait choisi. L'émission plus courte achète la brièveté au -prix de la propriété que la bibliothèque vend. - -##### Émettre `OrNull` seulement là où le constructeur tolère visiblement null - -Considérée parce qu'elle réutiliserait la lecture des gardes que D5 effectue déjà, n'appliquant la -nullabilité que là où le code l'accepte démontrablement. - -Écartée parce que l'absence de garde null n'est pas une preuve d'intention — elle est tout aussi -compatible avec un oubli — et parce qu'elle ferait dépendre la stabilité d'un test de l'écriture ou -non d'une garde sans rapport. C'est pire qu'une règle uniforme, dans un sens comme dans l'autre. - -#### Conséquences - -**Positives.** Un generator scaffoldé produit la même forme de valeur à chaque exécution. Rien dans -le défaut émis ne peut rendre un test intermittent par la nullabilité. - -**Négatives.** La branche null d'un constructeur, ou du code sous test, n'est jamais exercée par un -generator scaffoldé à moins que le développeur ne le demande. Un paramètre typé `string?` pour une -raison reçoit un generator qui n'explore jamais cette raison. - -Visiblement négatif aussi : pour un nullable de type valeur l'émetteur doit convertir explicitement, -donc le §5.2 porte un saut qui se lit comme gratuit tant que cette décision n'est pas connue. - -**Risques.** Ce saut est la partie de l'émetteur la plus susceptible d'être « simplifiée » en -défaut — `OrNull` est plus court, retourne exactement le type voulu, et ressemble au nettoyage -évident. Le réintroduire restaurerait l'instabilité en silence. Atténué par cet enregistrement et -par le cas de résolveur nommé ci-dessous. - -#### Actions de suivi - -* Conserver un cas de résolveur pour un paramètre nullable de type valeur assertant la conversion - explicite, et nommer cet enregistrement là où l'émetteur l'effectue, pour que le saut ne soit pas - simplifié. - -#### Références - -* §5.2, §14.4 de cette spécification ; D2 et D5 de cette section. - ---- - -### D11 — Garder le moteur de scaffolding chargeable par un hôte Roslyn - -**Statut :** Proposé -**Proposé :** 2026-07-30 -**Décideurs :** Reefact - -#### Contexte - -La CLI doit ouvrir un projet sur disque, ce qui exige un workspace conscient de MSBuild ; celui-ci -n'est disponible que sur .NET moderne, pas sur la cible de bas niveau. - -Un assembly chargé par le compilateur d'un consommateur — analyzer, code fix, code refactoring — -doit cibler le framework de bas niveau et être compilé contre la version minimale de Roslyn sous -laquelle il doit se charger. Construit contre une plus récente, il échoue à se charger, et il échoue -silencieusement. - -Un code refactoring Roslyn est une seconde surface plausible pour le moteur : la bibliothèque publie -déjà des analyzers, donc le chemin de packaging et de chargement existe, et appliquer un document -est l'opération naturelle d'un refactoring. - -Le travail du moteur est de l'inspection de symboles, de la lecture de syntaxe et de la construction -de chaînes. Il n'a besoin ni de système de fichiers, ni de console, ni de MSBuild. - -La surface de tests décrite au §12 est dominée par le comportement du moteur plutôt que par la -plomberie de commandes. - -Le dépôt hôte mesure la mutation sur tout projet dont le code est publié ou s'exécute (§13.5). - -#### Décision - -Le moteur de scaffolding est une bibliothèque séparée ciblant le framework de bas niveau et compilée -contre le plancher Roslyn de l'analyzer, ne faisant aucune entrée-sortie, la CLI étant une coquille -par-dessus. - -#### Justification - -La contrainte est asymétrique dans le temps. Cibler le plancher ne coûte presque rien au moteur -aujourd'hui, parce qu'aucune partie de son travail n'a besoin d'une API moderne. Découvrir plus tard -qu'il doit être chargeable par un compilateur signifie re-vérifier chaque API qu'il utilise contre ce -plancher, dans un code écrit sans cette contrainte à l'esprit. Payer maintenant est bon marché, -payer plus tard ne l'est pas, et c'est ce qui justifie de construire pour un consommateur qui -n'existe pas encore. - -La frontière qu'exige le consommateur futur est celle-là même que veut le code présent. Un moteur -qui prend une compilation et retourne un modèle, sans sortie propre, est la forme testable : le -résolveur et l'émetteur s'exercent sur une compilation en mémoire, sans projet sur disque ni analyse -d'arguments dans le chemin. - -Les séparer sépare aussi le budget de mutation. La plomberie de commandes et les règles de -résolution ne méritent pas la même attention, et un projet unique ne peut pas exprimer cette -différence. - -L'argument selon lequel la CLI pourrait gagner d'autres verbes ne justifie rien de tout cela. Des -verbes en plus sont des fichiers en plus au-dessus du même moteur, et après D1 la liste plausible -est de toute façon quasi vide. - -#### Alternatives considérées - -##### Un projet CLI unique contenant tout - -Considéré parce que c'est la plus petite chose qui fonctionne pour un outil à un seul verbe, et que -cela évite deux projets et deux suites de tests. - -Écarté parce qu'il ferme la voie de l'hôte Roslyn à l'instant de sa création, et parce qu'il force -chaque test du moteur à passer par les dépendances de la CLI. - -##### Un moteur séparé ciblant .NET moderne - -Considéré parce qu'il garde la frontière, et avec elle les bénéfices de test et de mutation, sans -accepter la contrainte de bas niveau. - -Écarté parce que la raison d'être principale de la frontière est le consommateur que cette variante -exclut. - -#### Conséquences - -**Positives.** Le moteur est chargeable tel quel par un hôte compilateur. Ses tests n'ont besoin -d'aucun projet sur disque. La mesure de mutation peut être visée là où elle paie. - -**Négatives.** Deux projets et deux suites de tests pour un verbe. Le moteur est écrit contre le -framework de bas niveau, donc les API de confort modernes lui sont indisponibles. - -**Risques.** L'épinglage au plancher Roslyn peut dériver si la référence de package du moteur est -laissée flottante, et l'échec de chargement qui en résulte est silencieux. Atténué par un épinglage -sur la même propriété de plancher que celle du package d'analyzers (§13.2). - -#### Actions de suivi - -* Si un code refactoring est un jour construit, le moteur devra être publié comme package propre - (§16). - -#### Références - -* §10, §12, §13.2, §13.5, §16 de cette spécification. - ---- - -### Un suivi côté bibliothèque, pas un enregistrement de décision - -`Any.Fixed(value)` — un `IAny` retournant une constante — permettrait à l'émetteur -d'abandonner le helper imbriqué `FixedValue` du §4.2. `Any.OneOf(value)` remplit presque le -rôle mais refuse `null` et consomme un tirage (§14.5). C'est un ajout à l'API publique de la -bibliothèque plutôt qu'une décision sur le tool, donc cela relève de la base de décision de la -bibliothèque et n'est **pas** requis pour la v1.0. - ---- - -## 16. Réservé pour la v1.1+ - -La v1.0 ne doit pas s'interdire ces évolutions ; le §11.3 est la contrainte qui garde la première -bon marché. - -**Nommage.** `AnyOrder` → `OrderFactory`, ou tout autre motif. Forme : - -```console -dum generate Order --name OrderFactory # ce type uniquement -dum generate Order --pattern "{Type}Factory" # cette exécution -``` - -plus un `dum.json` optionnel à la racine du projet pour un défaut à l'échelle du projet : - -```json -{ "naming": { "pattern": "Any{Type}" } } -``` - -`{Type}` est le seul emplacement. Le motif par défaut reste `Any{Type}`, donc un projet existant ne -voit aucun changement. C'est aussi la réponse à l'avertissement de masquage du §7. - -**Lire les gardes regex.** Laissé hors du §5.3 pour la v1.0 parce que la bibliothèque ne génère -qu'à partir du sous-ensemble régulier du langage des motifs, et qu'un motif non supporté lève à la -construction — ce qui rendrait le type émis entièrement inutilisable. Y revenir suppose la question -du sous-ensemble tranchée d'abord : soit le moteur valide un motif sans référencer la bibliothèque -(ce que D9 interdit aujourd'hui), soit la bibliothèque offre un moyen de le lui demander. - -**Autres éléments reportés.** `--all` ; les membres `init` / `required` et la construction par -propriétés ; le support d'`AnyContext` (D7) ; un sélecteur `--ctor` quand plusieurs constructeurs se -disputent ; l'extension du §5.3 à une bibliothèque auxiliaire de type `Guard.Against` ; la -publication de `JustDummies.GenAny` comme package propre une fois qu'un consommateur IDE existe ; le -code refactoring IDE lui-même. - -Délibérément **non** reportés — abandonnés : un verbe `check`, un mode source generator, et toute -forme de régénération ou de détection de dérive. D1 supprime le problème qu'ils résoudraient. - ---- - -## 17. Vérifications - -### 17.1 Ce qui a été contrôlé - -Le fichier émis du §4.1 a été écrit à la main exactement comme spécifié — paramètre `int?`, helper -`FixedValue` et `AnyCustomer` composé compris — puis compilé et exécuté contre le -`JustDummies.dll` construit depuis la source (asset `net8.0`), avec les analyzers JustDummies -branchés. Les résultats ci-dessous sont ce que le harnais a affiché. - -| Affirmation | Où | Résultat | -|---|---|---| -| Le squelette spécifié compile tel quel | §4.1 | compile, 0 avertissement | -| Le chaînage `.WithX` fonctionne et ne perturbe pas une base partagée | D2, §4.2 | deux `.WithStatus` sur une même base restent indépendants | -| `AnyOrder` est accepté par les seams de composition de la bibliothèque | D2, §15 | `Any.ListOf`, `Any.PairOf` et `.As` l'acceptent tous | -| `.WithX(IAny)` maintient la composition contrainte ouverte | §4.2 | `.WithReference(Any.String().StartingWith("ORD-").As(...))` donne `ORD-x9vDEd2` | -| Une recette construite **hors** d'un scope rejoue dedans | §8.2, §14.5 | deux exécutions `Any.Reproducibly(20260730, …)` ont produit des valeurs identiques | -| La chaîne dérivée des gardes ne lève jamais | §5.3 | 500 tirages à travers `OrderReference.Create`, aucune `AnyGenerationException` | -| La chaîne **sans** lecture des gardes lève par intermittence | §5.3 | **594 / 10 000** tirages ont levé, et **557 / 10 000** à la reprise contre une bibliothèque plus récente — environ 1 sur 17, conforme aux 588 que prédisent dix-sept longueurs équiprobables | -| La covariance des collections ne demande aucun adaptateur | §5.2, §14.5 | `Any.ListOf(...)` affecté à `IAny>` | -| Un nullable de type valeur **exige** bien le saut `.As` | §5.2 | `IAny` n'est pas un `IAny` ; `.As(value => (int?)value)` compile | -| Les bornes complémentaires se composent | §5.3 | `.GreaterThanOrEqualTo(0).LessThanOrEqualTo(100)` et `.NonEmpty().WithMaxLength(10)` tirent tous deux | -| Les bornes contradictoires sont rejetées deux fois | §5.3 | `ConflictingAnyConstraintException` à l'exécution, et `JD023` à la **compilation** | -| Un generator de motif n'admet aucune autre contrainte de chaîne | §5.3 | `Any.StringMatching(...).NonEmpty()` ne compile pas — `CS1061`, `AnyPattern` n'a que `DifferentFrom`/`Except` | -| Les regex de validation réalistes sortent du sous-ensemble supporté | §5.3 | 4 sur 5 rejetées : lookahead, limite de mot, backreference, catégorie Unicode | -| Un motif non supporté lève à la **construction**, pas au `Generate()` | §5.3 | donc le constructeur sans paramètre émis lèverait avant qu'un `With…` puisse surcharger | -| Les generators de collection ne portent aucune contrainte de longueur | §5.3 | `AnyList` expose `WithCount`, `WithCountBetween`, `WithMinCount`, `WithMaxCount` — pas de `WithLength` | -| **Chaque ligne du §5.2 compile** | §5.2 | 40 déclarations, chacune affectant l'expression émise à l'`IAny` du paramètre — 0 erreur, 0 warning, nullable activé, warnings-as-errors | -| **Chaque ligne du §5.2 tient sa promesse** | §5.2 | 3 000 tirages par ligne scalaire : `NonEmpty` jamais vide, `Guid` jamais `Empty`, `Enum` uniquement des membres déclarés, `Uri().Web()` absolue http(s) | -| **Chaque mapping de garde du §5.3 est solide** | §5.3 | 17 mappings × 4 000 tirages : toute valeur tirée est une valeur que la garde d'origine accepterait | -| **Chaque fait du §14 redérivé contre une bibliothèque plus récente** | §14 | 29 commits amont plus tard — exceptions retravaillées, parser regex refactoré — les décomptes, l'inventaire des analyzers et le sous-ensemble regex tiennent toujours | -| Les formes record, fabrique statique et noms atypiques fonctionnent | §4.2, §5.1 | record positionnel, type à constructeur privé plus `Create`, et paramètres `_id` / `@class` compilent et génèrent | -| Un constructeur sans paramètre casse la forme standard | §4.2 | émettre les deux constructeurs leur donne une seule signature — `CS0111` | -| Un nom de bibliothèque générique ne peut pas être masqué | §7 | un `AnySet` scaffoldé et `JustDummies.AnySet` coexistent ; l'arité fait partie de l'identité | -| Un nom non générique, si | §7 | `AnyPattern` dans le namespace de la cible résout vers le type scaffoldé, pas celui de la bibliothèque | -| Les paramètres `ref` / `out` cassent le site d'appel | §5.1 | `CS1620` ; `in` accepte un argument par valeur sans broncher | -| `FixedValue` accepte ce que `Any.OneOf` refuse | §4.2 | `FixedValue(null)` rend null ; `Any.OneOf(null)` lève `ArgumentException` | -| `.Positive()` est incorrect pour une garde `p < 1` sur un decimal | §5.3 | 1 tirage sur 5 000 est passé sous 1 sans contrainte ; ~1 sur 5 dès qu'une autre borne resserre | -| La sortie scaffoldée ne lève aucun diagnostic JD | D3, §12 | 0 diagnostic sur les fichiers émis | -| Les analyzers étaient réellement chargés | D3 | un fichier de contrôle a levé `JD006` et `JD005` dans le même build | -| `` les éteint | D3, §15 | le même fichier de contrôle, ainsi marqué, en a levé **0** — l'erreur `JD005` comprise | - -### 17.2 Comment le rejouer - -Rien du harnais n'est exotique ; il vaut la peine d'être recréé chaque fois que la bibliothèque -déménage ou change de version. - -1. Construire la bibliothèque et les analyzers en `Release` (branche `net8.0` pour la bibliothèque). -2. Créer un projet console `net8.0` jetable **hors** du dépôt, pour qu'aucune propriété de build à - l'échelle du dépôt ne s'applique. Référencer le `JustDummies.dll` construit par un - `` / ``, et l'analyzer construit par - ``. -3. Ajouter le domaine du §4.1 (`Order`, `OrderReference` avec son `Create` gardé, `Customer`, - `OrderStatus`) et les `AnyOrder.cs` / `AnyCustomer.cs` scaffoldés exactement comme le §4.1 les - spécifie. -4. Ajouter un **fichier de contrôle** avec deux violations connues — une contrainte dont le résultat - est jeté (`Any.String().NonEmpty();` comme instruction, `JD006`) et un generator dans une chaîne - interpolée (`$"{Any.Int32()}"`, `JD005`). Compiler, et confirmer que **les deux se déclenchent**. - Sans cette étape, « aucun diagnostic sur le fichier scaffoldé » ne se distingue pas de - « l'analyzer n'a jamais été chargé » — piège dans lequel cette vérification est tombée au premier - essai. -5. Préfixer ce même fichier de contrôle par `// ` et recompiler : les deux - diagnostics disparaissent et le build réussit. C'est la preuve de D3. -6. Lancer les assertions du §17.1. Pour la mesure, boucler - `Any.String().As(OrderReference.Create).Generate()` 10 000 fois en comptant les - `AnyGenerationException`. - -Note d'exécution : si seul un runtime .NET plus récent est installé, la sortie `net8.0` s'exécute -quand même sous `DOTNET_ROLL_FORWARD=LatestMajor`. diff --git a/doc/handwritten/for-maintainers/specifications/justdummies-tool.md b/doc/handwritten/for-maintainers/specifications/justdummies-tool.md deleted file mode 100644 index 71b1e95a..00000000 --- a/doc/handwritten/for-maintainers/specifications/justdummies-tool.md +++ /dev/null @@ -1,2169 +0,0 @@ -# JustDummies tool (`dum`) — specification v1.0 - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](justdummies-tool.fr.md) - -**Status:** specification, ready to implement. Nothing is built yet. -**Supersedes:** the working pre-specification 0.1 (never committed) - ---- - -## 0. How to read this document - -This specification is **self-contained on purpose**. JustDummies is expected to move to its own -repository before the tool is built, so nothing here may depend on being read inside -`Reefact/first-class-errors`. - -* **§1–§9 are the product.** What the tool does, what it emits, and why. Read §2 first: eleven - decisions carry everything else. §5 is the hard part and the only section with real design risk. -* **§10–§12 are the build.** Two projects, the contract between them, and the test plan. -* **§13 is the portability contract.** Everything the tool needs *from its host repository*, - stated as requirements rather than paths. If JustDummies has moved, start here. -* **§14 is the reference.** Every fact about the JustDummies library that this specification - relies on, inlined, with the command to re-derive each one. Nothing in §1–§12 requires reading - the library's source to be checked. -* **§15 is the reasoning.** Ten decision records in this repository's ADR format, held inside - the specification because the repository that should hold them does not exist yet. Read them - when you want to know *why*, or when you are tempted to reverse something in §2. -* **§16 is the boundary of v1.0.** What is deferred, and what was dropped outright. -* **§17 is the evidence.** The emitted skeleton of §4.1 was compiled and run against the real - library, and the two contested claims were measured. §17.2 says how to re-run all of it. - -Everything in this document is **decided** unless it appears in §16 (deferred) or is explicitly -marked open. There are no open questions blocking implementation. - ---- - -## 1. What `dum` is - -`dum` is a **scaffolder**, not a code generator. - -Given a type from the developer's own code, it writes **one C# file, once**, containing a named, -composable generator for that type. From the moment the file is written it belongs to the -developer: they read it, edit it, commit it, and never run the tool on it again. - -```console -$ cd Shop.Tests -$ dum generate Order -✓ AnyOrder.cs -``` - -```csharp -Order order = new AnyOrder() - .WithStatus(OrderStatus.Pending) - .Generate(); -``` - -The distinction from a *generator* is the whole product position and it settles most of the -design at once: - -* there is no drift, because there is nothing to keep in sync — the file is the developer's, not - the tool's; -* there is therefore **no `check` verb, no source generator, no regeneration story**; -* the tool is allowed to leave the file **unfinished**, because finishing it is the developer's - half of the deal. - -The value proposition stays distinct from the library's: the **library** makes values valid; the -**tool** makes the test concise. - -### 1.1 Design rules this specification answers to - -1. **Extremely simple to use.** The nominal invocation is one verb and one type name, from the - directory the file will land in, with no configuration file and no options. -2. **Cheap at both ends.** Nothing to configure before the first use; nothing to configure per - use. -3. **Generate as much as can be generated, and no more.** Where the tool cannot know, it says so - in the file and in the console, and hands the skeleton back. -4. **Naming is fixed in v1.0.** `Order` becomes `AnyOrder`, full stop. Renaming - (`OrderFactory`, a custom prefix) is v1.1+ and §16 reserves its shape so v1.0 does not block - it. - ---- - -## 2. Decisions - -These are the load-bearing decisions. All eleven are covered by the ten decision records in §15 — -context, argument, alternatives rejected, consequences; D5 and D6 share one. This table is the -index; it holds no argument of its own. - -| # | Decision | Why, in one line | -|---|---|---| -| **D1** | Scaffold once; the file belongs to the developer. | Kills drift, `check`, and the source-generator question in one move. | -| **D2** | The emitted type implements `IAny` and is **immutable**. | Composability, and it re-arms the `JustDummies.Usage` analyzers on the emitted type. | -| **D3** | The emitted file is **not** marked as generated code. | All 27 analyzers exempt generated code; marking it would blind the file. | -| **D4** | Never emit a member not resolved in the target compilation. | One rule covers the TFM split, the public-API baseline, version skew and unsigned arithmetic. | -| **D5** | Read constructor guard clauses to seed each generator. | Without it the emitted code produces values the constructor rejects. | -| **D6** | An unresolved parameter is emitted as a **compile error**. | The developer is already in the file; a red squiggle is the cheapest possible signal. | -| **D7** | The emitted generator draws from the **ambient** context and holds no state. | Draw-time resolution makes the §8.2 guarantee free; captured state would need a lifecycle rule. | -| **D8** | The emitted type lives in the **target type's namespace**. | Zero friction at the call site — and the sole cause of the §7 shadowing hazard. | -| **D9** | The tool takes **no dependency on the JustDummies package**. | Resolution by metadata name, exactly like the analyzers — version skew becomes structurally impossible. | -| **D10** | Never emit `.OrNull()`. | A dummy that is randomly `null` is the flakiness the library exists to remove. | -| **D11** | The scaffolding **engine is a separate library** at the Roslyn floor; the CLI is a shell. | The engine's plausible second consumer is an IDE refactoring, which is not a CLI and cannot load a `net8.0` assembly. | - ---- - -## 3. Command surface - -The tool ships as a .NET tool whose command is **`dum`**. - -```console -dotnet tool install --global JustDummies.Cli -dum generate [...] [options] -``` - -`generate` is the only verb in v1.0. - -| Option | Default | Meaning | -|---|---|---| -| `--project ` | the single `*.csproj` in the current directory | Project whose compilation is analyzed. | -| `--output ` | the current directory | Where the file is written. | -| `--namespace ` | the target type's namespace (D8) | Namespace of the emitted type. | -| `--force` | off | Overwrite an existing file. | -| `--dry-run` | off | Print the file to stdout; write nothing. | - -That is the entire surface. There is no config file, no `init`, no `list`, no `--all`, and — by -D1 — no `check`. §16 lists what is deliberately deferred. - -### 3.1 Where the tool is run - -From the **test project**, because that is where the file belongs. The test project references -the production project, so `Order` is reachable from its compilation, and `--output`'s default -puts `AnyOrder.cs` next to the tests that use it. - -`--project` resolution: if exactly one `*.csproj` sits in the current directory, use it; if none -or several, fail with a message naming the candidates and pointing at `--project`. - -### 3.2 Resolving the target type - -`Order` is matched, in order: - -1. by full metadata name, if the argument contains a `.` (`Shop.Domain.Order`); -2. by simple name across the compilation's source types and referenced assemblies. - -A **nested** type is written the way a developer would type it — `dum generate Order.Line` — and -the engine translates it for the lookup, where the separator is `+` rather than `.` -(`Shop.Domain.Order+Line`). Passing the dotted form straight to a metadata-name lookup returns -nothing, which would report a real type as missing. The generator it emits is a top-level type in -the containing namespace, named after the nested type alone: `AnyLine`. - -Zero matches → error, listing the closest names by edit distance. More than one match → error, -listing the full names, asking for one of them. Both exit `1`. - ---- - -## 4. The emitted file - -### 4.1 Worked example - -This example is not a sketch: it was compiled and run against the real library (§17). - -Source under analysis: - -```csharp -namespace Shop.Domain; - -public sealed class Order { - - public Order(OrderReference reference, Customer customer, int quantity, - OrderStatus status, IReadOnlyList tags, DateTime placedAt) { - if (reference is null) { throw new ArgumentNullException(nameof(reference)); } - if (quantity <= 0) { throw new ArgumentOutOfRangeException(nameof(quantity)); } - ... - } - -} - -public sealed class OrderReference { - - public static OrderReference Create(string value) { - if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException(...); } - ... - } - -} -``` - -`dum generate Order`, with `AnyCustomer` already scaffolded in the project, emits: - -```csharp -// Scaffolded by dum (JustDummies). This file is yours: read it, edit it, commit it. -// `dum generate Order --force` overwrites it. This type is partial, so members you add in a -// neighbouring file survive. - -using System; -using System.Collections.Generic; - -using JustDummies; - -namespace Shop.Domain; - -/// -/// A generator of arbitrary values. It draws from the ambient random -/// context, so a reproducibility scope pins it; to draw from an isolated -/// Any.WithSeed(...) context, pass that context's generators through the -/// With… overloads. -/// -public sealed partial class AnyOrder : IAny { - - private readonly IAny _reference; - private readonly IAny _customer; - private readonly IAny _quantity; - private readonly IAny _status; - private readonly IAny> _tags; - private readonly IAny _placedAt; - - /// Creates the generator with a default recipe for every constructor parameter. - public AnyOrder() - : this(reference: Any.String().NonEmpty().As(OrderReference.Create), - customer: new AnyCustomer(), - quantity: Any.Int32().Positive(), - status: Any.Enum(), - tags: Any.ListOf(Any.String().NonEmpty()), - placedAt: Any.DateTime()) { } - - private AnyOrder(IAny reference, - IAny customer, - IAny quantity, - IAny status, - IAny> tags, - IAny placedAt) { - _reference = reference; - _customer = customer; - _quantity = quantity; - _status = status; - _tags = tags; - _placedAt = placedAt; - } - - /// Pins reference to a fixed value. - public AnyOrder WithReference(OrderReference value) { - return WithReference(new FixedValue(value)); - } - - /// Draws reference from . - public AnyOrder WithReference(IAny generator) { - return new AnyOrder(generator, _customer, _quantity, _status, _tags, _placedAt); - } - - // ... one such pair per parameter ... - - /// Produces one arbitrary . - public Order Generate() { - return new Order(_reference.Generate(), - _customer.Generate(), - _quantity.Generate(), - _status.Generate(), - _tags.Generate(), - _placedAt.Generate()); - } - - private sealed class FixedValue : IAny { - - private readonly TValue _value; - - public FixedValue(TValue value) { - _value = value; - } - - public TValue Generate() { - return _value; - } - - } - -} -``` - -### 4.2 Shape rules - -* `public sealed partial class Any{Type} : IAny<{Type}>`. `partial` so the developer's own - members live in a neighbouring file and survive a `--force`. -* One `private readonly IAny _param;` per constructor parameter, in declaration order. -* A **public parameterless constructor** carrying the inferred recipe, written with named - arguments so the reader maps each expression to its parameter without counting. -* A **private all-arguments constructor** performing the copy. -* Per parameter, **two** `With{Param}` overloads returning a new instance: - `With{Param}(TParam value)` and `With{Param}(IAny generator)`. - The value overload is the ergonomic one; the generator overload is what keeps composition - possible and is why passing `Any.String().StartingWith("ORD-")` does not become a `JD011`/`JD012` - mistake. -* `public {Type} Generate()` calling the constructor with each field's `Generate()`. -* The private nested `FixedValue` helper. Rationale: it accepts `null` (which - `Any.OneOf(value)` rejects) and consumes no draw from the ambient source, so pinning a - parameter does not shift the values drawn for the others (§14.5). It is nested and private, so - any number of scaffolded files coexist. *(If `Any.Fixed(value)` is ever added to the - library, the helper can be dropped — see §15.)* -* `With{Param}` casing: the parameter name, first letter upper-cased, invariant culture. A - parameter named `_id` or `@class` is normalised by stripping the leading `_`/`@`. - -**The degenerate case has its own shape.** A constructor with no parameters (§5.1) collapses all of -the above: one public parameterless constructor, no fields, no private constructor, no `With` -methods, no `FixedValue` helper, and `Generate()` returning `new {Type}()`. Emitting the two -constructors unconditionally would give them the same signature and fail with `CS0111` — verified. -The result is still worth generating: `Any{Type}` is an `IAny`, so it composes into -`Any.ListOf(...)`, `Any.Combine(...)` and the rest, which a bare `new {Type}()` does not. - -### 4.3 Header rules - -Exactly three comment lines, as above. **No timestamp and no tool version**: both would make the -byte content depend on something other than the analyzed type, so every scaffold after a tool -upgrade would produce a spurious diff. Determinism is a hard requirement (§8.1). - -### 4.4 Language level - -The emitted code uses no construct newer than **C# 7.3**: no `var` (it reads better in a -skeleton), no target-typed `new`, no records, no switch expressions, no file-scoped namespace -unless the target type's own file already uses one. The file lands in the developer's project and -must compile at that project's `LangVersion`. - -The one exception is the namespace form, which is copied from the target type's declaration -style so the emitted file looks like its neighbours. - ---- - -## 5. Resolution — how a parameter becomes a generator - -For each parameter, the engine produces an expression of type `IAny`, or fails to and -marks the parameter unresolved. - -### 5.1 Choosing the constructor - -1. Public instance constructors, most parameters first; ties broken by source order. The chosen - signature is always printed (§6). -2. If the type has **no** accessible constructor but exposes a recognised static factory (§5.4) - returning itself, that factory is used instead and `Generate()` calls it. -3. A parameterless constructor yields a valid, trivial `AnyOrder` with no `With` methods. -4. Positional records work with no special handling — their primary constructor is an ordinary - public constructor. `init` and `required` members are **out of scope** (§16). -5. A constructor with a `ref` or `out` parameter is **not eligible**: `Generate()` passes plain - value arguments, and such a call site fails with `CS1620` — verified. Skip it and consider the - next candidate; if none remains, the type is unresolved (§7). `in` is fine, a value argument - binds to it. - -### 5.2 The base table - -Every entry is subject to D4: the member is emitted only if it resolves in the compilation. - -| Parameter type | Emitted | -|---|---| -| `string` | `Any.String().NonEmpty()` | -| `bool` | `Any.Boolean()` | -| `sbyte` `byte` `short` `ushort` `int` `uint` `long` `ulong` | `Any.SByte()` … `Any.UInt64()` | -| `float` `double` `decimal` | `Any.Single()` / `Any.Double()` / `Any.Decimal()` | -| `char` | `Any.Char()` | -| `Guid` | `Any.Guid().NonEmpty()` | -| `DateTime` `DateTimeOffset` `TimeSpan` | `Any.DateTime()` / `Any.DateTimeOffset()` / `Any.TimeSpan()` | -| `DateOnly` `TimeOnly` `Int128` `UInt128` `Half` | the matching factory — **`net8.0` asset only**, D4 decides | -| any `enum E` | `Any.Enum()` | -| `Uri` | `Any.Uri().Web()` | -| `T[]` | `Any.ArrayOf()` | -| `List` `IReadOnlyList` `IList` `ICollection` `IReadOnlyCollection` | `Any.ListOf()` | -| `IEnumerable` | `Any.SequenceOf()` | -| `HashSet` `ISet` | `Any.SetOf()` | -| `Dictionary` `IDictionary` `IReadOnlyDictionary` | `Any.DictionaryOf(, )` | -| `T?` where `T` is a reference type | the generator for `T` unchanged — **never** `.OrNull()` (D10) | -| `T?` where `T` is a value type | `.As(value => (T?)value)` — **never** `.OrNull()` (D10) | -| a type with a scaffolded `AnyT` in the compilation | `new AnyT()` (§5.4) | -| a type with a recognised one-parameter static factory | `.As(T.Create)` (§5.4) | -| anything else | unresolved (§5.5) | - -Three notes on the table. - -**`Any.String().NonEmpty()`, not `Any.String()`.** Unconstrained, `Any.String()` yields *0 to 16* -ASCII letters and digits (§14.5) — it can return the empty string. A constructor parameter of type -`string` in a domain type is overwhelmingly required non-empty, and a default that fails roughly -one call in seventeen (measured: §17) is exactly the flakiness the library exists to remove. Same -reasoning for `Any.Guid().NonEmpty()`. - -**Collections rely on covariance — and value types do not.** `IAny` is covariant, so -`Any.ListOf(...)`, whose type is `IAny>`, is directly assignable to a field of type -`IAny>`; no adapter is needed for any of the interface rows, and the same holds -for `HashSet`/`ISet` and `Dictionary`/`IReadOnlyDictionary`. - -Variance in C# applies only across **reference** conversions, which is why the two nullable rows -differ. `IAny` is an `IAny` and needs nothing; `IAny` is **not** an -`IAny`, so an `int?` parameter needs the explicit `.As(value => (int?)value)` hop. Getting -this wrong is the most likely way an implementer produces a table that does not compile — the -`net8.0`-only rows are all value types too. - -**Element generators recurse.** `IReadOnlyList` resolves its element through this same -table, so it becomes `Any.ListOf(new AnyOrderLine())` when `AnyOrderLine` exists. Recursion is -depth-limited to 3 and cycle-guarded; exceeding either makes the parameter unresolved. - -### 5.3 Guard clauses - -This is the feature that makes the tool worth building rather than templating. - -When the constructor's (or factory's) **body is available as source** — which it is for any type -in the developer's solution, and is not for a type coming from a NuGet package — the engine reads -its leading guard clauses and tightens the generator accordingly. - -A statement is a guard only when **all** of the following hold. The rule is deliberately -conservative, mirroring how the library's own analyzers under-report rather than misfire: - -* it is an `if` statement whose body throws unconditionally, with no `else`; -* it appears before the first assignment to a field or property; -* its condition mentions **exactly one** parameter and contains no `&&` or `||`; -* every other operand is a compile-time constant. - -The recognised set is closed: - -| Condition that throws | Constraint added | -|---|---| -| `p is null`, `p == null` | none — the generator never returns `null` anyway | -| `string.IsNullOrEmpty(p)`, `string.IsNullOrWhiteSpace(p)`, `p.Length == 0`, `p.Length < 1` | `.NonEmpty()` | -| `p.Length > N` | `.WithMaxLength(N)` | -| `p.Length < N` | `.WithMinLength(N)` | -| `p.Length != N` | `.WithLength(N)` | -| `p <= 0`; or `p < 1` on an **integral** type | `.Positive()` | -| `p < 0` | `.GreaterThanOrEqualTo(0)` | -| `p >= 0` | `.Negative()` | -| `p == 0` | `.NonZero()` | -| `p > N` | `.LessThanOrEqualTo(N)` | -| `p < N` | `.GreaterThanOrEqualTo(N)` | -| `p == Guid.Empty` | `.NonEmpty()` | -| `!Enum.IsDefined(typeof(E), p)` | none — `Any.Enum()` already draws only declared members | - -`.NonEmpty()` covers `IsNullOrWhiteSpace` as well as `IsNullOrEmpty`, because an unconstrained -`Any.String()` draws only ASCII letters and digits, so a non-empty draw can never be whitespace -(§14.5). - -**A size guard on a collection parameter maps to the count family, not the length family.** A -collection generator exposes `NonEmpty`, `WithCount`, `WithMinCount` and `WithMaxCount`, and no -`WithLength` at all (§14.3). So `p.Length > N` on a `T[]`, or `p.Count > N` on a `List`, becomes -`.WithMaxCount(N)`; `p.Count != N` becomes `.WithCount(N)`. Reading such a guard against the string -family instead would emit a member that does not resolve, and D4 would drop it **silently** — a -real constraint lost without a trace. `.NonEmpty()` is the one member spelled the same for both. - -Recognised constraints **compose when they bound different things, and are dropped when they -collide**. Two guards setting a lower and an upper bound are complementary — `.NonEmpty()` with -`.WithMaxLength(10)`, or `.GreaterThanOrEqualTo(0)` with `.LessThanOrEqualTo(100)` — and both are -kept. That is the ordinary bounded-range idiom, written as two consecutive guards; discarding it -would make guard reading useless for the case it most often meets. Both compositions were verified -against the library (§17). - -Two guards setting *the same* bound are irreconcilable: both are dropped and the parameter is -reported as `guards not combined`. So is a lower bound above an upper one — the library rejects -that chain with `ConflictingAnyConstraintException`, and `JD023` reports it at compile time (§17), -but the engine must not emit it in the first place. No recognised guard produces a charset or a -pattern constraint, so those axes never arise. - -**Regex guards are deliberately not read.** `!Regex.IsMatch(p, "…")` looks like the ideal guard to -translate: the library has `Any.StringMatching(...)`, and the pattern sits right there as a -literal. It is out of the set for v1.0, for a reason that generalises. - -The library builds values from the *regular* subset of the pattern language — lookarounds, -backreferences, word boundaries and Unicode categories are outside it, and a pattern using any of -them raises `UnsupportedRegexException`. Four of five realistic validation patterns tried against -it were rejected (§17); lookaheads and word boundaries are the ordinary vocabulary of a -hand-written validator. - -Worse, the rejection happens at **construction**, not at `Generate()`. The emitted parameterless -constructor runs the whole recipe in its initialiser, so `new AnyOrder()` would throw before any -`.WithReference(...)` could override it. The generated type would be unusable rather than merely -imprecise, and no call the developer could write would rescue it — verified (§17). - -And the engine cannot tell in advance. D9 keeps it from referencing the library, so it cannot ask -the library's own parser whether a pattern is supported, and re-implementing that check would -duplicate a parser it cannot see and drift from it. - -That yields a rule worth stating on its own, because the pattern row is the only thing that ever -broke it: **the engine never emits an expression whose validity depends on a value it cannot -check.** Every other row emits a member D4 resolves, with an argument that is a compile-time -constant of the right type. Reading regex guards is a v1.1 candidate (§16) and needs the subset -question answered first. - -Where two rows both match a condition, the **more specific wins**. `p < 1` on an integral type is -the `.Positive()` row; on `decimal`, `double` or `float` it is the `.GreaterThanOrEqualTo(N)` row, -because `.Positive()` would admit the values between zero and one that the guard rejects. That is a -rare draw for an otherwise unconstrained decimal — measured at one in five thousand — and a common -one as soon as the parameter carries another bound (§17). Exactly the profile of a defect that -survives casual testing. - -**Where the constraints attach.** A guard-derived constraint belongs to the generator for the -parameter's own type, *before* any conversion or composition. An `int?` parameter guarded by -`p <= 0` emits `Any.Int32().Positive().As(value => (int?)value)`, not the reverse; a factory -parameter guarded inside the factory's body emits `Any.String().NonEmpty().As(OrderReference.Create)`. -The `.As` hop always comes last, because it is the step that changes the type. - -Every constraint above is still subject to D4. `.Positive()` on a `uint` parameter does not -resolve (§14.3) and is skipped. - -Guard reading is also what makes factory composition correct rather than nominally present: -`OrderReference.Create` guards on `IsNullOrWhiteSpace`, so the tool emits -`Any.String().NonEmpty().As(OrderReference.Create)` — a chain that works — instead of -`Any.String().As(OrderReference.Create)`, which was measured throwing `AnyGenerationException` -**594 times in 10 000 draws**, and 557 on an independent re-run — about one in seventeen, -which is what an unconstrained draw over the seventeen lengths 0 to 16 predicts (§17). - -That single measurement is why this section exists at all; D5 + D6 sets out the argument and the -alternatives weighed against it. - -### 5.4 Composition - -**A scaffolded generator wins.** If the compilation contains a type named `Any{T}` implementing -`IAny` with a public parameterless constructor, the engine emits `new Any{T}()`. This is how -aggregates compose in cascade, and it works whether that type was scaffolded earlier or written -by hand. - -**Otherwise, a static factory.** A method qualifies when it is `public static`, returns the -parameter's type, takes exactly one parameter, and is named `Create`, `From`, `Of` or `Parse`. -If several qualify, `Create` wins; if several remain, the parameter is unresolved and the console -names the candidates. The emission is `.As(T.Create)`, -with §5.3 applied to the factory's own body. - -Convention, not attribute, not configuration: an attribute would mean touching the developer's -production code to please a test tool, and a configuration file breaks design rule 2. - -### 5.5 Unresolved parameters - -The parameter's argument in the public constructor becomes an identifier that does not exist: - -```csharp - public AnyOrder() - : this(reference: Any.String().NonEmpty().As(OrderReference.Create), - // TODO(dum): no generator inferred for 'Customer customer'. - // Scaffold one: dum generate Customer - // or write one here, or delete this argument and always pass .WithCustomer(...). - customer: TODO_supply_a_generator_for_customer, - quantity: Any.Int32().Positive(), - ... -``` - -The file does not compile until the developer acts. That is the point (D6). The compiler's own -message — *"The name 'TODO_supply_a_generator_for_customer' does not exist in the current -context"* — is the instruction, and it appears in the IDE, in the error list, and in CI. - -The two alternatives were rejected: a `throw` expression compiles and defers the failure to the -first test run, and omitting the parameter makes `AnyOrder` quietly unusable without saying so. -The developer runs the tool and opens the file in the same minute; a red squiggle at the exact -line costs them ten seconds, and a runtime failure a week later costs far more. - ---- - -## 6. Console output - -The console recap is not decoration: it is the mechanism that keeps the tool honest about what it -inferred and what it guessed. - -The run below is the same `Order` as §4.1, but *before* `AnyCustomer` was scaffolded — which is why -`customer` is the one parameter left open. Scaffolding `Customer` and re-running with `--force` -closes it, and that two-step is the intended way through a graph of aggregates. - -```console -$ dum generate Order - -Analyzing Shop.Domain.Order - constructor Order(OrderReference, Customer, int, OrderStatus, IReadOnlyList, DateTime) - - reference OrderReference Any.String().NonEmpty().As(OrderReference.Create) factory, guard - customer Customer — TODO - quantity int Any.Int32().Positive() guard - status OrderStatus Any.Enum() - tags IReadOnlyList Any.ListOf(Any.String().NonEmpty()) - placedAt DateTime Any.DateTime() - -✓ AnyOrder.cs — 5 of 6 parameters inferred, 1 TODO. - The file will not compile until you resolve it. That is deliberate. -``` - -The right-hand column carries the provenance of each expression: empty for the base table, -`guard` when §5.3 tightened it, `factory` when §5.4 composed it, `AnyX` when a scaffolded -generator was reused, `guards not combined` for the §5.3 conflict case, `no source` when the -constructor body was unavailable so no guard could be read, `unread guards` when the body throws in -a way the recognised set did not match, and `unavailable` when the generator exists in the library -but not in the asset this project resolves. - -That last value matters more than it looks. Without it, D4's degradation is indistinguishable from -the tool simply not knowing: a `DateOnly` parameter on a downlevel project would read as "not -inferred", when the truth is "inferred, but `Any.DateOnly()` does not exist here — retarget, or -write it yourself". One word turns a dead end into an instruction. - -**Provenance is data, not output.** The engine returns it in its result model (§10.3); the CLI -renders it. That is what makes the recap testable without a console. - -`--dry-run` prints the same recap to stderr and the file to stdout. - ---- - -## 7. Failure modes and exit codes - -| Situation | Exit | Behaviour | -|---|---|---| -| File written, everything inferred | `0` | — | -| File written, one or more TODOs | `0` | The write succeeded; the developer's build reports the rest. | -| `--dry-run` | `0` | Nothing written. | -| Type not found / ambiguous | `1` | Candidates listed. | -| Output file exists, no `--force` | `1` | Names the file, suggests `--force`, warns that edits are lost. | -| No project / several projects found | `1` | Candidates listed, `--project` suggested. | -| Project fails to load or restore | `1` | The MSBuild diagnostic, verbatim. | -| The project does not reference JustDummies | `1` | Nothing can be resolved (D4); says so and suggests the package. | -| `Any{Type}` shadows a `JustDummies.Any*` type | `0` | **Warning**, then generate. | - -That last row deserves its own note, and the check behind it is narrower than it first looks. The -library declares 40 public `Any*` type names, but **8 of them are generic** — `AnyList`, -`AnySet`, `AnyArray`, `AnySequence`, `AnyDictionary`, `AnyOneOf`, `AnyEnum`, -`AnyCollection<…>`. Arity is part of a type's identity in C#, so a scaffolded `AnySet` (arity 0) -and the library's `AnySet` **coexist without shadowing anything** — verified. A domain type -named `Set`, `List` or `Sequence` is a false alarm. - -The real collision set is the **32 non-generic** names (§14.2): `AnyString`, `AnyGuid`, `AnyUri`, -`AnyPattern`, `AnyChar`, `AnyBoolean`, `AnyDateTime`, `AnyContext`, `AnyDecimal`, `AnyInt32`, … -A domain type named `Pattern`, `Context` or `Uri` scaffolds to a name that, inside its own -namespace, **silently shadows the library's type** for every file in that namespace: C# resolves -the enclosing namespace before any `using`. It compiles; it is just wrong later — verified. The -tool warns, names both types, and generates anyway; under design rule 4 the rename is the -developer's call, and v1.1 gives them the switch. - -The check must therefore compare arity, not just the name. Warning on all 40 would cry wolf on the -eight that cannot collide. - -Multiple type arguments (`dum generate Order Customer Invoice`) are processed independently; the -exit code is the worst of them, and one failure does not prevent the others being written. - ---- - -## 8. Guarantees - -### 8.1 Determinism - -The same type analyzed against the same compilation produces a **byte-identical** file, on any -machine, under any tool version that resolves the same members. Nothing time-, path-, -culture- or hash-order-dependent enters the output: no timestamp, no tool version, no absolute -path, and every enumeration the emitter walks is ordered by declaration. - -This matters even without a `check` verb: it is what makes a re-scaffold reviewable as a diff. - -### 8.2 Reproducibility - -The emitted generator draws from the **ambient** random context, because every expression it -emits comes from the static `Any` façade, and the ambient source resolves the current `AsyncLocal` -frame **at draw time**, not at construction time (§14.5). Therefore: - -```csharp -AnyOrder recipe = new AnyOrder(); // built outside the scope -Any.Reproducibly(() => { - Order order = recipe.Generate(); // still pinned by the scope's seed -}); -``` - -is reproducible, and so is the ordinary case where both happen inside the scope. This was -verified (§17). - -**`Any.WithSeed(seed)` is out of scope (D7).** An `AnyContext` carries its own fixed random source -and is unaffected by the ambient scope, so a generator built from `Any.*` cannot draw from it. A -developer on `WithSeed` supplies that context's generators parameter by parameter through the -`.With{Param}(IAny)` overload, and the emitted XML doc says so in one sentence (§4.1). The -reasoning, and the alternatives weighed against it, are in D7. - -The emitter never produces static state, so `JD009` and `JD020` have nothing to fire on. - -### 8.3 No reflection in the emitted code - -The emitted file contains no reflection — it is constructor calls and fluent chains. The -library's *"no reflection"* claim is a claim about what runs in the developer's test, and it -holds. - -The **tool itself** is a build-time program and is under no such constraint; it uses Roslyn, which -is not reflection anyway. The two questions are independent. - ---- - -## 9. Non-goals for v1.0 - -Named explicitly so they are not mistaken for oversights. - -* **Realistic data.** The tool inherits the library's scope: arbitrary-but-valid, never plausible. - No names, no emails, no addresses. -* **Object-graph auto-filling.** Composition is one hop through `Any{T}` or a one-parameter - factory, depth-limited to 3. Beyond that the developer writes it. -* **Invariants the tool cannot see.** §5.3 reads a closed set of guard idioms. Where the - constructor throws in a way the set does not match — a cross-parameter rule, an arithmetic - condition, a regex guard (§5.3) — the parameter gets the neutral generator and the recap marks it - `unread guards`, so the developer knows where to look. Where validation is delegated entirely to a helper - (`Guard.Against.Null(p)`), there is no throw in the body to see, and the tool cannot tell that - parameter from an unconstrained one. In neither case does it guess. -* **Round-tripping.** The tool never reads a file it previously wrote. -* **`init` / `required` members, property-only construction.** Constructor and static factory only. -* **Anything under `--all`.** Explicit type arguments only. - ---- - -## 10. Architecture - -### 10.1 Two projects - -| Project | TFM | Role | -|---|---|---| -| `JustDummies.GenAny` | `netstandard2.0`, pinned to the Roslyn floor (§13.2) | The engine. Resolution, guard reading, composition, emission. | -| `JustDummies.Cli` | `net8.0`, `RollForward=Major` | The shell. Commands, project loading, file IO, console. | - -On the name: the repository's existing engine for the sibling tool is called `GenDoc` — a -**function** name, not a pattern name (`GenDoc` generates documentation). `GenAny` follows it -exactly: it generates the `AnyX` types, and `Any` is the library's central noun (`Any.String()`, -`IAny`, `AnyOrder`). "Scaffolder" was rejected as a project name — it names a generic role -rather than a product, and every framework has one. The word survives in the prose, where it -describes *behaviour* (§1); the project is named after what it *produces*. - -### 10.2 The boundary - -**`JustDummies.GenAny` owns** the resolution table (§5.2), guard reading (§5.3), composition and -factory recognition (§5.4), the emitter (§11.2), and the naming function (§11.3). -It depends on `Microsoft.CodeAnalysis.CSharp` **only** — not `Workspaces`, which it does not need: -guard reading wants a syntax tree and a semantic model, and emission is string building. - -**It performs no IO, writes to no console, and never touches MSBuild.** Those three constraints -are what keep it loadable inside a Roslyn host. - -**`JustDummies.Cli` owns** the Spectre command definitions and settings, project discovery, -`MSBuildLocator` / `MSBuildWorkspace`, file writing, `--force` / `--dry-run` handling, the console -recap rendering, and the exit codes of §7. - -### 10.3 The contract between them - -One entry point, shaped so the future IDE consumer can call it unchanged: - -* **Input** — a `Compilation`, the target `ITypeSymbol`, and an options record carrying the - namespace override and the type-naming pattern (§16). -* **Output** — a result model, never a bare string: - * the file name and the full source text; - * per-parameter rows: name, type display string, emitted expression (or none), and provenance - (§6); - * warnings, such as the `Any*` shadowing case of §7; - * a flag for "contains at least one TODO"; - * **failure as data, not as an exception** — a target type resolving to nothing or to several - candidates comes back as an outcome carrying that candidate list, so the CLI maps it to the - exit codes of §7 without catching anything. §11.1 puts type resolution inside the engine, so - the model has to carry this or the boundary leaks exceptions. - -The CLI renders that model; a code refactoring would apply the source text and ignore the rest. -Nothing in the model is a console string. - -### 10.4 Packaging - -`JustDummies.Cli` is packed as the .NET tool (`PackAsTool`, `ToolCommandName=dum`, -`PackageId=JustDummies.Cli`). `JustDummies.GenAny` is **not published as its own package** in -v1.0: it travels inside the tool package as an ordinary managed dependency, which is exactly how -the sibling repository ships its `GenDoc` engine. Publishing it later, when an IDE consumer -exists, is a purely additive decision. - -Consequence: neither project carries a public-API compatibility promise, so neither takes a -public-API baseline (§13.4). - -**D9 applies to both.** Neither project references the `JustDummies` package or project. Every -JustDummies symbol is resolved by metadata name against the developer's compilation, exactly as -the library's analyzers do. Version skew between tool and library is therefore structurally -impossible, and the tool package must declare no `JustDummies` dependency (§13.6). - ---- - -## 11. Implementation notes - -### 11.1 Pipeline - -1. `MSBuildLocator.RegisterDefaults()` — **before touching any Roslyn workspace type**. Loading - `MSBuildWorkspace` first is the classic way this fails, with a `FileNotFoundException` on - `Microsoft.Build` that names nothing useful. (CLI only.) -2. `MSBuildWorkspace.Create()`, open the project, take its `Compilation`. Workspace diagnostics - are surfaced, not swallowed. (CLI only.) -3. Hand the `Compilation` to the engine. Everything from here is `JustDummies.GenAny`. -4. Resolve `JustDummies.Any`, ``JustDummies.IAny`1`` and `JustDummies.AnyExtensions` by metadata - name. Absent → the engine reports it and the CLI exits `1` (§7). -5. Resolve the target type (§3.2), pick the constructor (§5.1). -6. Per parameter: base table (§5.2) → guards (§5.3) → composition (§5.4) → unresolved (§5.5). - Every candidate member is looked up in the compilation before it is kept (D4). -7. Emit into the result model (§10.3). -8. The CLI writes the file and renders the recap. - -### 11.2 Emitter - -A plain string builder over an ordered model, not `SyntaxFactory`. The output must be readable -and match a hand-written layout — aligned field declarations, explicit types, braces — and -`SyntaxFactory`-normalised whitespace does not produce that. Since the emitter is covered by -golden-file tests (§12), the fragility argument for a syntax API does not apply. - -### 11.3 Naming - -Route the emitted type name through **one** function, `TypeNaming.GeneratorNameFor(ITypeSymbol, -NamingOptions)`. v1.1 (§16) is then a change to that function plus an options binding, not a -sweep. In v1.0 `NamingOptions` carries a single fixed pattern, `Any{Type}`. - ---- - -## 12. Test plan - -**Engine — `JustDummies.GenAny.UnitTests`** (the bulk): - -* **Resolver unit tests.** Build a `CSharpCompilation` in memory with a reference to the built - `JustDummies.dll`, and assert the emitted expression string per parameter. Fast, no MSBuild. - Cover every row of §5.2, every row of §5.3, both §5.4 paths, and the §5.5 fallback. Include the - unsigned case (`p <= 0` on a `uint`), the value-type nullable case, both composition outcomes of - §5.3 (complementary bounds kept, same bound dropped), a size guard on a **collection** parameter - (which must reach `WithMaxCount`, never `WithMaxLength`), and `p < 1` on an integral and on a - `decimal` parameter — the two rows that differ only by the parameter's type. Add a negative case: - a constructor guarded by `!Regex.IsMatch(...)` must produce **no** pattern constraint, so the - exclusion of §5.3 cannot be undone by accident. -* **Emitter golden files.** One approved file per representative shape: no parameters, one - parameter, six parameters, a TODO, a name collision, a positional record, a static-factory - target. The no-parameter file pins the degenerate shape of §4.2 — emitting the two constructors - unconditionally there is a `CS0111`. The collision file must use a **non-generic** library name - (`Pattern`, `Context`, `Uri`), since a generic one cannot collide (§7). -* **Compile-the-output tests.** Each golden file is compiled against `JustDummies.dll` **with the - JustDummies analyzers wired**, and the compilation must produce no `CS*` error and no `JD*` - diagnostic. This is the check D3 buys: since the file is not marked as generated code, the - analyzers actually run on it. The harness must include a **control file with a known violation**, - asserted to fire — otherwise "no diagnostics" cannot be distinguished from "analyzers not - loaded" (§17.2). -* **The own-code test.** Scaffold the **hosting repository's real types**, compile the results, - and generate a value from each. The reasoning is recorded in the analyzer-on-own-code decision - (§13.7): a rule and the snippet that tests it, both written by the same author, share the same - misconception and pass together; code written for other reasons does not. `ErrorCode.Create` in - the current repository is the canonical case — it guards on `IsNullOrWhiteSpace`, so without - §5.3 the scaffolded code fails about one call in seventeen, which no golden file would reveal. - In a repository without such types, use any validating value object with a static factory. -* **Asset-selection test.** Scaffold against a `netstandard2.0`-asset consumer and a `net8.0`-asset - consumer for a type with a `DateOnly` parameter, and assert the first produces a TODO **marked - `unavailable`** — not merely a TODO — and the second `Any.DateOnly()`. This is the executable - proof of D4 (§13.8). - -**Shell — `JustDummies.Cli.UnitTests`:** project discovery, option handling, exit codes of §7, -and recap rendering from a fixed result model. - ---- - -## 13. What the hosting repository must provide - -JustDummies is expected to move to its own repository before this tool is built. This section -states each dependency on the host as a **requirement**, with the current repository's -realization as an example. If the library has moved, re-establish these there; do not build the -tool against another repository's infrastructure. - -### 13.1 Pinned package versions - -For the tool's dependencies. New to the tool: -`Microsoft.CodeAnalysis.Workspaces.MSBuild` and `Microsoft.Build.Locator` (CLI only). Already -present for the library and its analyzers: `Microsoft.CodeAnalysis.CSharp` and -`Spectre.Console.Cli`. *Current realization: central package management in -`Directory.Packages.props`.* - -### 13.2 A Roslyn floor property - -`JustDummies.GenAny` must compile against the **same minimum -Roslyn version as the analyzer package**, and must not float above it — an assembly loaded by a -consumer's compiler fails silently (`CS8032`) on an older host if it was built against a newer -Roslyn. *Current realization: `RoslynFloorVersion` = `4.8.0`, set once in `Directory.Build.props` -and applied with `VersionOverride`.* The CLI is **not** bound by this: it hosts its own compiler. - -The two therefore differ on purpose — the CLI carries a current Roslyn and hands a `Compilation` to -an engine compiled against an older one. That direction is the supported one: a newer runtime -satisfies an older reference. The reverse never holds, which is the whole reason the floor is -pinned rather than floated. - -### 13.3 Solution nesting - -If the host uses a `.sln`, add both projects and both test projects to -its `GlobalSection(NestedProjects)` under the source and test solution folders. A project missing -from that section appears loose at the solution root instead of grouped with its siblings. This -has been missed and fixed after the fact several times; check it every time a `.csproj` is added. - -### 13.4 Public-API baseline exclusion - -Neither `JustDummies.GenAny` nor `JustDummies.Cli` opts -into the public-API baseline: tools carry no compatibility promise, and the analyzer would flag -their entire surface as undeclared. *Current realization: only the shipping libraries import -`build/PublicApiBaseline.props`.* - -### 13.5 Mutation testing - -If the host measures mutation on projects whose code ships or runs, -both projects qualify. Give each its own configuration — the engine is the high-value target, the -shell is not — and register them with the rest. *Current realization: one JSON per project under -`build/stryker/`, driven by a dedicated workflow, advisory per pull request and enforced by a -weekly sweep.* - -### 13.6 A release train for the tool - -Separate from the library's. The tool does not version in -lockstep with the library (D9), so it must not ride the library's train. The train's packing step -must assert that the produced `.nupkg` declares **no `JustDummies` dependency** — the executable -form of D9. *Current realization: `tools/packaging/pack.sh` with one train per package family and -a standalone assertion already written for the library's train.* - -### 13.7 The analyzers must be runnable over the host's own code - -So the own-code test of §12 can -exist. *Current realization: the analyzer project is wired into the repository's own suites, a -decision taken after the analyzers' unit suite was found unable to catch five wrong rules that -running over real code caught immediately.* - -### 13.8 Two consumer TFMs for the packed library - -So the asset-selection test -of §12 can exist: one consumer at `net8.0` (resolves the `net8.0` asset) and one below it -(resolves `netstandard2.0`). *Current realization: an isolated project outside the solution, -multi-targeted, consuming the packed `.nupkg` from a local feed.* - -### 13.9 Test framework - -*Current realization: `xunit.v3`, `NFluent`, `Verify.XunitV3` for golden -files, `NSubstitute`.* Any equivalent works; the golden-file tests need a snapshot library. - -### 13.10 Commit, branch and pull-request conventions - -And an ADR process for §15. *Current -realization: Conventional Commits with a closed type and scope list, enforced by a hook and by -CI; ADRs under `doc/handwritten/for-maintainers/adr/` where an agent drafts as `Proposed` and the -maintainer accepts.* - ---- - -## 14. Library facts this specification depends on - -Everything below was read from the library's source. It is inlined so this document can be -implemented from without opening the library, and so a future reader can tell which claims are -load-bearing. §14.7 gives the command to re-derive each block. - -### 14.1 Package identity and target frameworks - -* `PackageId` **`JustDummies`**, `TargetFrameworks` **`netstandard2.0;net8.0`**, `Nullable` - enabled, `LangVersion` latest. -* The two assets diverge: the `net8.0` leg additionally carries `DateOnly`, `TimeOnly`, `Int128`, - `UInt128` and `Half`, guarded by `#if NET8_0_OR_GREATER`. A consumer below `net8.0` resolves the - `netstandard2.0` asset and does not see them. This is the fact D4 exists to absorb. -* The analyzers ship **inside** that package under `analyzers/dotnet/cs`, so every consumer gets - them automatically. This is why the emitted file is analyzed at all (D3). -* A companion package adapts the library to xUnit v3 (`[Reproducible]`); the tool does not - interact with it. - -### 14.2 Entry points - -`JustDummies.Any` is a static façade, split across partial files by family. The complete set of -factories, all drawing from the ambient random context: - -* **Primitives** — `String()`, `Boolean()`, `Char()`, `Guid()`, - `SByte()`, `Byte()`, `Int16()`, `UInt16()`, `Int32()`, `UInt32()`, `Int64()`, `UInt64()`, - `Single()`, `Double()`, `Decimal()`, - `TimeSpan()`, `DateTime()`, `DateTimeOffset()`, - `Enum() where TEnum : struct, Enum`. -* **`net8.0` asset only** — `DateOnly()`, `TimeOnly()`, `Int128()`, `UInt128()`, `Half()`. -* **Pattern** — `StringMatching(string)`, `StringMatching(Regex)`. -* **URI** — `Uri()`, then a family selector: `.Web()`, `.Ftp()`, `.Mailto()`, `.Relative()`, - `.WebSocket()`. -* **Choice** — `OneOf(params T[])`, `ElementOf(IReadOnlyList)`, - `ElementOf(IEnumerable)`. -* **Collections** — `ListOf`, `ArrayOf`, `SequenceOf`, `SetOf` (with an optional - comparer), `DictionaryOf` (with an optional key comparer). -* **Composition** — `Combine` in arities 2 through 8, `PairOf`, `TripleOf`. -* **Reproducibility** — `WithSeed(int)`, `UseSeed(int)`, `UseSeed(int, string)`, - `Reproducibly(...)`, `ReproduciblyAsync(...)`. - -Note the naming traps: it is **`Any.Boolean()`**, not `Any.Bool()`; and `double` maps to -**`Any.Double()`**, not `Any.Decimal()`. - -`AnyContext`, returned by `Any.WithSeed(int)`, mirrors the primitives, the pattern, the URI and -the choice entry points as **instance** methods drawing from its own fixed source. It does **not** -mirror the collection or composition entry points. D7 puts it out of scope. - -The library declares **40 public `Any*` type names** — 38 generators plus `AnyContext` and -`AnyGenerationException`. **8 are generic and 32 are not**, and only the non-generic ones can be -shadowed by a scaffolded `Any{Type}`; that 32-name set is what the warning of §7 checks against. -(`AnyCollection<…>`, the abstract base of the collection generators, is easy to miss when counting: -it is declared `public abstract class`, not `public sealed class`.) - -### 14.3 Constraint surfaces the emitter uses - -| Generator family | Constraint surface available to the emitter | -|---|---| -| `AnyString` | `NonEmpty`, `WithMinLength`, `WithMaxLength`, `WithLength`, `WithLengthBetween`, `StartingWith`, `EndingWith`, `Containing`, `Alpha`, `Numeric`, `AlphaNumeric`, `UpperCase`, `LowerCase`, `WithChars`, `OneOf`, `Except`, `DifferentFrom` | -| Signed integers (`SByte`, `Int16`, `Int32`, `Int64`) | `Positive`, `Negative`, `NonZero`, `Zero`, `Between`, `GreaterThan(OrEqualTo)`, `LessThan(OrEqualTo)`, `MultipleOf`, `OneOf`, `Except`, `DifferentFrom` | -| **Unsigned integers** (`Byte`, `UInt16`, `UInt32`, `UInt64`) | the same **less `Positive` and `Negative`**, which an unsigned type cannot express | -| `AnyDouble`, `AnySingle` | as signed integers, less `MultipleOf` | -| `AnyDecimal` | as signed integers, less `MultipleOf`, plus `WithScale` | -| `AnyGuid` | `NonEmpty`, `Empty`, `OneOf`, `Except`, `DifferentFrom` | -| `AnyBoolean` | `True`, `False`, `DifferentFrom` | -| `AnyEnum` | `AllowingCombinations`, `OneOf`, `Except`, `DifferentFrom` | -| Temporal (`DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`) | `After(OrEqualTo)`, `Before(OrEqualTo)`, `Between`, `WithGranularity`, `OneOf`, `Except`, `DifferentFrom` | -| `AnyTimeSpan` | temporal-style plus `Positive`, `Negative`, `NonZero`, `Zero` | -| Collections | `Empty`, `NonEmpty`, `WithCount`, `WithCountBetween`, `WithMinCount`, `WithMaxCount`, `Containing`, `ContainingAny` | - -Two rows bite. The **unsigned** one is why D4 must gate `.Positive()` rather than let the emitter -assume a uniform numeric algebra. The **collection** one is why a size guard must reach the count -family: there is no `WithLength` on a collection generator, so reading such a guard against the -string family emits a member that never resolves (§5.3). - -v1.0 draws on the size, sign and bound constraints only. The charset and pattern families are -listed because §16 may reach for them, not because the emitter uses them today. - -### 14.4 Composition seams - -* `AnyExtensions.As(this IAny, Func)` → `IAny`. - A method group such as `OrderReference.Create` binds directly. When the factory rejects the - generated value, the call throws `AnyGenerationException`. -* `Any.Combine` (arities 2–8) → `IAny`. -* Collection generators derive from a common base implementing `IAny`: - `ListOf` → `List`, `ArrayOf` → `T[]`, `SequenceOf` → `IEnumerable`, `SetOf` → - `HashSet`, `DictionaryOf` → `Dictionary`. -* `NullableExtensions.OrNull()` exists in two forms, one for value types and one for annotated - reference types. **D10 forbids emitting either.** - -### 14.5 Semantic invariants the emitted code depends on - -These five are the ones that would silently break the emitted code if they changed. Each is -exercised by §17. - -1. **The ambient source resolves at draw time.** Every `Any.*` factory captures a singleton - ambient source, and that source reads the current `AsyncLocal` frame inside `Generate()`, not - at construction. This is why a recipe built outside a reproducibility scope still replays - inside it (§8.2). -2. **`IAny` is covariant.** Which is why the collection interface rows of §5.2 need no - adapter — and why the value-type nullable row does. -3. **Generators are immutable recipes.** Every fluent constraint returns a new instance. D2 - inherits this. -4. **`Any.String()` unconstrained draws 0 to 16 ASCII letters and digits.** It can return the - empty string; it can never return whitespace. Both halves matter to §5.2 and §5.3. -5. **`Any.OneOf(value)` requires at least one value, rejects `null` elements, and consumes a - draw.** All three are why §4.2 emits a private `FixedValue` instead. - -### 14.6 Analyzer inventory - -28 diagnostic identifiers over 27 analyzer classes — `JD023` and `JD024` share one. - -| Range | Category | Severities | -|---|---|---| -| `JD001`–`JD004` | Reproducibility | all **Error** | -| `JD005` | Usage | **Error** | -| `JD006` | Usage | Warning | -| `JD007`–`JD010` | Reproducibility | Warning | -| `JD011` | Usage | **Disabled by default** | -| `JD012`–`JD013` | Usage | Warning | -| `JD014`–`JD017` | Constraints | Warning | -| `JD018` | Reproducibility | Warning | -| `JD019` | Reproducibility | **Disabled by default** | -| `JD020` | Reproducibility | Info | -| `JD021` | Reproducibility | Warning | -| `JD022` | Reproducibility | Info | -| `JD023` | Constraints | Warning | -| `JD024` | Constraints | Info | -| `JD025`–`JD026` | Constraints | Warning | -| `JD027`–`JD028` | Composition | Warning | - -Three facts about them drive decisions in this document: - -* **All 27 call `ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)`** — hence D3. -* **The `Usage` rules match any type implementing `IAny`**, not a list of built-in generators — - hence D2's second benefit. -* **The `Reproducibility` rules match chains rooted at the static `Any` façade**, deliberately - answering "no" for a generator reached through a local, a field or a parameter. `new - AnyOrder().Generate()` is therefore invisible to them; that is a known and accepted limit, not - a defect the tool can fix. - -### 14.7 How to re-derive these facts - -From the library's repository root: - -```console -# 14.1 package identity and the TFM split -grep -n "TargetFrameworks\|PackageId\|analyzers/dotnet/cs" JustDummies/JustDummies.csproj -grep -n "#if NET8_0_OR_GREATER" JustDummies/Any.Primitive.cs - -# 14.2 entry points, and the AnyContext mirror -grep -hn "public static" JustDummies/Any.*.cs -grep -n "public " JustDummies/AnyContext.cs -# Type names WITH their arity. `abstract` matters — AnyCollection is not sealed, and a pattern -# that only allows `sealed` under-counts by one. The arity is what §7's shadowing check needs: -# 8 generic names cannot collide with a scaffolded Any{Type}, the other 32 can. -grep -rhoP "^public (?:sealed |abstract )?class \KAny\w+(?:<[^>]*>)?" JustDummies/*.cs | sort -u - -# 14.3 constraint surfaces -grep -oP "public AnyInt32 \K\w+(?=\()" JustDummies/AnyInt32.cs | sort -u -grep -oP "public AnyUInt32 \K\w+(?=\()" JustDummies/AnyUInt32.cs | sort -u # note: no Positive/Negative - -# 14.4 composition seams -grep -n "public static" JustDummies/AnyExtensions.cs JustDummies/NullableExtensions.cs - -# 14.5 invariants — read the XML docs, they state all five -sed -n '1,60p' JustDummies/IAny.cs -grep -n "AmbientRandomSource.Instance" JustDummies/Any.Primitive.cs | head -3 - -# 14.6 analyzer inventory and the generated-code exemption -cat JustDummies.Analyzers/AnalyzerReleases.Unshipped.md -grep -rlc "ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)" JustDummies.Analyzers/*.cs | wc -l -``` - -Paths are those of the current repository; adjust them if the library has moved. - ---- - -## 15. Decision records - -All eleven decisions in §2 are architectural: a future maintainer would question each of them, and -each would stand unchanged if the implementation were rewritten. **Ten records** cover them — D5 -and D6 share one. In the ordinary course they would be entered into a repository's ADR base as -`Proposed`, numbered there, and accepted by the maintainer. - -**They are held inside this specification instead, because the repository that should hold them -does not exist yet.** JustDummies is expected to move out of `Reefact/first-class-errors` before -this tool is built, and these records describe a tool that will live in that new repository. -Entering them into the current base would assign them numbers — the stable handles the whole base -is built on — that would have to be abandoned or rewritten on migration, and would leave this -repository's log carrying decisions about code it no longer holds. - -Keeping them here costs nothing and buys two things. The reasoning stays attached to the -specification it justifies, so the decision history travels as a single artefact rather than as a -document plus eight files someone must remember to bring. And each record follows this repository's -ADR format section for section, so admission is mechanical: lift the record into the destination -repository's ADR base, assign its number there, keep its `Proposed:` date, and replace the record -here with a link. - -Until then they are drafts. No status is flipped in this document; the maintainer accepts them in -the base that will hold them. - -Three of these records were written after the decision table was, and why is worth keeping. D7, D8 -and D10 were each judged too small at first — a scope limit already scheduled for revisiting, a -namespace default with an override, one rule about one library method. Size was the wrong measure -every time; the test is whether the decision outlives the implementation, and all three do. - -More to the point, each turned out to carry a consequence elsewhere in this document that reads as -accidental unless the reasoning is written down. D10 is why §5.2 carries an explicit conversion for -nullable value types. D8 is the **sole cause** of the shadowing hazard in §7. D7 is why the emitted -type needs no lifecycle rule at all, and why two seeding analyzers have nothing to report on it. A -record that keeps a plausible cleanup from reintroducing a defect earns its place whatever its size, -and none of those three consequences is self-explanatory in the section where it lands. - ---- - -### D1 — Scaffold the generator once and hand the file to the developer - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The tool writes a C# file, containing a generator for a type of the developer's own code, into the -developer's own project. Three shapes exist for such a tool, all of them in use by real tooling: a -Roslyn source generator producing the file into the build's intermediate output; a file written -once into the source tree; and a file written into the source tree together with a verification -command that fails when it no longer matches what the tool would produce today. - -A file in the source tree can fall out of step, silently, with the type it was derived from when -that type's constructor changes. - -The library the tool serves states the absence of magic as part of its positioning: no reflection, -no object-graph filling, and its own description is "small, deterministic, explicit". - -The tool cannot infer every constructor parameter. Some parameters carry invariants expressed in -ways no closed rule set can read (§9), so a scaffolded file is expected to be incomplete for some -types. - -A source generator's output is not editable by the developer and does not appear in code review. A -file in the source tree is both. - -#### Decision - -The tool writes each generator file once and transfers ownership of it to the developer, who may -edit it freely and is never asked to regenerate it. - -#### Rationale - -Drift is the only serious objection to writing into the source tree, and it exists only while the -tool claims ownership of the file. Once ownership is transferred, "the file no longer matches what -the tool would produce" stops being a defect and becomes the expected state of a file the developer -has edited — which is precisely what the tool asks them to do. The objection dissolves rather than -being mitigated. - -That transfer is also what makes an incomplete file acceptable. A tool that owns its output must -produce something complete or fail; a tool that hands over a skeleton may stop where its knowledge -stops and say so, which is the honest position given that some invariants are unreadable. D5 and -D6 depend on this being settled first. - -Editability and review visibility serve a library whose selling point is that nothing happens -behind the developer's back. A generator they can read, step through in a debugger and modify is -consistent with that positioning; one materialised by the compiler is not. - -Removing ownership removes an entire class of machinery with it: no verification verb, no -regeneration protocol, no drift detection, no rules about which regions may be hand-edited. For a -tool whose first design rule is that it must be trivial to adopt, the machinery not built is worth -more than the guarantees it would have offered. - -#### Alternatives Considered - -##### A Roslyn source generator - -Considered because it makes drift structurally impossible: it re-runs on every build, so its output -cannot lag the type. - -Rejected because it forfeits everything that the file being real buys. The developer cannot edit -it, cannot complete the parameters the tool failed to infer, and reviewers never see it. It also -has no useful way to leave work unfinished, so the unresolved-parameter case would have to fail the -build with no place for the developer to act. - -##### A written file plus a verification verb - -Considered because it is the standard answer to drift for committed generated artefacts, and -integrates cleanly into continuous integration. - -Rejected because verification and editing are mutually exclusive. A command that fails whenever the -file differs from a fresh generation forbids the very editing this tool exists to invite. Keeping -both would mean encoding which regions belong to the tool and which to the developer — more -machinery than the whole feature is worth. - -#### Consequences - -**Positive.** The tool has one verb and no protocol. The scaffolded file is ordinary code: -reviewable, debuggable, editable. The unresolved-parameter path of D6 becomes available. - -**Negative.** A generator can fall behind its type. Adding a constructor parameter breaks the -generator's compilation, which surfaces the problem; changing a parameter's invariant does not — the -generator keeps producing values the constructor now rejects, and only a failing test reveals it. - -**Risks.** A developer may expect regeneration to preserve their edits. Mitigated by the emitted -header, which states that regeneration overwrites and that the type is `partial` so neighbouring -files survive, and by `--force` being required to overwrite at all. - -#### Follow-up Actions - -* State the "this file is yours" position prominently in the tool's user documentation: it inverts - the expectation set by most scaffolding tools. - -#### References - -* §1, §3, §4.3 of this specification. - ---- - -### D2 — Make the emitted generator a first-class `IAny` - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -`IAny` is the library's composition seam: `As`, `Combine`, the collection generators and the -choice generators all consume and produce it (§14.4). - -The interface is documented as an immutable recipe, and every generator in the library honours -that — each fluent constraint returns a new instance (§14.5). - -The analyzers' `Usage` category recognises a generator as the `IAny` interface itself or any -type implementing it, rather than as a fixed list of built-in types (§14.6). - -The emitted type exposes one fluent method per constructor parameter, which gives it the shape of a -builder. Builders in the wider ecosystem conventionally mutate and return `this`. - -#### Decision - -The emitted type implements `IAny` and is immutable, every `With` method returning a new -instance. - -#### Rationale - -Implementing the seam is what makes nested aggregates work with no additional code. An emitted -generator is directly usable as an element generator, a `Combine` operand or an `As` source; -without the interface, either the tool would emit adapters or the developer would write them. - -The second benefit is less obvious and worth as much: the `Usage` analyzers key on the interface, -so an emitted type that implements it is covered by them exactly as a built-in generator is. That -coverage matters more here than anywhere else, because the emitted file is the one the developer -edits (D3), often while meeting this API for the first time. - -Immutability is not a style preference but the seam's documented contract. A mutating `With` would -make the emitted type the only mutable generator in the ecosystem, and would behave surprisingly: -two generators derived from a shared base would interfere with each other. The cost is one -allocation per `With` call, on a code path that is not hot. - -#### Alternatives Considered - -##### A mutating builder returning `this` - -Considered because it is the conventional builder shape and allocates less. - -Rejected because it contradicts the documented contract of the interface it would implement, and -because deriving two generators from a shared base would silently corrupt both. - -##### A plain type exposing `Generate`, not implementing `IAny` - -Considered because it keeps the emitted file free of any library interface. - -Rejected because it forfeits both benefits at once: no composition with the library's seams, and no -analyzer coverage on the file that needs it most. - -#### Consequences - -**Positive.** Composition with every library seam comes free. Four analyzer rules extend to the -emitted type at no cost. - -**Negative.** One allocation per `With` call. The private all-arguments constructor grows with the -parameter count, so the emitted file is verbose for wide constructors. - -**Risks.** If the library ever relaxed the immutability contract, the emitted shape would be -stricter than required — harmless, and no action would be needed. - -#### Follow-up Actions - -* None. - -#### References - -* §4.2, §14.4, §14.5, §14.6 of this specification. - ---- - -### D3 — Leave the scaffolded file open to the JustDummies analyzers - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The analyzers ship inside the library's own package, so every consumer of the library receives them -automatically (§14.1). - -All 27 of them exempt generated code (§14.6). Roslyn classifies a file as generated when it is -named `*.g.cs` or `*.generated.cs`, or when it opens with an auto-generated header comment. - -The exemption was measured. One file containing exactly two violations — a `JD006` warning and a -`JD005` error — was compiled twice, changing nothing but its first line: without the header, both -were reported and the build failed; with `// `, neither was reported and the build -succeeded (§17). - -The scaffolded file is the file the developer edits (D1), and it may leave the tool incomplete -(D6). - -The one way the emitter can produce a chain the library rejects at run time is two guard-derived -constraints landing on the same axis (§5.3). `JD015` and `JD023` detect exactly that class of -unsatisfiable chain. - -The ecosystem convention is to mark generated files, chiefly so that style analyzers do not fire on -machine-written code. - -#### Decision - -The scaffolded file carries no generated-code marker, so the JustDummies analyzers analyse it as -they analyse hand-written code. - -#### Rationale - -The exemption is total, and the measurement shows how quietly it applies: a compile error became -silence on a one-line change. Marking the file would make it the only file in the developer's test -project outside the library's own safety net. - -It would also be the worst possible file to exempt. It is the one the developer will edit, using an -API they may be meeting for the first time, in a file the tool has just told them to complete. - -The coverage additionally backstops the emitter's own mistakes. The same-axis rule of §5.3 removes -the conflicting-chain case by construction, but a defect in that rule would otherwise surface only -as a run-time exception; with the file analysed, it surfaces in the editor instead. - -The conventional reason for marking — sparing machine-written code from style rules — does not -apply to a file that is, by D1, not machine-owned. It is the developer's code from the moment it is -written, and it should answer to the same rules as its neighbours. - -#### Alternatives Considered - -##### Marking the file with an auto-generated header - -Considered because it is the ecosystem convention, and because it would spare a scaffolded file -from the developer's own style analyzers on first generation. - -Rejected because it disables every JustDummies diagnostic on that file, which is the opposite of -what a file about to be hand-edited against an unfamiliar API needs. The measurement makes the cost -concrete: an error-severity diagnostic disappears without trace. - -##### Naming the file `*.g.cs` - -Considered as a lighter-touch variant of the same idea. - -Rejected for the same reason, plus one more: the name asserts machine ownership, which D1 denies. - -#### Consequences - -**Positive.** The scaffolded file is covered by the same diagnostics as the code around it, and -emitter mistakes surface at edit time rather than at run time. - -**Negative.** The developer's own analyzers and style rules also fire on it, so a first scaffold may -need a formatting pass to match house style. The emitter reduces this by writing explicit types and -conventional layout, but it cannot match every configuration. - -**Risks.** A future emitter change could introduce a diagnostic into every scaffolded file at once. -Mitigated by the compile-the-output tests (§12), which fail on any `JD` diagnostic. - -#### Follow-up Actions - -* Keep the control file in the compile-the-output test. Without a known violation asserted to fire, - the test cannot distinguish "no diagnostics" from "the analyzers never loaded", and silently - becomes a no-op — the trap this specification's own verification fell into on its first attempt - (§17.2). - -#### References - -* §2, §5.3, §14.6, §17 of this specification. - ---- - -### D4 — Emit only members resolved in the target compilation - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The library ships two divergent assets. The modern one carries five generator entry points that do -not exist on the downlevel one, because the underlying framework types do not exist there (§14.1). - -The unsigned integer generators expose no `Positive` or `Negative` constraint, since an unsigned -type cannot express either (§14.3). - -The tool holds no reference to the library (D9), so it cannot see the library's API at its own -compile time. - -The developer's compilation is the authority on what is actually available in their project: their -target framework selects the asset, and their package version selects the surface. - -A member emitted but absent is a compile error in the developer's project, attributed to the tool. - -#### Decision - -The engine emits a JustDummies member only after resolving that member in the developer's -compilation. - -#### Rationale - -The alternative is a table, inside the tool, of what exists per library version and per target -framework. It would need maintaining for every library release, would be wrong for any version the -tool predates, and would encode facts the compilation already knows exactly. - -Resolution replaces four independent special cases with one rule: the asset split, the unsigned -numeric surface, the tool being older or newer than the library, and the developer's own generators -being discovered. None of them has to be named anywhere in the emitter. - -The failure mode it produces is the right one. A member that cannot be resolved turns the parameter -into an unresolved one (D6) — a state the tool already handles and reports — rather than an -emission the developer meets as a compile error they did not cause and cannot interpret. - -It also makes the public-API guarantee free rather than something to enforce: anything resolvable -in the compilation is by construction part of the library's shipped public surface, so the tool -cannot emit against an internal member or one outside the compatibility baseline. - -#### Alternatives Considered - -##### A hard-coded table of members per library version - -Considered because it is simpler, needs no symbol lookup, and makes the emitter's knowledge -explicit and reviewable. - -Rejected because it is unmaintainable across versions and simply wrong for any library version -released after the tool. - -##### Referencing the library and emitting against its compile-time types - -Considered because it would let the compiler check the emitter's own use of the API, removing the -silent-typo failure mode that D9 accepts. - -Rejected because it contradicts D9, and because it would answer the wrong question anyway: the -version the tool references is not the version in the developer's project. - -#### Consequences - -**Positive.** The tool is correct against any library version and any target framework, holding no -per-version knowledge at all. - -**Negative.** Degradation is quiet by nature: a member that fails to resolve simply does not appear -in the emission, and without deliberate reporting the developer cannot tell a parameter the tool -could not infer from one whose generator exists but is unavailable here. - -**Risks.** A resolution defect — looking up a wrong metadata name — would degrade everything to -TODOs at once, which reads as the tool not working rather than as a bug. Mitigated by the -asset-selection test (§12), which asserts both the present and the absent case. - -#### Follow-up Actions - -* §6 carries the `unavailable` provenance value for this reason. Keep a test asserting it: without - one, the degradation this decision accepts becomes invisible again and the requirement decays - into a comment. - -#### References - -* §5.2, §5.3, §6, §14.1, §14.3 of this specification. - ---- - -### D5 + D6 — Seed generators from constructor guards, and leave the rest as a compile error - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -Unconstrained generators draw their full domain: the string generator yields zero to sixteen -characters, so it can return the empty string, and the integer generator draws the whole range -including negatives (§14.5). - -Domain constructors commonly reject part of that domain. - -This was measured on a real validating factory from this repository: an unconstrained string -generator composed onto it threw 594 times in 10 000 draws, and 557 on an independent re-run — -roughly one in seventeen, the rate an unconstrained draw over the lengths 0 to 16 predicts (§17). - -Guard clauses at the head of a constructor are the dominant validation idiom in the code this tool -targets. - -The tool has the constructor body as source for any type in the developer's solution, and does not -for a type coming from a package. - -Some invariants are not expressed as guards at all — validation delegated to a helper, a guard -library, or a rule spanning two parameters. - -The developer runs the tool and opens the resulting file within the same minute. - -#### Decision - -The engine derives constraints from a closed set of recognised constructor guard clauses, and emits -an identifier that does not exist for any parameter whose generator it cannot infer. - -#### Rationale - -Without guard reading the tool's default output is not merely imprecise, it is harmful: it -manufactures, inside the developer's test suite, the intermittent failure the library exists to -eliminate. One failure in seventeen is worse than no tool at all, because it discredits the library -at the moment of first use. - -A closed, syntactic set bounds the risk. Reading guards is not inference about intent; each -recognised form maps to exactly one constraint, and anything outside the set is ignored. -Conservative matching — one parameter, no boolean composition, constant operands — under-reports -rather than misfires, which is the correct bias here: a missing constraint yields a value the -constructor may reject and a visible failure, whereas a wrong constraint yields a value that -silently mis-exercises the test. - -For the parameters that remain unresolved, a compile error is the cheapest signal available. The -developer is in the file, having just run the tool; the compiler names the parameter in its own -message, and that message reaches the editor, the error list and continuous integration alike. A -signal delivered later costs more, and one never delivered costs most. - -Shipping a file that does not compile is defensible only because of D1. A tool that owned its -output could not do it; a tool handing over a skeleton can, and stating the gap plainly is more -honest than a file that compiles and fails later. - -#### Alternatives Considered - -##### Neutral generators, leaving all tightening to the developer - -Considered because it makes the tool claim nothing it cannot prove, which is attractive for a -library built on precision. - -Rejected on the measurement. The default output would fail intermittently for most validating -constructors, which is the highest-cost failure mode available and the one the library was built to -remove. - -##### A run-time exception for unresolved parameters - -Considered because the file then compiles, which is friendlier at first sight. - -Rejected because it defers the signal past the moment the developer is looking at the file, and -converts a scaffolding gap into a test failure whose cause is a line they never read. - -##### Omitting the unresolved parameter from the recipe - -Considered because it is the most elegant of the three: the generator would simply require the -developer to supply that parameter. - -Rejected because it is silent. The generator becomes partially usable without saying so, and the -gap surfaces as a null or a default deep inside a test. - -##### A declaration file mapping types to their construction - -Considered because it would let the developer teach the tool once, covering invariants no guard -expresses, and would make composition correct for value objects in general rather than only for -guarded ones. - -Rejected for the first version because it converts the tool into a convention system, contradicting -the design rule that nothing be configured before first use. Left open in §16. - -#### Consequences - -**Positive.** The emitted default works for the dominant validation idiom. Unresolved parameters -are impossible to overlook. - -**Negative.** A scaffolded file may not compile until edited, which will surprise anyone expecting -scaffolding to produce working code. Invariants outside the recognised set still yield values the -constructor rejects. - -**Risks.** The recognised set may match a guard whose meaning it mistakes, producing a constraint -that is wrong rather than absent — the one outcome worse than inferring nothing. Mitigated by the -conservative matching conditions and the same-axis conflict rule; the own-code test (§12) is the -check most likely to catch it, because it runs the emitter over code written for other reasons. - -#### Follow-up Actions - -* Every addition to the recognised guard set needs a case in the resolver suite and, where - possible, an instance in the own-code test. - -#### References - -* §5.3, §5.5, §9, §14.5, §17 of this specification. - ---- - -### D7 — Draw from the ambient context and hold no state - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The library offers two reproducibility mechanisms. The **ambient** context is pinned by a scope -(`Any.UseSeed`, `Any.Reproducibly`) and flows with the execution context; the **isolated** context -is created by `Any.WithSeed` and carries its own fixed random source, unaffected by any scope. - -Every static `Any.*` factory captures the ambient source object, and that source resolves the -current `AsyncLocal` frame **when `Generate()` runs**, not when the generator is built (§14.5). - -`AnyContext` mirrors the primitive, pattern, URI and choice entry points as instance methods. It -does **not** mirror the collection or composition entry points (§14.2). - -The emitted type carries a `With{Param}(IAny)` overload for every parameter (D2). It is -built once and may be generated from many times, possibly inside different scopes. - -Two analyzers, `JD009` and `JD020`, report draws from static initialisers and shared static -contexts. The emitted file is analyzed like hand-written code (D3). - -#### Decision - -The emitted generator builds its recipe from the static `Any` façade alone, holding no random -source, no seed and no static state of its own. - -#### Rationale - -Draw-time resolution is what makes this free. A recipe built outside a reproducibility scope and -generated inside one is still pinned by that scope, so the emitted type needs no lifecycle rule at -all: build it where it reads best, generate it where the seed matters. Any design that captured a -source at construction would have to specify that lifecycle, and would have to say what happens -when the generator outlives the scope it was born in. - -Holding no static state is what leaves `JD009` and `JD020` with nothing to report. Since the -emitted file is analyzed, an emitter that cached anything statically would be flagged in the -developer's own build rather than in ours — the diagnostic would be correct, and the tool would be -the one at fault. - -Supporting the isolated context would mean a second constructor and a second recipe path through -`AnyContext`. That path could not express every row of §5.2, because `AnyContext` mirrors no -collection or composition entry point: the surface would be larger *and* less capable. The case is -already covered without adding any: a developer on `WithSeed` passes that context's generators -per parameter through the overload D2 already provides. - -#### Alternatives Considered - -##### Capturing a seed at construction - -Considered because a generator that owns its seed is self-contained and obviously reproducible, -with nothing ambient to reason about. - -Rejected because it duplicates a mechanism the library already owns, and because two such -generators in one test would draw from independent sequences — so no single seed reported by a -failing test could replay the run as a whole, which is the property the library's reproducibility -exists to provide. - -##### A second constructor taking an `AnyContext` - -Considered because it closes the gap for a developer working with `Any.WithSeed`, which is a -supported way to use the library. - -Rejected for v1.0 because `AnyContext` mirrors only part of the façade, so the second path could -not resolve collection or composed parameters at all, and because the per-parameter override -already covers the case at no cost in surface. Left open in §16. - -#### Consequences - -**Positive.** No lifecycle rule and no static state. The reproducibility guarantee of §8.2 comes -free, and the two seeding analyzers have nothing to fire on. - -**Negative.** A developer using `Any.WithSeed` cannot hand the whole context to the generator and -must supply generators parameter by parameter, which is verbose for a wide constructor. - -**Risks.** A future emitter that memoised anything — a cached generator, a shared instance — would -break the reproducibility guarantee and the analyzer cleanliness at once. The compile-the-output -test catches the second; only a reproducibility test catches the first, and it is the one easy to -forget. - -#### Follow-up Actions - -* Keep a test asserting that a recipe built **outside** a scope replays inside it. It is the - executable form of this decision; §17 records the manual run it must replace. - -#### References - -* §8.2, §14.2, §14.5, §16 of this specification; D2 and D3 of this section. - ---- - -### D8 — Emit the generator into the target type's namespace - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The scaffolded file is written into the developer's test project, but the type it generates lives -in the production project. - -A test that uses `Order` already imports `Order`'s namespace. - -C# resolves a simple type name in the **enclosing namespace before any `using` directive**, so a -type declared in a namespace wins over an imported one of the same name and arity. - -The library declares 32 non-generic public `Any*` type names (§14.2); a scaffolded generator whose -name matches one of them, in a namespace where the library is imported, shadows it. - -The tool offers `--namespace` as a per-invocation override (§3), and the v1.1 naming pattern (§16) -changes the emitted type's name but not its namespace. - -The engine holds a `Compilation` and no MSBuild knowledge: it does not know the project's root -namespace or its folder-to-namespace convention (D11). - -#### Decision - -The emitted generator is declared in the namespace of the type it generates, unless `--namespace` -says otherwise. - -#### Rationale - -It is the only choice that costs nothing at the call site. A test already importing the domain -namespace writes `new AnyOrder()` and stops; any other namespace adds an import to every test file -that touches the generator. That is friction paid on every single use, and design rule 2 prices -that heavily — a tool too tedious to use at each call is not worth adopting. - -It is also the only choice the engine can make from what it holds. The namespace an IDE would -infer — the one implied by the output folder — requires the project's root namespace and its -folder convention, which is exactly the MSBuild knowledge D11 keeps out of the engine. - -The cost is real and accepted with open eyes: **this decision, and only this decision, creates the -shadowing hazard of §7.** A generator in a dedicated namespace could never shadow a library type, -because the developer's `using` would then compete on equal terms instead of losing outright to an -enclosing declaration. The hazard is bounded — 32 names, an arity-aware check, a warning naming -both types — and rare. Trading a rare warned collision against friction on every use is the right -way round. - -#### Alternatives Considered - -##### A dedicated namespace for generated helpers - -Considered because it removes the shadowing hazard entirely and keeps test helpers visibly apart -from domain code, which some codebases require as a matter of layering. - -Rejected because it charges an import to every test file, permanently, to avoid a hazard that -touches a handful of type names and announces itself when it occurs. `--namespace` gives that -layout to whoever wants it, per invocation, without imposing it on everyone. - -##### The namespace implied by the output folder - -Considered because it is what an IDE does when a file is added, so it would match a developer's -expectation. - -Rejected because deriving it needs the project's root namespace and folder-to-namespace -convention. The engine does not carry that (D11), so the CLI would have to discover and pass it, -widening the contract of §10.3 to reach a worse outcome than the target type's own namespace. - -#### Consequences - -**Positive.** Zero friction at the call site. The engine needs no project knowledge. The emitted -namespace declaration is copied from the target type's own file, so the scaffolded file matches its -neighbours in form as well as in name (§4.4). - -**Negative.** A test helper is declared in a production namespace, which some codebases will find -objectionable on layering grounds; `--namespace` is the answer, and it must be given on every -invocation. And this decision is the sole cause of the §7 hazard. - -**Risks.** A developer scaffolding a type named after one of the 32 non-generic library names gets -a silent shadow if they dismiss the warning. Mitigated by the warning naming both types, and by the -v1.1 naming pattern offering a rename that does not require moving namespaces. - -#### Follow-up Actions - -* The shadowing check must be arity-aware (§7). Warning on the eight generic names, which cannot - collide, would train developers to ignore the one warning that matters. - -#### References - -* §3, §4.4, §7, §14.2, §16 of this specification; D11 of this section. - ---- - -### D9 — Give the scaffolder no dependency on the JustDummies package - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The tool emits code that calls the library's API, but never calls that API itself. - -If the tool referenced the library, the developer's project would hold two versions of it: the one -the tool was built against and the one the project actually references. - -The library's own analyzers already resolve every library symbol by metadata name against the -consumer's compilation, referencing no library assembly; a rule whose type is absent from the -compilation simply stays silent. - -The host repository publishes package families on release trains, each train shipping its members -at a single version. - -#### Decision - -Neither the engine nor the CLI references the JustDummies package or project; every JustDummies -symbol is resolved by metadata name against the developer's compilation. - -#### Rationale - -The tool's correctness question is never "what does the library version I was built against offer" -but "what does the library version in this project offer". A reference answers the first while -implying the second, which is exactly how a tool begins emitting code that does not compile for -someone on a different version. - -Together with D4, removing the reference makes version skew structurally impossible rather than -merely tested. There is no version pair to test, because the tool holds no version of the library -at all. - -The library's analyzers already work this way, which demonstrates the pattern is sufficient for -exactly this job: symbols resolved by name, graceful silence when a type is absent. - -It also decouples the release trains. The tool ships when the tool changes and the library when the -library changes, and neither forces a release of the other. - -#### Alternatives Considered - -##### Referencing the library and versioning the two in lockstep - -Considered because it lets the compiler check the emitter's own use of the API, and because a -matching version number is an obvious compatibility story to present to users. - -Rejected because lockstep only guarantees the tool matches the library it shipped alongside, not -the one in the developer's project — the only case that matters — and because it would force a tool -release for every library release. - -#### Consequences - -**Positive.** No version matrix, no compatibility question to manage, and independent release -cadences. - -**Negative.** The emitter's knowledge of the API is expressed as strings, so a mistyped member name -is not a compile error in the tool. It surfaces as an unresolved member, which D4 turns into a -TODO — output that is wrong but quiet. - -**Risks.** That quiet failure mode is the real cost of this decision. Mitigated by the -compile-the-output and own-code tests (§12), which exercise the emitted expressions against a real -compilation, where a mistyped member appears as a TODO in a position that should have carried a -value. - -#### Follow-up Actions - -* The tool's package must assert at packing time that it declares no JustDummies dependency - (§13.6) — the executable form of this decision. - -#### References - -* §10.4, §13.6, §14.2 of this specification. - ---- - -### D10 — Never draw null for a nullable parameter - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The library exposes `OrNull` in two forms — one for value types, one for annotated reference types -— each returning a generator that yields `null` some of the time (§14.4). - -A constructor parameter declared `string?` or `int?` states that null is *permitted*. It does not -state that any particular test intends to exercise the null path. - -The library's stated principle is that constraints express the invariants a value must satisfy, -never what the test asserts. - -The emitted type carries a `With{Param}(IAny)` overload for every parameter (D2), so a -developer can supply any generator, including a nullable one, at a chosen parameter in a chosen -test. - -Variance in C# does not cross value types, so a nullable value-type parameter needs an explicit -conversion when the underlying generator is used. `OrNull` would need none, since it already -returns the nullable generator type (§5.2). - -A test that fails only on some runs is the failure mode the library exists to remove. - -#### Decision - -The emitter never applies `OrNull`, so a nullable parameter draws a value of its underlying type -and the developer opts into null explicitly. - -#### Rationale - -Nullability in a signature is permission, not intent. Reading it as intent makes the tool decide, -on the developer's behalf and at random, which runs exercise the null path — so a test written for -the ordinary path fails on the runs that happen to draw null, for a reason unrelated to anything it -asserts. That is the intermittent failure D5 exists to prevent, reached from the other direction. - -Opting in is already cheap and precise. The generator overload of D2 lets a developer ask for null -at the exact parameter and in the exact test where it matters, which is where that decision -belongs: the test that wants the null path says so, and no other test is affected. - -Refusing here also applies the library's own rule about constraints to a default. Emitting `OrNull` -would encode what a test might assert rather than what the value must satisfy, which is the -distinction the library is built on. - -#### Alternatives Considered - -##### Emitting `OrNull` for every nullable parameter - -Considered because it is the faithful reading of the declared type, needs no special case, and — -for nullable value types — is shorter than the conversion this decision forces. - -Rejected because faithfulness to the signature costs determinism: roughly half the generated values -would be null for no reason the test chose. The shorter emission buys brevity at the price of the -property the library sells. - -##### Emitting `OrNull` only where the constructor visibly tolerates null - -Considered because it would reuse the guard reading D5 already performs, applying nullability only -where the code demonstrably accepts it. - -Rejected because the absence of a null guard is not evidence of intent — it is equally consistent -with an oversight — and because it would make a test's stability depend on whether an unrelated -guard happened to be written. That is worse than a uniform rule in either direction. - -#### Consequences - -**Positive.** A scaffolded generator produces the same shape of value on every run. Nothing in the -emitted default can make a test intermittent through nullability. - -**Negative.** The null branch of a constructor, or of the code under test, is never exercised by a -scaffolded generator unless the developer asks for it. A parameter typed `string?` for a reason -receives a generator that never explores that reason. - -Visibly negative, too: for a nullable value type the emitter must convert explicitly, so §5.2 -carries a hop that reads as gratuitous unless this decision is known. - -**Risks.** That hop is the most likely part of the emitter to be "simplified" back into a defect — -`OrNull` is shorter, returns exactly the wanted type, and looks like the obvious cleanup. -Reintroducing it would restore the flakiness silently. Mitigated by this record and by the resolver -case named below. - -#### Follow-up Actions - -* Keep a resolver case for a nullable value-type parameter asserting the explicit conversion, and - name this record where the emitter performs it, so the hop is not simplified away. - -#### References - -* §5.2, §14.4 of this specification; D2 and D5 of this section. - ---- - -### D11 — Keep the scaffolding engine loadable by a Roslyn host - -**Status:** Proposed -**Proposed:** 2026-07-30 -**Decision Makers:** Reefact - -#### Context - -The CLI must open a project on disk, which requires an MSBuild-aware workspace; that is available -on modern .NET only, not on the downlevel target. - -An assembly loaded by a consumer's compiler — an analyzer, a code fix, a code refactoring — must -target the downlevel framework and be compiled against the lowest Roslyn version it has to load -under. Built against a higher one, it fails to load, and it fails silently. - -A Roslyn code refactoring is a plausible second surface for the engine: the library already ships -analyzers, so the packaging and load path exist, and applying a document is the natural operation -of a refactoring. - -The engine's work is symbol inspection, syntax reading and string building. It needs no file -system, no console and no MSBuild. - -The test surface described in §12 is dominated by engine behaviour rather than by command -plumbing. - -The host repository measures mutation on every project whose code ships or runs (§13.5). - -#### Decision - -The scaffolding engine is a separate library targeting the downlevel framework and compiled against -the analyzer Roslyn floor, performing no input or output, with the CLI as a shell over it. - -#### Rationale - -The constraint is asymmetric in time. Targeting the floor costs the engine almost nothing today, -because none of its work needs a modern API. Discovering later that it must be loadable by a -compiler means re-verifying every API it uses against that floor, throughout a codebase written -without the constraint in mind. Paying now is cheap and paying later is not, which is what -justifies building for a consumer that does not yet exist. - -The boundary the future consumer requires is the same one the present code wants. An engine that -takes a compilation and returns a model, with no output of its own, is the testable shape: the -resolver and emitter can be exercised over an in-memory compilation, with no project on disk and no -argument parsing in the way. - -Separating the two also separates the mutation budget. Command plumbing and the resolution rules do -not deserve equal scrutiny, and a single project cannot express that difference. - -The argument that the CLI may grow further verbs justifies none of this. Extra verbs are extra -files above the same engine, and after D1 the plausible list is nearly empty in any case. - -#### Alternatives Considered - -##### One CLI project holding everything - -Considered because it is the smallest thing that works for a tool with a single verb, and avoids -two projects and two test suites. - -Rejected because it closes the Roslyn-host path at the moment of creation, and because it forces -every engine test through the CLI's dependencies. - -##### A separate engine targeting modern .NET - -Considered because it keeps the boundary, and with it the testing and mutation benefits, without -accepting the downlevel constraint. - -Rejected because the boundary's principal purpose is the consumer that this variant excludes. - -#### Consequences - -**Positive.** The engine is loadable by a compiler host unchanged. Its tests need no project on -disk. Mutation measurement can be aimed where it pays. - -**Negative.** Two projects and two test suites for one verb. The engine is written against the -downlevel framework, so modern convenience APIs are unavailable to it. - -**Risks.** The Roslyn floor pin can drift if the engine's package reference is allowed to float, -and the resulting load failure is silent. Mitigated by pinning to the same floor property the -analyzer package uses (§13.2). - -#### Follow-up Actions - -* If a code refactoring is ever built, the engine will need publishing as its own package (§16). - -#### References - -* §10, §12, §13.2, §13.5, §16 of this specification. - ---- - -### A library follow-up, not a decision record - -`Any.Fixed(value)` — an `IAny` returning a constant — would let the emitter drop the nested -`FixedValue` helper of §4.2. `Any.OneOf(value)` almost fills the role but rejects `null` -and consumes a draw (§14.5). This is an addition to the library's public API rather than a decision -about the tool, so it belongs to the library's own decision base and is **not** required for v1.0. - ---- - -## 16. Reserved for v1.1+ - -v1.0 must not paint these into a corner; §11.3 is the constraint that keeps the first one cheap. - -**Naming.** `AnyOrder` → `OrderFactory`, or any other pattern. Shape: - -```console -dum generate Order --name OrderFactory # this type only -dum generate Order --pattern "{Type}Factory" # this run -``` - -plus an optional `dum.json` at the project root for a project-wide default: - -```json -{ "naming": { "pattern": "Any{Type}" } } -``` - -`{Type}` is the only placeholder. The default pattern stays `Any{Type}`, so an existing project -sees no change. This is also the answer to the shadowing warning of §7. - -**Reading regex guards.** Left out of §5.3 for v1.0 because the library generates from the regular -subset of the pattern language only, and an unsupported pattern throws at construction — which -would make the whole emitted type unusable. Reaching for it needs the subset question answered -first: either the engine validates a pattern without referencing the library (which D9 forbids -today), or the library offers a way to ask. - -**Other deferred items.** `--all`; `init` / `required` members and property-only construction; -`AnyContext` support (D7); an `--ctor` selector when several constructors compete; extending §5.3 -to a `Guard.Against`-style helper library; publishing `JustDummies.GenAny` as its own package once -an IDE consumer exists; the IDE code refactoring itself. - -Deliberately **not** deferred — dropped: a `check` verb, a source-generator mode, and any form of -regeneration or drift detection. D1 removes the problem they would solve. - ---- - -## 17. Verification - -### 17.1 What was checked - -The emitted file of §4.1 was written out by hand exactly as specified — including the `int?` -parameter, the `FixedValue` helper and the composed `AnyCustomer` — then compiled and run -against `JustDummies.dll` built from source (`net8.0` asset), with the JustDummies analyzers wired -in. The results below are what the harness printed. - -| Claim | Where | Result | -|---|---|---| -| The specified skeleton compiles as written | §4.1 | compiles, 0 warnings | -| `.WithX` chaining works and does not disturb a shared base | D2, §4.2 | two `.WithStatus` calls off one base stay independent | -| `AnyOrder` is accepted by the library's composition seams | D2, §15 | `Any.ListOf`, `Any.PairOf` and `.As` all accept it | -| `.WithX(IAny)` keeps constrained composition open | §4.2 | `.WithReference(Any.String().StartingWith("ORD-").As(...))` yields `ORD-x9vDEd2` | -| A recipe built **outside** a scope still replays inside it | §8.2, §14.5 | two `Any.Reproducibly(20260730, …)` runs produced identical values | -| The guard-derived chain never throws | §5.3 | 500 draws through `OrderReference.Create`, no `AnyGenerationException` | -| The chain **without** guard reading throws intermittently | §5.3 | **594 / 10 000** draws threw, and **557 / 10 000** on a re-run against a later library — about 1 in 17, matching the 588 predicted by seventeen equiprobable lengths | -| Collection covariance needs no adapter | §5.2, §14.5 | `Any.ListOf(...)` assigned to `IAny>` | -| A value-type nullable **does** need the `.As` hop | §5.2 | `IAny` is not an `IAny`; `.As(value => (int?)value)` compiles | -| Complementary bounds compose | §5.3 | `.GreaterThanOrEqualTo(0).LessThanOrEqualTo(100)` and `.NonEmpty().WithMaxLength(10)` both draw | -| Contradictory bounds are rejected twice over | §5.3 | `ConflictingAnyConstraintException` at run time, and `JD023` at **compile** time | -| A pattern generator admits no other string constraint | §5.3 | `Any.StringMatching(...).NonEmpty()` fails to compile — `CS1061`, `AnyPattern` has only `DifferentFrom`/`Except` | -| Realistic validation regexes fall outside the supported subset | §5.3 | 4 of 5 rejected: lookahead, word boundary, backreference, Unicode category | -| An unsupported pattern throws at **construction**, not at `Generate()` | §5.3 | so the emitted parameterless constructor would throw before any `With…` could override it | -| Collection generators carry no length constraint | §5.3 | `AnyList` exposes `WithCount`, `WithCountBetween`, `WithMinCount`, `WithMaxCount` — no `WithLength` | -| **Every row of §5.2 compiles** | §5.2 | 40 declarations, each assigning the emitted expression to the parameter's own `IAny` — 0 errors, 0 warnings, nullable on, warnings-as-errors | -| **Every row of §5.2 keeps its promise** | §5.2 | 3 000 draws per scalar row: `NonEmpty` never empty, `Guid` never `Empty`, `Enum` only declared members, `Uri().Web()` absolute http(s) | -| **Every guard mapping of §5.3 is sound** | §5.3 | 17 mappings × 4 000 draws: every value drawn is one the original guard would accept | -| **Every §14 fact re-derived against a later library** | §14 | 29 upstream commits later — reworked exceptions, refactored regex parser — the counts, the analyzer inventory and the regex subset all still hold | -| The record, static-factory and odd-name shapes work | §4.2, §5.1 | positional record, a type with only a private constructor plus `Create`, and `_id` / `@class` parameters all compile and generate | -| A zero-parameter constructor breaks the standard shape | §4.2 | emitting both constructors gives them one signature — `CS0111` | -| A generic library name cannot be shadowed | §7 | a scaffolded `AnySet` and `JustDummies.AnySet` coexist; arity is part of the identity | -| A non-generic one is | §7 | `AnyPattern` in the target's namespace resolves to the scaffolded type, not the library's | -| `ref` / `out` constructor parameters break the call site | §5.1 | `CS1620`; `in` binds a value argument without complaint | -| `FixedValue` accepts what `Any.OneOf` refuses | §4.2 | `FixedValue(null)` yields null; `Any.OneOf(null)` throws `ArgumentException` | -| `.Positive()` is unsound for a `p < 1` guard on a decimal | §5.3 | 1 draw in 5 000 fell below 1 unconstrained; ~1 in 5 once another bound narrows the range | -| The scaffolded output raises no JD diagnostic | D3, §12 | 0 diagnostics on the emitted files | -| The analyzers were genuinely loaded | D3 | a control file raised `JD006` and `JD005` in the same build | -| `` silences them | D3, §15 | the same control file, so marked, raised **0** — including the `JD005` error | - -### 17.2 How to re-run it - -Nothing about the harness is exotic; it is worth recreating whenever the library moves or its -version changes. - -1. Build the library and the analyzers in `Release` (`net8.0` leg for the library). -2. Create a throwaway `net8.0` console project **outside** the repository, so no repository-wide - build properties apply. Reference the built `JustDummies.dll` with a `` / - ``, and the built analyzer with - ``. -3. Add the domain of §4.1 (`Order`, `OrderReference` with its guarding `Create`, `Customer`, - `OrderStatus`) and the scaffolded `AnyOrder.cs` / `AnyCustomer.cs` exactly as §4.1 specifies. -4. Add a **control file** with two known violations — a discarded constraint - (`Any.String().NonEmpty();` as a statement, `JD006`) and a generator in an interpolated string - (`$"{Any.Int32()}"`, `JD005`). Build, and confirm **both fire**. Without this step, "no - diagnostics on the scaffolded file" is indistinguishable from "the analyzer never loaded" — a - trap this verification fell into on the first attempt. -5. Prepend `// ` to that same control file and rebuild: both diagnostics vanish - and the build succeeds. That is D3's evidence. -6. Run the assertions of §17.1. For the measurement, loop - `Any.String().As(OrderReference.Create).Generate()` 10 000 times, counting - `AnyGenerationException`. - -A note on running: if only a newer .NET runtime is installed, the `net8.0` output still runs under -`DOTNET_ROLL_FORWARD=LatestMajor`. diff --git a/doc/handwritten/for-maintainers/workflows/README.fr.md b/doc/handwritten/for-maintainers/workflows/README.fr.md index d5fdf953..955a143c 100644 --- a/doc/handwritten/for-maintainers/workflows/README.fr.md +++ b/doc/handwritten/for-maintainers/workflows/README.fr.md @@ -41,8 +41,8 @@ documentées une seule fois ici plutôt que répétées sur chaque page. pour satisfaire un seul job. À l'inverse, un job qui n'a besoin de *rien* fait le contraire : il déclare `permissions: {}` — le mapping vide explicite, car un `permissions:` nu est un null et non un mapping vide — pour que le plancher hérité - ne l'atteigne pas. Les jobs `gate` consultatifs de `mutation` et - `justdummies-mutation` sont ce cas : ils ne récupèrent rien et n'appellent aucune API. + ne l'atteigne pas. Le job `gate` consultatif de `mutation` est ce cas : il ne + récupère rien et n'appelle aucune API. - **Chaque job fixe `timeout-minutes`.** Le défaut GitHub est de six heures ; une étape bloquée retiendrait sinon un runner tout ce temps. Chaque plafond est fixé à quelques fois le temps observé, noté en commentaire à côté. @@ -73,7 +73,6 @@ documentées une seule fois ici plutôt que répétées sur chaque page. | [`ci`](ci.fr.md) | Construit et teste toute la solution sous Linux et Windows, avec couverture. Le barrage principal. | | [`sonar`](sonar.fr.md) | Analyse SonarQube Cloud — quality gate et remontée de couverture. | | [`mutation`](mutation.fr.md) | Tests de mutation des bibliothèques et de l'outillage FirstClassErrors avec Stryker.NET — check obligatoire sur ce qu'une PR modifie, plus un balayage complet hebdomadaire. | -| [`justdummies-mutation`](justdummies-mutation.fr.md) | Idem pour les packages JustDummies, avec son propre check obligatoire — séparé pour que la future séparation de dépôt soit un déplacement de fichier. | | [`analyzers`](analyzers.fr.md) | Dogfood des analyzers Roslyn embarqués, y compris sur le plus vieux compilateur supporté (le floor Roslyn). | | [`commit-lint`](commit-lint.fr.md) | Impose la convention Conventional Commits sur chaque commit de PR, via le même script que le hook local. | | [`lint`](lint.fr.md) | shellcheck et actionlint sur les fichiers que le compilateur C# ne voit jamais — les scripts POSIX et les définitions de workflow. Zéro constat, `info` compris. | diff --git a/doc/handwritten/for-maintainers/workflows/README.md b/doc/handwritten/for-maintainers/workflows/README.md index e7723b35..83d8ebeb 100644 --- a/doc/handwritten/for-maintainers/workflows/README.md +++ b/doc/handwritten/for-maintainers/workflows/README.md @@ -39,8 +39,8 @@ here instead of being repeated on every page. top-level block to satisfy one job. A job that needs *nothing* does the reverse: it declares `permissions: {}` — the explicit empty mapping, since a bare `permissions:` is a null and not an empty map — so the inherited floor does not - reach it. The advisory `gate` jobs of `mutation` and `justdummies-mutation` are - that case: they check nothing out and call no API. + reach it. The advisory `gate` job of `mutation` is that case: it checks nothing + out and calls no API. - **Every job sets `timeout-minutes`.** The GitHub default is six hours; a hung step would otherwise hold a runner for that long. Each cap is set a few times the observed run time, noted in a comment next to it. @@ -69,7 +69,6 @@ here instead of being repeated on every page. | [`ci`](ci.en.md) | Build and test the whole solution on Linux and Windows, with coverage. The primary gate. | | [`sonar`](sonar.en.md) | SonarQube Cloud analysis — quality gate and coverage reporting. | | [`mutation`](mutation.en.md) | Mutation testing of the FirstClassErrors libraries and tooling with Stryker.NET — a required check on what a PR changed, plus a weekly full sweep. | -| [`justdummies-mutation`](justdummies-mutation.en.md) | The same, for the JustDummies packages, with its own required check — kept separate so the future repository split is a file move. | | [`analyzers`](analyzers.en.md) | Dogfood the bundled Roslyn analyzers, including on the oldest supported compiler (the Roslyn floor). | | [`commit-lint`](commit-lint.en.md) | Enforce the Conventional Commits convention on every PR commit, using the same script as the local hook. | | [`lint`](lint.en.md) | shellcheck and actionlint over the files the C# compiler never sees — the POSIX scripts and the workflow definitions. Zero findings, `info` included. | diff --git a/doc/handwritten/for-maintainers/workflows/justdummies-mutation.en.md b/doc/handwritten/for-maintainers/workflows/justdummies-mutation.en.md deleted file mode 100644 index 750853f0..00000000 --- a/doc/handwritten/for-maintainers/workflows/justdummies-mutation.en.md +++ /dev/null @@ -1,164 +0,0 @@ -# `justdummies-mutation` workflow - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](justdummies-mutation.fr.md) - -> Maintainer documentation — part of the [workflow reference](README.md). -> Not part of the user documentation under `doc/`. - -**Workflow file:** [`.github/workflows/justdummies-mutation.yml`](../../../../.github/workflows/justdummies-mutation.yml) - -## What it is for - -Mutation testing for the **three JustDummies components** — `JustDummies`, its -xUnit v3 adapter `JustDummies.Xunit` ([ADR-0039](../adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md)), -and the analyzers that ship inside the package ([ADR-0044](../adr/0044-ship-justdummies-analyzers.md)). -On a pull request it mutates only the files the pull request changed, for the -adapter and the analyzers; the generator is measured by the weekly sweep alone -([ADR-0049](../adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md)). -What mutation testing *is*, and why this repository gates on it, is explained once -on the [`mutation`](mutation.en.md) page — this workflow is the same machine with -a different matrix. - -## Why it is a separate workflow - -`JustDummies` is a standalone, error-agnostic package that deliberately holds no -reference to `FirstClassErrors` ([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.md)), -and it is headed for a repository of its own. Splitting the mutation gate along -that future boundary now means the move is a **file move rather than an edit**: -nothing in this workflow names a FirstClassErrors project, and nothing in -[`mutation`](mutation.en.md) names a JustDummies one. - -It also gives JustDummies its **own check**, -**`JustDummies mutation gate`**, independent of the FirstClassErrors one. Two -checks, two bars that move independently — which is what two libraries at different -levels of test maturity need anyway. On pull requests both are **advisory** -([ADR-0046](../adr/0046-make-the-per-pull-request-mutation-gate-advisory.md)); the -enforced bar for each is its weekly full sweep. - -## When it runs - -- On every **pull request targeting `main`** — diff-scoped and **advisory**: it - reports the diff's score but never blocks the merge ([ADR-0046](../adr/0046-make-the-per-pull-request-mutation-gate-advisory.md)). -- **Weekly** on a schedule (Monday, 03:47 UTC) — the full sweep, the **enforced - bar**. The slot is offset from `mutation`'s so the two sweeps do not contend for - runners. -- On demand via **`workflow_dispatch`** — the full sweep. - -## How it runs - -Almost identically to [`mutation`](mutation.en.md), whose page documents the -mechanism in full: `changed` mutates the diff from the fork point, `gate` -collapses the matrix into one stable check name, `full` sweeps everything with the -threshold disabled. The per-component Stryker configurations are -[`build/stryker/justdummies.json`](../../../../build/stryker/justdummies.json), -[`build/stryker/justdummies-xunit.json`](../../../../build/stryker/justdummies-xunit.json) -and [`build/stryker/justdummies-analyzers.json`](../../../../build/stryker/justdummies-analyzers.json). - -**The one place the two workflows differ: the per-PR matrix is two legs, not -three.** The generator is swept weekly but is **not** mutated per pull request -([ADR-0049](../adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md)). -Because `--since` selects per changed **file** rather than per changed line, a -hundred-line diff touching one of the generator's larger sources pulls in that -whole file: measured at 844 mutants, still running after an hour, producing no -score at all. That is not a tuning gap — every lever Stryker exposes tops out -around −36 % where such a leg would need −95 %, sharding cannot go below one -file, and line-scoped `mutate` patterns select nothing. The adapter and the -analyzers are small, finish in about ninety seconds, and keep their leg. - -Two points from that page matter more here than anywhere else: - -- **`JustDummies` is the largest library in the repository** — a few thousand - mutants — so its full sweep is the longest job the repository runs. That is the - whole reason the gate is diff-scoped rather than a full sweep per pull request. -- **`"test-runner": "mtp"` and `"coverage-analysis": "off"` are not tuning knobs.** - With Stryker's default VSTest runner these suites score 0 % — every mutant - reported as survived, because the runner cannot activate a mutant in an xUnit v3 - test project. Read - [that section](mutation.en.md#two-settings-that-are-not-tuning-knobs) before - changing either. - -## `JustDummies` has no score threshold yet - -Every other library's bar was set from a measured full sweep of that library -([how and why](mutation.en.md#where-the-thresholds-come-from)). `JustDummies` was -not: it carries a few thousand mutants over a heavy suite, its full sweep runs -well past an hour, and **no score for it has been measured**. Rather than invent -a number, [`justdummies.json`](../../../../build/stryker/justdummies.json) sets -`break` to **0** — the score gate for this one library is off. - -That is deliberate and it is temporary. The leg still runs, still fails on a -broken build or a failing suite, and still lists its surviving mutants in the run -summary; what it does not yet do is refuse a pull request over a score. **The -first weekly sweep publishes the library-wide figure** — that is the run this -threshold is waiting on. Read it, and set `break` from it exactly as the other -libraries' bars were set. - -`JustDummies.Xunit` needs no such caveat: it is small enough that its bar came -from a full sweep like the rest, and it gates normally. - -The analyzers leg also ships with `break` at **0**, for a different reason: its -residual survivors are the analyzer-infrastructure and descriptor-string mutants -the FirstClassErrors analyzers carry too, so it reports rather than blocks -([ADR-0044](../adr/0044-ship-justdummies-analyzers.md)). - -## Permissions & security - -`contents: read` only. The workflow checks out, builds and runs tests; it stores -no secret and needs no write scope. - -## When JustDummies moves to its own repository - -Take, unchanged: - -- this workflow file, renamed to `mutation.yml` there (and its `name:` with it); -- [`build/stryker/justdummies.json`](../../../../build/stryker/justdummies.json), - [`build/stryker/justdummies-xunit.json`](../../../../build/stryker/justdummies-xunit.json) - and [`build/stryker/justdummies-analyzers.json`](../../../../build/stryker/justdummies-analyzers.json); -- [`.config/dotnet-tools.json`](../../../../.config/dotnet-tools.json) — the - Stryker pin; -- this page, plus the shared sections of [`mutation`](mutation.en.md) folded into - it, since the page it defers to will not exist over there. - -Then change exactly one thing: the **`solution`** field in the three -configurations, which still names `FirstClassErrors.sln`. The `project` and -`test-projects` paths are already repository-relative and unchanged by the move. - -On this side, delete this workflow, its configurations and this page, and drop -the `JustDummies mutation gate` entry from the branch protection. - -## Handle with care - -- **Keep this workflow and [`mutation`](mutation.en.md) in step.** They are - duplicated on purpose — that is what makes the split a file move — so a fix to - one is a fix to both until the split happens. -- Everything under - [*Handle with care* on the `mutation` page](mutation.en.md#handle-with-care) - applies here word for word: `fetch-depth: 0`, `--since` rejecting `HEAD`, - `if: always()` on `gate`, the pinned engine, where the thresholds live. - -## Running it locally - -```bash -dotnet tool restore -dotnet stryker --config-file build/stryker/justdummies.json -``` - -That is the full sweep of the largest library and it takes a while. To reproduce -what the gate does on a branch: - -```bash -dotnet stryker --config-file build/stryker/justdummies.json --since:$(git merge-base origin/main HEAD) -``` - -Reports land in `StrykerOutput/` (git-ignored); open `reports/mutation-report.html`. - -## Related - -- [`mutation`](mutation.en.md) — the same machine for the FirstClassErrors - libraries, and where the mechanism is documented in full. -- [`justdummies`](../../../../.github/workflows/justdummies.yml) *(no reference - page yet)* — the other JustDummies-scoped workflow: it proves the packaged - `netstandard2.0` and `net8.0` assets behave on their own runtimes. -- [ADR 0043 — Gate pull requests on the mutation score of what they - changed](../adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md) - — the decision both workflows implement. diff --git a/doc/handwritten/for-maintainers/workflows/justdummies-mutation.fr.md b/doc/handwritten/for-maintainers/workflows/justdummies-mutation.fr.md deleted file mode 100644 index 80ea2a04..00000000 --- a/doc/handwritten/for-maintainers/workflows/justdummies-mutation.fr.md +++ /dev/null @@ -1,183 +0,0 @@ -# Workflow `justdummies-mutation` - -🌍 🇬🇧 [English](justdummies-mutation.en.md) · 🇫🇷 Français (ce fichier) - -> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). -> Ne fait pas partie de la documentation utilisateur sous `doc/`. - -**Fichier du workflow :** [`.github/workflows/justdummies-mutation.yml`](../../../../.github/workflows/justdummies-mutation.yml) - -## À quoi il sert - -Les tests de mutation des **trois composants JustDummies** : `JustDummies`, son -adaptateur xUnit v3 `JustDummies.Xunit` -([ADR-0039](../adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.fr.md)), -et les analyseurs livrés dans le package -([ADR-0044](../adr/0044-ship-justdummies-analyzers.fr.md)). -Sur une pull request, il ne mute que les fichiers modifiés par celle-ci, pour -l'adaptateur et les analyseurs ; le générateur est mesuré par le seul balayage -hebdomadaire -([ADR-0049](../adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md)). -Ce que *sont* les tests de mutation, et pourquoi ce dépôt -en fait un barrage, est expliqué une seule fois sur la page -[`mutation`](mutation.fr.md) — ce workflow est la même machine avec une matrice -différente. - -## Pourquoi un workflow séparé - -`JustDummies` est un package autonome et agnostique des erreurs, qui ne référence -volontairement pas `FirstClassErrors` -([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.fr.md)), et il est -destiné à un dépôt à lui. Découper le barrage de mutation le long de cette -frontière future dès maintenant fait de la migration **un déplacement de fichier -plutôt qu'une réécriture** : rien dans ce workflow ne nomme un projet -FirstClassErrors, et rien dans [`mutation`](mutation.fr.md) ne nomme un projet -JustDummies. - -Cela donne aussi à JustDummies **son propre check**, -**`JustDummies mutation gate`**, indépendant de celui de FirstClassErrors. Deux -checks, deux barres qui évoluent séparément — ce dont deux bibliothèques de -maturité de test différente ont de toute façon besoin. Sur les pull requests, les -deux sont **consultatifs** -([ADR-0046](../adr/0046-make-the-per-pull-request-mutation-gate-advisory.md)) ; le -niveau imposé de chacun est son balayage complet hebdomadaire. - -## Quand il s'exécute - -- Sur chaque **pull request ciblant `main`** — cantonné au diff et **consultatif** : - il rapporte le score du diff mais ne bloque jamais le merge - ([ADR-0046](../adr/0046-make-the-per-pull-request-mutation-gate-advisory.md)). -- **Chaque semaine** sur planification (lundi, 03h47 UTC) — le balayage complet, le - **niveau imposé**. Le créneau est décalé de celui de `mutation` pour que les deux - balayages ne se disputent pas les runners. -- À la demande via **`workflow_dispatch`** — le balayage complet. - -## Comment il s'exécute - -Presque à l'identique de [`mutation`](mutation.fr.md), dont la page documente le -mécanisme en entier : `changed` mute le diff depuis le point de fourche, `gate` -regroupe la matrice sous un nom de check stable, `full` balaie tout avec le seuil -désactivé. Les configurations Stryker sont -[`build/stryker/justdummies.json`](../../../../build/stryker/justdummies.json), -[`build/stryker/justdummies-xunit.json`](../../../../build/stryker/justdummies-xunit.json) -et [`build/stryker/justdummies-analyzers.json`](../../../../build/stryker/justdummies-analyzers.json). - -**Le seul point où les deux workflows diffèrent : la matrice par PR compte deux -pattes, pas trois.** Le générateur est balayé chaque semaine mais **n'est pas** -muté par pull request -([ADR-0049](../adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md)). -Comme `--since` sélectionne par **fichier** changé et non par ligne changée, un -diff d'une centaine de lignes touchant l'une des grosses sources du générateur -entraîne ce fichier entier : mesuré à 844 mutants, encore en cours après une -heure, sans produire aucun score. Ce n'est pas un défaut de réglage — chaque -levier exposé par Stryker plafonne vers −36 % là où une telle patte aurait besoin -de −95 %, le sharding ne peut pas descendre sous un fichier, et les motifs -`mutate` limités aux lignes ne sélectionnent rien. L'adaptateur et les analyseurs -sont petits, terminent en quatre-vingt-dix secondes environ, et gardent leur -patte. - -Deux points de cette page comptent ici plus qu'ailleurs : - -- **`JustDummies` est la plus grosse bibliothèque du dépôt** — quelques milliers - de mutants — et son balayage complet est donc le job le plus long que le dépôt - exécute. C'est toute la raison pour laquelle le barrage est cantonné au diff - plutôt qu'un balayage complet par pull request. -- **`"test-runner": "mtp"` et `"coverage-analysis": "off"` ne sont pas des - réglages de confort.** Avec le runner VSTest par défaut de Stryker, ces suites - scorent 0 % — tous les mutants rapportés survivants, parce que le runner ne sait - pas activer un mutant dans un projet de tests xUnit v3. Lisez - [cette section](mutation.fr.md#deux-réglages-qui-nen-sont-pas) avant de toucher - à l'un ou à l'autre. - -## `JustDummies` n'a pas encore de seuil de score - -La barre de chaque autre bibliothèque a été fixée à partir d'un balayage complet -mesuré sur cette bibliothèque -([comment et pourquoi](mutation.fr.md#doù-viennent-les-seuils)). Pas celle de -`JustDummies` : elle porte quelques milliers de mutants sur une suite lourde, son -balayage complet dépasse largement l'heure, et **aucun score n'a été mesuré pour -elle**. Plutôt que d'inventer un chiffre, -[`justdummies.json`](../../../../build/stryker/justdummies.json) met `break` à -**0** — le barrage sur le score est coupé pour cette seule bibliothèque. - -C'est délibéré et c'est temporaire. La branche s'exécute toujours, échoue toujours -sur un build cassé ou une suite en échec, et liste toujours ses mutants -survivants dans le résumé du run ; ce qu'elle ne fait pas encore, c'est refuser -une pull request sur un score. **Le premier balayage hebdomadaire publie le -chiffre sur toute la bibliothèque** — c'est ce run que ce seuil attend. -Lisez-le, et fixez `break` à partir de là, exactement comme les barres des autres -bibliothèques l'ont été. - -`JustDummies.Xunit` n'appelle pas cette réserve : elle est assez petite pour que -sa barre vienne d'un balayage complet comme les autres, et elle barre normalement. - -La branche des analyseurs part elle aussi avec `break` à **0**, pour une autre -raison : ses survivants résiduels sont les mutants d'infrastructure d'analyseur et -de chaînes de descripteurs que portent aussi les analyseurs FirstClassErrors — -elle rapporte donc au lieu de bloquer -([ADR-0044](../adr/0044-ship-justdummies-analyzers.fr.md)). - -## Permissions & sécurité - -`contents: read` seulement. Le workflow fait un checkout, un build et lance des -tests ; il ne stocke aucun secret et n'a besoin d'aucun périmètre en écriture. - -## Quand JustDummies partira dans son propre dépôt - -À emporter tel quel : - -- ce fichier de workflow, renommé `mutation.yml` là-bas (et son `name:` avec) ; -- [`build/stryker/justdummies.json`](../../../../build/stryker/justdummies.json), - [`build/stryker/justdummies-xunit.json`](../../../../build/stryker/justdummies-xunit.json) - et [`build/stryker/justdummies-analyzers.json`](../../../../build/stryker/justdummies-analyzers.json) ; -- [`.config/dotnet-tools.json`](../../../../.config/dotnet-tools.json) — - l'épinglage de Stryker ; -- cette page, augmentée des sections partagées de [`mutation`](mutation.fr.md) - repliées dedans, puisque la page à laquelle elle renvoie n'existera pas là-bas. - -Puis changer exactement une chose : le champ **`solution`** des trois -configurations, qui nomme encore `FirstClassErrors.sln`. Les chemins `project` et -`test-projects` sont déjà relatifs au dépôt et inchangés par la migration. - -De ce côté-ci, supprimer ce workflow, ses configurations et cette page, et -retirer l'entrée `JustDummies mutation gate` de la protection de branche. - -## À manipuler avec précaution - -- **Gardez ce workflow et [`mutation`](mutation.fr.md) synchronisés.** Ils sont - dupliqués à dessein — c'est ce qui fait de la migration un déplacement de - fichier —, donc un correctif sur l'un est un correctif sur l'autre tant que la - séparation n'a pas eu lieu. -- Tout ce qui figure sous - [*À manipuler avec précaution* sur la page `mutation`](mutation.fr.md#à-manipuler-avec-précaution) - vaut ici mot pour mot : `fetch-depth: 0`, `--since` qui refuse `HEAD`, - `if: always()` sur `gate`, le moteur épinglé, l'endroit où vivent les seuils. - -## L'exécuter en local - -```bash -dotnet tool restore -dotnet stryker --config-file build/stryker/justdummies.json -``` - -C'est le balayage complet de la plus grosse bibliothèque, et cela prend un -moment. Pour reproduire ce que fait le barrage sur une branche : - -```bash -dotnet stryker --config-file build/stryker/justdummies.json --since:$(git merge-base origin/main HEAD) -``` - -Les rapports atterrissent dans `StrykerOutput/` (ignoré par git) ; ouvrez -`reports/mutation-report.html`. - -## Voir aussi - -- [`mutation`](mutation.fr.md) — la même machine pour les bibliothèques - FirstClassErrors, et l'endroit où le mécanisme est documenté en entier. -- [`justdummies`](../../../../.github/workflows/justdummies.yml) *(pas encore de - page de référence)* — l'autre workflow cantonné à JustDummies : il prouve que - les assets `netstandard2.0` et `net8.0` publiés se comportent bien sur leurs - runtimes respectifs. -- [ADR 0043 — Gate pull requests on the mutation score of what they - changed](../adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.fr.md) - — la décision que les deux workflows mettent en œuvre. diff --git a/tools/changelog/collect-prs.sh b/tools/changelog/collect-prs.sh index cbf75cd0..a01287da 100755 --- a/tools/changelog/collect-prs.sh +++ b/tools/changelog/collect-prs.sh @@ -1,20 +1,19 @@ #!/bin/sh -# Collect the merged pull requests that belong to ONE release train (lib, cli or -# dum) and emit them as slim JSON on stdout, for the changelog drafter to summarise. +# Collect the merged pull requests that belong to ONE release train (lib or cli) +# and emit them as slim JSON on stdout, for the changelog drafter to summarise. # # The train partition mirrors tools/packaging/release-notes.sh EXACTLY — by the # Conventional Commit scope carried by a pull request's own commits, not by # labels or by the pull request title: # lib -> scopes core, analyzers, testing, binder (FirstClassErrors + .Testing + .RequestBinder) # cli -> scopes cli, gendoc (the fce tool: CLI + GenDoc + worker) -# dum -> scope justdummies (the standalone JustDummies library) # A pull request is kept when at least one of its commits carries a scope in the # train's set. Pull requests whose commits are all scopeless infrastructure # (bare `ci:` / `chore:` / `docs:` ...) belong to neither train and are dropped — # the same rule release-notes.sh applies to commits, so the human-facing changelog # and the generated GitHub Release notes describe the same set of changes. # -# Usage: tools/changelog/collect-prs.sh +# Usage: tools/changelog/collect-prs.sh # Reads the optional environment variable FROM_REF: the previous tag whose merge # time bounds the range. When empty, the train's latest tag is used; when there # is none either (the train's first release), the whole history is taken. diff --git a/tools/commit-lint/lint-commit-message.sh b/tools/commit-lint/lint-commit-message.sh index 889ea865..7dd28f5d 100755 --- a/tools/commit-lint/lint-commit-message.sh +++ b/tools/commit-lint/lint-commit-message.sh @@ -24,9 +24,9 @@ set -u TYPES='feat|fix|build|chore|ci|docs|perf|refactor|revert|style|test' -SCOPES='core|analyzers|binder|cli|justdummies|gendoc|testing' +SCOPES='core|analyzers|binder|cli|gendoc|testing' TYPES_HUMAN='feat, fix, build, chore, ci, docs, perf, refactor, revert, style, test' -SCOPES_HUMAN='core, analyzers, binder, cli, justdummies, gendoc, testing' +SCOPES_HUMAN='core, analyzers, binder, cli, gendoc, testing' MAX=72 # --- options ------------------------------------------------------------------ diff --git a/tools/justdummies-check/JustDummiesCheck.csproj b/tools/justdummies-check/JustDummiesCheck.csproj deleted file mode 100644 index 29a5294a..00000000 --- a/tools/justdummies-check/JustDummiesCheck.csproj +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - net8.0;net6.0 - Exe - enable - enable - false - - - false - false - - false - - false - - - - - 1.0.0-justdummiescheck.dev - - $(MSBuildThisFileDirectory)packages - - $(DefaultItemExcludes);packages/** - - - - - - - - - - diff --git a/tools/justdummies-check/Program.cs b/tools/justdummies-check/Program.cs deleted file mode 100644 index c14b6749..00000000 --- a/tools/justdummies-check/Program.cs +++ /dev/null @@ -1,209 +0,0 @@ -#region Usings declarations - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text.RegularExpressions; - -using JustDummies; - -#endregion - -namespace JustDummiesCheck; - -// Packaged-asset compatibility consumer. Run once per consumer TFM by .github/workflows/justdummies.yml. -// The consumer's compile target dictates which packaged asset NuGet restored, and therefore what this -// program must observe: -// net8.0 consumer -> lib/net8.0 asset -> modern generators present -// net6.0 consumer -> lib/netstandard2.0 asset -> modern generators absent -// Any mismatch is a regression in packaging or conditional compilation; the program prints the offending -// asset moniker and exits non-zero so the workflow step turns red against the right asset. -internal static class Program { - -#if NET8_0_OR_GREATER - private const bool ExpectModernTypes = true; - private const string ConsumerTfm = "net8.0"; - private const string ExpectedFamily = ".NETCoreApp"; -#else - private const bool ExpectModernTypes = false; - private const string ConsumerTfm = "net6.0"; - private const string ExpectedFamily = ".NETStandard"; -#endif - - // The net8.0-only generators, guarded by #if NET8_0_OR_GREATER in JustDummies (Any.cs). Present on the net8.0 - // asset, absent on the netstandard2.0 asset — the exact conditional surface the acceptance criteria name. - private static readonly string[] ModernEntryPoints = { "DateOnly", "TimeOnly", "Int128", "UInt128", "Half" }; - - // Fixed seed for the cross-TFM golden sequence. SeedBatch draws from the COMMON surface under this seed, and - // Main prints the result as the SEEDBATCH banner. justdummies.yml compares that banner byte-for-byte between the - // net8.0 and netstandard2.0 legs: new Random(seed) keeps the legacy algorithm on modern .NET, so the two - // packaged assets SHOULD agree seed-for-seed — but nothing else asserts it, and Random reserves the right to - // differ across framework versions. This turns that silent assumption into a checked contract (issue #215). - private const int CrossTfmSeed = 20260719; - - private static int Main() { - Assembly dummies = typeof(Any).Assembly; - string assetMoniker = dummies.GetCustomAttribute()?.FrameworkName ?? "(none)"; - - // Machine-readable banner the workflow greps to prove which asset actually loaded — a program that - // silently did nothing would otherwise exit 0. RESULT= is emitted last, once the checks have run. - Console.WriteLine($"CONSUMER_TFM={ConsumerTfm}"); - Console.WriteLine($"ASSET={assetMoniker}"); - Console.WriteLine($"RUNTIME={RuntimeInformation.FrameworkDescription}"); - - // The seeded common-surface batch for THIS asset, on one grep-safe line (every draw renders to printable - // ASCII). justdummies.yml diffs it against the other leg's banner to prove cross-asset seed equality. Emitted - // here with the other identifying banners, before the checks run; a leg that no-oped would omit it. - Console.WriteLine($"SEEDBATCH={SeedBatch(Any.WithSeed(CrossTfmSeed))}"); - - List failures = new(); - - // 1. The restored asset is the one this consumer TFM is meant to force. - bool assetIsNetStandard = assetMoniker.IndexOf("NETStandard", StringComparison.OrdinalIgnoreCase) >= 0; - if (assetIsNetStandard == ExpectModernTypes) { - failures.Add($"wrong asset: consumer {ConsumerTfm} loaded '{assetMoniker}', expected a {ExpectedFamily} asset"); - } - - // 2. The conditional net8.0-only surface is present exactly on the net8.0 asset and absent otherwise. - foreach (string name in ModernEntryPoints) { - bool present = typeof(Any).GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: Type.EmptyTypes, modifiers: null) is not null; - if (present != ExpectModernTypes) { - failures.Add($"Any.{name}() is {(present ? "present" : "absent")} on '{assetMoniker}', expected {(ExpectModernTypes ? "present" : "absent")}"); - } - } - - // 3. Smoke: the common public surface actually works when consumed from the package. - RunSmoke(failures); - - if (failures.Count == 0) { - Console.WriteLine($"RESULT=PASS asset={assetMoniker}"); - - return 0; - } - - Console.Error.WriteLine($"RESULT=FAIL asset={assetMoniker} failures={failures.Count}"); - foreach (string failure in failures) { - Console.Error.WriteLine($" - [asset={assetMoniker}] {failure}"); - } - - return 1; - } - - private static void RunSmoke(List failures) { - // Scalars + constraints. - int roll = Any.Int32().Between(1, 6).Generate(); - Require(failures, roll is >= 1 and <= 6, $"Int32().Between(1,6) produced {roll}"); - - int positive = Any.Int32().Positive().Generate(); - Require(failures, positive > 0, $"Int32().Positive() produced {positive}"); - - string capped = Any.String().NonEmpty().WithMaxLength(50).Generate(); - Require(failures, capped.Length is >= 1 and <= 50, $"String().NonEmpty().WithMaxLength(50) produced length {capped.Length}"); - - double real = Any.Double().Between(0d, 1000d).Generate(); - Require(failures, real is >= 0d and <= 1000d, $"Double().Between(0,1000) produced {real.ToString("R", CultureInfo.InvariantCulture)}"); - - // A contradiction in the Arrange must fail fast at declaration time (part of the library's contract): - // the prefix alone requires 4 characters, so WithLength(3) cannot be satisfied. - bool threw = false; - try { Any.String().WithLength(3).StartingWith("ORD-"); } catch (ConflictingAnyConstraintException) { threw = true; } - Require(failures, threw, "a contradictory String constraint did not throw ConflictingAnyConstraintException"); - - // Composition through a factory (.As). - string composed = Any.Int32().Between(1, 999).As(n => "ID-" + n.ToString(CultureInfo.InvariantCulture)).Generate(); - Require(failures, composed.StartsWith("ID-", StringComparison.Ordinal), $"As(...) produced '{composed}'"); - - // Collections. - List list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4).Generate(); - Require(failures, list.Count == 4 && list.All(value => value is >= 0 and <= 9), $"ListOf(...).WithCount(4) produced [{string.Join(",", list)}]"); - - HashSet set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3).Generate(); - Require(failures, set.Count == 3, $"SetOf(...).WithCount(3) produced {set.Count} elements"); - - // issue #215: exercise the common generators the packaged-asset guard never touched, so a break on either - // asset (a packaging or conditional-compilation regression) surfaces here — OrNull, array/sequence, - // pair/triple, StringMatching and enum draws. These also ride the SEEDBATCH cross-asset comparison below. - int? maybeDiscount = Any.Int32().Between(0, 100).OrNull().Generate(); - Require(failures, maybeDiscount is null or (>= 0 and <= 100), $"Int32().Between(0,100).OrNull() produced {maybeDiscount}"); - - int[] trio = Any.ArrayOf(Any.Int32().Between(0, 9)).WithCount(3).Generate(); - Require(failures, trio.Length == 3 && trio.All(value => value is >= 0 and <= 9), $"ArrayOf(...).WithCount(3) produced [{string.Join(",", trio)}]"); - - List couple = Any.SequenceOf(Any.Int32().Between(0, 9)).WithCount(2).Generate().ToList(); - Require(failures, couple.Count == 2 && couple.All(value => value is >= 0 and <= 9), $"SequenceOf(...).WithCount(2) produced {couple.Count} elements"); - - (int, string) pair = Any.PairOf(Any.Int32().Between(1, 9), Any.String().NonEmpty().WithMaxLength(4)).Generate(); - Require(failures, pair.Item1 is >= 1 and <= 9 && pair.Item2.Length is >= 1 and <= 4, $"PairOf(...) produced ({pair.Item1},{pair.Item2})"); - - (bool, int, char) triple = Any.TripleOf(Any.Boolean(), Any.Int32().Between(0, 9), Any.Char()).Generate(); - Require(failures, triple.Item2 is >= 0 and <= 9, $"TripleOf(...) produced ({triple.Item1},{triple.Item2},{triple.Item3})"); - - string code = Any.StringMatching("[A-Z]{3}-[0-9]{4}").Generate(); - Require(failures, Regex.IsMatch(code, "^[A-Z]{3}-[0-9]{4}$"), $"StringMatching('[A-Z]{{3}}-[0-9]{{4}}') produced '{code}'"); - - Suit suit = Any.Enum().Generate(); - Require(failures, System.Enum.IsDefined(typeof(Suit), suit), $"Enum() produced {suit}"); - - // Seeded reproducibility: two contexts with the same seed replay an identical mixed sequence, and a - // different seed diverges. This is the library's crown-jewel guarantee — verified here on each asset. - string first = SeedBatch(Any.WithSeed(CrossTfmSeed)); - string second = SeedBatch(Any.WithSeed(CrossTfmSeed)); - Require(failures, first == second, "same-seed contexts diverged"); - - string other = SeedBatch(Any.WithSeed(987654321)); - Require(failures, first != other, "different-seed contexts produced identical sequences"); - } - - // Draws a fixed mixed sequence from the COMMON surface only (no modern types), so it compiles and runs - // on both assets. Rendered with InvariantCulture to match the library's own culture-invariant rendering. - // Every part renders to printable ASCII (unconstrained Char/String draw ASCII letters and digits; the - // pattern and enum are ASCII by construction), so the joined line is safe to emit as a one-line banner. - private static string SeedBatch(AnyContext any) { - List parts = new() { - any.Int32().Generate().ToString(CultureInfo.InvariantCulture), - any.Int32().Between(1, 1000).Generate().ToString(CultureInfo.InvariantCulture), - any.String().NonEmpty().WithMaxLength(50).Generate(), - any.Int64().Generate().ToString(CultureInfo.InvariantCulture), - any.UInt64().Generate().ToString(CultureInfo.InvariantCulture), - any.Double().Between(0d, 1000d).Generate().ToString("R", CultureInfo.InvariantCulture), - any.Decimal().Between(0m, 1000m).Generate().ToString(CultureInfo.InvariantCulture), - any.Boolean().Generate().ToString(), - any.Guid().Generate().ToString(), - any.Char().Generate().ToString(), - any.TimeSpan().Generate().Ticks.ToString(CultureInfo.InvariantCulture), - any.DateTime().Generate().Ticks.ToString(CultureInfo.InvariantCulture) - }; - - // issue #215: broaden the compared batch beyond scalars — OrNull, array/sequence, pair/triple, - // StringMatching and enum draws. Collections and tuples inherit THIS seeded context through their - // operand (which carries the source), so every added draw is still reproducible and cross-asset stable. - parts.Add(any.Int32().Between(0, 100).OrNull().Generate() is int discount ? discount.ToString(CultureInfo.InvariantCulture) : "null"); - parts.Add(any.String().NonEmpty().WithMaxLength(8).OrNull().Generate() ?? "null"); - parts.Add(string.Join(",", Any.ArrayOf(any.Int32().Between(0, 9)).WithCount(3).Generate().Select(value => value.ToString(CultureInfo.InvariantCulture)))); - parts.Add(string.Join(",", Any.SequenceOf(any.Int32().Between(0, 9)).WithCount(2).Generate().Select(value => value.ToString(CultureInfo.InvariantCulture)))); - - (int, char) pair = Any.PairOf(any.Int32().Between(1, 9), any.Char()).Generate(); - parts.Add($"({pair.Item1.ToString(CultureInfo.InvariantCulture)},{pair.Item2})"); - - (bool, int, char) triple = Any.TripleOf(any.Boolean(), any.Int32().Between(0, 9), any.Char()).Generate(); - parts.Add($"({triple.Item1},{triple.Item2.ToString(CultureInfo.InvariantCulture)},{triple.Item3})"); - - parts.Add(any.StringMatching("[A-Z]{3}-[0-9]{4}").Generate()); - parts.Add(any.Enum().Generate().ToString()); - - return string.Join("|", parts); - } - - private static void Require(List failures, bool condition, string message) { - if (!condition) { failures.Add(message); } - } - - // A small closed enum for the enum-draw coverage (issue #215). Members render to stable, culture-independent - // names, keeping the enum part of the SEEDBATCH banner comparable across the two asset legs. - private enum Suit { Clubs, Diamonds, Hearts, Spades } - -} diff --git a/tools/justdummies-check/nuget.config b/tools/justdummies-check/nuget.config deleted file mode 100644 index 98880567..00000000 --- a/tools/justdummies-check/nuget.config +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/tools/packaging/pack.sh b/tools/packaging/pack.sh index 87834101..2f0c6467 100755 --- a/tools/packaging/pack.sh +++ b/tools/packaging/pack.sh @@ -10,19 +10,18 @@ # It assumes the solution has already been built in Release (it packs with # --no-build). It writes the .nupkg / .snupkg into ./artifacts. # -# Usage: tools/packaging/pack.sh +# Usage: tools/packaging/pack.sh # is any valid SemVer (a real release passes the tag version; the # dry run passes a throwaway like 0.0.0-dryrun). # selects which release train to pack, since the trains are versioned # and released independently: # lib -> FirstClassErrors + FirstClassErrors.Testing + FirstClassErrors.RequestBinder (lockstep) # cli -> FirstClassErrors.Cli (the `fce` .NET tool) -# dum -> JustDummies (the standalone arbitrary-test-value library) set -eu if [ "$#" -ne 2 ] || [ -z "$1" ] || [ -z "$2" ]; then - echo "usage: tools/packaging/pack.sh " >&2 + echo "usage: tools/packaging/pack.sh " >&2 exit 2 fi version="$1" @@ -46,16 +45,8 @@ case "$scope" in # package -- asserted after the pack, below). Released on its own cadence and version. projects='FirstClassErrors.Cli/FirstClassErrors.Cli.csproj' ;; - dum) - # JustDummies, the standalone arbitrary-test-value library, and its xUnit v3 companion. Deliberately - # independent of everything else in this repository (ADR-0011): neither references a FirstClassErrors - # project, so they release on their own train and their packages must declare no FirstClassErrors - # dependency -- asserted below. JustDummies.Xunit rides this train because it versions with the library it - # adapts (ADR-0036); if JustDummies ever moves to its own repository, that pairing is worth revisiting. - projects='JustDummies/JustDummies.csproj JustDummies.Xunit/JustDummies.Xunit.csproj' - ;; *) - echo "error: unknown scope '$scope' (expected 'lib', 'cli' or 'dum')" >&2 + echo "error: unknown scope '$scope' (expected 'lib' or 'cli')" >&2 exit 2 ;; esac @@ -98,23 +89,6 @@ EOF echo "ok: every lib-train package pins its FirstClassErrors dependency to the co-published $version" fi -# Standalone guard for the dum train. JustDummies' whole identity is that it depends on nothing (ADR-0011): -# an architecture test asserts it at build time, and this asserts it on the shipped artifact itself -- a -# FirstClassErrors dependency sneaking into the nuspec must fail the pack, not surface on nuget.org. -if [ "$scope" = "dum" ]; then - for package in artifacts/JustDummies.*.nupkg; do - # Fail CLOSED, like the cli guard: an unmatched glob or an unreadable nuspec must not pass as - # "standalone" -- read the nuspec first (unzip fails loudly on both), then reject any - # FirstClassErrors dependency found in it. - nuspec="$(unzip -p "$package" '*.nuspec')" || { echo "error: cannot read the nuspec from $package" >&2; exit 1; } - if printf '%s\n' "$nuspec" | grep -q ']*id="FirstClassErrors'; then - echo "error: $package declares a FirstClassErrors dependency; JustDummies is standalone (ADR-0011)" >&2 - exit 1 - fi - echo "ok: $package is standalone (no FirstClassErrors dependency)" - done -fi - # Positive proof that the fce tool ships its GenDoc worker. `fce generate` does not do the whole job # in-process: it spawns FirstClassErrors.GenDoc.Worker in a child process (dotnet exec) and resolves it # next to the installed executable (ResolveWorkerAssemblyPath -> AppContext.BaseDirectory). PackAsTool packs diff --git a/tools/packaging/release-notes.sh b/tools/packaging/release-notes.sh index 792c8b0c..e91a4e9f 100755 --- a/tools/packaging/release-notes.sh +++ b/tools/packaging/release-notes.sh @@ -1,15 +1,14 @@ #!/bin/sh -# Generate GitHub Release notes for ONE release train (lib, cli or dum), containing only the +# Generate GitHub Release notes for ONE release train (lib or cli), containing only the # commits that belong to that train — so a lib release never lists cli work, and vice versa. # # The partition is by Conventional Commit scope (enforced by tools/commit-lint): # lib -> scopes core, analyzers, testing, binder (FirstClassErrors + .Testing + .RequestBinder) # cli -> scopes cli, gendoc (the fce tool: CLI + GenDoc + worker) -# dum -> scope justdummies (the standalone JustDummies library) # Commits with no scope (bare `ci:`, `build:`, `chore:` ...) are infrastructure and are left out # of both trains: these notes describe what changed for the consumer of the package, nothing else. # -# Usage: tools/packaging/release-notes.sh [] +# Usage: tools/packaging/release-notes.sh [] # Emits Markdown on stdout. Needs full history + tags in the checkout (actions/checkout with # fetch-depth: 0) so the previous same-train tag — the lower bound of the range — resolves. # is the upper bound and defaults to ; pass the release commit when the tag does not exist @@ -18,7 +17,7 @@ set -eu if [ "$#" -lt 2 ] || [ "$#" -gt 3 ] || [ -z "$1" ] || [ -z "$2" ]; then - echo "usage: tools/packaging/release-notes.sh []" >&2 + echo "usage: tools/packaging/release-notes.sh []" >&2 exit 2 fi scope="$1" diff --git a/tools/trains.sh b/tools/trains.sh index d75b9b65..d90c06a0 100644 --- a/tools/trains.sh +++ b/tools/trains.sh @@ -24,7 +24,6 @@ trains_rows() { cat <<'ROWS' lib|lib-v|core,analyzers,testing,binder|CHANGELOG.md|FirstClassErrors, FirstClassErrors.Testing and FirstClassErrors.RequestBinder cli|cli-v|cli,gendoc|FirstClassErrors.Cli/CHANGELOG.md|FirstClassErrors.Cli (the fce .NET tool) -dum|dum-v|justdummies|JustDummies/CHANGELOG.md|JustDummies ROWS }