diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9d491686a..e8e1ea35d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -7,7 +7,7 @@
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "10.0.100",
- "additionalVersions": ["6.0.428", "8.0.416", "9.0.307"]
+ "additionalVersions": ["6.0.428", "8.0.416", "9.0.308"]
}
},
@@ -38,6 +38,7 @@
"ms-azure-devops.azure-pipelines",
"GitHub.copilot-chat",
"GitHub.copilot",
+ "github.vscode-github-actions"
"mhutchie.git-graph",
"streetsidesoftware.code-spell-checker",
"streetsidesoftware.code-spell-checker-german",
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..5990d9c64
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 000000000..4ed5380b2
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,232 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+env:
+ BuildConfiguration: debug
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+
+permissions:
+ checks: write
+ pull-requests: write
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ timeout-minutes: 30
+ permissions:
+ pull-requests: write
+ contents: read
+ checks: write
+
+ steps:
+ - uses: actions/checkout@v6.0.1
+ with:
+ fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
+
+ - name: Setup .NET 9.0
+ uses: actions/setup-dotnet@v5.0.1
+ with:
+ dotnet-version: 9.0.x
+# source-url: https://pkgs.dev.azure.com/tonerdo/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json
+# env:
+# NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}
+
+ - name: Setup .NET 8.0
+ uses: actions/setup-dotnet@v5.0.1
+ with:
+ dotnet-version: 8.0.x
+# source-url: https://pkgs.dev.azure.com/tonerdo/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json
+# env:
+# NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}
+
+ - name: create folders for artifacts
+ run: |
+ mkdir -p ./artifacts/bin
+ mkdir -p ./artifacts/package
+ mkdir -p ./artifacts/package/debug
+ mkdir -p ./artifacts/package/release
+ mkdir -p ./artifacts/log
+ mkdir -p ./artifacts/publish
+ mkdir -p ./artifacts/reports
+
+
+ - name: Restore dependencies
+ run: dotnet restore coverlet.sln
+
+ - name: Build
+ run: |
+ dotnet build coverlet.sln --no-restore -bl:build.binlog -c ${{env.BuildConfiguration}}
+ dotnet build coverlet.sln --no-restore -bl:build.binlog -c release
+ dotnet pack -c ${{env.BuildConfiguration}}
+ dotnet pack -c release
+
+ # - name: Archive production artifacts
+ # uses: actions/upload-artifact@v5
+ # with:
+ # name: dist-bin-and-packages
+ # retention-days: 5
+ # path: |
+ # artifacts/bin
+ # artifacts/package
+ # artifacts/publish
+ # artifacts/log
+ # *.binlog
+
+ # Fail if there are any failed tests
+ #
+ # We support all current LTS versions of .NET and Windows, Mac and Linux.
+ #
+ # To check for failing tests locally run `dotnet test`.
+
+ # test:
+ # name: Tests for .net core ${{ matrix.framework }} on ${{ matrix.os }}
+ # needs: build
+ # runs-on: ${{ matrix.os }}
+ # strategy:
+ # matrix:
+ # os: [ubuntu-latest, windows-latest, macos-latest]
+ # framework: ['net9.0', 'net8.0']
+ # timeout-minutes: 30
+ # permissions:
+ # pull-requests: write
+ # steps:
+ # - name: Checkout
+ # uses: actions/checkout@v6.0.1
+ # with:
+ # fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
+
+ # - name: Setup .NET 9.0
+ # uses: actions/setup-dotnet@v5.0.1
+ # with:
+ # dotnet-version: 9.0.x
+
+ # - name: Setup dotnet 8.0
+ # uses: actions/setup-dotnet@v5.0.1
+ # with:
+ # dotnet-version: '8.0.x'
+
+ # - name: Download packages and artifacts
+ # uses: actions/download-artifact@v5
+ # with:
+ # name: dist-bin-and-packages
+
+ - run: |
+ echo "Test using script"
+ dotnet build-server shutdown
+ dotnet test ./test/coverlet.core.tests/coverlet.core.tests.csproj -c ${{env.BuildConfiguration}} --no-build -bl:test.core.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" -- --results-directory "./artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.tests.trx" --diagnostic --diagnostic-output-directory "./artifacts/log/${{env.BuildConfiguration}}" --diagnostic-output-fileprefix "coverlet.core.tests"
+ dotnet build-server shutdown
+ dotnet test ./test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj -c ${{env.BuildConfiguration}} --no-build -bl:test.msbuild.binlog --results-directory:"./artifacts/reports" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.xunit.extensions]*%2c[coverlet.tests.projectsample]*%2c[testgen_]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"./artifacts/log/${{env.BuildConfiguration}}/coverlet.msbuild.test.diag.log;tracelevel=verbose"
+ dotnet build-server shutdown
+ dotnet test ./test/coverlet.collector.tests/coverlet.collector.tests.csproj -c ${{env.BuildConfiguration}} --no-build -bl:test.collector.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"./artifacts/log/${{env.BuildConfiguration}}/coverlet.collector.test.diag.log;tracelevel=verbose"
+ dotnet build-server shutdown
+ dotnet test ./test/coverlet.integration.tests/coverlet.integration.tests.csproj -c ${{env.BuildConfiguration}} --no-build -bl:test.integration.binlog -- --results-directory "./artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.integration.tests.trx" --diagnostic --diagnostic-output-directory "./artifacts/log/${{env.BuildConfiguration}}" --diagnostic-output-fileprefix "coverlet.integration.tests"
+ name: Run unit tests with coverage
+ env:
+ MSBUILDDISABLENODEREUSE: 1
+
+ # - run: |
+ # echo "Test using script"
+ # dotnet build-server shutdown
+ # dotnet test ./test/coverlet.core.coverage.tests/coverlet.core.coverage.tests.csproj -c ${{env.BuildConfiguration}} --no-build -bl:test.core.coverage.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" -- --results-directory "./artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.coverage.tests.trx" --diagnostic --diagnostic-output-directory "./artifacts/log/${{env.BuildConfiguration}}" --diagnostic-output-fileprefix "coverlet.core.coverage.tests"
+ # name: Run unit test coverlet.core.coverage.tests
+ # if: success() && matrix.os == 'windows-latest'
+ # env:
+ # MSBUILDDISABLENODEREUSE: 1
+
+ - name: ReportGenerator
+ uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
+ if: success() && matrix.os == 'windows-latest'
+ with:
+ reports: ./artifacts/reports/**/*.cobertura.xml
+ assemblyfilters: -xunit*
+ targetdir: ./artifacts/reports
+ reporttypes: HtmlInline;Cobertura;MarkdownSummaryGithub;lcov
+
+ - name: Add Coverage PR Comment
+ uses: marocchino/sticky-pull-request-comment@v2.9.4
+ if: success() && matrix.os == 'windows-latest' && github.event_name == 'pull_request'
+ with:
+ recreate: true
+ path: ./artifacts/reports/SummaryGithub.md
+
+ - name: Write to Job Summary
+ if: matrix.os == 'windows-latest'
+ run: cat ./artifacts/reports/SummaryGithub.md >> $GITHUB_STEP_SUMMARY
+ shell: bash
+
+ - name: Upload Test Result Files
+ uses: actions/upload-artifact@v5
+ if: always()
+ with:
+ name: test-results-${{ matrix.os }}
+ path: ${{ github.workspace }}/artifacts/reports/**/*
+ retention-days: 5
+
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action/linux@v2
+ if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
+ with:
+ files: |
+ ${{ github.workspace }}/artifacts/reports/**/*.trx
+ ${{ github.workspace }}/test/**/*.trx
+ check_name: "Unit Tests ${{ matrix.os }}"
+ comment_mode: failures
+ compare_to_earlier_commit: false
+
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action/macos@v2.21.0
+ if: ${{ !cancelled() && matrix.os == 'macos-latest' }}
+ with:
+ files: |
+ ${{ github.workspace }}/artifacts/reports/**/*.trx
+ ${{ github.workspace }}/test/**/*.trx
+ check_name: "Unit Tests ${{ matrix.os }}"
+ comment_mode: failures
+ compare_to_earlier_commit: false
+
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action/windows@v2.21.0
+ if: ${{ !cancelled() && matrix.os == 'windows-latest' }}
+ with:
+ files: |
+ ${{ github.workspace }}/artifacts/reports/**/*.trx
+ ${{ github.workspace }}/test/**/*.trx
+ check_name: "Unit Tests ${{ matrix.os }}"
+ comment_mode: failures
+ compare_to_earlier_commit: false
+
+ # - uses: bibipkins/dotnet-test-reporter@v1.6.1
+ # with:
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+ # comment-title: 'Unit Test Results ${{ matrix.os }}'
+ # results-path: |
+ # ./artifacts/reports/**/*.trx
+ # ./test/**/*.trx
+ # coverage-path: |
+ # ./artifacts/bin/**/*.cobertura.xml
+ # ./artifacts/reports/**/*.cobertura.xml
+ # ./test/**/*.cobertura.xml
+ # coverage-threshold: 80
+ # coverage-type: cobertura
+ # show-failed-tests-only: true
+ # show-test-output: true
+
+ # - name: Upload coverage report artifact
+ # if: success() && matrix.os == 'windows-latest'
+ # uses: actions/upload-artifact@v5
+ # with:
+ # name: CoverageReport.${{matrix.os}}.${{matrix.framework}} # Artifact name
+ # path: ./artifacts/CoverageReport # Directory containing files to upload
+ # overwrite: true
diff --git a/.gitignore b/.gitignore
index 760fe06e1..87279532c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -318,8 +318,8 @@ FolderProfile.pubxml
/NuGet.config
nuget.config
*.dmp
-Playground*/
# extended playground
-coverlet.MTP/
+Playground*/
# ignore copilot agents
.github/agents/
+current.diff
diff --git a/BannedSymbols.txt b/BannedSymbols.txt
new file mode 100644
index 000000000..5bc5c2fb9
--- /dev/null
+++ b/BannedSymbols.txt
@@ -0,0 +1,10 @@
+T:System.ArgumentNullException; Use 'Guard' instead
+P:System.DateTime.Now; Use 'IClock' instead
+P:System.DateTime.UtcNow; Use 'IClock' instead
+M:System.Threading.Tasks.Task.Run(System.Action); Use 'ITask' instead
+M:System.Threading.Tasks.Task.WhenAll(System.Threading.Tasks.Task[]); Use 'ITask' instead
+M:System.Threading.Tasks.Task.WhenAll(System.Collections.Generic.IEnumerable{System.Threading.Tasks.Task}); Use 'ITask' instead
+M:System.String.IsNullOrEmpty(System.String); Use 'RoslynString.IsNullOrEmpty' instead
+M:System.String.IsNullOrWhiteSpace(System.String); Use 'RoslynString.IsNullOrWhiteSpace' instead
+M:System.Diagnostics.Debug.Assert(System.Boolean); Use 'RoslynDebug.Assert' instead
+M:System.Diagnostics.Debug.Assert(System.Boolean,System.String); Use 'RoslynDebug.Assert' instead
diff --git a/Directory.Build.props b/Directory.Build.props
index c22ceee79..4b7b9f322 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -28,7 +28,7 @@
true
$(MSBuildThisFileDirectory)artifacts
- 6.0.0
+ 8.0.0
@@ -36,17 +36,22 @@
true
-
+
+ true
+ true
+
+
+
-
+
$(RepoRoot)artifacts/reports/$(Configuration.ToLowerInvariant())
@(VSTestLogger)
-
+
$(RepoRoot)artifacts\reports\$(Configuration.ToLowerInvariant())
@(VSTestLogger)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index cdf6d83a8..4443ea854 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,20 +8,23 @@
17.11.48
- 4.12.0
+ 4.13.0
+
6.14.0
- 17.14.1
- 3.0.0
+ 18.0.1
+ 3.2.1
3.1.5
+ 1.9.1
-
+
+
@@ -29,6 +32,18 @@
+
+
+
+
+
+
+
+
+
+
@@ -41,24 +56,33 @@
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
diff --git a/Documentation/Coverlet.MTP.Integration.md b/Documentation/Coverlet.MTP.Integration.md
new file mode 100644
index 000000000..b1ec61b0a
--- /dev/null
+++ b/Documentation/Coverlet.MTP.Integration.md
@@ -0,0 +1,15 @@
+# Coverlet Microsoft Testing Platform Integration
+
+[Microsoft.Testing.Platform and Microsoft Test Framework](https://github.com/microsoft/testfx) is a lightweight alternativ for VSTest.
+
+More information is available here:
+
+- [Microsoft.Testing.Platform overview](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro?tabs=dotnetcli)
+- [Microsoft.Testing.Platform extensibility](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-architecture-extensions)
+
+coverlet.MTP uses MTP interface and implement coverlet.collector functionality.
+
+December 2025: [Microsoft coverage can be used for xunit](https://xunit.net/docs/getting-started/v3/code-coverage-with-mtp) as well.
+
+
+ToDo: Usage details
diff --git a/Documentation/MSBuildIntegration.md b/Documentation/MSBuildIntegration.md
index 1c3f9a0b8..396f6eb66 100644
--- a/Documentation/MSBuildIntegration.md
+++ b/Documentation/MSBuildIntegration.md
@@ -263,3 +263,31 @@ Here is an example of how to specify the parameter:
```shell
/p:ExcludeAssembliesWithoutSources="MissingAny"
```
+
+## Enable Restore of instrumented assembly
+
+The DisableManagedInstrumentationRestore property controls whether Coverlet should restore (revert) an assembly to its original state after instrumentation. By _default_, this is set to __false__, meaning:
+
+ 1. Coverlet instruments (modifies) the assembly to track code coverage
+ 1. After coverage collection, it restores the assembly back to its original state
+
+
+When set to __true__:
+- The assembly remains in its instrumented state
+- This can help avoid file access conflicts
+- Useful for testing/debugging instrumentation without restoration
+
+
+Example use case:
+
+```xml
+
+
+ true
+
+```
+
+This setting is particularly helpful when troubleshooting instrumentation issues or when dealing with file locking problems during coverage collection.
+
+> [!NOTE]
+> Make sure instrumented binaries are not deployed into production.
diff --git a/coverlet.sln b/coverlet.sln
index 228793780..6d35bb26d 100644
--- a/coverlet.sln
+++ b/coverlet.sln
@@ -92,6 +92,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.coverage.test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.integration.determisticbuild", "test\coverlet.integration.determisticbuild\coverlet.integration.determisticbuild.csproj", "{C80BF6A9-63EE-6D36-8913-627A7E2EA459}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.MTP", "src\coverlet.MTP\coverlet.MTP.csproj", "{B3F6B18B-AE59-CACC-BEFE-2C9796F51A68}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.MTP.unit.tests", "test\coverlet.MTP.unit.tests\coverlet.MTP.unit.tests.csproj", "{E97959B1-73BA-5B91-5795-5602ADC73FB5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.MTP.validation.tests", "test\coverlet.MTP.validation.tests\coverlet.MTP.validation.tests.csproj", "{9BB7E3B0-606F-2A58-C4A3-D233519875C5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -202,6 +208,18 @@ Global
{C80BF6A9-63EE-6D36-8913-627A7E2EA459}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C80BF6A9-63EE-6D36-8913-627A7E2EA459}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C80BF6A9-63EE-6D36-8913-627A7E2EA459}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B3F6B18B-AE59-CACC-BEFE-2C9796F51A68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B3F6B18B-AE59-CACC-BEFE-2C9796F51A68}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B3F6B18B-AE59-CACC-BEFE-2C9796F51A68}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B3F6B18B-AE59-CACC-BEFE-2C9796F51A68}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E97959B1-73BA-5B91-5795-5602ADC73FB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E97959B1-73BA-5B91-5795-5602ADC73FB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E97959B1-73BA-5B91-5795-5602ADC73FB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E97959B1-73BA-5B91-5795-5602ADC73FB5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9BB7E3B0-606F-2A58-C4A3-D233519875C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9BB7E3B0-606F-2A58-C4A3-D233519875C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9BB7E3B0-606F-2A58-C4A3-D233519875C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9BB7E3B0-606F-2A58-C4A3-D233519875C5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -234,6 +252,9 @@ Global
{71004336-9896-4AE5-8367-B29BB1680542} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{F74AD549-EFE0-4CD9-AD10-B2189E3FD5BB} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{C80BF6A9-63EE-6D36-8913-627A7E2EA459} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+ {B3F6B18B-AE59-CACC-BEFE-2C9796F51A68} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
+ {E97959B1-73BA-5B91-5795-5602ADC73FB5} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+ {9BB7E3B0-606F-2A58-C4A3-D233519875C5} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}
diff --git a/eng/azure-pipelines-nightly.yml b/eng/azure-pipelines-nightly.yml
index b9a3f5262..70174b8f7 100644
--- a/eng/azure-pipelines-nightly.yml
+++ b/eng/azure-pipelines-nightly.yml
@@ -5,7 +5,7 @@ steps:
- task: UseDotNet@2
inputs:
version: 8.0.414
- displayName: Install .NET Core SDK 8.0.412
+ displayName: Install .NET Core SDK 8.0.414
- task: UseDotNet@2
inputs:
diff --git a/eng/build.sh b/eng/build.sh
index a1e1891fa..1a303b2c4 100644
--- a/eng/build.sh
+++ b/eng/build.sh
@@ -1,34 +1,96 @@
#!/bin/bash
+set -e
-# build.sh - Helper script to build, package, and test the Coverlet project.
+# build.sh - Helper script to build and package the Coverlet project.
#
# This script performs the following tasks:
-# 1. Builds the project in debug configuration and generates a binary log.
-# 2. Packages the project in both debug and release configurations.
-# 3. Shuts down any running .NET build servers.
-# 4. Runs unit tests for various Coverlet components with code coverage enabled,
-# generating binary logs and diagnostic outputs.
-# 5. Outputs test results in xUnit TRX format and stores them in the specified directories.
+# 1. Cleans up temporary files and build artifacts
+# 2. Builds individual project targets (required for Linux compatibility)
+# 3. Packages the project in both debug and release configurations
#
# Usage:
# ./build.sh
#
# Note: Ensure that the .NET SDK is installed and available in the system PATH.
+# For running tests, use the separate test.sh script.
-# Build the project
-dotnet build -c debug -bl:build.binlog
-dotnet pack -c debug
-dotnet pack -c release
-dotnet build-server shutdown
+# Get the workspace root directory
+# Get the workspace root directory (parent of the script's directory)
+WORKSPACE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$WORKSPACE_ROOT"
+echo "Starting build... (root folder: ${PWD##*/})"
-# Run tests with code coverage
-dotnet test test/coverlet.collector.tests /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*" --results-directory:"./artifacts/reports" --diag:"artifacts/log/debug/coverlet.collector.test.log;tracelevel=verbose"
-dotnet build-server shutdown
-dotnet test test/coverlet.core.tests /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*" --results-directory:"./artifacts/reports" --verbosity detailed --diag ./artifacts/log/debug/coverlet.core.tests.log
-dotnet build-server shutdown
-dotnet test test/coverlet.core.coverage.tests /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*" -- --results-directory "$(pwd)/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.coverage.tests.trx" --diagnostic-verbosity debug --diagnostic --diagnostic-output-directory "$(pwd)/artifacts/log/debug"
+echo "Please cleanup '/tmp' folder if needed!"
+
+# Shutdown build server and kill any running test processes
dotnet build-server shutdown
-dotnet test test/coverlet.msbuild.tasks.tests /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*" --results-directory:"./artifacts/reports" --verbosity detailed --diag ./artifacts/log/debug/coverlet.msbuild.tasks.tests.log
+pkill -f "coverlet.core.tests.exe" 2>/dev/null || true
+
+# Delete coverage files
+echo "Cleaning up coverage files and build artifacts..."
+find . -name "coverage.cobertura.xml" -delete 2>/dev/null || true
+find . -name "coverage.json" -delete 2>/dev/null || true
+find . -name "coverage.net8.0.json" -delete 2>/dev/null || true
+find . -name "coverage.opencover.xml" -delete 2>/dev/null || true
+find . -name "coverage.net8.0.opencover.xml" -delete 2>/dev/null || true
+
+# Delete binlog files in integration tests
+rm -f test/coverlet.integration.determisticbuild/*.binlog 2>/dev/null || true
+
+# Remove artifacts directory
+rm -rf artifacts
+
+# Clean up local NuGet packages
+rm -rf "$HOME/.nuget/packages/coverlet.msbuild/V1.0.0" 2>/dev/null || true
+rm -rf "$HOME/.nuget/packages/coverlet.collector/V1.0.0" 2>/dev/null || true
+
+# Remove TestResults, bin, and obj directories
+find . -type d \( -name "TestResults" -o -name "bin" -o -name "obj" \) -exec rm -rf {} + 2>/dev/null || true
+
+# Remove preview packages from NuGet cache
+find "$HOME/.nuget/packages" -type d \( -path "*/coverlet.msbuild/8.0.0-preview*" -o -path "*/coverlet.collector/8.0.0-preview*" -o -path "*/coverlet.console/8.0.0-preview*" \) -exec rm -rf {} + 2>/dev/null || true
+
+echo "Cleanup complete. Starting build..."
+
+# Pack initial packages (Debug)
+dotnet pack -c Debug src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack -c Debug src/coverlet.collector/coverlet.collector.csproj /p:ContinuousIntegrationBuild=true
+
+# Build individual projects with binlog
+dotnet build src/coverlet.core/coverlet.core.csproj -bl:build.core.binlog /p:ContinuousIntegrationBuild=true
+dotnet build src/coverlet.collector/coverlet.collector.csproj -bl:build.collector.binlog /p:ContinuousIntegrationBuild=true
+dotnet build src/coverlet.console/coverlet.console.csproj -bl:build.console.binlog /p:ContinuousIntegrationBuild=true
+dotnet build src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj -bl:build.msbuild.tasks.binlog /p:ContinuousIntegrationBuild=true
+
+# Build test projects with binlog
+dotnet build test/coverlet.collector.tests/coverlet.collector.tests.csproj -bl:build.collector.tests.binlog /p:ContinuousIntegrationBuild=true
+dotnet build test/coverlet.core.coverage.tests/coverlet.core.coverage.tests.csproj -bl:build.core.coverage.tests.binlog /p:ContinuousIntegrationBuild=true
+dotnet build test/coverlet.core.tests/coverlet.core.tests.csproj -bl:build.coverlet.core.tests.binlog /p:ContinuousIntegrationBuild=true
+dotnet build test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj -bl:build.coverlet.msbuild.tasks.tests.binlog /p:ContinuousIntegrationBuild=true
+dotnet build test/coverlet.integration.tests/coverlet.integration.tests.csproj -f net8.0 -bl:build.coverlet.core.tests.8.0.binlog /p:ContinuousIntegrationBuild=true
+
+# Get the SDK version from global.json
+SDK_VERSION=$(grep -oP '"version"\s*:\s*"\K[^"]+' global.json)
+SDK_MAJOR_VERSION=$(echo "$SDK_VERSION" | cut -d'.' -f1)
+
+# Check if the SDK version is 9.0.* or higher (9.0.*, 10.0.*, etc.)
+if [[ "$SDK_MAJOR_VERSION" -ge 9 ]]; then
+ echo "Executing command for SDK version $SDK_VERSION (9.0+ detected)..."
+ dotnet build test/coverlet.integration.tests/coverlet.integration.tests.csproj -f net9.0 -bl:build.coverlet.core.tests.9.9.binlog /p:ContinuousIntegrationBuild=true
+fi
+
+# Create NuGet packages (Debug)
+dotnet pack -c Debug src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack -c Debug src/coverlet.collector/coverlet.collector.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack -c Debug src/coverlet.console/coverlet.console.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack -c Debug src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj /p:ContinuousIntegrationBuild=true
+
+# Create NuGet packages (Release)
+dotnet pack src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack src/coverlet.collector/coverlet.collector.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack src/coverlet.console/coverlet.console.csproj /p:ContinuousIntegrationBuild=true
+dotnet pack src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj /p:ContinuousIntegrationBuild=true
+
dotnet build-server shutdown
-dotnet test test/coverlet.integration.tests -f net8.0 --results-directory:"./artifacts/reports" --verbosity detailed --diag ./artifacts/log/debug/coverlet.integration.tests.net8.log
-dotnet test test/coverlet.integration.tests -f net9.0 --results-directory:"./artifacts/reports" --verbosity detailed --diag ./artifacts/log/debug/coverlet.integration.tests.net9.log
+
+echo "Build complete!"
diff --git a/eng/build.yml b/eng/build.yml
index 8bcc8020e..b3301c878 100644
--- a/eng/build.yml
+++ b/eng/build.yml
@@ -1,8 +1,8 @@
steps:
- task: UseDotNet@2
inputs:
- version: 8.0.412
- displayName: Install .NET Core SDK 8.0.8.0.414
+ version: 8.0.416
+ displayName: Install .NET Core SDK 8.0.416
- task: UseDotNet@2
inputs:
@@ -39,12 +39,14 @@ steps:
displayName: Pack
- script: |
- dotnet test test/coverlet.core.tests/coverlet.core.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.core.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.core.tests.diag.$(buildConfiguration).log;tracelevel=verbose"
+ artifacts\bin\coverlet.MTP.unit.tests\debug\coverlet.MTP.unit.tests.exe --diagnostic --diagnostic-verbosity $(BuildConfiguration) --report-xunit-trx --report-xunit-trx-filename "coverlet.MTP.unit.tests.trx" --diagnostic --diagnostic-output-directory "$(Build.SourcesDirectory)/artifacts/log/$(BuildConfiguration)"
+ artifacts\bin\coverlet.MTP.validation.tests\debug\coverlet.MTP.validation.tests.exe --diagnostic --diagnostic-verbosity $(BuildConfiguration) --report-xunit-trx --report-xunit-trx-filename "coverlet.MTP.validation.tests.trx" --diagnostic --diagnostic-output-directory "$(Build.SourcesDirectory)/artifacts/log/$(BuildConfiguration)"
+ dotnet test test/coverlet.core.tests/coverlet.core.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.core.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.core.tests.diag.$(BuildConfiguration).log;tracelevel=verbose"
dotnet test test/coverlet.core.coverage.tests/coverlet.core.coverage.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.core.coverage.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" -- --results-directory "$(Build.SourcesDirectory))/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.coverage.tests.trx" --diagnostic-verbosity debug --diagnostic --diagnostic-output-directory "$(Build.SourcesDirectory)/artifacts/log/$(BuildConfiguration)"
- dotnet test test/coverlet.msbuild.tasks.tests\coverlet.msbuild.tasks.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.msbuild.tasks.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.msbuild.tasks.tests.diag.$(buildConfiguration).log;tracelevel=verbose"
- dotnet test test/coverlet.collector.tests/coverlet.collector.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.collector.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.collector.tests.diag.$(buildConfiguration).log;tracelevel=verbose"
- dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -c $(BuildConfiguration) -f net8.0 --no-build -bl:test.integration.binlog --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.integration.tests.diag.net8.0.$(buildConfiguration).log;tracelevel=verbose"
- dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -c $(BuildConfiguration) -f net9.0 --no-build -bl:test.integration.binlog --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.integration.tests.diag.net9.0.$(buildConfiguration).log;tracelevel=verbose"
+ dotnet test test/coverlet.msbuild.tasks.tests\coverlet.msbuild.tasks.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.msbuild.tasks.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.msbuild.tasks.tests.diag.$(BuildConfiguration).log;tracelevel=verbose"
+ dotnet test test/coverlet.collector.tests/coverlet.collector.tests.csproj -c $(BuildConfiguration) --no-build -bl:test.collector.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.collector.tests.diag.$(BuildConfiguration).log;tracelevel=verbose"
+ dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -c $(BuildConfiguration) -f net8.0 --no-build -bl:test.integration.binlog --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.integration.tests.diag.net8.0.$(BuildConfiguration).log;tracelevel=verbose"
+ dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -c $(BuildConfiguration) -f net9.0 --no-build -bl:test.integration.binlog --diag:"$(Build.SourcesDirectory)/artifacts/log/coverlet.integration.tests.diag.net9.0.$(BuildConfiguration).log;tracelevel=verbose"
displayName: Run unit tests with coverage
env:
MSBUILDDISABLENODEREUSE: 1
@@ -65,5 +67,6 @@ steps:
parameters:
reports: $(Build.SourcesDirectory)\**\*.opencover.xml
condition: and(succeededORFailed(), eq(variables['buildConfiguration'], 'debug'), eq(variables['agent.os'], 'Windows_NT'))
+ minimumLineCoverage: 70
assemblyfilters: '-xunit;-coverlet.testsubject;-Coverlet.Tests.ProjectSample.*;-coverlet.core.tests.samples.netstandard;-coverletsamplelib.integration.template;-coverlet.tests.utils'
diff --git a/eng/test.sh b/eng/test.sh
new file mode 100644
index 000000000..0963a9f5a
--- /dev/null
+++ b/eng/test.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -e
+
+# Get the workspace root directory (parent of the script's directory)
+WORKSPACE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$WORKSPACE_ROOT"
+echo "Starting tests... (root folder: ${PWD##*/})"
+
+# Kill existing test processes if they exist
+pkill -f "coverlet.core.tests.dll" 2>/dev/null || true
+pkill -f "coverlet.core.coverage.tests.dll" 2>/dev/null || true
+pkill -f "coverlet.msbuild.tasks.tests.dll" 2>/dev/null || true
+pkill -f "coverlet.integration.tests.dll" 2>/dev/null || true
+
+# coverlet.core.tests
+dotnet build-server shutdown
+dotnet test test/coverlet.core.tests/coverlet.core.tests.csproj -c Debug --no-build -bl:test.core.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" -- --results-directory "$WORKSPACE_ROOT/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.tests.trx" --diagnostic --diagnostic-output-directory "$WORKSPACE_ROOT/artifacts/log/Debug" --diagnostic-output-fileprefix "coverlet.core.tests"
+
+# coverlet.core.coverage.tests !!!! does not work on Linux (Dev Container) VS debugger assemblies not available !!!!
+# dotnet build-server shutdown
+# dotnet test test/coverlet.core.coverage.tests/coverlet.core.coverage.tests.csproj -c Debug --no-build -bl:test.core.coverage.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" -- --results-directory "$WORKSPACE_ROOT/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.core.coverage.tests.trx" --diagnostic --diagnostic-output-directory "$WORKSPACE_ROOT/artifacts/log/Debug" --diagnostic-output-fileprefix "coverlet.core.coverage.tests"
+
+# coverlet.msbuild.tasks.tests
+dotnet build-server shutdown
+dotnet test test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj -c Debug --no-build -bl:test.msbuild.binlog --results-directory:"$WORKSPACE_ROOT/artifacts/reports" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.xunit.extensions]*%2c[coverlet.tests.projectsample]*%2c[testgen_]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$WORKSPACE_ROOT/artifacts/log/Debug/coverlet.msbuild.test.diag.log;tracelevel=verbose"
+
+# coverlet.collector.tests
+dotnet build-server shutdown
+dotnet test test/coverlet.collector.tests/coverlet.collector.tests.csproj -c Debug --no-build -bl:test.collector.binlog /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[coverlet.core.tests.samples.netstandard]*%2c[coverlet.tests.projectsample]*" /p:ExcludeByAttribute="GeneratedCodeAttribute" --diag:"$WORKSPACE_ROOT/artifacts/log/Debug/coverlet.collector.test.diag.log;tracelevel=verbose"
+
+# coverlet.integration.tests (default net8.0)
+dotnet build-server shutdown
+dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -f net8.0 -c Debug --no-build -bl:test.integration.binlog -- --results-directory "$WORKSPACE_ROOT/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.integration.tests.trx" --diagnostic --diagnostic-output-directory "$WORKSPACE_ROOT/artifacts/log/Debug" --diagnostic-output-fileprefix "coverlet.integration.tests"
+
+dotnet build-server shutdown
+
+# Get the SDK version from global.json
+SDK_VERSION=$(grep -oP '"version"\s*:\s*"\K[^"]+' global.json)
+SDK_MAJOR_VERSION=$(echo "$SDK_VERSION" | cut -d'.' -f1)
+
+# Check if the SDK version is 9.0.* or higher (9.0.*, 10.0.*, etc.)
+if [[ "$SDK_MAJOR_VERSION" -ge 9 ]]; then
+ # Check if the net9.0 test dll exists
+ if [ -f "$WORKSPACE_ROOT/artifacts/bin/coverlet.integration.tests/debug_net9.0/coverlet.integration.tests.dll" ]; then
+ echo "Executing command for SDK version $SDK_VERSION (9.0+ detected)..."
+ dotnet test test/coverlet.integration.tests/coverlet.integration.tests.csproj -f net9.0 -c Debug --no-build -bl:test.integration.binlog -- --results-directory "$WORKSPACE_ROOT/artifacts/reports" --report-xunit-trx --report-xunit-trx-filename "coverlet.integration.tests.trx" --diagnostic --diagnostic-output-directory "$WORKSPACE_ROOT/artifacts/log/Debug" --diagnostic-output-fileprefix "coverlet.integration.tests"
+ dotnet build-server shutdown
+ else
+ echo "Skipping command execution. Required file does not exist."
+ fi
+fi
diff --git a/global.json b/global.json
index 9128fc8e6..63ec0b6ce 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "9.0.307"
+ "version": "9.0.308"
}
}
diff --git a/src/coverlet.MTP/CoverletExtensionCollector.cs b/src/coverlet.MTP/CoverletExtensionCollector.cs
new file mode 100644
index 000000000..dc0cc2519
--- /dev/null
+++ b/src/coverlet.MTP/CoverletExtensionCollector.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// see details here: https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-architecture-extensions#the-itestsessionlifetimehandler-extensions
+// Coverlet instrumentation should be done before any test is executed, and the coverage data should be collected after all tests have run.
+// Coverlet collects code coverage data and does not need to be aware of the test framework being used. It also does not need test case details or test results.
+
+using coverlet.Extension.Logging;
+using Coverlet.Core;
+using Coverlet.Core.Abstractions;
+using Coverlet.Core.Enums;
+using Coverlet.Core.Helpers;
+using Coverlet.Core.Reporters;
+using Coverlet.Core.Symbols;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Testing.Platform.Extensions;
+using Microsoft.Testing.Platform.Extensions.TestHostControllers;
+using Microsoft.Testing.Platform.TestHost;
+
+namespace coverlet.Extension.Collector
+{
+ ///
+ /// Implements test session lifetime handling for coverage collection using the Microsoft Testing Platform.
+ ///
+ internal sealed class CoverletExtensionCollector : ITestHostProcessLifetimeHandler
+ {
+ private readonly CoverletLoggerAdapter _logger;
+ private readonly CoverletExtensionConfiguration _configuration;
+ private readonly IServiceProvider _serviceProvider;
+ private Coverage? _coverage;
+ private readonly Microsoft.Testing.Platform.Logging.ILoggerFactory _loggerFactory;
+ private readonly Microsoft.Testing.Platform.CommandLine.ICommandLineOptions _commandLineOptions;
+
+ private readonly CoverletExtension _extension = new();
+
+ string IExtension.Uid => _extension.Uid;
+
+ string IExtension.Version => _extension.Version;
+
+ string IExtension.DisplayName => _extension.DisplayName;
+
+ string IExtension.Description => _extension.Description;
+
+ ///
+ /// Initializes a new instance of the CoverletCollectorExtension class.
+ ///
+ public CoverletExtensionCollector(Microsoft.Testing.Platform.Logging.ILoggerFactory loggerFactory, Microsoft.Testing.Platform.CommandLine.ICommandLineOptions commandLineOptions)
+ {
+ _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
+ _commandLineOptions = commandLineOptions ?? throw new ArgumentNullException(nameof(commandLineOptions));
+ _configuration = new CoverletExtensionConfiguration();
+ _logger = new CoverletLoggerAdapter(_loggerFactory); // Initialize the logger adapter
+ _serviceProvider = CreateServiceProvider();
+ }
+
+ ///
+ public async Task BeforeRunAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var parameters = new CoverageParameters
+ {
+ IncludeFilters = _configuration.IncludePatterns,
+ ExcludeFilters = _configuration.ExcludePatterns,
+ IncludeTestAssembly = _configuration.IncludeTestAssembly,
+ SingleHit = false,
+ UseSourceLink = true,
+ SkipAutoProps = true,
+ ExcludeAssembliesWithoutSources = AssemblySearchType.MissingAll.ToString().ToLowerInvariant(),
+ };
+
+ string moduleDirectory = Path.GetDirectoryName(AppContext.BaseDirectory) ?? string.Empty;
+
+ _coverage = new Coverage(
+ moduleDirectory,
+ parameters,
+ _logger,
+ _serviceProvider.GetRequiredService(),
+ _serviceProvider.GetRequiredService(),
+ _serviceProvider.GetRequiredService(),
+ _serviceProvider.GetRequiredService());
+
+ // Instrument assemblies before any test execution
+ // Shall be executed asynchronous (out-process)
+ await Task.Run(() =>
+ {
+ CoveragePrepareResult prepareResult = _coverage.PrepareModules();
+ _logger.LogInformation($"Code coverage instrumentation completed. Instrumented {prepareResult.Results.Length} modules");
+ });
+
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to initialize code coverage");
+ _logger.LogError(ex);
+ }
+ }
+
+ ///
+ public async Task AfterRunAsync(int exitCode, CancellationToken cancellation)
+ {
+ try
+ {
+ if (_coverage == null)
+ {
+ _logger.LogError("Coverage instance not initialized");
+ }
+ else
+ {
+ _logger.LogInformation("\nCalculating coverage result...");
+ CoverageResult result = _coverage!.GetCoverageResult();
+
+ string dOutput = _configuration.OutputDirectory != null ? _configuration.OutputDirectory : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString();
+
+ string directory = Path.GetDirectoryName(dOutput)!;
+
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ ISourceRootTranslator sourceRootTranslator = _serviceProvider.GetRequiredService();
+ IFileSystem fileSystem = _serviceProvider.GetService()!;
+
+ // Convert to coverlet format
+ foreach (string format in _configuration.formats)
+ {
+ IReporter reporter = new ReporterFactory(format).CreateReporter();
+ if (reporter == null)
+ {
+ throw new InvalidOperationException($"Specified output format '{format}' is not supported");
+ }
+
+ if (reporter.OutputType == ReporterOutputType.Console)
+ {
+ // Output to console
+ _logger.LogInformation(" Outputting results to console", important: true);
+ _logger.LogInformation(reporter.Report(result, sourceRootTranslator), important: true);
+ }
+ else
+ {
+ // Output to file
+ string filename = Path.GetFileName(dOutput);
+ filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename;
+ filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}";
+
+ string report = Path.Combine(directory, filename);
+ _logger.LogInformation($" Generating report '{report}'", important: true);
+ await Task.Run(() => fileSystem.WriteAllText(report, reporter.Report(result, sourceRootTranslator)));
+ }
+ }
+
+ _logger.LogInformation("Code coverage collection completed");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to collect code coverage");
+ _logger.LogError(ex);
+ }
+ }
+
+ private IServiceProvider CreateServiceProvider()
+ {
+ var services = new ServiceCollection();
+
+ // Register core dependencies with explicit ILogger interface
+ services.AddSingleton(_logger); // Register the adapter with the correct interface
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Register instrumentation components with singleton lifetime
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Register SourceRootTranslator with its dependencies
+ services.AddSingleton(provider =>
+ new SourceRootTranslator(
+ _configuration.sourceMappingFile,
+ provider.GetRequiredService(),
+ provider.GetRequiredService()));
+
+ return services.BuildServiceProvider();
+ }
+
+ public Task OnTestSessionStartingAsync(SessionUid sessionUid, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task OnTestSessionFinishingAsync(SessionUid sessionUid, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ Task ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ Task ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation)
+ {
+ throw new NotImplementedException();
+ }
+
+ Task ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation)
+ {
+ throw new NotImplementedException();
+ }
+
+ Task IExtension.IsEnabledAsync()
+ {
+ return _extension.IsEnabledAsync();
+ }
+ }
+}
diff --git a/src/coverlet.MTP/CoverletExtensionCommandLineProvider.cs b/src/coverlet.MTP/CoverletExtensionCommandLineProvider.cs
new file mode 100644
index 000000000..cf8f70e6f
--- /dev/null
+++ b/src/coverlet.MTP/CoverletExtensionCommandLineProvider.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Testing.Platform.Extensions;
+using Microsoft.Testing.Platform.Extensions.CommandLine;
+
+namespace coverlet.Extension
+{
+
+ internal sealed class CoverletExtensionCommandLineProvider : ICommandLineOptionsProvider
+ {
+ private readonly IExtension _extension;
+
+ public CoverletExtensionCommandLineProvider(IExtension extension)
+ {
+ _extension = extension;
+ }
+
+ public Task IsEnabledAsync()
+ {
+ return _extension.IsEnabledAsync();
+ }
+
+ public string Uid => _extension.Uid;
+
+ public string Version => _extension.Version;
+
+ public string DisplayName => _extension.DisplayName;
+
+ public string Description => _extension.Description;
+ internal static readonly string[] s_sourceArray = new[] { "json", "lcov", "opencover", "cobertura", "teamcity" };
+
+ public IReadOnlyCollection GetCommandLineOptions()
+ {
+ // Microsoft.Testing.Platform.Extensions.CommandLine does not a default value for LineOptions
+ // Default value can be handled in validation
+
+ // see https://learn.microsoft.com/en-us/dotnet/api/system.commandline.argumentarity?view=system-commandline
+ // ExactlyOne - An arity that must have exactly one value.
+ // MaximumNumberOfValues - Gets the maximum number of values allowed for an argument.
+ // MinimumNumberOfValues - Gets the minimum number of values required for an argument.
+ // OneOrMore - An arity that must have at least one value.
+ // Zero - An arity that does not allow any values.
+ // ZeroOrMore - An arity that may have multiple values.
+ // ZeroOrOne - An arity that may have one value, but no more than one.
+
+ return
+ [
+ new CommandLineOption(name: "formats", description: "Specifies the output formats for the coverage report (e.g., 'json', 'lcov').", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "exclude", description: "Filter expressions to exclude specific modules and types.", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "include", description: "Filter expressions to include only specific modules and type", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "exclude-by-file", description: "Glob patterns specifying source files to exclude.", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "include-directory", description: "Include directories containing additional assemblies to be instrumented.", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "exclude-by-attribute", description: "Attributes to exclude from code coverage.", arity: ArgumentArity.OneOrMore, isHidden: false),
+ new CommandLineOption(name: "include-test-assembly", description: "Specifies whether to report code coverage of the test assembly.", arity: ArgumentArity.Zero, isHidden: false),
+ new CommandLineOption(name: "single-hit", description: "Specifies whether to limit code coverage hit reporting to a single hit for each location", arity: ArgumentArity.Zero, isHidden: false),
+ new CommandLineOption(name: "skipautoprops", description: "Neither track nor record auto-implemented properties.", arity: ArgumentArity.Zero, isHidden: false),
+ new CommandLineOption(name: "does-not-return-attribute", description: "Attributes that mark methods that do not return", arity: ArgumentArity.ZeroOrMore, isHidden: false),
+ new CommandLineOption(name: "exclude-assemblies-without-sources", description: "Specifies behavior of heuristic to ignore assemblies with missing source documents.", arity: ArgumentArity.ZeroOrOne, isHidden: false),
+ new CommandLineOption(name: "source-mapping-file", description: "Specifies the path to a SourceRootsMappings file.", arity: ArgumentArity.ZeroOrOne, isHidden: false)
+ ];
+ }
+
+ public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments)
+ {
+ if (commandOption.Name == "formats" )
+ {
+ // When no arguments are provided, validation should pass (default "json" will be used)
+ if (arguments.Length == 0 || arguments.Any(string.IsNullOrWhiteSpace))
+ {
+ return ValidationResult.ValidTask;
+ }
+ // Validate provided formats
+ foreach (string format in arguments)
+ {
+ if (!s_sourceArray.Contains(format))
+ {
+ return Task.FromResult(ValidationResult.Invalid($"The value '{format}' is not a valid option for '{commandOption.Name}'."));
+ }
+ }
+ return ValidationResult.ValidTask;
+ }
+ if (commandOption.Name == "exclude-assemblies-without-sources")
+ {
+ if (arguments.Length == 0)
+ {
+ return Task.FromResult(ValidationResult.Invalid($"At least one value must be specified for '{commandOption.Name}'."));
+ }
+ if (arguments.Length > 1)
+ {
+ return Task.FromResult(ValidationResult.Invalid($"Only one value is allowed for '{commandOption.Name}'."));
+ }
+ if (!arguments[0].Contains("MissingAll") && !arguments[0].Contains("MissingAny") && !arguments[0].Contains("None"))
+ {
+ return Task.FromResult(ValidationResult.Invalid($"The value '{arguments[0]}' is not a valid option for '{commandOption.Name}'."));
+ }
+ }
+ return ValidationResult.ValidTask;
+ }
+
+ public Task ValidateCommandLineOptionsAsync(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions commandLineOptions)
+ {
+ return ValidationResult.ValidTask;
+ }
+
+ }
+}
diff --git a/src/coverlet.MTP/CoverletExtensionConfiguration.cs b/src/coverlet.MTP/CoverletExtensionConfiguration.cs
new file mode 100644
index 000000000..b2e68b5d0
--- /dev/null
+++ b/src/coverlet.MTP/CoverletExtensionConfiguration.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// see details here: https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-architecture-extensions#the-itestsessionlifetimehandler-extensions
+// Coverlet instrumentation should be done before any test is executed, and the coverage data should be collected after all tests have run.
+// Coverlet collects code coverage data and does not need to be aware of the test framework being used. It also does not need test case details or test results.
+
+//using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Testing.Platform.Services;
+
+namespace coverlet.Extension
+{
+ internal class CoverletExtensionConfiguration
+ {
+ public string[] IncludePatterns { get; set; } = Array.Empty();
+ public string[] ExcludePatterns { get; set; } = Array.Empty();
+ public bool IncludeTestAssembly { get; set; }
+ public string OutputDirectory { get; set; } = string.Empty;
+ public string sourceMappingFile { get; set; } = string.Empty;
+ public bool EnableSourceMapping { get; set; }
+ public string[] formats { get; set; } = ["json"];
+
+ //public const string PipeName = "TESTINGPLATFORM_COVERLET_PIPENAME";
+ //public const string MutexName = "TESTINGPLATFORM_COVERLET_MUTEXNAME";
+ //public const string MutexNameSuffix = "TESTINGPLATFORM_COVERLET_MUTEXNAME_SUFFIX";
+
+ //public CoverletExtensionConfiguration(ITestApplicationModuleInfo testApplicationModuleInfo, PipeNameDescription pipeNameDescription, string mutexSuffix)
+ //{
+ // PipeNameValue = pipeNameDescription.Name;
+ // PipeNameKey = $"{PipeName}_{FNV_1aHashHelper.ComputeStringHash(testApplicationModuleInfo.GetCurrentTestApplicationFullPath())}_{mutexSuffix}";
+ // MutexSuffix = mutexSuffix;
+ //}
+ //public string PipeNameKey { get; } = PipeName;
+
+ //public string PipeNameValue { get; }
+ //public string MutexSuffix { get; }
+ public bool Enable { get; set; } = true;
+ }
+ public interface ICommandLineOptions
+ {
+ bool IsOptionSet(string optionName);
+
+ bool TryGetOptionArgumentList(
+ string optionName,
+ out string[]? arguments);
+ }
+ internal class GetCommandLineValues
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ICommandLineOptions _commandLineOptions;
+
+ public GetCommandLineValues(IServiceProvider serviceProvider, ICommandLineOptions commandLineOptions)
+ {
+ _serviceProvider = serviceProvider;
+ _commandLineOptions = commandLineOptions;
+ }
+
+ public void InitializeFromCommandLineArgs()
+ {
+ IServiceCollection serviceCollection = new ServiceCollection();
+ ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
+ ICommandLineOptions commandLineOptions = (ICommandLineOptions)_serviceProvider.GetCommandLineOptions();
+ CoverletExtensionConfiguration configuration = new CoverletExtensionConfiguration();
+
+ if (commandLineOptions.IsOptionSet("include"))
+ {
+ if (commandLineOptions.TryGetOptionArgumentList("include", out string[]? includeArgs))
+ {
+ configuration.IncludePatterns = includeArgs ?? Array.Empty();
+ }
+ else
+ {
+ configuration.IncludePatterns = Array.Empty();
+ }
+ }
+
+ if (commandLineOptions.IsOptionSet("exclude"))
+ {
+ if (commandLineOptions.TryGetOptionArgumentList("exclude", out string[]? excludeArgs))
+ {
+ configuration.ExcludePatterns = excludeArgs ?? Array.Empty();
+ }
+ else
+ {
+ configuration.ExcludePatterns = Array.Empty();
+ }
+ }
+
+ if (commandLineOptions.IsOptionSet("output-directory"))
+ {
+ if (commandLineOptions.TryGetOptionArgumentList("output-directory", out string[]? outputDirectoryArgs))
+ {
+ configuration.sourceMappingFile = outputDirectoryArgs!.Length > 0 ? outputDirectoryArgs[0] : string.Empty;
+ }
+ else
+ {
+ configuration.OutputDirectory = string.Empty;
+ }
+ }
+
+ if (commandLineOptions.IsOptionSet("source-mapping-file"))
+ {
+ if (commandLineOptions.TryGetOptionArgumentList("source-mapping-file", out string[]? sourceMappingFileArgs))
+ {
+ configuration.sourceMappingFile = sourceMappingFileArgs!.Length > 0 ? sourceMappingFileArgs[0] : string.Empty;
+ }
+ else
+ {
+ configuration.sourceMappingFile = string.Empty;
+ }
+ }
+
+ if (commandLineOptions.IsOptionSet("include-test-assembly"))
+ {
+ configuration.IncludeTestAssembly = true;
+ }
+ }
+ }
+}
diff --git a/src/coverlet.MTP/CoverletExtensionEnvironmentVariableProvider.cs b/src/coverlet.MTP/CoverletExtensionEnvironmentVariableProvider.cs
new file mode 100644
index 000000000..5a9f20fb5
--- /dev/null
+++ b/src/coverlet.MTP/CoverletExtensionEnvironmentVariableProvider.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using coverlet.Extension;
+using Microsoft.Testing.Platform.Configurations;
+using Microsoft.Testing.Platform.Extensions;
+using Microsoft.Testing.Platform.Extensions.TestHostControllers;
+using Microsoft.Testing.Platform.Logging;
+
+namespace Microsoft.Testing.Extensions.Diagnostics;
+
+#pragma warning disable CS9113 // Parameter is unread.
+internal sealed class CoverletExtensionEnvironmentVariableProvider(IConfiguration configuration, Platform.CommandLine.ICommandLineOptions commandLineOptions, ILoggerFactory loggerFactory) : ITestHostEnvironmentVariableProvider
+#pragma warning restore CS9113 // Parameter is unread.
+{
+ //private readonly coverlet.Extension.ICommandLineOptions _commandLineOptions = commandLineOptions;
+ //private readonly CoverletExtensionConfiguration? _coverletExtensionConfiguration;
+ private readonly CoverletExtension _extension = new();
+ private readonly IConfiguration _configuration = configuration;
+ //private readonly Platform.CommandLine.ICommandLineOptions _commandLineOptions;
+ //private readonly Platform.Logging.ILoggerFactory _loggerFactory = loggerFactory;
+ //private readonly Platform.CommandLine.ICommandLineOptions? _commandLineOptions;
+
+ //private readonly ILogger _logger = loggerFactory.CreateLogger();
+ public string Uid => nameof(CoverletExtensionEnvironmentVariableProvider);
+
+ public string Version => _extension.Version;
+
+ public string DisplayName => _extension.DisplayName;
+
+ public string Description => _extension.Description;
+
+ public Task IsEnabledAsync() => Task.FromResult(true);
+
+ public Task UpdateAsync(IEnvironmentVariables environmentVariables)
+ {
+ //environmentVariables.SetVariable(
+ // new(_CoverletExtensionConfiguration.PipeNameKey, _CoverletExtensionConfiguration.PipeNameValue, false, true));
+ //environmentVariables.SetVariable(
+ // new(CoverletExtensionConfiguration.MutexNameSuffix, _CoverletExtensionConfiguration.MutexSuffix, false, true));
+ return Task.CompletedTask;
+ }
+
+ public Task ValidateTestHostEnvironmentVariablesAsync(IReadOnlyEnvironmentVariables environmentVariables)
+ {
+
+ // No problem found
+ return ValidationResult.ValidTask;
+ }
+}
diff --git a/src/coverlet.MTP/CoverletExtensionProvider.cs b/src/coverlet.MTP/CoverletExtensionProvider.cs
new file mode 100644
index 000000000..f9f5f27ef
--- /dev/null
+++ b/src/coverlet.MTP/CoverletExtensionProvider.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using coverlet.Extension.Collector;
+using Microsoft.Testing.Extensions.Diagnostics;
+using Microsoft.Testing.Platform.Builder;
+using Microsoft.Testing.Platform.Extensions.TestHostControllers;
+using Microsoft.Testing.Platform.Services;
+
+namespace coverlet.Extension
+{
+ public static class CoverletExtensionProvider
+ {
+ public static void AddCoverletExtensionProvider(this ITestApplicationBuilder builder, bool ignoreIfNotSupported = false)
+ {
+ CoverletExtension _extension = new();
+ CoverletExtensionConfiguration coverletExtensionConfiguration = new();
+ if (ignoreIfNotSupported)
+ {
+#if !NETCOREAPP
+ coverletExtensionConfiguration.Enable = false;
+#endif
+ }
+
+ builder.TestHostControllers.AddEnvironmentVariableProvider(serviceProvider
+ => new CoverletExtensionEnvironmentVariableProvider(
+ serviceProvider.GetConfiguration(),
+ serviceProvider.GetCommandLineOptions(),
+ serviceProvider.GetLoggerFactory()));
+
+ // Fix for CS0029 and CS1662:
+ // Ensure that CoverletExtensionCollector implements ITestHostProcessLifetimeHandler
+ builder.TestHostControllers.AddProcessLifetimeHandler(static serviceProvider
+ => new CoverletExtensionCollector(
+ serviceProvider.GetLoggerFactory(),
+ serviceProvider.GetCommandLineOptions()) as ITestHostProcessLifetimeHandler);
+
+ builder.CommandLine.AddProvider(() => new CoverletExtensionCommandLineProvider(_extension));
+
+ }
+ }
+}
diff --git a/src/coverlet.MTP/Logging/CoverletLoggerAdapter.cs b/src/coverlet.MTP/Logging/CoverletLoggerAdapter.cs
new file mode 100644
index 000000000..861c0797a
--- /dev/null
+++ b/src/coverlet.MTP/Logging/CoverletLoggerAdapter.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Testing.Platform.Logging;
+
+namespace coverlet.Extension.Logging
+{
+ internal class CoverletLoggerAdapter : Coverlet.Core.Abstractions.ILogger
+ {
+ private readonly Microsoft.Testing.Platform.Logging.ILogger _logger;
+
+ public CoverletLoggerAdapter(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger("Coverlet");
+ }
+
+ public void LogVerbose(string message)
+ {
+ _logger.LogTrace(message);
+ }
+
+ public void LogInformation(string message, bool important = false)
+ {
+ if (important)
+ {
+ _logger.LogInformation($"[Important] {message}");
+ }
+ else
+ {
+ _logger.LogInformation(message);
+ }
+ }
+
+ public void LogWarning(string message)
+ {
+ _logger.LogWarning(message);
+ }
+
+ public void LogError(string message)
+ {
+ _logger.LogError(message);
+ }
+
+ public void LogError(Exception exception)
+ {
+ _logger.LogError(exception.ToString());
+ }
+ }
+}
diff --git a/src/coverlet.MTP/Properties/AssemblyInfo.cs b/src/coverlet.MTP/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..ea48a0c30
--- /dev/null
+++ b/src/coverlet.MTP/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection;
+
+[assembly: AssemblyKeyFile("coverlet.MTP.snk")]
diff --git a/src/coverlet.MTP/TestingPlatformBuilderHook.cs b/src/coverlet.MTP/TestingPlatformBuilderHook.cs
new file mode 100644
index 000000000..0c90b2b87
--- /dev/null
+++ b/src/coverlet.MTP/TestingPlatformBuilderHook.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Testing.Platform.Builder;
+
+namespace coverlet.Extension
+{
+ public static class TestingPlatformBuilderHook
+ {
+ ///
+ /// Adds crash dump support to the Testing Platform Builder.
+ ///
+ /// The test application builder.
+ /// The command line arguments.
+ public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] _)
+ {
+ // Ensure AddCoverletCoverageProvider is implemented or accessible
+ testApplicationBuilder.AddCoverletExtensionProvider();
+ }
+ }
+}
diff --git a/src/coverlet.MTP/build/coverlet.MTP.props b/src/coverlet.MTP/build/coverlet.MTP.props
new file mode 100644
index 000000000..f40001f37
--- /dev/null
+++ b/src/coverlet.MTP/build/coverlet.MTP.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/coverlet.MTP/build/coverlet.MTP.targets b/src/coverlet.MTP/build/coverlet.MTP.targets
new file mode 100644
index 000000000..0972175c5
--- /dev/null
+++ b/src/coverlet.MTP/build/coverlet.MTP.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.props b/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.props
new file mode 100644
index 000000000..0df982f9c
--- /dev/null
+++ b/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.props
@@ -0,0 +1,14 @@
+
+
+
+
+ Coverlet Code Coverage
+ coverlet.Extension.TestingPlatformBuilderHook
+
+
+
+
diff --git a/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.targets b/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.targets
new file mode 100644
index 000000000..9d105d1ee
--- /dev/null
+++ b/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.targets
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_CoverletSdkNETCoreSdkVersion>$(NETCoreSdkVersion)
+ <_CoverletSdkNETCoreSdkVersion Condition="$(_CoverletSdkNETCoreSdkVersion.Contains('-'))">$(_CoverletSdkNETCoreSdkVersion.Split('-')[0])
+ <_CoverletSdkMinVersionWithDependencyTarget>8.0.400
+ <_CoverletSourceRootTargetName>CoverletGetPathMap
+ <_CoverletSourceRootTargetName Condition="'$([System.Version]::Parse($(_CoverletSdkNETCoreSdkVersion)).CompareTo($([System.Version]::Parse($(_CoverletSdkMinVersionWithDependencyTarget)))))' >= '0' ">InitializeSourceRootMappedPaths
+
+
+
+
+
+
+
+ <_byProject Include="@(_LocalTopLevelSourceRoot->'%(MSBuildSourceProjectFile)')" OriginalPath="%(Identity)" />
+ <_mapping Include="@(_byProject->'%(Identity)|%(OriginalPath)=%(MappedPath)')" />
+
+
+ <_sourceRootMappingFilePath>$([MSBuild]::EnsureTrailingSlash('$(OutputPath)'))CoverletSourceRootsMapping_$(AssemblyName)
+
+
+
+
+
+
+
diff --git a/src/coverlet.MTP/buildTransitive/coverlet.MTP.props b/src/coverlet.MTP/buildTransitive/coverlet.MTP.props
new file mode 100644
index 000000000..7fea62ce7
--- /dev/null
+++ b/src/coverlet.MTP/buildTransitive/coverlet.MTP.props
@@ -0,0 +1,4 @@
+
+
+
diff --git a/src/coverlet.MTP/buildTransitive/coverlet.MTP.targets b/src/coverlet.MTP/buildTransitive/coverlet.MTP.targets
new file mode 100644
index 000000000..0972175c5
--- /dev/null
+++ b/src/coverlet.MTP/buildTransitive/coverlet.MTP.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/coverlet.MTP/coverlet.MTP.csproj b/src/coverlet.MTP/coverlet.MTP.csproj
new file mode 100644
index 000000000..7bb676783
--- /dev/null
+++ b/src/coverlet.MTP/coverlet.MTP.csproj
@@ -0,0 +1,108 @@
+
+
+
+ $(NetMinimum);netstandard2.0
+ Coverlet.MTP
+ true
+ true
+ enable
+ enable
+ $(NoWarn)
+ false
+
+ false
+ $(TargetsForTfmSpecificContentInPackage);CopyProjectReferencesToPackage
+
+ false
+
+ true
+ true
+ true
+
+ false
+
+
+
+
+
+ coverlet.MTP
+ coverlet.MTP
+ tonerdo
+ MIT
+ https://github.com/coverlet-coverage/coverlet
+ https://raw.githubusercontent.com/tonerdo/coverlet/master/_assets/coverlet-icon.svg?sanitize=true
+ coverlet-icon.png
+ false
+ coverage code coverage for Microsoft Testing Platform
+ coverage;microsoft-testing-platform;code-coverage
+ Coverlet.MTP.Integration.md
+ https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/Changelog.md
+ git
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ buildMultiTargeting
+
+
+ buildTransitive/$(TargetFramework)
+
+
+ build/$(TargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/coverlet.MTP/coverlet.MTP.snk b/src/coverlet.MTP/coverlet.MTP.snk
new file mode 100644
index 000000000..0571a4f19
Binary files /dev/null and b/src/coverlet.MTP/coverlet.MTP.snk differ
diff --git a/src/coverlet.MTP/coverletExtension.cs b/src/coverlet.MTP/coverletExtension.cs
new file mode 100644
index 000000000..2088be260
--- /dev/null
+++ b/src/coverlet.MTP/coverletExtension.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Testing.Platform.Extensions;
+
+namespace coverlet.Extension;
+
+internal class CoverletExtension : IExtension
+{
+ public string Uid => nameof(CoverletExtension);
+
+ public string DisplayName => "Coverlet Code Coverage Collector";
+
+ public string Version => typeof(CoverletExtension).Assembly.GetName().Version?.ToString() ?? "1.0.0";
+
+ public string Description => "Provides code coverage collection for the Microsoft Testing Platform";
+
+ public Task IsEnabledAsync() => Task.FromResult(true);
+}
diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs
index 4e3f5a729..88b4aa78d 100644
--- a/src/coverlet.collector/DataCollection/CoverageWrapper.cs
+++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs
@@ -38,7 +38,8 @@ public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger
SkipAutoProps = settings.SkipAutoProps,
DoesNotReturnAttributes = settings.DoesNotReturnAttributes,
DeterministicReport = settings.DeterministicReport,
- ExcludeAssembliesWithoutSources = settings.ExcludeAssembliesWithoutSources
+ ExcludeAssembliesWithoutSources = settings.ExcludeAssembliesWithoutSources,
+ DisableManagedInstrumentationRestore = settings.DisableManagedInstrumentationRestore
};
return new Coverage(
diff --git a/src/coverlet.collector/DataCollection/CoverletSettings.cs b/src/coverlet.collector/DataCollection/CoverletSettings.cs
index 0c80687f9..798727c75 100644
--- a/src/coverlet.collector/DataCollection/CoverletSettings.cs
+++ b/src/coverlet.collector/DataCollection/CoverletSettings.cs
@@ -86,6 +86,11 @@ internal class CoverletSettings
///
public string ExcludeAssembliesWithoutSources { get; set; }
+ ///
+ /// Disable managed instrumentation restore flag
+ ///
+ public bool DisableManagedInstrumentationRestore { get; set; }
+
public override string ToString()
{
var builder = new StringBuilder();
diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
index 733dacfcc..76a849dd5 100644
--- a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
+++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
@@ -48,6 +48,7 @@ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable
+ /// Disable Managed Instrumentation Restore flag
+ ///
+ /// Configuration element
+ /// Include Test Assembly Flag
+ private static bool ParseDisableManagedInstrumentationRestore(XmlElement configurationElement)
+ {
+ XmlElement disableManagedInstrumentationRestoreElement = configurationElement[CoverletConstants.DisableManagedInstrumentationRestore];
+ bool.TryParse(disableManagedInstrumentationRestoreElement?.InnerText, out bool disableManagedInstrumentationRestore);
+ return disableManagedInstrumentationRestore;
+ }
+
///
/// Parse skipautoprops flag
///
diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs
index 5ce4a79ef..5194c0511 100644
--- a/src/coverlet.collector/Utilities/CoverletConstants.cs
+++ b/src/coverlet.collector/Utilities/CoverletConstants.cs
@@ -27,5 +27,6 @@ internal static class CoverletConstants
public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute";
public const string DeterministicReport = "DeterministicReport";
public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources";
+ public const string DisableManagedInstrumentationRestore = "DisableManagedInstrumentationRestore";
}
}
diff --git a/src/coverlet.collector/coverlet.collector.csproj b/src/coverlet.collector/coverlet.collector.csproj
index bbbf049fb..034cc215a 100644
--- a/src/coverlet.collector/coverlet.collector.csproj
+++ b/src/coverlet.collector/coverlet.collector.csproj
@@ -1,6 +1,6 @@
- $(NetMinimum);netstandard2.0
+ $(NetMinimum)
coverlet.collector
true
true
@@ -43,6 +43,7 @@
+
diff --git a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs
index d363fab63..69ace65c1 100644
--- a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs
+++ b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs
@@ -8,7 +8,7 @@ namespace Coverlet.Core.Abstractions
{
internal interface IInstrumentationHelper
{
- void BackupOriginalModule(string module, string identifier);
+ void BackupOriginalModule(string module, string identifier, bool disableManagedInstrumentationRestore);
void DeleteHitsFile(string path);
string[] GetCoverableModules(string module, string[] directories, bool includeTestAssembly);
bool HasPdb(string module, out bool embedded);
diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs
index 918d8aedf..3de0cfd98 100644
--- a/src/coverlet.core/Coverage.cs
+++ b/src/coverlet.core/Coverage.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Toni Solarin-Sodara
+// Copyright (c) Toni Solarin-Sodara
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
@@ -45,6 +45,8 @@ internal class CoverageParameters
public bool DeterministicReport { get; set; }
[DataMember]
public string ExcludeAssembliesWithoutSources { get; set; }
+ [DataMember]
+ public bool DisableManagedInstrumentationRestore { get; set; }
}
internal class Coverage
@@ -134,7 +136,7 @@ public CoveragePrepareResult PrepareModules()
if (instrumenter.CanInstrument())
{
- _instrumentationHelper.BackupOriginalModule(module, Identifier);
+ _instrumentationHelper.BackupOriginalModule(module, Identifier, _parameters.DisableManagedInstrumentationRestore);
// Guard code path and restore if instrumentation fails.
try
diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs
index e6e3f0702..88b0a2c91 100644
--- a/src/coverlet.core/Helpers/InstrumentationHelper.cs
+++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs
@@ -237,35 +237,28 @@ private bool MatchDocumentsWithSourcesMissingAll(MetadataReader metadataReader)
///
/// The path to the module to be backed up.
/// A unique identifier to distinguish the backup file.
- public void BackupOriginalModule(string module, string identifier)
- {
- BackupOriginalModule(module, identifier, true);
- }
-
- ///
- /// Backs up the original module to a specified location.
- ///
- /// The path to the module to be backed up.
- /// A unique identifier to distinguish the backup file.
- /// Indicates whether to add the backup to the backup list. Required for test TestBackupOriginalModule
- public void BackupOriginalModule(string module, string identifier, bool withBackupList)
+ ///
+ public void BackupOriginalModule(string module, string identifier, bool disableManagedInstrumentationRestore)
{
string backupPath = GetBackupPath(module, identifier);
string backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb");
- _fileSystem.Copy(module, backupPath, true);
- if (withBackupList && !_backupList.TryAdd(module, backupPath))
+ if (!disableManagedInstrumentationRestore)
{
- throw new ArgumentException($"Key already added '{module}'");
- }
-
- string symbolFile = Path.ChangeExtension(module, ".pdb");
- if (_fileSystem.Exists(symbolFile))
- {
- _fileSystem.Copy(symbolFile, backupSymbolPath, true);
- if (withBackupList && !_backupList.TryAdd(symbolFile, backupSymbolPath))
+ _fileSystem.Copy(module, backupPath, true);
+ if (!_backupList.TryAdd(module, backupPath))
{
throw new ArgumentException($"Key already added '{module}'");
}
+
+ string symbolFile = Path.ChangeExtension(module, ".pdb");
+ if (_fileSystem.Exists(symbolFile))
+ {
+ _fileSystem.Copy(symbolFile, backupSymbolPath, true);
+ if (!_backupList.TryAdd(symbolFile, backupSymbolPath))
+ {
+ throw new ArgumentException($"Key already added '{module}'");
+ }
+ }
}
}
diff --git a/src/coverlet.core/Properties/AssemblyInfo.cs b/src/coverlet.core/Properties/AssemblyInfo.cs
index 0a6d02544..4279f28f2 100644
--- a/src/coverlet.core/Properties/AssemblyInfo.cs
+++ b/src/coverlet.core/Properties/AssemblyInfo.cs
@@ -9,6 +9,7 @@
[assembly: InternalsVisibleTo("coverlet.msbuild.tasks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e5f154a600df71cbdc8a8e69af077379c00889b9a597fbcac536c114911641809ef03b34a33dbe7befe8ea76535889175098bda0710bce04e321689e4458fc7515ca4a074b8618ad61489ec4d71171352e73ed04baeb1d8b8e4855342ef217968da2eebdfc53e119cdd93500a973974a3aed57c400f9bb187f784b0a0924099b")]
[assembly: InternalsVisibleTo("coverlet.console, PublicKey=00240000048000009400000006020000002400005253413100040000010001002515029761c695320036d518d74cc27defddd346afbfb4f16152ae3f4f0e779ae2fe048671a4ac3af595625db8e59fa3b5eeac22c06eacaebb54137ee8973449b68c5da8bbef903c2ac2d0b54143faf82f1b813fd24facfd5b6c7041ae5955ec63ba17cc57037b98eecbe44c7d2833c3aeabcc4e23109763f580067a74adacae")]
[assembly: InternalsVisibleTo("coverlet.collector, PublicKey=00240000048000009400000006020000002400005253413100040000010001003d23b9ef372215da7c81af920b919db5799fd021a1ca10b2e9e0ddac71237a29f8f6361a805a747457e561a7d616417f1870cda099486df25d580a4e11a0738293342881566254d7840e42f42fb9bfd8e8dca354df7dc68db14b2d0cd79bb2bf7afdbd62bd948d81b534cba7a326cf6ee840a1aee5dba0a1c660b30813ca99e5")]
+[assembly: InternalsVisibleTo("coverlet.MTP, PublicKey=00240000048000009400000006020000002400005253413100040000010001008975ae08cb877d76953491edb19b1422644aa480554144cbe2b645c8d9d05d96f53bedfb64e25a6abaa3b20ce6b850de907b88cae77aa183910fb522b289880c8eade9834aef64f98af8b521273ed65adce56db7700056c011841362f552bc144453078e4b9b77a2962206ff577fa476ddc657bde85819637d10a5cd18a3aed7")]
[assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
[assembly: InternalsVisibleTo("coverlet.core.coverage.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100094aad8eb75c06c9f2443dda84573b8db55cd6678452a60010db2643467ac28928db3a06b0b1ac3016645b448937d5e671b36504bcfc0fda27e996c5e1b0ee49747145cda6d47508d1e3c60b144634d95e33d4efe49536372df8139f48d3d897ae6931c2876d4f5d00215fd991cbcecde2705e53e19309e21c8b59d19eb925b1")]
diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj
index a974bd7df..df1c4ad87 100644
--- a/src/coverlet.core/coverlet.core.csproj
+++ b/src/coverlet.core/coverlet.core.csproj
@@ -2,7 +2,7 @@
Library
- $(NetMinimum);netstandard2.0
+ $(NetMinimum);$(NetCurrent);netstandard2.0
false
$(NoWarn);IDE0057
@@ -13,6 +13,7 @@
+
diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs
index 33701ca05..6f87cdcc7 100644
--- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs
+++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs
@@ -49,6 +49,8 @@ public class InstrumentationTask : BaseTask
public string ExcludeAssembliesWithoutSources { get; set; }
+ public bool DisableManagedInstrumentationRestore { get; set; }
+
[Output]
public ITaskItem InstrumenterState { get; set; }
@@ -103,7 +105,8 @@ public override bool Execute()
SkipAutoProps = SkipAutoProps,
DeterministicReport = DeterministicReport,
ExcludeAssembliesWithoutSources = ExcludeAssembliesWithoutSources,
- DoesNotReturnAttributes = DoesNotReturnAttribute?.Split(',')
+ DoesNotReturnAttributes = DoesNotReturnAttribute?.Split(','),
+ DisableManagedInstrumentationRestore = DisableManagedInstrumentationRestore
};
var coverage = new Coverage(Path,
diff --git a/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props b/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props
index 53ec786b4..7a596a7aa 100644
--- a/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props
+++ b/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props
@@ -24,6 +24,6 @@
$(MSBuildThisFileDirectory)../tasks/net8.0/
- $(MSBuildThisFileDirectory)../tasks/netstandard2.0/
+
diff --git a/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.targets b/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.targets
index 0defe0138..53d6644cb 100644
--- a/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.targets
+++ b/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.targets
@@ -50,7 +50,8 @@
SkipAutoProps="$(SkipAutoProps)"
DeterministicReport="$(DeterministicReport)"
DoesNotReturnAttribute="$(DoesNotReturnAttribute)"
- ExcludeAssembliesWithoutSources="$(ExcludeAssembliesWithoutSources)">
+ ExcludeAssembliesWithoutSources="$(ExcludeAssembliesWithoutSources)"
+ DisableManagedInstrumentationRestore="$(DisableManagedInstrumentationRestore)">
diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
index 72f8f7c3c..22c2169f6 100644
--- a/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
+++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
@@ -2,7 +2,7 @@
Library
- netstandard2.0;$(NetMinimum)
+ $(NetMinimum);netstandard2.0
coverlet.msbuild.tasks
true
$(TargetsForTfmSpecificContentInPackage);PackBuildOutputs
diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets
index 8b989e8bd..ea27ddc0e 100644
--- a/test/Directory.Build.targets
+++ b/test/Directory.Build.targets
@@ -14,7 +14,7 @@
This is required when the coverlet.msbuild imports are made in their src directory
(so that msbuild eval works even before they are built)
so that they can still find the tooling that will be built by the build. -->
- $(RepoRoot)artifacts\bin\coverlet.msbuild.tasks\$(Configuration.ToLowerInvariant())_netstandard2.0\
+ $(RepoRoot)artifacts\bin\coverlet.msbuild.tasks\$(Configuration.ToLowerInvariant())_net8.0\
diff --git a/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs b/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs
new file mode 100644
index 000000000..50855b52b
--- /dev/null
+++ b/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs
@@ -0,0 +1,131 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+using coverlet.Extension;
+using Microsoft.Testing.Platform.Extensions.CommandLine;
+using Xunit;
+
+namespace coverlet.MTP.unit.tests
+{
+ public class CoverletMTPCommandLineTests
+ {
+ private readonly CoverletExtension _extension = new();
+ private readonly CoverletExtensionCommandLineProvider _provider;
+
+ public CoverletMTPCommandLineTests()
+ {
+ _provider = new CoverletExtensionCommandLineProvider(_extension);
+ }
+
+ [Theory]
+ [InlineData("formats", "invalid", "The value 'invalid' is not a valid option for 'formats'.")]
+ [InlineData("exclude-assemblies-without-sources", "invalid", "The value 'invalid' is not a valid option for 'exclude-assemblies-without-sources'.")]
+ [InlineData("exclude-assemblies-without-sources", "", "At least one value must be specified for 'exclude-assemblies-without-sources'.")]
+ public async Task IsInvalid_When_Option_Has_InvalidValue(string optionName, string value, string expectedError)
+ {
+ CommandLineOption option = _provider.GetCommandLineOptions().First(x => x.Name == optionName);
+ var arguments = string.IsNullOrEmpty(value) ? Array.Empty() : [value];
+
+ var result = await _provider.ValidateOptionArgumentsAsync(option, arguments);
+
+ Assert.False(result.IsValid);
+ Assert.Equal(expectedError, result.ErrorMessage);
+ }
+
+ [Theory]
+ [InlineData("formats", "json")]
+ [InlineData("formats", "lcov")]
+ [InlineData("formats", "opencover")]
+ [InlineData("formats", "cobertura")]
+ [InlineData("formats", "teamcity")]
+ [InlineData("exclude-assemblies-without-sources", "MissingAll")]
+ [InlineData("exclude-assemblies-without-sources", "MissingAny")]
+ [InlineData("exclude-assemblies-without-sources", "None")]
+ public async Task IsValid_When_Option_Has_ValidValue(string optionName, string value)
+ {
+ CommandLineOption option = _provider.GetCommandLineOptions().First(x => x.Name == optionName);
+
+ var result = await _provider.ValidateOptionArgumentsAsync(option, [value]);
+
+ Assert.True(result.IsValid);
+ Assert.True(string.IsNullOrEmpty(result.ErrorMessage));
+ }
+
+ [Theory]
+ [InlineData("exclude")]
+ [InlineData("include")]
+ [InlineData("exclude-by-file")]
+ [InlineData("include-directory")]
+ [InlineData("exclude-by-attribute")]
+ [InlineData("does-not-return-attribute")]
+ [InlineData("source-mapping-file")]
+ public async Task IsValid_For_NonValidated_Options(string optionName)
+ {
+ CommandLineOption option = _provider.GetCommandLineOptions().First(x => x.Name == optionName);
+
+ var result = await _provider.ValidateOptionArgumentsAsync(option, ["any-value"]);
+
+ Assert.True(result.IsValid);
+ Assert.True(string.IsNullOrEmpty(result.ErrorMessage));
+ }
+
+ [Theory]
+ [InlineData("include-test-assembly")]
+ [InlineData("single-hit")]
+ [InlineData("skipautoprops")]
+ public async Task IsValid_For_FlagOptions(string optionName)
+ {
+ CommandLineOption option = _provider.GetCommandLineOptions().First(x => x.Name == optionName);
+
+ var result = await _provider.ValidateOptionArgumentsAsync(option, []);
+
+ Assert.True(result.IsValid);
+ Assert.True(string.IsNullOrEmpty(result.ErrorMessage));
+ }
+
+ [Fact]
+ public void GetCommandLineOptions_Returns_AllExpectedOptions()
+ {
+ var options = _provider.GetCommandLineOptions();
+
+ var expectedOptions = new[]
+ {
+ "formats",
+ "exclude",
+ "include",
+ "exclude-by-file",
+ "include-directory",
+ "exclude-by-attribute",
+ "include-test-assembly",
+ "single-hit",
+ "skipautoprops",
+ "does-not-return-attribute",
+ "exclude-assemblies-without-sources",
+ "source-mapping-file"
+ };
+
+ Assert.Equal(expectedOptions.Length, options.Count);
+ Assert.All(expectedOptions, name => Assert.Contains(options, o => o.Name == name));
+ }
+
+ [Fact]
+ public async Task ValidateCommandLineOptions_IsAlwaysValid()
+ {
+ var validateOptionsResult = await _provider.ValidateCommandLineOptionsAsync(new TestCommandLineOptions([]));
+ Assert.True(validateOptionsResult.IsValid);
+ Assert.True(string.IsNullOrEmpty(validateOptionsResult.ErrorMessage));
+ }
+
+ internal sealed class TestCommandLineOptions : Microsoft.Testing.Platform.CommandLine.ICommandLineOptions
+ {
+ private readonly Dictionary _options;
+
+ public TestCommandLineOptions(Dictionary options) => _options = options;
+
+ public bool IsOptionSet(string optionName) => _options.ContainsKey(optionName);
+
+ public bool TryGetOptionArgumentList(string optionName, [NotNullWhen(true)] out string[]? arguments) => _options.TryGetValue(optionName, out arguments);
+ }
+ }
+}
diff --git a/test/coverlet.MTP.unit.tests/Properties/AssemblyInfo.cs b/test/coverlet.MTP.unit.tests/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..072fdbc21
--- /dev/null
+++ b/test/coverlet.MTP.unit.tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection;
+
+[assembly: AssemblyKeyFile("coverlet.MTP.unit.tests.snk")]
diff --git a/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.csproj b/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.csproj
new file mode 100644
index 000000000..c7cc75272
--- /dev/null
+++ b/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+ TargetFramework=netstandard2.0
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.snk b/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.snk
new file mode 100644
index 000000000..8e27ac2d8
Binary files /dev/null and b/test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.snk differ
diff --git a/test/coverlet.MTP.validation.tests/CollectCoverageTests.cs b/test/coverlet.MTP.validation.tests/CollectCoverageTests.cs
new file mode 100644
index 000000000..26719535f
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/CollectCoverageTests.cs
@@ -0,0 +1,618 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Text.Json;
+using System.Xml.Linq;
+using Xunit;
+
+namespace coverlet.MTP.validation.tests;
+
+///
+/// Integration tests for Coverlet Microsoft Testing Platform extension.
+/// These tests verify code instrumentation and coverage data collection using MTP.
+/// Similar to coverlet.integration.tests.Collectors but for Microsoft Testing Platform instead of VSTest.
+///
+public class CollectCoverageTests
+{
+ private readonly string _buildConfiguration;
+ private readonly string _buildTargetFramework;
+ private readonly string _localPackagesPath;
+ private const string CoverageJsonFileName = "coverage.json";
+ private const string CoverageCoberturaFileName = "coverage.cobertura.xml";
+ private readonly string _repoRoot;
+
+ public CollectCoverageTests()
+ {
+ _buildConfiguration = "Debug";
+ _buildTargetFramework = "net8.0";
+
+ // Get local packages path (adjust based on your build output)
+ _repoRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", ".."));
+ _localPackagesPath = Path.Combine(_repoRoot, "artifacts", "package", _buildConfiguration.ToLowerInvariant());
+ }
+
+ [Fact]
+ public async Task BasicCoverage_CollectsDataForCoveredLines()
+ {
+ // Arrange
+ using var testProject = CreateTestProject(includeSimpleTest: true);
+ await BuildProject(testProject.ProjectPath);
+
+ // Act
+ var result = await RunTestsWithCoverage(testProject.ProjectPath, "--coverage");
+
+ TestContext.Current?.AddAttachment("Test Output", result.CombinedOutput);
+
+ // Assert
+ Assert.True( result.ExitCode == 0, $"Expected successful test run (exit code 0) but got {result.ExitCode}.\n\n{result.CombinedOutput}");
+ Assert.Contains("Passed!", result.StandardOutput);
+
+ string[] coverageFiles = Directory.GetFiles(testProject.OutputDirectory, CoverageJsonFileName, SearchOption.AllDirectories);
+ Assert.NotEmpty(coverageFiles);
+
+ var coverageData = ParseCoverageJson(coverageFiles[0]);
+ Assert.NotNull(coverageData);
+ Assert.True(coverageData.RootElement.TryGetProperty("Modules", out _));
+ }
+
+ [Fact]
+ public async Task CoverageWithFormat_GeneratesCorrectOutputFormat()
+ {
+ // Arrange
+ using var testProject = CreateTestProject(includeSimpleTest: true);
+ await BuildProject(testProject.ProjectPath);
+
+ // Act
+ var result = await RunTestsWithCoverage(
+ testProject.ProjectPath,
+ "--coverage --coverage-output-format cobertura");
+
+ TestContext.Current?.AddAttachment("Test Output", result.CombinedOutput);
+
+ // Assert
+ Assert.True(result.ExitCode == 0, $"Expected successful test run (exit code 0) but got {result.ExitCode}.\n\n{result.CombinedOutput}");
+
+ string[] coverageFiles = Directory.GetFiles(testProject.OutputDirectory, CoverageCoberturaFileName, SearchOption.AllDirectories);
+ Assert.NotEmpty(coverageFiles);
+
+ var xmlDoc = XDocument.Load(coverageFiles[0]);
+ Assert.NotNull(xmlDoc.Root);
+ Assert.Equal("coverage", xmlDoc.Root.Name.LocalName);
+ }
+
+ [Fact]
+ public async Task CoverageInstrumentation_TracksMethodHits()
+ {
+ // Arrange
+ using var testProject = CreateTestProject(includeMethodTests: true);
+ await BuildProject(testProject.ProjectPath);
+
+ // Act
+ var result = await RunTestsWithCoverage(testProject.ProjectPath, "--coverage");
+
+ TestContext.Current?.AddAttachment("Test Output", result.CombinedOutput);
+
+ // Assert
+ Assert.True(result.ExitCode == 0, $"Expected successful test run (exit code 0) but got {result.ExitCode}.\n\n{result.CombinedOutput}");
+
+ string[] coverageFiles = Directory.GetFiles(testProject.OutputDirectory, CoverageJsonFileName, SearchOption.AllDirectories);
+ var coverageData = ParseCoverageJson(coverageFiles[0]);
+
+ // Verify method-level coverage tracking
+ bool foundCoveredMethod = false;
+ if (coverageData.RootElement.TryGetProperty("Modules", out var modules))
+ {
+ foreach (var module in modules.EnumerateArray())
+ {
+ if (module.TryGetProperty("Documents", out var documents))
+ {
+ foreach (var document in documents.EnumerateArray())
+ {
+ if (document.TryGetProperty("Classes", out var classes))
+ {
+ foreach (var classInfo in classes.EnumerateArray())
+ {
+ if (classInfo.TryGetProperty("Methods", out var methods))
+ {
+ foreach (var method in methods.EnumerateArray())
+ {
+ if (method.TryGetProperty("Lines", out var lines) && lines.GetArrayLength() > 0)
+ {
+ foundCoveredMethod = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Assert.True(foundCoveredMethod);
+ }
+
+ [Fact]
+ public async Task BranchCoverage_TracksConditionalPaths()
+ {
+ // Arrange
+ using var testProject = CreateTestProject(includeBranchTest: true);
+ await BuildProject(testProject.ProjectPath);
+
+ // Act
+ var result = await RunTestsWithCoverage(testProject.ProjectPath, "--coverage");
+
+ TestContext.Current?.AddAttachment("Test Output", result.CombinedOutput);
+
+ // Assert
+ Assert.True(result.ExitCode == 0, $"Expected successful test run (exit code 0) but got {result.ExitCode}.\n\n{result.CombinedOutput}");
+
+ string[] coverageFiles = Directory.GetFiles(testProject.OutputDirectory, CoverageJsonFileName, SearchOption.AllDirectories);
+ var coverageData = ParseCoverageJson(coverageFiles[0]);
+
+ // Verify branch coverage is tracked
+ bool foundBranches = false;
+ if (coverageData.RootElement.TryGetProperty("Modules", out var modules))
+ {
+ foreach (var module in modules.EnumerateArray())
+ {
+ if (module.TryGetProperty("Documents", out var documents))
+ {
+ foreach (var document in documents.EnumerateArray())
+ {
+ if (document.TryGetProperty("Classes", out var classes))
+ {
+ foreach (var classInfo in classes.EnumerateArray())
+ {
+ if (classInfo.TryGetProperty("Methods", out var methods))
+ {
+ foreach (var method in methods.EnumerateArray())
+ {
+ if (method.TryGetProperty("Branches", out var branches) &&
+ branches.GetArrayLength() > 0)
+ {
+ foundBranches = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Assert.True(foundBranches);
+ }
+
+ [Fact]
+ public async Task MultipleCoverageFormats_GeneratesAllReports()
+ {
+ // Arrange
+ using var testProject = CreateTestProject(includeSimpleTest: true);
+ await BuildProject(testProject.ProjectPath);
+
+ // Act
+ var result = await RunTestsWithCoverage(
+ testProject.ProjectPath,
+ "--coverage --coverage-output-format json,cobertura,lcov");
+
+ TestContext.Current?.AddAttachment("Test Output", result.CombinedOutput);
+
+ // Assert
+ Assert.True(result.ExitCode == 0, $"Expected successful test run (exit code 0) but got {result.ExitCode}.\n\n{result.CombinedOutput}");
+
+ // Verify all formats are generated
+ Assert.NotEmpty(Directory.GetFiles(testProject.OutputDirectory, "coverage.json", SearchOption.AllDirectories));
+ Assert.NotEmpty(Directory.GetFiles(testProject.OutputDirectory, "coverage.cobertura.xml", SearchOption.AllDirectories));
+ Assert.NotEmpty(Directory.GetFiles(testProject.OutputDirectory, "coverage.info", SearchOption.AllDirectories));
+ }
+
+ #region Helper Methods
+
+ private TestProject CreateTestProject(
+
+ bool includeSimpleTest = false,
+ bool includeMethodTests = false,
+ bool includeMultipleClasses = false,
+ bool includeCalculatorTest = false,
+ bool includeBranchTest = false,
+ bool includeMultipleTests = false)
+ {
+ // Use repository artifacts folder instead of user temp
+ string artifactsTemp = Path.Combine(_repoRoot, "artifacts", "tmp", _buildConfiguration.ToLowerInvariant());
+ Directory.CreateDirectory(artifactsTemp);
+
+ string tempPath = Path.Combine(artifactsTemp, $"CoverletMTP_Test_{Guid.NewGuid():N}");
+ Directory.CreateDirectory(tempPath);
+
+ // Create NuGet.config to use local packages
+ CreateNuGetConfig(tempPath);
+
+ // Get coverlet.MTP package version
+ string coverletMtpVersion = GetCoverletMtpPackageVersion();
+
+ // Create project file with MTP enabled and coverlet.MTP reference
+ string projectFile = Path.Combine(tempPath, "TestProject.csproj");
+ File.WriteAllText(projectFile, $@"
+
+
+ net8.0
+ 12.0
+ false
+ true
+ true
+ true
+ Exe
+
+
+
+
+
+");
+
+ // Create test file based on parameters
+ string testCode = GenerateTestCode(
+ includeSimpleTest,
+ includeMethodTests,
+ includeMultipleClasses,
+ includeCalculatorTest,
+ includeBranchTest,
+ includeMultipleTests);
+
+ File.WriteAllText(Path.Combine(tempPath, "Tests.cs"), testCode);
+
+ return new TestProject(projectFile, Path.Combine(tempPath, "bin", _buildConfiguration, _buildTargetFramework));
+ }
+
+ private void CreateNuGetConfig(string projectPath)
+ {
+ string nugetConfig = $@"
+
+
+
+
+
+
+";
+
+ File.WriteAllText(Path.Combine(projectPath, "NuGet.config"), nugetConfig);
+ }
+
+ private string GetCoverletMtpPackageVersion()
+ {
+ // Look for coverlet.MTP package in local packages folder
+ if (Directory.Exists(_localPackagesPath))
+ {
+ var mtpPackages = Directory.GetFiles(_localPackagesPath, "coverlet.MTP.*.nupkg");
+ if (mtpPackages.Length > 0)
+ {
+ string packageName = Path.GetFileNameWithoutExtension(mtpPackages[0]);
+ // Extract version from filename (e.g., coverlet.MTP.8.0.0-preview.28.g4608ccb7ad.nupkg)
+ string version = packageName["coverlet.MTP.".Length..];
+ return version;
+ }
+ }
+
+ // Fallback to a default version
+ return "8.0.0-preview.*";
+ }
+
+ private string GenerateTestCode(
+ bool includeSimpleTest,
+ bool includeMethodTests,
+ bool includeMultipleClasses,
+ bool includeCalculatorTest,
+ bool includeBranchTest,
+ bool includeMultipleTests)
+ {
+ var codeBuilder = new System.Text.StringBuilder();
+ codeBuilder.AppendLine("// Copyright (c) Toni Solarin-Sodara");
+ codeBuilder.AppendLine("// Licensed under the MIT license. See LICENSE file in the project root for full license information.");
+ codeBuilder.AppendLine();
+ codeBuilder.AppendLine("using Xunit;");
+ codeBuilder.AppendLine();
+ codeBuilder.AppendLine("namespace TestProject;");
+ codeBuilder.AppendLine();
+
+ if (includeSimpleTest)
+ {
+ codeBuilder.AppendLine(@"
+public class SimpleTests
+{
+ [Fact]
+ public void SimpleTest_Passes()
+ {
+ int result = Add(2, 3);
+ Assert.Equal(5, result);
+ }
+
+ private int Add(int a, int b)
+ {
+ return a + b;
+ }
+}");
+ }
+
+ if (includeMethodTests)
+ {
+ codeBuilder.AppendLine(@"
+public class MethodTests
+{
+ [Fact]
+ public void Method_ExecutesAndIsCovered()
+ {
+ var sut = new SystemUnderTest();
+ int result = sut.Calculate(10, 5);
+ Assert.Equal(15, result);
+ }
+}
+
+public class SystemUnderTest
+{
+ public int Calculate(int x, int y)
+ {
+ int temp = x + y;
+ return temp;
+ }
+}");
+ }
+
+ if (includeCalculatorTest)
+ {
+ codeBuilder.AppendLine(@"
+public class CalculatorTests
+{
+ [Fact]
+ public void Calculator_Add_ReturnsSum()
+ {
+ var calc = new Calculator();
+ Assert.Equal(10, calc.Add(4, 6));
+ }
+
+ [Fact]
+ public void Calculator_Multiply_ReturnsProduct()
+ {
+ var calc = new Calculator();
+ Assert.Equal(20, calc.Multiply(4, 5));
+ }
+}
+
+public class Calculator
+{
+ public int Add(int a, int b) => a + b;
+ public int Multiply(int a, int b) => a * b;
+}");
+ }
+
+ if (includeBranchTest)
+ {
+ codeBuilder.AppendLine(@"
+public class BranchTests
+{
+ [Fact]
+ public void Branch_PositivePath_IsCovered()
+ {
+ var result = CheckValue(10);
+ Assert.Equal(""Positive"", result);
+ }
+
+ [Fact]
+ public void Branch_NegativePath_IsCovered()
+ {
+ var result = CheckValue(-5);
+ Assert.Equal(""Negative"", result);
+ }
+
+ private string CheckValue(int value)
+ {
+ if (value > 0)
+ {
+ return ""Positive"";
+ }
+ else if (value < 0)
+ {
+ return ""Negative"";
+ }
+ return ""Zero"";
+ }
+}");
+ }
+
+ if (includeMultipleTests)
+ {
+ codeBuilder.AppendLine(@"
+public class ConcurrentTests
+{
+ [Fact]
+ public void Test1() => Assert.True(true);
+
+ [Fact]
+ public void Test2() => Assert.True(true);
+
+ [Fact]
+ public void Test3() => Assert.True(true);
+}");
+ }
+
+ if (includeMultipleClasses)
+ {
+ codeBuilder.AppendLine(@"
+public class IncludedTests
+{
+ [Fact]
+ public void IncludedTest() => Assert.True(true);
+}
+
+// This would be in ExcludedClass.cs in real scenario
+public class ExcludedClass
+{
+ public void ExcludedMethod() { }
+}");
+ }
+
+ return codeBuilder.ToString();
+ }
+
+ private async Task BuildProject(string projectPath)
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"build \"{projectPath}\" -c {_buildConfiguration} -f {_buildTargetFramework}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ // Attach build output for debugging
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"Build failed:\nOutput: {output}\nError: {error}");
+ }
+
+ return process.ExitCode;
+ }
+
+ private async Task RunTestsWithCoverage(string projectPath, string arguments)
+ {
+ // For MTP, we need to run the test executable directly, not through dotnet test
+ string projectDir = Path.GetDirectoryName(projectPath)!;
+ string projectName = Path.GetFileNameWithoutExtension(projectPath);
+ string testExecutable = Path.Combine(projectDir, "bin", _buildConfiguration, _buildTargetFramework, $"{projectName}.dll");
+
+ if (!File.Exists(testExecutable))
+ {
+ throw new FileNotFoundException(
+ $"Test executable not found: {testExecutable}\n" +
+ $"Build may have failed silently.");
+ }
+
+ string coverletMtpDll = Path.Combine(
+ Path.GetDirectoryName(testExecutable)!,
+ "coverlet.MTP.dll");
+
+ if (!File.Exists(coverletMtpDll))
+ {
+ throw new FileNotFoundException(
+ $"Coverlet MTP extension not found: {coverletMtpDll}\n" +
+ $"The coverlet.MTP NuGet package may not have restored correctly.");
+ }
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"exec \"{testExecutable}\" {arguments} --diagnostic --diagnostic-verbosity trace",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = projectDir
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ string errorContext = process.ExitCode switch
+ {
+ 0 => "Success",
+ 1 => "Test failures occurred",
+ 2 => "Invalid command-line arguments",
+ 3 => "Test discovery failed",
+ 4 => "Test execution failed",
+ 5 => "Unexpected error (unhandled exception)",
+ _ => "Unknown error"
+ };
+
+ return new TestResult
+ {
+ ExitCode = process.ExitCode,
+ ErrorText = errorContext,
+ StandardOutput = output,
+ StandardError = error,
+ CombinedOutput = $"=== TEST EXECUTABLE ===\n{testExecutable}\n\n" +
+ $"=== ARGUMENTS ===\n{arguments}\n\n" +
+ $"=== EXIT CODE ===\n{process.ExitCode}\n\n" +
+ $"=== STDOUT ===\n{output}\n\n" +
+ $"=== STDERR ===\n{error}"
+ };
+ }
+
+ private JsonDocument ParseCoverageJson(string filePath)
+ {
+ string jsonContent = File.ReadAllText(filePath);
+ return JsonDocument.Parse(jsonContent);
+ }
+
+ #endregion
+
+ private class TestProject : IDisposable
+ {
+ public string ProjectPath { get; }
+ public string OutputDirectory { get; }
+
+ public TestProject(string projectPath, string outputDirectory)
+ {
+ ProjectPath = projectPath;
+ OutputDirectory = outputDirectory;
+ }
+
+ public void Dispose()
+ {
+ string? projectDir = Path.GetDirectoryName(ProjectPath);
+ if (projectDir == null || !Directory.Exists(projectDir))
+ return;
+
+ // Retry cleanup to handle file locks (especially on Windows)
+ for (int i = 0; i < 3; i++)
+ {
+ try
+ {
+ Directory.Delete(projectDir, recursive: true);
+ return; // Success
+ }
+ catch (IOException) when (i < 2)
+ {
+ // File may be locked by antivirus or other process
+ System.Threading.Thread.Sleep(100);
+ }
+ catch (UnauthorizedAccessException) when (i < 2)
+ {
+ // Mark files as normal (remove read-only) and retry
+ foreach (var file in Directory.GetFiles(projectDir, "*", SearchOption.AllDirectories))
+ {
+ File.SetAttributes(file, FileAttributes.Normal);
+ }
+ System.Threading.Thread.Sleep(100);
+ }
+ }
+
+ // Log cleanup failure but don't throw (test already finished)
+ Debug.WriteLine($"Warning: Failed to cleanup test directory: {projectDir}");
+ }
+ }
+
+ private class TestResult
+ {
+ public int ExitCode { get; set; }
+ public string ErrorText { get; set; } = string.Empty;
+ public string StandardOutput { get; set; } = string.Empty;
+ public string StandardError { get; set; } = string.Empty;
+ public string CombinedOutput { get; set; } = string.Empty;
+ }
+}
diff --git a/test/coverlet.MTP.validation.tests/Directory.Build.props b/test/coverlet.MTP.validation.tests/Directory.Build.props
new file mode 100644
index 000000000..9ed34a696
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/Directory.Build.props
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ true
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/Directory.Build.targets b/test/coverlet.MTP.validation.tests/Directory.Build.targets
new file mode 100644
index 000000000..8a7f9cd7b
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/Directory.Build.targets
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/HelpCommandTests.cs b/test/coverlet.MTP.validation.tests/HelpCommandTests.cs
new file mode 100644
index 000000000..8600e65e7
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/HelpCommandTests.cs
@@ -0,0 +1,610 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Xml.Linq;
+using Coverlet.Tests.Utils;
+using NuGet.Packaging;
+using Xunit;
+
+namespace Coverlet.MTP.validation.tests;
+
+///
+/// Tests to verify coverlet.MTP extension is properly loaded and command-line options are available.
+/// These tests check the --help output to ensure the extension is registered with Microsoft Testing Platform.
+/// Uses a dedicated test project in the TestProjects subdirectory for easier troubleshooting.
+///
+public class HelpCommandTests
+{
+ private readonly string _buildConfiguration;
+ private readonly string _buildTargetFramework;
+ private readonly string _localPackagesPath;
+ private const string PropsFileName = "MTPTest.props";
+ private string[] _testProjectTfms = [];
+ private static readonly string s_projectName = "coverlet.MTP.validation.tests";
+ private const string SutName = "BasicTestProject";
+ private readonly string _projectOutputPath = TestUtils.GetTestBinaryPath(s_projectName);
+ private readonly string _testProjectPath;
+ private readonly string _repoRoot ;
+
+ public HelpCommandTests()
+ {
+ _buildConfiguration = "Debug";
+ _buildTargetFramework = "net8.0";
+
+ // Get repository root
+ _repoRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", ".."));
+ _localPackagesPath = Path.Combine(_repoRoot, "artifacts", "packages", _buildConfiguration.ToLowerInvariant(), "Shipping");
+
+ _projectOutputPath = Path.Combine(_repoRoot, "artifacts", "bin", s_projectName, _buildConfiguration.ToLowerInvariant());
+
+ // Use dedicated test project in TestProjects subdirectory
+ _testProjectPath = Path.Combine(
+ Path.GetDirectoryName(typeof(HelpCommandTests).Assembly.Location)!,
+ "TestProjects",
+ "BasicTestProject");
+ }
+
+ private protected string GetPackageVersion(string filter)
+ {
+ string packagesPath = TestUtils.GetPackagePath(TestUtils.GetAssemblyBuildConfiguration().ToString().ToLowerInvariant());
+
+ if (!Directory.Exists(packagesPath))
+ {
+ throw new DirectoryNotFoundException($"Package directory '{packagesPath}' not found, run 'dotnet pack' on repository root");
+ }
+
+ List files = Directory.GetFiles(packagesPath, filter).ToList();
+ if (files.Count == 0)
+ {
+ throw new InvalidOperationException($"Could not find any package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was called.");
+ }
+ else if (files.Count > 1)
+ {
+ throw new InvalidOperationException($"Found more than one package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was only called once.");
+ }
+ else
+ {
+ using Stream pkg = File.OpenRead(files[0]);
+ using var reader = new PackageArchiveReader(pkg);
+ using Stream nuspecStream = reader.GetNuspec();
+ var manifest = Manifest.ReadFrom(nuspecStream, false);
+ return manifest.Metadata.Version?.OriginalVersion ?? throw new InvalidOperationException("Version is null");
+ }
+ }
+
+ private void CreateDeterministicTestPropsFile()
+ {
+ string propsFile = Path.Combine(_testProjectPath, PropsFileName);
+ File.Delete(propsFile);
+
+ XDocument deterministicTestProps = new();
+ deterministicTestProps.Add(
+ new XElement("Project",
+ new XElement("PropertyGroup",
+ new XElement("coverletMTPVersion", GetPackageVersion("*MTP*.nupkg")))));
+
+ string csprojPath = Path.Combine(_testProjectPath, SutName + ".csproj");
+ XElement csproj = XElement.Load(csprojPath)!;
+
+ // Use only the first top-level PropertyGroup in the project file
+ XElement? firstPropertyGroup = csproj.Elements("PropertyGroup").FirstOrDefault();
+ if (firstPropertyGroup is null)
+ throw new InvalidOperationException("No top-level found in project file.");
+
+ // Prefer TargetFrameworks, fall back to single TargetFramework
+ XElement? tfmsElement = firstPropertyGroup.Element("TargetFrameworks") ?? firstPropertyGroup.Element("TargetFramework");
+ if (tfmsElement is null)
+ throw new InvalidOperationException("No or element found in the first PropertyGroup.");
+
+ _testProjectTfms = tfmsElement.Value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.Contains(_buildTargetFramework, _testProjectTfms);
+
+ deterministicTestProps.Save(Path.Combine(propsFile));
+ }
+
+ [Fact]
+ public async Task Help_ShowsCoverletMtpExtension()
+ {
+ CreateDeterministicTestPropsFile();
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Equal(0, result.ExitCode);
+ Assert.Contains("Extension options:", result.StandardOutput);
+
+ // Verify coverlet.MTP is loaded and shows its options
+ Assert.Contains("--formats", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsFormatsOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ // Assert - Check for formats option from CoverletExtensionCommandLineProvider
+ Assert.Contains("--formats", result.StandardOutput);
+ Assert.Contains("Specifies the output formats for the coverage report", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsExcludeOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--exclude", result.StandardOutput);
+ Assert.Contains("Filter expressions to exclude specific modules and types", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsIncludeOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--include", result.StandardOutput);
+ Assert.Contains("Filter expressions to include only specific modules and type", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsExcludeByFileOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--exclude-by-file", result.StandardOutput);
+ Assert.Contains("Glob patterns specifying source files to exclude", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsIncludeDirectoryOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--include-directory", result.StandardOutput);
+ Assert.Contains("Include directories containing additional assemblies", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsExcludeByAttributeOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--exclude-by-attribute", result.StandardOutput);
+ Assert.Contains("Attributes to exclude from code coverage", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsIncludeTestAssemblyOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--include-test-assembly", result.StandardOutput);
+ Assert.Contains("Specifies whether to report code coverage of the test assembly", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsSingleHitOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--single-hit", result.StandardOutput);
+ Assert.Contains("limit code coverage hit reporting to a single hit", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsSkipAutoPropsOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--skipautoprops", result.StandardOutput);
+ Assert.Contains("Neither track nor record auto-implemented properties", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsDoesNotReturnAttributeOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--does-not-return-attribute", result.StandardOutput);
+ Assert.Contains("Attributes that mark methods that do not return", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsExcludeAssembliesWithoutSourcesOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--exclude-assemblies-without-sources", result.StandardOutput);
+ Assert.Contains("Specifies behavior of heuristic to ignore assemblies with missing source documents", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Help_ShowsSourceMappingFileOption()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithHelp();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Contains("--source-mapping-file", result.StandardOutput);
+ Assert.Contains("Specifies the path to a SourceRootsMappings file", result.StandardOutput);
+ }
+
+ [Fact]
+ public async Task Info_ShowsCoverletMtpExtension()
+ {
+ // Arrange
+ await EnsureTestProjectBuilt();
+
+ // Act
+ TestResult result = await RunTestsWithInfo();
+
+ TestContext.Current.AddAttachment(
+ "Test Output",
+ result.CombinedOutput);
+
+ // Assert
+ Assert.Equal(0, result.ExitCode);
+
+ // Verify coverlet.MTP extension is listed in --info output
+ Assert.Contains("coverlet", result.StandardOutput.ToLowerInvariant());
+ }
+
+ #region Helper Methods
+
+ private async Task EnsureTestProjectBuilt()
+ {
+ // Verify test project exists
+ string projectFile = Path.Combine(_testProjectPath, "BasicTestProject.csproj");
+ if (!File.Exists(projectFile))
+ {
+ throw new InvalidOperationException(
+ $"Test project not found at: {projectFile}\n" +
+ $"Please ensure the TestProjects/BasicTestProject directory exists.");
+ }
+
+ // CRITICAL: Ensure packages are built BEFORE running tests
+ EnsurePackagesBuilt();
+
+ // Create version props file
+ CreateDeterministicTestPropsFile();
+
+ // Update NuGet.config to point to local packages
+ UpdateNuGetConfig();
+
+ // Clean any previous builds to avoid stale references
+ await CleanProject(projectFile);
+
+ // Restore packages
+ await RestoreProject(projectFile);
+
+ // Build the test project
+ await BuildProject(projectFile);
+
+ // Verify coverlet.MTP.dll was deployed
+ VerifyCoverletMtpDeployed();
+ }
+
+ private void EnsurePackagesBuilt()
+ {
+ string packagesPath = TestUtils.GetPackagePath(
+ TestUtils.GetAssemblyBuildConfiguration().ToString().ToLowerInvariant());
+
+ // Check for coverlet.MTP
+ string[] mtpPackages = Directory.GetFiles(packagesPath, "coverlet.MTP.*.nupkg");
+ if (mtpPackages.Length == 0)
+ {
+ throw new InvalidOperationException(
+ $"coverlet.MTP package not found in '{packagesPath}'.\n" +
+ $"Run: dotnet pack src/coverlet.MTP -c {_buildConfiguration}");
+ }
+ }
+
+ private async Task CleanProject(string projectPath)
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"clean \"{projectPath}\" -c {_buildConfiguration}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(processStartInfo);
+ await process!.WaitForExitAsync();
+ }
+
+ private async Task RestoreProject(string projectPath)
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"restore \"{projectPath}\" --force --verbosity detailed",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ TestContext.Current?.AddAttachment(
+ "Restore Output",
+ $"STDOUT:\n{output}\n\nSTDERR:\n{error}");
+
+ throw new InvalidOperationException(
+ $"Restore failed with exit code {process.ExitCode}\n" +
+ $"Output: {output}\n" +
+ $"Error: {error}");
+ }
+ }
+
+ private void VerifyCoverletMtpDeployed()
+ {
+ string binPath = GetSUTBinaryPath();
+
+ string coverletMtpDll = Path.Combine(binPath, "coverlet.MTP.dll");
+ string coverletCoreDll = Path.Combine(binPath, "coverlet.core.dll");
+
+ if (!File.Exists(coverletMtpDll))
+ {
+ string[] deployedFiles = Directory.GetFiles(binPath, "*.dll");
+ throw new InvalidOperationException(
+ $"coverlet.MTP.dll not found in '{binPath}'.\n" +
+ $"Deployed files:\n{string.Join("\n", deployedFiles.Select(f => $" - {Path.GetFileName(f)}"))}");
+ }
+
+ if (!File.Exists(coverletCoreDll))
+ {
+ throw new InvalidOperationException(
+ $"coverlet.core.dll not found in '{binPath}'. This is a dependency of coverlet.MTP.");
+ }
+ }
+
+ private string GetSUTBinaryPath()
+ {
+ string binTestProjectPath = Path.Combine(_repoRoot, "artifacts", "bin", SutName);
+ string binPath = Path.Combine(binTestProjectPath, _buildConfiguration);
+ return binPath;
+ }
+
+ private void UpdateNuGetConfig()
+ {
+ string nugetConfigPath = Path.Combine(_testProjectPath, "NuGet.config");
+
+ string nugetConfig = $@"
+
+
+
+
+
+
+";
+
+ File.WriteAllText(nugetConfigPath, nugetConfig);
+ }
+
+private async Task BuildProject(string projectPath)
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"build \"{projectPath}\" -c {_buildConfiguration} -f {_buildTargetFramework} --no-restore",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ // Attach build output to test results for debugging
+ TestContext.Current?.AddAttachment(
+ "Build Output",
+ $"Exit Code: {process.ExitCode}\n\nSTDOUT:\n{output}\n\nSTDERR:\n{error}");
+
+ throw new InvalidOperationException(
+ $"Build failed with exit code {process.ExitCode}\n" +
+ $"Output: {output}\n" +
+ $"Error: {error}");
+ }
+
+ return process.ExitCode;
+ }
+
+ private async Task RunTestsWithHelp()
+ {
+ string testExecutable = Path.Combine(GetSUTBinaryPath(), SutName + ".dll");
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"exec \"{testExecutable}\" --help",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = _testProjectPath
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ return new TestResult
+ {
+ ExitCode = process.ExitCode,
+ StandardOutput = output,
+ StandardError = error,
+ CombinedOutput = $"STDOUT:\n{output}\n\nSTDERR:\n{error}"
+ };
+ }
+
+ private async Task RunTestsWithInfo()
+ {
+ string testExecutable = Path.Combine(GetSUTBinaryPath(), SutName + ".dll");
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"exec \"{testExecutable}\" --info",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = _testProjectPath
+ };
+
+ using var process = Process.Start(processStartInfo);
+
+ string output = await process!.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ return new TestResult
+ {
+ ExitCode = process.ExitCode,
+ StandardOutput = output,
+ StandardError = error,
+ CombinedOutput = $"STDOUT:\n{output}\n\nSTDERR:\n{error}"
+ };
+ }
+
+ #endregion
+
+ private class TestResult
+ {
+ public int ExitCode { get; set; }
+ public string StandardOutput { get; set; } = string.Empty;
+ public string StandardError { get; set; } = string.Empty;
+ public string CombinedOutput { get; set; } = string.Empty;
+ }
+}
diff --git a/test/coverlet.MTP.validation.tests/Properties/AssemblyInfo.cs b/test/coverlet.MTP.validation.tests/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..85787f58f
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection;
+
+[assembly: AssemblyKeyFile("coverlet.MTP.validation.tests.snk")]
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/BasicTestProject.csproj b/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/BasicTestProject.csproj
new file mode 100644
index 000000000..7d42b1c69
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/BasicTestProject.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+
+ net8.0
+ 12.0
+ Exe
+ false
+ true
+
+
+ https://api.nuget.org/v3/index.json;
+ $(RepoRoot)artifacts/package/$(Configuration.ToLowerInvariant())
+
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+ 8.0.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/DummyTests.cs b/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/DummyTests.cs
new file mode 100644
index 000000000..e38d2e567
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/BasicTestProject/DummyTests.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Xunit;
+
+namespace BasicTestProject;
+
+public class DummyTests
+{
+ [Fact]
+ public void DummyTest_Passes()
+ {
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void SimpleMath_Works()
+ {
+ int result = Add(2, 3);
+ Assert.Equal(5, result);
+ }
+
+ private int Add(int a, int b)
+ {
+ return a + b;
+ }
+}
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClass.cs b/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClass.cs
new file mode 100644
index 000000000..36046f546
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClass.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace CalculateClassLibrary
+{
+ public class CalculateClass
+ {
+ public static int Add(int x, int y) =>
+ x + y;
+
+ public static int Subtract(int x, int y) =>
+ x - y;
+ }
+}
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClassLibrary.csproj b/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClassLibrary.csproj
new file mode 100644
index 000000000..dbdcea46b
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/CalculateClassLibrary/CalculateClassLibrary.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netstandard2.0
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/CalculateTestProject.csproj b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/CalculateTestProject.csproj
new file mode 100644
index 000000000..c291677d4
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/CalculateTestProject.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Exe
+ Tests
+ true
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/UnitTest1.cs b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/UnitTest1.cs
new file mode 100644
index 000000000..dc7048460
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/UnitTest1.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using CalculateClassLibrary;
+using Xunit;
+
+namespace CalculateTestProject;
+
+public class UnitTest1
+{
+ [Fact]
+ public void AddTest()
+ {
+ Assert.Equal(5, CalculateClass.Add(2, 3));
+ }
+}
diff --git a/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/xunit.runner.json b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/xunit.runner.json
new file mode 100644
index 000000000..86c7ea05b
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/TestProjects/CalculateTestProject/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
+}
diff --git a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj
new file mode 100644
index 000000000..8dcb38c4b
--- /dev/null
+++ b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+ enable
+ enable
+ Exe
+ net8.0
+ true
+ true
+ false
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.snk b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.snk
new file mode 100644
index 000000000..db63df1d9
Binary files /dev/null and b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.snk differ
diff --git a/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs
index c5e616ebc..42ae033f9 100644
--- a/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs
+++ b/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Toni Solarin-Sodara
+// Copyright (c) Toni Solarin-Sodara
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
diff --git a/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj b/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj
index 67bca27ce..fe59ddfb5 100644
--- a/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj
+++ b/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj
@@ -4,13 +4,16 @@
net8.0
false
+ false
-
-
-
-
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj b/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
index a7ce141f5..5a3cc820f 100644
--- a/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
+++ b/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
@@ -6,6 +6,10 @@
false
+
+
+
+
diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
index 2268399fc..22a47a429 100644
--- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
+++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
@@ -103,6 +103,7 @@ public void TestBackupOriginalModule()
string module = typeof(InstrumentationHelperTests).Assembly.Location;
string identifier = Guid.NewGuid().ToString();
+ // Ensure the backup list is used to restore the original module
_instrumentationHelper.BackupOriginalModule(module, identifier, false);
string backupPath = Path.Combine(
diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
index 6ea7c3bb1..98c7ef653 100644
--- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
+++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
@@ -739,7 +739,7 @@ public void Instrumenter_MethodsWithoutReferenceToSource_AreSkipped()
var instrumenter = new Instrumenter(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace", parameters,
loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(Path.Combine(directory.FullName, Path.GetFileName(module)), loggerMock.Object, new FileSystem(), new AssemblyAdapter()), new CecilSymbolHelper());
- instrumentationHelper.BackupOriginalModule(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace");
+ instrumentationHelper.BackupOriginalModule(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace", false);
InstrumenterResult result = instrumenter.Instrument();
diff --git a/test/coverlet.core.tests/coverlet.core.tests.csproj b/test/coverlet.core.tests/coverlet.core.tests.csproj
index 4c5ac27cd..417f834ad 100644
--- a/test/coverlet.core.tests/coverlet.core.tests.csproj
+++ b/test/coverlet.core.tests/coverlet.core.tests.csproj
@@ -5,6 +5,7 @@
net8.0
Exe
true
+ true
true
false
$(NoWarn);CS8002
diff --git a/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj b/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj
index fed7a61a2..ac4799a87 100644
--- a/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj
+++ b/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj
@@ -3,7 +3,7 @@
- net9.0;net8.0
+ net9.0;net8.0
false
coverletsample.integration.determisticbuild
NU1604;NU1701
@@ -22,6 +22,8 @@
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/coverlet.integration.template/coverlet.integration.template.csproj b/test/coverlet.integration.template/coverlet.integration.template.csproj
index 3659285bb..238355aa3 100644
--- a/test/coverlet.integration.template/coverlet.integration.template.csproj
+++ b/test/coverlet.integration.template/coverlet.integration.template.csproj
@@ -18,4 +18,8 @@
+
+
+
+
diff --git a/test/coverlet.integration.tests/Collectors.cs b/test/coverlet.integration.tests/Collectors.cs
index 085da78da..7a733f947 100644
--- a/test/coverlet.integration.tests/Collectors.cs
+++ b/test/coverlet.integration.tests/Collectors.cs
@@ -50,7 +50,7 @@ public abstract class Collectors : BaseTest
public Collectors()
{
- _buildConfiguration = TestUtils.GetAssemblyBuildConfiguration().ToString();
+ _buildConfiguration = TestUtils.GetBuildConfigurationString();
_buildTargetFramework = TestUtils.GetAssemblyTargetFramework();
}
diff --git a/test/coverlet.integration.tests/DeterministicBuild.cs b/test/coverlet.integration.tests/DeterministicBuild.cs
index c0f303d7c..3a88adda8 100644
--- a/test/coverlet.integration.tests/DeterministicBuild.cs
+++ b/test/coverlet.integration.tests/DeterministicBuild.cs
@@ -32,7 +32,7 @@ public class DeterministicBuild : BaseTest, IDisposable
public DeterministicBuild(ITestOutputHelper output)
{
- _buildConfiguration = TestUtils.GetAssemblyBuildConfiguration().ToString();
+ _buildConfiguration = TestUtils.GetBuildConfigurationString();
_buildTargetFramework = TestUtils.GetAssemblyTargetFramework();
_artifactsPivot = _buildConfiguration + "_" + _buildTargetFramework;
_output = output;
@@ -74,7 +74,7 @@ private void CreateDeterministicTestPropsFile()
private protected void AssertCoverage(string standardOutput = "", string reportName = "", bool checkDeterministicReport = true)
{
- if (_buildConfiguration == "Debug")
+ if (_buildConfiguration == "debug")
{
bool coverageChecked = false;
string reportFilePath = "";
diff --git a/test/coverlet.integration.tests/Msbuild.cs b/test/coverlet.integration.tests/Msbuild.cs
index cfc14c6c4..83f57f54d 100644
--- a/test/coverlet.integration.tests/Msbuild.cs
+++ b/test/coverlet.integration.tests/Msbuild.cs
@@ -17,7 +17,7 @@ public class Msbuild : BaseTest
public Msbuild(ITestOutputHelper output)
{
- _buildConfiguration = TestUtils.GetAssemblyBuildConfiguration().ToString();
+ _buildConfiguration = TestUtils.GetBuildConfigurationString();
_buildTargetFramework = TestUtils.GetAssemblyTargetFramework();
_output = output;
}
diff --git a/test/coverlet.integration.tests/coverlet.integration.tests.csproj b/test/coverlet.integration.tests/coverlet.integration.tests.csproj
index 03996e3e2..d9b39fc0f 100644
--- a/test/coverlet.integration.tests/coverlet.integration.tests.csproj
+++ b/test/coverlet.integration.tests/coverlet.integration.tests.csproj
@@ -1,4 +1,4 @@
-
+
$(NetCurrent);$(NetMinimum)
Exe
@@ -13,6 +13,8 @@
+
+
@@ -22,7 +24,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
@@ -33,4 +35,8 @@
+
+
+
+
diff --git a/test/coverlet.tests.projectsample.aspmvcrazor.tests/coverlet.tests.projectsample.aspmvcrazor.tests.csproj b/test/coverlet.tests.projectsample.aspmvcrazor.tests/coverlet.tests.projectsample.aspmvcrazor.tests.csproj
index 9ad5b768c..14f387ff9 100644
--- a/test/coverlet.tests.projectsample.aspmvcrazor.tests/coverlet.tests.projectsample.aspmvcrazor.tests.csproj
+++ b/test/coverlet.tests.projectsample.aspmvcrazor.tests/coverlet.tests.projectsample.aspmvcrazor.tests.csproj
@@ -4,13 +4,16 @@
net8.0
false
Exe
+ false
-
-
-
-
+
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/test/coverlet.tests.projectsample.aspnet8.tests/coverlet.tests.projectsample.aspnet8.tests.csproj b/test/coverlet.tests.projectsample.aspnet8.tests/coverlet.tests.projectsample.aspnet8.tests.csproj
index b862ce6fa..408bc7def 100644
--- a/test/coverlet.tests.projectsample.aspnet8.tests/coverlet.tests.projectsample.aspnet8.tests.csproj
+++ b/test/coverlet.tests.projectsample.aspnet8.tests/coverlet.tests.projectsample.aspnet8.tests.csproj
@@ -4,13 +4,16 @@
net8.0
false
Exe
+ false
-
-
-
-
+
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj
index 25c9e0e8f..71226361f 100644
--- a/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj
+++ b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj
@@ -12,5 +12,9 @@
+
+
+
+
diff --git a/test/coverlet.tests.projectsample.netframework/coverlet.tests.projectsample.netframework.csproj b/test/coverlet.tests.projectsample.netframework/coverlet.tests.projectsample.netframework.csproj
index d34a33ca5..3d27162a8 100644
--- a/test/coverlet.tests.projectsample.netframework/coverlet.tests.projectsample.netframework.csproj
+++ b/test/coverlet.tests.projectsample.netframework/coverlet.tests.projectsample.netframework.csproj
@@ -1,7 +1,7 @@
- net472
+ $(FullFrameworkTFM)
false
false
diff --git a/test/coverlet.tests.utils/Properties/AssemblyInfo.cs b/test/coverlet.tests.utils/Properties/AssemblyInfo.cs
index 137c9aac2..be23a1e3a 100644
--- a/test/coverlet.tests.utils/Properties/AssemblyInfo.cs
+++ b/test/coverlet.tests.utils/Properties/AssemblyInfo.cs
@@ -9,4 +9,5 @@
[assembly: InternalsVisibleTo("coverlet.core.coverage.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100094aad8eb75c06c9f2443dda84573b8db55cd6678452a60010db2643467ac28928db3a06b0b1ac3016645b448937d5e671b36504bcfc0fda27e996c5e1b0ee49747145cda6d47508d1e3c60b144634d95e33d4efe49536372df8139f48d3d897ae6931c2876d4f5d00215fd991cbcecde2705e53e19309e21c8b59d19eb925b1")]
[assembly: InternalsVisibleTo("coverlet.integration.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001d24efbe9cbc2dc49b7a3d2ae34ca37cfb69b4f450acd768a22ce5cd021c8a38ae7dc68b2809a1ac606ad531b578f192a5690b2986990cbda4dd84ec65a3a4c1c36f6d7bb18f08592b93091535eaee2f0c8e48763ed7f190db2008e1f9e0facd5c0df5aaab74febd3430e09a428a72e5e6b88357f92d78e47512d46ebdc3cbb")]
[assembly: InternalsVisibleTo("coverlet.msbuild.tasks.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010071b1583d63637a225f3f640252fee7130f0f3f2127d75025c1c3ee2d6dfc79a4950919268e0784d7ff54b0eadd8e4762e3e150da422e20e091eb0811d9d84e1779d5b95e349d5428aebb16e82e081bdf805926c5a9eb2094aaed9d36442de024264976a8835c7d6923047cf2f745e8f0ded2332f8980acd390f725224d976ed8")]
-
+[assembly: InternalsVisibleTo("coverlet.MTP.validation.tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001575e172562f7b3ca4e105ef1539802ddf92de5f3d3a76937f99c73b1c64f46ec6ed1c5de46f2d39bc8916050376f749507bb5082958890e6e3ba9c68b4c6e4a56f16c1401f21f908c437a2b0b4dc263ef3bc1bc15d6a02a8e6cbf26a077f1590f91e248cf5ccd4642b7493b31cfa2bd9f921b662cd2cb8bcc66fc2cb533e2a8")]
+[assembly: InternalsVisibleTo("coverlet.MTP.unit.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d568d35e41a0829ae24628d27cc43572aa77a3d2f5ac0a6b7554a92d979a72ec0e084c38f83f1ccfc3d26bbeca74131f611a7600a6f218ffc0cbb5758c4e6da50b07fd499d96bdc4e8eb1e10a38231aefd3cde5a69cbade511129588352843950b489b9295a9fb7259b00f18f3a571bdca19b13ccda89cc2a4690f69ee2367b8")]
diff --git a/test/coverlet.tests.utils/TestUtils.cs b/test/coverlet.tests.utils/TestUtils.cs
index af4777f82..61182283f 100644
--- a/test/coverlet.tests.utils/TestUtils.cs
+++ b/test/coverlet.tests.utils/TestUtils.cs
@@ -45,6 +45,12 @@ public static string GetAssemblyTargetFramework()
throw new NotSupportedException($"Build configuration not supported");
}
+ public static string GetBuildConfigurationString()
+ {
+ // Returns lowercase configuration string to match MSBuild output paths on case-sensitive filesystems
+ return GetAssemblyBuildConfiguration().ToString().ToLower();
+ }
+
public static string GetTestProjectPath(string directoryName)
{
return Path.Join(Path.GetFullPath(Path.Join(AppContext.BaseDirectory, s_rel4Parents)), "test", directoryName);