From 941929d9c4a297991dc9bfcca2ea1e894024b649 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 6 Dec 2025 11:13:08 +0100 Subject: [PATCH 1/4] feat: Add support for Coverlet.MTP and update project dependencies - Updated target frameworks to net472 in coverlet.core and coverlet.msbuild.tasks projects - Adjusted CoverletToolsPath for multi-targeting support in buildMultiTargeting props and targets - Created unit tests for Coverlet.MTP command line options validation - Added documentation for Coverlet.MTP integration --- .github/dependabot.yml | 11 + .github/workflows/dotnet.yml | 232 ++++++++++++++++++ .gitignore | 1 + BannedSymbols.txt | 10 + Directory.Packages.props | 38 ++- Documentation/Coverlet.MTP.Integration.md | 1 + Documentation/MSBuildIntegration.md | 28 +++ coverlet.sln | 21 ++ eng/azure-pipelines-nightly.yml | 2 +- eng/build.yml | 14 +- global.json | 2 +- .../DataCollection/CoverageWrapper.cs | 3 +- .../DataCollection/CoverletSettings.cs | 5 + .../DataCollection/CoverletSettingsParser.cs | 13 + .../Utilities/CoverletConstants.cs | 1 + .../coverlet.collector.csproj | 3 +- .../Abstractions/IInstrumentationHelper.cs | 2 +- src/coverlet.core/Coverage.cs | 6 +- .../Helpers/InstrumentationHelper.cs | 37 ++- src/coverlet.core/Properties/AssemblyInfo.cs | 1 + src/coverlet.core/coverlet.core.csproj | 3 +- .../InstrumentationTask.cs | 5 +- .../coverlet.msbuild.props | 4 +- .../coverlet.msbuild.targets | 3 +- .../coverlet.msbuild.tasks.csproj | 2 +- test/Directory.Build.targets | 2 +- .../CoverletMTPCommandLineTests.cs | 132 ++++++++++ .../Properties/AssemblyInfo.cs | 6 + .../coverlet.MTP.unit.tests.csproj | 26 ++ .../coverlet.MTP.unit.tests.snk | Bin 0 -> 596 bytes test/coverlet.MTP.validation.tests/Tests.cs | 43 ++++ test/coverlet.MTP.validation.tests/Tests2.cs | 28 +++ .../coverlet.MTP.validation.tests.csproj | 36 +++ .../Coverage/InstrumenterHelper.cs | 2 +- ...rlet.core.tests.samples.netstandard.csproj | 4 + .../Helpers/InstrumentationHelperTests.cs | 1 + .../Instrumentation/InstrumenterTests.cs | 2 +- .../coverlet.core.tests.csproj | 1 + ...verlet.integration.determisticbuild.csproj | 2 +- .../coverlet.integration.template.csproj | 4 + .../coverlet.integration.tests.csproj | 8 +- ...coverlet.tests.projectsample.fsharp.fsproj | 4 + 42 files changed, 694 insertions(+), 55 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dotnet.yml create mode 100644 BannedSymbols.txt create mode 100644 Documentation/Coverlet.MTP.Integration.md create mode 100644 test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs create mode 100644 test/coverlet.MTP.unit.tests/Properties/AssemblyInfo.cs create mode 100644 test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.csproj create mode 100644 test/coverlet.MTP.unit.tests/coverlet.MTP.unit.tests.snk create mode 100644 test/coverlet.MTP.validation.tests/Tests.cs create mode 100644 test/coverlet.MTP.validation.tests/Tests2.cs create mode 100644 test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj 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..fcc49230f 100644 --- a/.gitignore +++ b/.gitignore @@ -323,3 +323,4 @@ Playground*/ coverlet.MTP/ # 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.Packages.props b/Directory.Packages.props index cdf6d83a8..64caf9796 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,20 +8,23 @@ 17.11.48 - 4.12.0 - 6.14.0 + 4.13.0 + + 7.0.1 - 17.14.1 + 18.0.1 3.0.0 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..f31a6d577 --- /dev/null +++ b/Documentation/Coverlet.MTP.Integration.md @@ -0,0 +1 @@ +### ToDo Description 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..9d8df9ea7 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", "{976491C7-114C-4FD4-92ED-AFD4BCD0BC18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.MTP.validation.tests", "test\coverlet.MTP.validation.tests\coverlet.MTP.validation.tests.csproj", "{E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.MTP.unit.tests", "test\coverlet.MTP.unit.tests\coverlet.MTP.unit.tests.csproj", "{C9B29FB1-BF7E-4A03-B369-B8CA822062D8}" +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 + {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Release|Any CPU.Build.0 = Release|Any CPU + {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Release|Any CPU.Build.0 = Release|Any CPU + {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -233,7 +251,10 @@ Global {0B109210-03CB-413F-888C-3023994AA384} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {71004336-9896-4AE5-8367-B29BB1680542} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {F74AD549-EFE0-4CD9-AD10-B2189E3FD5BB} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} + {976491C7-114C-4FD4-92ED-AFD4BCD0BC18} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD} {C80BF6A9-63EE-6D36-8913-627A7E2EA459} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} + {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} + {C9B29FB1-BF7E-4A03-B369-B8CA822062D8} = {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.yml b/eng/build.yml index 8bcc8020e..2a8cdd0be 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,12 @@ 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" + 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 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.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..21d1bf8cf 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -2,7 +2,7 @@ Library - $(NetMinimum);netstandard2.0 + $(NetMinimum);net472 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..0af9e3243 100644 --- a/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props +++ b/src/coverlet.msbuild.tasks/buildMultiTargeting/coverlet.msbuild.props @@ -20,10 +20,10 @@ $(MSBuildThisFileDirectory)..\tasks\net8.0\ - $(MSBuildThisFileDirectory)..\tasks\netstandard2.0\ + $(MSBuildThisFileDirectory)..\tasks\net472\ $(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..4831bbc50 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);net472 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..b1eab3280 --- /dev/null +++ b/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs @@ -0,0 +1,132 @@ +// 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("formats", "", "At least one format must be specified.")] + [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 0000000000000000000000000000000000000000..8e27ac2d82b1dc5774455d08202978cd07d2f74c GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098iXwzOnpn{s>MkvyJ#5HoNccaqvtO{#& zRH-eOnsV$82uwKmKOE1)(rdoTbQ2$88g>At@)-ZX%e8fkPHm+N2mMK%mc7L2>mCrJ zf-$cBJlRgsGP#2X%A`cBWJkR zwscABLD=(@NXJx5U@q;@sX7v+09P0+{}aSBWHCyaHTzn~_jc#IKoMH5kvn_b>YK6oQ8dBQO;d0e;1B~u+GYuZ zYbBj0XqYf*T@bcjtVg(MwhY2bzQOP>3zRy?$ZOTv&JqlosGKH7=zd>rPEInKb DataSource() + { + yield return (1, 1, 2); + yield return (2, 1, 3); + yield return (3, 1, 4); + } +} diff --git a/test/coverlet.MTP.validation.tests/Tests2.cs b/test/coverlet.MTP.validation.tests/Tests2.cs new file mode 100644 index 000000000..658ef182c --- /dev/null +++ b/test/coverlet.MTP.validation.tests/Tests2.cs @@ -0,0 +1,28 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace coverlet.MTP.validation.tests; + +[Arguments("Hello")] +[Arguments("World")] +public class MoreTests(string title) +{ + [Test] + public void ClassLevelDataRow() + { + Console.WriteLine(title); + Console.WriteLine(@"Did I forget that data injection works on classes too?"); + } + + [Test] + [MatrixDataSource] + public void Matrices( + [Matrix(1, 2, 3)] int a, + [Matrix(true, false)] bool b, + [Matrix("A", "B", "C")] string c) + { + Console.WriteLine(@"A new test will be created for each data row, whether it's on the class or method level!"); + + Console.WriteLine(@"Oh and this is a matrix test. That means all combinations of inputs are attempted."); + } +} 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..de385f0a2 --- /dev/null +++ b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj @@ -0,0 +1,36 @@ + + + + enable + enable + Exe + net8.0 + false + + false + true + + + + + + + + + + + + + + + + + + + + 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.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..97606d1fc 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 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/coverlet.integration.tests.csproj b/test/coverlet.integration.tests/coverlet.integration.tests.csproj index 03996e3e2..41335a7ed 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 @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers - + @@ -33,4 +33,8 @@ + + + + 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 @@ + + + + From 84d8894909ec6dfc0f4fd5ff8ceee151f7e95c23 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 6 Dec 2025 12:07:32 +0100 Subject: [PATCH 2/4] coverlet uses json as default whenever format is not specified --- test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs b/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs index b1eab3280..50855b52b 100644 --- a/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs +++ b/test/coverlet.MTP.unit.tests/CoverletMTPCommandLineTests.cs @@ -20,7 +20,6 @@ public CoverletMTPCommandLineTests() [Theory] [InlineData("formats", "invalid", "The value 'invalid' is not a valid option for 'formats'.")] - [InlineData("formats", "", "At least one format must be specified.")] [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) From a6ad42fac1d0ad78876d7fe2c01252ec16017904 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 7 Dec 2025 10:21:59 +0000 Subject: [PATCH 3/4] feat: Implement Coverlet Extension for Microsoft Testing Platform - Added CoverletExtensionCollector to handle test session lifecycle for coverage collection. - Introduced CoverletExtensionCommandLineProvider for command line options. - Created CoverletExtensionConfiguration to manage configuration settings. - Developed CoverletLoggerAdapter for logging integration with Microsoft Testing Platform. - Implemented CoverletExtensionEnvironmentVariableProvider for environment variable management. - Added CoverletExtensionProvider to register the Coverlet extension with the testing platform. - Created TestingPlatformBuilderHook to facilitate extension registration. - Updated project files to include necessary dependencies and configurations for Coverlet. - Added support for multiple target frameworks (net8.0 and net9.0). - Included build and packaging configurations for Coverlet.MTP. - Implemented command line options for coverage report formats and exclusions. - Established logging mechanisms for better traceability during coverage collection. --- .devcontainer/devcontainer.json | 2 +- .gitignore | 3 +- coverlet.sln | 36 +-- eng/build.sh | 101 ++++++-- eng/test.sh | 50 ++++ .../CoverletExtensionCollector.cs | 216 ++++++++++++++++++ .../CoverletExtensionCommandLineProvider.cs | 107 +++++++++ .../CoverletExtensionConfiguration.cs | 120 ++++++++++ ...letExtensionEnvironmentVariableProvider.cs | 50 ++++ src/coverlet.MTP/CoverletExtensionProvider.cs | 42 ++++ .../Logging/CoverletLoggerAdapter.cs | 49 ++++ src/coverlet.MTP/Properties/AssemblyInfo.cs | 6 + .../TestingPlatformBuilderHook.cs | 21 ++ src/coverlet.MTP/build/coverlet.MTP.props | 3 + src/coverlet.MTP/build/coverlet.MTP.targets | 3 + .../buildMultiTargeting/coverlet.MTP.props | 14 ++ .../buildMultiTargeting/coverlet.MTP.targets | 52 +++++ .../buildTransitive/coverlet.MTP.props | 3 + .../buildTransitive/coverlet.MTP.targets | 3 + src/coverlet.MTP/coverlet.MTP.csproj | 70 ++++++ src/coverlet.MTP/coverlet.MTP.snk | Bin 0 -> 596 bytes src/coverlet.MTP/coverletExtension.cs | 19 ++ .../coverlet.core.performancetest.csproj | 11 +- ...verlet.integration.determisticbuild.csproj | 2 + .../coverlet.integration.tests.csproj | 2 + ...sts.projectsample.aspmvcrazor.tests.csproj | 11 +- ...t.tests.projectsample.aspnet8.tests.csproj | 11 +- 27 files changed, 953 insertions(+), 54 deletions(-) create mode 100644 eng/test.sh create mode 100644 src/coverlet.MTP/CoverletExtensionCollector.cs create mode 100644 src/coverlet.MTP/CoverletExtensionCommandLineProvider.cs create mode 100644 src/coverlet.MTP/CoverletExtensionConfiguration.cs create mode 100644 src/coverlet.MTP/CoverletExtensionEnvironmentVariableProvider.cs create mode 100644 src/coverlet.MTP/CoverletExtensionProvider.cs create mode 100644 src/coverlet.MTP/Logging/CoverletLoggerAdapter.cs create mode 100644 src/coverlet.MTP/Properties/AssemblyInfo.cs create mode 100644 src/coverlet.MTP/TestingPlatformBuilderHook.cs create mode 100644 src/coverlet.MTP/build/coverlet.MTP.props create mode 100644 src/coverlet.MTP/build/coverlet.MTP.targets create mode 100644 src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.props create mode 100644 src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.targets create mode 100644 src/coverlet.MTP/buildTransitive/coverlet.MTP.props create mode 100644 src/coverlet.MTP/buildTransitive/coverlet.MTP.targets create mode 100644 src/coverlet.MTP/coverlet.MTP.csproj create mode 100644 src/coverlet.MTP/coverlet.MTP.snk create mode 100644 src/coverlet.MTP/coverletExtension.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9d491686a..0233a9193 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"] } }, diff --git a/.gitignore b/.gitignore index fcc49230f..87279532c 100644 --- a/.gitignore +++ b/.gitignore @@ -318,9 +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/coverlet.sln b/coverlet.sln index 9d8df9ea7..6d35bb26d 100644 --- a/coverlet.sln +++ b/coverlet.sln @@ -92,11 +92,11 @@ 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", "{976491C7-114C-4FD4-92ED-AFD4BCD0BC18}" +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.validation.tests", "test\coverlet.MTP.validation.tests\coverlet.MTP.validation.tests.csproj", "{E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}" +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.unit.tests", "test\coverlet.MTP.unit.tests\coverlet.MTP.unit.tests.csproj", "{C9B29FB1-BF7E-4A03-B369-B8CA822062D8}" +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 @@ -208,18 +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 - {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Release|Any CPU.ActiveCfg = Release|Any CPU - {976491C7-114C-4FD4-92ED-AFD4BCD0BC18}.Release|Any CPU.Build.0 = Release|Any CPU - {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14}.Release|Any CPU.Build.0 = Release|Any CPU - {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9B29FB1-BF7E-4A03-B369-B8CA822062D8}.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 @@ -251,10 +251,10 @@ Global {0B109210-03CB-413F-888C-3023994AA384} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {71004336-9896-4AE5-8367-B29BB1680542} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {F74AD549-EFE0-4CD9-AD10-B2189E3FD5BB} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} - {976491C7-114C-4FD4-92ED-AFD4BCD0BC18} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD} {C80BF6A9-63EE-6D36-8913-627A7E2EA459} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} - {E55E2E17-042F-0D1C-0DFC-2F1FCFA21C14} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} - {C9B29FB1-BF7E-4A03-B369-B8CA822062D8} = {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/build.sh b/eng/build.sh index a1e1891fa..dffa5a306 100644 --- a/eng/build.sh +++ b/eng/build.sh @@ -1,34 +1,93 @@ #!/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 +WORKSPACE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$WORKSPACE_ROOT" -# 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 +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/test.sh b/eng/test.sh new file mode 100644 index 000000000..2e20df19c --- /dev/null +++ b/eng/test.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +# Get the workspace root directory +WORKSPACE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$WORKSPACE_ROOT" + +# 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) maybe takes hours !!!! +# 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:"./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/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/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..bc3c7ac0d --- /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..fadf58885 --- /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..e2a09074b --- /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..72d89e0b3 --- /dev/null +++ b/src/coverlet.MTP/buildMultiTargeting/coverlet.MTP.targets @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + <_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..fadf58885 --- /dev/null +++ b/src/coverlet.MTP/buildTransitive/coverlet.MTP.props @@ -0,0 +1,3 @@ + + + diff --git a/src/coverlet.MTP/buildTransitive/coverlet.MTP.targets b/src/coverlet.MTP/buildTransitive/coverlet.MTP.targets new file mode 100644 index 000000000..e2a09074b --- /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..8cc123898 --- /dev/null +++ b/src/coverlet.MTP/coverlet.MTP.csproj @@ -0,0 +1,70 @@ + + + + net8.0;net9.0 + Coverlet.MTP + true + true + enable + enable + $(NoWarn) + true + true + true + true + + + + + 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 0000000000000000000000000000000000000000..0571a4f197442233fd542713211bedc163618650 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097rb*>1@hkbUHG?DGGn-n5sN~C~QK}5^q zwnfO<&|Q}GJMH^q;#z9Dqp}RegcD-*QoIyIQwNSqn|Gp?A_o6gf24NZ##g=QSQ%q|5T(r+qpsJV z*j_#f9sxD3ErSOQ%RnEP(LN(a~!#q9DQu&t2DqY)Dri^!dnexpJ z^>1%)8JTAJapEP+VT+6=f?ns?qY($fCkkY_jgQ;`6DF*I0#=z9M*xU5eOB~vh-}>R i@yaZ@Sk+_H3R2Q;W4ZT?h;;Ljd$)`ll=uG#18dH3Y9smp literal 0 HcmV?d00001 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/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.integration.determisticbuild/coverlet.integration.determisticbuild.csproj b/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj index 97606d1fc..ac4799a87 100644 --- a/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj +++ b/test/coverlet.integration.determisticbuild/coverlet.integration.determisticbuild.csproj @@ -22,6 +22,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/coverlet.integration.tests/coverlet.integration.tests.csproj b/test/coverlet.integration.tests/coverlet.integration.tests.csproj index 41335a7ed..d9b39fc0f 100644 --- a/test/coverlet.integration.tests/coverlet.integration.tests.csproj +++ b/test/coverlet.integration.tests/coverlet.integration.tests.csproj @@ -13,6 +13,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 From 5c644ab9c35a30d7f8af8c3e50e00f3de9882698 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 7 Dec 2025 12:34:25 +0100 Subject: [PATCH 4/4] Decrease minimum line coverage from 90 to 70 --- eng/publish-coverage-results.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/publish-coverage-results.yml b/eng/publish-coverage-results.yml index f2aad33d4..647ef3680 100644 --- a/eng/publish-coverage-results.yml +++ b/eng/publish-coverage-results.yml @@ -7,7 +7,7 @@ parameters: assemblyfilters: '-xunit*' classfilters: '' breakBuild: false - minimumLineCoverage: 90 + minimumLineCoverage: 70 steps: - task: Powershell@2