Skip to content

[fix] Fix CopyTraceDataCollectorArtifacts overwriting newer MTP CodeCoverage DLLs#15794

Draft
nohwnd wants to merge 4 commits into
mainfrom
fix/issue-15387-345ac389b57ce7df
Draft

[fix] Fix CopyTraceDataCollectorArtifacts overwriting newer MTP CodeCoverage DLLs#15794
nohwnd wants to merge 4 commits into
mainfrom
fix/issue-15387-345ac389b57ce7df

Conversation

@nohwnd
Copy link
Copy Markdown
Member

@nohwnd nohwnd commented May 17, 2026

🤖 This is an automated fix generated by the Issue Repro Triage & Auto-Fix agent.

Fixes #15387

Root Cause

In Microsoft.CodeCoverage.targets, the CopyTraceDataCollectorArtifacts target blindly copies the entire Microsoft.CodeCoverage package payload into the publish directory after ComputeFilesToPublish. When Microsoft.Testing.Extensions.CodeCoverage 18.1.0 is also referenced (e.g. via MSTest.Sdk 4.0.2), it places Microsoft.CodeCoverage.Core v18.1.0.0 into publish output, but this target then overwrites it with the older v18.0.6.0 from Microsoft.CodeCoverage 18.0.1.

At runtime, Microsoft.Testing.Extensions.CodeCoverage expects v18.1.0.0 but finds v18.0.6.0, causing:

System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.CodeCoverage.Core, Version=18.1.0.0'

Fix

Skip the CopyTraceDataCollectorArtifacts copy when Microsoft.Testing.Extensions.CodeCoverage is 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 — detect Microsoft.Testing.Extensions.CodeCoverage via PackageReference items and skip the copy when present.

🔍 Triaged by Issue Repro Triage & Auto-Fix 🔍

…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>
Copilot AI review requested due to automatic review settings May 17, 2026 01:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.CodeCoverage and conditionally skip CopyTraceDataCollectorArtifacts.
  • Updates the target’s inline documentation to explain why the copy is skipped in that scenario.

Comment on lines +21 to +23
<!-- 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.
Copy link
Copy Markdown
Member Author

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 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: .targets file 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'" />
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.CodeCoverage 18.1.0 is also referenced (e.g. via MSTest.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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🔧

@nohwnd

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>
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 17, 2026

Commit pushed: f0d0102

🔧 Iterated by PR Iteration Agent 🔧

</ItemGroup>

<PropertyGroup>
<_MtpCodeCoveragePresent Condition="'@(_MtpCodeCoverageRef)' != '' or '$(Pkgmicrosoft_Testing_Extensions_CodeCoverage)' != ''">true</_MtpCodeCoveragePresent>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 29, 2026

needs more careful analysis.

@nohwnd

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>
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 29, 2026

Commit pushed: c264ce4

🔧 Iterated by PR Iteration Agent 🔧

Copilot AI review requested due to automatic review settings May 29, 2026 15:52
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 29, 2026

Implemented the Overwrite="false" approach as analyzed in my previous comment.

What changed:

  • Removed the Microsoft.Testing.Extensions.CodeCoverage detection logic (_MtpCodeCoverageRef, _MtpCodeCoveragePresent, Pkgmicrosoft_Testing_Extensions_CodeCoverage checks)
  • Added Overwrite="false" to the Copy task
  • All TraceDataCollectorArtifacts are always copied, but files already present (placed by MTP CodeCoverage) are not overwritten

This is simpler and addresses both concerns from the analysis:

  1. ✅ MTP's newer DLLs won't be overwritten by older MC files
  2. ✅ TraceDataCollector-specific files (not provided by MTP) are still copied, so /collect:"Code Coverage" works for MSTest.Sdk users too

🔧 Iterated by PR Iteration Agent 🔧

🔧 Iterated by PR Iteration Agent 🔧

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines +17 to +26
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" />
Copy link
Copy Markdown
Member Author

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 29, 2026

Commit pushed: 64869a6

🔧 Iterated by PR Iteration Agent 🔧

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSTest.Sdk 4.0.2 has conflicting transitive dependencies of Microsoft.CodeCoverage.Core

3 participants