diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 01c73e172..74368c840 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -2,8 +2,13 @@ name: Codebelt Coverlet MTP CI on: pull_request: - branches: [master] + branches: [master, releases/v10] workflow_dispatch: + inputs: + run_mac_tests: + type: boolean + description: Include macOS tests and package validation despite hosted-runner capacity constraints. + default: false permissions: contents: read @@ -49,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-26.04, windows-2025, macos-26] + os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.run_mac_tests && '["ubuntu-26.04","windows-2025","macos-26"]' || '["ubuntu-26.04","windows-2025"]') }} project_name: [core, core-coverage, mtp] include: - project_name: core @@ -88,7 +93,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-26.04, windows-2025, macos-26] + os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.run_mac_tests && '["ubuntu-26.04","windows-2025","macos-26"]' || '["ubuntu-26.04","windows-2025"]') }} framework: [net9.0, net10.0] permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa74aa89..6bc48f949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.1.2] - 2026-11-03 + +This is a patch release that restores coverage for project assemblies accidentally matched by generated dependency exclusions and fixes a separate framework assembly-resolution failure reproduced with Cuemon on .NET Framework 4.8. The dependency-filter defect was first included in Codebelt 10.0.1; the facade-resolution defect has a separate, older upstream origin. Package identity, public APIs, dependency versions, and supported target frameworks are unchanged. Restored modules can change coverage totals and quality-gate outcomes. + +> [!WARNING] +> Versions 10.0.1 through 10.1.1 can produce incomplete coverage while tests and CI pass. BenchmarkDotNet's historical 92.3% report measured only its tooling runner; the library modules were missing. Review expected modules and source files when regenerating coverage. See the [full advisory](Documentation/DependencyExclusionRegression.md) for affected configurations, upstream [PR #1946](https://github.com/coverlet-coverage/coverlet/pull/1946) and commit [`c502253`](https://github.com/coverlet-coverage/coverlet/commit/c5022539cfdf3c6246c2e49b7789d7e8e529ef47), historical CI evidence, and the separately traced .NET Framework instrumentation fix validated with Cuemon. + +### Changed + +- Document exact dependency discovery and retained xUnit build exclusions, with prominent coverage alerts, upstream provenance, affected-version analysis, and verified results for the seven reported consumer repositories and Cuemon on .NET Framework 4.8. + +### Fixed + +- Generate exclusion filters from the managed runtime assets, including RID-specific assemblies, declared by non-project libraries in `deps.json` instead of from package IDs, so dependencies no longer exclude project assemblies that merely share a name prefix; preserve project outputs also listed in SDK reference entries, +- Accept hyphenated assembly names such as `[xunit.v3.mtp-v2]*` as valid exclude filters, and exclude the `xunit.abstractions`, `xunit.runner.visualstudio.testadapter`, `xunit.v3.runner.utility.netcore`, and `xunit.v3.runner.utility.netfx` build assets that the xUnit runner and VSTest adapter copy without `deps.json` runtime entries, +- Keep the runtime `netstandard.dll` facade bound to its owning assembly resolver so forwarded enum types retain configured framework fallbacks during instrumentation; restore coverage for `Cuemon.Core`, `Cuemon.Kernel`, and `Cuemon.Extensions.Core` on .NET Framework 4.8, with regression coverage for optional enum metadata. + ## [10.1.1] - 2026-10-20 This is a patch release that fixes module unload mutex collisions between unrelated coverage sessions and updates the Microsoft runtime pins and the `System.Security.Cryptography.Pkcs` override used by coverlet.core, with no new capabilities. @@ -97,6 +114,7 @@ This is a minor release focused on the Codebelt.Coverlet.MTP fork with narrowed - .vscode/settings.json configuration, - .devcontainer legacy configuration. +[10.1.2]: https://github.com/codebeltnet/coverlet/compare/v10.1.1...v10.1.2 [10.1.1]: https://github.com/codebeltnet/coverlet/compare/v10.1.0...v10.1.1 [10.1.0]: https://github.com/codebeltnet/coverlet/compare/v10.0.1...v10.1.0 [10.0.1]: https://github.com/codebeltnet/coverlet/compare/v10.0.0...v10.0.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 630f27014..e38def3fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,16 @@ Contributions are welcome. This repository is a deliberately narrow Codebelt-mai Use the .NET SDK pinned by `global.json`. The retained production matrix is `netstandard2.0`, `net9.0`, and `net10.0`; tests execute on `net9.0` and `net10.0`. +## Release branches + +Target `releases/v10` for changes to the 10.x maintenance line, following the maintenance-branch naming in the [GitHub Actions versioning example](https://github.com/actions/toolkit/blob/main/docs/action-versioning.md#sample-workflow). Its starting point is the `v10.1.1` release commit, and the 10.1.2 patch retains that release's SDK, dependency versions, and `netstandard2.0;net9.0;net10.0` package targets. CI accepts pull requests targeting either `releases/v10` or `master`. + +`master` continues development toward 11.0.0, including its .NET 11 changes and recorded upstream integrations. Keep changes for that release on `master` and the existing 11.0.0 branches. Forward-port maintenance fixes separately and validate them against the destination branch's framework matrix. Merging `master` into a maintenance hotfix would also bring its later product and framework changes into the patch release. + +After integrating a maintenance pull request and completing its checks, create the fixed full-version tag, such as `v10.1.2`, on the exact commit to release before starting the manual CI/release workflow. MinVer otherwise calculates a prerelease version above the latest release tag. Select `releases/v10` in the workflow's branch dropdown while that tagged commit is still its tip, or use `gh workflow run ci-pipeline.yml --ref v10.1.2` to select the tagged commit explicitly. The existing workflow builds, tests, packs, and validates that source before requesting approval in the `Production` environment for NuGet publication. The environment currently permits this maintenance branch and retains its required reviewer. The Actions guide's moving major-version tags are separate from this maintenance branch; NuGet releases retain fixed full-version tags. Do not move existing release tags. + +CI requires Windows and Ubuntu tests and package validation. macOS is optional because hosted-runner capacity can prevent jobs from starting. To include it, enable `run_mac_tests` in the manual workflow form, or add `-f run_mac_tests=true` to the workflow command. The option includes macOS in both matrices; every selected job must pass before publication. A default run does not establish macOS compatibility. This follows the opt-in behavior in [Codebelt xUnit's workflow](https://github.com/codebeltnet/xunit/blob/main/.github/workflows/ci-pipeline.yml). + ## Building, testing, and packing Clone this repository: diff --git a/Documentation/Coverlet.MTP.Integration.md b/Documentation/Coverlet.MTP.Integration.md index 523a2cb76..8cac70e01 100644 --- a/Documentation/Coverlet.MTP.Integration.md +++ b/Documentation/Coverlet.MTP.Integration.md @@ -10,6 +10,9 @@ More information is available here: `Codebelt.Coverlet.MTP` packages Coverlet's Microsoft Testing Platform integration while preserving the implementation assembly name `coverlet.MTP.dll` for extension discovery. +> [!WARNING] +> Versions 10.0.1 through 10.1.1 can omit project assemblies under default dependency exclusions when their assembly names share a generated prefix. The [10.1.2 coverage advisory](DependencyExclusionRegression.md) explains the correction, upstream origins, consumer validation, and the separate .NET Framework instrumentation fix validated with Cuemon. Existing high percentages or successful test runs do not establish that all expected modules were included. + ## Supported Runtime Versions This package provides explicit dependency groups for: @@ -81,7 +84,7 @@ dotnet exec --help | `--coverlet-file-prefix ` | Prefix for coverage report filenames to prevent overwrites when multiple test projects write to the same directory. When specified, files are named `.coverage.` instead of `coverage.`. (default: `none`) | | `--coverlet-include ` | Include assemblies matching filters (e.g., `[Assembly]Type`). Can be specified multiple times. (default: `none`) | | `--coverlet-include-directory ` | Include additional directories for sources. Can be specified multiple times. (default: `none`) | -| `--coverlet-exclude ` | Exclude assemblies matching filters (e.g., `[Assembly]Type`). Can be specified multiple times. User-specified filters are merged with defaults. (defaults: see "Default Exclusions Behavior" below) | +| `--coverlet-exclude ` | Exclude assemblies matching filters (e.g., `[Assembly]Type`). Can be specified multiple times. User-specified filters are merged with the permanent baseline; dynamic exclusions are skipped. (defaults: see "Default Exclusions Behavior" below) | | `--coverlet-exclude-by-file ` | Exclude source files matching glob patterns. Can be specified multiple times. (default: `none`) | | `--coverlet-exclude-by-attribute ` | Exclude methods/classes decorated with attributes. Can be specified multiple times. User-specified attributes are merged with defaults. (defaults: see "Default Exclusions Behavior" below) | | `--coverlet-include-test-assembly` | Include test assembly in coverage. (default: `false`) | @@ -105,17 +108,18 @@ Coverlet.MTP applies sensible default exclusions to reduce noise in coverage rep **Command Line Defaults:** -When using command line options **without** a configuration file, the following defaults are **automatically merged** with user-specified exclusions: +When using command line options **without** a configuration file, the following baseline exclusions are **automatically merged** with user-specified exclusions: -- **Default Exclude Filters:** `[coverlet.*]*`, `[Microsoft.VisualStudio.TestPlatform.*]*`, `[testhost*]*` +- **Default Exclude Filters:** `[coverlet.*]*`, `[Microsoft.VisualStudio.TestPlatform.*]*`, `[testhost*]*`, `[ReportGenerator.*]*` - **Default Exclude by Attributes:** `ExcludeFromCodeCoverage`, `ExcludeFromCodeCoverageAttribute`, `GeneratedCodeAttribute`, `CompilerGeneratedAttribute` -**Dynamic Exclude Filters (Assembly Naming Convention):** +**Dynamic Exclude Filters (Dependency Discovery):** -Coverlet.MTP also applies **dynamic exclusions** derived from the test assembly name and its namespace conventions at runtime. These are intended to automatically exclude common test infrastructure assemblies that follow standard naming patterns. +When neither `--coverlet-exclude` nor a configuration file supplies exclusions, Coverlet.MTP also discovers managed runtime assemblies from non-project libraries in the `*.deps.json` files next to the test module, including RID-specific runtime assets. Assemblies already loaded in the controller process are also excluded, except for the test assembly itself. -> [!WARNING] -> **Assembly Naming Limitation:** Dynamic exclusions rely on the assembly name following conventional naming schemas (e.g., `MyApp.Tests`, `MyApp.UnitTests`, `MyApp.IntegrationTests`). Assemblies that **do not follow these naming conventions** will **not** be automatically excluded by the dynamic filter. In such cases, you must add explicit `--coverlet-exclude` filters manually. +Generated filters match exact assembly names, such as `[Example.Testing.Infrastructure]*`. Package IDs are resolved to their runtime assembly filenames instead of being generalized into namespace wildcards. A project assembly sharing a dependency's name prefix remains eligible for coverage. User-specified wildcard filters retain their normal matching behavior. + +Default discovery also excludes the exact xUnit build asset names `xunit.abstractions`, `xunit.runner.visualstudio.testadapter`, `xunit.v3.runner.utility.netcore`, and `xunit.v3.runner.utility.netfx`. The xUnit runner and VSTest adapter copy these assets without declaring runtime entries in `deps.json`. These defaults are skipped with explicit exclusion filters or a configuration file. **Configuration File Behavior (Authoritative Mode):** diff --git a/Documentation/DependencyExclusionRegression.md b/Documentation/DependencyExclusionRegression.md new file mode 100644 index 000000000..84c56e0a8 --- /dev/null +++ b/Documentation/DependencyExclusionRegression.md @@ -0,0 +1,136 @@ +# Coverage advisory: incomplete library coverage + +Recorded on 2026-11-03 for the `Codebelt.Coverlet.MTP` 10.1.2 patch. This advisory describes inspected source and validation performed; it does not establish that a package has been published. + +> [!WARNING] +> `Codebelt.Coverlet.MTP` 10.0.1, 10.1.0, and 10.1.1 can silently omit library assemblies when automatically generated dependency exclusions match a shared assembly-name prefix. Tests and CI can pass, and Codecov or Sonar can display a high percentage for the small amount of code that remains. Check the expected modules and source files, not just the percentage. The 10.1.2 patch corrects this filtering and a separate framework assembly-resolution defect reproduced with Cuemon on .NET Framework 4.8. The two defects have different upstream origins and failure conditions. + +## Affected scope + +The defect concerns automatically generated MTP exclusions. It is not limited to Codebelt names or C# namespaces: it matches assembly names, normally the DLL filename without its extension. + +With default exclusions, the old implementation reads non-project dependency library names from `*.deps.json` and assemblies loaded in the controller. For names with at least three dot-separated segments, it replaces everything after the second segment with `.*`. Skipping project entries during discovery does not protect their assemblies from another dependency's resulting wildcard. + +| Discovered dependency | Old generated exclusion | Unrelated project assembly matched | +| --- | --- | --- | +| `Codebelt.Extensions.Xunit` | `[Codebelt.Extensions.*]*` | `Codebelt.Extensions.BenchmarkDotNet.dll` | +| `Example.Testing.Infrastructure` | `[Example.Testing.*]*` | `Example.Testing.Product.dll` | + +This does not mean every library, every Codebelt assembly, or every use of an affected version loses coverage. The discovered names, production assembly names, and effective configuration determine the result. A report may lose some modules or become entirely empty. Cuemon's successful coverage is compatible with this failure: the Codebelt example's wildcard does not match `Cuemon.*` assemblies. + +An explicit `--coverlet-exclude` option bypasses dynamic discovery and merges the supplied filters with the permanent baseline. An authoritative configuration file also bypasses dynamic discovery. Those precedence rules are preserved. Explicit include filters do not override a matching exclusion. + +## Origin and affected versions + +The prefix-generating code originated in [upstream PR #1946](https://github.com/coverlet-coverage/coverlet/pull/1946), [commit `c5022539cfdf3c6246c2e49b7789d7e8e529ef47`](https://github.com/coverlet-coverage/coverlet/commit/c5022539cfdf3c6246c2e49b7789d7e8e529ef47), whose source-commit date is 2026-06-01. The change combined loaded-assembly discovery with `deps.json` discovery and introduced wildcard expansion after the second name segment. The Codebelt fork inherited that implementation; package branding did not introduce it. + +| Codebelt package version | Release date recorded in this repository | Evidence for this defect | +| --- | --- | --- | +| 10.0.1 | 2026-09-20 | Tagged source already contains the broad filters. No full consumer rerun against this package was performed. | +| 10.1.0 | 2026-09-27 | Tagged source retains the same logic. No full consumer rerun against this package was performed. | +| 10.1.1 | 2026-10-20 | Tagged source retains the logic; full baseline runs reproduced missing modules in the seven repositories below. | +| 10.1.2 | Patch target in this branch | Exact dependency assembly filters and their validation are corrected; local fixed-collector runs are recorded below. | + +Source anchors: [10.0.1 helper](https://github.com/codebeltnet/coverlet/blob/v10.0.1/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs), [10.1.0 helper](https://github.com/codebeltnet/coverlet/blob/v10.1.0/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs), and [10.1.1 helper](https://github.com/codebeltnet/coverlet/blob/v10.1.1/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs). Release dates come from the [changelog](../CHANGELOG.md), not inferred tag-creation times. Identically numbered upstream `coverlet.MTP` and Codebelt packages must not be treated as the same release. The fork's original upstream marker and its later release/comparison commits are distinguished in [UPSTREAM.md](../UPSTREAM.md). + +## Historical BenchmarkDotNet evidence + +The [2026-10-22 BenchmarkDotNet CI run](https://github.com/codebeltnet/benchmarkdotnet/actions/runs/39814229489), at commit `5cfb096dee8c2c9369e8651ac0d937d4df7ce8ff`, succeeded. Its [Codecov commit record](https://api.codecov.io/api/v2/github/codebeltnet/repos/benchmarkdotnet/commits/5cfb096dee8c2c9369e8651ac0d937d4df7ce8ff/) reports 92.3% coverage, but contains only `tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs`: 12 covered lines out of 13, with no library `src/` files. + +The downloaded `TestResults-Release-Linux-X64-` artifact establishes that the omission happened before Codecov ingestion. Its .NET 10 and .NET 11 OpenCover reports have zero modules for the main unit tests, console unit tests, and console functional tests. The main functional tests report only `dotnet-bdn-runner`, with 11 of 12 sequence points visited in that Linux build. Its assembly name does not match `[Codebelt.Extensions.*]*`, explaining why a nonempty report survived. + +An [earlier Codecov record](https://api.codecov.io/api/v2/github/codebeltnet/repos/benchmarkdotnet/commits/87a13e5b7162fb35c417658976a421a53e4d5683/) includes both libraries and the runner. These are distinct compilation/reporting snapshots; their totals demonstrate lost report scope, not a controlled percentage comparison. + +| Recorded commit | Files | Measured lines | Covered lines | Coverage | +| --- | ---: | ---: | ---: | ---: | +| `87a13e5b7162fb35c417658976a421a53e4d5683` | 7 | 389 | 355 | 91.25% | +| `5cfb096dee8c2c9369e8651ac0d937d4df7ce8ff` | 1 | 13 | 12 | 92.3% | + +## Correction and compatibility + +The 10.1.2 correction reads actual managed runtime asset filenames from non-project `deps.json` libraries, including RID-specific assets and packages containing multiple assemblies. Package IDs no longer stand in for assembly names. Both dependency-derived names and loaded assembly names produce exact assembly filters; filter pruning still removes exact filters already covered by an intentional permanent baseline wildcard. + +Project runtime assets are removed from dependency-derived exclusions even if the SDK lists the same DLL again in a non-project reference entry. Expanded BenchmarkDotNet functional validation exposed this compatibility case for `dotnet-bdn-runner`: the duplicate reference must not suppress a DLL already identified as project output. Reference-only external assets remain eligible for dependency exclusion. + +Native assets and `_._` placeholders do not become managed assembly exclusions. Exact defaults also retain the known xUnit build assets copied without runtime dependency entries: `xunit.abstractions`, `xunit.runner.visualstudio.testadapter`, `xunit.v3.runner.utility.netcore`, and `xunit.v3.runner.utility.netfx`. The filter validator now accepts hyphenated names such as `xunit.v3.mtp-v2`, rather than silently discarding those exact filters as invalid. + +The patch preserves package identity, assembly identity, public API surface, dependency versions, and the `netstandard2.0;net9.0;net10.0` package targets. It restores the intended separation between dependency assemblies and project assemblies. The planned major release's framework changes are outside this hotfix. The patch classification does not promise identical report contents: restored modules can change totals, percentages, coverage thresholds, and quality-gate outcomes. Review those changes rather than automatically lowering a threshold to preserve an old result. + +Existing wildcard syntax and CLI/configuration precedence remain intact. An explicitly configured broad wildcard still excludes every matching module. Hyphenated filter expressions are now honored where the previous validator rejected them. + +## Consumer verification on 2026-11-03 + +The comparison used Windows x64 and .NET 10 Release builds of isolated copies of the available local consumer checkouts, including their existing edits. These are not pristine release-tag builds. All discovered test projects in the seven repositories were run. Original checkouts were not edited. The released 10.1.1 collector was the baseline; only `coverlet.core.dll` and `coverlet.MTP.dll` were replaced for fixed runs. Exclusions were unchanged. OpenCover was selected to inspect visited sequence points. + +| Repository | Test projects | Tests passing with each collector | Baseline library modules | Fixed library modules | Fixed visited / total sequence points | +| --- | ---: | ---: | ---: | ---: | ---: | +| BenchmarkDotNet | 4 | 171 | 0 | 2 | 240 / 257 | +| Carter | 6 | 39 | 0 | 5 | 52 / 52 | +| Globalization | 1 | 1,223 | 0 | 1 | 121 / 121 | +| ASP.Versioning | 2 | 237 | 0 | 1 | 383 / 387 | +| Swashbuckle.AspNetCore | 4 | 121 | 0 | 2 | 779 / 782 | +| Newtonsoft.Json | 3 | 151 | 0 | 3 | 637 / 638 | +| AWS Signature V4 | 1 | 1 | 0 | 1 | 65 / 65 | +| Total | 21 | 1,943 | 0 | 15 | 2,277 / 2,302 | + +The table counts library modules and their sequence points. BenchmarkDotNet also retains `dotnet-bdn-runner` coverage, with 11 of 12 sequence points visited in its functional test report. Its main library unit-test report alone has 173 of 194 sequence points visited. For repositories with multiple test projects, sequence points are deduplicated by module, source file, method, and source span; visited points are the union across their reports. They are not sums of repeated module totals. These results confirm module inclusion and executed code, not that every line must be covered. Swashbuckle's first long-path temporary copy triggered a Windows XML/URI path error with both collectors; the reported comparison was repeated successfully from a short path. + +All 21 projects were rerun after the separate resolver correction described below. All 1,943 tests passed again, and each project's module names, total sequence points, and visited sequence points matched its preceding filter-corrected report. Those reruns are retained in each project's `fixed-resolver` directory, with `resolver-consumer-summary.json` recording the comparison under the local validation-artifact root. + +Local snapshot provenance (all include pre-existing working-tree edits): BenchmarkDotNet `c95c59c`, Carter `bb8e6b5`, Globalization `af2cf95`, ASP.Versioning `b79c017`, Swashbuckle.AspNetCore `3393a7c`, Newtonsoft.Json `c061a0b`, and AWS Signature V4 `cc77402`. A commit identifier alone does not reproduce those edited snapshots. Build logs, test logs, snapshot metadata, and raw reports are retained locally under `artifacts/dependency-exclusion-validation`; they are ignored validation artifacts, not files shipped in the package. + +## Separate Cuemon .NET Framework instrumentation fix + +> [!IMPORTANT] +> The dependency-prefix correction alone did not repair Cuemon's .NET Framework coverage. A second correction in 10.1.2 fixes the reproduced assembly-resolution failure. Successful test execution alone concealed both defects. + +An isolated `Cuemon.Core.Tests` .NET Framework 4.8 run passed all 729 tests with both released 10.1.1 and the collector containing only the dependency-filter correction. Both reports omitted `Cuemon.Core`, `Cuemon.Kernel`, and `Cuemon.Extensions.Core`. Diagnostics showed `Mono.Cecil.AssemblyResolutionException` while writing metadata for optional enum parameters: `Cuemon.Core.dll` and `Cuemon.Extensions.Core.dll` could not resolve `System, Version=4.0.0.0`, and `Cuemon.Kernel.dll` could not resolve `mscorlib, Version=4.0.0.0`. The collector restored the modules after those instrumentation failures and continued running tests. The resulting reports had only 832 visited sequence points out of 4,286 across the remaining modules. + +The runtime `netstandard.dll` facade was cached globally as a Cecil assembly loaded without `ReaderParameters.AssemblyResolver`. Resolving a forwarded enum therefore used Cecil's default resolver instead of Coverlet's configured runtime/reference-assembly fallbacks. The correction loads the same runtime facade lazily per resolver with `AssemblyResolver = this` and registers it in the resolver's cache for disposal. Official facade identity checks, framework search order, and exclusion configuration are preserved; there are no Cuemon-specific paths or names in production code. + +That unbound facade-loading pattern is already present in upstream [PR #625](https://github.com/coverlet-coverage/coverlet/pull/625), [commit `3856c76a5627a8d38d2cf2eae9360f05f7d878c5`](https://github.com/coverlet-coverage/coverlet/commit/3856c76a5627a8d38d2cf2eae9360f05f7d878c5), whose source-commit date is 2019-12-03. This establishes source provenance, not the first failing package release or that every historical .NET Framework workload failed. It is distinct from dependency-filter PR #1946. The concrete failure was reproduced against released Codebelt 10.1.1 and the intermediate filter-only correction. + +After replacing only the two `netstandard2.0` collector DLLs with the resolver-corrected build, the same 729 tests passed with unchanged exclusion settings. Diagnostics contained no module-instrumentation failures. The three previously missing library modules appeared with executed sequence points: + +| Module | Before resolver correction | Fixed visited / total sequence points | +| --- | --- | ---: | +| `Cuemon.Core` | Missing | 2,194 / 2,825 | +| `Cuemon.Kernel` | Missing | 465 / 1,245 | +| `Cuemon.Extensions.Core` | Missing | 205 / 692 | + +That snapshot is based on local Cuemon commit `9d5e7197` plus existing edits. It was built with SDK `11.0.100-rc.3.26522.148`; `MinVerVersionOverride=11.0.0-rc.3` retained the assembly major version expected by its tests in the Git-free copy. An initial build without that override failed one assembly-version assertion with either collector; the versioned comparison above passed all tests. This check covers one Cuemon test project on Windows x64, not its entire .NET Framework suite. Logs and reports for the focused reproduction are retained under `artifacts/dependency-exclusion-validation/consumers/cuemon-net48-versioned/resolver-before` and `resolver-after`, with adjacent test logs. + +## Regression protection and validation limits + +[CoverageConfigurationDependencyDiscoveryTests](../test/coverlet.MTP.tests/Configuration/CoverageConfigurationDependencyDiscoveryTests.cs) exercises real `deps.json` reading, dynamic configuration, production filter validation, and module selection. It covers the reported Codebelt names, a generic shared prefix, differing package/assembly names, multiple runtime assemblies, RID-specific assets, duplicate project/reference entries, hyphens, infrastructure exclusions, and explicit CLI/configuration wildcard behavior. [ProcessAssemblyHelperTests](../test/coverlet.core.tests/Helpers/ProcessAssemblyHelperTests.cs) and [InstrumentationHelperTests](../test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs) cover discovery, exact filters, pruning, and filter validity. + +[CecilAssemblyResolverTests](../test/coverlet.core.tests/Instrumentation/CecilAssemblyResolverTests.cs) resolves real runtime facade forwards through a private framework search directory for `StringComparison` and `MidpointRounding`, then writes a consumer assembly containing an optional enum parameter. The original resolver failed the configured-path assertions; the corrected resolver passes both cases. This regression exercises Cecil's real resolution and metadata-writing path without requiring Cuemon or a .NET Framework installation. + +On Windows, after the resolver correction, the complete core suite passed 351 tests with nine platform skips, and the complete MTP suite passed 427 tests with seven platform skips, on each of .NET 9 and .NET 10. The `netstandard2.0` build passed without warnings or errors. The consumer checks above used replacement collector DLLs, not installation of a newly packed NuGet package. Fixed-consumer runs on Linux/macOS and .NET 11 were not performed; the historical .NET 11 reports establish the earlier failure only. These results do not assert that all possible coverage failures are resolved. + +## Ubuntu remote validation on 2026-11-03 + +The current working tree, including the resolver correction and its new tests, was built and tested in an isolated Linux x64 Docker workspace through the `dotnet-remote-testing` skill. The runner automatically selected `codebeltnet/ubuntu-testrunner:8-9-10-11` to cover the discovered framework set, pinned to digest `sha256:1df4b744ff2b2bf215fa2dc63e4f81b3c8dcf7c701c9bd456c78a2ec160713d9`. The executed test projects target .NET 9 and .NET 10; the image's additional SDKs do not establish execution on those other frameworks. The observed build/test SDK was 10.0.403. The run used the root `coverlet.slnx` solution and Debug configuration without modifying project files or package references. + +| Test project | .NET 9 passed / skipped / failed | .NET 10 passed / skipped / failed | +| --- | ---: | ---: | +| `coverlet.core.tests` | 347 / 13 / 0 | 347 / 13 / 0 | +| `coverlet.MTP.tests` | 417 / 17 / 0 | 417 / 17 / 0 | +| `coverlet.core.coverage.tests` | 95 / 0 / 0 | 95 / 0 / 0 | +| `coverlet.MTP.validation.tests` | 2 / 6 / 36 | 2 / 6 / 36 | + +Both forwarded-enum resolver cases passed on each framework. All six real dependency-discovery/filter-application cases also passed on each framework, including the exact `Codebelt.Extensions.Xunit` / `Codebelt.Extensions.BenchmarkDotNet` reproduction, generic names, and explicit CLI/configuration wildcards. The three core/MTP/instrumentation suites passed 1,718 tests in total with 60 platform skips. + +The entire solution run was not green: its aggregate result was 1,722 passed, 72 skipped, and 72 failed, with exit code 1. Every failure was in package validation because `/workspace/artifacts/package/debug` was absent from the isolated workspace. For example, `CollectCoverageTests.BasicCoverage_CollectsDataForCoveredLines` failed on both frameworks with `DirectoryNotFoundException` in `MtpValidationTestBase.GetCoverletMtpPackageVersion`, which requires a package produced by `dotnet pack` first. CI supplies that package in a separate pack/download phase; the remote runner performs restore, build, and test. This run does not validate installation or use of a newly packed package. + +The runner reported 547.2 seconds of cumulative test time and 325.7 seconds of wall time including preparation, restore, and build; parallel execution accounts for cumulative test time exceeding wall time. Runner output and the retained core, MTP, and package-validation TRX files are under `artifacts/dependency-exclusion-validation/ubuntu-remote`. Cuemon's actual .NET Framework 4.8 execution remains the Windows check described above; the portable resolver regression was verified on Linux. Modern consumer repositories were not rerun in this container. + +## Upgrade and report review + +1. When the 10.1.2 package is available, update the test projects that use an affected `Codebelt.Coverlet.MTP` version and regenerate coverage. +2. Confirm each expected library module appears in the raw report with visited sequence points, and check the included source-file list before accepting an aggregate percentage. For JSON, inspect module keys; for OpenCover, inspect `Modules/Module`, source files, and visited sequence points. +3. Re-evaluate Codecov/Sonar totals and coverage gates against the restored module set. Existing historical uploads are not repaired by upgrading the collector. + +If upgrading is deferred, an explicitly reviewed CLI exclusion set or authoritative configuration file can bypass dynamic defaults. In that mode, maintain the required infrastructure/external-dependency exclusions yourself; simply adding an include filter does not override the bad generated exclusion. See [configuration precedence and default exclusions](Coverlet.MTP.Integration.md#default-exclusions-behavior). + +Implementation provenance and full commit identifiers are recorded in [UPSTREAM.md](../UPSTREAM.md#2026-11-03-dependency-exclusion-correction-for-1012). diff --git a/README.md b/README.md index e7b98d082..85c6566f1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Cross-platform code coverage for .NET with explicit TFM support and framework-al `Codebelt.Coverlet.MTP` is a deliberately narrow Codebelt-maintained fork of [Coverlet](https://github.com/coverlet-coverage/coverlet) for Microsoft Testing Platform integration. Coverlet remains the upstream project; this repository keeps the Coverlet engine and MTP integration recognizable while focusing package identity, target frameworks, dependency groups, and CI/CD on the Codebelt-maintained MTP package. +> [!WARNING] +> Coverage reports from versions 10.0.1 through 10.1.1 can silently omit library assemblies when generated dependency exclusions match a shared assembly-name prefix. Passing tests and a high coverage percentage do not establish that the expected library modules were measured. See the [10.1.2 coverage advisory](Documentation/DependencyExclusionRegression.md) for affected configurations, upstream origins, verified consumer results, upgrade checks, and the separate .NET Framework instrumentation fix validated with Cuemon. + ## Scope This repository produces one public NuGet package: `Codebelt.Coverlet.MTP`. It preserves the implementation assembly name and namespace used by the MTP extension (`coverlet.MTP.dll`, `Coverlet.MTP`, and `Coverlet.MTP.TestingPlatformBuilderHook`) so existing Microsoft Testing Platform discovery remains compatible. @@ -89,6 +92,8 @@ See [Coverlet.MTP.Integration.md](Documentation/Coverlet.MTP.Integration.md) for This fork is based on [coverlet-coverage/coverlet](https://github.com/coverlet-coverage/coverlet). The intent is not to replace Coverlet or rewrite its instrumentation internals; the fork exists to provide explicit target-framework support and framework-aligned NuGet dependency groups for the Microsoft Testing Platform package. +See [UPSTREAM.md](UPSTREAM.md) for verified provenance and intentional behavior differences. + For Coverlet drivers outside this repository's scope, such as the console tool, VSTest collector, or MSBuild integration, use the upstream Coverlet packages and documentation. ## License and notices diff --git a/UPSTREAM.md b/UPSTREAM.md new file mode 100644 index 000000000..8e2cd010e --- /dev/null +++ b/UPSTREAM.md @@ -0,0 +1,85 @@ +# Upstream provenance + +This ledger records the upstream origin and retained behavior relevant to the 10.1.2 hotfix branch. It is not a claim of a new upstream synchronization or a reconstruction of unreviewed history. Upstream is [coverlet-coverage/coverlet](https://github.com/coverlet-coverage/coverlet), branch `master`. + +## Historical anchors + +| Reference | Exact target | Meaning | +| --- | --- | --- | +| Original upstream marker `v10.0.1-coverlet` | `7fbbe4e72b06b0a6fe3932f65944c7627ecf6b17` | Original marker retained by this fork; distinct from the later Codebelt release with a similar version number. | +| Codebelt `v10.0.1` | `97668061b72e03668de55d6cca39a559c1d3ff10` | Fork release whose source already includes the later upstream dependency-exclusion implementation. | +| Codebelt `v10.1.1` | `5bd86483a104328783255960116c1fb87ce2b35d` | Baseline used to compare this hotfix's implementation and unchanged package/framework declarations. | + +These are verified commit targets. No tag-creation dates, earlier synchronization dates, or historical test outcomes are inferred from them. The 10.0.1 release date in the changelog is not a tag-creation timestamp. + +## 2026-11-03: prepare the 10.x maintenance line + +Operation date: 2026-11-03. The selected release strategy keeps `master` on the 11.0.0 development line and uses `releases/v10` for 10.x maintenance, matching the branch naming in the GitHub Actions versioning example. The maintenance branch was created at the exact `v10.1.1` target, `5bd86483a104328783255960116c1fb87ce2b35d`, with identical tree content, and renamed from the provisional local `release/10` to `releases/v10`. The branch is now published in `codebeltnet/coverlet`, and [PR #9](https://github.com/codebeltnet/coverlet/pull/9) targets it. The existing active branch ruleset was extended to include `refs/heads/releases/v10`, preserving the same pull-request, deletion, and non-fast-forward protections without changing the default branch's rules. + +At inspection, `master` remains at `0e2e2fe394c2ea6343b9b228382f1dbd2ef165c3`, containing the earlier 11.0.0 work and ancestry-preserving upstream integration from PR #7. The existing `v11.0.0/double-digits-chore` branch, at `922b11cd71fccd97ce3fc8e369beb71fad4bb81d` and used by PR #8, contains that complete master history plus its subsequent work. Neither branch is reset, reverted, or rewritten by this maintenance preparation. Their detailed synchronization record remains in the 11.0.0 line's `UPSTREAM.md`. + +PR #9's 13 existing commits are the hotfix work based on `v10.1.1`; the newer upstream and .NET 11 commits are already in `master`, not additional hotfix commits. Against the selected maintenance base, the hotfix has no merge conflicts and retains the tested 10.1.1 package/framework baseline. The workflow filter is extended to accept `releases/v10` as well as `master`. Future 10.x fixes must be forward-ported to the 11.0.0 line separately; no forward-port, upstream adoption, package release, or publication is performed by this branch preparation. + +The existing manual workflow can run against `releases/v10` or a fixed release tag. The `Production` environment has no branch restriction and retains its required reviewer. A stable 10.1.2 release requires its fixed `v10.1.2` tag on the selected commit before the run; otherwise MinVer calculates a prerelease version. This setup does not create a release tag, approve deployment, or publish a package. + +The earlier [manual run 41717154404](https://github.com/codebeltnet/coverlet/actions/runs/41717154404), on hotfix commit `a06eac4b88f682bdc838472bfc4b9b488d13f076`, passed the build, release-version guard, and all six Windows/Ubuntu test jobs. Its three macOS jobs were cancelled without acquiring a runner. Their annotations report hosted-runner acquisition failure and macOS arm64 capacity constraints; no macOS test execution occurred. Packing, package validation, and publication were skipped because the test matrix did not complete successfully. This is a partial CI result caused by runner allocation, not evidence of a failing macOS test or successful package validation. + +Following the same queue behavior in [PR run 41722023562](https://github.com/codebeltnet/coverlet/actions/runs/41722023562), the user selected the opt-in macOS approach used by Codebelt xUnit. The new manual `run_mac_tests` boolean defaults to false. Windows and Ubuntu remain in both the test and package-validation matrices for every run; macOS joins both matrices when explicitly enabled. Existing matrix dependencies still block packing or publication if any selected job fails or is cancelled. The default run therefore does not validate macOS. This is a user-requested CI scheduling change, not an upstream integration or a product-support change. + +## 2026-11-03: dependency exclusion correction for 10.1.2 + +Operation date: 2026-11-03. This is a fork-local correction of inherited behavior, not an upstream merge. The hotfix comparison base is Codebelt `v10.1.1` (`5bd86483a104328783255960116c1fb87ce2b35d`). + +### Origin and source comparison + +The defect originates in upstream [PR #1946](https://github.com/coverlet-coverage/coverlet/pull/1946), commit [`c5022539cfdf3c6246c2e49b7789d7e8e529ef47`](https://github.com/coverlet-coverage/coverlet/commit/c5022539cfdf3c6246c2e49b7789d7e8e529ef47). Its source-commit date is 2026-06-01. That implementation converts package IDs and loaded assembly names with at least three segments into wildcards after the second segment, allowing dependency exclusions to match project assemblies with the same prefix. It is inherited in the inspected Codebelt 10.0.1, 10.1.0, and 10.1.1 tag targets. + +A targeted comparison also inspected upstream [PR #2069](https://github.com/coverlet-coverage/coverlet/pull/2069), commit [`f37bf3d1ea8795cc1bdc57b29ad2ec81d10c9f1a`](https://github.com/coverlet-coverage/coverlet/commit/f37bf3d1ea8795cc1bdc57b29ad2ec81d10c9f1a), source-commit date 2026-11-03. That snapshot narrows loaded-assembly filters and accepts hyphens, while retaining the package-name wildcard route for `deps.json`. This is a comparison of the relevant paths at a pinned commit, not a full review or integration of that upstream snapshot. + +### Retained fork behavior and destination commits + +The Codebelt correction resolves `deps.json` entries to their managed runtime assembly filenames, including multiple assemblies and RID-specific assets, and uses exact assembly filters for both discovery sources. Native assets and placeholders are skipped. Known xUnit build assets without runtime entries retain exact default exclusions. Hyphenated assembly filters are valid. Existing CLI/configuration precedence and intentionally broad permanent baseline filters remain in effect. + +Consumer verification also found SDK metadata listing the same runner DLL as both a project and a reference. The follow-up in [ProcessAssemblyHelper](src/coverlet.core/Helpers/ProcessAssemblyHelper.cs) gives project runtime assets precedence over duplicate dependency entries, with regression coverage in the discovery test. It is recorded in destination commit `c65e2d357204e573eae59401a619252da6ec8075`. + +This differs from the inspected upstream `f37bf3d` dependency-filter behavior because package-name wildcards still remove unrelated project modules. The reason and regression evidence are documented in the [coverage advisory](Documentation/DependencyExclusionRegression.md), [discovery regression](test/coverlet.MTP.tests/Configuration/CoverageConfigurationDependencyDiscoveryTests.cs), and [filter tests](test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs). + +| Destination commit in this branch | Role | +| --- | --- | +| `86dba83dcbdd11054e7a6f6ab398ca4124c755c2` | Implements exact runtime-asset exclusions, retained xUnit build exclusions, filter validation, and regression tests. | +| `c65e2d357204e573eae59401a619252da6ec8075` | Preserves project runtime assets also listed in non-project reference entries. | +| `13b23354cebef95040bb5218a9fe5eb6f1de35f4` | Updates the integration guide for the corrected discovery behavior. | +| `7fc6b11d4535d29f400399506b762a976caee7b8` | Records the initial 10.1.2 changelog draft; this advisory expands that draft. | + +No other changes from upstream #2069 are claimed as adopted. This operation does not merge or alter upstream ancestry, move tags, change package/assembly identities, alter dependencies, or introduce the planned major release's framework changes. This ledger's documentation additions remain pending until integrated by the repository's normal process. + +### Validation and remaining work + +- Windows x64: complete MTP tests passed 427 tests with seven platform skips on each of `net9.0` and `net10.0`; focused core discovery/filter tests passed 121 tests on each framework. +- Build-only: `netstandard2.0` MTP/core build passed with zero warnings and errors. +- Windows x64 consumer comparisons: 21 .NET 10 test projects across BenchmarkDotNet, Carter, Globalization, ASP.Versioning, Swashbuckle.AspNetCore, Newtonsoft.Json, and AWS Signature V4 passed 1,943 tests with each collector. The released baseline omitted all 15 library modules represented by these runs; the corrected collector included them with visited sequence points and retained BenchmarkDotNet's runner coverage. See the advisory for the exact scope and per-repository totals. +- The initial Cuemon .NET Framework 4.8 comparison passed 729 tests with each collector but still failed framework assembly resolution for three library modules. The separate resolver correction and successful revalidation are recorded below. +- Fixed-consumer Linux/macOS and .NET 11 execution, installation of a newly packed package, remote CI for this hotfix, and package publication have not been established by these local checks. Historical CI/report inspection is recorded separately from fixed-collector validation in the advisory. + +The original bug is recorded in inherited ancestry; the correction is implemented and locally tested in the destination commits above. That does not establish that a synchronization PR is merged, remote CI is green, or 10.1.2 has been released. No release or publish operation is authorized by this ledger. + +## 2026-11-03: runtime facade resolver correction for 10.1.2 + +Operation date: 2026-11-03. This is a second fork-local correction using the same Codebelt `v10.1.1` comparison base (`5bd86483a104328783255960116c1fb87ce2b35d`), not an upstream synchronization. The earlier dependency-filter correction alone did not repair the Cuemon .NET Framework failure. + +### Origin and retained behavior + +The relevant source pattern is present in upstream [PR #625](https://github.com/coverlet-coverage/coverlet/pull/625), commit [`3856c76a5627a8d38d2cf2eae9360f05f7d878c5`](https://github.com/coverlet-coverage/coverlet/commit/3856c76a5627a8d38d2cf2eae9360f05f7d878c5), source-commit date 2019-12-03: the runtime `netstandard.dll` facade is read into a static Cecil assembly without its owning custom resolver. This is source provenance, not a claim that every package or workload since 2019 fails. The concrete missing-module failure was reproduced with released Codebelt 10.1.1 and the intermediate filter-only correction. It has a different origin from dependency-filter PR #1946. + +The correction in [CecilAssemblyResolver](src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs) loads the same runtime facade lazily per resolver with `ReaderParameters.AssemblyResolver` set to that resolver. Registering the facade in the base cache gives it the same disposal lifetime. Forwarded enum types can then use the existing framework runtime/reference-assembly fallbacks and configured search directories when Cecil writes optional-parameter metadata. Official facade identity checks, fallback order, CLI/configuration behavior, dependencies, and package targets are preserved. There are no Cuemon-specific production branches. [CecilAssemblyResolverTests](test/coverlet.core.tests/Instrumentation/CecilAssemblyResolverTests.cs) exercises real facade forwarding and optional enum metadata writing for two enum types; both configured-path assertions failed before the correction and pass after it. + +This correction differs from the inherited static, unbound facade behavior for the reason and reproduction above. Its source and regression tests are in the working tree; no destination commit, upstream adoption, remote CI result, or released package is claimed yet. No upstream commits were merged or ancestry changed by this operation. + +### Validation and limits + +- Windows x64, isolated Cuemon `Cuemon.Core.Tests` on .NET Framework 4.8: all 729 tests passed before and after. Before the resolver correction, `Cuemon.Core`, `Cuemon.Kernel`, and `Cuemon.Extensions.Core` were missing after framework assembly-resolution failures. After it, there were no module-instrumentation failures, and those modules respectively contained 2,194/2,825, 465/1,245, and 205/692 visited/total sequence points. Exclusion settings and consumer assemblies were unchanged between runs; only the two collector DLLs were replaced. +- Windows x64, complete core suite: 351 tests passed with nine platform skips on each of `net9.0` and `net10.0`; complete MTP suite: 427 tests passed with seven platform skips on each framework. +- Build-only: MTP/core `netstandard2.0` build passed with zero warnings and errors. +- Compatibility rerun after the resolver correction: all 1,943 tests in the 21 .NET 10 consumer projects passed again. Per-project module names and visited/total sequence points were unchanged from their preceding filter-corrected reports, including BenchmarkDotNet's runner. +- Ubuntu remote verification: the current working tree was built and tested using the `dotnet-remote-testing` skill in `codebeltnet/ubuntu-testrunner:8-9-10-11`, pinned to `sha256:1df4b744ff2b2bf215fa2dc63e4f81b3c8dcf7c701c9bd456c78a2ec160713d9`. On each of .NET 9 and .NET 10, core passed 347 tests with 13 skips, MTP passed 417 with 17 skips, and coverage instrumentation passed 95 with no skips. All resolver and real dependency-discovery regressions passed. The complete solution result was 1,722 passed, 72 skipped, and 72 failed: all 72 failures were package-validation tests requiring the absent `/workspace/artifacts/package/debug` fixture. This establishes the core/MTP/instrumentation results, not successful packaged installation. Cumulative test time was 547.2 seconds; total wall time was 325.7 seconds. See the [Ubuntu validation record](Documentation/DependencyExclusionRegression.md#ubuntu-remote-validation-on-2026-11-03) for details and artifact locations. +- The Cuemon check covers one test project, not its entire framework suite. Snapshot provenance, build settings, raw-report locations, and remaining platform/package validation limits are recorded in the [coverage advisory](Documentation/DependencyExclusionRegression.md#separate-cuemon-net-framework-instrumentation-fix). diff --git a/src/coverlet.MTP/Configuration/CoverageConfiguration.cs b/src/coverlet.MTP/Configuration/CoverageConfiguration.cs index e5f18a0af..aec525896 100644 --- a/src/coverlet.MTP/Configuration/CoverageConfiguration.cs +++ b/src/coverlet.MTP/Configuration/CoverageConfiguration.cs @@ -131,9 +131,7 @@ public string[] GetExcludeFilters() return _configFileSettings.ExcludeFilters; } - // Priority 3: Dynamic defaults derived from the assemblies already loaded into this - // process. Because only loaded assemblies are inspected, transitive dependencies that - // have not yet been resolved are never included in the list. + // Priority 3: Dynamic defaults from dependency runtime assets and loaded assemblies. string[] dynamicDefaults = BuildDynamicExcludeFilters(); LogOptionValue(CoverletOptionNames.Exclude, dynamicDefaults, isExplicit: false); return dynamicDefaults; @@ -146,7 +144,8 @@ public string[] GetExcludeFilters() /// /// /// deps.json — reads *.deps.json next to the test module and converts - /// every non-project RuntimeLibrary entry into a filter pattern. Covers + /// managed runtime assets of non-project RuntimeLibrary entries into exact + /// assembly filters. Covers /// test-host infrastructure (e.g. xunit.v3.mtp-v2, testhost, /// Microsoft.TestPlatform.*) that is only loaded inside the test-host process, /// not in the controller process where instrumentation runs. @@ -187,7 +186,11 @@ private string[] BuildDynamicExcludeFilters() // such as Microsoft.Testing.Extensions.MSBuild that are held open before instrumentation). IReadOnlyList loadedNames = _processAssemblyHelper.GetLoadedAssemblyNames(simpleTestName); + // xUnit's runner and VSTest adapter copy these build assets without declaring + // runtime entries in deps.json. Keep their infrastructure exclusions exact as well. IEnumerable dynamicFilters = depsNames.Concat(loadedNames) + .Concat(["xunit.abstractions", "xunit.runner.visualstudio.testadapter", + "xunit.v3.runner.utility.netcore", "xunit.v3.runner.utility.netfx"]) .Select(Coverlet.Core.Helpers.ProcessAssemblyHelper.ToExcludeFilter); IEnumerable merged = s_baselineExcludeFilters diff --git a/src/coverlet.core/Abstractions/IProcessAssemblyHelper.cs b/src/coverlet.core/Abstractions/IProcessAssemblyHelper.cs index 1a712c67f..b33cf650f 100644 --- a/src/coverlet.core/Abstractions/IProcessAssemblyHelper.cs +++ b/src/coverlet.core/Abstractions/IProcessAssemblyHelper.cs @@ -21,8 +21,9 @@ internal interface IProcessAssemblyHelper IReadOnlyList GetLoadedAssemblyNames(string testAssemblyName); /// - /// Returns the simple assembly names of all non-project NuGet package entries - /// declared in the *.deps.json files located next to the test module. + /// Returns the simple assembly names from managed runtime assets of non-project libraries + /// declared in the *.deps.json files located next to the test module, + /// including RID-specific assets. Package IDs are not used as assembly names. /// These entries represent test-host infrastructure that is not yet loaded into /// the controller process when is called. /// diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 4ad9a4e57..9680f0b7d 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -426,7 +426,7 @@ public bool IsValidFilterExpression(string filter) if (filter.EndsWith("]")) return false; - if (new Regex(@"[^\w*]", s_regexOptions, TimeSpan.FromSeconds(10)).IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", ""))) + if (new Regex(@"[^\w*-]", s_regexOptions, TimeSpan.FromSeconds(10)).IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", ""))) return false; return true; diff --git a/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs b/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs index 559e6ea49..a417bf4d7 100644 --- a/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs +++ b/src/coverlet.core/Helpers/ProcessAssemblyHelper.cs @@ -38,6 +38,7 @@ public IReadOnlyList GetDepsJsonAssemblyNames(string testModuleDirectory throw new ArgumentException("Test assembly name must not be null or whitespace.", nameof(testAssemblyName)); var names = new HashSet(StringComparer.OrdinalIgnoreCase); + var projectNames = new HashSet(StringComparer.OrdinalIgnoreCase); using var reader = new DependencyContextJsonReader(); foreach (string depsFile in Directory.EnumerateFiles(testModuleDirectory, "*.deps.json")) @@ -49,14 +50,26 @@ public IReadOnlyList GetDepsJsonAssemblyNames(string testModuleDirectory foreach (RuntimeLibrary lib in context.RuntimeLibraries) { - // Skip the test project itself; we only want infrastructure packages. - if (lib.Type.Equals("project", StringComparison.OrdinalIgnoreCase)) - continue; + bool isProject = lib.Type.Equals("project", StringComparison.OrdinalIgnoreCase); - if (!string.IsNullOrWhiteSpace(lib.Name) && - !lib.Name.Equals(testAssemblyName, StringComparison.OrdinalIgnoreCase)) + // Package IDs need not match their assemblies (for example xunit.v3.mtp-v2). + // Include every managed runtime asset, including RID-specific assemblies. + foreach (string assetPath in lib.RuntimeAssemblyGroups.SelectMany(group => group.AssetPaths)) { - names.Add(lib.Name); + string extension = Path.GetExtension(assetPath); + if (!extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) && + !extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)) + continue; + + string name = Path.GetFileNameWithoutExtension(assetPath); + if (!string.IsNullOrWhiteSpace(name) && + !name.Equals(testAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + if (isProject) + projectNames.Add(name); + else + names.Add(name); + } } } } @@ -66,34 +79,20 @@ public IReadOnlyList GetDepsJsonAssemblyNames(string testModuleDirectory } } + // The SDK can also list a project executable's DLL in a separate reference entry. + // Its project identity takes precedence over that duplicate dependency entry. + names.ExceptWith(projectNames); return [.. names]; } /// /// Converts a simple assembly name to a coverlet exclude-filter pattern. - /// For names with three or more dot-separated segments the third segment onwards is - /// replaced by a wildcard so the filter is resilient to assembly names that differ - /// from the NuGet package name (e.g. dashes vs. dots in the suffix). - /// - /// xunit.v3.mtp-v2 → [xunit.v3.*]* - /// Microsoft.Extensions.Configuration.Binder → [Microsoft.Extensions.*]* - /// coverlet.core → [coverlet.core]* - /// xunit → [xunit]* - /// + /// Matches only that assembly, so dependencies cannot exclude project assemblies + /// merely because they share a name prefix. /// internal static string ToExcludeFilter(string assemblyName) { - int first = assemblyName.IndexOf('.'); - if (first < 0) - return $"[{assemblyName}]*"; - - int second = assemblyName.IndexOf('.', first + 1); - if (second < 0) - return $"[{assemblyName}]*"; // exactly two segments — keep exact - - // Three or more segments: use first two as prefix and wildcard the rest. - string prefix = assemblyName.Substring(0, second); - return $"[{prefix}.*]*"; + return $"[{assemblyName}]*"; } /// diff --git a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs index 6df84950b..82cb13be3 100644 --- a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs +++ b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs @@ -45,9 +45,9 @@ internal class NetstandardAwareAssemblyResolver : DefaultAssemblyResolver private static readonly System.Reflection.Assembly s_netStandardAssembly; private static readonly string s_name; private static readonly byte[] s_publicKeyToken; - private static readonly AssemblyDefinition s_assemblyDefinition; private readonly string _modulePath; + private readonly Lazy _netStandardAssemblyDefinition; private readonly Lazy _compositeResolver; private readonly ILogger _logger; @@ -60,7 +60,6 @@ static NetstandardAwareAssemblyResolver() System.Reflection.AssemblyName name = s_netStandardAssembly.GetName(); s_name = name.Name; s_publicKeyToken = name.GetPublicKeyToken(); - s_assemblyDefinition = AssemblyDefinition.ReadAssembly(s_netStandardAssembly.Location); } catch (FileNotFoundException) { @@ -73,6 +72,16 @@ public NetstandardAwareAssemblyResolver(string modulePath, ILogger logger) _modulePath = modulePath; _logger = logger; + // Forwarded types must retain this resolver's framework fallbacks and search paths. + // Register the per-resolver facade so the base cache also owns its disposal. + _netStandardAssemblyDefinition = new Lazy(() => + { + AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(s_netStandardAssembly.Location, + new ReaderParameters { AssemblyResolver = this }); + RegisterAssembly(assembly); + return assembly; + }, true); + // this is lazy because we cannot create NetCoreSharedFrameworkResolver if not on .NET Core runtime, // runtime folders are different _compositeResolver = new Lazy(() => new CompositeCompilationAssemblyResolver( @@ -117,7 +126,7 @@ public override AssemblyDefinition Resolve(AssemblyNameReference name) { if (CheckIfSearchingNetstandard(name)) { - return s_assemblyDefinition; + return _netStandardAssemblyDefinition.Value; } else { diff --git a/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDependencyDiscoveryTests.cs b/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDependencyDiscoveryTests.cs new file mode 100644 index 000000000..f8fbd0c96 --- /dev/null +++ b/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDependencyDiscoveryTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Coverlet.Core.Abstractions; +using Coverlet.Core.Helpers; +using Microsoft.Testing.Platform.CommandLine; +using Moq; +using Xunit; + +namespace Coverlet.MTP.Configuration.Tests; + +public sealed class CoverageConfigurationDependencyDiscoveryTests +{ + [Theory] + [InlineData("Codebelt.Extensions.Xunit", "Codebelt.Extensions.Xunit", "Codebelt.Extensions.BenchmarkDotNet")] + [InlineData("Example.Testing.Infrastructure", "Example.Testing.Infrastructure", "Example.Testing.Product")] + [InlineData("xunit.v3.mtp-v2", "xunit.v3.core", "xunit.v3.Product")] + [InlineData("Example.Testing.Package", "Example.Testing.test-host", "Example.Testing.Product")] + public void WhenDependencySharesProjectPrefixThenOnlyDependencyAssembliesAreExcluded( + string packageName, string dependencyAssembly, string projectAssembly) + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + try + { + File.WriteAllText(Path.Combine(directory, "MyTests.deps.json"), $$""" + { + "runtimeTarget": { "name": ".NETCoreApp,Version=v10.0" }, + "targets": { + ".NETCoreApp,Version=v10.0": { + "MyTests/1.0.0": { "runtime": { "MyTests.dll": {} } }, + "{{projectAssembly}}/1.0.0": { "runtime": { "{{projectAssembly}}.dll": {} } }, + "{{projectAssembly}}.Reference/1.0.0.0": { + "runtime": { "{{projectAssembly}}.dll": {}, "External.Reference.dll": {} } + }, + "{{packageName}}/1.0.0": { + "runtime": { + "lib/net10.0/{{dependencyAssembly}}.dll": {}, + "lib/net10.0/External.Helper.dll": {} + }, + "runtimeTargets": { + "runtimes/win/lib/net10.0/External.Windows.dll": { "rid": "win", "assetType": "runtime" }, + "runtimes/win/native/External.Native.dll": { "rid": "win", "assetType": "native" } + } + }, + "Empty.Package/1.0.0": { "runtime": { "lib/net10.0/_._": {} } } + } + }, + "libraries": { + "MyTests/1.0.0": { "type": "project", "serviceable": false, "sha512": "" }, + "{{projectAssembly}}/1.0.0": { "type": "project", "serviceable": false, "sha512": "" }, + "{{projectAssembly}}.Reference/1.0.0.0": { "type": "reference", "serviceable": false, "sha512": "" }, + "{{packageName}}/1.0.0": { "type": "package", "serviceable": false, "sha512": "" }, + "Empty.Package/1.0.0": { "type": "package", "serviceable": false, "sha512": "" } + } + } + """); + + var config = new CoverageConfiguration( + Mock.Of(), configFileSettings: null, + testModulePath: Path.Combine(directory, "MyTests.dll")); + var selector = new InstrumentationHelper( + Mock.Of(), Mock.Of(), Mock.Of(), + Mock.Of(), Mock.Of()); + string[] modules = [ + $"{projectAssembly}.dll", $"{dependencyAssembly}.dll", "External.Helper.dll", "External.Windows.dll", "External.Reference.dll", + "Microsoft.VisualStudio.TestPlatform.TestFramework.dll", "testhost.dll", "ReportGenerator.Mtp.dll", + "xunit.abstractions.dll", "xunit.runner.visualstudio.testadapter.dll", + "xunit.v3.runner.utility.netcore.dll", "xunit.v3.runner.utility.netfx.dll" + ]; + + string[] filters = config.GetExcludeFilters(); + Assert.All(filters, filter => Assert.True(selector.IsValidFilterExpression(filter), $"Invalid generated filter: {filter}")); + string[] validFilters = [.. filters.Where(selector.IsValidFilterExpression)]; + string[] selected = [.. selector.SelectModules(modules, config.GetIncludeFilters(), validFilters)]; + + Assert.Contains($"{projectAssembly}.dll", selected); + Assert.Equal([$"{projectAssembly}.dll"], selected); + Assert.DoesNotContain("[_]*", filters); + Assert.DoesNotContain("[External.Native]*", filters); + Assert.DoesNotContain("[Empty.Package]*", filters); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WhenWildcardExplicitlyConfiguredThenMatchingProjectIsStillExcluded(bool useConfigFile) + { + string[] explicitFilters = ["[Example.Testing.*]*"]; + var options = new Mock(); + if (!useConfigFile) + { + string[]? cliFilters = explicitFilters; + options.Setup(x => x.TryGetOptionArgumentList(Coverlet.MTP.CommandLine.CoverletOptionNames.Exclude, out cliFilters)) + .Returns(true); + } + + var config = new CoverageConfiguration(options.Object, + useConfigFile ? new CoverletMTPSettings { IsFromConfigFile = true, ExcludeFilters = explicitFilters } : null, + testModulePath: null); + var selector = new InstrumentationHelper( + Mock.Of(), Mock.Of(), Mock.Of(), + Mock.Of(), Mock.Of()); + + string[] selected = [.. selector.SelectModules( + ["Example.Testing.Product.dll", "Other.Product.dll", "xunit.v3.runner.utility.netcore.dll"], + config.GetIncludeFilters(), config.GetExcludeFilters())]; + + Assert.Equal(["Other.Product.dll", "xunit.v3.runner.utility.netcore.dll"], selected); + } +} diff --git a/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDynamicExcludeTests.cs b/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDynamicExcludeTests.cs index aa4a1e163..1cfd382a3 100644 --- a/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDynamicExcludeTests.cs +++ b/test/coverlet.MTP.tests/Configuration/CoverageConfigurationDynamicExcludeTests.cs @@ -87,7 +87,7 @@ public void WhenNoConfigFileThenBaselineFilterAlwaysPresent() } [Fact] - public void WhenDepsJsonHelperReturnsEmptyListThenBaselineFallbackUsed() + public void WhenDepsJsonHelperReturnsEmptyListThenBaselineAndBuildAssetFiltersUsed() { _mockProcessAssemblyHelper .Setup(x => x.GetDepsJsonAssemblyNames(It.IsAny(), It.IsAny())) @@ -105,7 +105,9 @@ public void WhenDepsJsonHelperReturnsEmptyListThenBaselineFallbackUsed() string[] result = config.GetExcludeFilters(); - Assert.Equal(["[coverlet.*]*", "[Microsoft.VisualStudio.TestPlatform.*]*", "[testhost*]*", "[ReportGenerator.*]*"], result); + Assert.Equal(["[coverlet.*]*", "[Microsoft.VisualStudio.TestPlatform.*]*", "[testhost*]*", "[ReportGenerator.*]*", + "[xunit.abstractions]*", "[xunit.runner.visualstudio.testadapter]*", + "[xunit.v3.runner.utility.netcore]*", "[xunit.v3.runner.utility.netfx]*"], result); } [Fact] @@ -222,8 +224,8 @@ public void WhenLoadedAssemblyReturnsEntryThenFilterApplied() string[] result = config.GetExcludeFilters(); - // Three-segment name → wildcarded to [Microsoft.Testing.*]* - Assert.Contains("[Microsoft.Testing.*]*", result); + Assert.Contains("[Microsoft.Testing.Extensions.MSBuild]*", result); + Assert.DoesNotContain("[Microsoft.Testing.*]*", result); } [Fact] @@ -246,10 +248,9 @@ public void WhenBothSourcesReturnEntriesThenAllFiltersApplied() string[] result = config.GetExcludeFilters(); - // Three-segment names are wildcarded to their two-segment prefix. - Assert.Contains("[xunit.v3.*]*", result); + Assert.Contains("[xunit.v3.core]*", result); Assert.Contains("[testhost]*", result); - Assert.Contains("[Microsoft.Testing.*]*", result); + Assert.Contains("[Microsoft.Testing.Extensions.MSBuild]*", result); } #endregion diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 8fce237ed..bb57d8a52 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -139,6 +139,8 @@ public void TestBackupOriginalModule() [InlineData("[*]Coverlet.Core*")] [InlineData("[coverlet.*]*")] [InlineData("[*]ClassLibrary1.Tests.*")] + [InlineData("[xunit.v3.mtp-v2]*")] + [InlineData("[Example.Testing.test-host]*")] public void TestIsValidFilterExpression(string pattern) { Assert.True(_instrumentationHelper.IsValidFilterExpression(pattern)); @@ -146,7 +148,7 @@ public void TestIsValidFilterExpression(string pattern) [Theory] [InlineData("[*]")] - [InlineData("[-]*")] + [InlineData("[!]*")] [InlineData("*")] [InlineData("][")] [InlineData(null)] diff --git a/test/coverlet.core.tests/Helpers/ProcessAssemblyHelperTests.cs b/test/coverlet.core.tests/Helpers/ProcessAssemblyHelperTests.cs index d05271c9c..ca951895c 100644 --- a/test/coverlet.core.tests/Helpers/ProcessAssemblyHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/ProcessAssemblyHelperTests.cs @@ -76,12 +76,12 @@ public void WhenGetLoadedAssemblyNamesWithWhitespaceThenThrowsArgumentException( [InlineData("ReportGenerator.Mtp", "[ReportGenerator.Mtp]*")] // Two segments — exact [InlineData("coverlet.core", "[coverlet.core]*")] - // Three segments — wildcard from third onwards - [InlineData("xunit.v3.mtp-v2", "[xunit.v3.*]*")] - [InlineData("xunit.runner.utility", "[xunit.runner.*]*")] - [InlineData("Microsoft.Testing.Platform", "[Microsoft.Testing.*]*")] - // Four segments — still uses first-two-segment prefix - [InlineData("Microsoft.Extensions.Configuration.Binder", "[Microsoft.Extensions.*]*")] + // Longer names must also match only the discovered assembly. + [InlineData("xunit.v3.core", "[xunit.v3.core]*")] + [InlineData("xunit.runner.utility", "[xunit.runner.utility]*")] + [InlineData("Microsoft.Testing.Platform", "[Microsoft.Testing.Platform]*")] + [InlineData("Microsoft.Extensions.Configuration.Binder", "[Microsoft.Extensions.Configuration.Binder]*")] + [InlineData("Codebelt.Extensions.Xunit", "[Codebelt.Extensions.Xunit]*")] public void WhenToExcludeFilterThenReturnsCorrectPattern(string assemblyName, string expectedFilter) { string result = ProcessAssemblyHelper.ToExcludeFilter(assemblyName); @@ -120,7 +120,7 @@ public void WhenGetLoadedAssemblyNamesThenContainsKnownAssembly() } [Fact] - public void WhenGetDepsJsonAssemblyNamesThenReturnsRuntimePackageNamesAndSkipsProjectLibraries() + public void WhenGetDepsJsonAssemblyNamesThenReturnsRuntimeAssemblyNamesAndSkipsProjectLibraries() { string testModuleDirectory = AppContext.BaseDirectory; string testAssemblyName = Path.GetFileNameWithoutExtension(typeof(ProcessAssemblyHelperTests).Assembly.Location); @@ -128,6 +128,7 @@ public void WhenGetDepsJsonAssemblyNamesThenReturnsRuntimePackageNamesAndSkipsPr IReadOnlyList result = _sut.GetDepsJsonAssemblyNames(testModuleDirectory, testAssemblyName); Assert.NotEmpty(result); + Assert.Contains(typeof(FactAttribute).Assembly.GetName().Name!, result, StringComparer.OrdinalIgnoreCase); Assert.DoesNotContain(testAssemblyName, result, StringComparer.OrdinalIgnoreCase); var unique = new HashSet(result, StringComparer.OrdinalIgnoreCase); diff --git a/test/coverlet.core.tests/Instrumentation/CecilAssemblyResolverTests.cs b/test/coverlet.core.tests/Instrumentation/CecilAssemblyResolverTests.cs new file mode 100644 index 000000000..b9f1d2533 --- /dev/null +++ b/test/coverlet.core.tests/Instrumentation/CecilAssemblyResolverTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using Coverlet.Core.Abstractions; +using Coverlet.Core.Instrumentation; +using Mono.Cecil; +using Moq; +using Xunit; + +namespace Coverlet.Core.Tests.Instrumentation +{ + public class CecilAssemblyResolverTests + { + [Theory] + [InlineData("StringComparison")] + [InlineData("MidpointRounding")] + public void RuntimeNetstandardFacade_UsesConfiguredResolverForForwardedEnums(string enumName) + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + try + { + using ModuleDefinition primitiveTypes = ModuleDefinition.CreateModule("PrimitiveTypes", ModuleKind.Dll); + using var resolver = new NetstandardAwareAssemblyResolver(null, Mock.Of()); + AssemblyDefinition facade = resolver.Resolve(AssemblyNameReference.Parse( + "netstandard, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51")); + ExportedType forwardedEnum = facade.MainModule.ExportedTypes.Single(type => type.FullName == "System." + enumName); + var frameworkReference = (AssemblyNameReference)forwardedEnum.Scope; + string frameworkPath = Path.Combine(directory, frameworkReference.Name + ".dll"); + + // A private framework search path stands in for the resolver's runtime/reference + // assembly fallbacks. Cecil's default resolver cannot see this directory. + using (AssemblyDefinition framework = AssemblyDefinition.CreateAssembly( + new AssemblyNameDefinition(frameworkReference.Name, frameworkReference.Version), + frameworkReference.Name, ModuleKind.Dll)) + { + framework.MainModule.Types.Add(new TypeDefinition("System", "Int32", TypeAttributes.Public | TypeAttributes.Sealed, + new TypeReference("System", "ValueType", framework.MainModule, framework.MainModule.TypeSystem.CoreLibrary))); + var enumType = new TypeDefinition("System", enumName, + TypeAttributes.Public | TypeAttributes.Sealed, + new TypeReference("System", "Enum", framework.MainModule, framework.MainModule.TypeSystem.CoreLibrary)); + enumType.Fields.Add(new FieldDefinition("value__", FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName, + primitiveTypes.TypeSystem.Int32)); + framework.MainModule.Types.Add(enumType); + framework.Write(frameworkPath); + } + + resolver.AddSearchDirectory(directory); + + TypeDefinition resolvedEnum = forwardedEnum.Resolve(); + + Assert.Equal(frameworkPath, resolvedEnum.Module.FileName); + Assert.True(resolvedEnum.IsEnum); + + // Writing optional enum constants is the metadata path that failed on .NET Framework. + using AssemblyDefinition consumer = AssemblyDefinition.CreateAssembly( + new AssemblyNameDefinition("EnumConsumer", new Version(1, 0)), "EnumConsumer", + new ModuleParameters { Kind = ModuleKind.Dll, AssemblyResolver = resolver }); + var consumerType = new TypeDefinition("Example", "Consumer", TypeAttributes.Public, consumer.MainModule.TypeSystem.Object); + consumer.MainModule.Types.Add(consumerType); + AssemblyNameReference facadeReference = AssemblyNameReference.Parse(facade.Name.FullName); + consumer.MainModule.AssemblyReferences.Add(facadeReference); + var method = new MethodDefinition("Choose", MethodAttributes.Public | MethodAttributes.Static, consumer.MainModule.TypeSystem.Int32); + method.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.Optional | ParameterAttributes.HasDefault, + new TypeReference("System", enumName, consumer.MainModule, facadeReference)) { Constant = 0 }); + method.Body.GetILProcessor().Emit(Mono.Cecil.Cil.OpCodes.Ldc_I4_0); + method.Body.GetILProcessor().Emit(Mono.Cecil.Cil.OpCodes.Ret); + consumerType.Methods.Add(method); + using var output = new MemoryStream(); + consumer.Write(output); + Assert.NotEqual(0, output.Length); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + } +}