diff --git a/Documentation/ArcadeSdk.md b/Documentation/ArcadeSdk.md index a5df38e66b2..0e931ff053d 100644 --- a/Documentation/ArcadeSdk.md +++ b/Documentation/ArcadeSdk.md @@ -1064,6 +1064,114 @@ To override the default Shared Framework version that is selected based on the t Timeout to apply to an individual invocation of the test runner (e.g. `xunit.console.exe`) for a single configuration. Integer number of milliseconds. +### Code Coverage Properties + +Arcade SDK supports code coverage collection when using the VSTest runner. To enable code coverage, set `UseVSTestRunner` to `true` and `CollectCoverage` to `true`. + +#### `CollectCoverage` (bool) + +Set to `true` to enable code coverage collection when running tests. Default is `false`. + +```text +msbuild Project.UnitTests.csproj /t:Test /p:UseVSTestRunner=true /p:CollectCoverage=true +``` + +Or set it in your project file: + +```xml + + true + true + +``` + +#### `CodeCoverageFormat` (string) + +Specifies the output format for code coverage reports. Supported values: `cobertura`, `opencover`, `lcov`, `json`, or a combination like `cobertura,opencover`. Default is `cobertura`. + +The Cobertura format is supported by Azure DevOps and can be published using the `PublishCodeCoverageResults` task. + +#### `CodeCoverageOutputDirectory` (string) + +Directory where code coverage reports will be generated. Default is `$(ArtifactsTestResultsDir)coverage`. + +#### `CoverageDeterministic` (bool) + +Enable or disable generation of deterministic coverage reports. Default is `true`. + +#### `CoverageInclude` (string) + +Semicolon-separated list of assembly patterns to include in code coverage. Uses glob patterns. Default is empty (includes all assemblies). + +Example: +```xml +[MyProject]*;[MyLibrary]* +``` + +#### `CoverageExclude` (string) + +Semicolon-separated list of assembly patterns to exclude from code coverage. Uses glob patterns. Default is empty. + +Example: +```xml +[*.Tests]*;[xunit.*]* +``` + +#### `CoverageIncludeByFile` (string) + +Semicolon-separated list of file path patterns to include in code coverage. Uses glob patterns. Default is empty. + +#### `CoverageExcludeByFile` (string) + +Semicolon-separated list of file path patterns to exclude from code coverage. Uses glob patterns. Default is empty. + +Example: +```xml +**/*Designer.cs;**/Generated/*.cs +``` + +#### `CoverageExcludeByAttribute` (string) + +Semicolon-separated list of attributes to exclude from code coverage. Default is empty. + +Example: +```xml +Obsolete;GeneratedCode;CompilerGenerated +``` + +#### Example: Complete Code Coverage Configuration + +```xml + + + net8.0 + true + true + cobertura + [*.Tests]*;[xunit.*]* + **/*Designer.cs + + + + + + +``` + +Then run tests with: +```text +./build.sh --test +``` + +Code coverage reports will be generated in `artifacts/TestResults/coverage/` directory in Cobertura format, which can be published to Azure DevOps using the `PublishCodeCoverageResults` task in your pipeline: + +```yaml +- task: PublishCodeCoverageResults@2 + inputs: + summaryFileLocation: '$(Build.SourcesDirectory)/artifacts/TestResults/coverage/**/coverage.cobertura.xml' + codecoverageTool: 'cobertura' +``` + ### `GenerateResxSource` (bool) When set to `true`, Arcade will generate a class source for all embedded .resx files. diff --git a/Documentation/CodeCoverageExample.md b/Documentation/CodeCoverageExample.md new file mode 100644 index 00000000000..6252a4181a5 --- /dev/null +++ b/Documentation/CodeCoverageExample.md @@ -0,0 +1,108 @@ +# Code Coverage Example for Arcade SDK + +This example demonstrates how to enable code coverage collection in test projects using Arcade SDK. + +## Prerequisites + +- Arcade SDK 11.0.0 or later +- Test project using XUnit + +## Basic Configuration + +Add the following properties to your test project file: + +```xml + + + net8.0 + + + true + + + true + + + cobertura + + +``` + +## Running Tests with Coverage + +Run tests using the build script: + +```bash +./build.sh --test +``` + +Or using MSBuild directly: + +```bash +dotnet build /t:Test /p:Configuration=Release +``` + +## Coverage Reports + +Code coverage reports will be generated in the `artifacts/TestResults/coverage/` directory. + +For Cobertura format, the report file will be named `coverage.cobertura.xml` and can be published to Azure DevOps. + +## Azure DevOps Integration + +Add the following task to your Azure Pipelines YAML to publish coverage results: + +```yaml +- task: PublishCodeCoverageResults@2 + inputs: + summaryFileLocation: '$(Build.SourcesDirectory)/artifacts/TestResults/coverage/**/coverage.cobertura.xml' + codecoverageTool: 'cobertura' + displayName: 'Publish Code Coverage Results' +``` + +## Advanced Configuration + +### Filtering Coverage + +Exclude specific assemblies or files from coverage: + +```xml + + + [*.Tests]*;[xunit.*]*;[Moq]* + + + **/*Designer.cs;**/Generated/*.cs + + + Obsolete;GeneratedCode;CompilerGenerated + +``` + +### Multiple Output Formats + +Generate coverage in multiple formats: + +```xml + + cobertura,opencover,lcov + +``` + +## Troubleshooting + +### Coverage not collected + +- Ensure `UseVSTestRunner` is set to `true` +- Verify `CollectCoverage` is set to `true` +- Check that `coverlet.collector` package is restored (should be automatic) + +### Empty coverage report + +- Make sure tests are actually running and passing +- Verify the test project has a reference to the code you want to cover + +## See Also + +- [Arcade SDK Documentation](../Documentation/ArcadeSdk.md) +- [Coverlet Documentation](https://github.com/coverlet-coverage/coverlet) diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.props b/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.props index 25f911cb753..ad035ae3786 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.props +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.props @@ -36,4 +36,39 @@ + + + + false + + + cobertura + + + $(ArtifactsTestResultsDir)coverage + + + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.targets b/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.targets index 8ae93d3051f..756ede01728 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.targets +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/Tests.targets @@ -73,6 +73,7 @@ $(TestRunnerAdditionalArguments) $(RunArguments) $(RunCommand) + $(CollectCoverage) diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/VSTest.targets b/src/Microsoft.DotNet.Arcade.Sdk/tools/VSTest.targets index 6aba0addf1a..9b7f84946f7 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/VSTest.targets +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/VSTest.targets @@ -27,6 +27,7 @@ <_TestRunnerCommand>"$(DotNetTool)" test $(_TestAssembly) --logger:"console%3Bverbosity=normal" --logger:"trx%3BLogFileName=$(_TestResultTrxFileName)" --logger:"html%3BLogFileName=$(_TestResultHtmlFileName)" "--ResultsDirectory:$(_TestResultDirectory)" "--Framework:%(TestToRun.TargetFrameworkIdentifier),Version=%(TestToRun.TargetFrameworkVersion)" <_TestRunnerCommand Condition="'%(TestToRun.TestRunSettingsFile)' != ''">$(_TestRunnerCommand) "--settings:%(TestToRun.TestRunSettingsFile)" + <_TestRunnerCommand Condition="'%(TestToRun.CollectCoverage)' == 'true'">$(_TestRunnerCommand) --collect:"XPlat Code Coverage" <_TestRunnerCommand Condition="'$(_TestRunnerAdditionalArguments)' != ''">$(_TestRunnerCommand) $(_TestRunnerAdditionalArguments) + + + + <_GeneratedRunSettingsFile>$(ArtifactsObjDir)$(MSBuildProjectName).runsettings + + + <_CoverageIncludeFilter Condition="'$(CoverageInclude)' != ''">$([System.String]::new('$(CoverageInclude)').Replace(';',',')) + + + <_CoverageExcludeFilter Condition="'$(CoverageExclude)' != ''">$([System.String]::new('$(CoverageExclude)').Replace(';',',')) + + + <_CoverageIncludeByFileFilter Condition="'$(CoverageIncludeByFile)' != ''">$([System.String]::new('$(CoverageIncludeByFile)').Replace(';',',')) + + + <_CoverageExcludeByFileFilter Condition="'$(CoverageExcludeByFile)' != ''">$([System.String]::new('$(CoverageExcludeByFile)').Replace(';',',')) + + + <_CoverageExcludeByAttributeFilter Condition="'$(CoverageExcludeByAttribute)' != ''">$([System.String]::new('$(CoverageExcludeByAttribute)').Replace(';',',')) + + + + + <_RunSettingsLines Include="<?xml version="1.0" encoding="utf-8"?>" /> + <_RunSettingsLines Include="<RunSettings>" /> + <_RunSettingsLines Include=" <DataCollectionRunSettings>" /> + <_RunSettingsLines Include=" <DataCollectors>" /> + <_RunSettingsLines Include=" <DataCollector friendlyName="XPlat code coverage">" /> + <_RunSettingsLines Include=" <Configuration>" /> + <_RunSettingsLines Include=" <Format>$(_CodeCoverageFormat)</Format>" /> + <_RunSettingsLines Include=" <OutputPath>$(_CodeCoverageOutputPath)</OutputPath>" /> + <_RunSettingsLines Include=" <DeterministicReport>$(_CoverageDeterministic)</DeterministicReport>" /> + <_RunSettingsLines Include=" <Include>$(_CoverageIncludeFilter)</Include>" Condition="'$(_CoverageIncludeFilter)' != ''" /> + <_RunSettingsLines Include=" <Exclude>$(_CoverageExcludeFilter)</Exclude>" Condition="'$(_CoverageExcludeFilter)' != ''" /> + <_RunSettingsLines Include=" <ExcludeByFile>$(_CoverageExcludeByFileFilter)</ExcludeByFile>" Condition="'$(_CoverageExcludeByFileFilter)' != ''" /> + <_RunSettingsLines Include=" <IncludeTestAssembly>false</IncludeTestAssembly>" /> + <_RunSettingsLines Include=" <ExcludeByAttribute>$(_CoverageExcludeByAttributeFilter)</ExcludeByAttribute>" Condition="'$(_CoverageExcludeByAttributeFilter)' != ''" /> + <_RunSettingsLines Include=" <SkipAutoProps>true</SkipAutoProps>" /> + <_RunSettingsLines Include=" <DoesNotReturnAttribute>DoesNotReturnAttribute</DoesNotReturnAttribute>" /> + <_RunSettingsLines Include=" <SingleHit>false</SingleHit>" /> + <_RunSettingsLines Include=" <UseSourceLink>true</UseSourceLink>" /> + <_RunSettingsLines Include=" </Configuration>" /> + <_RunSettingsLines Include=" </DataCollector>" /> + <_RunSettingsLines Include=" </DataCollectors>" /> + <_RunSettingsLines Include=" </DataCollectionRunSettings>" /> + <_RunSettingsLines Include="</RunSettings>" /> + + + + + + + $(_GeneratedRunSettingsFile) + + + + + + + $(VSTestRunSettingsFile) + + + <_CodeCoverageFormat Condition="'$(CollectCoverage)' == 'true'">$(CodeCoverageFormat) + <_CodeCoverageOutputPath Condition="'$(CollectCoverage)' == 'true'">$(CodeCoverageOutputDirectory) + <_CoverageDeterministic Condition="'$(CollectCoverage)' == 'true'">$(CoverageDeterministic)