diff --git a/.backlog/completed/task-1 - Add-PR-template-and-issue-templates.md b/.backlog/completed/task-1 - Add-PR-template-and-issue-templates.md index 4d797366..3143dff2 100644 --- a/.backlog/completed/task-1 - Add-PR-template-and-issue-templates.md +++ b/.backlog/completed/task-1 - Add-PR-template-and-issue-templates.md @@ -19,10 +19,10 @@ priority: medium Create GitHub PR and issue templates to guide contributors: -- .github/pull_request_template.md — checklist covering tests, build, conventional commit format, and docs -- .github/ISSUE_TEMPLATE/1-bug-report.yml — structured bug report form (GitHub issue form, not markdown) -- .github/ISSUE_TEMPLATE/2-feature-request.yml — structured feature request form (GitHub issue form, not markdown) -- .github/ISSUE_TEMPLATE/config.yml — disables blank issues (`blank_issues_enabled: false`) +- .github/pull_request_template.md - checklist covering tests, build, conventional commit format, and docs +- .github/ISSUE_TEMPLATE/1-bug-report.yml - structured bug report form (GitHub issue form, not markdown) +- .github/ISSUE_TEMPLATE/2-feature-request.yml - structured feature request form (GitHub issue form, not markdown) +- .github/ISSUE_TEMPLATE/config.yml - disables blank issues (`blank_issues_enabled: false`) ## Acceptance Criteria @@ -38,8 +38,8 @@ Create GitHub PR and issue templates to guide contributors: Created all four GitHub community health files under `.github/`: -- `pull_request_template.md` — contributor checklist covering tests, build, conventional commit format, and docs -- `ISSUE_TEMPLATE/1-bug-report.yml` — structured GitHub issue form for bug reports -- `ISSUE_TEMPLATE/2-feature-request.yml` — structured GitHub issue form for feature requests -- `ISSUE_TEMPLATE/config.yml` — disables blank issues (`blank_issues_enabled: false`) +- `pull_request_template.md` - contributor checklist covering tests, build, conventional commit format, and docs +- `ISSUE_TEMPLATE/1-bug-report.yml` - structured GitHub issue form for bug reports +- `ISSUE_TEMPLATE/2-feature-request.yml` - structured GitHub issue form for feature requests +- `ISSUE_TEMPLATE/config.yml` - disables blank issues (`blank_issues_enabled: false`) diff --git a/.backlog/completed/task-10 - Ensure-dependabot-updates-NuGet-packages-across-all-projects-in-a-single-PR.md b/.backlog/completed/task-10 - Ensure-dependabot-updates-NuGet-packages-across-all-projects-in-a-single-PR.md index 30d88ea0..e1dd9af7 100644 --- a/.backlog/completed/task-10 - Ensure-dependabot-updates-NuGet-packages-across-all-projects-in-a-single-PR.md +++ b/.backlog/completed/task-10 - Ensure-dependabot-updates-NuGet-packages-across-all-projects-in-a-single-PR.md @@ -30,7 +30,7 @@ Root cause: `directories: ["**/*"]` creates one PR per matched directory per ung | coverlet.msbuild | 4 | 4 separate PRs | | Microsoft.SourceLink.GitHub | 4 | 4 separate PRs | -With Dependabot's default open-pull-requests-limit of 5, only some directories would receive a PR for a given package before the limit was reached — explaining why only AutoFakeItEasy.Tests was updated for Microsoft.NET.Test.Sdk 18.4.0. +With Dependabot's default open-pull-requests-limit of 5, only some directories would receive a PR for a given package before the limit was reached - explaining why only AutoFakeItEasy.Tests was updated for Microsoft.NET.Test.Sdk 18.4.0. Evidence from git log and closed PR history: @@ -42,7 +42,7 @@ Fix: Keep `directories: ["**/*"]` (discovery is working correctly) and add three - Testing: Microsoft.NET.Test.Sdk, coverlet.msbuild - Common: Castle.Core, JetBrains.Annotations, Microsoft.SourceLink.GitHub, Microsoft.NETFramework.ReferenceAssemblies -- Other: `*` catch-all — consolidates any package not matched by a named group into a single PR, guarding against future ungrouped shared packages +- Other: `*` catch-all - consolidates any package not matched by a named group into a single PR, guarding against future ungrouped shared packages ## Acceptance Criteria @@ -55,7 +55,7 @@ Fix: Keep `directories: ["**/*"]` (discovery is working correctly) and add three ## Implementation Plan -In .github/dependabot.yml, add three groups under the nuget ecosystem entry (order matters — Other must be last so named groups take priority): +In .github/dependabot.yml, add three groups under the nuget ecosystem entry (order matters - Other must be last so named groups take priority): Testing: patterns: @@ -79,7 +79,7 @@ Applied to `.github/dependabot.yml`. The `directories: ["**/*"]` setting was alr Added three new groups to the nuget ecosystem entry: -- **Testing** — `Microsoft.NET.Test.Sdk`, `coverlet.msbuild` -- **Common** — `Castle.Core`, `JetBrains.Annotations`, `Microsoft.SourceLink.GitHub`, `Microsoft.NETFramework.ReferenceAssemblies` -- **Other** — `*` catch-all placed last so named groups take priority; consolidates any future ungrouped shared package into a single PR automatically +- **Testing** - `Microsoft.NET.Test.Sdk`, `coverlet.msbuild` +- **Common** - `Castle.Core`, `JetBrains.Annotations`, `Microsoft.SourceLink.GitHub`, `Microsoft.NETFramework.ReferenceAssemblies` +- **Other** - `*` catch-all placed last so named groups take priority; consolidates any future ungrouped shared package into a single PR automatically diff --git a/.backlog/completed/task-11 - Fix-snyk-workflow.md b/.backlog/completed/task-11 - Fix-snyk-workflow.md index 62e8106b..4dbc0c6f 100644 --- a/.backlog/completed/task-11 - Fix-snyk-workflow.md +++ b/.backlog/completed/task-11 - Fix-snyk-workflow.md @@ -17,7 +17,7 @@ priority: medium Two issues need fixing in `.github/workflows/snyk.yml`: -### Issue 1 — Deprecated action +### Issue 1 - Deprecated action All three scan/monitor steps use `snyk/actions/dotnet@master`, which is officially deprecated and no longer supported by Snyk (no .NET-specific replacement exists). @@ -26,7 +26,7 @@ The recommended migration is `snyk/actions/setup@master` (installs the Snyk CLI combined with explicit `run: snyk ...` commands. Since the workflow already runs on `ubuntu-latest`, the Docker-based `setup` action works without any runner change. -### Issue 2 — Multiple SARIF runs under the same category +### Issue 2 - Multiple SARIF runs under the same category The single `upload-sarif` step points to the `snyk/` directory, which contains two SARIF files (`opensource.sarif` and `code.sarif`). GitHub Code Scanning no longer @@ -38,7 +38,7 @@ causing the workflow to fail with: **Fix:** replace the single directory upload with two steps, each pointing to a specific file with a distinct `category`. The `category` parameter creates an -independent slot in the GitHub Advanced Security dashboard — uploads coexist and +independent slot in the GitHub Advanced Security dashboard - uploads coexist and neither overwrites the other. diff --git a/.backlog/completed/task-13 - Upgrade-test-projects-from-net8-to-net10.md b/.backlog/completed/task-13 - Upgrade-test-projects-from-net8-to-net10.md index aa6e6aac..41c814a3 100644 --- a/.backlog/completed/task-13 - Upgrade-test-projects-from-net8-to-net10.md +++ b/.backlog/completed/task-13 - Upgrade-test-projects-from-net8-to-net10.md @@ -15,7 +15,7 @@ priority: low ## Description -Replace `net8.0` with `net10.0` in all four test projects. .NET 10 is LTS (supported until November 2028), making it the right long-term target. .NET 9 STS reaches end of support in May 2026 and is not worth targeting. .NET 8 LTS ends November 2026. Library projects already target `netstandard2.0`/`netstandard2.1` which covers all modern .NET versions — no change is needed there. +Replace `net8.0` with `net10.0` in all four test projects. .NET 10 is LTS (supported until November 2028), making it the right long-term target. .NET 9 STS reaches end of support in May 2026 and is not worth targeting. .NET 8 LTS ends November 2026. Library projects already target `netstandard2.0`/`netstandard2.1` which covers all modern .NET versions - no change is needed there. **Current state:** diff --git a/.backlog/completed/task-3 - Enforce-Conventional-Commits.md b/.backlog/completed/task-3 - Enforce-Conventional-Commits.md index 636e1574..ef63beb6 100644 --- a/.backlog/completed/task-3 - Enforce-Conventional-Commits.md +++ b/.backlog/completed/task-3 - Enforce-Conventional-Commits.md @@ -24,7 +24,7 @@ Add tooling to enforce the Conventional Commits specification at commit time. Th ## Acceptance Criteria - [x] #1 A git commit-msg hook is in place that validates the message against Conventional Commits format -- [x] #2 The commit is prevented when the message is non-conforming — not just warned +- [x] #2 The commit is prevented when the message is non-conforming - not just warned - [x] #3 Setup instructions are added to CONTRIBUTING.md so contributors activate the hooks after cloning @@ -53,8 +53,8 @@ Validated behavior locally: valid messages pass and invalid messages fail with n Enforced Conventional Commits using Husky.NET + CommitLint.Net via the repository-local .NET tool manifest (`dotnet-tools.json`). Key deliverables: -- `commit-message-config.json` — Conventional Commits rules with allowed types -- `.husky/commit-msg` + `.husky/task-runner.json` — local git hook that blocks non-conforming messages at commit time -- `.github/workflows/commit-message.yml` — CI mirror that validates the latest commit message using the same config, catching any hook bypasses -- `CONTRIBUTING.md` updated — single bootstrap (`dotnet tool restore` + `dotnet husky install`) activates all local tools and hooks after cloning +- `commit-message-config.json` - Conventional Commits rules with allowed types +- `.husky/commit-msg` + `.husky/task-runner.json` - local git hook that blocks non-conforming messages at commit time +- `.github/workflows/commit-message.yml` - CI mirror that validates the latest commit message using the same config, catching any hook bypasses +- `CONTRIBUTING.md` updated - single bootstrap (`dotnet tool restore` + `dotnet husky install`) activates all local tools and hooks after cloning diff --git a/.backlog/completed/task-4 - Add-AI-powered-PR-review.md b/.backlog/completed/task-4 - Add-AI-powered-PR-review.md index 740dfc34..743a93a1 100644 --- a/.backlog/completed/task-4 - Add-AI-powered-PR-review.md +++ b/.backlog/completed/task-4 - Add-AI-powered-PR-review.md @@ -30,14 +30,14 @@ Integrate an AI-powered PR review tool (e.g. CodeRabbit, Reviewpad, or similar) ## Final Summary -Selected and configured **CodeRabbit** — free for public OSS repos, no infrastructure to maintain. +Selected and configured **CodeRabbit** - free for public OSS repos, no infrastructure to maintain. Delivered `.coderabbit.yaml` at the repo root with: -- `auto_review.enabled: true` + `high_level_summary: true` — automatic PR summary comment on every PR (AC #1) -- `profile: "chill"` — non-blocking review style that complements rather than replaces human review -- `auto_apply_labels: true` — automatic PR labelling +- `auto_review.enabled: true` + `high_level_summary: true` - automatic PR summary comment on every PR (AC #1) +- `profile: "chill"` - non-blocking review style that complements rather than replaces human review +- `auto_apply_labels: true` - automatic PR labelling -AC #3 (OSS licensing) is satisfied — CodeRabbit is free for public repos. +AC #3 (OSS licensing) is satisfied - CodeRabbit is free for public repos. AC #2 (project conventions awareness) was intentionally skipped by the team; the tool autodiscovers `AGENTS.md` but no explicit `reviews.instructions` were added. diff --git a/.backlog/completed/task-5 - Add-SECURITY.md.md b/.backlog/completed/task-5 - Add-SECURITY.md.md index 376cc42c..6f2e6265 100644 --- a/.backlog/completed/task-5 - Add-SECURITY.md.md +++ b/.backlog/completed/task-5 - Add-SECURITY.md.md @@ -16,7 +16,7 @@ priority: low ## Description -Create a SECURITY.md file at the repository root that documents the vulnerability disclosure process — how to report a security issue privately, expected response time, and which versions receive security fixes. GitHub surfaces this file prominently and uses it to populate the 'Report a vulnerability' link. +Create a SECURITY.md file at the repository root that documents the vulnerability disclosure process - how to report a security issue privately, expected response time, and which versions receive security fixes. GitHub surfaces this file prominently and uses it to populate the 'Report a vulnerability' link. ## Acceptance Criteria diff --git a/.backlog/completed/task-6 - Add-CODE_OF_CONDUCT.md.md b/.backlog/completed/task-6 - Add-CODE_OF_CONDUCT.md.md index db1e033e..850f86ee 100644 --- a/.backlog/completed/task-6 - Add-CODE_OF_CONDUCT.md.md +++ b/.backlog/completed/task-6 - Add-CODE_OF_CONDUCT.md.md @@ -22,5 +22,5 @@ Add a CODE_OF_CONDUCT.md to the repository root. The [Contributor Covenant](http - [x] #1 CODE_OF_CONDUCT.md exists at the repository root - [x] #2 Based on a recognised standard (Contributor Covenant v2.1) -- [x] #3 Enforcement contact set to maintainer GitHub profile (@piotrzajac) — no email per project policy +- [x] #3 Enforcement contact set to maintainer GitHub profile (@piotrzajac) - no email per project policy diff --git a/.backlog/completed/task-8 - Fix-Dependabot-commit-messages-to-follow-Conventional-Commits.md b/.backlog/completed/task-8 - Fix-Dependabot-commit-messages-to-follow-Conventional-Commits.md index 817dced2..6e7af0a0 100644 --- a/.backlog/completed/task-8 - Fix-Dependabot-commit-messages-to-follow-Conventional-Commits.md +++ b/.backlog/completed/task-8 - Fix-Dependabot-commit-messages-to-follow-Conventional-Commits.md @@ -35,13 +35,13 @@ Added `commit-message` configuration to both entries in `.github/dependabot.yml` - NuGet entry: `prefix: "chore(nuget)"` - GitHub Actions entry: `prefix: "chore(github-actions)"` -`include: "scope"` was intentionally omitted — that option produces `deps`/`deps-dev` as the scope (dependency-type based), not the ecosystem name. Embedding the scope directly in `prefix` is the only way to produce ecosystem-specific scopes. +`include: "scope"` was intentionally omitted - that option produces `deps`/`deps-dev` as the scope (dependency-type based), not the ecosystem name. Embedding the scope directly in `prefix` is the only way to produce ecosystem-specific scopes. ## Implementation Notes -`commit-message.include: "scope"` in Dependabot produces `chore(deps):` or `chore(deps-dev):` based on the dependency type — it does NOT use the ecosystem name as the scope. To get `chore(nuget):` and `chore(github-actions):`, the scope must be baked directly into the `prefix` value and `include: "scope"` must be omitted. +`commit-message.include: "scope"` in Dependabot produces `chore(deps):` or `chore(deps-dev):` based on the dependency type - it does NOT use the ecosystem name as the scope. To get `chore(nuget):` and `chore(github-actions):`, the scope must be baked directly into the `prefix` value and `include: "scope"` must be omitted. ## Final Summary @@ -52,5 +52,5 @@ Updated `.github/dependabot.yml` to add `commit-message` configuration to both e - NuGet entry: `prefix: "chore(nuget)"` → produces `chore(nuget): bump from X to Y` - GitHub Actions entry: `prefix: "chore(github-actions)"` → produces `chore(github-actions): bump from X to Y` -`include: "scope"` was intentionally omitted — that option produces `deps`/`deps-dev` as scope (dependency-type based), not the ecosystem name. Embedding the scope in `prefix` is the only way to get ecosystem-specific scopes that satisfy the commit-message CI workflow introduced in TASK-3. +`include: "scope"` was intentionally omitted - that option produces `deps`/`deps-dev` as scope (dependency-type based), not the ecosystem name. Embedding the scope in `prefix` is the only way to get ecosystem-specific scopes that satisfy the commit-message CI workflow introduced in TASK-3. diff --git a/.backlog/completed/task-9 - Fix-test-mutations-workflow-to-use-dotnet-tool-restore.md b/.backlog/completed/task-9 - Fix-test-mutations-workflow-to-use-dotnet-tool-restore.md index a22b2261..9f0d8ce3 100644 --- a/.backlog/completed/task-9 - Fix-test-mutations-workflow-to-use-dotnet-tool-restore.md +++ b/.backlog/completed/task-9 - Fix-test-mutations-workflow-to-use-dotnet-tool-restore.md @@ -20,8 +20,8 @@ priority: medium TASK-3 added `dotnet-stryker` to the repository-level tool manifest (`dotnet-tools.json` at repo root) alongside Husky and CommitLint.Net. The `.github/workflows/test-mutations.yml` workflow was not updated accordingly and still contains a dedicated "install stryker.net" step that: -1. Runs `dotnet new tool-manifest` — creating a brand-new `.config/dotnet-tools.json` in the working directory, which conflicts with the repo-level manifest. -2. Runs `dotnet tool install --local dotnet-stryker` — redundantly re-installing a tool that is already declared in the repo manifest. +1. Runs `dotnet new tool-manifest` - creating a brand-new `.config/dotnet-tools.json` in the working directory, which conflicts with the repo-level manifest. +2. Runs `dotnet tool install --local dotnet-stryker` - redundantly re-installing a tool that is already declared in the repo manifest. This step must be replaced with `dotnet tool restore`, which restores all tools (husky, commitlint.net, dotnet-stryker) from the existing manifest in a single, consistent step. @@ -45,7 +45,7 @@ In `.github/workflows/test-mutations.yml`, replace the multi-line PowerShell " `dotnet tool restore` resolves the manifest by walking up the directory tree from the working directory, finding `dotnet-tools.json` at the repo root. No path argument is needed. -The old step used `dotnet new tool-manifest` which would create `.config/dotnet-tools.json` in the runner's working directory — a different path from the repo-root `dotnet-tools.json` — causing a manifest collision. +The old step used `dotnet new tool-manifest` which would create `.config/dotnet-tools.json` in the runner's working directory - a different path from the repo-root `dotnet-tools.json` - causing a manifest collision. ## Final Summary diff --git a/.backlog/decisions/decision-1 - Share-fixture-across-member-data-rows-by-default.md b/.backlog/decisions/decision-1 - Share-fixture-across-member-data-rows-by-default.md index 845e7902..d4017ecc 100644 --- a/.backlog/decisions/decision-1 - Share-fixture-across-member-data-rows-by-default.md +++ b/.backlog/decisions/decision-1 - Share-fixture-across-member-data-rows-by-default.md @@ -10,7 +10,7 @@ status: accepted ## Decision -Default `MemberAutoDataBaseAttribute.ShareFixture = true`. The same `IFixture` instance — with all its customizations and registered mock objects — is reused across every data row produced by the member. Users can opt out with `ShareFixture = false` when independent fixtures per row are required. +Default `MemberAutoDataBaseAttribute.ShareFixture = true`. The same `IFixture` instance - with all its customizations and registered mock objects - is reused across every data row produced by the member. Users can opt out with `ShareFixture = false` when independent fixtures per row are required. ## Consequences diff --git a/.backlog/decisions/decision-13 - Exclude-Moq-from-Dependabot-group-remaining-NuGet-updates.md b/.backlog/decisions/decision-13 - Exclude-Moq-from-Dependabot-group-remaining-NuGet-updates.md index 43567881..4162ace7 100644 --- a/.backlog/decisions/decision-13 - Exclude-Moq-from-Dependabot-group-remaining-NuGet-updates.md +++ b/.backlog/decisions/decision-13 - Exclude-Moq-from-Dependabot-group-remaining-NuGet-updates.md @@ -10,7 +10,7 @@ Moq introduced SponsorLink in a controversial update that embedded telemetry and ## Decision -Explicitly exclude `Moq` from Dependabot NuGet updates — it requires manual review and a deliberate upgrade decision. +Explicitly exclude `Moq` from Dependabot NuGet updates - it requires manual review and a deliberate upgrade decision. Group all other NuGet updates into logical Dependabot groups: `xUnit`, `AutoFixture`, `Analyzers`, `Testing`, `Common`, and `Other`. GitHub Actions dependencies are also grouped and updated weekly with a `chore(github-actions):` prefix. diff --git a/.backlog/decisions/decision-17 - Add-data-narrowing-parameter-attributes.md b/.backlog/decisions/decision-17 - Add-data-narrowing-parameter-attributes.md index 73fa5c4f..d002882d 100644 --- a/.backlog/decisions/decision-17 - Add-data-narrowing-parameter-attributes.md +++ b/.backlog/decisions/decision-17 - Add-data-narrowing-parameter-attributes.md @@ -12,10 +12,10 @@ AutoFixture generates arbitrary values for parameters. Tests for boundary condit Add four parameter-level attributes to Core, all implemented via `IParameterCustomizationSource`: -- `[Except(v1, v2)]` — generate values excluding the specified set -- `[PickFromValues(v1, v2)]` — pick randomly from a fixed set -- `[PickFromRange(min, max)]` — generate within a numeric range -- `[PickNegative]` — generate only negative numeric values +- `[Except(v1, v2)]` - generate values excluding the specified set +- `[PickFromValues(v1, v2)]` - pick randomly from a fixed set +- `[PickFromRange(min, max)]` - generate within a numeric range +- `[PickNegative]` - generate only negative numeric values All four are backed by new specimen builders (`RandomExceptValuesGenerator`, `RandomFixedValuesGenerator`) and request types (`ExceptValuesRequest`, `FixedValuesRequest`). diff --git a/.backlog/decisions/decision-18 - Integrate-Semgrep-for-SAST-scanning.md b/.backlog/decisions/decision-18 - Integrate-Semgrep-for-SAST-scanning.md index 1cb1f321..27cf22f5 100644 --- a/.backlog/decisions/decision-18 - Integrate-Semgrep-for-SAST-scanning.md +++ b/.backlog/decisions/decision-18 - Integrate-Semgrep-for-SAST-scanning.md @@ -15,5 +15,5 @@ Add Semgrep as a parallel SAST scanning step in CI alongside CodeQL. Use the def ## Consequences - Broader SAST coverage through two independent tools with different rule philosophies. -- Some overlap with CodeQL is intentional — independent tools reaching the same conclusion increases confidence. +- Some overlap with CodeQL is intentional - independent tools reaching the same conclusion increases confidence. - Semgrep findings are reviewed in CI alongside CodeQL results; both must pass before merge. diff --git a/.backlog/decisions/decision-2 - Decouple-xUnit-from-AutoFixture-via-provider-pattern.md b/.backlog/decisions/decision-2 - Decouple-xUnit-from-AutoFixture-via-provider-pattern.md index 0371c2bd..af569350 100644 --- a/.backlog/decisions/decision-2 - Decouple-xUnit-from-AutoFixture-via-provider-pattern.md +++ b/.backlog/decisions/decision-2 - Decouple-xUnit-from-AutoFixture-via-provider-pattern.md @@ -17,4 +17,4 @@ Stop inheriting from AutoFixture attributes. Derive from xUnit's `DataAttribute` - Clear boundary between the xUnit layer and AutoFixture: each can evolve independently. - The provider pattern enables per-parameter customization via `IParameterCustomizationSource` attributes applied later in the pipeline. -- All three mock modules (AutoMoq, AutoFakeItEasy, AutoNSubstitute) override only `Customize(IFixture)` — the rest of the pipeline is inherited from Core. +- All three mock modules (AutoMoq, AutoFakeItEasy, AutoNSubstitute) override only `Customize(IFixture)` - the rest of the pipeline is inherited from Core. diff --git a/.backlog/decisions/decision-23 - Performance-benchmark-tooling-and-approach.md b/.backlog/decisions/decision-23 - Performance-benchmark-tooling-and-approach.md index cb8748be..d6bfd6a7 100644 --- a/.backlog/decisions/decision-23 - Performance-benchmark-tooling-and-approach.md +++ b/.backlog/decisions/decision-23 - Performance-benchmark-tooling-and-approach.md @@ -23,12 +23,12 @@ Four decisions needed to be made before implementation: | Tool | Category | Verdict | | --- | --- | --- | -| BenchmarkDotNet | Micro-benchmark | **Accepted** — handles JIT warm-up, GC pressure, allocation tracking (`[MemoryDiagnoser]`), and Markdown/JSON export; de-facto standard for .NET | -| dotnet-counters | Runtime diagnostic | Supplementary only — useful for investigating GC pressure after a benchmark reveals a problem | -| PerfView | ETW profiler | Supplementary only — right tool to pinpoint which specimen builder caused a regression; Windows-only (consistent with CI) | -| MiniProfiler | ASP.NET app profiler | Rejected — requires a running web host; cannot measure in-process library calls | -| k6 | HTTP load testing | Rejected — no HTTP surface in this library | -| Gatling | HTTP load testing | Rejected — same reason as k6 | +| BenchmarkDotNet | Micro-benchmark | **Accepted** - handles JIT warm-up, GC pressure, allocation tracking (`[MemoryDiagnoser]`), and Markdown/JSON export; de-facto standard for .NET | +| dotnet-counters | Runtime diagnostic | Supplementary only - useful for investigating GC pressure after a benchmark reveals a problem | +| PerfView | ETW profiler | Supplementary only - right tool to pinpoint which specimen builder caused a regression; Windows-only (consistent with CI) | +| MiniProfiler | ASP.NET app profiler | Rejected - requires a running web host; cannot measure in-process library calls | +| k6 | HTTP load testing | Rejected - no HTTP surface in this library | +| Gatling | HTTP load testing | Rejected - same reason as k6 | Benchmarks are an opt-in `dotnet run --configuration Release` step, never part of `dotnet test`. @@ -40,13 +40,13 @@ Used only as a `[Benchmark(Baseline = true)]` reference inside `LibraryOverheadB to produce a Ratio column; it is not the primary measurement target. **Full attribute pipeline** (`attribute.GetData(MethodInfo)`): exercises attribute wiring, -customization composition, and per-parameter specimen resolution — the cost uniquely +customization composition, and per-parameter specimen resolution - the cost uniquely attributable to this library on top of AutoFixture. **Decision: full attribute pipeline.** `LibraryOverheadBenchmark` is the deliberate exception: it pairs a fixture-only method alongside the full-pipeline methods so the output Ratio column quantifies how much overhead this library adds on top of AutoFixture. This serves the same -goal as the original decision — isolating our contribution — but does so quantitatively rather +goal as the original decision - isolating our contribution - but does so quantitatively rather than by exclusion. ### 3. Project structure: three projects, one per mock module @@ -81,7 +81,7 @@ the numbers and is not attributable to this library, and it requires a circular dependency on a separately compiled test assembly. Benchmarks use a real `MethodInfo` obtained via reflection from a representative test method -defined in the benchmark project itself — not a synthetic stub — so that parameter type +defined in the benchmark project itself - not a synthetic stub - so that parameter type resolution and per-parameter customization attribute scanning are exercised realistically. ## Consequences diff --git a/.backlog/decisions/decision-24 - BDD-style-test-naming-with-GIVEN-WHEN-THEN.md b/.backlog/decisions/decision-24 - BDD-style-test-naming-with-GIVEN-WHEN-THEN.md new file mode 100644 index 00000000..5ba7823b --- /dev/null +++ b/.backlog/decisions/decision-24 - BDD-style-test-naming-with-GIVEN-WHEN-THEN.md @@ -0,0 +1,41 @@ +--- +id: decision-24 +title: BDD-style test naming with GIVEN/WHEN/THEN +date: '2026-04-24' +status: accepted +category: testing +--- +## Context + +Test method names in most .NET projects are either free-form prose or follow the `MethodName_StateUnderTest_ExpectedBehavior` convention. Both styles embed the subject in the name, which means the name reads like an implementation detail rather than a behavioral specification. As the test suite in this repository grew, reviewers needed to open test bodies to understand what a failing test meant - the name alone was insufficient. + +xUnit's `DisplayName` property existed and was rendered in CI output and IDE test runners but was left blank or simply duplicated the method name verbatim, adding no value. + +## Decision + +Every test method must carry a `DisplayName` written in `UPPER CASE GIVEN/WHEN/THEN` form: + +- `GIVEN` introduces the precondition (state before the action). +- `WHEN` introduces the action or event under test. +- `THEN` introduces the expected outcome. + +For `[Fact]` tests where there is no meaningful precondition, `WHEN...THEN` alone is acceptable. + +The method name mirrors `DisplayName` in `PascalCase_WithUnderscores` form - every word from the display name is preserved, capitalised, and joined by underscores. The word `Test` is never used as a prefix or suffix in either the display name or the method name. + +```csharp +// Fact: single action → single outcome +[Fact(DisplayName = "WHEN parameterless constructor is invoked THEN fixture and provider are created")] +public void WhenParameterlessConstructorIsInvoked_ThenFixtureAndProviderAreCreated() + +// Theory: precondition + action → outcome +[Theory(DisplayName = "GIVEN test method has object parameters WHEN test run THEN parameters are generated")] +public void GivenTestMethodHasObjectParameters_WhenTestRun_ThenParametersAreGenerated(...) +``` + +## Consequences + +- Test output in CI and IDEs reads as a specification document. A failing test announces its behavioral contract without requiring the reader to open the source file. +- The `PascalCase_WithUnderscores` method name is intentionally redundant with `DisplayName` so that stack traces in non-IDE environments (e.g., raw `dotnet test` output) still carry the full semantic label. +- Test names are longer than those produced by the `MethodName_State_Expected` convention. This is an accepted trade-off: clarity of intent outweighs brevity. +- The convention is enforced by code review (CodeRabbit, see decision-22) rather than by a compiler or analyzer rule. diff --git a/.backlog/decisions/decision-25 - AAA-test-structure-with-mandatory-section-comments.md b/.backlog/decisions/decision-25 - AAA-test-structure-with-mandatory-section-comments.md new file mode 100644 index 00000000..9b5b2a5d --- /dev/null +++ b/.backlog/decisions/decision-25 - AAA-test-structure-with-mandatory-section-comments.md @@ -0,0 +1,39 @@ +--- +id: decision-25 +title: AAA test structure with mandatory section comments +date: '2026-04-24' +status: accepted +category: testing +--- +## Context + +Tests without visible structural landmarks require readers to mentally parse setup code from the invocation under test from the assertion. + +The question was whether to mandate the `// Arrange / // Act / // Assert` comment triad on every test method, including trivially short ones where the structure is apparent from a glance, or to leave it as an optional aid. + +## Decision + +Every test method must contain exactly three section comments in order: + +```csharp +[Fact(DisplayName = "...")] +public void WhenX_ThenY() +{ + // Arrange + var sut = new Subject(); + + // Act + var result = sut.DoSomething(); + + // Assert + Assert.Equal(expected, result); +} +``` + +Empty sections are permitted when nothing belongs there (no setup needed, or the act is implicit in the constructor call in the Assert line). An empty `// Arrange` or `// Act` comment is not noise - it is an intentional signal that the section was considered and found to have no content. + +## Consequences + +- The structure is scannable at a glance. Readers know where each phase begins without parsing the code. +- Empty sections self-document the absence of setup or explicit invocation rather than leaving it ambiguous whether the section was forgotten. +- Some trivial tests feel over-structured. This is accepted: the overhead is three comment lines; the benefit is uniform scanability across the entire test suite. diff --git a/.backlog/decisions/decision-26 - TreatWarningsAsErrors-with-the-five-package-analyzer-stack.md b/.backlog/decisions/decision-26 - TreatWarningsAsErrors-with-the-five-package-analyzer-stack.md new file mode 100644 index 00000000..3e62120d --- /dev/null +++ b/.backlog/decisions/decision-26 - TreatWarningsAsErrors-with-the-five-package-analyzer-stack.md @@ -0,0 +1,44 @@ +--- +id: decision-26 +title: TreatWarningsAsErrors with the five-package analyzer stack +date: '2026-04-24' +status: accepted +category: coding-style +--- +## Context + +Analyzer warnings that do not fail builds tend to accumulate over time. Style and quality violations that are treated as advisory become invisible noise: contributors learn to ignore them, and the warning count grows until the list is useless as a quality signal. + +As the project matured, the question was: + +1. Whether to enforce a strict zero-warning build policy. +2. Which analyzer packages to include. +3. How to handle legitimate suppressions. +4. How to handle informational advisories that are not code quality issues (e.g., NuGet + vulnerability notifications). + +## Decision + +`TreatWarningsAsErrors=true` and `CodeAnalysisTreatWarningsAsErrors=true` are set globally in +`Directory.Build.props`. The analyzer stack is fixed at five packages: + +| Package | Domain | +| --- | --- | +| StyleCop.Analyzers | Namespace ordering, `using` placement, member ordering, formatting | +| Roslynator.Analyzers | General C# quality, idiomatic patterns | +| Roslynator.Formatting.Analyzers | Whitespace and blank-line formatting | +| SonarAnalyzer.CSharp | Reliability, maintainability, security hotspots | +| Microsoft.CodeAnalysis.NetAnalyzers | .NET API usage correctness | + +Test projects additionally include `xunit.analyzers` for xUnit2-specific best practices. + +NuGet audit warnings (NU1901–NU1904) are listed in `WarningsNotAsErrors` because they are informational dependency advisories, not code quality issues; they are reviewed separately. + +`[SuppressMessage]` is permitted only with a non-empty `Justification` parameter. + +## Consequences + +- A passing `dotnet build` is a complete quality signal: no style violations, no analyzer issues, no code correctness warnings have been introduced. +- Adding a new analyzer package to the stack is a deliberate decision: it may introduce violations across all existing code and must be resolved before the build passes again. +- The five packages cover distinct domains with minimal overlap, so contradictory rules are unlikely; when they do conflict, the `.editorconfig` severity settings are the tiebreaker. +- Each `[SuppressMessage]` with a `Justification` serves as in-code documentation of an intentional exception, making suppressions discoverable and reviewable. diff --git a/.backlog/decisions/decision-27 - Minimize-XML-documentation-prefer-self-documenting-names.md b/.backlog/decisions/decision-27 - Minimize-XML-documentation-prefer-self-documenting-names.md new file mode 100644 index 00000000..9fb63344 --- /dev/null +++ b/.backlog/decisions/decision-27 - Minimize-XML-documentation-prefer-self-documenting-names.md @@ -0,0 +1,32 @@ +--- +id: decision-27 +title: Minimize XML documentation; prefer self-documenting names +date: '2026-04-24' +status: accepted +category: design +--- +## Context + +StyleCop enforces XML documentation comments on all public members by default (CS1591). +Comprehensive XML docs require maintenance alongside code changes; they tend to drift from the implementation and produce noise on members whose names are already self-explanatory. + +This library's public API is an xUnit attribute layer - most public properties and methods are named directly after their behavior. Blindly satisfying CS1591 would produce meaningless one-liners such as `/// Gets the fixture.` on `public IFixture Fixture { get; }`, which adds no information beyond what the declaration +already communicates. + +However, a blanket "no XML docs" rule does not match the codebase's actual needs: some members have non-obvious behavioral consequences, and some internal types were copied from external sources whose origin deserves attribution. + +## Decision + +Documentation warning codes CS1591, CS1573, and CS1712 are suppressed globally in the `.editorconfig`. `GenerateDocumentationFile=true` is retained so that any XML comments that are written are validated at build time - a malformed comment or a missing `` tag will still produce an error. + +XML documentation comments are written only where the member name alone does not communicate its contract, side-effect, or constraint. Current examples of members that do carry comments: + +- Boolean behavioral properties whose effect is non-obvious from the name alone (e.g., `IgnoreVirtualMembers`, `ShareFixture`). +- Types or members copied from external sources, where an origin-attribution comment explains the provenance (e.g., `Check.cs`). + +## Consequences + +- No pressure to write meaningless one-liner docs that add noise without adding information. +- The small number of comments that do exist carry signal: their presence on a member indicates that the name alone was judged insufficient. +- NuGet consumers browsing IntelliSense see tooltips only on the members that genuinely need them; the majority of well-named attributes and properties communicate through their names. +- When adding a new public member, the authoring question is: "Is the name sufficient?" If yes, no comment is needed. If the answer requires more than one short sentence to explain, the name should be reconsidered first. diff --git a/.backlog/decisions/decision-28 - Seal-attribute-derived-types-by-default.md b/.backlog/decisions/decision-28 - Seal-attribute-derived-types-by-default.md new file mode 100644 index 00000000..f93f94a8 --- /dev/null +++ b/.backlog/decisions/decision-28 - Seal-attribute-derived-types-by-default.md @@ -0,0 +1,24 @@ +--- +id: decision-28 +title: Seal attribute-derived types by default +date: '2026-04-24' +status: accepted +category: design +--- +## Context + +The CA1813 analyzer rule ("Avoid unsealed attributes") specifically targets `Attribute`-derived types (see decision-26). Unsealed attribute classes carry a measurable performance cost at runtime because the CLR must check for derived types on every attribute lookup, and they widen the public inheritance surface unnecessarily. + +Non-attribute public types such as `ICustomization` and `ISpecimenBuilder` implementations are intentionally open. Consumers of this NuGet library may legitimately subclass or compose those types to extend AutoFixture's object-creation pipeline. Applying a blanket "seal everything" rule to those types would break a supported and documented extension scenario. + +## Decision + +All `Attribute`-derived types are `sealed` unless they are explicitly `abstract` (e.g., `AutoDataBaseAttribute`, `InlineAutoDataBaseAttribute`). This is exactly what the CA1813 rule enforces and is the narrowest scope that eliminates the runtime cost and inheritance-surface concerns for attributes. + +Non-attribute public types (customizations, specimen builders, and other framework extension points) follow normal object-oriented design principles: they are open when consumer subclassing is a supported scenario and sealed only when there is a specific reason to prevent inheritance. + +## Consequences + +- The CA1813 rule is enforced by the analyzer on every build, so attribute sealing is never accidentally omitted. +- Non-attribute public types remain open for consumer extension without requiring suppression or justification. +- Any `[SuppressMessage("Performance", "CA1813", Justification = "...")]` in the codebase documents an intentional exception for an attribute type that must remain unsealed (e.g., an abstract base attribute), making such exceptions discoverable by search. diff --git a/.backlog/decisions/decision-29 - NotNull-fluent-guard-over-ArgumentNullException.md b/.backlog/decisions/decision-29 - NotNull-fluent-guard-over-ArgumentNullException.md new file mode 100644 index 00000000..bf9ef7cb --- /dev/null +++ b/.backlog/decisions/decision-29 - NotNull-fluent-guard-over-ArgumentNullException.md @@ -0,0 +1,43 @@ +--- +id: decision-29 +title: NotNull() fluent guard over throw new ArgumentNullException +date: '2026-04-24' +status: accepted +category: coding-style +--- +## Context + +Null-guard patterns in .NET have evolved over time: + +1. `if (x == null) throw new ArgumentNullException(nameof(x));` - verbose, requires a temporary variable when the validated value must also be assigned. +2. `ArgumentNullException.ThrowIfNull(x);` - concise but available only from .NET 6+. +3. C# nullable reference types with compiler enforcement - requires opt-in per project and does not eliminate runtime checks in public API. + +This library targets netstandard2.0, netstandard2.1, net472, and net48. `ArgumentNullException.ThrowIfNull` is unavailable on the .NET Framework and netstandard targets. A consistent, cross-framework guard mechanism that also supports fluent chaining at constructor callsites was needed. + +## Decision + +Use `Check.NotNull(this T value, string parameterName)` from `src/Objectivity.AutoFixture.XUnit2.Core/Common/Check.cs` for all null guards. `NotNull()` is a fluent extension method that returns the validated value, enabling inline assignment: + +```csharp +// Instead of: +if (fixture == null) throw new ArgumentNullException(nameof(fixture)); +this.Fixture = fixture; + +// Write: +this.Fixture = fixture.NotNull(nameof(fixture)); + +// Or chain directly: +fixture.NotNull(nameof(fixture)).Customize(new AutoMoqCustomization()); +``` + +The implementation throws `ArgumentNullException` with the correct parameter name internally. +JetBrains Annotations attributes (`[ContractAnnotation("value:null => halt")]` and `[ValidatedNotNull]`) are applied to `NotNull()` so that Rider and ReSharper understand the value is guaranteed non-null after the call. + +## Consequences + +- Constructor bodies read as straightforward assignments rather than guard-block-then-assign pairs, reducing visual noise in constructors with multiple parameters. +- Fluent chaining enables single-expression patterns (`x.NotNull(nameof(x)).Method()`) without introducing a temporary variable. +- The implementation works across all target frameworks (netstandard2.0, net472, net48) with no API availability concern. +- IDE null-safety analysis in Rider and ReSharper remains accurate: the annotations prevent false "possible null dereference" warnings after a `NotNull()` call. +- `Check.cs` carries an origin-attribution comment explaining its provenance; this is one of the explicit exceptions to the minimize-XML-documentation rule (see decision-27). diff --git a/.backlog/decisions/decision-3 - Extract-shared-logic-into-a-Core-assembly.md b/.backlog/decisions/decision-3 - Extract-shared-logic-into-a-Core-assembly.md index 38737e09..ecf275df 100644 --- a/.backlog/decisions/decision-3 - Extract-shared-logic-into-a-Core-assembly.md +++ b/.backlog/decisions/decision-3 - Extract-shared-logic-into-a-Core-assembly.md @@ -16,4 +16,4 @@ Create `Objectivity.AutoFixture.XUnit2.Core` as a shared assembly containing all - Hub-and-spoke architecture: each mock module overrides only `Customize(IFixture)`; all shared behavior lives in one place. - Bugs and improvements to Core benefit all three modules simultaneously. -- Core cannot be installed by consumers directly — it is an implementation detail. +- Core cannot be installed by consumers directly - it is an implementation detail. diff --git a/.backlog/decisions/decision-4 - Support-multiple-mocking-frameworks-as-separate-modules.md b/.backlog/decisions/decision-4 - Support-multiple-mocking-frameworks-as-separate-modules.md index beefbe2f..2633d488 100644 --- a/.backlog/decisions/decision-4 - Support-multiple-mocking-frameworks-as-separate-modules.md +++ b/.backlog/decisions/decision-4 - Support-multiple-mocking-frameworks-as-separate-modules.md @@ -10,7 +10,7 @@ AutoFixture supports multiple mock backends (Moq, NSubstitute, FakeItEasy). Team ## Decision -Build a separate NuGet package for each mocking framework — `AutoMoq`, `AutoFakeItEasy`, `AutoNSubstitute` — each depending on Core. Every module exposes equivalent three attribute shapes (`[AutoMockData]`, `[InlineAutoMockData]`, `[MemberAutoMockData]`) within its own module namespace, and overrides only `Customize(IFixture)` to apply the framework-specific `ICustomization`. +Build a separate NuGet package for each mocking framework - `AutoMoq`, `AutoFakeItEasy`, `AutoNSubstitute` - each depending on Core. Every module exposes equivalent three attribute shapes (`[AutoMockData]`, `[InlineAutoMockData]`, `[MemberAutoMockData]`) within its own module namespace, and overrides only `Customize(IFixture)` to apply the framework-specific `ICustomization`. ## Consequences diff --git a/.backlog/decisions/decision-7 - Publish-Core-bundled-into-module-packages-not-standalone.md b/.backlog/decisions/decision-7 - Publish-Core-bundled-into-module-packages-not-standalone.md index 9e2b9134..fece6105 100644 --- a/.backlog/decisions/decision-7 - Publish-Core-bundled-into-module-packages-not-standalone.md +++ b/.backlog/decisions/decision-7 - Publish-Core-bundled-into-module-packages-not-standalone.md @@ -6,7 +6,7 @@ status: accepted --- ## Context -Core contains all shared infrastructure but has no value in isolation — it requires a mock backend to function. Publishing it as a standalone NuGet package would invite consumers to install it alone, producing a broken setup with no data generation capability. +Core contains all shared infrastructure but has no value in isolation - it requires a mock backend to function. Publishing it as a standalone NuGet package would invite consumers to install it alone, producing a broken setup with no data generation capability. ## Decision diff --git a/.backlog/decisions/decision-8 - Suppress-virtual-member-population-via-customization.md b/.backlog/decisions/decision-8 - Suppress-virtual-member-population-via-customization.md index a9c13da8..db139b36 100644 --- a/.backlog/decisions/decision-8 - Suppress-virtual-member-population-via-customization.md +++ b/.backlog/decisions/decision-8 - Suppress-virtual-member-population-via-customization.md @@ -15,5 +15,5 @@ Introduce `IgnoreVirtualMembersSpecimenBuilder` (returning `OmitSpecimen` for vi ## Consequences - Mock proxies are not interfered with by AutoFixture's property-population pass. -- Opt-in per attribute — tests that genuinely need virtual properties populated are unaffected. +- Opt-in per attribute - tests that genuinely need virtual properties populated are unaffected. - Reduces the most common setup error for users working with interface-implementing types. diff --git a/.backlog/decisions/decision-9 - Add-CustomizeWith-parameter-attributes.md b/.backlog/decisions/decision-9 - Add-CustomizeWith-parameter-attributes.md index 8c1aa19d..af4a1f96 100644 --- a/.backlog/decisions/decision-9 - Add-CustomizeWith-parameter-attributes.md +++ b/.backlog/decisions/decision-9 - Add-CustomizeWith-parameter-attributes.md @@ -12,8 +12,8 @@ Users occasionally need to apply a full `ICustomization` to a single parameter w Add two parameter-level attributes to Core: -- `[CustomizeWith(typeof(T))]` — activates the specified `ICustomization` for a parameter -- `[CustomizeWith]` — generic variant; equivalent in behavior, reduces casting boilerplate +- `[CustomizeWith(typeof(T))]` - activates the specified `ICustomization` for a parameter +- `[CustomizeWith]` - generic variant; equivalent in behavior, reduces casting boilerplate Both are implemented via `IParameterCustomizationSource` and apply the customization to the fixture immediately before the parameter is resolved. diff --git a/.backlog/tasks/task-14 - Prepare-documentation.md b/.backlog/tasks/task-14 - Prepare-documentation.md index fa69a5fb..4c8a0c40 100644 --- a/.backlog/tasks/task-14 - Prepare-documentation.md +++ b/.backlog/tasks/task-14 - Prepare-documentation.md @@ -13,11 +13,11 @@ priority: low ## Description -All documentation currently lives in `README.md` — a single flat file covering installation, all attributes, data filtering attributes, and tips & tricks. As the library grows this becomes hard to navigate and limits discoverability. +All documentation currently lives in `README.md` - a single flat file covering installation, all attributes, data filtering attributes, and tips & tricks. As the library grows this becomes hard to navigate and limits discoverability. **Goal:** Migrate `README.md` content into a structured documentation site under a `/docs` folder, served via GitHub Pages with a dedicated GitHub Actions workflow. -**Phase 1 — Investigation (required before implementation):** +**Phase 1 - Investigation (required before implementation):** Evaluate and select a documentation tool. Candidates: | Tool | Runtime | Notes | @@ -28,21 +28,21 @@ Evaluate and select a documentation tool. Candidates: Selection criteria: Markdown-native, GitHub Pages compatible, minimal config, good built-in search, low maintenance burden, minimal CI runtime dependency. -**Phase 2 — Implementation:** +**Phase 2 - Implementation:** -1. **Create `/docs` folder** — break `README.md` content into logical pages, e.g.: +1. **Create `/docs` folder** - break `README.md` content into logical pages, e.g.: - Getting started (installation, quick example) - Attributes: `AutoMockData`, `InlineAutoMockData`, `MemberAutoMockData` - Parameter attributes: `Frozen`, `IgnoreVirtualMembers`, `CustomizeWith` - Data filtering attributes: `Except`, `PickFromRange`, `PickFromValues`, `PickNegative` - Tips & tricks 2. **Add tool config** (e.g. `mkdocs.yml`, `vitepress.config.ts`) at the repo root or `/docs`. -3. **Add GitHub Actions workflow** under `.github/workflows/docs.yml` — build and deploy to GitHub Pages on push to `master` using `actions/upload-pages-artifact` + `actions/deploy-pages`. **Requires explicit approval before implementation** (per `AGENTS.md`). -4. **Slim down `README.md`** — keep project summary, badges, and a link to the full docs site. +3. **Add GitHub Actions workflow** under `.github/workflows/docs.yml` - build and deploy to GitHub Pages on push to `master` using `actions/upload-pages-artifact` + `actions/deploy-pages`. **Requires explicit approval before implementation** (per `AGENTS.md`). +4. **Slim down `README.md`** - keep project summary, badges, and a link to the full docs site. **Constraints:** -- Docs build workflow can run on `ubuntu-latest` (CI currently runs on `windows-latest` for .NET Framework — docs are independent). +- Docs build workflow can run on `ubuntu-latest` (CI currently runs on `windows-latest` for .NET Framework - docs are independent). - Changes to `.github/workflows/` require explicit approval before implementation. @@ -61,5 +61,5 @@ Selection criteria: Markdown-native, GitHub Pages compatible, minimal config, go - `/docs/` (new folder) - `README.md` (slimmed down) -- `.github/workflows/docs.yml` (new — requires explicit approval) +- `.github/workflows/docs.yml` (new - requires explicit approval) - Tool-specific config file at repo root (e.g. `mkdocs.yml`, `vitepress.config.ts`) diff --git a/.backlog/tasks/task-19 - Add-performance-benchmarks.md b/.backlog/tasks/task-19 - Add-performance-benchmarks.md index 7cd0edbf..5f60b29b 100644 --- a/.backlog/tasks/task-19 - Add-performance-benchmarks.md +++ b/.backlog/tasks/task-19 - Add-performance-benchmarks.md @@ -20,7 +20,7 @@ AutoFixture, (b) which layer of the attribute pipeline is responsible for that o (c) which usage patterns are expensive enough to warrant guidance or refactoring. Tooling selection, benchmark scope, project structure, and xUnit execution model analysis are -recorded in [DECISION-23 — Performance benchmark tooling and approach](../decisions/decision-23%20-%20Performance-benchmark-tooling-and-approach.md). +recorded in [DECISION-23 - Performance benchmark tooling and approach](../decisions/decision-23%20-%20Performance-benchmark-tooling-and-approach.md). **Summary of decisions:** @@ -45,9 +45,9 @@ recorded in [DECISION-23 — Performance benchmark tooling and approach](../deci The three methods isolate responsibility: -- **Layer A — AutoFixture only** (`Baseline = true`): `new Fixture(); Customize(AutoMoq); fixture.Create()` — pure AutoFixture cost, no attribute machinery -- **Layer B — Customization chain**: Layer A + `AutoDataCommonCustomization` — adds our recursion-handling setup (`DoNotThrowOnRecursion`, `OmitOnRecursion`) -- **Layer C — Full pipeline**: `attribute.GetData(MethodInfo)` called twice — adds attribute wiring, reflection on parameters, per-parameter `SpecimenContext` allocation +- **Layer A - AutoFixture only** (`Baseline = true`): `new Fixture(); Customize(AutoMoq); fixture.Create()` - pure AutoFixture cost, no attribute machinery +- **Layer B - Customization chain**: Layer A + `AutoDataCommonCustomization` - adds our recursion-handling setup (`DoNotThrowOnRecursion`, `OmitOnRecursion`) +- **Layer C - Full pipeline**: `attribute.GetData(MethodInfo)` called twice - adds attribute wiring, reflection on parameters, per-parameter `SpecimenContext` allocation Delta (A→B) = cost of `DoNotThrowOnRecursionCustomization` LINQ enumeration + allocations. Delta (B→C) = cost of attribute machinery: `testMethod.GetParameters()`, `p.GetCustomAttributes()`, LINQ sort per parameter, `new SpecimenContext()` per parameter. @@ -81,4 +81,4 @@ dotnet run --project src/Objectivity.AutoFixture.XUnit2.AutoMoq.Benchmarks \ ## Related - Future: CI benchmarking with PR delta comments (noted in DECISION-23 Consequences; no task - created yet — requires a separate design decision on tooling and workflow). + created yet - requires a separate design decision on tooling and workflow). diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b412d5cb..1d13a374 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,5 +17,5 @@ Closes # - [ ] Mutation score remains at least at the level prior the change (verified by Stryker) - [ ] New tests follow the GIVEN/WHEN/THEN naming convention and AAA structure (see [AGENTS.md](../AGENTS.md)) - [ ] No new `[SuppressMessage]` without a justification comment -- [ ] No `// TODO:` comments added — open a GitHub issue instead +- [ ] No `// TODO:` comments added - open a GitHub issue instead - [ ] No new dependencies introduced that are incompatible with the MIT license (verified by FOSSA) diff --git a/AGENTS.md b/AGENTS.md index 752b422e..6a843b17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# AGENTS.md — AI Agent Context for AutoFixture.XUnit2.AutoMock +# AGENTS.md - AI Agent Context for AutoFixture.XUnit2.AutoMock This file provides context for AI coding assistants (Claude Code, GitHub Copilot, OpenAI Codex, Cursor, and others) working in this repository. Read it before making any changes. @@ -9,8 +9,8 @@ Cursor, and others) working in this repository. Read it before making any change **C# NuGet library collection** that bridges: -- **[AutoFixture](https://github.com/AutoFixture/AutoFixture)** — generates anonymous test data automatically -- **Mocking frameworks** — Moq, FakeItEasy, NSubstitute +- **[AutoFixture](https://github.com/AutoFixture/AutoFixture)** - generates anonymous test data automatically +- **Mocking frameworks** - Moq, FakeItEasy, NSubstitute The result: xUnit2 test attributes that auto-generate both test data *and* mocks in one step, eliminating boilerplate setup in unit tests. @@ -21,7 +21,7 @@ eliminating boilerplate setup in unit tests. - `Objectivity.AutoFixture.XUnit2.AutoFakeItEasy` (uses FakeItEasy) - `Objectivity.AutoFixture.XUnit2.AutoNSubstitute` (uses NSubstitute) -`Core` is **not published standalone** — it is bundled into each of the three packages above. +`Core` is **not published standalone** - it is bundled into each of the three packages above. --- @@ -93,7 +93,7 @@ Every mock module (AutoMoq, AutoFakeItEasy, AutoNSubstitute) derives from `AutoD in Core and overrides only `Customize(IFixture)`. The base class orchestrates the full lifecycle: 1. Applies `AutoDataCommonCustomization` (handles `IgnoreVirtualMembers`) -2. Calls `Customize(fixture)` — the only method mock modules override +2. Calls `Customize(fixture)` - the only method mock modules override 3. Delegates to `IAutoFixtureAttributeProvider` to generate test data Each mock module's sole responsibility is the `Customize` method: @@ -128,7 +128,7 @@ if it is truly module-specific. --- -## Key Public API — Attributes +## Key Public API - Attributes All three modules expose the same three attributes (prefixed with the mock library name): @@ -139,7 +139,7 @@ All three modules expose the same three attributes (prefixed with the mock libra | `[MemberAutoMockData("MemberName")]` | Values from a static member, auto-generated for the rest | All three accept `IgnoreVirtualMembers = true` to prevent AutoFixture from populating -`virtual` properties (useful when classes implement interfaces — the CLR marks interface +`virtual` properties (useful when classes implement interfaces - the CLR marks interface method implementations as `virtual sealed`). ### Parameter Attributes (Core, apply to all modules) @@ -161,7 +161,7 @@ method implementations as `virtual sealed`). ### Naming Pattern -Test methods use BDD-style names — this is **mandatory**, not optional: +Test methods follow BDD-style naming (see [decision-24]): ```csharp // Fact: single action → single outcome @@ -181,7 +181,7 @@ Rules: ### Test Structure -All tests follow AAA (Arrange / Act / Assert) with **explicit section comments**: +Tests follow AAA structure with mandatory section comments (see [decision-25]): ```csharp [Fact(DisplayName = "...")] @@ -213,7 +213,7 @@ public void GivenX_WhenY_ThenZ(IFakeObjectUnderTest value) { ... } ### Test Organization - Test projects mirror source projects 1:1 in namespace and folder structure -- Test classes use `[Collection("SubjectClassName")]` — use the **subject** class name, not the test class name (e.g. `[Collection("AutoMockDataAttribute")]` on `AutoMockDataAttributeTests`) +- Test classes use `[Collection("SubjectClassName")]` - use the **subject** class name, not the test class name (e.g. `[Collection("AutoMockDataAttribute")]` on `AutoMockDataAttributeTests`) - Test classes use `[Trait("Category", "CategoryName")]` for categorization - Test file naming: `Tests.cs` - Interface definitions used as test doubles (e.g., `IFakeObjectUnderTest.cs`) live in the test project root @@ -230,13 +230,13 @@ Do not mock AutoFixture internals beyond those two abstractions. ### Enforced by Analyzers (Build Will Fail If Violated) -The following analyzers run on every build with `TreatWarningsAsErrors=true`: +The following analyzers run on every build with `TreatWarningsAsErrors=true` (see [decision-26]): -- **StyleCop.Analyzers** — namespace ordering, `using` placement, member ordering, documentation -- **Roslynator.Analyzers** + **Roslynator.Formatting.Analyzers** — general C# quality rules -- **SonarAnalyzer.CSharp** — reliability, maintainability, security hotspots -- **Microsoft.CodeAnalysis.NetAnalyzers** — .NET API usage correctness -- **xunit.analyzers** — xUnit2-specific best practices +- **StyleCop.Analyzers** - namespace ordering, `using` placement, member ordering, documentation +- **Roslynator.Analyzers** + **Roslynator.Formatting.Analyzers** - general C# quality rules +- **SonarAnalyzer.CSharp** - reliability, maintainability, security hotspots +- **Microsoft.CodeAnalysis.NetAnalyzers** - .NET API usage correctness +- **xunit.analyzers** - xUnit2-specific best practices The `.editorconfig` in `src/` configures severity for all rules. Suppressing analyzer is allowed only when cost of fixing is significant and in such case the `[SuppressMessage]` is required with justification. @@ -244,9 +244,9 @@ The `.editorconfig` in `src/` configures severity for all rules. Suppressing ana - `using` directives go **inside** the namespace block (StyleCop SA1210) - `global::` prefix on external packages (AutoFixture, Moq, etc.) to avoid ambiguity -- XML documentation comments should not be included as the code should be self-explanatory -- Sealed classes preferred over open classes unless designed for inheritance -- `NotNull()` guard extension (from `Core/Common/`) for null checks instead of `ArgumentNullException` +- XML documentation is minimized; prefer self-documenting names (see [decision-27]) +- Attribute-derived types are sealed by default; non-attribute public types follow normal design principles (see [decision-28]) +- `NotNull()` guard extension (from `Core/Common/`) for null checks instead of `ArgumentNullException` (see [decision-29]) - Latest C# language version is used throughout ### Strong-Name Signing @@ -259,13 +259,13 @@ fully sign locally unless you have the private key. ## What to Avoid -- **Do not create a new solution or project** without discussion — new modules would need to be added to all 5 solution files. +- **Do not create a new solution or project** without discussion - new modules would need to be added to all 5 solution files. - **Do not add public API to `Core`** that is specific to one mocking library. - **Do not add `[SuppressMessage]`** without a justification comment. -- **Do not use `new Fixture()`** in tests — inject `IFixture` via the test method signature or use `[AutoMockData]`. -- **Do not omit `DisplayName`** — every `[Fact]` and `[Theory]` must have one. -- **Do not add `// TODO:` comments** — open a GitHub issue instead. -- **Do not pin dependencies** to a version manually — Dependabot ignores Moq updates intentionally +- **Do not use `new Fixture()`** in tests - inject `IFixture` via the test method signature or use `[AutoMockData]`. +- **Do not omit `DisplayName`** - every `[Fact]` and `[Theory]` must have one. +- **Do not add `// TODO:` comments** - open a GitHub issue instead. +- **Do not pin dependencies** to a version manually - Dependabot ignores Moq updates intentionally (see `dependabot.yml`) due to the `Moq` SponsorLink controversy. --- @@ -324,17 +324,17 @@ These rules apply to all AI coding assistants working in this repository. ### Before Making Changes - **Propose before acting** on any non-trivial change (new attribute, refactor, CI change). Describe the approach and wait for approval. -- **Suggest creating a backlog task** if one does not already exist before implementation begins. Search the backlog first to avoid duplicates. Use short, plain-English titles (e.g. "Prepare documentation", "Upgrade test projects to net10.0") — do not apply Conventional Commits prefixes to task titles. +- **Suggest creating a backlog task** if one does not already exist before implementation begins. Search the backlog first to avoid duplicates. Use short, plain-English titles (e.g. "Prepare documentation", "Upgrade test projects to net10.0") - do not apply Conventional Commits prefixes to task titles. - **Suggest a branch checkout** for any non-trivial change before implementation begins. Use the Conventional Commits type as a prefix and a short kebab-case description, e.g. `git checkout -b fix/enumerable-extensions-allocation`. Common prefixes: `feat/`, `fix/`, `refactor/`, `chore/`, `ci/`, `docs/`. -- **Prefer `dotnet build` over reading files** to verify correctness — the analyser stack catches style and correctness issues that are hard to spot by inspection alone. +- **Prefer `dotnet build` over reading files** to verify correctness - the analyser stack catches style and correctness issues that are hard to spot by inspection alone. ### After Making Changes -After any C# code change, offer to run the following steps in order — do not run them automatically, as the user may choose to defer: +After any C# code change, offer to run the following steps in order - do not run them automatically, as the user may choose to defer: 1. [**Build**](#build) 2. [**Test**](#test) -3. [**Mutation Tests**](#mutation-tests) *(slow — typically run before raising a PR)* +3. [**Mutation Tests**](#mutation-tests) *(slow - typically run before raising a PR)* ### Committing @@ -352,19 +352,19 @@ After any C# code change, offer to run the following steps in order — do not r When proposing any non-trivial solution, evaluate it against these quality dimensions and call out relevant trade-offs: -- **Performance** — avoid unnecessary allocations, reflection, or lazy evaluation in hot paths; note if a change affects test-run throughput -- **Security** — flag any new use of user-controlled input, serialization, or external data; this is a testing library so the attack surface is low, but be explicit when it changes -- **Maintainability** — prefer the simplest implementation that satisfies the requirement; avoid abstraction layers that do not carry their weight -- **Readability** — code should be easy to read and understand; favour clear naming and straightforward control flow over clever one-liners -- **Testability** — new public types should be injectable or otherwise testable without relying on concrete dependencies; preserve existing mock seams -- **Modularity** — keep changes inside the appropriate layer; do not leak framework-specific concerns into Core -- **Separation of concerns** — split logic into focused, single-purpose components rather than concentrating multiple responsibilities in one place; each class, method or task should have one clear reason to change -- **Developer experience** — prefer readable error messages, discoverable APIs, and minimal configuration; the goal of this library is to reduce boilerplate +- **Performance** - avoid unnecessary allocations, reflection, or lazy evaluation in hot paths; note if a change affects test-run throughput +- **Security** - flag any new use of user-controlled input, serialization, or external data; this is a testing library so the attack surface is low, but be explicit when it changes +- **Maintainability** - prefer the simplest implementation that satisfies the requirement; avoid abstraction layers that do not carry their weight +- **Readability** - code should be easy to read and understand; favour clear naming and straightforward control flow over clever one-liners +- **Testability** - new public types should be injectable or otherwise testable without relying on concrete dependencies; preserve existing mock seams +- **Modularity** - keep changes inside the appropriate layer; do not leak framework-specific concerns into Core +- **Separation of concerns** - split logic into focused, single-purpose components rather than concentrating multiple responsibilities in one place; each class, method or task should have one clear reason to change +- **Developer experience** - prefer readable error messages, discoverable APIs, and minimal configuration; the goal of this library is to reduce boilerplate ## Backlog CLI Reference When MCP is unavailable, use the CLI via `npx backlog` from the repo root. -Prefer the CLI over editing task files directly — the CLI validates structure. +Prefer the CLI over editing task files directly - the CLI validates structure. | Goal | Command | | --- | --- | diff --git a/CLAUDE.md b/CLAUDE.md index ecc173c9..aeb749a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ @AGENTS.md -## Claude Code — Specific Instructions +## Claude Code - Specific Instructions Use **plan mode** before starting any non-trivial change. diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/ExceptAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/ExceptAttribute.cs index 64a8e1d7..e7e509bd 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/ExceptAttribute.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/ExceptAttribute.cs @@ -1,50 +1,45 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.Attributes -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - - using global::AutoFixture; - using global::AutoFixture.Kernel; - using global::AutoFixture.Xunit2; - - using Objectivity.AutoFixture.XUnit2.Core.Common; - using Objectivity.AutoFixture.XUnit2.Core.Requests; - using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; - - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class ExceptAttribute : CustomizeAttribute - { - private readonly HashSet inputValues; - private readonly Lazy> readonlyValues; - - public ExceptAttribute(params object[] values) - { - this.inputValues = new HashSet(values.NotNull(nameof(values))); - if (this.inputValues.Count == 0) - { - throw new ArgumentException("At least one value is expected to be specified.", nameof(values)); - } - - this.readonlyValues = new Lazy>(() => Array.AsReadOnly(this.inputValues.ToArray())); - } - - public IReadOnlyCollection Values => this.readonlyValues.Value; - - public override ICustomization GetCustomization(ParameterInfo parameter) - { - if (parameter is null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - return new CompositeSpecimenBuilder( - new FilteringSpecimenBuilder( - new RequestFactoryRelay((type) => new ExceptValuesRequest(type, this.inputValues.ToArray())), - new EqualRequestSpecification(parameter)), - new RandomExceptValuesGenerator()) - .ToCustomization(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.Requests; + using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class ExceptAttribute : CustomizeAttribute + { + private readonly HashSet inputValues; + private readonly Lazy> readonlyValues; + + public ExceptAttribute(params object[] values) + { + this.inputValues = new HashSet(values.NotNull(nameof(values))); + if (this.inputValues.Count == 0) + { + throw new ArgumentException("At least one value is expected to be specified.", nameof(values)); + } + + this.readonlyValues = new Lazy>(() => Array.AsReadOnly(this.inputValues.ToArray())); + } + + public IReadOnlyCollection Values => this.readonlyValues.Value; + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new CompositeSpecimenBuilder( + new FilteringSpecimenBuilder( + new RequestFactoryRelay((type) => new ExceptValuesRequest(type, this.inputValues.ToArray())), + new EqualRequestSpecification(parameter.NotNull(nameof(parameter)))), + new RandomExceptValuesGenerator()) + .ToCustomization(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromRangeAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromRangeAttribute.cs index 96111b6f..c9fca70b 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromRangeAttribute.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromRangeAttribute.cs @@ -1,73 +1,69 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.Attributes -{ - using System; - using System.Reflection; - - using global::AutoFixture; - using global::AutoFixture.Kernel; - using global::AutoFixture.Xunit2; - - using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; - - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class PickFromRangeAttribute : CustomizeAttribute - { - public PickFromRangeAttribute(int minimum, int maximum) - : this((object)minimum, maximum) - { - } - - public PickFromRangeAttribute(uint minimum, uint maximum) - : this((object)minimum, maximum) - { - } - - public PickFromRangeAttribute(long minimum, long maximum) - : this((object)minimum, maximum) - { - } - - public PickFromRangeAttribute(ulong minimum, ulong maximum) - : this((object)minimum, maximum) - { - } - - public PickFromRangeAttribute(double minimum, double maximum) - : this((object)minimum, maximum) - { - } - - public PickFromRangeAttribute(float minimum, float maximum) - : this((object)minimum, maximum) - { - } - - private PickFromRangeAttribute(object minimum, object maximum) - { - if (((IComparable)minimum).CompareTo((IComparable)maximum) > 0) - { - throw new ArgumentOutOfRangeException(nameof(minimum), $"Parameter {nameof(minimum)} must be lower or equal to parameter {nameof(maximum)}."); - } - - this.Minimum = minimum; - this.Maximum = maximum; - } - - public object Minimum { get; } - - public object Maximum { get; } - - public override ICustomization GetCustomization(ParameterInfo parameter) - { - if (parameter is null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - return new FilteringSpecimenBuilder( - new RequestFactoryRelay((type) => new RangedNumberRequest(type, this.Minimum, this.Maximum)), - new EqualRequestSpecification(parameter)) - .ToCustomization(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class PickFromRangeAttribute : CustomizeAttribute + { + public PickFromRangeAttribute(int minimum, int maximum) + : this((object)minimum, maximum) + { + } + + public PickFromRangeAttribute(uint minimum, uint maximum) + : this((object)minimum, maximum) + { + } + + public PickFromRangeAttribute(long minimum, long maximum) + : this((object)minimum, maximum) + { + } + + public PickFromRangeAttribute(ulong minimum, ulong maximum) + : this((object)minimum, maximum) + { + } + + public PickFromRangeAttribute(double minimum, double maximum) + : this((object)minimum, maximum) + { + } + + public PickFromRangeAttribute(float minimum, float maximum) + : this((object)minimum, maximum) + { + } + + private PickFromRangeAttribute(object minimum, object maximum) + { + if (((IComparable)minimum).CompareTo((IComparable)maximum) > 0) + { + throw new ArgumentOutOfRangeException(nameof(minimum), $"Parameter {nameof(minimum)} must be lower or equal to parameter {nameof(maximum)}."); + } + + this.Minimum = minimum; + this.Maximum = maximum; + } + + public object Minimum { get; } + + public object Maximum { get; } + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new FilteringSpecimenBuilder( + new RequestFactoryRelay((type) => new RangedNumberRequest(type, this.Minimum, this.Maximum)), + new EqualRequestSpecification(parameter.NotNull(nameof(parameter)))) + .ToCustomization(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromValuesAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromValuesAttribute.cs index 1bb95b96..a345ac12 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromValuesAttribute.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickFromValuesAttribute.cs @@ -1,50 +1,45 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.Attributes -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - - using global::AutoFixture; - using global::AutoFixture.Kernel; - using global::AutoFixture.Xunit2; - - using Objectivity.AutoFixture.XUnit2.Core.Common; - using Objectivity.AutoFixture.XUnit2.Core.Requests; - using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; - - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class PickFromValuesAttribute : CustomizeAttribute - { - private readonly HashSet inputValues; - private readonly Lazy> readonlyValues; - - public PickFromValuesAttribute(params object[] values) - { - this.inputValues = new HashSet(values.NotNull(nameof(values))); - if (this.inputValues.Count == 0) - { - throw new ArgumentException("At least one value is expected to be specified.", nameof(values)); - } - - this.readonlyValues = new Lazy>(() => Array.AsReadOnly(this.inputValues.ToArray())); - } - - public IReadOnlyCollection Values => this.readonlyValues.Value; - - public override ICustomization GetCustomization(ParameterInfo parameter) - { - if (parameter is null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - return new CompositeSpecimenBuilder( - new FilteringSpecimenBuilder( - new RequestFactoryRelay((type) => new FixedValuesRequest(type, this.inputValues.ToArray())), - new EqualRequestSpecification(parameter)), - new RandomFixedValuesGenerator()) - .ToCustomization(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.Requests; + using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class PickFromValuesAttribute : CustomizeAttribute + { + private readonly HashSet inputValues; + private readonly Lazy> readonlyValues; + + public PickFromValuesAttribute(params object[] values) + { + this.inputValues = new HashSet(values.NotNull(nameof(values))); + if (this.inputValues.Count == 0) + { + throw new ArgumentException("At least one value is expected to be specified.", nameof(values)); + } + + this.readonlyValues = new Lazy>(() => Array.AsReadOnly(this.inputValues.ToArray())); + } + + public IReadOnlyCollection Values => this.readonlyValues.Value; + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new CompositeSpecimenBuilder( + new FilteringSpecimenBuilder( + new RequestFactoryRelay((type) => new FixedValuesRequest(type, this.inputValues.ToArray())), + new EqualRequestSpecification(parameter.NotNull(nameof(parameter)))), + new RandomFixedValuesGenerator()) + .ToCustomization(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickNegativeAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickNegativeAttribute.cs index 27728052..d4188c18 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickNegativeAttribute.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/PickNegativeAttribute.cs @@ -1,32 +1,29 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.Attributes -{ - using System; - using System.Reflection; - - using global::AutoFixture; - using global::AutoFixture.Kernel; - using global::AutoFixture.Xunit2; - using Objectivity.AutoFixture.XUnit2.Core.Factories; - using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; - - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class PickNegativeAttribute : CustomizeAttribute - { - private readonly IFactory negativeValuesRequestFactory = new NegativeValuesRequestFactory(); - - public override ICustomization GetCustomization(ParameterInfo parameter) - { - if (parameter is null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - return new CompositeSpecimenBuilder( - new FilteringSpecimenBuilder( - new RequestFactoryRelay(this.negativeValuesRequestFactory.Create), - new EqualRequestSpecification(parameter)), - new RandomFixedValuesGenerator()) - .ToCustomization(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.Factories; + using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class PickNegativeAttribute : CustomizeAttribute + { + private readonly IFactory negativeValuesRequestFactory = new NegativeValuesRequestFactory(); + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new CompositeSpecimenBuilder( + new FilteringSpecimenBuilder( + new RequestFactoryRelay(this.negativeValuesRequestFactory.Create), + new EqualRequestSpecification(parameter.NotNull(nameof(parameter)))), + new RandomFixedValuesGenerator()) + .ToCustomization(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomExceptValuesGenerator.cs b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomExceptValuesGenerator.cs index 9bf1b854..9d6f3661 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomExceptValuesGenerator.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomExceptValuesGenerator.cs @@ -1,47 +1,43 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - - using global::AutoFixture; - using global::AutoFixture.Kernel; - using Objectivity.AutoFixture.XUnit2.Core.Common; - using Objectivity.AutoFixture.XUnit2.Core.Requests; - - internal class RandomExceptValuesGenerator : ISpecimenBuilder - { - [SuppressMessage("Major Bug", "S2583:Conditionally executed code should be reachable", Justification = "Analyzer issue as the code is reachable")] - public object Create(object request, ISpecimenContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (request.NotNull(nameof(request)) is ExceptValuesRequest exceptValuesRequest) - { - var duplicateLimiter = new HashSet(); - object result; - - do - { - result = context.Resolve(exceptValuesRequest.OperandType); - var hasDuplicate = duplicateLimiter.Contains(result); - if (hasDuplicate) - { - throw new ObjectCreationException("The value could not be created. Probably all possible values were excluded."); - } - - duplicateLimiter.Add(result); - } - while (exceptValuesRequest.Values.Contains(result)); - - return result; - } - - return new NoSpecimen(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.Requests; + + internal class RandomExceptValuesGenerator : ISpecimenBuilder + { + [SuppressMessage("Major Bug", "S2583:Conditionally executed code should be reachable", Justification = "Analyzer issue as the code is reachable")] + public object Create(object request, ISpecimenContext context) + { + context.NotNull(nameof(context)); + + if (request.NotNull(nameof(request)) is ExceptValuesRequest exceptValuesRequest) + { + var duplicateLimiter = new HashSet(); + object result; + + do + { + result = context.Resolve(exceptValuesRequest.OperandType); + var hasDuplicate = duplicateLimiter.Contains(result); + if (hasDuplicate) + { + throw new ObjectCreationException("The value could not be created. Probably all possible values were excluded."); + } + + duplicateLimiter.Add(result); + } + while (exceptValuesRequest.Values.Contains(result)); + + return result; + } + + return new NoSpecimen(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomFixedValuesGenerator.cs b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomFixedValuesGenerator.cs index 621d8f3c..291d5286 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomFixedValuesGenerator.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RandomFixedValuesGenerator.cs @@ -1,52 +1,49 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders -{ - using System; - using System.Collections; - using System.Collections.Concurrent; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - - using global::AutoFixture.Kernel; - - using Objectivity.AutoFixture.XUnit2.Core.Common; - using Objectivity.AutoFixture.XUnit2.Core.Requests; - - internal sealed class RandomFixedValuesGenerator : ISpecimenBuilder - { - private readonly ConcurrentDictionary enumerators = new(); - - public object Create(object request, ISpecimenContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (request.NotNull(nameof(request)) is FixedValuesRequest fixedValuesRequest) - { - return this.CreateValue(fixedValuesRequest); - } - - return new NoSpecimen(); - } - - [SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "It is good enough for collection randomization.")] - private static IEnumerator CreateEnumerable(FixedValuesRequest request) - { - var random = new Random(); - - // Stryker disable once all : mutating ordering by random still brings random results - var values = request.Values.OrderBy((_) => random.Next()).ToArray(); - - return new RoundRobinEnumerable(values); - } - - private object CreateValue(FixedValuesRequest request) - { - var generator = this.enumerators.GetOrAdd(request, CreateEnumerable); - generator.MoveNext(); - - return generator.Current; - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + + using global::AutoFixture.Kernel; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.Requests; + + internal sealed class RandomFixedValuesGenerator : ISpecimenBuilder + { + private readonly ConcurrentDictionary enumerators = new(); + + public object Create(object request, ISpecimenContext context) + { + context.NotNull(nameof(context)); + + if (request.NotNull(nameof(request)) is FixedValuesRequest fixedValuesRequest) + { + return this.CreateValue(fixedValuesRequest); + } + + return new NoSpecimen(); + } + + [SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "It is good enough for collection randomization.")] + private static IEnumerator CreateEnumerable(FixedValuesRequest request) + { + var random = new Random(); + + // Stryker disable once all : mutating ordering by random still brings random results + var values = request.Values.OrderBy((_) => random.Next()).ToArray(); + + return new RoundRobinEnumerable(values); + } + + private object CreateValue(FixedValuesRequest request) + { + var generator = this.enumerators.GetOrAdd(request, CreateEnumerable); + generator.MoveNext(); + + return generator.Current; + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RequestFactoryRelay.cs b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RequestFactoryRelay.cs index 4fbbcada..4e2c9182 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RequestFactoryRelay.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/SpecimenBuilders/RequestFactoryRelay.cs @@ -1,64 +1,61 @@ -namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders -{ - using System; - using System.Collections; - using System.Reflection; - - using global::AutoFixture.Kernel; - - using Objectivity.AutoFixture.XUnit2.Core.Common; - - internal sealed class RequestFactoryRelay : ISpecimenBuilder - { - private readonly Func requestFactory; - - public RequestFactoryRelay(Func requestFactory) - { - this.requestFactory = requestFactory.NotNull(nameof(requestFactory)); - } - - public object Create(object request, ISpecimenContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (request.NotNull(nameof(request)) is ParameterInfo parameterInfo) - { - var parameterType = Nullable.GetUnderlyingType(parameterInfo.ParameterType) - ?? parameterInfo.ParameterType; - - return parameterType != typeof(string) - && parameterType.TryGetEnumerableSingleTypeArgument(out var itemType) - ? this.CreateMultiple(parameterType, itemType, context) - : this.CreateSingle(parameterType, context); - } - - return new NoSpecimen(); - } - - private object CreateSingle(Type type, ISpecimenContext context) - { - var transformedRequest = this.requestFactory(type); - return transformedRequest is not null - ? context.Resolve(transformedRequest) - : new NoSpecimen(); - } - - private object CreateMultiple(Type collectionType, Type itemType, ISpecimenContext context) - { - var transformedRequest = this.requestFactory(itemType); - if (transformedRequest is not null - && context.Resolve(new MultipleRequest(transformedRequest)) is IEnumerable elements) - { - var items = elements.ToTypedArray(itemType); - return collectionType.IsArray || collectionType.IsAbstract - ? items - : Activator.CreateInstance(collectionType, items); - } - - return new NoSpecimen(); - } - } -} +namespace Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders +{ + using System; + using System.Collections; + using System.Reflection; + + using global::AutoFixture.Kernel; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + + internal sealed class RequestFactoryRelay : ISpecimenBuilder + { + private readonly Func requestFactory; + + public RequestFactoryRelay(Func requestFactory) + { + this.requestFactory = requestFactory.NotNull(nameof(requestFactory)); + } + + public object Create(object request, ISpecimenContext context) + { + context.NotNull(nameof(context)); + + if (request.NotNull(nameof(request)) is ParameterInfo parameterInfo) + { + var parameterType = Nullable.GetUnderlyingType(parameterInfo.ParameterType) + ?? parameterInfo.ParameterType; + + return parameterType != typeof(string) + && parameterType.TryGetEnumerableSingleTypeArgument(out var itemType) + ? this.CreateMultiple(parameterType, itemType, context) + : this.CreateSingle(parameterType, context); + } + + return new NoSpecimen(); + } + + private object CreateSingle(Type type, ISpecimenContext context) + { + var transformedRequest = this.requestFactory(type); + return transformedRequest is not null + ? context.Resolve(transformedRequest) + : new NoSpecimen(); + } + + private object CreateMultiple(Type collectionType, Type itemType, ISpecimenContext context) + { + var transformedRequest = this.requestFactory(itemType); + if (transformedRequest is not null + && context.Resolve(new MultipleRequest(transformedRequest)) is IEnumerable elements) + { + var items = elements.ToTypedArray(itemType); + return collectionType.IsArray || collectionType.IsAbstract + ? items + : Activator.CreateInstance(collectionType, items); + } + + return new NoSpecimen(); + } + } +}