From 830ca380ddfcbfb5e9965176d26bf04c5d44baba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 18 Oct 2026 11:44:42 +0200 Subject: [PATCH] docs: add an agent guide CLAUDE.md and the Copilot instructions had drifted from the code (old API names, a removed attribute, a missing solution file) and each served only one tool. A single AGENTS.md with the hard constraints, plus deeper reference docs read on demand, gives every agent and contributor one verified source to keep current. Docs/agents/design-goals.md gives every change the same priorities. Correctness is a must; the other goals are strong defaults and win in this order when they conflict: - Stable and compatible - AOT and trimming friendly - A consistent, discoverable and pleasant API - Fast, especially on the happy path - Extensible - Errors at compile time rather than at run time - Documented and pleasant to read, using Docusaurus features where they help It also sets the code rules (modern C# within the target frameworks, async rules including ConfigureAwait(false), patterns to avoid), the checks before a commit (including no new Sonar findings and asking before suppressing one), and how to handle review findings. Goals of this repository: - Every mockable shape compiles and behaves - One entry point and one verb surface - Consistent vocabulary - Allocation-free hot path - Invisible generated code - AOT by construction, within the Roslyn floor - Correct incremental caching --- .github/copilot-instructions.md | 238 ------------------------------- AGENTS.md | 54 +++++++ CLAUDE.md | 118 --------------- Docs/agents/design-goals.md | 62 ++++++++ Docs/agents/project-structure.md | 85 +++++++++++ Docs/agents/workflows.md | 137 ++++++++++++++++++ 6 files changed, 338 insertions(+), 356 deletions(-) delete mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md create mode 100644 Docs/agents/design-goals.md create mode 100644 Docs/agents/project-structure.md create mode 100644 Docs/agents/workflows.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index d982e7690..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,238 +0,0 @@ -# Copilot Instructions for Mockolate - -## Project Overview - -Mockolate is a modern, strongly-typed mocking library for .NET, powered by source generators. It enables fast, compile-time validated mocks for interfaces and classes with full IntelliSense support. - -**Key characteristics:** -- Source generator-based (no runtime proxy generation) -- Strongly-typed with compile-time safety -- AOT compatible (NativeAOT and trimming) -- Supports .NET Standard 2.0, .NET 8, .NET 10, and .NET Framework 4.8 - -## Architecture - -The repository is organized into several key components: - -### Source Projects -- **Mockolate**: Main library providing the mocking API -- **Mockolate.SourceGenerators**: Source generators that create mock implementations at compile time -- **Mockolate.Analyzers**: Roslyn analyzers for compile-time validation -- **Mockolate.Analyzers.CodeFixers**: Code fix providers for analyzer diagnostics - -### Test Projects -- **Mockolate.Tests**: Core unit tests for the library -- **Mockolate.SourceGenerators.Tests**: Tests for source generators -- **Mockolate.Analyzers.Tests**: Tests for analyzers -- **Mockolate.Api.Tests**: API compatibility tests -- **Mockolate.ExampleTests**: Example usage tests -- **Aot/Mockolate.AotCompatibility.TestApp**: AOT compatibility validation - -## Build System - -The project uses **Nuke Build** for automation. Build scripts are available for all platforms: -- `build.sh` (Linux/macOS) -- `build.ps1` (Windows PowerShell) -- `build.cmd` (Windows Command Prompt) - -### Common Build Targets -```bash -./build.sh # Default build -./build.sh CodeCoverage # Build and run tests with coverage -./build.sh ApiChecks # Run API compatibility tests -./build.sh CodeAnalysis # Run SonarCloud analysis -./build.sh MutationTests # Run Stryker mutation tests -./build.sh Benchmarks # Run performance benchmarks -./build.sh Pack # Create NuGet packages -``` - -### Building and Testing -```bash -# Quick build -dotnet build Mockolate.sln - -# Run all tests -dotnet test Mockolate.sln - -# Run specific test project -dotnet test Tests/Mockolate.Tests/Mockolate.Tests.csproj -``` - -## Coding Standards - -### General Conventions -- Use **tabs** for indentation (tab width: 4) -- Follow `.editorconfig` settings strictly -- All code must be covered by unit tests -- Maintain > 90% code coverage -- Follow conventional commits for commit messages - -### C# Conventions -- **DO NOT** use `var` - always use explicit types -- Use expression-bodied members for accessors, properties, methods, and lambdas -- Prefer pattern matching over `is` with cast or `as` with null check -- Use language keywords (`int`, `string`) over BCL types (`Int32`, `String`) -- Sort usings alphabetically with `System` namespaces first -- Do not use `this.` qualification - -### Naming Conventions -- Use PascalCase for public members -- Use camelCase with leading underscore prefix for private fields (e.g., `_fieldName`) -- Use meaningful, descriptive names - -### Testing Standards -- Use **xUnit** for all tests -- Test class names should end with `Tests` (e.g., `MockabilityAnalyzerTests`) -- Test method names should be descriptive using `When_Should` pattern or similar -- Use `[Fact]` for simple tests, `[Theory]` with `[InlineData]` for parameterized tests -- Arrange-Act-Assert pattern is preferred -- Each test should verify a single behavior - -## Source Generator Development - -When working on source generators: -- Source generators target .NET Standard 2.0 -- Use incremental generators (`IIncrementalGenerator`) for performance -- Test source generators using the `Microsoft.CodeAnalysis.Testing` framework -- Ensure generated code compiles and passes all tests -- Generated code should be readable and follow the same coding standards - -## Analyzer Development - -When working on analyzers: -- Analyzers target .NET Standard 2.0 -- Create corresponding code fixers when appropriate -- Write comprehensive tests for analyzers using `CSharpAnalyzerVerifier` -- Follow Roslyn analyzer guidelines -- Provide clear, actionable diagnostic messages - -## API Surface - -The library provides several key APIs: - -### Mock Creation -- `Mock.Create()` - Create mocks for interfaces or classes -- `Mock.Factory` - Create factory for shared behavior -- `BaseClass.WithConstructorParameters()` - For classes without default constructors - -### Setup -- `mock.SetupMock.Method.*` - Configure method behavior -- `mock.SetupMock.Property.*` - Configure property behavior -- `mock.SetupMock.Indexer()` - Configure indexer behavior -- `mock.SetupMock.Delegate(...)` - Configure delegate behavior (for Action, Func, and custom delegates) - -### Verification -- `mock.VerifyMock.Invoked.*` - Verify method calls -- `mock.VerifyMock.Invoked(...)` - Verify delegate invocations (for Action, Func, and custom delegates) -- `mock.VerifyMock.Got.*` - Verify property reads -- `mock.VerifyMock.Set.*` - Verify property writes -- `mock.VerifyMock.GotIndexer()` - Verify indexer reads -- `mock.VerifyMock.SetIndexer()` - Verify indexer writes - -### Events -- `mock.RaiseOnMock.*` - Raise events on mocks -- `mock.VerifyMock.SubscribedTo.*` - Verify event subscriptions -- `mock.VerifyMock.UnsubscribedFrom.*` - Verify event unsubscriptions - -## Quality Standards - -### Static Code Analysis -- SonarCloud analysis must pass with no issues -- Quality gate requires: - - All SonarCloud issues resolved - - Code coverage > 90% - - No code smells or vulnerabilities - -### Mutation Testing -- Stryker mutation testing is run on main builds -- Aim for high mutation score to ensure test quality - -### Multi-Platform Testing -- Tests run on Ubuntu, Windows, and macOS -- Ensure cross-platform compatibility -- Use platform-agnostic file paths and APIs - -## Dependencies - -- Use Central Package Management (Directory.Packages.props) -- All assemblies are strong-named -- Keep dependencies minimal -- Prefer Microsoft.* packages when available - -## Common Tasks - -### Adding a New Feature -1. Start with tests (TDD approach) -2. Implement in the appropriate project (Mockolate, SourceGenerators, or Analyzers) -3. Update documentation if API surface changes -4. Ensure all tests pass and coverage remains > 90% -5. Run full build with `./build.sh CodeCoverage` - -### Fixing a Bug -1. Write a failing test that reproduces the bug -2. Fix the issue with minimal code changes -3. Verify the test passes -4. Check for similar issues elsewhere -5. Update related documentation if needed - -### Adding Tests -- Tests must be in the appropriate test project -- Follow existing test patterns -- Use descriptive test names -- Ensure tests are deterministic and fast -- Mock external dependencies - -## CI/CD - -The project uses GitHub Actions with several workflows: - -### build.yml (Main Build) -- Runs on pushes to main and tags -- Executes unit tests on Ubuntu, Windows, macOS -- Runs API tests -- Performs mutation testing -- Runs SonarCloud analysis -- Creates NuGet packages -- Publishes to NuGet on version tags - -### ci.yml -- Runs on pull requests -- Quick validation build - -### ci-analysis.yml -- Additional code quality checks - -## Pull Request Guidelines - -- Follow conventional commits format for PR titles -- Include clear description of changes -- All tests must pass -- Coverage must remain > 90% -- No SonarCloud issues -- Link related issues -- Update documentation for API changes - -## Documentation - -- README is the GitHub/NuGet landing page (pitch + quick start + links to docs); full reference docs live under `Docs/pages/` and are published to https://docs.testably.org/Mockolate/ -- Keep `Docs/pages/` up to date with API changes; the README's link index should match the published structure -- Document public APIs with XML comments -- Examples should be in Mockolate.ExampleTests -- Follow existing documentation style - -## Troubleshooting - -### Source Generator Issues -- Clean and rebuild: `dotnet clean && dotnet build` -- Check generated files in `obj` directories -- Enable detailed logging with MSBuild verbosity - -### Test Failures -- Run tests individually to isolate issues -- Check for platform-specific issues -- Verify test data and assumptions - -### Build Issues -- Ensure correct .NET SDK version (10.0.100) -- Check global.json for SDK requirements -- Verify all NuGet packages are restored diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..cf1d42e35 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# Mockolate Agent Guide + +Mockolate is a source-generated mocking library for .NET. Users write `IFoo.CreateMock()`; a Roslyn incremental generator emits the mock at compile time, analyzers validate the usage, and the runtime library records interactions, evaluates setups and answers verifications. Read the docs under [References](#references) only when a task needs them. + +## SDK and Build + +- Use the SDK pinned in `global.json` (.NET 10, `rollForward: latestMinor`). Tests also run on .NET 8, and on .NET Framework 4.8 on Windows only. +- Build from the root with `dotnet build Mockolate.slnx`. Shared settings live in the root `Directory.Build.props` (strong naming, `MinimumRoslynVersion`), `Source/Directory.Build.props` (target frameworks, warnings as errors, AOT and trimming flags) and `Tests/Directory.Build.props` (test frameworks, xUnit, aweXpect). +- Package versions belong only in `Directory.Packages.props`. `VersionOverride` is used only to pin Roslyn packages to `$(MinimumRoslynVersion)`. + +## Design Goals + +Correctness is a must. The other goals are strong defaults and win in this order when they conflict: stable and compatible, AOT and trimming friendly, a consistent and pleasant API, a fast happy path, extensible, errors at compile time, documented. [Docs/agents/design-goals.md](Docs/agents/design-goals.md) explains each goal, the code rules, the checks before a commit and the goals of this repository. + +## Project Constraints + +- **Target frameworks**: `Mockolate` ships `netstandard2.0`, `net8.0` and `net10.0`. The generator, analyzers and code fixers target `netstandard2.0` only. Code compiled for `netstandard2.0` or `net48` has no `IsExternalInit`, `System.Index` or `System.Range` unless polyfilled, so records, `init` and list patterns need care: `Mockolate` polyfills `IsExternalInit`, `Mockolate.Tests` does not, and guards such code with `#if NET8_0_OR_GREATER`. +- **Roslyn version**: the generator, analyzers and code fixers compile against `MinimumRoslynVersion` (5.0.0), the oldest compiler they must load in. Never use a newer Roslyn API. It cannot drop below 5.0 because the emitted `Mock.g.cs` uses a C# 14 extension block. +- **Generated code** runs inside every consumer's compilation. Keep it reflection-free, fully qualified with `global::`, valid under nullable and warnings-as-errors, and deterministic. Keep the generator incremental: pipeline entities must have value equality (`EquatableArray`, records) so caching works; `MockGenerator.EqualityTests` covers this. +- **Analyzers**: rule IDs are `MockolateNNNN`. Add the descriptor in `Rules.cs`, its texts in `Resources.resx`, the row in `AnalyzerReleases.Unshipped.md` and the user docs in `Docs/pages/08-analyzers.md`. +- **AOT and trimming**: the runtime library sets `IsAotCompatible` and `IsTrimmable` for `net8.0`+ with warnings as errors. Do not add reflection-based dispatch to the library or the generated code. +- **Performance**: recording, setup matching and verification are hot paths with allocation-free fast paths (`FastMockInteractions`, the `Fast*Buffer` types). Measure hot-path changes with the benchmarks before and after. +- **Public API**: every change to the public surface of `Mockolate` shows up in the API snapshots. Document new public members with XML comments. Mark breaking changes with `!` in the PR title (`feat!:`, `refactor!:`). +- **Strong naming**: all assemblies are signed with `strongname.snk`; an `InternalsVisibleTo` needs `PublicKey="$(PublicKey)"`. +- **Style**: follow `.editorconfig`: tabs, explicit types instead of `var`, `_camelCase` private fields, no `this.`, `System` usings first. Tests use xUnit with aweXpect (`await That(...)`). + +## Validation + +- Run the tests for what you changed before pushing; see [workflows](Docs/agents/workflows.md#which-tests-to-run). +- Generator output changes must update the snapshots in `Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected`: run the explicit `AcceptSnapshotChanges` test and review the diff. New generator special cases go into `Tests/Mockolate.Tests/GeneratorCoverage`, which both test projects share. +- Public API changes must update `Tests/Mockolate.Api.Tests/*.awexpect-snapshot.txt` (one per target framework). Never commit `*.awexpect-snapshot.pending.txt`. +- Analyzer and code fix changes need tests in `Tests/Mockolate.Analyzers.Tests`. +- Changes in `Pipeline/BenchmarkReport.cs` need `Tests/Build.Tests`. +- Coverage must stay above 90% and SonarCloud must report no issues. + +## Repository Conventions + +- Files are stored with LF and checked out with CRLF on Windows (`.gitattributes` `text=auto`, `.editorconfig` `end_of_line = crlf`). Raw string literals take the file's line endings, so compare multi-line strings with `.IgnoringNewlineStyle()`. +- Some files start with a UTF-8 BOM and most do not. Keep each file's encoding: prefer the edit tools over PowerShell rewrites (`Set-Content`, `Out-File`), which add or drop the BOM depending on version and `-Encoding`, and check `git diff` for a changed U+FEFF. +- User docs live in `Docs/pages/` and are published to https://docs.testably.org/Mockolate/. The README is the NuGet landing page and links there. Update both for user-visible API changes. + +## Git Workflow + +- Branch from `main` and open a pull request. PRs are squash-merged, so the PR title becomes the commit subject and must follow conventional commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `ci:`, `chore:`). +- Commit messages: the subject is the PR title, the body explains why (not what or how), each paragraph is a single line, and there are no co-author or attribution trailers. +- Never commit `.claude/`, `Artifacts/`, `TestResults/` or pending snapshots. +- Dependabot PRs use `chore:` and are auto-merged by Mergify once unit and API tests pass. Pushing a `vX.Y.Z` tag publishes to nuget.org; do not create tags unless asked. +- Check review findings against the code before acting on them. + +## References + +- [Design goals](Docs/agents/design-goals.md): the goals and code rules for every change, the checks before a commit and the goals of this repository. +- [Workflows](Docs/agents/workflows.md): build, test, snapshot, benchmark, mutation, AOT and documentation commands, the Nuke targets in `Pipeline/` and the CI workflows. +- [Project structure](Docs/agents/project-structure.md): projects, architecture and where code lives. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1110a2809..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,118 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What This Project Is - -Mockolate is a strongly-typed .NET mocking library powered by **Roslyn source generators**. Unlike reflection-based mocking libraries, it generates mock implementations at compile time, providing full IntelliSense support and AOT compatibility. - -**Supported targets**: .NET Standard 2.0, .NET 8, .NET 10, .NET Framework 4.8 - -## Build and Test Commands - -```bash -# Build -dotnet build Mockolate.slnx - -# Run all tests -dotnet test Mockolate.slnx - -# Run a specific test project -dotnet test Tests/Mockolate.Tests/Mockolate.Tests.csproj - -# Run a single test (by name filter) -dotnet test Tests/Mockolate.Tests/Mockolate.Tests.csproj --filter "FullyQualifiedName~MethodName" - -# Full build with coverage (requires Nuke) -./build.sh CodeCoverage # Linux/macOS -./build.ps1 CodeCoverage # Windows PowerShell - -# Other Nuke targets -./build.sh ApiChecks # API compatibility tests -./build.sh MutationTests # Stryker mutation tests -./build.sh Pack # Create NuGet packages -``` - -**Requires**: .NET 10 SDK (see `global.json`). For source generator changes, run `dotnet clean && dotnet build` to force regeneration. - -## Architecture - -The repository has four source projects that work together: - -### Source/Mockolate -The main runtime library. Users reference this package directly. Key responsibilities: -- `MockRegistry` — central store for behaviors, setups, and recorded interactions per mock instance -- `MockBehavior` — controls strict vs. loose behavior -- `Setup*` / `Verify*` extension methods — fluent API for configuring and verifying mocks -- `It.*` parameter matchers (e.g., `It.IsAny()`, `It.Is(predicate)`) -- Exception types: `MockException`, `MockNotSetupException`, `MockVerificationException`, `MockVerificationTimeoutException` -- `Web/` — special support for mocking `HttpClient` via its handler - -### Source/Mockolate.SourceGenerators -An `IIncrementalGenerator` that runs at compile time and generates `Mock.{TypeName}.g.cs` files in `obj/`. Given a `[GenerateMock]` attribute or `Mock.Create()` usage, it generates a concrete class implementing the target interface or class. The generator: -- Handles interfaces, abstract classes, and delegates -- Emits method overrides that delegate to `MockRegistry` -- Targets .NET Standard 2.0 (Roslyn constraint) - -### Source/Mockolate.Analyzers + Source/Mockolate.Analyzers.CodeFixers -Roslyn analyzers that validate mock usage at compile time: -- `MockabilityAnalyzer` — checks that `T` in `Mock.Create()` can actually be mocked -- `UseVerificationAnalyzer` — enforces that verifications are properly structured -Both target .NET Standard 2.0. Code fixers provide IDE quick-fixes for diagnostics. - -### How the pieces connect - -``` -User writes: IFoo sut = IFoo.CreateMock(); - sut.Mock.Setup.Bar(...).Returns(...); - sut.Mock.Verify.Bar(...).Once(); - -At compile time: - SourceGenerator → generates Mock.IFoo.g.cs (implements IFoo, wires into MockRegistry) - Analyzers → validate that IFoo is mockable and setup/verify calls are correct - -At runtime: - IFoo.CreateMock() → returns a IFoo instance - Method calls → recorded in MockRegistry, returns configured behavior - Verify calls → query MockRegistry for recorded interactions -``` - -### Test Projects - -| Project | Purpose | -|---|---| -| `Mockolate.Tests` | Core behavioral tests (methods, properties, indexers, events, delegates, HttpClient) | -| `Mockolate.SourceGenerators.Tests` | Generator output tests using `Microsoft.CodeAnalysis.Testing` | -| `Mockolate.Analyzers.Tests` | Analyzer diagnostic tests using `CSharpAnalyzerVerifier` | -| `Mockolate.Api.Tests` | API surface snapshots (aweXpect.Snapshots.PublicApi) — catches unintentional breaking changes | -| `Mockolate.ExampleTests` | Living documentation showing idiomatic usage | -| `Mockolate.AotCompatibility.TestApp` | AOT publish smoke test | - -## Coding Standards - -- **No `var`** — always use explicit types -- **Tabs** for indentation (width 4), per `.editorconfig` -- **Private fields**: `_camelCase` prefix -- **Public members**: PascalCase -- No `this.` qualification -- Expression-bodied members for single-expression accessors/methods -- Pattern matching preferred over `is`/`as` casts -- `using` directives: alphabetical, `System.*` first -- Language keywords (`int`, `string`) over BCL type names - -## Quality Gates - -All PRs must pass: -- Code coverage **> 90%** -- **Zero SonarCloud issues** -- Multi-platform test run (Ubuntu, Windows, macOS via GitHub Actions) -- API compatibility check (snapshot diff in `Mockolate.Api.Tests`) - -## Key Conventions - -- **Conventional commits** required for PR titles (`feat:`, `fix:`, `chore:`, etc.) -- New public API must have XML doc comments -- All assemblies are **strong-named** (`Directory.Build.props`) -- Package versions managed centrally in `Directory.Packages.props` -- Documentation lives in `Docs/pages/`; the README links to it for reference content. Published at https://docs.testably.org/Mockolate/ -- Examples go in `Mockolate.ExampleTests`, not inline in test helper files \ No newline at end of file diff --git a/Docs/agents/design-goals.md b/Docs/agents/design-goals.md new file mode 100644 index 000000000..48c4abb2b --- /dev/null +++ b/Docs/agents/design-goals.md @@ -0,0 +1,62 @@ +# Design goals + +These goals apply to every Testably repository. They guide agents and contributors when a change has more than one reasonable solution. The goals of this repository at the end refine them. + +Correctness is a hard requirement. Every other goal is a strong default: follow it, and when a change has to break one, say so in the pull request and explain why. + +## Decision test + +Before proposing a change, ask: does it keep the library correct, and does it make it faster, more consistent, easier to extend or more pleasant to use? If the answer is no, reconsider the change. + +## Goals + +When goals conflict, the earlier one wins. + +1. **Correct, with clear failures (must).** Behaviour is exact and covered by tests. A failure states what was expected and what was found, and tests pin the complete message. +2. **Stable and compatible.** Follow semantic versioning: breaking changes only in a major version, on purpose and documented. Public API changes show up in the API snapshots and are reviewed. Keep dependencies minimal and their version floors low, and support the declared target frameworks. +3. **AOT and trimming friendly.** No unguarded reflection: prefer source generators, otherwise put reflection behind a feature guard with trimming annotations. A change that touches reflection runs the AOT smoke tests. +4. **A consistent, discoverable and pleasant API.** Extension methods exist only on the types they apply to, so IntelliSense shows what makes sense. Naming, option chaining and null handling follow the existing patterns (see the repository-specific rules). Every public member has XML documentation. Prefer minimal ceremony for the user: sensible defaults, no boilerplate, failure messages that read well. +5. **Fast, especially on the happy path.** Succeeding calls are the common case. Avoid allocations in hot paths, and back every performance claim or change with a benchmark before and after. +6. **Extensible.** Third parties build on the same extension points the library uses itself. New features use the public extension surface instead of internals, and the core stays small and stable. +7. **Errors at compile time rather than at run time.** Prefer analyzers, source generators and types that make misuse a compiler error over runtime checks. +8. **Documented and pleasant to read.** A public feature is not done until it is documented on the docs site, with examples that are tested where the repository can test them. The docs are rendered with Docusaurus: use its features where they help the reader, such as admonitions (`:::note`, `:::tip`, `:::warning`) for remarks, tabs for alternatives (for example synchronous and asynchronous usage, or one tab per test framework) and collapsible `
` for long output, and follow the style of the existing pages. + +## Code + +- **Modern C#.** Use current language features and APIs where the target frameworks allow them, with polyfills for netstandard2.0 and net48 instead of avoiding a feature. Check that the oldest target framework still compiles. +- **Async.** + - Use `ValueTask` for operations that often complete synchronously. + - Pass the `CancellationToken` through every async path. + - Use `ConfigureAwait(false)` on every `await` in library code, so that callers who block on a result cannot deadlock on a single-threaded synchronization context. Existing code does not follow this everywhere yet; new and changed code does. + - Do not block on async code (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`), except at a deliberate synchronous API boundary, and complete synchronously there where possible. + - Do not use `Task.Run` to make code look asynchronous; use it only where the real counterpart does work in the background. +- **Avoid.** + - Catching exceptions without handling or rethrowing them. + - LINQ, closures and boxing in hot paths; use loops and cached delegates there. + - String concatenation in loops; use a `StringBuilder`. + +## Before you commit + +Run the checks that match the change: + +- **Any code change:** the tests of the affected projects, on the oldest and the newest target framework. +- **Public API:** the API snapshot tests; review and accept intentional changes, never commit pending snapshots. +- **Reflection, trimming or AOT:** the AOT smoke tests. +- **Hot path or a performance claim:** a benchmark before and after. +- **New public feature:** documentation and its examples. +- **Failure messages:** tests that pin the complete message. +- **Static analysis:** no new Sonar or analyzer findings. Avoid the common ones: high cognitive complexity, duplicated string literals, unused members and parameters, methods that could be static, long parameter lists. Do not suppress a finding without asking the maintainer first. + +## Reviews + +Verify every review finding against the code before changing anything. If you disagree, push back once with concrete reasoning; if the reviewer or the maintainer confirms the finding, implement it. + +## Goals of this repository + +1. **Every mockable shape compiles and behaves.** Shapes that cannot be mocked are reported by the analyzer, never as errors in generated code. +2. **One entry point and one verb surface.** Mocks come from `CreateMock()`, and new capabilities fit under the existing `sut.Mock.…` verbs. +3. **Consistent vocabulary** for setups, verifications and matchers, across every arity. +4. **Allocation-free hot path.** Recording and counting do not allocate, and concurrency never loses an interaction. +5. **Invisible generated code.** Clash-free, fully qualified and hidden from IntelliSense. +6. **AOT by construction, within the Roslyn floor.** No Reflection.Emit, expression trees or runtime generic instantiation, and nothing newer than `MinimumRoslynVersion`. +7. **Correct incremental caching.** Value-equatable pipeline models only. diff --git a/Docs/agents/project-structure.md b/Docs/agents/project-structure.md new file mode 100644 index 000000000..a4bc65fad --- /dev/null +++ b/Docs/agents/project-structure.md @@ -0,0 +1,85 @@ +# Project Structure + +## Layout + +| Folder | Content | +|---|---| +| `Source/` | The shipped projects | +| `Tests/` | Test projects and the AOT smoke test app | +| `Benchmarks/` | BenchmarkDotNet comparison with other mocking libraries | +| `Pipeline/` | Nuke build (`Build.csproj`) and the benchmark report generator | +| `Docs/pages/` | User docs published to https://docs.testably.org/Mockolate/ | +| `Docs/agents/` | Guides for contributors and agents (this folder) | +| `.github/` | CI workflows, Dependabot and Mergify configuration | + +## Source Projects + +Only the `Mockolate` package is published. It carries the generator, analyzers and code fixers in `analyzers/dotnet/cs`. + +### `Source/Mockolate` + +The runtime library (`netstandard2.0`, `net8.0`, `net10.0`). + +| Location | Responsibility | +|---|---| +| `MockRegistry*.cs` | Per-mock store for the behavior, setups and recorded interactions; generated mocks delegate every member access to it | +| `MockBehavior.cs`, `MockBehaviorExtensions.cs`, `Behavior/` | Loose or strict behavior, default values, base class handling, constructor parameters | +| `Setup/` | Method, property, indexer and event setups, callbacks, ref struct setups | +| `Interactions/` | Recorded interactions; `FastMockInteractions` and the `Fast*Buffer` types are the allocation-sensitive recording path | +| `Verify/` | Verification results and count sources | +| `It*.cs`, `Match*.cs`, `Parameters/` | Parameter matchers (`It.IsAny()`, `It.Is(...)`, `It.Matches(...)`, ...) | +| `Monitor/` | `MockMonitor` for interactions within a scope | +| `Web/` | `HttpClient` support: setups, verifications and `It` extensions for requests and content | +| `Exceptions/` | `MockException`, `MockNotSetupException`, `MockVerificationException`, `MockVerificationTimeoutException` | +| `Internals/` | Helpers and polyfills for older frameworks | + +### `Source/Mockolate.SourceGenerators` + +An `IIncrementalGenerator` (`MockGenerator.cs`, `netstandard2.0`). + +- It always emits `Mock.g.cs`, which declares `CreateMock` in a C# 14 extension block. +- It finds `CreateMock()` invocations in the global `Mockolate` namespace (`MockGeneratorHelpers.cs`), including chained `.Implementing()` calls, and emits one `Mock..g.cs` per mocked type or type combination. +- Shared files depend on what the mocks need: `MethodSetups.g.cs`, `IndexerSetups.g.cs`, `ActionFunc.g.cs`, `ReturnsThrowsAsyncExtensions.g.cs`, `RefStructMethodSetups.g.cs`, `MockBehaviorExtensions.g.cs`, `Mock.AsExtensions.g.cs`. +- `Entities/` holds the equatable model extracted from symbols (`Class`, `Method`, `Property`, `Event`, `EquatableArray`, ...); `Internals/EntityCache.cs` deduplicates it per compilation. +- `Sources/` turns the model into C# with `StringBuilder`. +- Output depends on the consumer's compilation, for example whether `OverloadResolutionPriorityAttribute` exists. + +### `Source/Mockolate.Analyzers` and `Source/Mockolate.Analyzers.CodeFixers` + +`netstandard2.0`, compiled against `MinimumRoslynVersion`. + +| Rule | Analyzer | Meaning | +|---|---|---| +| `Mockolate0001` | `UseVerificationAnalyzer` | A verification result must be used (code fix: `UseVerificationCodeFixProvider`) | +| `Mockolate0002` | `MockabilityAnalyzer` | The type passed to `CreateMock()` or `Implementing()` must be mockable | +| `Mockolate0003` | `MockabilityAnalyzer` | Ref struct usage is not supported on this compilation (descriptor declared in the analyzer for release tracking) | + +## How the Pieces Connect + +``` +User code: IFoo sut = IFoo.CreateMock(); + sut.Mock.Setup.Bar(It.IsAny()).Returns(42); + sut.Bar(1); + sut.Mock.Verify.Bar(It.IsAny()).Once(); + +Compile time: generator -> Mock.IFoo.g.cs implements IFoo and routes every member to MockRegistry + analyzers -> IFoo is mockable, the verification result is used + +Run time: member call -> recorded in MockRegistry, answered by the matching setup or the behavior + Verify -> counts the matching recorded interactions +``` + +Other entry points on `sut.Mock` include `Raise` (events), `As()` (other implemented interfaces), `Monitor()` and `VerifySetup`. + +## Test Projects + +| Project | Purpose | +|---|---| +| `Tests/Mockolate.Tests` | Behavior tests against generated mocks (methods, properties, indexers, events, delegates, ref structs, `HttpClient`). `GeneratorCoverage/` holds the types that exercise generator special cases | +| `Tests/Mockolate.Internal.Tests` | Tests of internal types (`InternalsVisibleTo`) | +| `Tests/Mockolate.SourceGenerators.Tests` | Generator tests on in-memory compilations, incremental caching tests and the output snapshots in `Snapshot/` | +| `Tests/Mockolate.Analyzers.Tests` | Analyzer and code fix tests with `Microsoft.CodeAnalysis.Testing` verifiers in `Verifiers/` | +| `Tests/Mockolate.Api.Tests` | Public API snapshots per target framework | +| `Tests/Mockolate.ExampleTests` | Realistic usage examples (file system, Azure SDK clients) | +| `Tests/Build.Tests` | Tests for `Pipeline/BenchmarkReport.cs`, which it compiles in | +| `Tests/Aot/Mockolate.AotCompatibility.TestApp` | Native AOT publish smoke test | diff --git a/Docs/agents/workflows.md b/Docs/agents/workflows.md new file mode 100644 index 000000000..ceddf8d94 --- /dev/null +++ b/Docs/agents/workflows.md @@ -0,0 +1,137 @@ +# Development Workflows + +Commands run from the repository root. The examples use PowerShell; on Linux and macOS use `./build.sh` instead of `./build.ps1`. + +## Build + +```powershell +dotnet build Mockolate.slnx +``` + +The test projects reference the generator as an analyzer through a project reference, so a normal build regenerates their mocks. If an IDE keeps showing stale generated members, run `dotnet clean` and build again. + +## Test + +```powershell +# One project, all of its target frameworks +dotnet test Tests/Mockolate.Tests + +# One framework and a name filter (VSTest filter syntax) +dotnet test Tests/Mockolate.Tests -f net10.0 --filter "FullyQualifiedName~IsTrueTests" + +# Everything in the solution +dotnet test Mockolate.slnx +``` + +Target frameworks per test project: + +| Project | Frameworks | +|---|---| +| `Mockolate.Tests`, `Mockolate.Internal.Tests`, `Mockolate.ExampleTests` | `net10.0`, `net8.0`, `net48` (Windows only) | +| `Mockolate.SourceGenerators.Tests`, `Mockolate.Analyzers.Tests`, `Build.Tests` | `net10.0` | +| `Mockolate.Api.Tests` | `net8.0` | + +A newer language feature can compile for `net10.0` and fail for `net48`; build or test `net48` too when you use one. + +### Which tests to run + +| Change in | Run | +|---|---| +| `Source/Mockolate` | `Mockolate.Tests`, `Mockolate.Internal.Tests`, `Mockolate.Api.Tests` | +| `Source/Mockolate.SourceGenerators` | `Mockolate.SourceGenerators.Tests`, `Mockolate.Tests` (it compiles the generated mocks) | +| `Source/Mockolate.Analyzers*` | `Mockolate.Analyzers.Tests` | +| Usage examples | `Mockolate.ExampleTests` (not part of the `UnitTests` target, so CI does not run it) | +| `Pipeline/BenchmarkReport.cs` | `Build.Tests` | + +## Snapshots + +### Generator output + +`Tests/Mockolate.SourceGenerators.Tests/Snapshot/MockGenerationSnapshotTests.cs` runs the generator for each scenario and compares every emitted `.g.cs` file with `Snapshot/Expected//`. The inputs are the files in `Tests/Mockolate.Tests/GeneratorCoverage`. `[DebuggerNonUserCode]` is emitted only by a Release build of the generator and is stripped before comparing. + +After an intended change, overwrite the expected files with the explicit acceptance test and review the diff: + +```powershell +dotnet test Tests/Mockolate.SourceGenerators.Tests --filter "FullyQualifiedName~AcceptSnapshotChanges" -- xUnit.Explicit=only +git diff -- Tests/Mockolate.SourceGenerators.Tests/Snapshot +``` + +To add a scenario, add an entry to `Scenarios` and run the acceptance test. + +### Public API + +`Tests/Mockolate.Api.Tests` uses aweXpect.Snapshots.PublicApi. It reads the built `Mockolate.dll` of every target framework listed in `Source/Directory.Build.props` (in the same configuration as the tests) and compares it with `ApiApprovalTests.Mockolate..awexpect-snapshot.txt`. On a change it writes `*.awexpect-snapshot.pending.txt` next to the snapshot. To accept: + +- rename the pending file to the snapshot name, or +- run the explicit `AcceptApiChanges` test, or +- run the tests with `AWEXPECT_ACCEPT_SNAPSHOTS=all`. + +```powershell +dotnet test Tests/Mockolate.Api.Tests --filter "FullyQualifiedName~AcceptApiChanges" -- xUnit.Explicit=only +``` + +Commit the updated snapshots with the change; never commit pending files. + +## AOT Smoke Test + +`Tests/Aot/Mockolate.AotCompatibility.TestApp` publishes a mock with Native AOT. CI does not run it, so publish it manually after changes to generated code or reflection-sensitive library code: + +```powershell +dotnet publish Tests/Aot/Mockolate.AotCompatibility.TestApp -c Release -f net10.0 +``` + +The native link step needs the platform toolchain (on Windows the Visual Studio C++ build tools). Trim and AOT warnings appear before linking. + +## Benchmarks + +`Benchmarks/Mockolate.Benchmarks` compares Mockolate with Moq, NSubstitute, FakeItEasy, TUnit.Mocks and Imposter using BenchmarkDotNet (`MediumRun`, in-process toolchain, `MemoryDiagnoser`). Run a single class in Release: + +```powershell +./build.ps1 Benchmarks --benchmark-filter "*MockCreationBenchmarks*" +# or directly +dotnet run -c Release --project Benchmarks/Mockolate.Benchmarks -- --filter "*MockCreationBenchmarks*" +``` + +CI runs one job per class: `CallbackBenchmarks`, `CompleteEventBenchmarks`, `CompleteIndexerBenchmarks`, `CompleteMethodBenchmarks`, `CompletePropertyBenchmarks`, `MockCreationBenchmarks`. A new benchmark class must be added to the matrix in both `.github/workflows/ci.yml` and `build.yml`. On PRs the results are posted as a comment with the latest `main` run as baseline; on `main` they are appended to `Docs/pages/static/js/data.js` on the `benchmarks` branch, which feeds the benchmarks page of the docs site. + +## Mutation Tests + +Stryker mutates `Source/Mockolate` against `Mockolate.Tests` and `Mockolate.Internal.Tests` on `net8.0` with mutation level `Advanced`. Outside `main` it only mutates code changed since `main`. A full run takes long; run it only when asked: + +```powershell +./build.ps1 MutationTestExecution +``` + +Reports go to `Artifacts/Stryker`. The `MutationTests` target also prepares the PR comment and expects to run in GitHub Actions. + +## Build Pipeline (Nuke) + +`Pipeline/Build.csproj` is a Nuke build, started through `build.ps1`, `build.sh` or `build.cmd`. Always name a target: without one, Nuke runs the defaults `ApiChecks`, `Benchmarks` and `CodeAnalysis`. `./build.ps1 --help` lists the targets. The configuration is Debug locally and Release on CI. Every run cleans `Artifacts/` and `TestResults/` first. + +| Target | Purpose | +|---|---| +| `Compile` | Restore and build the solution with the GitVersion-derived version | +| `UnitTests` | Run `Build.Tests`, `Mockolate.Tests`, `Mockolate.Internal.Tests`, `Mockolate.Analyzers.Tests` and `Mockolate.SourceGenerators.Tests` for each framework (`net48` only on Windows) with coverage | +| `CodeCoverage` | `UnitTests` plus a ReportGenerator OpenCover report in `TestResults/reports` | +| `ApiChecks` | Run `Mockolate.Api.Tests` | +| `CodeAnalysis` | SonarCloud analysis (needs `SonarToken`) | +| `Pack` | Build the NuGet package into `Artifacts/Packages`, with a release-specific README | +| `Benchmarks` | Run the benchmarks (`--benchmark-filter`, default `*`) and print the reports | +| `BenchmarkComment`, `PublishBenchmarkReport` | CI only: PR comment and docs site data | +| `MutationTests`, `MutationTestExecution`, `MutationTestComment`, `MutationTestDashboard` | Stryker run, PR comment and dashboard upload | +| `UpdateReadme` | Write the packaged README to `Artifacts/README.md` | + +## CI Workflows + +| Workflow | Trigger | Jobs | +|---|---|---| +| `ci.yml` | PR to `main` | Unit tests (`CodeCoverage`) on Ubuntu, Windows and macOS; API tests; mutation tests; benchmark matrix; SonarCloud (same-repo PRs, not Dependabot); test result publishing | +| `ci-analysis.yml` | After `CI` completes | Posts the mutation and benchmark comments on the PR | +| `build.yml` | Push to `main`, `v*` tags | Same checks as CI plus pack; on `main` publishes the benchmark report and uploads the mutation dashboard; on a tag pushes to nuget.org (trusted publishing), creates the GitHub release for non-prerelease tags and comments on released issues | +| `notify-docs-site.yml` | After a successful `Build` on `main` | Triggers a rebuild of the docs site in `Testably/Testably.Site` | + +Mergify (`.github/mergify.yml`) squash-merges Dependabot PRs once the three unit test jobs and the API tests pass. + +## Documentation + +User docs are Markdown pages in `Docs/pages/` (Docusaurus front matter, numbered file names for ordering, `_category_.json` per folder). They are published at https://docs.testably.org/Mockolate/ by `Testably/Testably.Site` after each successful build on `main`; there is no local docs build in this repository. Code samples in the docs are not compiled, so check them against `Mockolate.ExampleTests` or a quick test. The README is the NuGet landing page; `Pack` adds version-specific badges to the packaged copy.