diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 742bef891..f885c38cf 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -25,3 +25,8 @@ updates:
- dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces"
- dependency-name: "Microsoft.CodeAnalysis.Common"
- dependency-name: "Microsoft.CodeAnalysis.Workspaces.Common"
+ # Analyzer-shipped BCL packages must match minimum supported SDK host.
+ # See: https://github.com/rjmurillo/moq.analyzers/issues/850
+ - dependency-name: "System.Collections.Immutable"
+ - dependency-name: "System.Reflection.Metadata"
+ - dependency-name: "Microsoft.CodeAnalysis.AnalyzerUtilities"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index dca12913c..aa91ef519 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -33,11 +33,89 @@ permissions:
actions: read
jobs:
+ # Build, validate, and package. Fast gate (~5 min).
+ # Tests, analyzer-load-test, and perf run as separate downstream jobs.
build:
+ runs-on: ubuntu-24.04-arm
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Setup, Restore, and Build Solution
+ uses: ./.github/actions/setup-restore-build
+
+ - name: Validate analyzer host compatibility
+ shell: pwsh
+ run: |
+ # Verify shipped analyzer DLLs don't reference assembly versions
+ # exceeding what the minimum supported SDK host (.NET 8) provides.
+ # See: https://github.com/rjmurillo/moq.analyzers/issues/850
+ $maxVersions = @{
+ 'System.Collections.Immutable' = [Version]'8.0.0.0'
+ 'System.Reflection.Metadata' = [Version]'8.0.0.0'
+ }
+ $shippedDlls = @(
+ 'artifacts/bin/Moq.Analyzers/release/Moq.Analyzers.dll',
+ 'artifacts/bin/Moq.Analyzers/release/Moq.CodeFixes.dll',
+ 'artifacts/bin/Moq.Analyzers/release/Microsoft.CodeAnalysis.AnalyzerUtilities.dll'
+ )
+ $failed = $false
+ foreach ($dll in $shippedDlls) {
+ $name = [System.IO.Path]::GetFileName($dll)
+ $bytes = [System.IO.File]::ReadAllBytes($dll)
+ $asm = [System.Reflection.Assembly]::Load($bytes)
+ $dllFailed = $false
+ foreach ($ref in $asm.GetReferencedAssemblies()) {
+ if ($maxVersions.ContainsKey($ref.Name) -and $ref.Version -gt $maxVersions[$ref.Name]) {
+ Write-Error "$name references $($ref.Name) $($ref.Version), max allowed is $($maxVersions[$ref.Name])"
+ $failed = $true
+ $dllFailed = $true
+ }
+ }
+ if (-not $dllFailed) { Write-Host "$name - OK" }
+ }
+ if ($failed) { exit 1 }
+
+ - name: Upload binlogs
+ uses: actions/upload-artifact@v6
+ if: success() || failure()
+ with:
+ name: binlogs
+ path: ./artifacts/logs
+ if-no-files-found: error
+
+ - name: Upload SARIF files
+ uses: actions/upload-artifact@v6
+ if: success() || failure()
+ with:
+ name: SARIF files
+ path: ./artifacts/obj/**/*.sarif
+
+ - name: Upload packages
+ uses: actions/upload-artifact@v6
+ with:
+ name: packages
+ path: |
+ ./artifacts/package
+ if-no-files-found: error
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v6
+ if: success() || failure()
+ with:
+ name: artifacts
+ path: ./artifacts
+ if-no-files-found: error
+
+ # Run unit tests on multiple platforms.
+ # Build happens independently on each runner so tests use platform-native binaries.
+ test:
strategy:
fail-fast: false
matrix:
- os: [windows-11-arm, ubuntu-24.04-arm]
+ os: [ubuntu-24.04-arm, windows-latest]
runs-on: ${{ matrix.os }}
@@ -45,8 +123,6 @@ jobs:
IS_CODACY_COVERAGE_ALLOWED: ${{ secrets.CODACY_PROJECT_TOKEN != '' }}
IS_QLTY_COVERAGE_ALLOWED: ${{ secrets.QLTY_COVERAGE_TOKEN != '' }}
IS_TARGET_MAIN: ${{ github.ref == 'refs/heads/main' }}
- RUN_FULL_PERF: ${{ (github.event_name == 'schedule' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_performance == 'true') }}
- FORCE_PERF_BASELINE: ${{ github.event.inputs.force_baseline }} # This is also used in PerfCore.ps1 to determine if the baseline should be forced
steps:
- uses: actions/checkout@v6
@@ -75,29 +151,14 @@ jobs:
name: CoverageHistory-${{ matrix.os }}
path: ./artifacts/TestResults/coveragehistory
- - name: Upload binlogs
- uses: actions/upload-artifact@v6
- if: success() || failure()
- with:
- name: binlogs-${{ matrix.os }}
- path: ./artifacts/logs
- if-no-files-found: error
-
- name: Upload *.received.* files
uses: actions/upload-artifact@v6
if: failure()
with:
- name: verify-test-results
+ name: verify-test-results-${{ matrix.os }}
path: |
**/*.received.*
- - name: Upload SARIF files
- uses: actions/upload-artifact@v6
- if: success() || failure()
- with:
- name: SARIF files (${{ matrix.os }})
- path: ./artifacts/obj/**/*.sarif
-
- name: Upload Test Report
uses: actions/upload-artifact@v6
if: success() || failure()
@@ -131,26 +192,205 @@ jobs:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: ${{ github.workspace }}/artifacts/TestResults/coverage/Cobertura.xml
- - name: Upload packages
- uses: actions/upload-artifact@v6
+ # Verify the shipped nupkg loads without CS8032 on every supported host.
+ # This is the end-to-end integration test for issue #850.
+ #
+ # The analyzer targets netstandard2.0 because it must load in two hosts:
+ # 1. dotnet CLI - csc runs on .NET (8/9/10), tested on Linux ARM
+ # 2. msbuild.exe - csc runs on .NET Framework (VS), tested on Windows x64
+ analyzer-load-test:
+ needs: build
+ runs-on: ${{ matrix.runs-on }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # dotnet CLI compiler host (modern .NET runtime)
+ - dotnet-version: '8.0.x'
+ tfm: net8.0
+ runs-on: ubuntu-24.04-arm
+ build-engine: dotnet
+ - dotnet-version: '9.0.x'
+ tfm: net9.0
+ runs-on: ubuntu-24.04-arm
+ build-engine: dotnet
+ - dotnet-version: '10.0.x'
+ tfm: net10.0
+ runs-on: ubuntu-24.04-arm
+ build-engine: dotnet
+ # MSBuild/.NET Framework compiler host - VS 2022 (GA)
+ - dotnet-version: '8.0.x'
+ tfm: net472
+ runs-on: windows-2022
+ build-engine: msbuild
+ - dotnet-version: '8.0.x'
+ tfm: net48
+ runs-on: windows-2022
+ build-engine: msbuild
+ - dotnet-version: '8.0.x'
+ tfm: net481
+ runs-on: windows-2022
+ build-engine: msbuild
+ # MSBuild/.NET Framework compiler host - VS 2026 (beta)
+ - dotnet-version: '8.0.x'
+ tfm: net472
+ runs-on: windows-2025-vs2026
+ build-engine: msbuild
+ - dotnet-version: '8.0.x'
+ tfm: net48
+ runs-on: windows-2025-vs2026
+ build-engine: msbuild
+ - dotnet-version: '8.0.x'
+ tfm: net481
+ runs-on: windows-2025-vs2026
+ build-engine: msbuild
+
+ steps:
+ - name: Setup .NET ${{ matrix.dotnet-version }}
+ uses: actions/setup-dotnet@v5
with:
- name: packages-${{ matrix.os }}
- path: |
- ./artifacts/package
- if-no-files-found: error
+ dotnet-version: ${{ matrix.dotnet-version }}
+
+ - name: Setup MSBuild
+ if: matrix.build-engine == 'msbuild'
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Download nupkg artifact
+ uses: actions/download-artifact@v7
+ with:
+ name: packages
+ path: ./local-feed
+
+ - name: Create test project
+ shell: pwsh
+ run: |
+ $pkg = Get-ChildItem ./local-feed -Recurse -Filter 'Moq.Analyzers.*.nupkg' |
+ Where-Object { $_.Name -notlike '*.symbols.*' } |
+ Select-Object -First 1
+ if (-not $pkg) {
+ Write-Error "Could not find Moq.Analyzers.*.nupkg (excluding symbols) in ./local-feed"
+ exit 1
+ }
+ $version = $pkg.Name -replace '^Moq\.Analyzers\.' -replace '\.nupkg$'
+ $feedDir = (Resolve-Path $pkg.DirectoryName).Path
+ Write-Host "Testing Moq.Analyzers $version with ${{ matrix.build-engine }} / ${{ matrix.tfm }}"
+ echo "ANALYZER_VERSION=$version" >> $env:GITHUB_ENV
+ echo "FEED_DIR=$feedDir" >> $env:GITHUB_ENV
+
+ New-Item -ItemType Directory -Path test-project | Out-Null
+
+ @"
+
+
+
+
+
+
+
+
+ "@ | Set-Content test-project/nuget.config
+
+ @"
+
+
+ Library
+ ${{ matrix.tfm }}
+ latest
+
+
+
+
+
+
+ "@ | Set-Content test-project/TestAnalyzerLoad.csproj
+
+ @"
+ namespace TestAnalyzerLoad;
+ public class Placeholder { }
+ "@ | Set-Content test-project/Placeholder.cs
+
+ - name: Build with dotnet CLI
+ if: matrix.build-engine == 'dotnet'
+ shell: pwsh
+ working-directory: test-project
+ run: |
+ $output = dotnet build -v n 2>&1 | Out-String
+ $buildExitCode = $LASTEXITCODE
+ Write-Host $output
+
+ if ($buildExitCode -ne 0) {
+ Write-Host "::error::Build failed with exit code $buildExitCode (dotnet / ${{ matrix.tfm }})"
+ exit $buildExitCode
+ }
+
+ if ($output -match 'CS8032') {
+ Write-Host "::error::CS8032: analyzer failed to load (dotnet / ${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
+ exit 1
+ }
+ if ($output -match '(?i)could not load file or assembly') {
+ Write-Host "::error::Assembly binding failure (dotnet / ${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
+ exit 1
+ }
+
+ Write-Host ""
+ Write-Host "Analyzer loaded successfully (dotnet / ${{ matrix.tfm }})"
+
+ - name: Build with MSBuild
+ if: matrix.build-engine == 'msbuild'
+ shell: pwsh
+ working-directory: test-project
+ run: |
+ $output = msbuild TestAnalyzerLoad.csproj -restore -p:Configuration=Release -v:n 2>&1 | Out-String
+ $buildExitCode = $LASTEXITCODE
+ Write-Host $output
+
+ if ($buildExitCode -ne 0) {
+ Write-Host "::error::Build failed with exit code $buildExitCode (msbuild / ${{ matrix.tfm }})"
+ exit $buildExitCode
+ }
+
+ if ($output -match 'CS8032') {
+ Write-Host "::error::CS8032: analyzer failed to load (msbuild / ${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
+ exit 1
+ }
+ if ($output -match '(?i)could not load file or assembly') {
+ Write-Host "::error::Assembly binding failure (msbuild / ${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
+ exit 1
+ }
+
+ Write-Host ""
+ Write-Host "Analyzer loaded successfully (msbuild / ${{ matrix.tfm }})"
+
+ # Performance validation runs last, after build and tests confirm correctness.
+ # Builds from source on Linux ARM to get consistent benchmark results.
+ perf:
+ needs: [build, test]
+ runs-on: ubuntu-24.04-arm
+
+ env:
+ RUN_FULL_PERF: ${{ (github.event_name == 'schedule' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_performance == 'true') }}
+ FORCE_PERF_BASELINE: ${{ github.event.inputs.force_baseline }}
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Setup, Restore, and Build Solution
+ uses: ./.github/actions/setup-restore-build
- name: Get baseline SHA
id: get-baseline-sha
run: |
if (-not (Test-Path build/perf/baseline.json)) {
- Write-Error "baseline.json not found – aborting performance job."
+ Write-Error "baseline.json not found - aborting performance job."
exit 1
}
$baseline = Get-Content build/perf/baseline.json | ConvertFrom-Json
echo "sha=$($baseline.sha)" >> $env:GITHUB_OUTPUT
shell: pwsh
- # The machine is not guaranteed to have the .NET SDK installed from the baseline SHA, so we need to install it
+ # The baseline SHA may require a different .NET SDK version
- name: Checkout baseline
uses: actions/checkout@v6
with:
@@ -214,14 +454,6 @@ jobs:
uses: actions/upload-artifact@v6
if: success() || failure()
with:
- name: performance-${{ matrix.os }}
+ name: performance
path: |
./artifacts/performance/**
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v6
- if: success() || failure()
- with:
- name: artifacts-${{ matrix.os }}
- path: ./artifacts
- if-no-files-found: error
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 08cccfb4c..7d696286e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -27,8 +27,7 @@ jobs:
uses: actions/download-artifact@v7
with:
path: packages
- pattern: packages-ubuntu-*
- merge-multiple: true
+ name: packages
- name: Publish NuGet package
shell: pwsh
run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1655f7107..1e2f24776 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -597,6 +597,105 @@ When making CI/CD changes:
3. **Update documentation**: Document new CI features or changes
4. **Consider performance impact**: Ensure changes don't significantly impact CI duration
+### Running Workflows Locally with `gh act`
+
+[`gh act`](https://github.com/nektos/gh-act) runs GitHub Actions workflows locally using Docker.
+This is useful for verifying CI changes before pushing.
+
+**Install:**
+
+```bash
+gh extension install nektos/gh-act
+```
+
+**List available jobs:**
+
+```bash
+gh act -l # list jobs triggered by push
+gh act -l pull_request # list jobs triggered by pull_request
+```
+
+**Run a specific job:**
+
+```bash
+gh act -j build # run the build job
+gh act -j analyzer-load-test # run the analyzer load test
+```
+
+**Run a specific workflow file:**
+
+```bash
+gh act -W .github/workflows/main.yml
+```
+
+**Filter matrix entries:**
+
+```bash
+gh act -j analyzer-load-test --matrix tfm:net8.0
+gh act -j analyzer-load-test --matrix build-engine:msbuild
+gh act -j build --matrix os:ubuntu-24.04-arm
+```
+
+**Enable artifact passing between jobs:**
+
+Jobs that depend on artifacts from earlier jobs (like `analyzer-load-test` downloading
+the nupkg from `build`) require a local artifact server:
+
+```bash
+gh act -W .github/workflows/main.yml --artifact-server-path /tmp/artifacts
+```
+
+**Handle secrets:**
+
+The workflow references optional secrets (`CODACY_PROJECT_TOKEN`, `QLTY_COVERAGE_TOKEN`).
+These can be skipped locally, but if needed:
+
+```bash
+gh act -s GITHUB_TOKEN="$(gh auth token)" # pass GitHub token
+gh act --secret-file .secrets # load from file (one KEY=VALUE per line)
+```
+
+**Simulate `workflow_dispatch` with inputs:**
+
+```bash
+gh act workflow_dispatch --input run_performance=true --input force_baseline=false
+```
+
+**Container architecture:**
+
+The project uses ARM runners (`ubuntu-24.04-arm`, `windows-11-arm`). Docker defaults to the
+host architecture. To force a specific architecture:
+
+```bash
+gh act --container-architecture linux/amd64
+```
+
+**Create an `.actrc` file** in the repo root to set default flags (one per line):
+
+```text
+--artifact-server-path=/tmp/artifacts
+--container-architecture=linux/amd64
+```
+
+**Skip steps that only make sense in CI:**
+
+`act` sets the `ACT` environment variable automatically. Steps can check for it:
+
+```yaml
+if: ${{ !env.ACT }}
+```
+
+**Limitations:**
+
+- Windows-based jobs (`windows-latest`, `windows-11-arm`) do not run in `act`. Test those
+ through GitHub Actions directly.
+- ARM runner images may not match GitHub-hosted runners exactly.
+- The `microsoft/setup-msbuild` action requires Windows and will not work in `act`.
+- Composite actions (like `setup-restore-build`) work, but may need the action source
+ checked out locally.
+
+For full documentation, see [nektosact.com](https://nektosact.com/usage/).
+
### Performance Testing Guidelines
**When Performance Testing is Required:**
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 02c2c40a2..80401b7c1 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -5,4 +5,5 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a9b45c0d5..38550ba98 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,7 +22,14 @@
-
+
+
@@ -34,7 +41,16 @@
-
+
+
diff --git a/build/targets/packaging/Packaging.targets b/build/targets/packaging/Packaging.targets
new file mode 100644
index 000000000..4dbcb010e
--- /dev/null
+++ b/build/targets/packaging/Packaging.targets
@@ -0,0 +1,40 @@
+
+
+
+
+ <_MaxSystemCollectionsImmutableMajor>8
+ <_MaxSystemReflectionMetadataMajor>8
+
+
+
+
+
+ <_SciRef Include="@(ResolvedCompileFileDefinitions)"
+ Condition="'%(NuGetPackageId)' == 'System.Collections.Immutable'" />
+ <_SrmRef Include="@(ResolvedCompileFileDefinitions)"
+ Condition="'%(NuGetPackageId)' == 'System.Reflection.Metadata'" />
+
+
+
+
+
+
+
+
diff --git a/renovate.json b/renovate.json
index 2ef7914db..e3426367f 100644
--- a/renovate.json
+++ b/renovate.json
@@ -20,6 +20,16 @@
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true
+ },
+ {
+ "description": "Analyzer-shipped BCL packages must match minimum supported SDK host. Manual review required.",
+ "matchPackageNames": [
+ "System.Collections.Immutable",
+ "System.Reflection.Metadata",
+ "Microsoft.CodeAnalysis.AnalyzerUtilities"
+ ],
+ "automerge": false,
+ "labels": ["analyzer-compat"]
}
],
"platformAutomerge": true
diff --git a/src/tools/PerfDiff/PerfDiff.csproj b/src/tools/PerfDiff/PerfDiff.csproj
index 06996fdbc..3de991806 100644
--- a/src/tools/PerfDiff/PerfDiff.csproj
+++ b/src/tools/PerfDiff/PerfDiff.csproj
@@ -12,6 +12,8 @@
+
+
diff --git a/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj b/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj
index 69c241a35..025714126 100644
--- a/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj
+++ b/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj
@@ -14,6 +14,8 @@
+
+
diff --git a/tests/Moq.Analyzers.Test/AnalyzerAssemblyCompatibilityTests.cs b/tests/Moq.Analyzers.Test/AnalyzerAssemblyCompatibilityTests.cs
new file mode 100644
index 000000000..23c12cdee
--- /dev/null
+++ b/tests/Moq.Analyzers.Test/AnalyzerAssemblyCompatibilityTests.cs
@@ -0,0 +1,101 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Moq.Analyzers.Test;
+
+public class AnalyzerAssemblyCompatibilityTests
+{
+ // Primary shipped assemblies built by this project
+ private static readonly string PrimaryAnalyzerAssembly = "Moq.Analyzers";
+ private static readonly string PrimaryCodeFixAssembly = "Moq.CodeFixes";
+
+ // Bundled third-party assemblies included in the package
+ private static readonly string BundledAnalyzerUtilities = "Microsoft.CodeAnalysis.AnalyzerUtilities";
+
+ // Maximum assembly versions that the minimum supported SDK host (.NET 8) provides.
+ // The analyzer must not reference anything higher, or it will fail to load with CS8032.
+ // See: https://github.com/rjmurillo/moq.analyzers/issues/850
+ private static readonly Version MaxImmutableVersion = new(8, 0, 0, 0);
+ private static readonly Version MaxMetadataVersion = new(8, 0, 0, 0);
+
+ public static TheoryData ShippedAssemblies =>
+ new()
+ {
+ { PrimaryAnalyzerAssembly },
+ { PrimaryCodeFixAssembly },
+ { BundledAnalyzerUtilities },
+ };
+
+ [Theory]
+ [MemberData(nameof(ShippedAssemblies))]
+ public void ShippedDlls_MustNotExceedMinimumHostAssemblyVersions(string assemblyName)
+ {
+ FileInfo testAssembly = new(Assembly.GetExecutingAssembly().Location);
+ FileInfo dllFile = new(Path.Combine(testAssembly.DirectoryName!, $"{assemblyName}.dll"));
+
+ Assert.True(dllFile.Exists, $"Expected shipped DLL not found: {dllFile.FullName}");
+
+ AssemblyLoadContext context = new("compat-check", isCollectible: true);
+ try
+ {
+ Assembly assembly = context.LoadFromAssemblyPath(dllFile.FullName);
+
+ // For bundled third-party DLLs, verify we are testing the same artifact
+ // that the primary analyzer assembly references, not a different version
+ // that might exist in the test output from the test project's own dependencies.
+ if (string.Equals(assemblyName, BundledAnalyzerUtilities, StringComparison.Ordinal))
+ {
+ VerifyBundledAssemblyMatchesPrimaryReference(context, testAssembly.DirectoryName!, assembly);
+ }
+
+ AssemblyName[] references = assembly.GetReferencedAssemblies();
+
+ AssertVersionNotExceeded(assemblyName, references, "System.Collections.Immutable", MaxImmutableVersion);
+ AssertVersionNotExceeded(assemblyName, references, "System.Reflection.Metadata", MaxMetadataVersion);
+ }
+ finally
+ {
+ context.Unload();
+ }
+ }
+
+ private static void VerifyBundledAssemblyMatchesPrimaryReference(
+ AssemblyLoadContext context,
+ string outputDirectory,
+ Assembly bundledAssembly)
+ {
+ // Load the primary analyzer assembly to verify the bundled assembly matches
+ // what the analyzer actually references. This ensures we're testing the
+ // artifact that will be packaged, not a different version from the test project.
+ string primaryPath = Path.Combine(outputDirectory, $"{PrimaryAnalyzerAssembly}.dll");
+ Assembly primaryAssembly = context.LoadFromAssemblyPath(primaryPath);
+
+ AssemblyName? expectedReference = primaryAssembly
+ .GetReferencedAssemblies()
+ .FirstOrDefault(r => string.Equals(r.Name, bundledAssembly.GetName().Name, StringComparison.Ordinal));
+
+ Assert.NotNull(expectedReference);
+ Assert.Equal(
+ expectedReference.Version,
+ bundledAssembly.GetName().Version);
+ }
+
+ private static void AssertVersionNotExceeded(
+ string assemblyName,
+ AssemblyName[] references,
+ string referenceName,
+ Version maxVersion)
+ {
+ AssemblyName? reference = references.FirstOrDefault(
+ r => string.Equals(r.Name, referenceName, StringComparison.Ordinal));
+
+ if (reference?.Version is null)
+ {
+ return;
+ }
+
+ Assert.True(
+ reference.Version <= maxVersion,
+ $"{assemblyName} references {referenceName} version {reference.Version}, but the minimum supported SDK host only provides {maxVersion}. See: https://github.com/rjmurillo/moq.analyzers/issues/850");
+ }
+}