Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
85121f9
chore: build on the .NET 11 preview SDK and add net11.0 targets
vbreuss Sep 4, 2026
71d890b
feat: detect C# union support and emit the ParameterArg<T> union type
vbreuss Sep 4, 2026
84da575
feat: union-typed setup and verify overloads for methods
vbreuss Sep 4, 2026
4c75ae4
feat: union-typed setup and verify indexers
vbreuss Sep 4, 2026
3539957
fix: report unused verifications that return IgnoreParameters
vbreuss Sep 5, 2026
1db3d77
docs: describe union parameters and add the union benchmark project
vbreuss Sep 5, 2026
19756eb
chore: address SonarCloud findings
vbreuss Sep 5, 2026
1cfaff0
chore: add the UnionTests target to the Fallout build schema
vbreuss Sep 5, 2026
48d7ba7
fix: stop generating runtimeconfig.dev.json for test projects
vbreuss Sep 5, 2026
01daa6a
fix: address code review findings in union parameter generation
vbreuss Sep 5, 2026
b77f405
chore: remove the union benchmark project
vbreuss Sep 5, 2026
201d98b
test: deduplicate generator snapshot files
vbreuss Sep 5, 2026
ab21f0f
fix: treat the default ParameterArg<T> as the literal default in Value
vbreuss Sep 5, 2026
434b4d9
test: update the union snapshots for the explicit indexer setter type…
vbreuss Oct 18, 2026
6f563a8
chore: update aweXpect.Snapshots.PublicApi to 0.6.0
vbreuss Oct 19, 2026
1a10b67
chore: update the .NET 11 SDK to RC2
vbreuss Oct 19, 2026
4a4c059
fix: require a union-capable language version for the MockolateUnionP…
vbreuss Oct 31, 2026
1150e95
docs: keep each union parameters paragraph on a single line
vbreuss Oct 31, 2026
2fe7f95
chore: pin the full .NET 11 RC3 SDK version
vbreuss Oct 31, 2026
02afaf4
fix: address review findings in the union parameter generation
vbreuss Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .fallout/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"Pack",
"PublishBenchmarkReport",
"Restore",
"UnionTests",
"UnitTests",
"UpdateReadme"
]
Expand Down
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
# while mounting and running tests in Linux
*.sh text eol=lf
build text eol=lf

# Snapshot files are regenerated by AcceptSnapshotChanges; collapse them in GitHub diffs
Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/** linguist-generated=true
39 changes: 37 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run unit tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 CodeCoverage
Expand Down Expand Up @@ -60,6 +61,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run AOT smoke tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 AotSmokeTests
Expand All @@ -82,6 +84,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: API checks
run: ./build.sh ApiChecks
- name: Upload artifacts
Expand All @@ -93,6 +96,33 @@ jobs:
./Artifacts/*
./TestResults/*.trx

union-tests:
name: "Union tests (net11.0, C# preview)"
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup .NET SDKs
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Union tests
run: ./build.sh UnionTests
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Union-tests
path: |
./Artifacts/*
./TestResults/*.trx

benchmarks:
name: "Benchmarks (${{ matrix.benchmark }})"
strategy:
Expand Down Expand Up @@ -120,6 +150,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run benchmarks
run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*"
- name: Upload artifacts
Expand Down Expand Up @@ -150,6 +181,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Publish benchmark report
run: ./build.sh PublishBenchmarkReport
env:
Expand All @@ -172,6 +204,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run mutation tests
run: ./build.sh MutationTests MutationTestsDashboard

Expand All @@ -192,12 +225,13 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run sonarcloud analysis
run: ./build.sh CodeAnalysis

publish-test-results:
name: "Publish Tests Results"
needs: [ api-tests, unit-tests, aot-smoke-tests ]
needs: [ api-tests, unit-tests, aot-smoke-tests, union-tests ]
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down Expand Up @@ -229,6 +263,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Pack nuget packages
run: ./build.sh Pack
- name: Upload packages
Expand All @@ -243,7 +278,7 @@ jobs:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: macos-latest
environment: production
needs: [ pack, unit-tests, aot-smoke-tests, api-tests, publish-test-results, static-code-analysis ]
needs: [ pack, unit-tests, aot-smoke-tests, api-tests, union-tests, publish-test-results, static-code-analysis ]
permissions:
contents: write
id-token: write
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Upload mutation dashboard and create comment
run: ./build.sh MutationTestsComment
# The artifacts of the workflow run are untrusted, because a pull request controls the code that
Expand Down Expand Up @@ -53,6 +54,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Create benchmark comment
run: ./build.sh BenchmarkComment
# The artifacts of the workflow run are untrusted, because a pull request controls the code that
Expand Down
35 changes: 34 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run unit tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 CodeCoverage
Expand Down Expand Up @@ -55,6 +56,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run AOT smoke tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 AotSmokeTests
Expand All @@ -77,6 +79,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: API checks
run: ./build.sh ApiChecks
- name: Upload artifacts
Expand All @@ -103,6 +106,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run mutation tests
run: ./build.sh MutationTests
- name: Upload artifacts
Expand All @@ -113,6 +117,33 @@ jobs:
path: |
./Artifacts/*

union-tests:
name: "Union tests (net11.0, C# preview)"
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup .NET SDKs
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
Comment thread
vbreuss marked this conversation as resolved.
Resolved
- name: Union tests
run: ./build.sh UnionTests
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Union-tests
path: |
./Artifacts/*
./TestResults/*.trx

benchmarks:
name: "Benchmarks (${{ matrix.benchmark }})"
strategy:
Expand All @@ -138,6 +169,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run benchmarks
run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*"
- name: Upload artifacts
Expand Down Expand Up @@ -167,12 +199,13 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run sonarcloud analysis
run: ./build.sh CodeAnalysis

publish-test-results:
name: "Publish Tests Results"
needs: [ api-tests, unit-tests, aot-smoke-tests ]
needs: [ api-tests, unit-tests, aot-smoke-tests, union-tests ]
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Mockolate is a source-generated mocking library for .NET. Users write `IFoo.Crea

## SDK and Build

- Use the SDK pinned in `global.json` (.NET 10, `rollForward: latestPatch`). Tests also run on .NET 8, and on .NET Framework 4.8 on Windows only.
- Use the SDK pinned in `global.json` (currently a .NET 11 preview, `rollForward: latestPatch`). Tests also run on .NET 10 and .NET 8, and on .NET Framework 4.8 on Windows only. On .NET 11 the test projects compile with the preview language version and set `MockolateUnionParameters`, so the union-typed setup surface is exercised there.
- 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, TUnit, aweXpect).
- Package versions belong only in `Directory.Packages.props`. `VersionOverride` is used only to pin Roslyn packages to `$(MinimumRoslynVersion)`.

Expand All @@ -14,7 +14,7 @@ Correctness is a must. The other goals are strong defaults and win in this order

## 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`.
- **Target frameworks**: `Mockolate` ships `netstandard2.0`, `net8.0`, `net10.0` and `net11.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::`, clash-free with user member names, valid under nullable and warnings-as-errors, and deterministic. The generator reports no diagnostics: it skips what it cannot mock and `MockabilityAnalyzer` reports it, so mockability rules change in both. Keep the generator incremental: pipeline entities must have value equality (`EquatableArray`, records) so caching works; `MockGenerator.EqualityTests` covers this.
- **Arity**: the library hand-writes setup and verification types for up to four parameters; the generator emits the same shapes for more. A change to one arity applies to every arity in both places.
Expand Down
6 changes: 5 additions & 1 deletion Benchmarks/Mockolate.Benchmarks/Mockolate.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- Pin the classic overloads so results stay comparable to the CI baseline once the SDK defaults to C# 15. -->
<Import Project="..\..\Source\Mockolate\build\Mockolate.props"/>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net11.0</TargetFramework>
<MockolateUnionParameters>false</MockolateUnionParameters>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateProgramFile>false</GenerateProgramFile>
Expand Down
3 changes: 2 additions & 1 deletion Docs/agents/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Only the `Mockolate` package is published. It carries the generator, analyzers a

### `Source/Mockolate`

The runtime library (`netstandard2.0`, `net8.0`, `net10.0`).
The runtime library (`netstandard2.0`, `net8.0`, `net10.0`, `net11.0`).

| Location | Responsibility |
|---|---|
Expand All @@ -43,6 +43,7 @@ An `IIncrementalGenerator` (`MockGenerator.cs`, `netstandard2.0`).
- `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.
- Union mode (`Sources.MockClass.Unions.cs`): with C# 15 (or `MockolateUnionParameters=true` under `LangVersion` preview) the setup and verify overloads take `ParameterArg<T>?` or `Func<T, bool>` per parameter instead of the classic matcher and value set; below that the output is unchanged.

### `Source/Mockolate.Analyzers` and `Source/Mockolate.Analyzers.CodeFixers`

Expand Down
5 changes: 3 additions & 2 deletions Docs/agents/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ 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.Tests`, `Mockolate.Internal.Tests`, `Mockolate.ExampleTests` | `net11.0` (C# preview, union parameters), `net10.0`, `net8.0`, `net48` (Windows only) |
| `Mockolate.SourceGenerators.Tests`, `Mockolate.Analyzers.Tests` | `net11.0` |
| `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.
Expand Down
2 changes: 1 addition & 1 deletion Docs/pages/00-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Mockolate is a source-generated, strongly-typed and AOT-compatible

<img align="right" width="200" src="https://raw.githubusercontent.com/Testably/Mockolate/main/Docs/Mockolate_256x256.png" alt="Mockolate logo" />

[**Mockolate**](https://github.com/Testably/Mockolate) is a modern, strongly-typed, AOT-compatible mocking library for .NET, powered by source generators. It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10 and .NET Framework 4.8.
[**Mockolate**](https://github.com/Testably/Mockolate) is a modern, strongly-typed, AOT-compatible mocking library for .NET, powered by source generators. It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10, .NET 11 and .NET Framework 4.8.

```csharp
IChocolateDispenser dispenser = IChocolateDispenser.CreateMock();
Expand Down
3 changes: 1 addition & 2 deletions Docs/pages/05-setup/01-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ description: Set up return values, exceptions and callbacks for methods of a Moc

# Methods

Use `dispenser.Mock.Setup.MethodName(…)` to set up a method. Pass a value or a
[parameter matcher](../06-parameter-matching.md) for each parameter to decide which calls the setup applies to.
Use `dispenser.Mock.Setup.MethodName(…)` to set up a method. Pass a value or a [parameter matcher](../06-parameter-matching.md) for each parameter to decide which calls the setup applies to. With C# 15 you can also pass a predicate directly (see [Union parameters](../06-parameter-matching.md#union-parameters-c-15)).

## Returns / Throws

Expand Down
12 changes: 12 additions & 0 deletions Docs/pages/05-setup/03-indexers.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ When several setups match the same key, the one defined last wins.
Use `.SkippingBaseClass()` to prevent calls to the base class implementation for a single indexer, or
[`MockBehavior.SkippingBaseClass()`](../04-create-mocks.md#customizing-mock-behavior) for the whole mock.

## Union parameters (C# 15)

With C# 15, indexer keys accept predicates directly, in setups and verifications alike:

```csharp
dispenser.Mock.Setup[type => type.StartsWith("D")].Returns(10);

dispenser.Mock.Verify[type => type.StartsWith("D")].Got().Once();
```

Only an indexer that is the sole indexer of its key count on the type takes union-typed keys; indexers sharing a key count keep the classic matcher/value overloads. See [Union parameters](../06-parameter-matching.md#union-parameters-c-15) for the details.

## Indexers with only one accessor

As for [properties](02-properties.md#properties-with-only-one-accessor), the setup and verify members only offer the
Expand Down
Loading
Loading