[fix] Fix CopyTraceDataCollectorArtifacts overwriting newer MTP CodeCoverage DLLs#15794
[fix] Fix CopyTraceDataCollectorArtifacts overwriting newer MTP CodeCoverage DLLs#15794nohwnd wants to merge 4 commits into
Conversation
…e DLLs When Microsoft.Testing.Extensions.CodeCoverage is referenced alongside Microsoft.NET.Test.Sdk, the CopyTraceDataCollectorArtifacts target blindly copies older Microsoft.CodeCoverage artifacts into the publish directory, overwriting the newer DLLs from the MTP package. This causes a FileNotFoundException at runtime because the newer version is expected but the older one is present. Fix: Skip the blind copy when Microsoft.Testing.Extensions.CodeCoverage is directly referenced, as that package already provides the correct version of all its artifacts. Fixes #15387 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Microsoft.CodeCoverage.targets packaging logic to avoid overwriting newer code coverage binaries during dotnet publish when newer coverage tooling is present, addressing the publish-time assembly version conflict described in #15387.
Changes:
- Adds MSBuild logic to detect
Microsoft.Testing.Extensions.CodeCoverageand conditionally skipCopyTraceDataCollectorArtifacts. - Updates the target’s inline documentation to explain why the copy is skipped in that scenario.
| <!-- Detect whether Microsoft.Testing.Extensions.CodeCoverage is directly referenced. --> | ||
| <ItemGroup> | ||
| <_MtpCodeCoverageRef Include="@(PackageReference)" Condition="'%(PackageReference.Identity)' == 'Microsoft.Testing.Extensions.CodeCoverage'" /> |
|
|
||
| <!-- This target required to enable /collect:"Code Coverage" in "dotnet publish" scenario with "dotnet vstest". | ||
| E.g: Release pipelines where user/project nuget cache not available on current machine. --> | ||
| E.g: Release pipelines where user/project nuget cache not available on current machine. |
nohwnd
left a comment
There was a problem hiding this comment.
🧠 Expert Review — PR #15794
Activated dimensions (via src/package/ routing): Dependency & Package Integrity, Build Script & Infrastructure Hygiene, Cross-TFM & Framework Resolution
Critical: Detection logic may miss transitive references
The core fix uses @(PackageReference) to detect whether Microsoft.Testing.Extensions.CodeCoverage is referenced. This only catches direct references in the project file. However, the PR's own motivation describes the scenario where the package arrives transitively via MSTest.Sdk 4.0.2 — which would not be visible in @(PackageReference).
In that scenario, _MtpCodeCoverageRef will be empty, the condition evaluates to ''== '' (true = copy proceeds), and the newer DLLs are still overwritten. The fix would have no effect for the described root cause.
See the inline comment for a concrete suggestion using $(Pkgmicrosoft_Testing_Extensions_CodeCoverage) which NuGet sets for all resolved packages including transitive ones.
Other dimensions: No issues found
- Build Script & Infrastructure Hygiene: MSBuild syntax is valid; target ordering (
AfterTargets="ComputeFilesToPublish") is appropriate. - Cross-TFM:
.targetsfile is TFM-agnostic; no concerns. - Description alignment: The PR title and description accurately describe the intent. The description is well-written and matches the diff.
🧠 Reviewed by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
|
|
||
| <!-- Detect whether Microsoft.Testing.Extensions.CodeCoverage is directly referenced. --> | ||
| <ItemGroup> | ||
| <_MtpCodeCoverageRef Include="@(PackageReference)" Condition="'%(PackageReference.Identity)' == 'Microsoft.Testing.Extensions.CodeCoverage'" /> |
There was a problem hiding this comment.
[Dependency & Package Integrity] Transitive references not detected — fix may not work in the described scenario
@(PackageReference) only contains directly declared package references from the project file. Transitive dependencies (packages brought in by other packages) do not appear here.
The PR description motivates the fix with this exact scenario:
When
Microsoft.Testing.Extensions.CodeCoverage18.1.0 is also referenced (e.g. viaMSTest.Sdk 4.0.2)...
If a user has MSTest.Sdk as their SDK (or has it as a PackageReference) and Microsoft.Testing.Extensions.CodeCoverage arrives transitively, this detection will see an empty _MtpCodeCoverageRef and still perform the overwrite — exactly the bug being fixed.
A more robust approach would be to check @(ResolvedPackageReference) (populated by RunResolvePackageDependencies) or use the NuGet-generated package path property:
<!-- Available after NuGet resolves packages; includes transitive references -->
<_MtpCodeCoverageRef Include="@(PackageReference)" Condition="'%(Identity)' == 'Microsoft.Testing.Extensions.CodeCoverage'" />
<!-- Also check via the NuGet package property (set for direct and some transitive scenarios) -->
<PropertyGroup>
<_MtpCodeCoveragePresent Condition="'$(Pkgmicrosoft_Testing_Extensions_CodeCoverage)' != ''">true</_MtpCodeCoveragePresent>
</PropertyGroup>Or, alternatively, check whether a file from the newer package is already present in the publish output (file-based version comparison), though that is more complex.
At minimum, the condition should also cover '$(Pkgmicrosoft_Testing_Extensions_CodeCoverage)' != '', as NuGet sets this property for all resolved packages (direct and transitive).
There was a problem hiding this comment.
Good catch. This was addressed in the follow-up commit f0d0102 — the condition now also checks $(Pkgmicrosoft_Testing_Extensions_CodeCoverage), which NuGet sets for all resolved packages including transitive ones:
<_MtpCodeCoveragePresent Condition="'@(_MtpCodeCoverageRef)' != '' or '$(Pkgmicrosoft_Testing_Extensions_CodeCoverage)' != ''">true</_MtpCodeCoveragePresent>This covers the MSTest.Sdk transitive scenario described in the PR.
🔧 Iterated by PR Iteration Agent 🔧
🔧 Iterated by PR Iteration Agent 🔧
This comment has been minimized.
This comment has been minimized.
… reference Use $(Pkgmicrosoft_Testing_Extensions_CodeCoverage) in addition to @(PackageReference) to detect when Microsoft.Testing.Extensions.CodeCoverage is present transitively (e.g. via MSTest.Sdk). NuGet sets the Pkg* property for all resolved packages, including transitive ones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
| </ItemGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <_MtpCodeCoveragePresent Condition="'@(_MtpCodeCoverageRef)' != '' or '$(Pkgmicrosoft_Testing_Extensions_CodeCoverage)' != ''">true</_MtpCodeCoveragePresent> |
There was a problem hiding this comment.
It sounds weird to have property conditioned on an item.
Also sounds weird to have two ways for checking the package reference.
I'm also not sure if the overall fix is even correct. This won't handle if CC is brought transitively.
| </ItemGroup> | ||
|
|
||
| <Copy SourceFiles="@(TraceDataCollectorArtifacts)" DestinationFolder="$(PublishDir)%(RecursiveDir)" /> | ||
| <Copy SourceFiles="@(TraceDataCollectorArtifacts)" DestinationFolder="$(PublishDir)%(RecursiveDir)" |
There was a problem hiding this comment.
I would say this copy invocation should skip any existing files in destination (regardless of whether or not they are different).
I'm also not even sure why the whole target is needed. Maybe if you track the original problem it tries to solve, you can find a whole better solution.
|
needs more careful analysis. |
This comment has been minimized.
This comment has been minimized.
…f MTP detection Replace the Microsoft.Testing.Extensions.CodeCoverage detection logic with Overwrite="false" on the Copy task. This is simpler and more robust: - Files already placed by MTP CodeCoverage (which runs before this target) are not overwritten with older versions from this package. - All TraceDataCollector-specific files are still copied for users without MTP. - No package detection logic needed (avoids Pkgxxx reliability concerns). The previous detection-based approach also had a gap: it skipped copying *all* files, including TraceDataCollector-specific ones that MTP doesn't provide, which could silently break /collect:"Code Coverage" for MSTest.Sdk users. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
|
Implemented the What changed:
This is simpler and addresses both concerns from the analysis:
|
| Uses Overwrite="false" so that files already placed by Microsoft.Testing.Extensions.CodeCoverage | ||
| (which runs before this target) are not overwritten with older versions from this package. --> | ||
| <Target Name="CopyTraceDataCollectorArtifacts" AfterTargets="ComputeFilesToPublish"> | ||
|
|
||
| <ItemGroup> | ||
| <TraceDataCollectorArtifacts Include="$(MSBuildThisFileDirectory)\**\*.*" /> | ||
| </ItemGroup> | ||
|
|
||
| <Copy SourceFiles="@(TraceDataCollectorArtifacts)" DestinationFolder="$(PublishDir)%(RecursiveDir)" /> | ||
| <Copy SourceFiles="@(TraceDataCollectorArtifacts)" DestinationFolder="$(PublishDir)%(RecursiveDir)" | ||
| Overwrite="false" /> |
nohwnd
left a comment
There was a problem hiding this comment.
🧠 Expert Review — PR #15794
The Overwrite="false" approach is a clean, robust fix. It works regardless of MSBuild target execution order between this package and Microsoft.Testing.Extensions.CodeCoverage (as long as MTP uses the default Overwrite=true), so the fix is more reliable than the previous PackageReference-detection approach.
No blocking issues found. One inline note on the code comment's accuracy and a minor observation about incremental publish behavior.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
| <!-- This target required to enable /collect:"Code Coverage" in "dotnet publish" scenario with "dotnet vstest". | ||
| E.g: Release pipelines where user/project nuget cache not available on current machine. --> | ||
| E.g: Release pipelines where user/project nuget cache not available on current machine. | ||
| Uses Overwrite="false" so that files already placed by Microsoft.Testing.Extensions.CodeCoverage |
There was a problem hiding this comment.
The comment says the fix works because MTP CodeCoverage "runs before this target," but the fix is actually ordering-independent:
- MTP first: newer files land, this target skips them (Overwrite=false) ✓
- This target first: older files land, MTP then overwrites with newer (default Overwrite=true) ✓
The fix is correct either way, but the comment may mislead future readers into thinking a specific execution order is required. Consider rewording to:
<!-- Uses Overwrite="false" so that files already placed by Microsoft.Testing.Extensions.CodeCoverage
(which ships newer versions of these artifacts) are not replaced with the older versions
from this package, regardless of target execution order. -->Also worth noting: with Overwrite="false", incremental re-publishes to a non-clean output directory will not refresh stale DLLs from this package. This is generally fine for CI scenarios where publish targets a clean directory, but may be surprising in local dev incremental publishes.
There was a problem hiding this comment.
Updated the comment to clarify the fix is ordering-independent:
<!-- Uses Overwrite="false" so that files already placed by Microsoft.Testing.Extensions.CodeCoverage
(which ships newer versions of these artifacts) are not replaced with the older versions
from this package, regardless of target execution order. -->🔧 Iterated by PR Iteration Agent 🔧
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Fixes #15387
Root Cause
In
Microsoft.CodeCoverage.targets, theCopyTraceDataCollectorArtifactstarget blindly copies the entireMicrosoft.CodeCoveragepackage payload into the publish directory afterComputeFilesToPublish. WhenMicrosoft.Testing.Extensions.CodeCoverage18.1.0 is also referenced (e.g. viaMSTest.Sdk 4.0.2), it placesMicrosoft.CodeCoverage.Corev18.1.0.0 into publish output, but this target then overwrites it with the older v18.0.6.0 fromMicrosoft.CodeCoverage18.0.1.At runtime,
Microsoft.Testing.Extensions.CodeCoverageexpects v18.1.0.0 but finds v18.0.6.0, causing:Fix
Skip the
CopyTraceDataCollectorArtifactscopy whenMicrosoft.Testing.Extensions.CodeCoverageis directly referenced. That package already provides the correct version of all its artifacts, so the blind copy is both unnecessary and harmful in that scenario.Change
src/package/Microsoft.CodeCoverage/Microsoft.CodeCoverage.targets— detectMicrosoft.Testing.Extensions.CodeCoverageviaPackageReferenceitems and skip the copy when present.