diff --git a/.editorconfig b/.editorconfig
index e95e12d08e..09547bc855 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -265,6 +265,10 @@ dotnet_diagnostic.MA0006.severity = none
# Rationale: See https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md
dotnet_diagnostic.MA0009.severity = suggestion
+# Purpose: Use string.Create instead of FormattableString.Invariant
+# Rationale: See https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0111.md
+dotnet_diagnostic.MA0111.severity = error
+
# Purpose: Use an overload of 'System.ArgumentException' with the parameter name
# Rationale: We don't want to force this
dotnet_diagnostic.MA0015.severity = suggestion
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000000..c1eb48e7e0
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+The guidelines for this repository are maintained in [agents.md](../agents.md) at the root of the repository.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e97fb5f22f..e14858b7bb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,11 +7,16 @@ jobs:
name: "Build, Test, Analyze and Publish"
runs-on: windows-latest
+ permissions:
+ id-token: write
+ attestations: write
+ contents: read
+
env:
DOTNET_NOLOGO: true
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -21,10 +26,10 @@ jobs:
dotnet-version: |
6.0.x
8.0.x
- 9.0.x
+ 10.0.x
- name: Cache .nuke/temp
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
.nuke/temp
@@ -49,9 +54,22 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: TestResults/reports/lcov.info
+ - name: Check for NuGet package existence
+ id: check_nupkg
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: andstor/file-existence-action@v3
+ with:
+ files: "Artifacts/*.nupkg"
+
+ - name: Attest NuGet package
+ uses: actions/attest@v4
+ if: startsWith(github.ref, 'refs/tags/') && steps.check_nupkg.outputs.files_exists == 'true'
+ with:
+ subject-path: ./Artifacts/*.nupkg
+
- name: Upload artifacts
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: windows-artifacts
path: |
@@ -67,7 +85,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -77,14 +95,14 @@ jobs:
dotnet-version: |
6.0.x
8.0.x
- 9.0.x
+ 10.0.x
- name: Run NUKE
run: ./build.sh UnitTests
- name: Upload artifacts
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: ${{ runner.os }}-artifacts
path: |
@@ -101,7 +119,7 @@ jobs:
steps:
- name: Download Artifacts
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v8
with:
path: artifacts
diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml
index 9e5c7b0b6e..94699ce1c2 100644
--- a/.github/workflows/code_quality.yml
+++ b/.github/workflows/code_quality.yml
@@ -13,15 +13,15 @@ jobs:
name: Qodana Scan
runs-on: windows-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: 'Qodana Scan'
- uses: JetBrains/qodana-action@v2025.2
+ uses: JetBrains/qodana-action@v2026.1
with:
upload-result: ${{ github.ref_name == 'main' || github.ref_name == 'develop' }}
- args: --baseline,qodana.sarif.json,--ide,QDNET
+ args: --baseline qodana.sarif.json --linter qodana-dotnet --within-docker false
pr-mode: ${{ github.event_name == 'pull_request_target' }}
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index da02270af2..3681484b35 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -28,11 +28,11 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
- 9.0.x
+ 10.0.x
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
@@ -48,8 +48,8 @@ jobs:
- name: Build
run: |
- dotnet restore FluentAssertions.sln --configfile nuget.config
- dotnet build FluentAssertions.sln --configuration CI --no-restore
+ dotnet restore FluentAssertions.slnx --configfile nuget.config
+ dotnet build FluentAssertions.slnx --configuration CI --no-restore
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
new file mode 100644
index 0000000000..c1eb48e7e0
--- /dev/null
+++ b/.junie/guidelines.md
@@ -0,0 +1 @@
+The guidelines for this repository are maintained in [agents.md](../agents.md) at the root of the repository.
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
index fc3a2420e0..9f8039c0ec 100644
--- a/.nuke/parameters.json
+++ b/.nuke/parameters.json
@@ -1,4 +1,4 @@
{
"$schema": "./build.schema.json",
- "Solution": "FluentAssertions.sln"
+ "Solution": "FluentAssertions.slnx"
}
\ No newline at end of file
diff --git a/.packageguard/cache.bin b/.packageguard/cache.bin
index 68eba45c8e..509299b601 100644
Binary files a/.packageguard/cache.bin and b/.packageguard/cache.bin differ
diff --git a/Build/Build.cs b/Build/Build.cs
index a089ea93b1..829ac29256 100644
--- a/Build/Build.cs
+++ b/Build/Build.cs
@@ -61,7 +61,7 @@ class Build : NukeBuild
readonly Solution Solution;
[Required]
- [GitVersion(Framework = "net9.0", NoCache = true, NoFetch = true)]
+ [GitVersion(Framework = "net10.0", NoCache = true, NoFetch = true)]
readonly GitVersion GitVersion;
[Required]
@@ -129,7 +129,7 @@ class Build : NukeBuild
DotNetBuild(s => s
.SetProjectFile(Solution)
.SetConfiguration(Configuration)
- .When(_ => GenerateBinLog is true, c => c
+ .When(_ => GenerateBinLog == true, c => c
.SetBinaryLog(ArtifactsDirectory / $"{Solution.Core.FluentAssertions.Name}.binlog")
)
.EnableNoLogo()
@@ -228,7 +228,7 @@ class Build : NukeBuild
{
ReportGenerator(s => s
.SetProcessToolPath(NuGetToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll",
- framework: "net9.0"))
+ framework: "net10.0"))
.SetTargetDirectory(TestResultsDirectory / "reports")
.AddReports(TestResultsDirectory / "**/coverage.cobertura.xml")
.AddReportTypes(
@@ -299,7 +299,7 @@ from project in projects
from framework in frameworks
select new { project, framework };
- DotNetTest(s => s
+ DotNetRun(s => s
.SetConfiguration(Configuration.Debug)
.SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US")
.EnableNoBuild()
diff --git a/Build/CompressionExtensions.cs b/Build/CompressionExtensions.cs
index 334960480d..89753805b2 100644
--- a/Build/CompressionExtensions.cs
+++ b/Build/CompressionExtensions.cs
@@ -9,7 +9,7 @@ public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory)
{
using Stream stream = File.OpenRead(archive);
- using var reader = ReaderFactory.Open(stream);
+ using var reader = ReaderFactory.OpenReader(stream);
while (reader.MoveToNextEntry())
{
diff --git a/Build/_build.csproj b/Build/_build.csproj
index f6891e6651..6d7e45f810 100644
--- a/Build/_build.csproj
+++ b/Build/_build.csproj
@@ -1,23 +1,24 @@
Exe
- net9.0
+ net10.0CS0649;CS0169..\..\
- 9.0.4
+ 10.1.01
-
-
+
+
+
-
-
+
+
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..79709e3ef5
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@agents.md
diff --git a/Directory.Build.props b/Directory.Build.props
index 79f0d8f4e9..69be3ee979 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 13.0
+ 14.0falsetrue
@@ -13,7 +13,7 @@
true
- 9.0
+ 10.0Alltrue
@@ -30,11 +30,11 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/FluentAssertions.sln b/FluentAssertions.sln
deleted file mode 100644
index 3766cd7232..0000000000
--- a/FluentAssertions.sln
+++ /dev/null
@@ -1,233 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.3.32505.426
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5A0B454-22D4-4694-99FF-D6A8B7DE7DA3}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- Tests\Default.testsettings = Tests\Default.testsettings
- Directory.Build.props = Directory.Build.props
- nuget.config = nuget.config
- README.md = README.md
- docs\_pages\releases.md = docs\_pages\releases.md
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "Build\_build.csproj", "{364DD16C-D759-49DC-A04A-64D40205295B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specs", "Specs", "{963262D0-9FD5-4741-8C0E-E2F34F110EF3}"
- ProjectSection(SolutionItems) = preProject
- Tests\.editorconfig = Tests\.editorconfig
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyA", "Tests\AssemblyA\AssemblyA.csproj", "{7144BD9D-2A5F-45B6-AC5B-E35578D03350}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyB", "Tests\AssemblyB\AssemblyB.csproj", "{D9CCCAF6-FF9E-4688-9D96-645E27616AC4}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFrameworks", "TestFrameworks", "{4D8FA213-8724-4C43-B68A-F018148D238C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnit2.Specs", "Tests\TestFrameworks\XUnit2.Specs\XUnit2.Specs.csproj", "{F50C8F77-3778-4432-84EC-B9B2B502E708}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{31891850-3EDC-480A-9B6C-F60540E9C90F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTestV2.Specs", "Tests\TestFrameworks\MSTestV2.Specs\MSTestV2.Specs.csproj", "{A4E37052-5581-4E70-A9C3-FF8364B2F332}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions", "Src\FluentAssertions\FluentAssertions.csproj", "{34E3713D-C02F-4868-BBE7-47DAD2C7F03D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnit3.Specs", "Tests\TestFrameworks\NUnit3.Specs\NUnit3.Specs.csproj", "{C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnit4.Specs", "Tests\TestFrameworks\NUnit4.Specs\NUnit4.Specs.csproj", "{23891E7D-05CE-4893-AC38-A95CC9D3603E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSpec.Specs", "Tests\TestFrameworks\MSpec.Specs\MSpec.Specs.csproj", "{4F210C41-7E8E-424A-B956-FC1AA47663C9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Tests\Benchmarks\Benchmarks.csproj", "{FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Approval.Tests", "Tests\Approval.Tests\Approval.Tests.csproj", "{F5115158-A038-4D14-A04E-46E7863E40B9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Specs", "Tests\FluentAssertions.Specs\FluentAssertions.Specs.csproj", "{6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UWP.Specs", "Tests\UWP.Specs\UWP.Specs.csproj", "{08087654-2C32-4818-95E4-45362373441D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Equivalency.Specs", "Tests\FluentAssertions.Equivalency.Specs\FluentAssertions.Equivalency.Specs.csproj", "{A946043D-D3F8-46A4-B485-A88412C417FE}"
-EndProject
-Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VB.Specs", "Tests\VB.Specs\VB.Specs.vbproj", "{0C0211B6-D185-4518-A15A-38AC092EDC50}"
-EndProject
-Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Specs", "Tests\FSharp.Specs\FSharp.Specs.fsproj", "{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleExtensions", "Tests\ExampleExtensions\ExampleExtensions.csproj", "{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Extensibility.Specs", "Tests\FluentAssertions.Extensibility.Specs\FluentAssertions.Extensibility.Specs.csproj", "{450FC408-A4E2-4483-B064-2007024D6CF1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TUnit.Specs", "Tests\TestFrameworks\TUnit.Specs\TUnit.Specs.csproj", "{6540564E-9B6E-4E1E-8881-6F0DD0F35576}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnit3.Specs", "Tests\TestFrameworks\XUnit3.Specs\XUnit3.Specs.csproj", "{AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnit3Core.Specs", "Tests\TestFrameworks\XUnit3Core.Specs\XUnit3Core.Specs.csproj", "{77BB9496-169D-43DA-BCED-7DB3ACD9179A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTestV4.Specs", "Tests\TestFrameworks\MSTestV4.Specs\MSTestV4.Specs.csproj", "{06D5201F-65F6-AA25-8592-A78668E25B66}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- CI|Any CPU = CI|Any CPU
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {364DD16C-D759-49DC-A04A-64D40205295B}.CI|Any CPU.ActiveCfg = Release|Any CPU
- {364DD16C-D759-49DC-A04A-64D40205295B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {364DD16C-D759-49DC-A04A-64D40205295B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.CI|Any CPU.Build.0 = Debug|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350}.Release|Any CPU.Build.0 = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.CI|Any CPU.Build.0 = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4}.Release|Any CPU.Build.0 = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.CI|Any CPU.Build.0 = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {F50C8F77-3778-4432-84EC-B9B2B502E708}.Release|Any CPU.Build.0 = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.CI|Any CPU.Build.0 = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {A4E37052-5581-4E70-A9C3-FF8364B2F332}.Release|Any CPU.Build.0 = Debug|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.CI|Any CPU.ActiveCfg = Release|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.CI|Any CPU.Build.0 = Release|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D}.Release|Any CPU.Build.0 = Release|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.CI|Any CPU.Build.0 = Debug|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2}.Release|Any CPU.Build.0 = Debug|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.CI|Any CPU.Build.0 = Debug|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {23891E7D-05CE-4893-AC38-A95CC9D3603E}.Release|Any CPU.Build.0 = Release|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.CI|Any CPU.Build.0 = Debug|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.Release|Any CPU.ActiveCfg = Debug|Any CPU
- {4F210C41-7E8E-424A-B956-FC1AA47663C9}.Release|Any CPU.Build.0 = Debug|Any CPU
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}.CI|Any CPU.ActiveCfg = Release|Any CPU
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71}.Release|Any CPU.Build.0 = Release|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.CI|Any CPU.ActiveCfg = Release|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.CI|Any CPU.Build.0 = Release|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F5115158-A038-4D14-A04E-46E7863E40B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.CI|Any CPU.Build.0 = Debug|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Release|Any CPU.Build.0 = Release|Any CPU
- {08087654-2C32-4818-95E4-45362373441D}.CI|Any CPU.ActiveCfg = Debug|x64
- {08087654-2C32-4818-95E4-45362373441D}.Debug|Any CPU.ActiveCfg = Debug|x64
- {08087654-2C32-4818-95E4-45362373441D}.Release|Any CPU.ActiveCfg = Release|x64
- {A946043D-D3F8-46A4-B485-A88412C417FE}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {A946043D-D3F8-46A4-B485-A88412C417FE}.CI|Any CPU.Build.0 = Debug|Any CPU
- {A946043D-D3F8-46A4-B485-A88412C417FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A946043D-D3F8-46A4-B485-A88412C417FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A946043D-D3F8-46A4-B485-A88412C417FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A946043D-D3F8-46A4-B485-A88412C417FE}.Release|Any CPU.Build.0 = Release|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.CI|Any CPU.Build.0 = Debug|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0C0211B6-D185-4518-A15A-38AC092EDC50}.Release|Any CPU.Build.0 = Release|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.CI|Any CPU.Build.0 = Debug|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Release|Any CPU.Build.0 = Release|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.CI|Any CPU.Build.0 = Debug|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Release|Any CPU.Build.0 = Release|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.CI|Any CPU.Build.0 = Debug|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {450FC408-A4E2-4483-B064-2007024D6CF1}.Release|Any CPU.Build.0 = Release|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.CI|Any CPU.Build.0 = Debug|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576}.Release|Any CPU.Build.0 = Release|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.CI|Any CPU.Build.0 = Debug|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A}.Release|Any CPU.Build.0 = Release|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.CI|Any CPU.Build.0 = Debug|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A}.Release|Any CPU.Build.0 = Release|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.CI|Any CPU.ActiveCfg = Debug|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.CI|Any CPU.Build.0 = Debug|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {06D5201F-65F6-AA25-8592-A78668E25B66}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {7144BD9D-2A5F-45B6-AC5B-E35578D03350} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {D9CCCAF6-FF9E-4688-9D96-645E27616AC4} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {F50C8F77-3778-4432-84EC-B9B2B502E708} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {A4E37052-5581-4E70-A9C3-FF8364B2F332} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {34E3713D-C02F-4868-BBE7-47DAD2C7F03D} = {31891850-3EDC-480A-9B6C-F60540E9C90F}
- {C8335EA3-C6CE-47C9-AB4F-CE37157E1EB2} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {23891E7D-05CE-4893-AC38-A95CC9D3603E} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {4F210C41-7E8E-424A-B956-FC1AA47663C9} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {F5115158-A038-4D14-A04E-46E7863E40B9} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {08087654-2C32-4818-95E4-45362373441D} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {A946043D-D3F8-46A4-B485-A88412C417FE} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {0C0211B6-D185-4518-A15A-38AC092EDC50} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {450FC408-A4E2-4483-B064-2007024D6CF1} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
- {6540564E-9B6E-4E1E-8881-6F0DD0F35576} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {AF1479D8-519F-4C6B-9E8C-5A84A6D5549A} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {77BB9496-169D-43DA-BCED-7DB3ACD9179A} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- {06D5201F-65F6-AA25-8592-A78668E25B66} = {4D8FA213-8724-4C43-B68A-F018148D238C}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {75DDA3D8-9D6F-4865-93F4-DDE11DEE8290}
- EndGlobalSection
- GlobalSection(MonoDevelopProperties) = preSolution
- StartupItem = Src\FluentAssertions\FluentAssertions.csproj
- EndGlobalSection
-EndGlobal
diff --git a/FluentAssertions.slnx b/FluentAssertions.slnx
new file mode 100644
index 0000000000..15ff2cba12
--- /dev/null
+++ b/FluentAssertions.slnx
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FluentAssertions.sln.DotSettings b/FluentAssertions.slnx.DotSettings
similarity index 100%
rename from FluentAssertions.sln.DotSettings
rename to FluentAssertions.slnx.DotSettings
diff --git a/NodeVersion b/NodeVersion
index 3516580bbb..91d5f6ff8e 100644
--- a/NodeVersion
+++ b/NodeVersion
@@ -1 +1 @@
-20.17.0
+22.18.0
diff --git a/README.md b/README.md
index 0adfe90c96..c4200f674f 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ Expected numbers to contain 4 item(s) because we thought we put four items in th
Visit https://www.fluentassertions.com for [background information](https://fluentassertions.com/about/), [usage documentation](https://fluentassertions.com/introduction), an [extensibility guide](https://fluentassertions.com/extensibility/), support information and more [tips & tricks](https://fluentassertions.com/tips/).
# Building
-Install Visual Studio 2022 17.14+ or JetBrains Rider 2024.3 as well as the Build Tools 2022 (including the Universal Windows Platform build tools). You will also need to have .NET Framework 4.7 SDK and .NET 9.0 SDK installed. Check the [global.json](global.json) for the current minimum required version.
+Install Visual Studio 2026 18.0+ or JetBrains Rider 2025.3. You will also need to have .NET Framework 4.7 SDK and .NET 10.0 SDK installed. Check the [global.json](global.json) for the current minimum required version.
You can also build, run the unit tests and package the code using the following command-line:
diff --git a/Src/FluentAssertions/AggregateExceptionExtractor.cs b/Src/FluentAssertions/AggregateExceptionExtractor.cs
index 9f697b2776..1262e57d60 100644
--- a/Src/FluentAssertions/AggregateExceptionExtractor.cs
+++ b/Src/FluentAssertions/AggregateExceptionExtractor.cs
@@ -6,6 +6,7 @@
namespace FluentAssertions;
+[System.Diagnostics.StackTraceHidden]
public class AggregateExceptionExtractor : IExtractExceptions
{
public IEnumerable OfType(Exception actualException)
diff --git a/Src/FluentAssertions/AndConstraint.cs b/Src/FluentAssertions/AndConstraint.cs
index 796336bd30..fd46c5ee30 100644
--- a/Src/FluentAssertions/AndConstraint.cs
+++ b/Src/FluentAssertions/AndConstraint.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions;
+[StackTraceHidden]
[DebuggerNonUserCode]
public class AndConstraint
{
diff --git a/Src/FluentAssertions/AndWhichConstraint.cs b/Src/FluentAssertions/AndWhichConstraint.cs
index 37d6183a47..9b0e83e1ae 100644
--- a/Src/FluentAssertions/AndWhichConstraint.cs
+++ b/Src/FluentAssertions/AndWhichConstraint.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Formatting;
@@ -10,6 +11,8 @@ namespace FluentAssertions;
/// Provides a property that can be used in chained assertions where the prior assertions returns a
/// single object that the assertion continues on.
///
+[StackTraceHidden]
+[DebuggerNonUserCode]
public class AndWhichConstraint : AndConstraint
{
private readonly AssertionChain assertionChain;
@@ -98,7 +101,7 @@ private static TSubject Single(IEnumerable subjects)
matchedElements.Select(ele => "\t" + Formatter.ToString(ele)));
string message = "More than one object found. FluentAssertions cannot determine which object is meant."
- + $" Found objects:{Environment.NewLine}{foundObjects}";
+ + $" Found objects:{Environment.NewLine}{foundObjects}";
AssertionEngine.TestFramework.Throw(message);
}
diff --git a/Src/FluentAssertions/AssertionConfiguration.cs b/Src/FluentAssertions/AssertionConfiguration.cs
index 82a2833716..463118084f 100644
--- a/Src/FluentAssertions/AssertionConfiguration.cs
+++ b/Src/FluentAssertions/AssertionConfiguration.cs
@@ -1,10 +1,11 @@
-using FluentAssertions.Configuration;
+using FluentAssertions.Configuration;
namespace FluentAssertions;
///
/// Provides access to the global configuration and options to customize the behavior of FluentAssertions.
///
+[System.Diagnostics.StackTraceHidden]
public static class AssertionConfiguration
{
public static GlobalConfiguration Current => AssertionEngine.Configuration;
diff --git a/Src/FluentAssertions/AssertionEngine.cs b/Src/FluentAssertions/AssertionEngine.cs
index ef04504101..69d8bd808f 100644
--- a/Src/FluentAssertions/AssertionEngine.cs
+++ b/Src/FluentAssertions/AssertionEngine.cs
@@ -12,10 +12,10 @@ namespace FluentAssertions;
///
/// Represents the run-time configuration of the assertion library.
///
+[StackTraceHidden]
public static class AssertionEngine
{
private static readonly object Lockable = new();
- private static ITestFramework testFramework;
private static bool isInitialized;
static AssertionEngine()
@@ -30,24 +30,21 @@ public static ITestFramework TestFramework
{
get
{
- if (testFramework is not null)
+ if (field is not null)
{
- return testFramework;
+ return field;
}
lock (Lockable)
{
#pragma warning disable CA1508
- if (testFramework is null)
+ field ??= TestFrameworkFactory.GetFramework(Configuration.TestFramework);
#pragma warning restore CA1508
- {
- testFramework = TestFrameworkFactory.GetFramework(Configuration.TestFramework);
- }
}
- return testFramework;
+ return field;
}
- set => testFramework = value;
+ set;
}
///
@@ -63,7 +60,7 @@ public static void ResetToDefaults()
{
isInitialized = false;
Configuration = new GlobalConfiguration();
- testFramework = null;
+ TestFramework = null;
EnsureInitialized();
}
@@ -152,3 +149,4 @@ private static bool IsFramework(Assembly assembly)
#endif
}
}
+
diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs
index 624adec0b6..35d7a6f5e6 100644
--- a/Src/FluentAssertions/AssertionExtensions.cs
+++ b/Src/FluentAssertions/AssertionExtensions.cs
@@ -6,6 +6,7 @@
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml.Linq;
using FluentAssertions.Collections;
@@ -161,7 +162,7 @@ public static ExecutionTime ExecutionTime(this Func action)
///
/// Returns an object that can be used to assert the
- /// current .
+ /// current .
///
[Pure]
public static ExecutionTimeAssertions Should(this ExecutionTime executionTime)
@@ -210,7 +211,7 @@ public static XAttributeAssertions Should([NotNull] this XAttribute actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -220,7 +221,7 @@ public static StreamAssertions Should([NotNull] this Stream actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -229,6 +230,75 @@ public static BufferedStreamAssertions Should([NotNull] this BufferedStream actu
return new BufferedStreamAssertions(actualValue, AssertionChain.GetOrCreate());
}
+#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ ///
+ /// Returns a object that can be used to assert the
+ /// current .
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static SpanAssertions Should(this Span actualValue)
+ {
+ return new SpanAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+ ///
+ /// Returns a object that can be used to assert the
+ /// current .
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static SpanAssertions Should(this ReadOnlySpan actualValue)
+ {
+ return new SpanAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+ ///
+ /// Returns a object that can be used to assert the
+ /// current of strings.
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static StringCollectionAssertions Should(this Span actualValue)
+ {
+ return new StringCollectionAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+ ///
+ /// Returns a object that can be used to assert the
+ /// current of strings.
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static StringCollectionAssertions Should(this ReadOnlySpan actualValue)
+ {
+ return new StringCollectionAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+ ///
+ /// Returns a object that can be used to assert the
+ /// current .
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static GenericCollectionAssertions Should(this Memory actualValue)
+ {
+ return new GenericCollectionAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+ ///
+ /// Returns a object that can be used to assert the
+ /// current .
+ ///
+ [Pure]
+ [OverloadResolutionPriority(-1)]
+ public static GenericCollectionAssertions Should(this ReadOnlyMemory actualValue)
+ {
+ return new GenericCollectionAssertions(actualValue.ToArray(), AssertionChain.GetOrCreate());
+ }
+
+#endif
+
///
/// Forces enumerating a collection. Should be used to assert that a method that uses the
/// keyword throws a particular exception.
@@ -281,6 +351,7 @@ private static void ForceEnumeration(T subject, Func enumerab
/// current .
///
[Pure]
+ [OverloadResolutionPriority(-2)]
public static ObjectAssertions Should([NotNull] this object actualValue)
{
return new ObjectAssertions(actualValue, AssertionChain.GetOrCreate());
@@ -289,7 +360,7 @@ public static ObjectAssertions Should([NotNull] this object actualValue)
#pragma warning disable AV1564 // Avoid signatures that take a bool parameter
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -299,7 +370,7 @@ public static BooleanAssertions Should(this bool actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -311,7 +382,7 @@ public static NullableBooleanAssertions Should([NotNull] this bool? actualValue)
#pragma warning restore AV1564
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -321,7 +392,7 @@ public static GuidAssertions Should(this Guid actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -331,7 +402,7 @@ public static NullableGuidAssertions Should([NotNull] this Guid? actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -341,7 +412,7 @@ public static GenericCollectionAssertions Should([NotNull] this IEnumerabl
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -351,7 +422,7 @@ public static StringCollectionAssertions Should([NotNull] this IEnumerable
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -363,7 +434,7 @@ public static GenericDictionaryAssertions, TKey, TValu
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current of .
///
[Pure]
@@ -375,7 +446,7 @@ public static GenericDictionaryAssertions
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -387,7 +458,7 @@ public static GenericDictionaryAssertions Should
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -397,7 +468,7 @@ public static DateTimeAssertions Should(this DateTime actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -407,7 +478,7 @@ public static DateTimeOffsetAssertions Should(this DateTimeOffset actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -417,7 +488,7 @@ public static NullableDateTimeAssertions Should([NotNull] this DateTime? actualV
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -428,7 +499,7 @@ public static NullableDateTimeOffsetAssertions Should([NotNull] this DateTimeOff
#if NET6_0_OR_GREATER
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -438,7 +509,7 @@ public static DateOnlyAssertions Should(this DateOnly actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -448,7 +519,7 @@ public static NullableDateOnlyAssertions Should([NotNull] this DateOnly? actualV
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -458,7 +529,7 @@ public static TimeOnlyAssertions Should(this TimeOnly actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -470,7 +541,7 @@ public static NullableTimeOnlyAssertions Should([NotNull] this TimeOnly? actualV
#endif
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -480,7 +551,7 @@ public static ComparableTypeAssertions Should([NotNull] this IComparable
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -490,7 +561,7 @@ public static NumericAssertions Should(this int actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -500,7 +571,7 @@ public static NullableNumericAssertions Should([NotNull] this int? actualVa
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -510,7 +581,7 @@ public static NumericAssertions Should(this uint actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -520,7 +591,7 @@ public static NullableNumericAssertions Should([NotNull] this uint? actual
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -530,7 +601,7 @@ public static NumericAssertions Should(this decimal actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -540,7 +611,7 @@ public static NullableNumericAssertions Should([NotNull] this decimal?
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -550,7 +621,7 @@ public static NumericAssertions Should(this byte actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -560,7 +631,7 @@ public static NullableNumericAssertions Should([NotNull] this byte? actual
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -570,7 +641,7 @@ public static NumericAssertions Should(this sbyte actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -580,7 +651,7 @@ public static NullableNumericAssertions Should([NotNull] this sbyte? actu
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -590,7 +661,7 @@ public static NumericAssertions Should(this short actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -600,7 +671,7 @@ public static NullableNumericAssertions Should([NotNull] this short? actu
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -610,7 +681,7 @@ public static NumericAssertions Should(this ushort actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -620,7 +691,7 @@ public static NullableNumericAssertions Should([NotNull] this ushort? ac
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -630,7 +701,7 @@ public static NumericAssertions Should(this long actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -640,7 +711,7 @@ public static NullableNumericAssertions Should([NotNull] this long? actual
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -650,7 +721,7 @@ public static NumericAssertions Should(this ulong actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -660,7 +731,7 @@ public static NullableNumericAssertions Should([NotNull] this ulong? actu
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -670,7 +741,7 @@ public static NumericAssertions Should(this float actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -680,7 +751,7 @@ public static NullableNumericAssertions Should([NotNull] this float? actu
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -690,7 +761,7 @@ public static NumericAssertions Should(this double actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -700,7 +771,7 @@ public static NullableNumericAssertions Should([NotNull] this double? ac
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -710,7 +781,7 @@ public static StringAssertions Should([NotNull] this string actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current .
///
[Pure]
@@ -720,7 +791,7 @@ public static SimpleTimeSpanAssertions Should(this TimeSpan actualValue)
}
///
- /// Returns an object that can be used to assert the
+ /// Returns a object that can be used to assert the
/// current nullable .
///
[Pure]
@@ -731,7 +802,7 @@ public static NullableSimpleTimeSpanAssertions Should([NotNull] this TimeSpan? a
///
/// Returns a object that can be used to assert the
- /// current .
+ /// current .
///
[Pure]
public static TypeAssertions Should([NotNull] this Type subject)
@@ -753,7 +824,7 @@ public static TypeSelectorAssertions Should(this TypeSelector typeSelector)
}
///
- /// Returns a object
+ /// Returns a object
/// that can be used to assert the current .
///
///
@@ -813,8 +884,8 @@ public static PropertyInfoSelectorAssertions Should(this PropertyInfoSelector pr
}
///
- /// Returns a object that can be used to assert the
- /// current .
+ /// Returns an object that can be used to assert the
+ /// current .
///
[Pure]
public static ActionAssertions Should([NotNull] this Action action)
@@ -824,7 +895,7 @@ public static ActionAssertions Should([NotNull] this Action action)
///
/// Returns a object that can be used to assert the
- /// current .
+ /// current .
///
[Pure]
public static NonGenericAsyncFunctionAssertions Should([NotNull] this Func action)
@@ -844,7 +915,7 @@ public static GenericAsyncFunctionAssertions Should([NotNull] this Func
/// Returns a object that can be used to assert the
- /// current .
+ /// current .
///
[Pure]
public static FunctionAssertions Should([NotNull] this Func func)
@@ -884,7 +955,7 @@ public static IMonitor Monitor(this T eventSource)
/// is .
public static IMonitor Monitor(this T eventSource, Action configureOptions)
{
- Guard.ThrowIfArgumentIsNull(configureOptions, nameof(configureOptions));
+ Guard.ThrowIfArgumentIsNull(configureOptions);
var options = new EventMonitorOptions();
configureOptions(options);
diff --git a/Src/FluentAssertions/AsyncAssertionsExtensions.cs b/Src/FluentAssertions/AsyncAssertionsExtensions.cs
index 4981a66dd0..fa5fb7d8ed 100644
--- a/Src/FluentAssertions/AsyncAssertionsExtensions.cs
+++ b/Src/FluentAssertions/AsyncAssertionsExtensions.cs
@@ -21,7 +21,7 @@ public static class AsyncAssertionsExtensions
///
///
/// Please note that this assertion cannot identify whether the previous assertion was successful or not.
- /// In case it was not successful and it is running within an active
+ /// In case it was not successful and it is running within an active
/// there is no current result to compare with.
/// So, this extension will compare with the default value.
///
diff --git a/Src/FluentAssertions/AtLeast.cs b/Src/FluentAssertions/AtLeast.cs
index 95750816f2..37fd679599 100644
--- a/Src/FluentAssertions/AtLeast.cs
+++ b/Src/FluentAssertions/AtLeast.cs
@@ -1,5 +1,6 @@
-namespace FluentAssertions;
+namespace FluentAssertions;
+[System.Diagnostics.StackTraceHidden]
public static class AtLeast
{
public static OccurrenceConstraint Once() => new AtLeastTimesConstraint(1);
diff --git a/Src/FluentAssertions/AtMost.cs b/Src/FluentAssertions/AtMost.cs
index e11c892068..5268a80de2 100644
--- a/Src/FluentAssertions/AtMost.cs
+++ b/Src/FluentAssertions/AtMost.cs
@@ -1,5 +1,6 @@
-namespace FluentAssertions;
+namespace FluentAssertions;
+[System.Diagnostics.StackTraceHidden]
public static class AtMost
{
public static OccurrenceConstraint Once() => new AtMostTimesConstraint(1);
diff --git a/Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs
index 1076b460ae..8c5beea1d7 100644
--- a/Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs
index 8630d3d5a8..115da14aef 100644
--- a/Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/ParsingState.cs b/Src/FluentAssertions/CallerIdentification/ParsingState.cs
index 18ac092068..607fc3b8b0 100644
--- a/Src/FluentAssertions/CallerIdentification/ParsingState.cs
+++ b/Src/FluentAssertions/CallerIdentification/ParsingState.cs
@@ -1,4 +1,4 @@
-namespace FluentAssertions.CallerIdentification;
+namespace FluentAssertions.CallerIdentification;
internal enum ParsingState
{
diff --git a/Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs
index 1cb9f41906..8eda4c9aa3 100644
--- a/Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs
index c5480ef6b5..b01d7e3101 100644
--- a/Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs
index e7a82a85dd..b45af424c4 100644
--- a/Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs b/Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs
index 371facc25f..4dbf5eed8d 100644
--- a/Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs
+++ b/Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace FluentAssertions.CallerIdentification;
diff --git a/Src/FluentAssertions/CallerIdentification/StatementParser.cs b/Src/FluentAssertions/CallerIdentification/StatementParser.cs
index 2b9497653d..26dbaf0add 100644
--- a/Src/FluentAssertions/CallerIdentification/StatementParser.cs
+++ b/Src/FluentAssertions/CallerIdentification/StatementParser.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using System.Text;
diff --git a/Src/FluentAssertions/CallerIdentifier.cs b/Src/FluentAssertions/CallerIdentifier.cs
index 4948e4ea0f..a13e0af809 100644
--- a/Src/FluentAssertions/CallerIdentifier.cs
+++ b/Src/FluentAssertions/CallerIdentifier.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -16,6 +16,7 @@ namespace FluentAssertions;
/// Tries to extract the name of the variable or invocation on which the assertion is executed.
///
// REFACTOR: Should be internal and treated as an implementation detail of the AssertionScope
+[StackTraceHidden]
public static class CallerIdentifier
{
public static Action Logger { get; set; } = _ => { };
@@ -91,6 +92,14 @@ public static string[] DetermineCallerIdentities()
return callers;
}
+ ///
+ /// Represents a specific point in the call stack to facilitate skipping over internal FluentAssertions
+ /// frames when analyzing stack traces for caller identification.
+ ///
+ ///
+ /// Instances of this class are used to track and adjust starting points for stack trace searches,
+ /// ensuring that FluentAssertions internal frames are excluded from caller identification processes.
+ ///
private sealed class StackFrameReference : IDisposable
{
public int SkipStackFrameCount { get; }
@@ -125,11 +134,23 @@ public void Dispose()
private static readonly AsyncLocal StartStackSearchAfterStackFrame = new();
+ ///
+ /// Temporarily adjusts the stack trace search behavior to focus on the current scope during Fluent Assertions evaluations.
+ ///
+ /// An object that reverts the stack trace search behavior upon disposal.
internal static IDisposable OverrideStackSearchUsingCurrentScope()
{
return new StackFrameReference();
}
+ ///
+ /// Determines whether there is only one Fluent Assertions scope present in the current call stack.
+ /// This is used to identify if a nested Fluent Assertions scope exists within the stack trace.
+ ///
+ ///
+ /// A boolean value indicating true if only one Fluent Assertions scope exists on the call stack;
+ /// otherwise, false.
+ ///
internal static bool OnlyOneFluentAssertionScopeOnCallStack()
{
var allStackFrames = GetFrames(new StackTrace());
@@ -290,3 +311,4 @@ private static StackFrame[] GetFrames(StackTrace stack)
.ToArray();
}
}
+
diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
index afff6d0f95..f9599df2ee 100644
--- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
+++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -36,7 +37,6 @@ public GenericCollectionAssertions(TCollection actualValue, AssertionChain asser
#pragma warning disable CS0659, S1206 // Ignore not overriding Object.GetHashCode()
#pragma warning disable CA1065 // Ignore throwing NotSupportedException from Equals
-
[DebuggerNonUserCode]
public class GenericCollectionAssertions : ReferenceTypeAssertions
where TCollection : IEnumerable
@@ -1186,28 +1186,28 @@ public AndWhichConstraint ContainSingle(
switch (actualItems.Count)
{
case 0: // Fail, Collection is empty
- {
- assertionChain
- .BecauseOf(because, becauseArgs)
- .FailWith("Expected {context:collection} to contain a single item{reason}, but the collection is empty.");
+ {
+ assertionChain
+ .BecauseOf(because, becauseArgs)
+ .FailWith("Expected {context:collection} to contain a single item{reason}, but the collection is empty.");
- break;
- }
+ break;
+ }
case 1: // Success Condition
- {
- match = actualItems.Single();
- break;
- }
+ {
+ match = actualItems.Single();
+ break;
+ }
default: // Fail, Collection contains more than a single item
- {
- assertionChain
- .BecauseOf(because, becauseArgs)
- .FailWith("Expected {context:collection} to contain a single item{reason}, but found {0}.", Subject);
+ {
+ assertionChain
+ .BecauseOf(because, becauseArgs)
+ .FailWith("Expected {context:collection} to contain a single item{reason}, but found {0}.", Subject);
- break;
- }
+ break;
+ }
}
}
@@ -1304,7 +1304,7 @@ public AndConstraint EndWith(IEnumerable expectation, [StringSyn
/// A collection of expected elements.
///
///
- /// A equality comparison the is used to determine whether two objects should be treated as equal.
+ /// An equality comparison the is used to determine whether two objects should be treated as equal.
///
///
/// A formatted phrase as is supported by explaining why the assertion
@@ -1350,6 +1350,7 @@ public AndConstraint EndWith(T element, [StringSyntax("CompositeFor
/// . Elements are compared using their method.
///
/// A params array with the expected elements.
+ [EditorBrowsable(EditorBrowsableState.Never)]
public AndConstraint Equal(params T[] elements)
{
AssertSubjectEquality(elements, ObjectExtensions.GetComparer(), string.Empty);
@@ -1357,6 +1358,18 @@ public AndConstraint Equal(params T[] elements)
return new AndConstraint((TAssertions)this);
}
+ ///
+ /// Asserts that the current collection contains all the same elements in the same order as the collection identified by
+ /// . Elements are compared using their method.
+ ///
+ /// A params array with the expected elements.
+ public AndConstraint BeEqualTo(params T[] elements)
+ {
+ AssertSubjectEquality(elements, ObjectExtensions.GetComparer(), string.Empty);
+
+ return new AndConstraint((TAssertions)this);
+ }
+
///
/// Asserts that two collections contain the same items in the same order, where equality is determined using a
/// .
@@ -1365,7 +1378,7 @@ public AndConstraint Equal(params T[] elements)
/// The collection to compare the subject with.
///
///
- /// A equality comparison the is used to determine whether two objects should be treated as equal.
+ /// An equality comparison the is used to determine whether two objects should be treated as equal.
///
///
/// A formatted phrase as is supported by explaining why the assertion
@@ -1374,6 +1387,7 @@ public AndConstraint Equal(params T[] elements)
///
/// Zero or more objects to format using the placeholders in .
///
+ [EditorBrowsable(EditorBrowsableState.Never)]
public AndConstraint Equal(
IEnumerable expectation, Func equalityComparison,
[StringSyntax("CompositeFormat")] string because = "",
@@ -1396,6 +1410,7 @@ public AndConstraint Equal(
///
/// Zero or more objects to format using the placeholders in .
///
+ [EditorBrowsable(EditorBrowsableState.Never)]
public AndConstraint Equal(IEnumerable expected, [StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
@@ -1404,6 +1419,50 @@ public AndConstraint Equal(IEnumerable expected, [StringSyntax("
return new AndConstraint((TAssertions)this);
}
+ ///
+ /// Asserts that the current collection contains all the same elements in the same order as the collection identified by
+ /// . Elements are compared using their method.
+ ///
+ /// An with the expected elements.
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ public AndConstraint BeEqualTo(IEnumerable expected,
+ [StringSyntax("CompositeFormat")] string because = "",
+ params object[] becauseArgs)
+ {
+ return Equal(expected, because, becauseArgs);
+ }
+
+ ///
+ /// Asserts that two collections contain the same items in the same order, where equality is determined using a
+ /// .
+ ///
+ ///
+ /// The collection to compare the subject with.
+ ///
+ ///
+ /// An equality comparison that is used to determine whether two objects should be treated as equal.
+ ///
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ public AndConstraint BeEqualTo(
+ IEnumerable expectation, Func equalityComparison,
+ [StringSyntax("CompositeFormat")] string because = "",
+ params object[] becauseArgs)
+ {
+ return Equal(expectation, equalityComparison, because, becauseArgs);
+ }
+
///
/// Asserts that the number of items in the collection matches the supplied amount.
///
@@ -2738,6 +2797,7 @@ public AndConstraint NotContainNulls(
/// Zero or more objects to format using the placeholders in .
///
/// is .
+ [EditorBrowsable(EditorBrowsableState.Never)]
public AndConstraint NotEqual(IEnumerable unexpected, [StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
@@ -2761,6 +2821,26 @@ public AndConstraint NotEqual(IEnumerable unexpected, [StringSyn
return new AndConstraint((TAssertions)this);
}
+ ///
+ /// Asserts that the current collection does not contain all the same elements in the same order as the collection
+ /// identified by . Elements are compared using their .
+ ///
+ /// An with the elements that are not expected.
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ /// is .
+ public AndConstraint NotBeEqualTo(IEnumerable unexpected,
+ [StringSyntax("CompositeFormat")] string because = "",
+ params object[] becauseArgs)
+ {
+ return NotEqual(unexpected, because, becauseArgs);
+ }
+
///
/// Asserts that the number of items in the collection does not match the supplied amount.
///
@@ -3276,7 +3356,7 @@ public AndConstraint StartWith(IEnumerable expectation, [StringS
/// A collection of expected elements.
///
///
- /// A equality comparison the is used to determine whether two objects should be treated as equal.
+ /// An equality comparison the is used to determine whether two objects should be treated as equal.
///
///
/// A formatted phrase as is supported by explaining why the assertion
diff --git a/Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs b/Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs
index 68f3c66a4a..9607f5fd72 100644
--- a/Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs
+++ b/Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
diff --git a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs
index b5955cbe83..bb4a6433bc 100644
--- a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs
+++ b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
diff --git a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs
index e4b28e29b1..230c312baf 100644
--- a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs
+++ b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
namespace FluentAssertions.Collections.MaximumMatching;
diff --git a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs
index 8bf25ee394..e87a2a58a2 100644
--- a/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs
+++ b/Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs
@@ -1,4 +1,4 @@
-using System.Collections;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
diff --git a/Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs b/Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs
index df5f2bd565..5b478b680a 100644
--- a/Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs
+++ b/Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq.Expressions;
using FluentAssertions.Formatting;
diff --git a/Src/FluentAssertions/Collections/SpanAssertions.cs b/Src/FluentAssertions/Collections/SpanAssertions.cs
new file mode 100644
index 0000000000..032cd437aa
--- /dev/null
+++ b/Src/FluentAssertions/Collections/SpanAssertions.cs
@@ -0,0 +1,69 @@
+#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using FluentAssertions.Execution;
+
+namespace FluentAssertions.Collections;
+
+///
+/// Contains a number of methods to assert that a or matches an expected set of values.
+///
+///
+/// Note: Assertion methods convert spans to arrays for comparison, which involves heap allocation.
+/// This is necessary to integrate with the existing assertion infrastructure.
+///
+[DebuggerNonUserCode]
+public class SpanAssertions : GenericCollectionAssertions>
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The values of the span under test.
+ /// The assertion chain that is used to build fluent continuations.
+ public SpanAssertions(T[] actualValue, AssertionChain assertionChain)
+ : base(actualValue, assertionChain)
+ {
+ }
+
+ ///
+ /// Asserts that the current span is equal to .
+ ///
+ /// The expected values to compare against.
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ ///
+ /// An which can be used to chain assertions.
+ ///
+ public AndConstraint> BeEqualTo(Span expected,
+ [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
+ {
+ return Equal(expected.ToArray(), because, becauseArgs);
+ }
+
+ ///
+ /// Asserts that the current span is equal to .
+ ///
+ /// The expected values to compare against.
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ ///
+ /// An which can be used to chain assertions.
+ ///
+ public AndConstraint> BeEqualTo(ReadOnlySpan expected,
+ [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
+ {
+ return Equal(expected.ToArray(), because, becauseArgs);
+ }
+}
+#endif
diff --git a/Src/FluentAssertions/Collections/SpanAssertionsExtensions.cs b/Src/FluentAssertions/Collections/SpanAssertionsExtensions.cs
new file mode 100644
index 0000000000..b44c255c11
--- /dev/null
+++ b/Src/FluentAssertions/Collections/SpanAssertionsExtensions.cs
@@ -0,0 +1,54 @@
+#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using FluentAssertions.Collections;
+using FluentAssertions.Common;
+using FluentAssertions.Primitives;
+
+// ReSharper disable once CheckNamespace
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+namespace FluentAssertions;
+
+///
+/// Provides string-specific assertion extensions for .
+///
+[DebuggerNonUserCode]
+public static class SpanAssertionsExtensions
+{
+ ///
+ /// Asserts that a or is exactly the same as ,
+ /// including the casing and any leading or trailing whitespace.
+ ///
+ /// The on which the assertion is executed.
+ /// The expected string.
+ ///
+ /// A formatted phrase as is supported by explaining why the assertion
+ /// is needed. If the phrase does not start with the word because, it is prepended automatically.
+ ///
+ ///
+ /// Zero or more objects to format using the placeholders in .
+ ///
+ ///
+ /// An which can be used to chain assertions.
+ ///
+ /// is .
+ public static AndConstraint> Be([NotNull] this SpanAssertions assertions, string expected,
+ [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
+ {
+ Guard.ThrowIfArgumentIsNull(expected);
+
+ var subject = new string(assertions.Subject!);
+
+ var stringEqualityValidator = new StringValidator(assertions.CurrentAssertionChain,
+ new StringEqualityStrategy(StringComparer.Ordinal, "be"),
+ because, becauseArgs);
+
+ stringEqualityValidator.Validate(subject, expected);
+
+ return new AndConstraint>(assertions);
+ }
+}
+
+#endif
diff --git a/Src/FluentAssertions/Collections/StringCollectionAssertions.cs b/Src/FluentAssertions/Collections/StringCollectionAssertions.cs
index 20fcd05306..94daadec75 100644
--- a/Src/FluentAssertions/Collections/StringCollectionAssertions.cs
+++ b/Src/FluentAssertions/Collections/StringCollectionAssertions.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using FluentAssertions.Common;
@@ -54,6 +55,7 @@ public StringCollectionAssertions(TCollection actualValue, AssertionChain assert
///
/// An with the expected elements.
[SuppressMessage("Class Design", "AV1010:Member hides inherited member")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public new AndConstraint Equal(params string[] expected)
{
return base.Equal(expected.AsEnumerable());
@@ -65,6 +67,7 @@ public StringCollectionAssertions(TCollection actualValue, AssertionChain assert
/// the element order, use instead.
///
/// An with the expected elements.
+ [EditorBrowsable(EditorBrowsableState.Never)]
public AndConstraint Equal(IEnumerable expected)
{
return base.Equal(expected);
@@ -135,7 +138,8 @@ public AndConstraint BeEquivalentTo(IEnumerable expectation
options = config(AssertionConfiguration.Current.Equivalency.CloneDefaults()).AsCollection();
var context =
- new EquivalencyValidationContext(Node.From>(() => CurrentAssertionChain.CallerIdentifier), options)
+ new EquivalencyValidationContext(Node.From>(() => CurrentAssertionChain.CallerIdentifier),
+ options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
diff --git a/Src/FluentAssertions/Collections/WhoseValueConstraint.cs b/Src/FluentAssertions/Collections/WhoseValueConstraint.cs
index 50516dd040..4c15197ffd 100644
--- a/Src/FluentAssertions/Collections/WhoseValueConstraint.cs
+++ b/Src/FluentAssertions/Collections/WhoseValueConstraint.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Collections;
+[System.Diagnostics.StackTraceHidden]
public class WhoseValueConstraint : AndConstraint
where TCollection : IEnumerable>
where TAssertions : GenericDictionaryAssertions
diff --git a/Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs b/Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs
index bdbd5008c8..60ff569047 100644
--- a/Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs
+++ b/Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Reflection;
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class CSharpAccessModifierExtensions
{
internal static CSharpAccessModifier GetCSharpAccessModifier(this MethodBase methodBase)
@@ -105,3 +106,4 @@ internal static CSharpAccessModifier GetCSharpAccessModifier(this Type type)
return CSharpAccessModifier.InvalidForCSharp;
}
}
+
diff --git a/Src/FluentAssertions/Common/Clock.cs b/Src/FluentAssertions/Common/Clock.cs
index b7bd423253..9cf2e24cf0 100644
--- a/Src/FluentAssertions/Common/Clock.cs
+++ b/Src/FluentAssertions/Common/Clock.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
@@ -7,6 +7,7 @@ namespace FluentAssertions.Common;
///
/// Default implementation for for production use.
///
+[System.Diagnostics.StackTraceHidden]
internal class Clock : IClock
{
public void Delay(TimeSpan timeToDelay) => Task.Delay(timeToDelay).GetAwaiter().GetResult();
diff --git a/Src/FluentAssertions/Common/DateTimeExtensions.cs b/Src/FluentAssertions/Common/DateTimeExtensions.cs
index 74e00b3611..e48e1e5fe5 100644
--- a/Src/FluentAssertions/Common/DateTimeExtensions.cs
+++ b/Src/FluentAssertions/Common/DateTimeExtensions.cs
@@ -18,3 +18,4 @@ public static DateTimeOffset ToDateTimeOffset(this DateTime dateTime, TimeSpan o
return new DateTimeOffset(DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), offset);
}
}
+
diff --git a/Src/FluentAssertions/Common/EnumerableExtensions.cs b/Src/FluentAssertions/Common/EnumerableExtensions.cs
index 8c9c1063f7..b6a9718045 100644
--- a/Src/FluentAssertions/Common/EnumerableExtensions.cs
+++ b/Src/FluentAssertions/Common/EnumerableExtensions.cs
@@ -4,6 +4,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class EnumerableExtensions
{
public static ICollection ConvertOrCastToCollection(this IEnumerable source)
@@ -55,4 +56,19 @@ public static int IndexOfFirstDifferenceWith(this IEnumerable
+ /// Enumerates a sequence, returning each element along with its index.
+ ///
+ /// The type of the elements in the sequence.
+ /// The sequence to enumerate.
+ /// An enumerable of tuples, where each tuple contains the index and the element at that index in the sequence.
+ public static IEnumerable<(int Index, T Item)> Index(this IEnumerable items)
+ {
+ int index = 0;
+ foreach (var item in items)
+ {
+ yield return (index++, item);
+ }
+ }
}
diff --git a/Src/FluentAssertions/Common/ExceptionExtensions.cs b/Src/FluentAssertions/Common/ExceptionExtensions.cs
index 1e3bbd3fe5..8479a559ea 100644
--- a/Src/FluentAssertions/Common/ExceptionExtensions.cs
+++ b/Src/FluentAssertions/Common/ExceptionExtensions.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class ExceptionExtensions
{
public static ExceptionDispatchInfo Unwrap(this TargetInvocationException exception)
@@ -18,3 +19,4 @@ public static ExceptionDispatchInfo Unwrap(this TargetInvocationException except
return ExceptionDispatchInfo.Capture(result);
}
}
+
diff --git a/Src/FluentAssertions/Common/ExpressionExtensions.cs b/Src/FluentAssertions/Common/ExpressionExtensions.cs
index bca2089a11..1bf02dc0fe 100644
--- a/Src/FluentAssertions/Common/ExpressionExtensions.cs
+++ b/Src/FluentAssertions/Common/ExpressionExtensions.cs
@@ -7,6 +7,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class ExpressionExtensions
{
///
@@ -56,79 +57,79 @@ public static IEnumerable GetMemberPaths(
#pragma warning restore IDE0010
{
case ExpressionType.Lambda:
- {
- node = ((LambdaExpression)node).Body;
- break;
- }
+ {
+ node = ((LambdaExpression)node).Body;
+ break;
+ }
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
- {
- var unaryExpression = (UnaryExpression)node;
- node = unaryExpression.Operand;
- break;
- }
+ {
+ var unaryExpression = (UnaryExpression)node;
+ node = unaryExpression.Operand;
+ break;
+ }
case ExpressionType.MemberAccess:
- {
- var memberExpression = (MemberExpression)node;
- node = memberExpression.Expression;
+ {
+ var memberExpression = (MemberExpression)node;
+ node = memberExpression.Expression;
- break;
- }
+ break;
+ }
case ExpressionType.ArrayIndex:
- {
- var binaryExpression = (BinaryExpression)node;
- node = binaryExpression.Left;
+ {
+ var binaryExpression = (BinaryExpression)node;
+ node = binaryExpression.Left;
- break;
- }
+ break;
+ }
case ExpressionType.Parameter:
- {
- node = null;
- break;
- }
+ {
+ node = null;
+ break;
+ }
case ExpressionType.Call:
- {
- var methodCallExpression = (MethodCallExpression)node;
-
- if (methodCallExpression is not { Method.Name: "get_Item", Arguments: [ConstantExpression] })
{
- throw new ArgumentException(GetUnsupportedExpressionMessage(expression.Body), nameof(expression));
- }
+ var methodCallExpression = (MethodCallExpression)node;
+
+ if (methodCallExpression is not { Method.Name: "get_Item", Arguments: [ConstantExpression] })
+ {
+ throw new ArgumentException(GetUnsupportedExpressionMessage(expression.Body), nameof(expression));
+ }
- node = methodCallExpression.Object;
- break;
- }
+ node = methodCallExpression.Object;
+ break;
+ }
default:
- {
- throw new ArgumentException(GetUnsupportedExpressionMessage(expression.Body), nameof(expression));
- }
+ {
+ throw new ArgumentException(GetUnsupportedExpressionMessage(expression.Body), nameof(expression));
+ }
}
}
}
@@ -242,3 +243,4 @@ public static void ValidateMemberPath(
private static string GetUnsupportedExpressionMessage(Expression expression) =>
$"Expression <{expression}> cannot be used to select a member.";
}
+
diff --git a/Src/FluentAssertions/Common/Guard.cs b/Src/FluentAssertions/Common/Guard.cs
index 2ca3fa22f9..b8ff3fd48d 100644
--- a/Src/FluentAssertions/Common/Guard.cs
+++ b/Src/FluentAssertions/Common/Guard.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -6,6 +6,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class Guard
{
public static void ThrowIfArgumentIsNull([ValidatedNotNull][NoEnumeration] T obj,
diff --git a/Src/FluentAssertions/Common/IClock.cs b/Src/FluentAssertions/Common/IClock.cs
index ad953a8ad3..4e72fa8679 100644
--- a/Src/FluentAssertions/Common/IClock.cs
+++ b/Src/FluentAssertions/Common/IClock.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
diff --git a/Src/FluentAssertions/Common/ITimer.cs b/Src/FluentAssertions/Common/ITimer.cs
index c1bd9f293f..f210425614 100644
--- a/Src/FluentAssertions/Common/ITimer.cs
+++ b/Src/FluentAssertions/Common/ITimer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace FluentAssertions.Common;
diff --git a/Src/FluentAssertions/Common/IntegerExtensions.cs b/Src/FluentAssertions/Common/IntegerExtensions.cs
index 3e742bfeba..903a9dd867 100644
--- a/Src/FluentAssertions/Common/IntegerExtensions.cs
+++ b/Src/FluentAssertions/Common/IntegerExtensions.cs
@@ -1,8 +1,9 @@
-namespace FluentAssertions.Common;
+namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class IntegerExtensions
{
public static string Times(this int count) => count == 1 ? "1 time" : $"{count} times";
- internal static bool IsConsecutiveTo(this int startNumber, int endNumber) => endNumber == (startNumber + 1);
+ internal static bool IsConsecutiveTo(this int startNumber, int endNumber) => endNumber == startNumber + 1;
}
diff --git a/Src/FluentAssertions/Common/Iterator.cs b/Src/FluentAssertions/Common/Iterator.cs
index efb1dc3371..63626e001f 100644
--- a/Src/FluentAssertions/Common/Iterator.cs
+++ b/Src/FluentAssertions/Common/Iterator.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections;
using System.Collections.Generic;
@@ -8,13 +8,13 @@ namespace FluentAssertions.Common;
/// A smarter enumerator that can provide information about the relative location (current, first, last)
/// of the current item within the collection without unnecessarily iterating the collection.
///
+[System.Diagnostics.StackTraceHidden]
internal sealed class Iterator : IEnumerator
{
private const int InitialIndex = -1;
private readonly IEnumerable enumerable;
private readonly int? maxItems;
private IEnumerator enumerator;
- private T current;
private T next;
private bool hasNext;
@@ -35,10 +35,10 @@ public void Reset()
Index = InitialIndex;
enumerator = enumerable.GetEnumerator();
- hasCurrent = false;
hasNext = false;
hasCompleted = false;
- current = default;
+ Current = default;
+ hasCurrent = false;
next = default;
}
@@ -59,12 +59,12 @@ public T Current
throw new InvalidOperationException($"Please call {nameof(MoveNext)} first");
}
- return current;
+ return field;
}
private set
{
- current = value;
+ field = value;
hasCurrent = true;
}
}
@@ -138,3 +138,4 @@ public void Dispose()
enumerator.Dispose();
}
}
+
diff --git a/Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs b/Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs
index 1b2cbc65ce..fc807be666 100644
--- a/Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs
+++ b/Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class KeyValuePairCollectionExtensions
{
public static IEnumerable GetKeys(this TCollection collection)
@@ -81,3 +82,4 @@ public static TValue GetValue(this TCollection collec
};
}
}
+
diff --git a/Src/FluentAssertions/Common/MemberPath.cs b/Src/FluentAssertions/Common/MemberPath.cs
index 58334bcbca..7121d92260 100644
--- a/Src/FluentAssertions/Common/MemberPath.cs
+++ b/Src/FluentAssertions/Common/MemberPath.cs
@@ -9,6 +9,7 @@ namespace FluentAssertions.Common;
/// Encapsulates a dotted candidate to a (nested) member of a type as well as the
/// declaring type of the deepest member.
///
+[System.Diagnostics.StackTraceHidden]
internal class MemberPath
{
private readonly string dottedPath;
@@ -69,7 +70,11 @@ public bool IsSameAs(MemberPath candidate)
return false;
}
- private bool IsParentOf(MemberPath candidate)
+ ///
+ /// Determines whether the current path is the prefix of
+ /// (i.e., the current path "owns" the candidate), treating numeric indices and wildcards as interchangeable.
+ ///
+ public bool IsParentOf(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;
@@ -136,3 +141,4 @@ public override string ToString()
return dottedPath;
}
}
+
diff --git a/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs b/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
index 07dbccad00..d8fe45c39c 100644
--- a/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
+++ b/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
@@ -1,4 +1,4 @@
-#if NET6_0_OR_GREATER || NETSTANDARD2_1
+#if NET6_0_OR_GREATER || NETSTANDARD2_1
using System;
#endif
using System.Collections.Generic;
@@ -11,6 +11,7 @@ namespace FluentAssertions.Common;
/// Sets the equal with any numeric index qualifier.
/// All other comparisons are default string equality.
///
+[System.Diagnostics.StackTraceHidden]
internal class MemberPathSegmentEqualityComparer : IEqualityComparer
{
private const string AnyIndexQualifier = "*";
@@ -49,3 +50,4 @@ public int GetHashCode(string obj)
#endif
}
}
+
diff --git a/Src/FluentAssertions/Common/MethodInfoExtensions.cs b/Src/FluentAssertions/Common/MethodInfoExtensions.cs
index 98637454ed..2d1348be8a 100644
--- a/Src/FluentAssertions/Common/MethodInfoExtensions.cs
+++ b/Src/FluentAssertions/Common/MethodInfoExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -7,6 +7,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class MethodInfoExtensions
{
///
@@ -66,3 +67,4 @@ private static (bool Success, MethodImplAttribute Attribute) RecreateMethodImplA
return (false, null);
}
}
+
diff --git a/Src/FluentAssertions/Common/ObjectExtensions.cs b/Src/FluentAssertions/Common/ObjectExtensions.cs
index e1bf0f0032..8db90f98d4 100644
--- a/Src/FluentAssertions/Common/ObjectExtensions.cs
+++ b/Src/FluentAssertions/Common/ObjectExtensions.cs
@@ -5,6 +5,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class ObjectExtensions
{
public static Func GetComparer()
@@ -87,3 +88,4 @@ public static string ToFormattedString(this object source)
return Formatter.ToString(source);
}
}
+
diff --git a/Src/FluentAssertions/Common/PropertyInfoExtensions.cs b/Src/FluentAssertions/Common/PropertyInfoExtensions.cs
index ec2f738dd2..587cea6828 100644
--- a/Src/FluentAssertions/Common/PropertyInfoExtensions.cs
+++ b/Src/FluentAssertions/Common/PropertyInfoExtensions.cs
@@ -1,7 +1,8 @@
-using System.Reflection;
+using System.Reflection;
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class PropertyInfoExtensions
{
internal static bool IsVirtual(this PropertyInfo property)
@@ -22,3 +23,4 @@ internal static bool IsAbstract(this PropertyInfo property)
return methodInfo.IsAbstract;
}
}
+
diff --git a/Src/FluentAssertions/Common/StartTimer.cs b/Src/FluentAssertions/Common/StartTimer.cs
index 63870db3bd..422bcb6ee7 100644
--- a/Src/FluentAssertions/Common/StartTimer.cs
+++ b/Src/FluentAssertions/Common/StartTimer.cs
@@ -1,4 +1,4 @@
-namespace FluentAssertions.Common;
+namespace FluentAssertions.Common;
///
/// Factory for starting a timer on demand.
diff --git a/Src/FluentAssertions/Common/StopwatchTimer.cs b/Src/FluentAssertions/Common/StopwatchTimer.cs
index 889a316e5f..dbda4bcac5 100644
--- a/Src/FluentAssertions/Common/StopwatchTimer.cs
+++ b/Src/FluentAssertions/Common/StopwatchTimer.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Diagnostics;
namespace FluentAssertions.Common;
+[StackTraceHidden]
internal sealed class StopwatchTimer : ITimer
{
private readonly Stopwatch stopwatch = Stopwatch.StartNew();
@@ -19,3 +20,4 @@ public void Dispose()
}
}
}
+
diff --git a/Src/FluentAssertions/Common/StringExtensions.cs b/Src/FluentAssertions/Common/StringExtensions.cs
index d24ab5eab2..f8e5c6b503 100644
--- a/Src/FluentAssertions/Common/StringExtensions.cs
+++ b/Src/FluentAssertions/Common/StringExtensions.cs
@@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Linq;
+#if NET6_0_OR_GREATER
+using System.Runtime.CompilerServices;
+#endif
using System.Text.RegularExpressions;
using FluentAssertions.Formatting;
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class StringExtensions
{
///
@@ -178,4 +183,16 @@ public static string RenderAsIndentedBlock(this string message)
return " \"" + string.Join(Environment.NewLine + " ", wrappedLines) + "\"";
}
+
+ ///
+ /// Simplifies setting the invariant culture when using interpolated strings.
+ ///
+#if NET6_0_OR_GREATER
+ public static string Invariant(ref DefaultInterpolatedStringHandler formattable)
+ => string.Create(CultureInfo.InvariantCulture, ref formattable);
+#else
+ public static string Invariant(FormattableString formattable)
+ => string.Create(CultureInfo.InvariantCulture, formattable);
+#endif
}
+
diff --git a/Src/FluentAssertions/Common/TimeOnlyExtensions.cs b/Src/FluentAssertions/Common/TimeOnlyExtensions.cs
index e327319193..cee301084c 100644
--- a/Src/FluentAssertions/Common/TimeOnlyExtensions.cs
+++ b/Src/FluentAssertions/Common/TimeOnlyExtensions.cs
@@ -3,6 +3,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class TimeOnlyExtensions
{
///
@@ -27,3 +28,4 @@ public static bool IsCloseTo(this TimeOnly subject, TimeOnly other, TimeSpan pre
}
#endif
+
diff --git a/Src/FluentAssertions/Common/TypeExtensions.cs b/Src/FluentAssertions/Common/TypeExtensions.cs
index 4cd89baa9c..1a2c86354e 100644
--- a/Src/FluentAssertions/Common/TypeExtensions.cs
+++ b/Src/FluentAssertions/Common/TypeExtensions.cs
@@ -11,6 +11,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class TypeExtensions
{
private const BindingFlags PublicInstanceMembersFlag =
@@ -482,3 +483,4 @@ public static Type NullableOrActualType(this Type type)
return type;
}
}
+
diff --git a/Src/FluentAssertions/Common/TypeReflector.cs b/Src/FluentAssertions/Common/TypeReflector.cs
index d630c2f0a3..33b5fd9879 100644
--- a/Src/FluentAssertions/Common/TypeReflector.cs
+++ b/Src/FluentAssertions/Common/TypeReflector.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -6,6 +6,7 @@
namespace FluentAssertions.Common;
+[System.Diagnostics.StackTraceHidden]
internal static class TypeReflector
{
public static IEnumerable GetAllTypesFromAppDomain(Func predicate)
@@ -56,3 +57,4 @@ private static IEnumerable GetExportedTypes(Assembly assembly)
}
}
}
+
diff --git a/Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs b/Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs
index cc938cc6a2..67eb0cfada 100644
--- a/Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs
+++ b/Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs
@@ -1,9 +1,9 @@
-using FluentAssertions.Configuration;
+using FluentAssertions.Configuration;
namespace FluentAssertions.Common;
///
-/// Defines the modes in which custom implementations of
+/// Defines the modes in which custom implementations of
/// are detected as configured through .
///
public enum ValueFormatterDetectionMode
diff --git a/Src/FluentAssertions/Configuration/GlobalConfiguration.cs b/Src/FluentAssertions/Configuration/GlobalConfiguration.cs
index ebe6577e69..5dba8e4ecd 100644
--- a/Src/FluentAssertions/Configuration/GlobalConfiguration.cs
+++ b/Src/FluentAssertions/Configuration/GlobalConfiguration.cs
@@ -1,9 +1,8 @@
namespace FluentAssertions.Configuration;
+[System.Diagnostics.StackTraceHidden]
public class GlobalConfiguration
{
- private TestFramework? testFramework;
-
///
/// Provides access to the formatting defaults for all assertions.
///
@@ -22,10 +21,10 @@ public class GlobalConfiguration
///
public TestFramework? TestFramework
{
- get => testFramework;
+ get;
set
{
- testFramework = value;
+ field = value;
AssertionEngine.TestFramework = null;
}
}
diff --git a/Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs b/Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs
index 059efa534a..7b5081ea33 100644
--- a/Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs
+++ b/Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs
@@ -1,10 +1,12 @@
using System;
+
using FluentAssertions.Common;
using FluentAssertions.Equivalency;
using JetBrains.Annotations;
namespace FluentAssertions.Configuration;
+[System.Diagnostics.StackTraceHidden]
public class GlobalEquivalencyOptions
{
private EquivalencyOptions defaults = new();
@@ -49,3 +51,4 @@ public EquivalencyOptions CloneDefaults()
return new EquivalencyOptions(defaults);
}
}
+
diff --git a/Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs b/Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs
index 4676e9c890..f15e6ea1ef 100644
--- a/Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs
+++ b/Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs
@@ -4,16 +4,15 @@
namespace FluentAssertions.Configuration;
+[System.Diagnostics.StackTraceHidden]
public class GlobalFormattingOptions : FormattingOptions
{
- private string valueFormatterAssembly;
-
public string ValueFormatterAssembly
{
- get => valueFormatterAssembly;
+ get;
set
{
- valueFormatterAssembly = value;
+ field = value;
ValueFormatterDetectionMode = ValueFormatterDetectionMode.Specific;
}
}
@@ -34,3 +33,4 @@ public string ValueFormatterAssembly
};
}
}
+
diff --git a/Src/FluentAssertions/CustomAssertionAttribute.cs b/Src/FluentAssertions/CustomAssertionAttribute.cs
index 87dfa929b1..198b7a96c4 100644
--- a/Src/FluentAssertions/CustomAssertionAttribute.cs
+++ b/Src/FluentAssertions/CustomAssertionAttribute.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace FluentAssertions;
@@ -8,4 +8,6 @@ namespace FluentAssertions;
///
[AttributeUsage(AttributeTargets.Method)]
#pragma warning disable CA1813 // Avoid unsealed attributes. This type has shipped.
+[System.Diagnostics.StackTraceHidden]
public class CustomAssertionAttribute : Attribute;
+
diff --git a/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs b/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs
index 9e0cc90410..6b332c1d81 100644
--- a/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs
+++ b/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs
@@ -7,4 +7,6 @@ namespace FluentAssertions;
/// internally, or directly uses AssertionChain.
///
[AttributeUsage(AttributeTargets.Assembly)]
+[System.Diagnostics.StackTraceHidden]
public sealed class CustomAssertionsAssemblyAttribute : Attribute;
+
diff --git a/Src/FluentAssertions/Disposable.cs b/Src/FluentAssertions/Disposable.cs
index ac98c8167a..3cd8ec4337 100644
--- a/Src/FluentAssertions/Disposable.cs
+++ b/Src/FluentAssertions/Disposable.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
namespace FluentAssertions;
+[System.Diagnostics.StackTraceHidden]
internal sealed class Disposable : IDisposable
{
private readonly Action action;
@@ -16,3 +17,4 @@ public void Dispose()
action();
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs b/Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs
index d7cfd1e1e5..aff493b87b 100644
--- a/Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs
+++ b/Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
internal static class AssertionChainExtensions
{
///
@@ -17,3 +18,4 @@ public static AssertionChain For(this AssertionChain chain, IEquivalencyValidati
.BecauseOf(context.Reason);
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Comparands.cs b/Src/FluentAssertions/Equivalency/Comparands.cs
index 4e763acf9b..2f483e8476 100644
--- a/Src/FluentAssertions/Equivalency/Comparands.cs
+++ b/Src/FluentAssertions/Equivalency/Comparands.cs
@@ -1,20 +1,20 @@
-using System;
+using System;
+
+using System.Globalization;
using FluentAssertions.Common;
-using static System.FormattableString;
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
public class Comparands
{
- private Type compileTimeType;
-
public Comparands()
{
}
public Comparands(object subject, object expectation, Type compileTimeType)
{
- this.compileTimeType = compileTimeType;
+ CompileTimeType = compileTimeType;
Subject = subject;
Expectation = expectation;
}
@@ -36,11 +36,11 @@ public Type CompileTimeType
{
get
{
- return compileTimeType != typeof(object) || Expectation is null ? compileTimeType : RuntimeType;
+ return field != typeof(object) || Expectation is null ? field : RuntimeType;
}
// SMELL: Do we really need this? Can we replace it by making Comparands generic or take a constructor parameter?
- set => compileTimeType = value;
+ set;
}
///
@@ -74,6 +74,7 @@ public Type GetExpectedType(IEquivalencyOptions options)
public override string ToString()
{
- return Invariant($"{{Subject={Subject}, Expectation={Expectation}}}");
+ return string.Create(CultureInfo.InvariantCulture, $"{{Subject={Subject}, Expectation={Expectation}}}");
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/ConversionSelector.cs b/Src/FluentAssertions/Equivalency/ConversionSelector.cs
index c2985fcae1..f0f8248a34 100644
--- a/Src/FluentAssertions/Equivalency/ConversionSelector.cs
+++ b/Src/FluentAssertions/Equivalency/ConversionSelector.cs
@@ -11,6 +11,7 @@ namespace FluentAssertions.Equivalency;
///
/// Collects the members that need to be converted by the .
///
+[System.Diagnostics.StackTraceHidden]
public class ConversionSelector
{
private sealed class ConversionSelectorRule
@@ -89,7 +90,7 @@ public override string ToString()
{
if (inclusions.Count == 0 && exclusions.Count == 0)
{
- return "Without automatic conversion.";
+ return "Without automatic conversion";
}
var descriptionBuilder = new StringBuilder();
@@ -112,3 +113,4 @@ public ConversionSelector Clone()
return new ConversionSelector(new List(inclusions), new List(exclusions));
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Digit.cs b/Src/FluentAssertions/Equivalency/Digit.cs
index 123b552896..56a41b9d8e 100644
--- a/Src/FluentAssertions/Equivalency/Digit.cs
+++ b/Src/FluentAssertions/Equivalency/Digit.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
internal class Digit
{
private readonly int length;
@@ -49,3 +50,4 @@ public bool Increment()
return success;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs b/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs
index 1d1c62864a..57bcadc959 100644
--- a/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs
+++ b/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs
@@ -8,6 +8,7 @@
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
internal sealed class EqualityStrategyProvider
{
private readonly List referenceTypes = [];
@@ -17,8 +18,6 @@ internal sealed class EqualityStrategyProvider
[CanBeNull]
private readonly Func defaultStrategy;
- private bool? compareRecordsByValue;
-
public EqualityStrategyProvider()
{
}
@@ -30,10 +29,10 @@ public EqualityStrategyProvider(Func defaultStrategy)
public bool? CompareRecordsByValue
{
- get => compareRecordsByValue;
+ get;
set
{
- compareRecordsByValue = value;
+ field = value;
typeCache.Clear();
}
}
@@ -65,9 +64,9 @@ public EqualityStrategy GetEqualityStrategy(Type type)
return EqualityStrategy.ForceEquals;
}
- if ((compareRecordsByValue != null || defaultStrategy is null) && typeKey.IsRecord())
+ if ((CompareRecordsByValue != null || defaultStrategy is null) && typeKey.IsRecord())
{
- return compareRecordsByValue is true ? EqualityStrategy.ForceEquals : EqualityStrategy.ForceMembers;
+ return CompareRecordsByValue == true ? EqualityStrategy.ForceEquals : EqualityStrategy.ForceMembers;
}
if (defaultStrategy is not null)
@@ -107,7 +106,7 @@ public override string ToString()
{
var builder = new StringBuilder();
- if (compareRecordsByValue is true)
+ if (CompareRecordsByValue == true)
{
builder.AppendLine("- Compare records by value");
}
@@ -129,3 +128,4 @@ public override string ToString()
return builder.ToString();
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyOptions.cs b/Src/FluentAssertions/Equivalency/EquivalencyOptions.cs
index 039a2195c1..857e94d96d 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyOptions.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyOptions.cs
@@ -12,6 +12,7 @@ namespace FluentAssertions.Equivalency;
///
/// Represents the run-time type-specific behavior of a structural equivalency assertion.
///
+[System.Diagnostics.StackTraceHidden]
public class EquivalencyOptions
: SelfReferenceEquivalencyOptions>
{
@@ -94,7 +95,7 @@ public EquivalencyOptions WithoutStrictOrderingFor(
}
///
- /// Creates a new set of options based on the current instance which acts on a a collection of the .
+ /// Creates a new set of options based on the current instance which acts on a collection of the .
///
public EquivalencyOptions> AsCollection()
{
@@ -208,3 +209,4 @@ public EquivalencyOptions()
PreferringDeclaredMemberTypes();
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyPlan.cs b/Src/FluentAssertions/Equivalency/EquivalencyPlan.cs
index f81b3d48c2..edf514bed3 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyPlan.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyPlan.cs
@@ -1,5 +1,3 @@
-#region
-
using System;
using System.Collections;
using System.Collections.Generic;
@@ -7,14 +5,13 @@
using FluentAssertions.Equivalency.Inlining;
using FluentAssertions.Equivalency.Steps;
-#endregion
-
namespace FluentAssertions.Equivalency;
///
/// Represents a mutable collection of equivalency steps that can be reordered and/or amended with additional
/// custom equivalency steps.
///
+[System.Diagnostics.StackTraceHidden]
public class EquivalencyPlan : IEnumerable
{
private List steps = GetDefaultSteps();
@@ -166,3 +163,4 @@ private static List GetDefaultSteps()
];
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyStep.cs b/Src/FluentAssertions/Equivalency/EquivalencyStep.cs
index 58f689f771..497e7677f3 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyStep.cs
@@ -1,8 +1,9 @@
-namespace FluentAssertions.Equivalency;
+namespace FluentAssertions.Equivalency;
///
/// Convenient implementation of that will only invoke
///
+[System.Diagnostics.StackTraceHidden]
public abstract class EquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs b/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
index d6cee4a0cd..c00e6ae223 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
@@ -1,13 +1,18 @@
+#if !NET6_0_OR_GREATER
+using System;
+#endif
+
+using System.Globalization;
using FluentAssertions.Equivalency.Execution;
using FluentAssertions.Equivalency.Tracing;
using FluentAssertions.Execution;
-using static System.FormattableString;
namespace FluentAssertions.Equivalency;
///
/// Provides information on a particular property during an assertion for structural equality of two object graphs.
///
+[System.Diagnostics.StackTraceHidden]
public class EquivalencyValidationContext : IEquivalencyValidationContext
{
private Tracer tracer;
@@ -27,7 +32,7 @@ public EquivalencyValidationContext(INode root, IEquivalencyOptions options)
public IEquivalencyOptions Options { get; }
- private CyclicReferenceDetector CyclicReferenceDetector { get; set; }
+ internal CyclicReferenceDetector CyclicReferenceDetector { get; set; }
public IEquivalencyValidationContext AsNestedMember(IMember expectationMember)
{
@@ -98,6 +103,7 @@ internal void ResetTracing()
public override string ToString()
{
- return Invariant($"{{Path=\"{CurrentNode.Subject.PathAndName}\"}}");
+ return string.Create(CultureInfo.InvariantCulture, $"{{Path=\"{CurrentNode.Subject.PathAndName}\"}}");
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
index 6a796efdec..25f40f194b 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
@@ -7,6 +7,7 @@ namespace FluentAssertions.Equivalency;
///
/// Is responsible for validating the equivalency of a subject with another object.
///
+[System.Diagnostics.StackTraceHidden]
internal class EquivalencyValidator : IValidateChildNodeEquivalency
{
private const int MaxDepth = 10;
diff --git a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs
index 445537aa1e..a5ba14b3ec 100644
--- a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs
+++ b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs
@@ -11,6 +11,7 @@ namespace FluentAssertions.Equivalency.Execution;
///
/// Ensures that all the rules remove the collection index from the path before processing it further.
///
+[System.Diagnostics.StackTraceHidden]
internal class CollectionMemberOptionsDecorator : IEquivalencyOptions, IContainTypingRules
{
private readonly IEquivalencyOptions inner;
@@ -91,5 +92,14 @@ public EqualityStrategy GetEqualityStrategy(Type type)
///
public bool IgnoreJsonPropertyCasing => inner.IgnoreJsonPropertyCasing;
+ public bool EnableFullDump => inner.EnableFullDump;
+
+ ///
+ public bool TreatNullCollectionsAsEmpty => inner.TreatNullCollectionsAsEmpty;
+
+ ///
+ public bool TreatNullStringsAsEmpty => inner.TreatNullStringsAsEmpty;
+
public ITraceWriter TraceWriter => inner.TraceWriter;
}
+
diff --git a/Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs b/Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs
index 4ca13ce49d..d8d21327bb 100644
--- a/Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs
+++ b/Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs
@@ -7,6 +7,7 @@ namespace FluentAssertions.Equivalency.Execution;
/// Keeps track of objects and their location within an object graph so that cyclic references can be detected
/// and handled upon.
///
+[System.Diagnostics.StackTraceHidden]
internal class CyclicReferenceDetector : ICloneable2
{
#region Private Definitions
@@ -45,3 +46,4 @@ public object Clone()
};
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs b/Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs
index 345e572a05..0733af802c 100644
--- a/Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs
+++ b/Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Equivalency.Execution;
+[System.Diagnostics.StackTraceHidden]
internal class ObjectInfo : IObjectInfo
{
public ObjectInfo(Comparands comparands, INode currentNode)
@@ -23,3 +24,4 @@ public ObjectInfo(Comparands comparands, INode currentNode)
public Type RuntimeType { get; }
}
+
diff --git a/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs b/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs
index 4d68cb292d..58a782bd1a 100644
--- a/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs
+++ b/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs
@@ -1,14 +1,15 @@
using System;
+using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using FluentAssertions.Common;
-using static System.FormattableString;
namespace FluentAssertions.Equivalency.Execution;
///
/// Represents an object tracked by the including it's location within an object graph.
///
+[System.Diagnostics.StackTraceHidden]
internal class ObjectReference
{
private readonly object @object;
@@ -76,7 +77,7 @@ public override int GetHashCode()
public override string ToString()
{
- return Invariant($"{{\"{path}\", {@object}}}");
+ return string.Create(CultureInfo.InvariantCulture, $"{{\"{path}\", {@object}}}");
}
///
@@ -90,3 +91,4 @@ public override string ToString()
///
public bool CompareByMembers { get; }
}
+
diff --git a/Src/FluentAssertions/Equivalency/Field.cs b/Src/FluentAssertions/Equivalency/Field.cs
index fc3d00a2fd..4fd1af6c72 100644
--- a/Src/FluentAssertions/Equivalency/Field.cs
+++ b/Src/FluentAssertions/Equivalency/Field.cs
@@ -8,6 +8,7 @@ namespace FluentAssertions.Equivalency;
///
/// A specialized type of that represents a field of an object in a structural equivalency assertion.
///
+[System.Diagnostics.StackTraceHidden]
internal class Field : Node, IMember
{
private readonly FieldInfo fieldInfo;
@@ -18,7 +19,7 @@ public Field(FieldInfo fieldInfo, INode parent)
this.fieldInfo = fieldInfo;
DeclaringType = fieldInfo.DeclaringType;
ReflectedType = fieldInfo.ReflectedType;
- Subject = new Pathway(parent.Subject.PathAndName, fieldInfo.Name, pathAndName => $"field {parent.GetSubjectId().Combine(pathAndName)}");
+ Subject = new Pathway(parent.Subject.PathAndName, fieldInfo.Name, pathAndName => $"field {parent.GetSubjectId().Combine(pathAndName)}");
Expectation = new Pathway(parent.Expectation.PathAndName, fieldInfo.Name, pathAndName => $"field {pathAndName}");
GetSubjectId = parent.GetSubjectId;
Type = fieldInfo.FieldType;
@@ -33,7 +34,7 @@ public object GetValue(object obj)
return fieldInfo.GetValue(obj);
}
- public Type DeclaringType { get; set; }
+ public Type DeclaringType { get; }
public CSharpAccessModifier GetterAccessibility => fieldInfo.GetCSharpAccessModifier();
@@ -42,3 +43,4 @@ public object GetValue(object obj)
public bool IsBrowsable =>
isBrowsable ??= fieldInfo.GetCustomAttribute() is not { State: EditorBrowsableState.Never };
}
+
diff --git a/Src/FluentAssertions/Equivalency/IAssertionContext.cs b/Src/FluentAssertions/Equivalency/IAssertionContext.cs
index 1c6d662a82..4b993796ad 100644
--- a/Src/FluentAssertions/Equivalency/IAssertionContext.cs
+++ b/Src/FluentAssertions/Equivalency/IAssertionContext.cs
@@ -18,7 +18,7 @@ public interface IAssertionContext
TSubject Subject { get; }
///
- /// Gets the value of the expectation object that was matched with the subject using a .
+ /// Gets the value of the expectation object that was matched with the subject using an .
///
TSubject Expectation { get; }
diff --git a/Src/FluentAssertions/Equivalency/IEquivalencyOptions.cs b/Src/FluentAssertions/Equivalency/IEquivalencyOptions.cs
index 61a77d6efe..03d17a0734 100644
--- a/Src/FluentAssertions/Equivalency/IEquivalencyOptions.cs
+++ b/Src/FluentAssertions/Equivalency/IEquivalencyOptions.cs
@@ -132,4 +132,19 @@ public interface IEquivalencyOptions
/// When set to true, the comparison will ignore the casing of JSON property names when comparing objects to JSON trees.
///
bool IgnoreJsonPropertyCasing { get; }
+
+ ///
+ /// Gets a value indicating whether the full dump of the subject should be included in the failure message.
+ ///
+ bool EnableFullDump { get; }
+
+ ///
+ /// Gets a value indicating whether null collections should be treated as equivalent to empty collections.
+ ///
+ bool TreatNullCollectionsAsEmpty { get; }
+
+ ///
+ /// Gets a value indicating whether null strings should be treated as equivalent to empty strings.
+ ///
+ bool TreatNullStringsAsEmpty { get; }
}
diff --git a/Src/FluentAssertions/Equivalency/INode.cs b/Src/FluentAssertions/Equivalency/INode.cs
index 102760f49f..9434efdee3 100644
--- a/Src/FluentAssertions/Equivalency/INode.cs
+++ b/Src/FluentAssertions/Equivalency/INode.cs
@@ -30,7 +30,7 @@ public interface INode
///
/// Gets the path from the root of the subject upto and including the current node.
///
- Pathway Subject { get; internal set; }
+ Pathway Subject { get; }
///
/// Gets the path from the root of the expectation upto and including the current node.
diff --git a/Src/FluentAssertions/Equivalency/IObjectInfo.cs b/Src/FluentAssertions/Equivalency/IObjectInfo.cs
index a02a04a1a0..5a10a3da8e 100644
--- a/Src/FluentAssertions/Equivalency/IObjectInfo.cs
+++ b/Src/FluentAssertions/Equivalency/IObjectInfo.cs
@@ -30,7 +30,7 @@ public interface IObjectInfo
///
/// Gets the compile-time type of the current object. If the current object is not the root object and the type is not ,
- /// then it returns the same as the property does.
+ /// then it returns the same as the property does.
///
Type CompileTimeType { get; }
diff --git a/Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs b/Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs
index d314e64b53..2eff6916d0 100644
--- a/Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs
+++ b/Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs
@@ -9,6 +9,7 @@ namespace FluentAssertions.Equivalency.Inlining;
/// of the assertion APIs provided by Fluent Assertions.
///
/// The expected type of the subject to which the assertion is applied.
+[System.Diagnostics.StackTraceHidden]
internal class ActionBasedInlineAssertion(Action assertion) : IInlineEquivalencyAssertion
{
///
@@ -24,3 +25,4 @@ public void Execute(AssertionChain assertionChain, Comparands comparands)
}
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs b/Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs
index eef612dbbd..1f48475dc4 100644
--- a/Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs
+++ b/Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs
@@ -8,6 +8,7 @@ namespace FluentAssertions.Equivalency.Inlining;
/// Represents a condition-based inline equivalency assertion that evaluates a specified condition against a subject during object equivalency checks.
///
/// The expected type of the subject being asserted.
+[System.Diagnostics.StackTraceHidden]
internal class ConditionBasedInlineAssertion(Expression> condition) : IInlineEquivalencyAssertion
{
///
@@ -22,3 +23,4 @@ public void Execute(AssertionChain assertionChain, Comparands comparands)
.FailWith("Expected {context:subject} to meet condition {0}, but it did not.", condition);
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs
index b4c5b62f97..f9d5fe9ee7 100644
--- a/Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs
@@ -13,6 +13,7 @@ namespace FluentAssertions.Equivalency.Inlining;
/// This step allows users to define custom equivalency behaviors inline during assertion
/// execution.
///
+[System.Diagnostics.StackTraceHidden]
public class InlineEquivalencyStep : IEquivalencyStep
{
///
@@ -31,3 +32,4 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
return EquivalencyResult.ContinueWithNext;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/JsonProperty.cs b/Src/FluentAssertions/Equivalency/JsonProperty.cs
index c50668f4b0..58b679667d 100644
--- a/Src/FluentAssertions/Equivalency/JsonProperty.cs
+++ b/Src/FluentAssertions/Equivalency/JsonProperty.cs
@@ -12,6 +12,7 @@ namespace FluentAssertions.Equivalency;
/// information about a specific JSON property, its parent object, and its counterpart in an expectation object
/// when performing comparisons.
///
+[System.Diagnostics.StackTraceHidden]
internal class JsonProperty(JsonNode property, JsonObject parent, INode expectationParent) : IMember
{
// SMELL: A lot of properties are required by the IMember interface, but they are not used. In the future
@@ -145,3 +146,4 @@ public static JsonProperty Find(IMember expectedMember, INode parent, object sub
}
#endif
+
diff --git a/Src/FluentAssertions/Equivalency/MemberFactory.cs b/Src/FluentAssertions/Equivalency/MemberFactory.cs
index 6fed0af560..b34c371c83 100644
--- a/Src/FluentAssertions/Equivalency/MemberFactory.cs
+++ b/Src/FluentAssertions/Equivalency/MemberFactory.cs
@@ -1,9 +1,11 @@
using System;
+
using System.Reflection;
using FluentAssertions.Common;
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
public static class MemberFactory
{
public static IMember Create(MemberInfo memberInfo, INode parent)
@@ -29,3 +31,4 @@ internal static IMember Find(object target, string memberName, INode parent)
return field is not null ? new Field(field, parent) : null;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/MemberSelectionContext.cs b/Src/FluentAssertions/Equivalency/MemberSelectionContext.cs
index 2e5417bf16..f234134479 100644
--- a/Src/FluentAssertions/Equivalency/MemberSelectionContext.cs
+++ b/Src/FluentAssertions/Equivalency/MemberSelectionContext.cs
@@ -6,6 +6,7 @@ namespace FluentAssertions.Equivalency;
///
/// Provides contextual information to an .
///
+[System.Diagnostics.StackTraceHidden]
public class MemberSelectionContext
{
private readonly Type compileTimeType;
@@ -42,3 +43,4 @@ public Type Type
}
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/MemberVisibility.cs b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
index a0eddebf20..f41b857b57 100644
--- a/Src/FluentAssertions/Equivalency/MemberVisibility.cs
+++ b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
#pragma warning disable CA1714
namespace FluentAssertions.Equivalency;
diff --git a/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs b/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs
index 01abeeba12..1f14f26269 100644
--- a/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs
+++ b/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs
@@ -4,6 +4,7 @@
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
internal static class MemberVisibilityExtensions
{
private static readonly ConcurrentDictionary Cache = new();
@@ -41,3 +42,4 @@ public static MemberKind ToMemberKind(this MemberVisibility visibility)
});
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs
index 4962f36860..70d6cb84df 100644
--- a/Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using FluentAssertions.Execution;
@@ -8,6 +8,7 @@ namespace FluentAssertions.Equivalency;
/// Supports recursively comparing two multi-dimensional arrays for equivalency using strict order for the array items
/// themselves.
///
+[System.Diagnostics.StackTraceHidden]
internal class MultiDimensionalArrayEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -105,3 +106,4 @@ private static bool HaveSameRank(object subject, Array expectation, AssertionCha
return assertionChain.Succeeded;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs b/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
index 1708c3e7a7..1dc2a26169 100644
--- a/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
+++ b/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using FluentAssertions.Common;
@@ -6,6 +6,7 @@
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
public class NestedExclusionOptionBuilder
{
///
@@ -49,3 +50,4 @@ public NestedExclusionOptionBuilder For(
return new NestedExclusionOptionBuilder(capturedOptions, currentPathSelectionRule);
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Node.cs b/Src/FluentAssertions/Equivalency/Node.cs
index b8b08bc94a..6de5450342 100644
--- a/Src/FluentAssertions/Equivalency/Node.cs
+++ b/Src/FluentAssertions/Equivalency/Node.cs
@@ -1,41 +1,36 @@
using System;
using System.Collections;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using FluentAssertions.Common;
namespace FluentAssertions.Equivalency;
+[System.Diagnostics.StackTraceHidden]
internal class Node : INode
{
private static readonly Regex MatchFirstIndex = new(@"^\[[0-9]+\]$");
- private GetSubjectId subjectIdProvider;
-
private string cachedSubjectId;
- private Pathway subject;
public GetSubjectId GetSubjectId
{
- get => () => cachedSubjectId ??= subjectIdProvider();
- protected init => subjectIdProvider = value;
+ get => () => cachedSubjectId ??= field();
+ protected init => field = value;
}
- public Type Type { get; protected set; }
+ public Type Type { get; init; }
- public Type ParentType { get; protected set; }
+ public Type ParentType { get; init; }
public Pathway Subject
{
- get => subject;
- set
+ get;
+ protected set
{
- subject = value;
-
- if (Expectation is null)
- {
- Expectation = value;
- }
+ field = value;
+ Expectation ??= value;
}
}
@@ -53,7 +48,7 @@ public bool IsRoot
private bool IsFirstIndex => MatchFirstIndex.IsMatch(Subject.PathAndName);
- public bool RootIsCollection { get; protected set; }
+ public bool RootIsCollection { get; init; }
public void AdjustForRemappedSubject(IMember subjectMember)
{
@@ -78,7 +73,7 @@ public static INode From(GetSubjectId getSubjectId)
{
return new Node
{
- subjectIdProvider = () => getSubjectId() ?? "root",
+ GetSubjectId = () => getSubjectId() ?? "root",
Subject = new Pathway(string.Empty, string.Empty, _ => getSubjectId()),
Type = typeof(T),
ParentType = null,
@@ -142,6 +137,7 @@ public override bool Equals(object obj)
private bool Equals(Node other) => (Type, Subject.Name, Subject.Path) == (other.Type, other.Subject.Name, other.Subject.Path);
+ [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override int GetHashCode()
{
unchecked
@@ -156,3 +152,4 @@ public override int GetHashCode()
public override string ToString() => Subject.Description;
}
+
diff --git a/Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs b/Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs
index 645bba4ad2..20a1581893 100644
--- a/Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs
+++ b/Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs
@@ -7,6 +7,7 @@ namespace FluentAssertions.Equivalency;
///
/// Collection of s.
///
+[System.Diagnostics.StackTraceHidden]
public class OrderingRuleCollection : IEnumerable
{
private readonly List rules = [];
@@ -30,7 +31,7 @@ public OrderingRuleCollection(IEnumerable orderingRules)
/// Returns an enumerator that iterates through the collection.
///
///
- /// A that can be used to iterate through the collection.
+ /// An that can be used to iterate through the collection.
///
/// 1
public IEnumerator GetEnumerator()
@@ -42,7 +43,7 @@ public IEnumerator GetEnumerator()
/// Returns an enumerator that iterates through a collection.
///
///
- /// An object that can be used to iterate through the collection.
+ /// An object that can be used to iterate through the collection.
///
/// 2
IEnumerator IEnumerable.GetEnumerator()
@@ -70,3 +71,4 @@ public bool IsOrderingStrictFor(IObjectInfo objectInfo)
return results.Contains(OrderStrictness.Strict) && !results.Contains(OrderStrictness.NotStrict);
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Pathway.cs b/Src/FluentAssertions/Equivalency/Pathway.cs
index 8bd3f24689..5eccbe4299 100644
--- a/Src/FluentAssertions/Equivalency/Pathway.cs
+++ b/Src/FluentAssertions/Equivalency/Pathway.cs
@@ -9,8 +9,6 @@ public record Pathway
{
public delegate string GetDescription(string pathAndName);
- private string path = string.Empty;
- private string name = string.Empty;
private string pathAndName;
private readonly GetDescription getDescription;
@@ -38,10 +36,10 @@ public Pathway(Pathway parent, string name, GetDescription getDescription)
///
public string Path
{
- get => path;
+ get;
private init
{
- path = value;
+ field = value;
pathAndName = null;
}
}
@@ -51,10 +49,10 @@ private init
///
public string Name
{
- get => name;
+ get => field;
private init
{
- name = value;
+ field = value;
pathAndName = null;
}
}
@@ -62,7 +60,7 @@ private init
///
/// Gets the path and name of the field or property separated by dots.
///
- public string PathAndName => pathAndName ??= path.Combine(name);
+ public string PathAndName => pathAndName ??= Path.Combine(Name);
///
/// Gets the display representation of this path.
diff --git a/Src/FluentAssertions/Equivalency/Property.cs b/Src/FluentAssertions/Equivalency/Property.cs
index 33cce0a884..31a2fc3ccf 100644
--- a/Src/FluentAssertions/Equivalency/Property.cs
+++ b/Src/FluentAssertions/Equivalency/Property.cs
@@ -9,6 +9,7 @@ namespace FluentAssertions.Equivalency;
/// A specialized type of that represents a property of an object in a structural equivalency assertion.
///
#pragma warning disable CA1716
+[System.Diagnostics.StackTraceHidden]
internal class Property : Node, IMember
{
private readonly PropertyInfo propertyInfo;
@@ -24,7 +25,7 @@ public Property(Type reflectedType, PropertyInfo propertyInfo, INode parent)
ReflectedType = reflectedType;
this.propertyInfo = propertyInfo;
DeclaringType = propertyInfo.DeclaringType;
- Subject = new Pathway(parent.Subject.PathAndName, propertyInfo.Name, pathAndName => $"property {parent.GetSubjectId().Combine(pathAndName)}");
+ Subject = new Pathway(parent.Subject.PathAndName, propertyInfo.Name, pathAndName => $"property {parent.GetSubjectId().Combine(pathAndName)}");
Expectation = new Pathway(parent.Expectation.PathAndName, propertyInfo.Name, pathAndName => $"property {pathAndName}");
Type = propertyInfo.PropertyType;
ParentType = propertyInfo.DeclaringType;
@@ -45,14 +46,7 @@ public object GetValue(object obj)
public CSharpAccessModifier SetterAccessibility => propertyInfo.GetSetMethod(nonPublic: true).GetCSharpAccessModifier();
- public bool IsBrowsable
- {
- get
- {
- isBrowsable ??=
- propertyInfo.GetCustomAttribute() is not { State: EditorBrowsableState.Never };
-
- return isBrowsable.Value;
- }
- }
+ public bool IsBrowsable =>
+ isBrowsable ??= propertyInfo.GetCustomAttribute() is not { State: EditorBrowsableState.Never };
}
+
diff --git a/Src/FluentAssertions/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs b/Src/FluentAssertions/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
index cfcdb8d5da..8fab431258 100644
--- a/Src/FluentAssertions/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
@@ -2,15 +2,8 @@
namespace FluentAssertions.Equivalency.Selection;
-internal class CollectionMemberSelectionRuleDecorator : IMemberSelectionRule
+internal class CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule) : IPathBasedSelectionRule
{
- private readonly IMemberSelectionRule selectionRule;
-
- public CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule)
- {
- this.selectionRule = selectionRule;
- }
-
public bool IncludesMembers => selectionRule.IncludesMembers;
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
@@ -19,6 +12,12 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable
/// Selection rule that removes a particular property from the structural comparison.
///
-internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
+internal class ExcludeMemberByPathSelectionRule(MemberPath pathToExclude) : SelectMemberByPathSelectionRule
{
- private MemberPath memberToExclude;
-
- public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
- {
- memberToExclude = pathToExclude;
- }
+ private MemberPath memberToExclude = pathToExclude;
protected override void AddOrRemoveMembersFrom(List selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
@@ -27,6 +22,8 @@ public void AppendPath(MemberPath nextPath)
memberToExclude = memberToExclude.AsParentCollectionOf(nextPath);
}
+ protected override MemberPath MemberPath => memberToExclude;
+
public MemberPath CurrentPath => memberToExclude;
public override string ToString()
diff --git a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs
index cc1fd9cd45..e497250682 100644
--- a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
diff --git a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByTypeSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByTypeSelectionRule.cs
new file mode 100644
index 0000000000..adade7e849
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByTypeSelectionRule.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions.Common;
+
+namespace FluentAssertions.Equivalency.Selection;
+
+///
+/// Selection rule that removes members of a particular type from the structural comparison.
+///
+internal class ExcludeMemberByTypeSelectionRule : IMemberSelectionRule
+{
+ private readonly Type targetType;
+
+ public ExcludeMemberByTypeSelectionRule(Type targetType)
+ {
+ Guard.ThrowIfArgumentIsNull(targetType);
+ this.targetType = targetType;
+ }
+
+ public bool IncludesMembers => false;
+
+ public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
+ MemberSelectionContext context)
+ {
+ return selectedMembers.Where(p => !ShouldExclude(p.Type)).ToArray();
+ }
+
+ private bool ShouldExclude(Type memberType)
+ {
+ if (targetType.IsGenericTypeDefinition)
+ {
+ // Check if memberType derives from the target open generic type
+ // e.g., class Derived : OpenGeneric or class Derived : OpenGeneric
+ Type baseType = memberType;
+ do
+ {
+ if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == targetType)
+ {
+ return true;
+ }
+
+ baseType = baseType.BaseType;
+ }
+ while (baseType is not null);
+
+ return false;
+ }
+
+ return targetType.IsAssignableFrom(memberType);
+ }
+
+ ///
+ /// 2
+ public override string ToString()
+ {
+ if (targetType.IsGenericTypeDefinition)
+ {
+ return $"Exclude members whose type derives from/is a closed generic type of {targetType}";
+ }
+
+ if (targetType.IsSealed)
+ {
+ return $"Exclude members of type {targetType}";
+ }
+
+ return $"Exclude members whose type is/derives from {targetType}";
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs
index 79ffb0df6c..9828300d5b 100644
--- a/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
namespace FluentAssertions.Equivalency.Selection;
diff --git a/Src/FluentAssertions/Equivalency/Selection/IPathBasedSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IPathBasedSelectionRule.cs
new file mode 100644
index 0000000000..f7ded83c08
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Selection/IPathBasedSelectionRule.cs
@@ -0,0 +1,16 @@
+namespace FluentAssertions.Equivalency.Selection;
+
+///
+/// Represents a selection rule whose effect is determined by a configured member path.
+/// This allows callers to ask whether the rule targets any members within the subtree rooted at
+/// a given , even when the rule is wrapped by decorators such as the
+/// collection-member options decorator.
+///
+internal interface IPathBasedSelectionRule : IMemberSelectionRule
+{
+ ///
+ /// Returns true when this rule targets at least one member of
+ /// or one of its descendants.
+ ///
+ bool SelectsMembersOf(INode currentNode);
+}
diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
index 4ed440b5a5..79775410b3 100644
--- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
@@ -8,17 +8,12 @@ namespace FluentAssertions.Equivalency.Selection;
///
/// Selection rule that includes a particular property in the structural comparison.
///
-internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
+internal class IncludeMemberByPathSelectionRule(MemberPath pathToInclude) : SelectMemberByPathSelectionRule
{
- private readonly MemberPath memberToInclude;
-
- public IncludeMemberByPathSelectionRule(MemberPath pathToInclude)
- {
- memberToInclude = pathToInclude;
- }
-
public override bool IncludesMembers => true;
+ protected override MemberPath MemberPath => pathToInclude;
+
protected override void AddOrRemoveMembersFrom(List selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
{
@@ -26,7 +21,7 @@ protected override void AddOrRemoveMembersFrom(List selectedMembers, IN
{
var memberPath = new MemberPath(context.Type, memberInfo.DeclaringType, parentPath.Combine(memberInfo.Name));
- if (memberToInclude.IsSameAs(memberPath) || memberToInclude.IsParentOrChildOf(memberPath))
+ if (pathToInclude.IsSameAs(memberPath) || pathToInclude.IsParentOrChildOf(memberPath))
{
selectedMembers.Add(MemberFactory.Create(memberInfo, parent));
}
@@ -35,6 +30,6 @@ protected override void AddOrRemoveMembersFrom(List selectedMembers, IN
public override string ToString()
{
- return "Include member root." + memberToInclude;
+ return "Include member root." + pathToInclude;
}
}
diff --git a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
index ac37b31834..618dcd70c5 100644
--- a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
@@ -1,36 +1,81 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
+using FluentAssertions.Common;
namespace FluentAssertions.Equivalency.Selection;
-internal abstract class SelectMemberByPathSelectionRule : IMemberSelectionRule
+internal abstract class SelectMemberByPathSelectionRule : IPathBasedSelectionRule
{
+ private static readonly Regex LeadingCollectionIndexRegex = new(@"^\[[0-9]+]\.?");
+
public virtual bool IncludesMembers => false;
+ protected abstract MemberPath MemberPath { get; }
+
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
MemberSelectionContext context)
{
- var currentPath = RemoveRootIndexQualifier(currentNode.Expectation.PathAndName);
var members = selectedMembers.ToList();
- AddOrRemoveMembersFrom(members, currentNode, currentPath, context);
+ AddOrRemoveMembersFrom(members, currentNode, GetPathRelativeToSelectionRoot(currentNode), context);
return members;
}
+ ///
+ /// Returns true if this rule would select members within the subtree rooted at
+ /// , meaning the rule path targets any member at or below the current node.
+ ///
+ public bool SelectsMembersOf(INode currentNode)
+ {
+ if (currentNode.IsRoot)
+ {
+ return !MemberPath.ToString().IsNullOrEmpty();
+ }
+
+ string currentPath = GetPathRelativeToSelectionRoot(currentNode);
+
+ if (string.IsNullOrEmpty(currentPath))
+ {
+ return !MemberPath.ToString().IsNullOrEmpty();
+ }
+
+ // Compare normalized path segments rather than raw strings so collection rules like "Items[].Name"
+ // still match the concrete node path "Items[0].Name", and so paths built through
+ // MemberPath.AsParentCollectionOf are interpreted consistently.
+ return new MemberPath(currentPath).IsParentOf(MemberPath);
+ }
+
protected abstract void AddOrRemoveMembersFrom(List selectedMembers,
INode parent, string parentPath,
MemberSelectionContext context);
- private static string RemoveRootIndexQualifier(string path)
+ ///
+ /// Returns the path used for member-selection matching at .
+ /// For items inside a root collection, Fluent Assertions exposes paths such as [0].Name, but
+ /// selection rules are expressed relative to the collection item type (for example Name).
+ /// This method removes that root-item index so both sides use the same coordinate system.
+ ///
+ private static string GetPathRelativeToSelectionRoot(INode currentNode)
{
- Match match = new Regex(@"^\[[0-9]+]").Match(path);
-
- if (match.Success)
+ if (currentNode.IsRoot)
{
- path = path.Substring(match.Length);
+ return string.Empty;
}
- return path;
+ string expectationPath = currentNode.Expectation.PathAndName;
+ return RemoveLeadingCollectionIndex(expectationPath);
+ }
+
+ ///
+ /// Removes only the leading root-collection index from , such as turning
+ /// [0] into an empty path and [0].Name into Name.
+ /// Nested collection indices are intentionally left in place; they are handled later by
+ /// comparison, which treats [] and concrete indices as equivalent.
+ ///
+ private static string RemoveLeadingCollectionIndex(string path)
+ {
+ Match match = LeadingCollectionIndexRegex.Match(path);
+ return match.Success ? path.Substring(match.Length) : path;
}
}
diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs
index 14fea3f009..00f6348f14 100644
--- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs
+++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs
@@ -21,6 +21,7 @@ namespace FluentAssertions.Equivalency;
///
/// Represents the run-time behavior of a structural equivalency assertion.
///
+[StackTraceHidden]
public abstract class SelfReferenceEquivalencyOptions : IEquivalencyOptions, IContainTypingRules
where TSelf : SelfReferenceEquivalencyOptions
{
@@ -92,12 +93,15 @@ protected SelfReferenceEquivalencyOptions(IEquivalencyOptions defaults)
includedFields = defaults.IncludedFields;
ignoreNonBrowsableOnSubject = defaults.IgnoreNonBrowsableOnSubject;
excludeNonBrowsableOnExpectation = defaults.ExcludeNonBrowsableOnExpectation;
+ EnableFullDump = defaults.EnableFullDump;
IgnoreLeadingWhitespace = defaults.IgnoreLeadingWhitespace;
IgnoreTrailingWhitespace = defaults.IgnoreTrailingWhitespace;
IgnoreCase = defaults.IgnoreCase;
IgnoreNewlineStyle = defaults.IgnoreNewlineStyle;
IncludeFullStringsInDifference = defaults.IncludeFullStringsInDifference;
IgnoreJsonPropertyCasing = defaults.IgnoreJsonPropertyCasing;
+ TreatNullCollectionsAsEmpty = defaults.TreatNullCollectionsAsEmpty;
+ TreatNullStringsAsEmpty = defaults.TreatNullStringsAsEmpty;
ConversionSelector = defaults.ConversionSelector.Clone();
@@ -211,6 +215,13 @@ EqualityStrategy IEquivalencyOptions.GetEqualityStrategy(Type type)
public bool IgnoreJsonPropertyCasing { get; set; }
+ ///
+ public bool EnableFullDump { get; private set; }
+
+ public bool TreatNullCollectionsAsEmpty { get; private set; }
+
+ public bool TreatNullStringsAsEmpty { get; private set; }
+
public ITraceWriter TraceWriter { get; private set; }
///
@@ -408,6 +419,58 @@ public TSelf Excluding(Expression> predicate)
return (TSelf)this;
}
+ ///
+ /// Excludes all members whose type is or derives from from the structural equality check.
+ ///
+ ///
+ /// The type of members to exclude. All members whose type is or derives from this type will be excluded.
+ /// For sealed types, only exact type matches are excluded. For open generic types, use the overload.
+ ///
+ ///
+ ///
+ /// For non-sealed types, this excludes members of the specified type and all derived types.
+ /// For example, Excluding<BaseClass>() will exclude members of type BaseClass and DerivedClass.
+ ///
+ ///
+ /// For sealed types (like ), only exact type matches are excluded.
+ ///
+ ///
+ public TSelf Excluding()
+ {
+ return Excluding(typeof(TMember));
+ }
+
+ ///
+ /// Excludes all members whose type is or derives from the specified from the structural equality check.
+ ///
+ ///
+ /// The type of members to exclude. All members whose type is or derives from this type will be excluded.
+ /// For sealed types, only exact type matches are excluded. For open generic types (e.g., typeof(Nullable<>)),
+ /// all closed generics and types deriving from them will be excluded.
+ ///
+ /// is .
+ ///
+ ///
+ /// For non-sealed types, this excludes members of the specified type and all derived types.
+ /// For example, Excluding(typeof(BaseClass)) will exclude members of type BaseClass and DerivedClass.
+ ///
+ ///
+ /// For sealed types (like ), only exact type matches are excluded.
+ ///
+ ///
+ /// For open generic types, this excludes all closed generics and types deriving from them. For example,
+ /// Excluding(typeof(Nullable<>)) excludes all nullable value types like int?, double?, etc.
+ /// Similarly, Excluding(typeof(List<>)) excludes List<int>, List<string>,
+ /// and any types deriving from those closed generics.
+ ///
+ ///
+ public TSelf Excluding(Type type)
+ {
+ Guard.ThrowIfArgumentIsNull(type);
+ AddSelectionRule(new ExcludeMemberByTypeSelectionRule(type));
+ return (TSelf)this;
+ }
+
///
/// Includes the specified member in the equality check.
///
@@ -757,6 +820,15 @@ public TSelf WithTracing(ITraceWriter writer = null)
return (TSelf)this;
}
+ ///
+ /// Configures the equivalency options to include the full representation of the subject in assertion failure messages.
+ ///
+ public TSelf WithFullDump()
+ {
+ EnableFullDump = true;
+ return (TSelf)this;
+ }
+
///
/// Instructs the equivalency comparison to try to convert the values of
/// matching properties before running any of the other steps.
@@ -838,6 +910,26 @@ public TSelf IncludingFullStringsInDifference()
return (TSelf)this;
}
+ ///
+ /// Instructs the comparison to treat null collections as equivalent to empty collections,
+ /// regardless of whether the subject or the expectation is null.
+ ///
+ public TSelf ComparingNullCollectionsAsEmpty()
+ {
+ TreatNullCollectionsAsEmpty = true;
+ return (TSelf)this;
+ }
+
+ ///
+ /// Instructs the comparison to treat null strings as equivalent to empty strings,
+ /// regardless of whether the subject or the expectation is null.
+ ///
+ public TSelf ComparingNullStringsAsEmpty()
+ {
+ TreatNullStringsAsEmpty = true;
+ return (TSelf)this;
+ }
+
#if NET6_0_OR_GREATER
///
/// Tells the comparison to ignore the casing when trying to match a property to a JSON property.
@@ -851,7 +943,7 @@ public TSelf IgnoringJsonPropertyCasing()
///
/// Returns the comparer for strings, which is either an explicitly specified comparer via
- /// or an ordinal
+ /// or an ordinal
/// comparer depending on .
///
internal IEqualityComparer GetStringComparerOrDefault()
@@ -909,30 +1001,30 @@ public override string ToString()
foreach (IMemberSelectionRule rule in selectionRules)
{
- builder.Append("- ").AppendLine(rule.ToString());
+ builder.Append("- ").Append(rule).AppendLine();
}
foreach (ITypingRule rule in typingRules)
{
- builder.Append("- ").AppendLine(rule.ToString());
+ builder.Append("- ").Append(rule).AppendLine();
}
foreach (IMemberMatchingRule rule in matchingRules)
{
- builder.Append("- ").AppendLine(rule.ToString());
+ builder.Append("- ").Append(rule).AppendLine();
}
foreach (IEquivalencyStep step in userEquivalencySteps)
{
- builder.Append("- ").AppendLine(step.ToString());
+ builder.Append("- ").Append(step).AppendLine();
}
foreach (IOrderingRule rule in OrderingRules)
{
- builder.Append("- ").AppendLine(rule.ToString());
+ builder.Append("- ").Append(rule).AppendLine();
}
- builder.Append("- ").AppendLine(ConversionSelector.ToString());
+ builder.Append("- ").Append(ConversionSelector).AppendLine();
return builder.ToString();
}
@@ -1014,3 +1106,4 @@ private TSelf AddEquivalencyStep(IEquivalencyStep equivalencyStep)
#endregion
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs b/Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs
index f8a9c02f33..4af7e61781 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
internal sealed class AssertionContext : IAssertionContext
{
private AssertionContext(INode currentNode, TSubject subject, TSubject expectation,
@@ -34,3 +35,4 @@ internal static AssertionContext CreateFrom(Comparands comparands, IEq
context.Reason.Arguments);
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs b/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
index c636079e68..17e5ddda05 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
@@ -6,8 +6,9 @@
namespace FluentAssertions.Equivalency.Steps;
///
-/// Represents a collection of assertion results obtained through a .
+/// Represents a collection of assertion results obtained through an .
///
+[System.Diagnostics.StackTraceHidden]
internal class AssertionResultSet
{
private readonly Dictionary
public bool ContainsSuccessfulSet() => set.Values.Any(v => v.Length == 0);
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
index cb6d2fcc03..31f1b83743 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
@@ -1,4 +1,5 @@
using System;
+
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Execution;
@@ -6,6 +7,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class AssertionRuleEquivalencyStep : IEquivalencyStep
{
private readonly Func predicate;
@@ -115,3 +117,4 @@ public override string ToString()
return "Invoke Action<" + typeof(TSubject).Name + "> when " + description;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs b/Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs
index 9d45787682..0cb2d09a4e 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using FluentAssertions.Common;
-using static System.FormattableString;
namespace FluentAssertions.Equivalency.Steps;
@@ -11,6 +10,7 @@ namespace FluentAssertions.Equivalency.Steps;
///
/// Whether or not the conversion is attempted depends on the .
///
+[System.Diagnostics.StackTraceHidden]
public class AutoConversionStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -37,14 +37,15 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
if (TryChangeType(comparands.Subject, expectationType, out object convertedSubject))
{
context.Tracer.WriteLine(member =>
- Invariant($"Converted subject {comparands.Subject} at {member.Subject} to {expectationType}"));
+ string.Create(CultureInfo.InvariantCulture, $"Converted subject {comparands.Subject} at {member.Subject} to {expectationType}"));
comparands.Subject = convertedSubject;
}
else
{
context.Tracer.WriteLine(member =>
- Invariant($"Subject {comparands.Subject} at {member.Subject} could not be converted to {expectationType}"));
+ string.Create(CultureInfo.InvariantCulture,
+ $"Subject {comparands.Subject} at {member.Subject} could not be converted to {expectationType}"));
}
return EquivalencyResult.ContinueWithNext;
@@ -85,3 +86,4 @@ public override string ToString()
return string.Empty;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs
index 6b04dbb728..b694f9a916 100644
--- a/Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs
@@ -10,6 +10,7 @@ namespace FluentAssertions.Equivalency.Steps;
/// Specific equivalency step for handling date and time types where the types are different and the failure message
/// should make that clear.
///
+[System.Diagnostics.StackTraceHidden]
public class DateAndTimeEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -51,3 +52,4 @@ private static bool IsOfDateOrTimeType(Type type) =>
}
#pragma warning restore AV1000
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs
index 67db86940f..f53a550a04 100644
--- a/Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs
@@ -1,11 +1,15 @@
+#if !NET6_0_OR_GREATER
+using System;
+#endif
using System.Collections;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using FluentAssertions.Common;
using FluentAssertions.Execution;
-using static System.FormattableString;
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class DictionaryEquivalencyStep : EquivalencyStep
{
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
@@ -18,21 +22,23 @@ protected override EquivalencyResult OnHandle(Comparands comparands,
var assertionChain = AssertionChain.GetOrCreate().For(context);
- if (PreconditionsAreMet(expectation, subject, assertionChain) && expectation is not null)
+ if (PreconditionsAreMet(expectation, subject, assertionChain))
{
foreach (object key in expectation.Keys)
{
if (context.Options.IsRecursive)
{
context.Tracer.WriteLine(member =>
- Invariant($"Recursing into dictionary item {key} at {member.Expectation}"));
+ string.Create(CultureInfo.InvariantCulture,
+ $"Recursing into dictionary item {key} at {member.Expectation}"));
- nestedValidator.AssertEquivalencyOf(new Comparands(subject[key], expectation[key], typeof(object)), context.AsDictionaryItem(key));
+ nestedValidator.AssertEquivalencyOf(new Comparands(subject[key], expectation[key], typeof(object)),
+ context.AsDictionaryItem(key));
}
else
{
context.Tracer.WriteLine(member =>
- Invariant(
+ string.Create(CultureInfo.InvariantCulture,
$"Comparing dictionary item {key} at {member.Expectation} between subject and expectation"));
assertionChain.WithCallerPostfix($"[{key.ToFormattedString()}]").ReuseOnce();
@@ -46,21 +52,21 @@ protected override EquivalencyResult OnHandle(Comparands comparands,
private static bool PreconditionsAreMet(IDictionary expectation, IDictionary subject, AssertionChain assertionChain)
{
- return AssertIsDictionary(subject, assertionChain)
- && AssertEitherIsNotNull(expectation, subject, assertionChain)
- && AssertSameLength(expectation, subject, assertionChain);
+ return AssertSubjectIsNotNull(subject, assertionChain)
+ && AssertExpectationIsNotNull(expectation, subject, assertionChain)
+ && AssertSameLength(expectation, subject, assertionChain);
}
- private static bool AssertEitherIsNotNull(IDictionary expectation, IDictionary subject, AssertionChain assertionChain)
+ private static bool AssertExpectationIsNotNull(IDictionary expectation, IDictionary subject, AssertionChain assertionChain)
{
assertionChain
- .ForCondition((expectation is null && subject is null) || expectation is not null)
+ .ForCondition(expectation is not null)
.FailWith("Expected {context:subject} to be {0}{reason}, but found {1}.", null, subject);
return assertionChain.Succeeded;
}
- private static bool AssertIsDictionary(IDictionary subject, AssertionChain assertionChain)
+ private static bool AssertSubjectIsNotNull(IDictionary subject, AssertionChain assertionChain)
{
assertionChain
.ForCondition(subject is not null)
@@ -72,9 +78,9 @@ private static bool AssertIsDictionary(IDictionary subject, AssertionChain asser
private static bool AssertSameLength(IDictionary expectation, IDictionary subject, AssertionChain assertionChain)
{
assertionChain
- .ForCondition(expectation is null || subject.Keys.Count == expectation.Keys.Count)
+ .ForCondition(subject.Keys.Count == expectation.Keys.Count)
.FailWith("Expected {context:subject} to be a dictionary with {0} item(s), but it only contains {1} item(s).",
- expectation?.Keys.Count, subject?.Keys.Count);
+ expectation.Keys.Count, subject.Keys.Count);
return assertionChain.Succeeded;
}
diff --git a/Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs b/Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs
index e0ca76da14..8e253e604b 100644
--- a/Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -10,6 +10,7 @@ namespace FluentAssertions.Equivalency.Steps;
///
/// Provides Reflection-backed meta-data information about a type implementing the interface.
///
+[System.Diagnostics.StackTraceHidden]
internal sealed class DictionaryInterfaceInfo
{
// ReSharper disable once PossibleNullReferenceException
@@ -135,3 +136,4 @@ private static Dictionary ConvertToDictionaryInternal $"IDictionary<{Key}, {Value}>";
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs b/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
index 0cf246e82d..18a0b80d5d 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
@@ -8,6 +8,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class EnumEqualityStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -39,21 +40,21 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
switch (context.Options.EnumEquivalencyHandling)
{
case EnumEquivalencyHandling.ByValue:
- {
- HandleByValue(assertionChain, comparands, context.Reason);
- break;
- }
+ {
+ HandleByValue(assertionChain, comparands, context.Reason);
+ break;
+ }
case EnumEquivalencyHandling.ByName:
- {
- HandleByName(assertionChain, comparands, context.Reason);
- break;
- }
+ {
+ HandleByName(assertionChain, comparands, context.Reason);
+ break;
+ }
default:
- {
- throw new InvalidOperationException($"Do not know how to handle {context.Options.EnumEquivalencyHandling}");
- }
+ {
+ throw new InvalidOperationException($"Do not know how to handle {context.Options.EnumEquivalencyHandling}");
+ }
}
}
@@ -119,3 +120,4 @@ private static string GetDisplayNameForEnumComparison(object o, decimal? v)
return o is not null ? Convert.ToDecimal(o, CultureInfo.InvariantCulture) : null;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
index 1b6906ba58..f88ac4078a 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
@@ -1,10 +1,12 @@
using System;
+
using System.Collections;
using System.Linq;
using FluentAssertions.Execution;
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class EnumerableEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -17,7 +19,13 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
var assertionChain = AssertionChain.GetOrCreate().For(context);
- if (AssertSubjectIsCollection(assertionChain, comparands.Subject))
+ bool treatNullAsEmpty = context.Options.TreatNullCollectionsAsEmpty;
+
+ bool subjectIsUsable = treatNullAsEmpty
+ ? comparands.Subject is null || AssertSubjectIsCollection(assertionChain, comparands.Subject)
+ : AssertSubjectIsCollection(assertionChain, comparands.Subject);
+
+ if (subjectIsUsable)
{
var validator = new EnumerableEquivalencyValidator(assertionChain, valueChildNodes, context)
{
@@ -25,7 +33,9 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
OrderingRules = context.Options.OrderingRules
};
- validator.Execute(ToArray(comparands.Subject), ToArray(comparands.Expectation));
+ validator.Execute(
+ treatNullAsEmpty && comparands.Subject is null ? [] : ToArray(comparands.Subject),
+ treatNullAsEmpty && comparands.Expectation is null ? [] : ToArray(comparands.Expectation));
}
return EquivalencyResult.EquivalencyProven;
@@ -77,3 +87,4 @@ private static bool IsIgnorableArrayLikeType(object value)
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ArraySegment<>));
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs
index 11e01ebb00..8e11be3ed3 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs
@@ -1,216 +1,160 @@
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using FluentAssertions.Equivalency.Execution;
using FluentAssertions.Equivalency.Tracing;
using FluentAssertions.Execution;
-using static System.FormattableString;
+using FluentAssertions.Formatting;
+using static FluentAssertions.Common.StringExtensions;
namespace FluentAssertions.Equivalency.Steps;
///
/// Executes a single equivalency assertion on two collections, optionally recursive and with or without strict ordering.
///
-internal class EnumerableEquivalencyValidator
+[System.Diagnostics.StackTraceHidden]
+internal class EnumerableEquivalencyValidator(
+ AssertionChain assertionChain,
+ IValidateChildNodeEquivalency parent,
+ IEquivalencyValidationContext context)
{
- private const int FailedItemsFastFailThreshold = 10;
+ private readonly Tracer tracer = context.Tracer;
- #region Private Definitions
+ public required bool Recursive { get; init; }
- private readonly AssertionChain assertionChain;
- private readonly IValidateChildNodeEquivalency parent;
- private readonly IEquivalencyValidationContext context;
+ public required OrderingRuleCollection OrderingRules { get; init; }
- #endregion
-
- public EnumerableEquivalencyValidator(AssertionChain assertionChain, IValidateChildNodeEquivalency parent,
- IEquivalencyValidationContext context)
- {
- this.assertionChain = assertionChain;
- this.parent = parent;
- this.context = context;
- Recursive = false;
- }
-
- public bool Recursive { get; init; }
-
- public OrderingRuleCollection OrderingRules { get; init; }
-
- public void Execute(object[] subject, T[] expectation)
+ public void Execute(object[] subjects, T[] expectations)
{
- if (AssertIsNotNull(expectation, subject) && AssertCollectionsHaveSameCount(subject, expectation))
+ if (AssertIsNotNull(expectations, subjects))
{
if (Recursive)
{
- using var _ = context.Tracer.WriteBlock(member =>
- Invariant($"Structurally comparing {subject} and expectation {expectation} at {member.Expectation}"));
+ using var _ = tracer.WriteBlock(member =>
+ Invariant($"Structurally comparing {subjects} and expectation {expectations} at {member.Expectation}"));
- AssertElementGraphEquivalency(subject, expectation, context.CurrentNode);
+ ExecuteRecursiveAssertion(subjects, expectations);
}
else
{
- using var _ = context.Tracer.WriteBlock(member =>
+ using var _ = tracer.WriteBlock(member =>
Invariant(
- $"Comparing subject {subject} and expectation {expectation} at {member.Expectation} using simple value equality"));
+ $"Comparing subject {subjects} and expectation {expectations} at {member.Expectation} using simple value equality"));
- subject.Should().BeEquivalentTo(expectation);
+ subjects.Should().BeEquivalentTo(expectations);
}
}
}
- private bool AssertIsNotNull(object expectation, object[] subject)
+ private bool AssertIsNotNull(T[] expectations, object[] subjects)
{
assertionChain
- .ForCondition(expectation is not null)
- .FailWith("Expected {context:subject} to be , but found {0}.", [subject]);
+ .ForCondition(expectations is not null)
+ .FailWith("Expected {context:subject} to be , but found {0}.", [subjects]);
return assertionChain.Succeeded;
}
- private bool AssertCollectionsHaveSameCount(ICollection subject, ICollection expectation)
+ private void ExecuteRecursiveAssertion(object[] subjects, T[] expectation)
{
- assertionChain
- .AssertEitherCollectionIsNotEmpty(subject, expectation)
- .Then
- .AssertCollectionHasEnoughItems(subject, expectation)
- .Then
- .AssertCollectionHasNotTooManyItems(subject, expectation);
+ List> remainingSubjects = subjects
+ .Select((item, index) => new IndexedItem(item, index))
+ .ToList();
- return assertionChain.Succeeded;
- }
-
- private void AssertElementGraphEquivalency(object[] subjects, T[] expectations, INode currentNode)
- {
- unmatchedSubjectIndexes = Enumerable.Range(0, subjects.Length).ToList();
+ List remainingExpectations = new(expectation);
- if (OrderingRules.IsOrderingStrictFor(new ObjectInfo(new Comparands(subjects, expectations, typeof(T[])), currentNode)))
+ bool isOrderingStrict = IsOrderingStrictFor(subjects, remainingExpectations, context.CurrentNode);
+ if (isOrderingStrict)
{
- AssertElementGraphEquivalencyWithStrictOrdering(subjects, expectations);
+ new StrictlyOrderedEquivalencyStrategy(parent, context)
+ .FindAndRemoveMatches(remainingSubjects, remainingExpectations);
}
else
{
- AssertElementGraphEquivalencyWithLooseOrdering(subjects, expectations);
+ new LooselyOrderedEquivalencyStrategy(assertionChain, parent, context)
+ .FindAndRemoveMatches(remainingSubjects, remainingExpectations);
}
+
+ ReportRemainingOrMissingItems(remainingSubjects, remainingExpectations, subjects, expectation.Length,
+ isOrderingStrict ? "in order" : "in any order");
}
- private void AssertElementGraphEquivalencyWithStrictOrdering(object[] subjects, T[] expectations)
+ private bool IsOrderingStrictFor(object[] subjects, List expectations, INode currentNode)
{
- int failedCount = 0;
+ return OrderingRules.IsOrderingStrictFor(new ObjectInfo(new Comparands(subjects, expectations, typeof(T[])),
+ currentNode));
+ }
+
+#pragma warning disable CA1305
- foreach (int index in Enumerable.Range(0, expectations.Length))
+ private void ReportRemainingOrMissingItems(List> remainingSubjects, List remainingExpectations,
+ object[] allSubjects,
+ int expectationLength, string orderingDescription)
+ {
+ if (remainingSubjects.Count > 0 || remainingExpectations.Count > 0)
{
- T expectation = expectations[index];
+ StringBuilder message = new();
+ string phrase = expectationLength switch
+ {
+ 0 => "be an empty collection",
+ 1 => "contain exactly one item",
+ _ => $"contain exactly {expectationLength} items {orderingDescription}"
+ };
- using var _ = context.Tracer.WriteBlock(member =>
- Invariant(
- $"Strictly comparing expectation {expectation} at {member.Expectation} to item with index {index} in {subjects}"));
+ message.Append(
+ $"Expected {{context:collection}} to {phrase}{{reason}}, but ");
- bool succeeded = StrictlyMatchAgainst(subjects, expectation, index);
- if (!succeeded)
+ if (remainingExpectations.Count > 0)
{
- failedCount++;
- if (failedCount >= FailedItemsFastFailThreshold)
+ message.Append("it misses {0}");
+ if (remainingSubjects.Count > 0)
{
- context.Tracer.WriteLine(member =>
- $"Aborting strict order comparison of collections after {FailedItemsFastFailThreshold} items failed at {member.Expectation}");
-
- break;
+ message.Append(" and ");
}
}
- }
- }
-
- private void AssertElementGraphEquivalencyWithLooseOrdering(object[] subjects, T[] expectations)
- {
- int failedCount = 0;
-
- foreach (int index in Enumerable.Range(0, expectations.Length))
- {
- T expectation = expectations[index];
-
- using var _ = context.Tracer.WriteBlock(member =>
- Invariant(
- $"Finding the best match of {expectation} within all items in {subjects} at {member.Expectation}[{index}]"));
- bool succeeded = LooselyMatchAgainst(subjects, expectation, index);
-
- if (!succeeded)
+ if (remainingSubjects.Count > 0)
{
- failedCount++;
-
- if (failedCount >= FailedItemsFastFailThreshold)
+ if (remainingExpectations.Count == 0)
{
- context.Tracer.WriteLine(member =>
- $"Fail failing loose order comparison of collection after {FailedItemsFastFailThreshold} items failed at {member.Expectation}");
-
- break;
+ // Subjects are truly extra (the subject collection has more items than expected).
+ // Show each extraneous item with its original index to help pinpoint the divergence.
+ if (remainingSubjects.Count == 1)
+ {
+ message.Append($"found one extraneous item at index {remainingSubjects[0].Index}: {{1}}");
+ }
+ else
+ {
+ string formattedItems = string.Join(", ",
+ remainingSubjects.Select(s =>
+ $"{Formatter.ToString(s.Item).EscapePlaceholders()} (at index {s.Index})"));
+
+ message.Append("found extraneous items ");
+ message.Append(formattedItems);
+ }
+ }
+ else
+ {
+ // Both missing and extra subjects exist (same-size collections with pairwise mismatches).
+ // Fall back to the compact list format without per-item indices.
+ phrase = remainingSubjects.Count > 1 ? "extraneous items" : "one extraneous item";
+ message.Append($"found {phrase} {{1}}");
}
}
- }
- }
-
- private List unmatchedSubjectIndexes;
-
- private bool LooselyMatchAgainst(IList subjects, T expectation, int expectationIndex)
- {
- var results = new AssertionResultSet();
- int index = 0;
-
- GetTraceMessage getMessage = member =>
- $"Comparing subject at {member.Subject}[{index}] with the expectation at {member.Expectation}[{expectationIndex}]";
-
- int indexToBeRemoved = -1;
- for (var metaIndex = 0; metaIndex < unmatchedSubjectIndexes.Count; metaIndex++)
- {
- index = unmatchedSubjectIndexes[metaIndex];
- object subject = subjects[index];
-
- using var _ = context.Tracer.WriteBlock(getMessage);
- string[] failures = TryToMatch(subject, expectation, expectationIndex);
-
- results.AddSet(index, failures);
-
- if (results.ContainsSuccessfulSet())
+ if (context.Options.EnableFullDump)
{
- context.Tracer.WriteLine(_ => "It's a match");
- indexToBeRemoved = metaIndex;
- break;
+ message.AppendLine();
+ message.AppendLine();
+ message.AppendLine("Full dump of {context:subject}: {2}");
}
- context.Tracer.WriteLine(_ => $"Contained {failures.Length} failures");
- }
-
- if (indexToBeRemoved != -1)
- {
- unmatchedSubjectIndexes.RemoveAt(indexToBeRemoved);
- }
+ object subjectsArg = remainingSubjects.Count == 1
+ ? remainingSubjects[0].Item
+ : remainingSubjects.Select(s => s.Item).ToList();
- foreach (string failure in results.GetTheFailuresForTheSetWithTheFewestFailures(expectationIndex))
- {
- assertionChain.AddPreFormattedFailure(failure);
+ assertionChain.FailWith(message.ToString(), remainingExpectations, subjectsArg, allSubjects);
}
-
- return indexToBeRemoved != -1;
- }
-
- private string[] TryToMatch(object subject, T expectation, int expectationIndex)
- {
- using var scope = new AssertionScope();
-
- parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(T)), context.AsCollectionItem(expectationIndex));
-
- return scope.Discard();
- }
-
- private bool StrictlyMatchAgainst(object[] subjects, T expectation, int expectationIndex)
- {
- using var scope = new AssertionScope();
- object subject = subjects[expectationIndex];
- IEquivalencyValidationContext equivalencyValidationContext = context.AsCollectionItem(expectationIndex);
-
- parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(T)), equivalencyValidationContext);
-
- bool failed = scope.HasFailures();
- return !failed;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidatorExtensions.cs b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidatorExtensions.cs
deleted file mode 100644
index f3f3d00e21..0000000000
--- a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidatorExtensions.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Collections.Generic;
-using FluentAssertions.Execution;
-
-namespace FluentAssertions.Equivalency.Steps;
-
-internal static class EnumerableEquivalencyValidatorExtensions
-{
- public static Continuation AssertEitherCollectionIsNotEmpty(this AssertionChain assertionChain,
- ICollection subject,
- ICollection expectation)
- {
- return assertionChain
- .WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count,
- chain => chain
- .ForCondition(subject.Count > 0 || expectation.Count == 0)
- .FailWith(", but found an empty collection.")
- .Then
- .ForCondition(subject.Count == 0 || expectation.Count > 0)
- .FailWith($", but {{0}}{Environment.NewLine}contains {{1}} item(s).",
- subject,
- subject.Count));
- }
-
- public static Continuation AssertCollectionHasEnoughItems(this AssertionChain assertionChain, ICollection subject,
- ICollection expectation)
- {
- return assertionChain
- .WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count,
- chain => chain
- .ForCondition(subject.Count >= expectation.Count)
- .FailWith($", but {{0}}{Environment.NewLine}contains {{1}} item(s) less than{Environment.NewLine}{{2}}.",
- subject,
- expectation.Count - subject.Count,
- expectation));
- }
-
- public static Continuation AssertCollectionHasNotTooManyItems(this AssertionChain assertionChain,
- ICollection subject,
- ICollection expectation)
- {
- return assertionChain
- .WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count,
- chain => chain
- .ForCondition(subject.Count <= expectation.Count)
- .FailWith($", but {{0}}{Environment.NewLine}contains {{1}} item(s) more than{Environment.NewLine}{{2}}.",
- subject,
- subject.Count - expectation.Count,
- expectation));
- }
-}
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumerableExtensions.cs b/Src/FluentAssertions/Equivalency/Steps/EnumerableExtensions.cs
new file mode 100644
index 0000000000..b2faeb9339
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumerableExtensions.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+[System.Diagnostics.StackTraceHidden]
+internal static class EnumerableExtensions
+{
+ ///
+ /// Converts an enumerable collection of into an .
+ ///
+ public static IndexedItemCollection ToIndexedList(this IEnumerable> source) => new(source);
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
index 6bd954d16b..48febbfd49 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
@@ -1,16 +1,19 @@
-using System;
using System.Collections.Generic;
+
+using FluentAssertions.Common;
using FluentAssertions.Execution;
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class EqualityComparerEquivalencyStep : IEquivalencyStep
{
private readonly IEqualityComparer comparer;
public EqualityComparerEquivalencyStep(IEqualityComparer comparer)
{
- this.comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
+ Guard.ThrowIfArgumentIsNull(comparer);
+ this.comparer = comparer;
}
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -48,3 +51,4 @@ public override string ToString()
return $"Use {comparer} for objects of type {typeof(T)}";
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs b/Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs
index 207044c478..febfd913c4 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs
@@ -1,10 +1,28 @@
-using System.Globalization;
+using System.Globalization;
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
internal static class EquivalencyValidationContextExtensions
{
+ // Pre-cached string representations of common collection indices to avoid repeated allocations.
+ private static readonly string[] CachedIndexStrings = InitializeCachedIndexStrings(1024);
+
+ private static string[] InitializeCachedIndexStrings(int count)
+ {
+ var result = new string[count];
+ for (int i = 0; i < count; i++)
+ {
+ result[i] = i.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return result;
+ }
+
public static IEquivalencyValidationContext AsCollectionItem(this IEquivalencyValidationContext context,
int index) =>
- context.AsCollectionItem(index.ToString(CultureInfo.InvariantCulture));
+ context.AsCollectionItem(index < CachedIndexStrings.Length
+ ? CachedIndexStrings[index]
+ : index.ToString(CultureInfo.InvariantCulture));
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
index 1d398fdcd2..f490d467d9 100644
--- a/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@@ -8,6 +9,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class GenericDictionaryEquivalencyStep : IEquivalencyStep
{
#pragma warning disable SA1110 // Allow opening parenthesis on new line to reduce line length
@@ -225,3 +227,4 @@ public KeyDifference(List missingKeys, List additiona
public List AdditionalKeys { get; }
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs
index f7e4e7297f..83935c1ec4 100644
--- a/Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections;
+
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -8,6 +9,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class GenericEnumerableEquivalencyStep : IEquivalencyStep
{
#pragma warning disable SA1110 // Allow opening parenthesis on new line to reduce line length
@@ -20,7 +22,12 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
{
Type expectedType = comparands.GetExpectedType(context.Options);
- if (comparands.Expectation is null || !IsGenericCollection(expectedType))
+ if (!IsGenericCollection(expectedType))
+ {
+ return EquivalencyResult.ContinueWithNext;
+ }
+
+ if (comparands.Expectation is null && !context.Options.TreatNullCollectionsAsEmpty)
{
return EquivalencyResult.ContinueWithNext;
}
@@ -35,7 +42,7 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
"to use for asserting the equivalency of the collection. ",
interfaceTypes.Select(type => "IEnumerable<" + type.GetGenericArguments().Single() + ">")));
- if (AssertSubjectIsCollection(assertionChain, comparands.Subject))
+ if (AssertSubjectIsCollection(assertionChain, comparands.Subject, context.Options.TreatNullCollectionsAsEmpty))
{
var validator = new EnumerableEquivalencyValidator(assertionChain, valueChildNodes, context)
{
@@ -45,12 +52,13 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
Type typeOfEnumeration = GetTypeOfEnumeration(expectedType);
- var subjectAsArray = EnumerableEquivalencyStep.ToArray(comparands.Subject);
+ var subjectAsArray = comparands.Subject is null ? [] : EnumerableEquivalencyStep.ToArray(comparands.Subject);
+ object expectation = comparands.Expectation ?? Array.CreateInstance(typeOfEnumeration, 0);
try
{
HandleMethod.MakeGenericMethod(typeOfEnumeration)
- .Invoke(null, [validator, subjectAsArray, comparands.Expectation]);
+ .Invoke(null, [validator, subjectAsArray, expectation]);
}
catch (TargetInvocationException e)
{
@@ -64,8 +72,13 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
private static void HandleImpl(EnumerableEquivalencyValidator validator, object[] subject, IEnumerable expectation) =>
validator.Execute(subject, ToArray(expectation));
- private static bool AssertSubjectIsCollection(AssertionChain assertionChain, object subject)
+ private static bool AssertSubjectIsCollection(AssertionChain assertionChain, object subject, bool treatNullAsEmpty)
{
+ if (treatNullAsEmpty && subject is null)
+ {
+ return true;
+ }
+
assertionChain
.ForCondition(subject is not null)
.FailWith("Expected {context:subject} not to be {0}.", new object[] { null });
@@ -125,3 +138,4 @@ private static T[] ToArray(IEnumerable value)
}
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/IndexedItemCollection{T}.cs b/Src/FluentAssertions/Equivalency/Steps/IndexedItemCollection{T}.cs
new file mode 100644
index 0000000000..246611af1d
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/IndexedItemCollection{T}.cs
@@ -0,0 +1,52 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+///
+/// Represents a collection of indexed items of type that implements .
+///
+/// The type of elements in the collection.
+[System.Diagnostics.StackTraceHidden]
+internal class IndexedItemCollection : IReadOnlyList>
+{
+ private readonly List> items = new();
+
+ public IndexedItemCollection(IEnumerable source)
+ {
+ items = source.Select((item, index) => new IndexedItem(item, index)).ToList();
+ }
+
+ public IndexedItemCollection(IEnumerable> source)
+ {
+ items.AddRange(source);
+ }
+
+ ///
+ public IEnumerator> GetEnumerator() => items.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)items).GetEnumerator();
+
+ public int Count => items.Count;
+
+ ///
+ public IndexedItem this[int index] => items[index];
+
+ public void Remove(IndexedItem item)
+ {
+ items.Remove(item);
+ }
+
+ ///
+ /// Removes items from the specified list of expectations that do not have a corresponding match
+ /// in the current collection.
+ ///
+ /// The list of expectations to be filtered by removing unmatched items.
+ public void RemoveMatchedItemFrom(List expectations)
+ {
+ var existingItems = new HashSet(items.Select(x => x.Item));
+ expectations.RemoveAll(expectation => !existingItems.Contains(expectation));
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/IndexedItem{T}.cs b/Src/FluentAssertions/Equivalency/Steps/IndexedItem{T}.cs
new file mode 100644
index 0000000000..4f7b46082b
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/IndexedItem{T}.cs
@@ -0,0 +1,13 @@
+namespace FluentAssertions.Equivalency.Steps;
+
+///
+/// Encapsulates an item of type associated with an index.
+///
+/// The type of the item being indexed.
+[System.Diagnostics.StackTraceHidden]
+internal class IndexedItem(T item, int index)
+{
+ public T Item { get; } = item;
+
+ public int Index { get; } = index;
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs b/Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs
index 8ae2289f0f..e7bd20ec19 100644
--- a/Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs
@@ -6,6 +6,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class JsonConversionStep : IEquivalencyStep
{
///
@@ -63,3 +64,4 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
}
#endif
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/LooselyOrderedEquivalencyStrategy.cs b/Src/FluentAssertions/Equivalency/Steps/LooselyOrderedEquivalencyStrategy.cs
new file mode 100644
index 0000000000..06b37dfbbd
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/LooselyOrderedEquivalencyStrategy.cs
@@ -0,0 +1,326 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using FluentAssertions.Common;
+using FluentAssertions.Equivalency.Execution;
+using FluentAssertions.Equivalency.Tracing;
+using FluentAssertions.Execution;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+[System.Diagnostics.StackTraceHidden]
+internal class LooselyOrderedEquivalencyStrategy(
+ AssertionChain assertionChain,
+ IValidateChildNodeEquivalency parent,
+ IEquivalencyValidationContext context)
+{
+ private const int MaximumFailuresToReport = 10;
+
+ private readonly Tracer tracer = context.Tracer;
+
+ // Populated during Phase 1 + 2 using skip-formatting dry-runs (no string allocation).
+ private Dictionary<(object Subject, object Expectation, int ExpectationIndex), int> countCache = new();
+
+ // Populated lazily during Phase 3 for the ~n selected pairs with full formatting.
+ private Dictionary<(object Subject, object Expectation, int ExpectationIndex), string[]> fullFailuresCache = new();
+
+ public void FindAndRemoveMatches(List> subjects, List expectations)
+ {
+ countCache = new(new ReferentialComparer());
+ fullFailuresCache = new(new ReferentialComparer());
+
+ var expectationsWithIndexes = new IndexedItemCollection(expectations);
+
+ // First find the exact matches and remove them from the collection
+ FindAndRemoveExactMatches(subjects, expectationsWithIndexes);
+
+ // Reorder the remaining expectations based on their similarity to the remaining unmatched subjects.
+ expectationsWithIndexes = SortExpectationsByMinDistance(subjects, expectationsWithIndexes);
+
+ // If there are still unmatched expectations, find the closest matches
+ FindAndRemoveClosestMatches(subjects, expectationsWithIndexes);
+
+ // Remove the items that are no longer needed from the original list of expectations
+ expectationsWithIndexes.RemoveMatchedItemFrom(expectations);
+ }
+
+ private void FindAndRemoveExactMatches(List> subjects, IndexedItemCollection expectationsWithIndexes)
+ {
+ int expectationIndex = 0;
+ while (expectationsWithIndexes.Count > expectationIndex)
+ {
+ IndexedItem expectation = expectationsWithIndexes[expectationIndex];
+ if (StrictlyMatchAgainst(subjects, expectation.Item, expectation.Index))
+ {
+ // It's a full match, so we don't have to look at it again
+ expectationsWithIndexes.Remove(expectation);
+ }
+ else
+ {
+ // Continue with the next item, but we leave the unmatched items.
+ expectationIndex++;
+ }
+ }
+ }
+
+ [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ private bool StrictlyMatchAgainst(List> remainingSubjects, TExpectation expectation, int expectationIndex)
+ {
+ foreach ((int index, IndexedItem subject) in remainingSubjects.Index())
+ {
+ using var _ = tracer.WriteBlock(member =>
+ $"Comparing subject at {member.Subject}[{index}] with the expectation at {member.Expectation}[{expectationIndex}]");
+
+ int failures = TryToMatchCount(expectation, subject.Item, expectationIndex);
+ if (failures == 0)
+ {
+ tracer.WriteLine(_ => "It's a match");
+ remainingSubjects.RemoveAt(index);
+ return true;
+ }
+
+ tracer.WriteLine(_ => $"Contained {failures} failures");
+ }
+
+ return false;
+ }
+
+ ///
+ /// This is an optimization for loosely-ordered collection comparison. By processing expectations
+ /// that are most similar to remaining subjects first, the algorithm is more likely
+ /// to find the correct pairings earlier, leading to better error messages when mismatches occur.
+ ///
+ private IndexedItemCollection SortExpectationsByMinDistance(List> remainingSubjects,
+ IndexedItemCollection expectationsWithIndexes)
+ {
+ if (remainingSubjects.Count > 0)
+ {
+ return expectationsWithIndexes
+ .Select(e => new
+ {
+ Expectation = e,
+ MinDistance = remainingSubjects.Min(a => TryToMatchCount(e.Item, a.Item, e.Index))
+ })
+ .OrderBy(x => x.MinDistance)
+ .Select(x => x.Expectation)
+ .ToIndexedList();
+ }
+ else
+ {
+ return expectationsWithIndexes;
+ }
+ }
+
+ private void FindAndRemoveClosestMatches(List> remainingSubjects,
+ IndexedItemCollection expectationsWithIndexes)
+ {
+ int nrFailures = 0;
+ if (expectationsWithIndexes.Count > 0 && remainingSubjects.Count > 0)
+ {
+ IReadOnlyList<(IndexedItem, IndexedItem, string[])> bestMatches =
+ FindClosestMismatches(remainingSubjects, expectationsWithIndexes);
+
+ foreach (var (expectation, subject, failures) in bestMatches)
+ {
+ foreach (string failure in failures)
+ {
+ if (nrFailures < MaximumFailuresToReport)
+ {
+ assertionChain.AddPreFormattedFailure(failure);
+ nrFailures++;
+ }
+ }
+
+ remainingSubjects.Remove(subject);
+ expectationsWithIndexes.Remove(expectation);
+ }
+ }
+ }
+
+ private IReadOnlyList<(IndexedItem Expectation, IndexedItem Actual, string[] Failures)> FindClosestMismatches(
+ List> remainingSubjects, IndexedItemCollection expectationsWithIndexes)
+ {
+ // For small collections, use exact permutation search to find the globally optimal assignment.
+ // factorial(8) = 40,320 which is well within reason.
+ const int maxSizeForExactSearch = 8;
+
+ return remainingSubjects.Count <= maxSizeForExactSearch ?
+ FindClosestMismatchesByPermutation(remainingSubjects, expectationsWithIndexes) :
+ FindClosestMismatchesByGreedyAssignment(remainingSubjects, expectationsWithIndexes);
+ }
+
+ ///
+ /// Finds the best assignment by exhaustively trying all permutations. Only suitable for small collections.
+ /// Uses failure counts for scoring (no string formatting) and only fetches full failure strings for the
+ /// winning assignment.
+ ///
+ private IReadOnlyList<(IndexedItem Expectation, IndexedItem Actual, string[] Failures)> FindClosestMismatchesByPermutation(
+ List> remainingSubjects, IndexedItemCollection expectationsWithIndexes)
+ {
+ var bestScore = int.MaxValue;
+ IReadOnlyList> bestAssignment = null;
+
+ foreach (IReadOnlyList> assignment in remainingSubjects.Permute())
+ {
+ int score = 0;
+ bool tooHigh = false;
+
+ for (int index = 0; index < expectationsWithIndexes.Count && index < assignment.Count; index++)
+ {
+ IndexedItem expectationWithIndex = expectationsWithIndexes[index];
+ score += TryToMatchCount(expectationWithIndex.Item, assignment[index].Item, expectationWithIndex.Index);
+
+ if (score >= bestScore)
+ {
+ tooHigh = true;
+ break;
+ }
+ }
+
+ if (!tooHigh && score < bestScore)
+ {
+ bestScore = score;
+ bestAssignment = assignment;
+ }
+ }
+
+ if (bestAssignment is null)
+ {
+ return Array.Empty<(IndexedItem, IndexedItem, string[])>();
+ }
+
+ // Fetch full failure strings only for the winning assignment.
+ int pairCount = Math.Min(expectationsWithIndexes.Count, bestAssignment.Count);
+ var result = new List<(IndexedItem, IndexedItem, string[])>(pairCount);
+
+ for (int index = 0; index < pairCount; index++)
+ {
+ IndexedItem expectationWithIndex = expectationsWithIndexes[index];
+ string[] failures = TryToMatch(expectationWithIndex.Item, bestAssignment[index].Item, expectationWithIndex.Index);
+ result.Add((expectationWithIndex, bestAssignment[index], failures));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Finds a near-optimal assignment using a greedy strategy. Suitable for large collections where the exact
+ /// permutation search would be prohibitively expensive. All distances are already cached from Phase 1, so
+ /// this is O(n² log n) rather than O(n! × n).
+ ///
+ private IReadOnlyList<(IndexedItem Expectation, IndexedItem Actual, string[] Failures)> FindClosestMismatchesByGreedyAssignment(
+ List> remainingSubjects, IndexedItemCollection expectationsWithIndexes)
+ {
+ int subjectCount = remainingSubjects.Count;
+ int expectationCount = expectationsWithIndexes.Count;
+ int pairCount = expectationCount * subjectCount;
+
+ // Use failure counts (no string allocation) to build the pair list for sorting/assignment.
+ var allPairs = new List<(int ExpectationIndex, int SubjectIndex, int Count)>(pairCount);
+
+ for (int expectationIndex = 0; expectationIndex < expectationCount; expectationIndex++)
+ {
+ IndexedItem exp = expectationsWithIndexes[expectationIndex];
+
+ for (int subjectIndex = 0; subjectIndex < subjectCount; subjectIndex++)
+ {
+ int count = TryToMatchCount(exp.Item, remainingSubjects[subjectIndex].Item, exp.Index);
+ allPairs.Add((expectationIndex, subjectIndex, count));
+ }
+ }
+
+ // Sort by distance ascending. When distances are equal, use expectation index then subject index as
+ // tiebreakers so that assignments are deterministic and follow natural ordering.
+ allPairs.Sort(static (a, b) =>
+ {
+ int relativeOrder = a.Count.CompareTo(b.Count);
+ if (relativeOrder != 0)
+ {
+ return relativeOrder;
+ }
+
+ relativeOrder = a.ExpectationIndex.CompareTo(b.ExpectationIndex);
+ return relativeOrder != 0 ? relativeOrder : a.SubjectIndex.CompareTo(b.SubjectIndex);
+ });
+
+ var assignedExpectationIndexes = new bool[expectationCount];
+ var assignedSubjectIndexes = new bool[subjectCount];
+ int totalToAssign = Math.Min(expectationCount, subjectCount);
+
+ var result = new List<(IndexedItem, IndexedItem, string[])>(totalToAssign);
+
+ // First checks candidate matches from best to worst, then picks the first unused expectation/subject pair it finds,
+ // computes detailed failures only for that chosen pair, and then repeats until all possible matches are assigned.
+ foreach (var (expectationIndex, subjectIndex, _) in allPairs)
+ {
+ if (!assignedExpectationIndexes[expectationIndex] && !assignedSubjectIndexes[subjectIndex])
+ {
+ // Fetch full failure strings only for the selected pairs (~n total).
+ string[] failures = TryToMatch(expectationsWithIndexes[expectationIndex].Item,
+ remainingSubjects[subjectIndex].Item, expectationsWithIndexes[expectationIndex].Index);
+
+ result.Add((expectationsWithIndexes[expectationIndex], remainingSubjects[subjectIndex], failures));
+ assignedExpectationIndexes[expectationIndex] = true;
+ assignedSubjectIndexes[subjectIndex] = true;
+
+ if (result.Count == totalToAssign)
+ {
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Performs a dry-run comparison using a scope that skips formatting. Returns the number of failures
+ /// without allocating any string messages. Used in Phase 1 and Phase 2 to avoid eager
+ /// FailureMessageFormatter + Regex.Replace costs for pairs that will ultimately be discarded.
+ ///
+ private int TryToMatchCount(TExpectation expectation, object subject, int expectationIndex)
+ {
+ var cacheKey = (subject, (object)expectation, expectationIndex);
+
+ if (countCache.TryGetValue(cacheKey, out int cachedCount))
+ {
+ return cachedCount;
+ }
+
+ using var scope = new AssertionScope { UseDryRun = true };
+
+ var itemContext = (EquivalencyValidationContext)context.AsCollectionItem(expectationIndex);
+ itemContext.CyclicReferenceDetector = (CyclicReferenceDetector)((EquivalencyValidationContext)context).CyclicReferenceDetector.Clone();
+
+ parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(TExpectation)), itemContext);
+
+ int count = scope.GetFailureCount();
+ scope.Discard();
+ countCache[cacheKey] = count;
+
+ return count;
+ }
+
+ private string[] TryToMatch(TExpectation expectation, object subject, int expectationIndex)
+ {
+ var cacheKey = (subject, (object)expectation, expectationIndex);
+
+ if (fullFailuresCache.TryGetValue(cacheKey, out string[] cachedResult))
+ {
+ return cachedResult;
+ }
+
+ using var scope = new AssertionScope();
+
+ var itemContext = (EquivalencyValidationContext)context.AsCollectionItem(expectationIndex);
+ itemContext.CyclicReferenceDetector = (CyclicReferenceDetector)((EquivalencyValidationContext)context).CyclicReferenceDetector.Clone();
+
+ parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(TExpectation)), itemContext);
+
+ string[] failures = scope.Discard();
+ fullFailuresCache[cacheKey] = failures;
+
+ return failures;
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/ReadOnlyCollectionExtensions.cs b/Src/FluentAssertions/Equivalency/Steps/ReadOnlyCollectionExtensions.cs
new file mode 100644
index 0000000000..8b2ca781c1
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/ReadOnlyCollectionExtensions.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+[System.Diagnostics.StackTraceHidden]
+internal static class ReadOnlyCollectionExtensions
+{
+ ///
+ /// Generates all possible permutations of the given collection.
+ ///
+ /// The type of the elements in the collection.
+ /// The collection of items for which permutations are to be generated.
+ /// A sequence of all possible permutations, where each permutation is represented as a read-only list of elements.
+ public static IEnumerable> Permute(this IReadOnlyList items)
+ {
+ int[] indices = Enumerable.Range(0, items.Count).ToArray();
+
+ do
+ {
+ var result = new T[indices.Length];
+ for (int index = 0; index < indices.Length; index++)
+ {
+ result[index] = items[indices[index]];
+ }
+
+ yield return result;
+ }
+ while (NextPermutation(indices));
+ }
+
+ private static bool NextPermutation(int[] indices)
+ {
+ int index = indices.Length - 2;
+ while (index >= 0 && indices[index] >= indices[index + 1])
+ {
+ index--;
+ }
+
+ if (index < 0)
+ {
+ return false;
+ }
+
+ int j = indices.Length - 1;
+ while (indices[j] <= indices[index])
+ {
+ j--;
+ }
+
+ (indices[index], indices[j]) = (indices[j], indices[index]);
+ Array.Reverse(indices, index + 1, indices.Length - index - 1);
+ return true;
+ }
+
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs
index c41e899078..b1770b6283 100644
--- a/Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs
@@ -1,5 +1,6 @@
-namespace FluentAssertions.Equivalency.Steps;
+namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class ReferenceEqualityEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
diff --git a/Src/FluentAssertions/Equivalency/Steps/ReferentialComparer.cs b/Src/FluentAssertions/Equivalency/Steps/ReferentialComparer.cs
new file mode 100644
index 0000000000..6b8241bab8
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/ReferentialComparer.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+///
+/// Provides a mechanism for comparing tuples that consist of a subject, an expectation,
+/// and an expectation index. The comparison is based on object references and the expectation index.
+///
+[System.Diagnostics.StackTraceHidden]
+internal sealed class ReferentialComparer : IEqualityComparer<(object Subject, object Expectation, int ExpectationIndex)>
+{
+ public bool Equals((object Subject, object Expectation, int ExpectationIndex) x,
+ (object Subject, object Expectation, int ExpectationIndex) y)
+ {
+ return ReferenceEquals(x.Subject, y.Subject)
+ && ReferenceEquals(x.Expectation, y.Expectation)
+ && x.ExpectationIndex == y.ExpectationIndex;
+ }
+
+ public int GetHashCode((object Subject, object Expectation, int ExpectationIndex) obj)
+ {
+ int hashCode = RuntimeHelpers.GetHashCode(obj.Subject);
+ hashCode = (hashCode * 397) + RuntimeHelpers.GetHashCode(obj.Expectation);
+ hashCode = (hashCode * 397) + obj.ExpectationIndex;
+ return hashCode;
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs
index 114a0aa726..f928070aa1 100644
--- a/Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs
@@ -1,9 +1,10 @@
-namespace FluentAssertions.Equivalency.Steps;
+namespace FluentAssertions.Equivalency.Steps;
///
/// Represents a composite equivalency step that passes the execution to all user-supplied steps that can handle the
/// current context.
///
+[System.Diagnostics.StackTraceHidden]
public class RunAllUserStepsEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
diff --git a/Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs
index af3598ec1d..3920ab5932 100644
--- a/Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs
@@ -2,6 +2,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class SimpleEqualityEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -21,3 +22,4 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
return EquivalencyResult.ContinueWithNext;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/StrictlyOrderedEquivalencyStrategy.cs b/Src/FluentAssertions/Equivalency/Steps/StrictlyOrderedEquivalencyStrategy.cs
new file mode 100644
index 0000000000..809a41bad8
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Steps/StrictlyOrderedEquivalencyStrategy.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using FluentAssertions.Equivalency.Tracing;
+using FluentAssertions.Execution;
+using static FluentAssertions.Common.StringExtensions;
+
+namespace FluentAssertions.Equivalency.Steps;
+
+[System.Diagnostics.StackTraceHidden]
+internal class StrictlyOrderedEquivalencyStrategy(
+ IValidateChildNodeEquivalency parent,
+ IEquivalencyValidationContext context)
+{
+ private const int FailedItemsFastFailThreshold = 10;
+ private readonly Tracer tracer = context.Tracer;
+
+ public void FindAndRemoveMatches(List> subjects, List expectations)
+ {
+ int failedCount = 0;
+
+ int index = 0;
+ for (; index < expectations.Count && index < subjects.Count; index++)
+ {
+ TExpectation expectation = expectations[index];
+
+ int tempIndex = index;
+ using var _ = tracer.WriteBlock(member =>
+ Invariant($"Strictly comparing expectation {expectation} at {member.Expectation} to item with index {tempIndex} in {subjects}"));
+
+ bool succeeded = StrictlyMatchAgainst(subjects, expectation, index);
+ if (!succeeded)
+ {
+ failedCount++;
+ if (failedCount >= FailedItemsFastFailThreshold)
+ {
+ tracer.WriteLine(member =>
+ $"Aborting strict order comparison of collections after {FailedItemsFastFailThreshold} items failed at {member.Expectation}");
+
+ break;
+ }
+ }
+ }
+
+ subjects.RemoveRange(0, index);
+ expectations.RemoveRange(0, index);
+ }
+
+ private bool StrictlyMatchAgainst(List> subjects, T expectation, int expectationIndex)
+ {
+ using var scope = new AssertionScope();
+
+ object subject = subjects[expectationIndex].Item;
+ IEquivalencyValidationContext equivalencyValidationContext = context.AsCollectionItem(expectationIndex);
+
+ parent.AssertEquivalencyOf(new Comparands(subject, expectation, typeof(T)), equivalencyValidationContext);
+
+ return !scope.HasFailures();
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
index 80c8b32452..bf283246d3 100644
--- a/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
@@ -3,6 +3,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class StringEqualityEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -17,7 +18,7 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
var assertionChain = AssertionChain.GetOrCreate().For(context);
- if (!ValidateAgainstNulls(assertionChain, comparands, context.CurrentNode))
+ if (!ValidateAgainstNulls(assertionChain, comparands, context.CurrentNode, context.Options.TreatNullStringsAsEmpty))
{
return EquivalencyResult.EquivalencyProven;
}
@@ -73,7 +74,8 @@ private static Func, EquivalencyOptions> Crea
return o;
};
- private static bool ValidateAgainstNulls(AssertionChain assertionChain, Comparands comparands, INode currentNode)
+ private static bool ValidateAgainstNulls(AssertionChain assertionChain, Comparands comparands, INode currentNode,
+ bool treatNullAsEmpty)
{
object expected = comparands.Expectation;
object subject = comparands.Subject;
@@ -82,6 +84,16 @@ private static bool ValidateAgainstNulls(AssertionChain assertionChain, Comparan
if (onlyOneNull)
{
+ if (treatNullAsEmpty)
+ {
+ string nonNullValue = (string)(expected ?? subject);
+
+ if (nonNullValue.Length == 0)
+ {
+ return false;
+ }
+ }
+
assertionChain.FailWith(
"Expected {0} to be {1}{reason}, but found {2}.", currentNode.Subject.Description.AsNonFormattable(), expected,
subject);
@@ -100,9 +112,10 @@ private static bool ValidateSubjectIsString(AssertionChain assertionChain, Compa
}
assertionChain.FailWith(
- "Expected {0} to be {1}, but found {2}.", currentNode.AsNonFormattable(),
- comparands.RuntimeType, comparands.Subject.GetType());
+ "Expected {0} to be of type {1}, but found {2} of type {3}.", currentNode.AsNonFormattable(),
+ comparands.RuntimeType, comparands.Subject, comparands.Subject.GetType());
return assertionChain.Succeeded;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs
index 188b4f7d79..2481aca98d 100644
--- a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs
@@ -5,6 +5,7 @@
namespace FluentAssertions.Equivalency.Steps;
+[System.Diagnostics.StackTraceHidden]
public class StructuralEqualityEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -108,3 +109,4 @@ private static IEnumerable GetMembersFromExpectation(INode currentNode,
return members;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs
index e63c850e43..04fdf8183c 100644
--- a/Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs
@@ -13,6 +13,7 @@ namespace FluentAssertions.Equivalency.Steps;
/// This differs from the default equivalency assertion which states that two objects are equivalent if they have the
/// same properties and values, regardless of their type.
///
+[System.Diagnostics.StackTraceHidden]
public class TypeEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
@@ -57,3 +58,4 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
return EquivalencyResult.ContinueWithNext;
}
}
+
diff --git a/Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs
index 2af82fa77d..1ddaa1228a 100644
--- a/Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions.Equivalency.Selection;
using FluentAssertions.Execution;
namespace FluentAssertions.Equivalency.Steps;
@@ -6,36 +9,170 @@ namespace FluentAssertions.Equivalency.Steps;
///
/// Ensures that types that are marked as value types are treated as such.
///
+[System.Diagnostics.StackTraceHidden]
public class ValueTypeEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
IValidateChildNodeEquivalency valueChildNodes)
{
- Type expectationType = comparands.GetExpectedType(context.Options);
- EqualityStrategy strategy = context.Options.GetEqualityStrategy(expectationType);
+ var options = context.Options;
+ Type expectationType = comparands.GetExpectedType(options);
+ EqualityStrategy strategy = options.GetEqualityStrategy(expectationType);
- bool canHandle = strategy is EqualityStrategy.Equals or EqualityStrategy.ForceEquals;
+ if (strategy is not (EqualityStrategy.Equals or EqualityStrategy.ForceEquals))
+ {
+ return EquivalencyResult.ContinueWithNext;
+ }
- if (canHandle)
+ if (ReportConflictIfAny(context, comparands, options, expectationType, strategy))
{
- context.Tracer.WriteLine(member =>
- {
- string strategyName = strategy == EqualityStrategy.Equals
- ? $"{expectationType} overrides Equals"
- : "we are forced to use Equals";
+ return EquivalencyResult.EquivalencyProven;
+ }
+
+ ApplyValueSemantics(comparands, context, expectationType, strategy);
+ return EquivalencyResult.EquivalencyProven;
+ }
- return $"Treating {member.Expectation.Description} as a value type because {strategyName}.";
- });
+ ///
+ /// Checks for any selection rule that targets members of the current node, which would be silently
+ /// ignored under value-semantic comparison. Reports the conflict and returns true if one is found.
+ ///
+ private static bool ReportConflictIfAny(
+ IEquivalencyValidationContext context,
+ Comparands comparands,
+ IEquivalencyOptions options,
+ Type expectationType,
+ EqualityStrategy strategy)
+ {
+ // Path-based rules can be checked cheaply by comparing path strings (also handles deep paths like o.Child.Text).
+ IMemberSelectionRule conflictingRule = options.SelectionRules
+ .OfType()
+ .FirstOrDefault(rule => rule.SelectsMembersOf(context.CurrentNode));
- AssertionChain.GetOrCreate()
- .For(context)
- .ReuseOnce();
+ // Non-path rules (based on predicates) require evaluating them against the actual member list
+ conflictingRule ??= FindConflictingNonPathRule(context.CurrentNode, comparands, options);
- comparands.Subject.Should().Be(comparands.Expectation, context.Reason.FormattedMessage, context.Reason.Arguments);
+ if (conflictingRule is not null)
+ {
+ ReportConflict(context, expectationType, conflictingRule, strategy);
+ }
- return EquivalencyResult.EquivalencyProven;
+ return conflictingRule is not null;
+ }
+
+ ///
+ /// Finds the first non-path selection rule that would modify the member list of ,
+ /// indicating it would be silently ignored due to value-semantic comparison.
+ ///
+ private static IMemberSelectionRule FindConflictingNonPathRule(
+ INode currentNode, Comparands comparands, IEquivalencyOptions options)
+ {
+ var selectionContext = new MemberSelectionContext(comparands.CompileTimeType, comparands.RuntimeType, options);
+ IList allMembers = GetAllMembers(currentNode, selectionContext);
+
+ // If the type has no accessible members, no selection rule can meaningfully conflict.
+ if (allMembers.Count == 0)
+ {
+ return null;
}
- return EquivalencyResult.ContinueWithNext;
+ return options.SelectionRules
+ .Where(rule => !IsInfrastructureRule(rule) && rule is not IPathBasedSelectionRule)
+ .FirstOrDefault(rule => RuleAffectsMembers(rule, allMembers, GetFilteredMembers(rule, currentNode, allMembers, selectionContext)));
+ }
+
+ ///
+ /// Applies in isolation to determine whether it modifies the member list.
+ /// Inclusion rules are run from an empty starting set; exclusion rules are run on the full member list.
+ ///
+ private static IList GetFilteredMembers(
+ IMemberSelectionRule rule, INode currentNode, IList allMembers, MemberSelectionContext context)
+ {
+ IEnumerable seed = rule.IncludesMembers ? [] : allMembers;
+ return rule.SelectMembers(currentNode, seed, context).ToList();
+ }
+
+ /// Gets all properties and fields of the current node's type, respecting the configured visibility.
+ private static IList GetAllMembers(INode currentNode, MemberSelectionContext context)
+ {
+ IEnumerable members = new AllPropertiesSelectionRule().SelectMembers(currentNode, [], context);
+ return new AllFieldsSelectionRule().SelectMembers(currentNode, members, context).ToList();
+ }
+
+ ///
+ /// Returns true if actually affects the member set of the current value type.
+ /// For inclusion rules, a conflict exists only when the rule selects at least one member of this type,
+ /// meaning the user intended to compare this value type member-by-member.
+ /// For exclusion rules, a conflict exists when the rule removes at least one member from the full set.
+ ///
+ private static bool RuleAffectsMembers(IMemberSelectionRule rule, IList allMembers, IList filteredMembers) =>
+ rule.IncludesMembers
+ ? filteredMembers.Count > 0
+ : MemberSetChanged(allMembers, filteredMembers);
+
+ /// Returns true if represents a different set of members than .
+ private static bool MemberSetChanged(IList before, IList after) =>
+ before.Count != after.Count || before.Except(after).Any();
+
+ ///