Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.
- 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::`, 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`.
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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<PackageVersion Include="TUnit.Engine" Version="1.75.0"/>
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.5.1"/>
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.12.0"/>
<PackageVersion Include="aweXpect.Snapshots.PublicApi" Version="0.5.0"/>
<PackageVersion Include="aweXpect.Snapshots.PublicApi" Version="0.6.0"/>
<PackageVersion Include="aweXpect" Version="3.4.0"/>
<PackageVersion Include="aweXpect.Chronology" Version="1.1.0"/>
<PackageVersion Include="aweXpect.Mockolate" Version="3.4.0"/>
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`) 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 @@ -5,7 +5,7 @@
[![Nuget](https://img.shields.io/nuget/v/Mockolate)](https://www.nuget.org/packages/Mockolate)

[**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.
It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10, .NET 11 and .NET Framework 4.8.

- **Source generator-based**: No runtime proxy generation.
- **Fast**: Direct dispatch with no reflection or dynamic proxies.
Expand Down
3 changes: 2 additions & 1 deletion Docs/pages/setup/02-methods.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Methods

Use `sut.Mock.Setup.MethodName(…)` to set up methods. You can specify argument matchers for each parameter.
Use `sut.Mock.Setup.MethodName(…)` to set up methods. You can specify argument matchers for each parameter; with C# 15
you can also pass predicates directly (see *Union Parameters* under Parameter Matching).

## Returns / Throws

Expand Down
13 changes: 13 additions & 0 deletions Docs/pages/setup/03-indexers.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,16 @@ does not intercept.
methods.
- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific indexer (only for class mocks).
- When you specify overlapping setups, the most recently defined setup takes precedence.

## Union Parameters (C# 15)

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

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

sut.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* under Parameter Matching for the details.
38 changes: 38 additions & 0 deletions Docs/pages/setup/04-parameter-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,44 @@ verify surface for it. A `virtual` class member is not flagged at all, because t
to the wrapped instance or to `base` and it keeps working. See
[Mockolate0003](../analyzers#mockolate0003) for what the member does at runtime.

## Union Parameters (C# 15)

When the consuming project compiles with C# 15, Mockolate generates the setup and verify overloads with union-typed
parameters: every value-capable parameter becomes a `ParameterArg<T>?` that accepts an `It` matcher or a direct value,
and a `Func<T, bool>` overload accepts a predicate. The number of overloads stays the same as before, but predicates no
longer need `It.Satisfies`:

```csharp
sut.Mock.Setup.Dispense(type => type.StartsWith("D"), It.IsAny<int>()).Returns(true);

sut.Mock.Verify.Dispense("Dark", amount => amount > 10).Once();
```

Union mode is enabled automatically once the compiler ships C# 15 and the project's effective language version is
C# 15 or later. On a preview compiler, opt in with `<MockolateUnionParameters>true</MockolateUnionParameters>` in the
project file; `false` keeps the classic overloads on any compiler.

Behaviour of the union-typed parameters:

- `null` and `default` stand for the parameter's declared default value, or for `default(T)` when it has none, exactly
as with the classic value overloads.
- A parameter whose type is a delegate offers the raw delegate instead of a predicate, so a lambda is still matched as
a value.
- For an `object` parameter, a lambda literal is a predicate, while a delegate stored in a variable is a value.
- Within a union-typed member, `ref`, `out` and `ref readonly` parameters as well as `Span<T>` and `ReadOnlySpan<T>`
parameters keep their matcher slot (no predicate for that parameter); `in` parameters behave like by-value ones.
- Overloaded method names (including a generic sibling; a same-named member in another scope — public, protected or
static — does not count), generic methods, `params` methods, members with custom ref-struct parameters, and indexers
that share their key count with another same-scope indexer keep the classic matcher/value overloads. Use
`It.Satisfies` for predicates there.

The generated `ParameterArg<T>` type relies on `UnionAttribute`, `OverloadResolutionPriorityAttribute` and
`CallerArgumentExpressionAttribute`. Mockolate declares whichever of them the compilation lacks (neither the referenced
frameworks nor the project itself provide it). Another generator such as PolySharp can provide them without Mockolate
noticing; set `<MockolateUnionAttributePolyfills>false</MockolateUnionAttributePolyfills>` to declare that
`OverloadResolutionPriorityAttribute` and `CallerArgumentExpressionAttribute` come from elsewhere (`UnionAttribute`
keeps following the compilation), or list the attribute names the project provides.

## Parameter Predicates

When the method name is unique (no overloads), you can use argument matchers from the `Match` class for more flexible parameters matching:
Expand Down
Loading
Loading