diff --git a/.gitignore b/.gitignore
index b9830ce42..294c9a4cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,6 +118,7 @@ _TeamCity*
# Visual Studio code coverage results
*.coverage
*.coveragexml
+lcov.info
# NCrunch
_NCrunch_*
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9f9da7337..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: csharp
-mono: none
-dotnet: 2.0.0
-os: linux
-dist: trusty
-sudo: false
-env:
- global:
- - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- - DOTNET_CLI_TELEMETRY_OPTOUT: 1
-script:
- - dotnet msbuild build.proj
- - dotnet msbuild build.proj /p:Configuration=Release
\ No newline at end of file
diff --git a/README.md b/README.md
index 2756802f1..729e771b8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# coverlet [](https://www.travis-ci.org/tonerdo/coverlet) [](https://ci.appveyor.com/project/tonerdo/coverlet) [](https://coveralls.io/github/tonerdo/coverlet?branch=master) [](LICENSE)
+# coverlet [](https://ci.appveyor.com/project/tonerdo/coverlet) [](https://codecov.io/gh/tonerdo/coverlet) [](LICENSE)
Coverlet is a cross platform code coverage library for .NET Core, with support for line, branch and method coverage.
@@ -64,7 +64,7 @@ Supported Formats:
* opencover
* cobertura
-The output folder of the coverage result file can also be specified using the `CoverletOutputDirectory` property.
+The output of the coverage result can also be specified using the `CoverletOutput` property.
### Threshold
@@ -74,31 +74,60 @@ Coverlet allows you to specify a coverage threshold below which it fails the bui
dotnet test /p:CollectCoverage=true /p:Threshold=80
```
-The above command will automatically fail the build if the average code coverage of all instrumented modules falls below 80%.
+The above command will automatically fail the build if the line, branch or method coverage of _any_ of the instrumented modules falls below 80%. You can specify what type of coverage to apply the threshold value to using the `ThresholdType` property. For example to apply the threshold check to only **line** coverage:
-### Excluding From Coverage
+```bash
+dotnet test /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line
+```
+
+You can specify multiple values for `ThresholdType` by separating them with commas. Valid values include `line`, `branch` and `method`.
-#### Attributes
-You can ignore a method or an entire class from code coverage by creating and applying any of the following attributes:
+### Excluding From Coverage
-* ExcludeFromCoverage
-* ExcludeFromCoverageAttribute
+#### Attributes
-Coverlet just uses the type name, so the attributes can be created under any namespace of your choosing.
+You can ignore a method or an entire class from code coverage by creating and applying the `ExcludeFromCodeCoverage` attribute present in the `System.Diagnostics.CodeAnalysis` namespace.
-#### Source Files
-You can also ignore specific source files from code coverage using the `Exclude` property
+#### Source Files
+You can also ignore specific source files from code coverage using the `ExcludeByFile` property
- Use single or multiple paths (separate by comma)
- Use absolute or relative paths (relative to the project directory)
- Use file path or directory path with globbing (e.g `dir1/*.cs`)
```bash
-dotnet test /p:CollectCoverage=true /p:Exclude=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
+dotnet test /p:CollectCoverage=true /p:ExcludeByFile=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
```
+#### Filters
+Coverlet gives the ability to have fine grained control over what gets excluded using "filter expressions".
+
+Syntax: `/p:Exclude=[Assembly-Filter]Type-Filter`
+
+Wildcards
+- `*` => matches zero or more characters
+- `?` => the prefixed character is optional
+
+Examples
+ - `/p:Exclude="[*]*"` => Excludes all types in all assemblies (nothing is instrumented)
+ - `/p:Exclude="[coverlet.*]Coverlet.Core.Coverage"` => Excludes the Coverage class in the `Coverlet.Core` namespace belonging to any assembly that matches `coverlet.*` (e.g `coverlet.core`)
+ - `/p:Exclude="[*]Coverlet.Core.Instrumentation.*"` => Excludes all types belonging to `Coverlet.Core.Instrumentation` namespace in any assembly
+ - `/p:Exclude="[coverlet.*.tests?]*"` => Excludes all types in any assembly starting with `coverlet.` and ending with `.test` or `.tests` (the `?` makes the `s` optional)
+ - `/p:Exclude=\"[coverlet.*]*,[*]Coverlet.Core*\"` => Excludes assemblies matching `coverlet.*` and excludes all types belonging to the `Coverlet.Core` namespace in any assembly
+
+```bash
+dotnet test /p:CollectCoverage=true /p:Exclude="[coverlet.*]Coverlet.Core.Coverage"
+```
+
+
+You can specify multiple filter expressions by separting them with a comma (`,`). If you specify multiple filters, then [you'll have to escape the surrounding quotes](https://github.com/Microsoft/msbuild/issues/2999#issuecomment-366078677) like this:
+`/p:Exclude=\"[coverlet.*]*,[*]Coverlet.Core*\"`.
+
+### Cake Addin
+If you're using [Cake Build](https://cakebuild.net) for your build script you can use the [Cake.Coverlet](https://github.com/Romanx/Cake.Coverlet) addin to provide you extensions to dotnet test for passing coverlet arguments in a strongly typed manner.
+
## Roadmap
-* Filter modules to be instrumented
+* Merging outputs (multiple test projects, one coverage result)
* Support for more output formats (e.g. JaCoCo)
* Console runner (removes the need for requiring a NuGet package)
diff --git a/appveyor.yml b/appveyor.yml
index ba3e0dfda..8d53f1fe3 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,10 +1,12 @@
version: '{build}'
+image:
+ - Visual Studio 2015
+ - Ubuntu
configuration:
- Debug
- Release
build_script:
- - echo "Building for %CONFIGURATION%"
- - dotnet msbuild build.proj /p:Configuration=%CONFIGURATION%
+ - ps: echo "Building for $env:CONFIGURATION"
+ - ps: dotnet msbuild build.proj /p:Configuration=$env:CONFIGURATION
test_script:
- - ps: if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
- - cmd: IF "%CONFIGURATION%"=="Release" ( %USERPROFILE%\.nuget\packages\coveralls.net\0.7.0\tools\csmacnz.Coveralls.exe --opencover -i test\coverlet.core.tests\coverage.xml --useRelativePaths )
\ No newline at end of file
+ - ps: if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
\ No newline at end of file
diff --git a/build.proj b/build.proj
index b53aa9917..bb78914ce 100644
--- a/build.proj
+++ b/build.proj
@@ -8,11 +8,11 @@
-
+
-
+
@@ -24,11 +24,11 @@
-
+
-
+
diff --git a/coverlet.msbuild.nuspec b/coverlet.msbuild.nuspec
index dd875f2fc..da87bdfac 100644
--- a/coverlet.msbuild.nuspec
+++ b/coverlet.msbuild.nuspec
@@ -2,7 +2,7 @@
coverlet.msbuild
- 1.2.0
+ 2.1.0
coverlet.msbuild
tonerdo
tonerdo
diff --git a/coverlet.sln b/coverlet.sln
index 3571bc819..c14d01a85 100644
--- a/coverlet.sln
+++ b/coverlet.sln
@@ -1,19 +1,25 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core", "src\coverlet.core\coverlet.core.csproj", "{31084026-D563-4B91-BE71-174C4270CCF4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core", "src\coverlet.core\coverlet.core.csproj", "{31084026-D563-4B91-BE71-174C4270CCF4}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.msbuild.tasks", "src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj", "{FA73E423-9790-4F35-B018-3C4E3CA338BA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.msbuild.tasks", "src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj", "{FA73E423-9790-4F35-B018-3C4E3CA338BA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.tests", "test\coverlet.core.tests\coverlet.core.tests.csproj", "{E7637CC6-43F7-461A-A0BF-3C14562419BD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.tests", "test\coverlet.core.tests\coverlet.core.tests.csproj", "{E7637CC6-43F7-461A-A0BF-3C14562419BD}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.console", "src\coverlet.console\coverlet.console.csproj", "{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.console", "src\coverlet.console\coverlet.console.csproj", "{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tracker", "src\coverlet.tracker\coverlet.tracker.csproj", "{F4273009-536D-4999-A126-B0A2E3AA3E70}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.testsubject", "test\coverlet.testsubject\coverlet.testsubject.csproj", "{AE117FAA-C21D-4F23-917E-0C8050614750}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.performancetest", "test\coverlet.core.performancetest\coverlet.core.performancetest.csproj", "{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -24,63 +30,105 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{31084026-D563-4B91-BE71-174C4270CCF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31084026-D563-4B91-BE71-174C4270CCF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.ActiveCfg = Debug|x64
- {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.Build.0 = Debug|x64
- {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.ActiveCfg = Debug|x86
- {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.Build.0 = Debug|x86
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.Build.0 = Debug|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.Build.0 = Debug|Any CPU
{31084026-D563-4B91-BE71-174C4270CCF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31084026-D563-4B91-BE71-174C4270CCF4}.Release|Any CPU.Build.0 = Release|Any CPU
- {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.ActiveCfg = Release|x64
- {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.Build.0 = Release|x64
- {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.ActiveCfg = Release|x86
- {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.Build.0 = Release|x86
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.ActiveCfg = Release|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.Build.0 = Release|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.ActiveCfg = Release|Any CPU
+ {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.Build.0 = Release|Any CPU
{FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.ActiveCfg = Debug|x64
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.Build.0 = Debug|x64
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.ActiveCfg = Debug|x86
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.Build.0 = Debug|x86
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.Build.0 = Debug|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.Build.0 = Debug|Any CPU
{FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|Any CPU.Build.0 = Release|Any CPU
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.ActiveCfg = Release|x64
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.Build.0 = Release|x64
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.ActiveCfg = Release|x86
- {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.Build.0 = Release|x86
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.ActiveCfg = Release|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.Build.0 = Release|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.ActiveCfg = Release|Any CPU
+ {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.Build.0 = Release|Any CPU
{E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.ActiveCfg = Debug|x64
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.Build.0 = Debug|x64
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.ActiveCfg = Debug|x86
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.Build.0 = Debug|x86
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.Build.0 = Debug|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.Build.0 = Debug|Any CPU
{E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|Any CPU.Build.0 = Release|Any CPU
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.ActiveCfg = Release|x64
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.Build.0 = Release|x64
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.ActiveCfg = Release|x86
- {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.Build.0 = Release|x86
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.ActiveCfg = Release|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.Build.0 = Release|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.ActiveCfg = Release|Any CPU
+ {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.Build.0 = Release|Any CPU
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.ActiveCfg = Debug|x64
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.Build.0 = Debug|x64
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.ActiveCfg = Debug|x86
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.Build.0 = Debug|x86
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.Build.0 = Debug|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.Build.0 = Debug|Any CPU
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|Any CPU.Build.0 = Release|Any CPU
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.ActiveCfg = Release|x64
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.Build.0 = Release|x64
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.ActiveCfg = Release|x86
- {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.Build.0 = Release|x86
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.ActiveCfg = Release|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.Build.0 = Release|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.ActiveCfg = Release|Any CPU
+ {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.Build.0 = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x64.Build.0 = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x86.Build.0 = Debug|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x64.ActiveCfg = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x64.Build.0 = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x86.Build.0 = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.Build.0 = Debug|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.Build.0 = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.Build.0 = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.Build.0 = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.Build.0 = Debug|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.ActiveCfg = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.Build.0 = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.ActiveCfg = Release|Any CPU
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{31084026-D563-4B91-BE71-174C4270CCF4} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{FA73E423-9790-4F35-B018-3C4E3CA338BA} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{E7637CC6-43F7-461A-A0BF-3C14562419BD} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
+ {F4273009-536D-4999-A126-B0A2E3AA3E70} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
+ {AE117FAA-C21D-4F23-917E-0C8050614750} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+ {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}
EndGlobalSection
EndGlobal
diff --git a/src/coverlet.core/Attributes/ExcludeFromCoverage.cs b/src/coverlet.core/Attributes/ExcludeFromCoverage.cs
index e2ac0e523..0fb1ef097 100644
--- a/src/coverlet.core/Attributes/ExcludeFromCoverage.cs
+++ b/src/coverlet.core/Attributes/ExcludeFromCoverage.cs
@@ -2,6 +2,6 @@
namespace Coverlet.Core.Attributes
{
- [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor)]
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)]
public sealed class ExcludeFromCoverageAttribute : Attribute { }
}
\ No newline at end of file
diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs
index 0ec0b4c84..4b56096f6 100644
--- a/src/coverlet.core/Coverage.cs
+++ b/src/coverlet.core/Coverage.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using Coverlet.Core.Helpers;
@@ -12,24 +13,36 @@ public class Coverage
{
private string _module;
private string _identifier;
- private IEnumerable _excludeRules;
+ private string[] _filters;
+ private string[] _excludes;
private List _results;
- public Coverage(string module, string identifier, IEnumerable excludeRules = null)
+ public string Identifier
+ {
+ get { return _identifier; }
+ }
+
+ public Coverage(string module, string[] filters, string[] excludes)
{
_module = module;
- _identifier = identifier;
- _excludeRules = excludeRules;
+ _filters = filters;
+ _excludes = excludes;
+ _identifier = Guid.NewGuid().ToString();
_results = new List();
}
public void PrepareModules()
{
- string[] modules = InstrumentationHelper.GetDependencies(_module);
- var excludedFiles = InstrumentationHelper.GetExcludedFiles(_excludeRules);
+ string[] modules = InstrumentationHelper.GetCoverableModules(_module);
+ string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludes);
+ _filters = _filters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
+
foreach (var module in modules)
{
- var instrumenter = new Instrumenter(module, _identifier, excludedFiles);
+ if (InstrumentationHelper.IsModuleExcluded(module, _filters))
+ continue;
+
+ var instrumenter = new Instrumenter(module, _identifier, _filters, excludes);
if (instrumenter.CanInstrument())
{
InstrumentationHelper.BackupOriginalModule(module, _identifier);
@@ -47,37 +60,79 @@ public CoverageResult GetCoverageResult()
foreach (var result in _results)
{
Documents documents = new Documents();
- foreach (var doc in result.Documents)
+ foreach (var doc in result.Documents.Values)
{
- foreach (var line in doc.Lines)
+ // Construct Line Results
+ foreach (var line in doc.Lines.Values)
{
if (documents.TryGetValue(doc.Path, out Classes classes))
{
if (classes.TryGetValue(line.Class, out Methods methods))
{
- if (methods.TryGetValue(line.Method, out Lines lines))
+ if (methods.TryGetValue(line.Method, out Method method))
{
- documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget });
+ documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
else
{
- documents[doc.Path][line.Class].Add(line.Method, new Lines());
- documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget });
+ documents[doc.Path][line.Class].Add(line.Method, new Method());
+ documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
}
else
{
documents[doc.Path].Add(line.Class, new Methods());
- documents[doc.Path][line.Class].Add(line.Method, new Lines());
- documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget });
+ documents[doc.Path][line.Class].Add(line.Method, new Method());
+ documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
}
else
{
documents.Add(doc.Path, new Classes());
documents[doc.Path].Add(line.Class, new Methods());
- documents[doc.Path][line.Class].Add(line.Method, new Lines());
- documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget });
+ documents[doc.Path][line.Class].Add(line.Method, new Method());
+ documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
+ }
+ }
+
+ // Construct Branch Results
+ foreach (var branch in doc.Branches.Values)
+ {
+ if (documents.TryGetValue(doc.Path, out Classes classes))
+ {
+ if (classes.TryGetValue(branch.Class, out Methods methods))
+ {
+ if (methods.TryGetValue(branch.Method, out Method method))
+ {
+ method.Branches.Add(new BranchInfo
+ { Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
+ );
+ }
+ else
+ {
+ documents[doc.Path][branch.Class].Add(branch.Method, new Method());
+ documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
+ { Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
+ );
+ }
+ }
+ else
+ {
+ documents[doc.Path].Add(branch.Class, new Methods());
+ documents[doc.Path][branch.Class].Add(branch.Method, new Method());
+ documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
+ { Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
+ );
+ }
+ }
+ else
+ {
+ documents.Add(doc.Path, new Classes());
+ documents[doc.Path].Add(branch.Class, new Methods());
+ documents[doc.Path][branch.Class].Add(branch.Method, new Method());
+ documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
+ { Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
+ );
}
}
}
@@ -97,30 +152,48 @@ private void CalculateCoverage()
{
foreach (var result in _results)
{
- if (!File.Exists(result.HitsFilePath)) { continue; }
- var lines = InstrumentationHelper.ReadHitsFile(result.HitsFilePath);
- foreach (var line in lines)
+ if (!File.Exists(result.HitsFilePath))
{
- var info = line.Split(',');
- // Ignore malformed lines
- if (info.Length != 4)
- continue;
+ // File not instrumented, or nothing in it called. Warn about this?
+ continue;
+ }
- var document = result.Documents.FirstOrDefault(d => d.Path == info[0]);
- if (document == null)
- continue;
+ using (var fs = new FileStream(result.HitsFilePath, FileMode.Open))
+ using (var sr = new StreamReader(fs))
+ {
+ string row;
+ while ((row = sr.ReadLine()) != null)
+ {
+ var info = row.Split(',');
+ // Ignore malformed lines
+ if (info.Length != 5)
+ continue;
- int start = int.Parse(info[1]);
- int end = int.Parse(info[2]);
- bool target = info[3] == "B";
+ bool isBranch = info[0] == "B";
- for (int j = start; j <= end; j++)
- {
- var subLine = document.Lines.First(l => l.Number == j);
- subLine.Hits = subLine.Hits + 1;
+ if (!result.Documents.TryGetValue(info[1], out var document))
+ {
+ continue;
+ }
+
+ int start = int.Parse(info[2]);
+ int hits = int.Parse(info[4]);
- if (j == start)
- subLine.IsBranchTarget = target;
+ if (isBranch)
+ {
+ int ordinal = int.Parse(info[3]);
+ var branch = document.Branches[(start, ordinal)];
+ branch.Hits = hits;
+ }
+ else
+ {
+ int end = int.Parse(info[3]);
+ for (int j = start; j <= end; j++)
+ {
+ var line = document.Lines[j];
+ line.Hits = hits;
+ }
+ }
}
}
@@ -128,4 +201,4 @@ private void CalculateCoverage()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/coverlet.core/CoverageDetails.cs b/src/coverlet.core/CoverageDetails.cs
new file mode 100644
index 000000000..e83cfdb44
--- /dev/null
+++ b/src/coverlet.core/CoverageDetails.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Coverlet.Core
+{
+ public class CoverageDetails
+ {
+ public double Covered { get; internal set; }
+ public int Total { get; internal set; }
+ public double Percent
+ {
+ get => Math.Round(Total == 0 ? Total : Covered / Total, 3);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/coverlet.core/CoverageResult.cs b/src/coverlet.core/CoverageResult.cs
index 302361e66..dc70d9c79 100644
--- a/src/coverlet.core/CoverageResult.cs
+++ b/src/coverlet.core/CoverageResult.cs
@@ -5,14 +5,31 @@
namespace Coverlet.Core
{
- public class LineInfo
+ public class BranchInfo
{
+ public int Line { get; set; }
+ public int Offset { get; set; }
+ public int EndOffset { get; set; }
+ public int Path { get; set; }
+ public uint Ordinal { get; set; }
public int Hits { get; set; }
- public bool IsBranchPoint { get; set; }
}
- public class Lines : SortedDictionary { }
- public class Methods : Dictionary { }
+ public class Lines : SortedDictionary { }
+
+ public class Branches : List { }
+
+ public class Method
+ {
+ internal Method()
+ {
+ Lines = new Lines();
+ Branches = new Branches();
+ }
+ public Lines Lines;
+ public Branches Branches;
+ }
+ public class Methods : Dictionary { }
public class Classes : Dictionary { }
public class Documents : Dictionary { }
public class Modules : Dictionary { }
diff --git a/src/coverlet.core/CoverageSummary.cs b/src/coverlet.core/CoverageSummary.cs
index 670bfd51c..36b29c12e 100644
--- a/src/coverlet.core/CoverageSummary.cs
+++ b/src/coverlet.core/CoverageSummary.cs
@@ -6,147 +6,173 @@ namespace Coverlet.Core
{
public class CoverageSummary
{
- public double CalculateLineCoverage(Lines lines)
+ public CoverageDetails CalculateLineCoverage(Lines lines)
{
- double linesCovered = lines.Where(l => l.Value.Hits > 0).Count();
- double coverage = lines.Count == 0 ? lines.Count : linesCovered / lines.Count;
- return Math.Round(coverage, 3);
+ var details = new CoverageDetails();
+ details.Covered = lines.Where(l => l.Value > 0).Count();
+ details.Total = lines.Count;
+ return details;
}
- public double CalculateLineCoverage(Methods methods)
+ public CoverageDetails CalculateLineCoverage(Methods methods)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var method in methods)
- total += CalculateLineCoverage(method.Value);
-
- average = total / methods.Count;
- return Math.Round(average, 3);
+ {
+ var methodCoverage = CalculateLineCoverage(method.Value.Lines);
+ details.Covered += methodCoverage.Covered;
+ details.Total += methodCoverage.Total;
+ }
+ return details;
}
- public double CalculateLineCoverage(Classes classes)
+ public CoverageDetails CalculateLineCoverage(Classes classes)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var @class in classes)
- total += CalculateLineCoverage(@class.Value);
-
- average = total / classes.Count;
- return Math.Round(average, 3);
+ {
+ var classCoverage = CalculateLineCoverage(@class.Value);
+ details.Covered += classCoverage.Covered;
+ details.Total += classCoverage.Total;
+ }
+ return details;
}
- public double CalculateLineCoverage(Documents documents)
+ public CoverageDetails CalculateLineCoverage(Documents documents)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var document in documents)
- total += CalculateLineCoverage(document.Value);
-
- average = total / documents.Count;
- return Math.Round(average, 3);
+ {
+ var documentCoverage = CalculateLineCoverage(document.Value);
+ details.Covered += documentCoverage.Covered;
+ details.Total += documentCoverage.Total;
+ }
+ return details;
}
- public double CalculateLineCoverage(Modules modules)
+ public CoverageDetails CalculateLineCoverage(Modules modules)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var module in modules)
- total += CalculateLineCoverage(module.Value);
-
- average = total / modules.Count;
- return Math.Round(average, 3);
+ {
+ var moduleCoverage = CalculateLineCoverage(module.Value);
+ details.Covered += moduleCoverage.Covered;
+ details.Total += moduleCoverage.Total;
+ }
+ return details;
}
- public double CalculateBranchCoverage(Lines lines)
+ public CoverageDetails CalculateBranchCoverage(IList branches)
{
- double pointsCovered = lines.Where(l => l.Value.Hits > 0 && l.Value.IsBranchPoint).Count();
- double totalPoints = lines.Where(l => l.Value.IsBranchPoint).Count();
- double coverage = totalPoints == 0 ? totalPoints : pointsCovered / totalPoints;
- return Math.Round(coverage, 3);
+ var details = new CoverageDetails();
+ details.Covered = branches.Count(bi => bi.Hits > 0);
+ details.Total = branches.Count;
+ return details;
}
- public double CalculateBranchCoverage(Methods methods)
+ public CoverageDetails CalculateBranchCoverage(Methods methods)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var method in methods)
- total += CalculateBranchCoverage(method.Value);
-
- average = total / methods.Count;
- return Math.Round(average, 3);
+ {
+ var methodCoverage = CalculateBranchCoverage(method.Value.Branches);
+ details.Covered += methodCoverage.Covered;
+ details.Total += methodCoverage.Total;
+ }
+ return details;
}
- public double CalculateBranchCoverage(Classes classes)
+ public CoverageDetails CalculateBranchCoverage(Classes classes)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var @class in classes)
- total += CalculateBranchCoverage(@class.Value);
-
- average = total / classes.Count;
- return Math.Round(average, 3);
+ {
+ var classCoverage = CalculateBranchCoverage(@class.Value);
+ details.Covered += classCoverage.Covered;
+ details.Total += classCoverage.Total;
+ }
+ return details;
}
- public double CalculateBranchCoverage(Documents documents)
+ public CoverageDetails CalculateBranchCoverage(Documents documents)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var document in documents)
- total += CalculateBranchCoverage(document.Value);
-
- average = total / documents.Count;
- return Math.Round(average, 3);
+ {
+ var documentCoverage = CalculateBranchCoverage(document.Value);
+ details.Covered += documentCoverage.Covered;
+ details.Total += documentCoverage.Total;
+ }
+ return details;
}
- public double CalculateBranchCoverage(Modules modules)
+ public CoverageDetails CalculateBranchCoverage(Modules modules)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var module in modules)
- total += CalculateBranchCoverage(module.Value);
-
- average = total / modules.Count;
- return Math.Round(average, 3);
+ {
+ var moduleCoverage = CalculateBranchCoverage(module.Value);
+ details.Covered += moduleCoverage.Covered;
+ details.Total += moduleCoverage.Total;
+ }
+ return details;
}
- public double CalculateMethodCoverage(Lines lines)
+ public CoverageDetails CalculateMethodCoverage(Lines lines)
{
- if (lines.Any(l => l.Value.Hits > 0))
- return 1;
-
- return 0;
+ var details = new CoverageDetails();
+ details.Covered = lines.Any(l => l.Value > 0) ? 1 : 0;
+ details.Total = 1;
+ return details;
}
- public double CalculateMethodCoverage(Methods methods)
+ public CoverageDetails CalculateMethodCoverage(Methods methods)
{
- double total = 0, average = 0;
- foreach (var method in methods)
- total += CalculateMethodCoverage(method.Value);
-
- average = total / methods.Count;
- return Math.Round(average, 3);
+ var details = new CoverageDetails();
+ var methodsWithLines = methods.Where(m => m.Value.Lines.Count > 0);
+ foreach (var method in methodsWithLines)
+ {
+ var methodCoverage = CalculateMethodCoverage(method.Value.Lines);
+ details.Covered += methodCoverage.Covered;
+ }
+ details.Total = methodsWithLines.Count();
+ return details;
}
- public double CalculateMethodCoverage(Classes classes)
+ public CoverageDetails CalculateMethodCoverage(Classes classes)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var @class in classes)
- total += CalculateMethodCoverage(@class.Value);
-
- average = total / classes.Count;
- return Math.Round(average, 3);
+ {
+ var classCoverage = CalculateMethodCoverage(@class.Value);
+ details.Covered += classCoverage.Covered;
+ details.Total += classCoverage.Total;
+ }
+ return details;
}
- public double CalculateMethodCoverage(Documents documents)
+ public CoverageDetails CalculateMethodCoverage(Documents documents)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var document in documents)
- total += CalculateMethodCoverage(document.Value);
-
- average = total / documents.Count;
- return Math.Round(average, 3);
+ {
+ var documentCoverage = CalculateMethodCoverage(document.Value);
+ details.Covered += documentCoverage.Covered;
+ details.Total += documentCoverage.Total;
+ }
+ return details;
}
- public double CalculateMethodCoverage(Modules modules)
+ public CoverageDetails CalculateMethodCoverage(Modules modules)
{
- double total = 0, average = 0;
+ var details = new CoverageDetails();
foreach (var module in modules)
- total += CalculateMethodCoverage(module.Value);
-
- average = total / modules.Count;
- return Math.Round(average, 3);
+ {
+ var moduleCoverage = CalculateMethodCoverage(module.Value);
+ details.Covered += moduleCoverage.Covered;
+ details.Total += moduleCoverage.Total;
+ }
+ return details;
}
}
}
\ No newline at end of file
diff --git a/src/coverlet.core/CoverageTracker.cs b/src/coverlet.core/CoverageTracker.cs
deleted file mode 100644
index 0ff0b4a69..000000000
--- a/src/coverlet.core/CoverageTracker.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-using Coverlet.Core.Attributes;
-using Coverlet.Core.Extensions;
-
-namespace Coverlet.Core
-{
- public static class CoverageTracker
- {
- private static Dictionary> _markers;
-
- [ExcludeFromCoverage]
- static CoverageTracker()
- {
- _markers = new Dictionary>();
- AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
- }
-
- [ExcludeFromCoverage]
- public static void MarkExecuted(string path, string marker)
- {
- lock (_markers)
- {
- _markers.TryAdd(path, new List());
- _markers[path].Add(marker);
- if (_markers[path].Count >= 100000)
- {
- File.AppendAllLines(path, _markers[path]);
- _markers[path].Clear();
- }
- }
- }
-
- public static void CurrentDomain_ProcessExit(object sender, EventArgs e)
- {
- foreach (var kvp in _markers)
- File.AppendAllLines(kvp.Key, kvp.Value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/coverlet.core/Extensions/HelperExtensions.cs b/src/coverlet.core/Extensions/HelperExtensions.cs
new file mode 100644
index 000000000..fe8f45cae
--- /dev/null
+++ b/src/coverlet.core/Extensions/HelperExtensions.cs
@@ -0,0 +1,16 @@
+
+using System;
+using Coverlet.Core.Attributes;
+
+namespace Coverlet.Core.Extensions
+{
+ internal static class HelperExtensions
+ {
+ [ExcludeFromCoverage]
+ public static TRet Maybe(this T value, Func action, TRet defValue = default(TRet))
+ where T : class
+ {
+ return (value != null) ? action(value) : defValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs
index 96b140f03..1919c1363 100644
--- a/src/coverlet.core/Helpers/InstrumentationHelper.cs
+++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;
+using System.Text.RegularExpressions;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
@@ -12,10 +13,10 @@ namespace Coverlet.Core.Helpers
{
internal static class InstrumentationHelper
{
- public static string[] GetDependencies(string module)
+ public static string[] GetCoverableModules(string module)
{
- IEnumerable modules = Directory.GetFiles(Path.GetDirectoryName(module), "*.dll");
- modules = modules.Where(a => IsAssembly(a) && Path.GetFileName(a) != Path.GetFileName(module));
+ IEnumerable modules = Directory.EnumerateFiles(Path.GetDirectoryName(module)).Where(f => f.EndsWith(".exe") || f.EndsWith(".dll"));
+ modules = modules.Where(m => IsAssembly(m) && Path.GetFileName(m) != Path.GetFileName(module));
return modules.ToArray();
}
@@ -40,15 +41,13 @@ public static bool HasPdb(string module)
public static void CopyCoverletDependency(string module)
{
- var directory = Path.GetDirectoryName(module);
var moduleFileName = Path.GetFileName(module);
-
- var assembly = typeof(Coverage).Assembly;
- string name = Path.GetFileName(assembly.Location);
- if (name == moduleFileName)
+ if (Path.GetFileName(typeof(Coverage).Assembly.Location) == moduleFileName)
return;
- File.Copy(assembly.Location, Path.Combine(directory, name), true);
+ var directory = Path.GetDirectoryName(module);
+ var assembly = typeof(Coverlet.Tracker.CoverageTracker).Assembly;
+ File.Copy(assembly.Location, Path.Combine(directory, Path.GetFileName(assembly.Location)), true);
}
public static void BackupOriginalModule(string module, string identifier)
@@ -65,54 +64,135 @@ public static void RestoreOriginalModule(string module, string identifier)
// See: https://github.com/tonerdo/coverlet/issues/25
var retryStrategy = CreateRetryStrategy();
- RetryHelper.Retry(() => {
+ RetryHelper.Retry(() =>
+ {
File.Copy(backupPath, module, true);
File.Delete(backupPath);
}, retryStrategy, 10);
}
- public static IEnumerable ReadHitsFile(string path)
+ public static void DeleteHitsFile(string path)
{
// Retry hitting the hits file - retry up to 10 times, since the file could be locked
// See: https://github.com/tonerdo/coverlet/issues/25
var retryStrategy = CreateRetryStrategy();
+ RetryHelper.Retry(() => File.Delete(path), retryStrategy, 10);
+ }
+
+ public static bool IsValidFilterExpression(string filter)
+ {
+ if (filter == null)
+ return false;
+
+ if (!filter.StartsWith("["))
+ return false;
+
+ if (!filter.Contains("]"))
+ return false;
+
+ if (filter.Count(f => f == '[') > 1)
+ return false;
+
+ if (filter.Count(f => f == ']') > 1)
+ return false;
+
+ if (filter.IndexOf(']') < filter.IndexOf('['))
+ return false;
+
+ if (filter.IndexOf(']') - filter.IndexOf('[') == 1)
+ return false;
- return RetryHelper.Do(() => File.ReadLines(path), retryStrategy, 10);
+ if (filter.EndsWith("]"))
+ return false;
+
+ if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", "")))
+ return false;
+
+ return true;
}
- public static void DeleteHitsFile(string path)
+ public static bool IsModuleExcluded(string module, string[] filters)
{
- // Retry hitting the hits file - retry up to 10 times, since the file could be locked
- // See: https://github.com/tonerdo/coverlet/issues/25
- var retryStrategy = CreateRetryStrategy();
+ if (filters == null)
+ return false;
- RetryHelper.Retry(() => File.Delete(path), retryStrategy, 10);
+ module = Path.GetFileNameWithoutExtension(module);
+ if (module == null)
+ return false;
+
+ foreach (var filter in filters)
+ {
+ string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1);
+ string typePattern = filter.Substring(filter.IndexOf(']') + 1);
+
+ if (typePattern != "*")
+ continue;
+
+ modulePattern = WildcardToRegex(modulePattern);
+
+ var regex = new Regex(modulePattern);
+
+ if (regex.IsMatch(module))
+ return true;
+ }
+
+ return false;
}
- public static IEnumerable GetExcludedFiles(IEnumerable excludeRules,
- string parentDir = null)
+ public static bool IsTypeExcluded(string module, string type, string[] filters)
+ {
+ if (filters == null)
+ return false;
+
+ module = Path.GetFileNameWithoutExtension(module);
+ if (module == null)
+ return false;
+
+ foreach (var filter in filters)
+ {
+ string typePattern = filter.Substring(filter.IndexOf(']') + 1);
+ string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1);
+
+ typePattern = WildcardToRegex(typePattern);
+ modulePattern = WildcardToRegex(modulePattern);
+
+ if (new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module))
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool IsLocalMethod(string method)
+ => new Regex(WildcardToRegex("<*>*__*|*")).IsMatch(method);
+
+ public static string[] GetExcludedFiles(string[] excludes)
{
const string RELATIVE_KEY = nameof(RELATIVE_KEY);
- parentDir = string.IsNullOrWhiteSpace(parentDir)? Directory.GetCurrentDirectory() : parentDir;
+ string parentDir = Directory.GetCurrentDirectory();
- if (excludeRules == null || !excludeRules.Any()) return Enumerable.Empty();
+ if (excludes == null || !excludes.Any()) return Array.Empty();
- var matcherDict = new Dictionary(){ {RELATIVE_KEY, new Matcher()}};
- foreach (var excludeRule in excludeRules)
+ var matcherDict = new Dictionary() { { RELATIVE_KEY, new Matcher() } };
+ foreach (var excludeRule in excludes)
{
- if (Path.IsPathRooted(excludeRule)) {
+ if (Path.IsPathRooted(excludeRule))
+ {
var root = Path.GetPathRoot(excludeRule);
- if (!matcherDict.ContainsKey(root)) {
+ if (!matcherDict.ContainsKey(root))
+ {
matcherDict.Add(root, new Matcher());
}
matcherDict[root].AddInclude(excludeRule.Substring(root.Length));
- } else {
+ }
+ else
+ {
matcherDict[RELATIVE_KEY].AddInclude(excludeRule);
}
}
var files = new List();
- foreach(var entry in matcherDict)
+ foreach (var entry in matcherDict)
{
var root = entry.Key;
var matcher = entry.Value;
@@ -123,20 +203,7 @@ public static IEnumerable GetExcludedFiles(IEnumerable excludeRu
files.AddRange(currentFiles);
}
- return files.Distinct();
- }
-
- private static bool IsAssembly(string filePath)
- {
- try
- {
- AssemblyName.GetAssemblyName(filePath);
- return true;
- }
- catch
- {
- return false;
- }
+ return files.Distinct().ToArray();
}
private static string GetBackupPath(string module, string identifier)
@@ -158,6 +225,26 @@ TimeSpan retryStrategy()
return retryStrategy;
}
+
+ private static string WildcardToRegex(string pattern)
+ {
+ return "^" + Regex.Escape(pattern).
+ Replace("\\*", ".*").
+ Replace("\\?", "?") + "$";
+ }
+
+ private static bool IsAssembly(string filePath)
+ {
+ try
+ {
+ AssemblyName.GetAssemblyName(filePath);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
}
}
diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs
index 1387546d0..0b287295d 100644
--- a/src/coverlet.core/Instrumentation/Instrumenter.cs
+++ b/src/coverlet.core/Instrumentation/Instrumenter.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using Coverlet.Core.Attributes;
using Coverlet.Core.Helpers;
+using Coverlet.Core.Symbols;
+using Coverlet.Tracker;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -17,15 +20,17 @@ internal class Instrumenter
{
private readonly string _module;
private readonly string _identifier;
- private readonly IEnumerable _excludedFiles;
+ private readonly string[] _filters;
+ private readonly string[] _excludedFiles;
private readonly static Lazy _markExecutedMethodLoader = new Lazy(GetMarkExecutedMethod);
private InstrumenterResult _result;
- public Instrumenter(string module, string identifier, IEnumerable excludedFiles = null)
+ public Instrumenter(string module, string identifier, string[] filters, string[] excludedFiles)
{
_module = module;
_identifier = identifier;
- _excludedFiles = excludedFiles ?? Enumerable.Empty();
+ _filters = filters;
+ _excludedFiles = excludedFiles ?? Array.Empty();
}
public bool CanInstrument() => InstrumentationHelper.HasPdb(_module);
@@ -57,11 +62,16 @@ private void InstrumentModule()
{
resolver.AddSearchDirectory(Path.GetDirectoryName(_module));
var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver };
+
using (var module = ModuleDefinition.ReadModule(stream, parameters))
{
- foreach (var type in module.GetTypes())
+ var types = module.GetTypes();
+ foreach (TypeDefinition type in types)
{
- InstrumentType(type);
+ var actualType = type.DeclaringType ?? type;
+ if (!actualType.CustomAttributes.Any(IsExcludeAttribute)
+ && !InstrumentationHelper.IsTypeExcluded(_module, actualType.FullName, _filters))
+ InstrumentType(type);
}
module.Write(stream);
@@ -71,12 +81,14 @@ private void InstrumentModule()
private void InstrumentType(TypeDefinition type)
{
- if (type.CustomAttributes.Any(IsExcludeAttribute))
- return;
-
- foreach (var method in type.Methods)
+ var methods = type.GetMethods();
+ foreach (var method in methods)
{
- if (!method.CustomAttributes.Any(IsExcludeAttribute))
+ MethodDefinition actualMethod = method;
+ if (InstrumentationHelper.IsLocalMethod(method.Name))
+ actualMethod = methods.FirstOrDefault(m => m.Name == method.Name.Split('>')[0].Substring(1)) ?? method;
+
+ if (!actualMethod.CustomAttributes.Any(IsExcludeAttribute))
InstrumentMethod(method);
}
}
@@ -99,52 +111,74 @@ private void InstrumentMethod(MethodDefinition method)
private void InstrumentIL(MethodDefinition method)
{
+ method.Body.SimplifyMacros();
ILProcessor processor = method.Body.GetILProcessor();
var index = 0;
var count = processor.Body.Instructions.Count;
+ var branchPoints = CecilSymbolHelper.GetBranchPoints(method);
+
for (int n = 0; n < count; n++)
{
var instruction = processor.Body.Instructions[index];
var sequencePoint = method.DebugInformation.GetSequencePoint(instruction);
- if (sequencePoint == null || sequencePoint.IsHidden)
+ var targetedBranchPoints = branchPoints.Where(p => p.EndOffset == instruction.Offset);
+
+ if (sequencePoint != null && !sequencePoint.IsHidden)
{
- index++;
- continue;
- }
+ var target = AddInstrumentationCode(method, processor, instruction, sequencePoint);
+ foreach (var _instruction in processor.Body.Instructions)
+ ReplaceInstructionTarget(_instruction, instruction, target);
- var target = AddInstrumentationCode(method, processor, instruction, sequencePoint);
- foreach (var _instruction in processor.Body.Instructions)
- ReplaceInstructionTarget(_instruction, instruction, target);
+ foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers)
+ ReplaceExceptionHandlerBoundary(handler, instruction, target);
- foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers)
- ReplaceExceptionHandlerBoundary(handler, instruction, target);
+ index += 3;
+ }
- index += 4;
+ foreach (var _branchTarget in targetedBranchPoints)
+ {
+ /*
+ * Skip branches with no sequence point reference for now.
+ * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method.
+ * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which
+ * I am currently not sure how to handle.
+ */
+ if (_branchTarget.StartLine == -1 || _branchTarget.Document == null)
+ continue;
+
+ var target = AddInstrumentationCode(method, processor, instruction, _branchTarget);
+ foreach (var _instruction in processor.Body.Instructions)
+ ReplaceInstructionTarget(_instruction, instruction, target);
+
+ foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers)
+ ReplaceExceptionHandlerBoundary(handler, instruction, target);
+
+ index += 3;
+ }
+
+ index++;
}
- method.Body.SimplifyMacros();
method.Body.OptimizeMacros();
}
private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint)
{
- var document = _result.Documents.FirstOrDefault(d => d.Path == sequencePoint.Document.Url);
- if (document == null)
- {
+ if (!_result.Documents.TryGetValue(sequencePoint.Document.Url, out var document))
+ {
document = new Document { Path = sequencePoint.Document.Url };
- _result.Documents.Add(document);
+ _result.Documents.Add(document.Path, document);
}
for (int i = sequencePoint.StartLine; i <= sequencePoint.EndLine; i++)
{
- if (!document.Lines.Exists(l => l.Number == i))
- document.Lines.Add(new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName });
+ if (!document.Lines.ContainsKey(i))
+ document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName });
}
- string flag = IsBranchTarget(processor, instruction) ? "B" : "L";
- string marker = $"{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine},{flag}";
+ string marker = $"L,{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine}";
var pathInstr = Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath);
var markInstr = Instruction.Create(OpCodes.Ldstr, marker);
@@ -157,21 +191,40 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor
return pathInstr;
}
- private static bool IsBranchTarget(ILProcessor processor, Instruction instruction)
+ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint)
{
- foreach (var _instruction in processor.Body.Instructions)
- {
- if (_instruction.Operand is Instruction target)
- {
- if (target == instruction)
- return true;
- }
-
- if (_instruction.Operand is Instruction[] targets)
- return targets.Any(t => t == instruction);
+ if (!_result.Documents.TryGetValue(branchPoint.Document, out var document))
+ {
+ document = new Document { Path = branchPoint.Document };
+ _result.Documents.Add(document.Path, document);
}
- return false;
+ var key = (branchPoint.StartLine, (int)branchPoint.Ordinal);
+ if (!document.Branches.ContainsKey(key))
+ document.Branches.Add(key,
+ new Branch
+ {
+ Number = branchPoint.StartLine,
+ Class = method.DeclaringType.FullName,
+ Method = method.FullName,
+ Offset = branchPoint.Offset,
+ EndOffset = branchPoint.EndOffset,
+ Path = branchPoint.Path,
+ Ordinal = branchPoint.Ordinal
+ }
+ );
+
+ string marker = $"B,{document.Path},{branchPoint.StartLine},{branchPoint.Ordinal}";
+
+ var pathInstr = Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath);
+ var markInstr = Instruction.Create(OpCodes.Ldstr, marker);
+ var callInstr = Instruction.Create(OpCodes.Call, processor.Body.Method.Module.ImportReference(_markExecutedMethodLoader.Value));
+
+ processor.InsertBefore(instruction, callInstr);
+ processor.InsertBefore(callInstr, markInstr);
+ processor.InsertBefore(markInstr, pathInstr);
+
+ return pathInstr;
}
private static void ReplaceInstructionTarget(Instruction instruction, Instruction oldTarget, Instruction newTarget)
@@ -214,7 +267,16 @@ private static void ReplaceExceptionHandlerBoundary(ExceptionHandler handler, In
private static bool IsExcludeAttribute(CustomAttribute customAttribute)
{
- return customAttribute.AttributeType.Name == nameof(ExcludeFromCoverageAttribute) || customAttribute.AttributeType.Name == "ExcludeFromCoverage";
+ var excludeAttributeNames = new[]
+ {
+ nameof(ExcludeFromCoverageAttribute),
+ "ExcludeFromCoverage",
+ nameof(ExcludeFromCodeCoverageAttribute),
+ "ExcludeFromCodeCoverage"
+ };
+
+ var attributeName = customAttribute.AttributeType.Name;
+ return excludeAttributeNames.Any(a => a.Equals(attributeName));
}
private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method)
diff --git a/src/coverlet.core/Instrumentation/InstrumenterResult.cs b/src/coverlet.core/Instrumentation/InstrumenterResult.cs
index dd7c9aef0..615e2e316 100644
--- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs
+++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs
@@ -7,24 +7,41 @@ internal class Line
public int Number;
public string Class;
public string Method;
- public bool IsBranchTarget;
public int Hits;
}
+ internal class Branch : Line
+ {
+ public int Offset;
+ public int EndOffset;
+ public int Path;
+ public uint Ordinal;
+ }
+
internal class Document
{
- public Document() => Lines = new List();
+ public Document()
+ {
+ Lines = new Dictionary();
+ Branches = new Dictionary<(int Line, int Ordinal), Branch>();
+ }
public string Path;
- public List Lines { get; private set; }
+
+ public Dictionary Lines { get; private set; }
+ public Dictionary<(int Line, int Ordinal), Branch> Branches { get; private set; }
}
internal class InstrumenterResult
{
- public InstrumenterResult() => Documents = new List();
+ public InstrumenterResult()
+ {
+ Documents = new Dictionary();
+ }
+
public string Module;
public string HitsFilePath;
public string ModulePath;
- public List Documents { get; private set; }
+ public Dictionary Documents { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/coverlet.core/Properties/AssemblyInfo.cs b/src/coverlet.core/Properties/AssemblyInfo.cs
index fb93e0f80..ab633e6d9 100644
--- a/src/coverlet.core/Properties/AssemblyInfo.cs
+++ b/src/coverlet.core/Properties/AssemblyInfo.cs
@@ -1 +1,7 @@
-[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("Coverlet.Core.Tests")]
\ No newline at end of file
+[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.core.snk")]
+[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("Coverlet.Core.Tests,PublicKey=" +
+"0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a8" +
+"2e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e8" +
+"6c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054" +
+"d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722" +
+"aead64ad")]
\ No newline at end of file
diff --git a/src/coverlet.core/Reporters/CoberturaReporter.cs b/src/coverlet.core/Reporters/CoberturaReporter.cs
index 16f3c40d1..e2f1772b0 100644
--- a/src/coverlet.core/Reporters/CoberturaReporter.cs
+++ b/src/coverlet.core/Reporters/CoberturaReporter.cs
@@ -12,18 +12,19 @@ public class CoberturaReporter : IReporter
{
public string Format => "cobertura";
- public string Extension => "xml";
+ public string Extension => "cobertura.xml";
public string Report(CoverageResult result)
{
CoverageSummary summary = new CoverageSummary();
- int totalLines = 0, coveredLines = 0, totalBranches = 0, coveredBranches = 0;
+ var lineCoverage = summary.CalculateLineCoverage(result.Modules);
+ var branchCoverage = summary.CalculateBranchCoverage(result.Modules);
XDocument xml = new XDocument();
XElement coverage = new XElement("coverage");
- coverage.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(result.Modules).ToString()));
- coverage.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(result.Modules).ToString()));
+ coverage.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(result.Modules).Percent.ToString()));
+ coverage.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(result.Modules).Percent.ToString()));
coverage.Add(new XAttribute("version", "1.9"));
coverage.Add(new XAttribute("timestamp", ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString()));
@@ -36,8 +37,8 @@ public string Report(CoverageResult result)
{
XElement package = new XElement("package");
package.Add(new XAttribute("name", Path.GetFileNameWithoutExtension(module.Key)));
- package.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(module.Value).ToString()));
- package.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(module.Value).ToString()));
+ package.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(module.Value).Percent.ToString()));
+ package.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(module.Value).Percent.ToString()));
package.Add(new XAttribute("complexity", "0"));
XElement classes = new XElement("classes");
@@ -48,8 +49,8 @@ public string Report(CoverageResult result)
XElement @class = new XElement("class");
@class.Add(new XAttribute("name", cls.Key));
@class.Add(new XAttribute("filename", GetRelativePathFromBase(basePath, document.Key)));
- @class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).ToString()));
- @class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).ToString()));
+ @class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).Percent.ToString()));
+ @class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).Percent.ToString()));
@class.Add(new XAttribute("complexity", "0"));
XElement classLines = new XElement("lines");
@@ -57,37 +58,41 @@ public string Report(CoverageResult result)
foreach (var meth in cls.Value)
{
+ // Skip all methods with no lines
+ if (meth.Value.Lines.Count == 0)
+ continue;
+
XElement method = new XElement("method");
method.Add(new XAttribute("name", meth.Key.Split(':')[2].Split('(')[0]));
method.Add(new XAttribute("signature", "(" + meth.Key.Split(':')[2].Split('(')[1]));
- method.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(meth.Value).ToString()));
- method.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(meth.Value).ToString()));
+ method.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(meth.Value.Lines).Percent.ToString()));
+ method.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(meth.Value.Branches).Percent.ToString()));
XElement lines = new XElement("lines");
- foreach (var ln in meth.Value)
+ foreach (var ln in meth.Value.Lines)
{
+ bool isBranchPoint = meth.Value.Branches.Any(b => b.Line == ln.Key);
XElement line = new XElement("line");
line.Add(new XAttribute("number", ln.Key.ToString()));
- line.Add(new XAttribute("hits", ln.Value.Hits.ToString()));
- line.Add(new XAttribute("branch", ln.Value.IsBranchPoint.ToString()));
-
- totalLines++;
- if (ln.Value.Hits > 0) coveredLines++;
+ line.Add(new XAttribute("hits", ln.Value.ToString()));
+ line.Add(new XAttribute("branch", isBranchPoint.ToString()));
-
- if (ln.Value.IsBranchPoint)
+ if (isBranchPoint)
{
- line.Add(new XAttribute("condition-coverage", "100% (1/1)"));
+ var branches = meth.Value.Branches.Where(b => b.Line == ln.Key).ToList();
+ var branchInfoCoverage = summary.CalculateBranchCoverage(branches);
+ line.Add(new XAttribute("condition-coverage", $"{branchInfoCoverage.Percent*100}% ({branchInfoCoverage.Covered}/{branchInfoCoverage.Total})"));
XElement conditions = new XElement("conditions");
- XElement condition = new XElement("condition");
- condition.Add(new XAttribute("number", "0"));
- condition.Add(new XAttribute("type", "jump"));
- condition.Add(new XAttribute("coverage", "100%"));
-
- totalBranches++;
- if (ln.Value.Hits > 0) coveredBranches++;
+ var byOffset = branches.GroupBy(b => b.Offset).ToDictionary(b => b.Key, b => b.ToList());
+ foreach (var entry in byOffset)
+ {
+ XElement condition = new XElement("condition");
+ condition.Add(new XAttribute("number", entry.Key));
+ condition.Add(new XAttribute("type", entry.Value.Count() > 2 ? "switch" : "jump")); // Just guessing here
+ condition.Add(new XAttribute("coverage", $"{summary.CalculateBranchCoverage(entry.Value).Percent * 100}%"));
+ conditions.Add(condition);
+ }
- conditions.Add(condition);
line.Add(conditions);
}
@@ -110,10 +115,10 @@ public string Report(CoverageResult result)
packages.Add(package);
}
- coverage.Add(new XAttribute("lines-covered", coveredLines.ToString()));
- coverage.Add(new XAttribute("lines-valid", totalLines.ToString()));
- coverage.Add(new XAttribute("branches-covered", coveredBranches.ToString()));
- coverage.Add(new XAttribute("branches-valid", totalBranches.ToString()));
+ coverage.Add(new XAttribute("lines-covered", lineCoverage.Covered.ToString()));
+ coverage.Add(new XAttribute("lines-valid", lineCoverage.Total.ToString()));
+ coverage.Add(new XAttribute("branches-covered", branchCoverage.Covered.ToString()));
+ coverage.Add(new XAttribute("branches-valid", branchCoverage.Total.ToString()));
coverage.Add(sources);
coverage.Add(packages);
diff --git a/src/coverlet.core/Reporters/LcovReporter.cs b/src/coverlet.core/Reporters/LcovReporter.cs
index 327de30e5..0def9057d 100644
--- a/src/coverlet.core/Reporters/LcovReporter.cs
+++ b/src/coverlet.core/Reporters/LcovReporter.cs
@@ -12,58 +12,47 @@ public class LcovReporter : IReporter
public string Report(CoverageResult result)
{
+ CoverageSummary summary = new CoverageSummary();
List lcov = new List();
- int numSequencePoints = 0, numBranchPoints = 0, numMethods = 0, numBlockBranch = 1;
- int visitedSequencePoints = 0, visitedBranchPoints = 0, visitedMethods = 0;
foreach (var module in result.Modules)
{
foreach (var doc in module.Value)
{
+ var docLineCoverage = summary.CalculateLineCoverage(doc.Value);
+ var docBranchCoverage = summary.CalculateBranchCoverage(doc.Value);
+ var docMethodCoverage = summary.CalculateMethodCoverage(doc.Value);
+
lcov.Add("SF:" + doc.Key);
foreach (var @class in doc.Value)
{
- bool methodVisited = false;
foreach (var method in @class.Value)
{
- lcov.Add($"FN:{method.Value.First().Key - 1},{method.Key}");
- lcov.Add($"FNDA:{method.Value.First().Value.Hits},{method.Key}");
+ // Skip all methods with no lines
+ if (method.Value.Lines.Count == 0)
+ continue;
- foreach (var line in method.Value)
- {
- lcov.Add($"DA:{line.Key},{line.Value.Hits}");
- numSequencePoints++;
+ lcov.Add($"FN:{method.Value.Lines.First().Key - 1},{method.Key}");
+ lcov.Add($"FNDA:{method.Value.Lines.First().Value},{method.Key}");
- if (line.Value.IsBranchPoint)
- {
- lcov.Add($"BRDA:{line.Key},{numBlockBranch},{numBlockBranch},{line.Value.Hits}");
- numBlockBranch++;
- numBranchPoints++;
- }
+ foreach (var line in method.Value.Lines)
+ lcov.Add($"DA:{line.Key},{line.Value}");
- if (line.Value.Hits > 0)
- {
- visitedSequencePoints++;
- methodVisited = true;
- if (line.Value.IsBranchPoint)
- visitedBranchPoints++;
- }
+ foreach (var branch in method.Value.Branches)
+ {
+ lcov.Add($"BRDA:{branch.Line},{branch.Offset},{branch.Path},{branch.Hits}");
}
-
- numMethods++;
- if (methodVisited)
- visitedMethods++;
}
}
- lcov.Add($"LH:{visitedSequencePoints}");
- lcov.Add($"LF:{numSequencePoints}");
+ lcov.Add($"LF:{docLineCoverage.Total}");
+ lcov.Add($"LH:{docLineCoverage.Covered}");
- lcov.Add($"BRF:{numBranchPoints}");
- lcov.Add($"BRH:{visitedBranchPoints}");
+ lcov.Add($"BRF:{docBranchCoverage.Total}");
+ lcov.Add($"BRH:{docBranchCoverage.Covered}");
- lcov.Add($"FNF:{numMethods}");
- lcov.Add($"FNH:{visitedMethods}");
+ lcov.Add($"FNF:{docMethodCoverage.Total}");
+ lcov.Add($"FNH:{docMethodCoverage.Covered}");
lcov.Add("end_of_record");
}
diff --git a/src/coverlet.core/Reporters/OpenCoverReporter.cs b/src/coverlet.core/Reporters/OpenCoverReporter.cs
index 9e4fb4694..1464fc152 100644
--- a/src/coverlet.core/Reporters/OpenCoverReporter.cs
+++ b/src/coverlet.core/Reporters/OpenCoverReporter.cs
@@ -11,7 +11,7 @@ public class OpenCoverReporter : IReporter
{
public string Format => "opencover";
- public string Extension => "xml";
+ public string Extension => "opencover.xml";
public string Report(CoverageResult result)
{
@@ -21,8 +21,8 @@ public string Report(CoverageResult result)
XElement coverageSummary = new XElement("Summary");
XElement modules = new XElement("Modules");
- int numSequencePoints = 0, numBranchPoints = 0, numClasses = 0, numMethods = 0;
- int visitedSequencePoints = 0, visitedBranchPoints = 0, visitedClasses = 0, visitedMethods = 0;
+ int numClasses = 0, numMethods = 0;
+ int visitedClasses = 0, visitedMethods = 0;
int i = 1;
@@ -62,12 +62,19 @@ public string Report(CoverageResult result)
foreach (var meth in cls.Value)
{
+ // Skip all methods with no lines
+ if (meth.Value.Lines.Count == 0)
+ continue;
+
+ var methLineCoverage = summary.CalculateLineCoverage(meth.Value.Lines);
+ var methBranchCoverage = summary.CalculateBranchCoverage(meth.Value.Branches);
+
XElement method = new XElement("Method");
method.Add(new XAttribute("cyclomaticComplexity", "0"));
method.Add(new XAttribute("nPathComplexity", "0"));
- method.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(meth.Value).ToString()));
- method.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(meth.Value).ToString()));
+ method.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString()));
+ method.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString()));
method.Add(new XAttribute("isConstructor", meth.Key.Contains("ctor").ToString()));
method.Add(new XAttribute("isGetter", meth.Key.Contains("get_").ToString()));
method.Add(new XAttribute("isSetter", meth.Key.Contains("set_").ToString()));
@@ -79,15 +86,15 @@ public string Report(CoverageResult result)
fileRef.Add(new XAttribute("uid", i.ToString()));
XElement methodPoint = new XElement("MethodPoint");
- methodPoint.Add(new XAttribute("vc", meth.Value.Select(l => l.Value.Hits).Sum().ToString()));
+ methodPoint.Add(new XAttribute("vc", methLineCoverage.Covered.ToString()));
methodPoint.Add(new XAttribute("upsid", "0"));
methodPoint.Add(new XAttribute(XName.Get("type", "xsi"), "SequencePoint"));
methodPoint.Add(new XAttribute("ordinal", j.ToString()));
methodPoint.Add(new XAttribute("offset", j.ToString()));
methodPoint.Add(new XAttribute("sc", "0"));
- methodPoint.Add(new XAttribute("sl", meth.Value.First().Key.ToString()));
+ methodPoint.Add(new XAttribute("sl", meth.Value.Lines.First().Key.ToString()));
methodPoint.Add(new XAttribute("ec", "1"));
- methodPoint.Add(new XAttribute("el", meth.Value.Last().Key.ToString()));
+ methodPoint.Add(new XAttribute("el", meth.Value.Lines.Last().Key.ToString()));
methodPoint.Add(new XAttribute("bec", "0"));
methodPoint.Add(new XAttribute("bev", "0"));
methodPoint.Add(new XAttribute("fileid", i.ToString()));
@@ -100,10 +107,10 @@ public string Report(CoverageResult result)
int kBr = 0;
var methodVisited = false;
- foreach (var lines in meth.Value)
+ foreach (var lines in meth.Value.Lines)
{
XElement sequencePoint = new XElement("SequencePoint");
- sequencePoint.Add(new XAttribute("vc", lines.Value.Hits.ToString()));
+ sequencePoint.Add(new XAttribute("vc", lines.Value.ToString()));
sequencePoint.Add(new XAttribute("upsid", lines.Key.ToString()));
sequencePoint.Add(new XAttribute("ordinal", k.ToString()));
sequencePoint.Add(new XAttribute("sl", lines.Key.ToString()));
@@ -115,45 +122,40 @@ public string Report(CoverageResult result)
sequencePoint.Add(new XAttribute("fileid", i.ToString()));
sequencePoints.Add(sequencePoint);
- if (lines.Value.IsBranchPoint)
+ if (lines.Value > 0)
{
- XElement branchPoint = new XElement("BranchPoint");
- branchPoint.Add(new XAttribute("vc", lines.Value.Hits.ToString()));
- branchPoint.Add(new XAttribute("upsid", lines.Key.ToString()));
- branchPoint.Add(new XAttribute("ordinal", kBr.ToString()));
- branchPoint.Add(new XAttribute("path", ""));
- branchPoint.Add(new XAttribute("offset", kBr.ToString()));
- branchPoint.Add(new XAttribute("offsetend", kBr.ToString()));
- branchPoint.Add(new XAttribute("sl", lines.Key.ToString()));
- branchPoint.Add(new XAttribute("fileid", i.ToString()));
- branchPoints.Add(branchPoint);
- kBr++;
- numBranchPoints++;
- }
-
- numSequencePoints++;
- if (lines.Value.Hits > 0)
- {
- visitedSequencePoints++;
classVisited = true;
methodVisited = true;
- if (lines.Value.IsBranchPoint)
- visitedBranchPoints++;
}
k++;
}
+ foreach (var branche in meth.Value.Branches)
+ {
+ XElement branchPoint = new XElement("BranchPoint");
+ branchPoint.Add(new XAttribute("vc", branche.Hits.ToString()));
+ branchPoint.Add(new XAttribute("upsid", branche.Line.ToString()));
+ branchPoint.Add(new XAttribute("ordinal", branche.Ordinal.ToString()));
+ branchPoint.Add(new XAttribute("path", branche.Path.ToString()));
+ branchPoint.Add(new XAttribute("offset", branche.Offset.ToString()));
+ branchPoint.Add(new XAttribute("offsetend", branche.EndOffset.ToString()));
+ branchPoint.Add(new XAttribute("sl", branche.Line.ToString()));
+ branchPoint.Add(new XAttribute("fileid", i.ToString()));
+ branchPoints.Add(branchPoint);
+ kBr++;
+ }
+
numMethods++;
if (methodVisited)
visitedMethods++;
- methodSummary.Add(new XAttribute("numSequencePoints", meth.Value.Count().ToString()));
- methodSummary.Add(new XAttribute("visitedSequencePoints", meth.Value.Where(l => l.Value.Hits > 0).Count().ToString()));
- methodSummary.Add(new XAttribute("numBranchPoints", meth.Value.Where(l => l.Value.IsBranchPoint).Count().ToString()));
- methodSummary.Add(new XAttribute("visitedBranchPoints", meth.Value.Where(l => l.Value.IsBranchPoint && l.Value.Hits > 0).Count().ToString()));
- methodSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(meth.Value).ToString()));
- methodSummary.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(meth.Value).ToString()));
+ methodSummary.Add(new XAttribute("numSequencePoints", methLineCoverage.Total.ToString()));
+ methodSummary.Add(new XAttribute("visitedSequencePoints", methLineCoverage.Covered.ToString()));
+ methodSummary.Add(new XAttribute("numBranchPoints", methBranchCoverage.Total.ToString()));
+ methodSummary.Add(new XAttribute("visitedBranchPoints", methBranchCoverage.Covered.ToString()));
+ methodSummary.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString()));
+ methodSummary.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString()));
methodSummary.Add(new XAttribute("maxCyclomaticComplexity", "0"));
methodSummary.Add(new XAttribute("minCyclomaticComplexity", "0"));
methodSummary.Add(new XAttribute("visitedClasses", "0"));
@@ -176,18 +178,22 @@ public string Report(CoverageResult result)
if (classVisited)
visitedClasses++;
- classSummary.Add(new XAttribute("numSequencePoints", cls.Value.Select(c => c.Value.Count).Sum().ToString()));
- classSummary.Add(new XAttribute("visitedSequencePoints", cls.Value.Select(c => c.Value.Where(l => l.Value.Hits > 0).Count()).Sum().ToString()));
- classSummary.Add(new XAttribute("numBranchPoints", cls.Value.Select(c => c.Value.Count(l => l.Value.IsBranchPoint)).Sum().ToString()));
- classSummary.Add(new XAttribute("visitedBranchPoints", cls.Value.Select(c => c.Value.Where(l => l.Value.Hits > 0 && l.Value.IsBranchPoint).Count()).Sum().ToString()));
- classSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(cls.Value).ToString()));
- classSummary.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(cls.Value).ToString()));
+ var classLineCoverage = summary.CalculateLineCoverage(cls.Value);
+ var classBranchCoverage = summary.CalculateBranchCoverage(cls.Value);
+ var classMethodCoverage = summary.CalculateMethodCoverage(cls.Value);
+
+ classSummary.Add(new XAttribute("numSequencePoints", classLineCoverage.Total.ToString()));
+ classSummary.Add(new XAttribute("visitedSequencePoints", classLineCoverage.Covered.ToString()));
+ classSummary.Add(new XAttribute("numBranchPoints", classBranchCoverage.Total.ToString()));
+ classSummary.Add(new XAttribute("visitedBranchPoints", classBranchCoverage.Covered.ToString()));
+ classSummary.Add(new XAttribute("sequenceCoverage", classLineCoverage.Percent.ToString()));
+ classSummary.Add(new XAttribute("branchCoverage", classBranchCoverage.Percent.ToString()));
classSummary.Add(new XAttribute("maxCyclomaticComplexity", "0"));
classSummary.Add(new XAttribute("minCyclomaticComplexity", "0"));
classSummary.Add(new XAttribute("visitedClasses", classVisited ? "1" : "0"));
classSummary.Add(new XAttribute("numClasses", "1"));
- classSummary.Add(new XAttribute("visitedMethods", "0"));
- classSummary.Add(new XAttribute("numMethods", cls.Value.Count.ToString()));
+ classSummary.Add(new XAttribute("visitedMethods", classMethodCoverage.Covered.ToString()));
+ classSummary.Add(new XAttribute("numMethods", classMethodCoverage.Total.ToString()));
@class.Add(classSummary);
@class.Add(className);
@@ -202,12 +208,15 @@ public string Report(CoverageResult result)
modules.Add(module);
}
- coverageSummary.Add(new XAttribute("numSequencePoints", numSequencePoints.ToString()));
- coverageSummary.Add(new XAttribute("visitedSequencePoints", visitedSequencePoints.ToString()));
- coverageSummary.Add(new XAttribute("numBranchPoints", numBranchPoints.ToString()));
- coverageSummary.Add(new XAttribute("visitedBranchPoints", visitedBranchPoints.ToString()));
- coverageSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(result.Modules).ToString()));
- coverageSummary.Add(new XAttribute("branchCoverage", summary.CalculateLineCoverage(result.Modules).ToString()));
+ var moduleLineCoverage = summary.CalculateLineCoverage(result.Modules);
+ var moduleBranchCoverage = summary.CalculateLineCoverage(result.Modules);
+
+ coverageSummary.Add(new XAttribute("numSequencePoints", moduleLineCoverage.Total.ToString()));
+ coverageSummary.Add(new XAttribute("visitedSequencePoints", moduleLineCoverage.Covered.ToString()));
+ coverageSummary.Add(new XAttribute("numBranchPoints", moduleBranchCoverage.Total.ToString()));
+ coverageSummary.Add(new XAttribute("visitedBranchPoints", moduleBranchCoverage.Covered.ToString()));
+ coverageSummary.Add(new XAttribute("sequenceCoverage", moduleLineCoverage.Percent.ToString()));
+ coverageSummary.Add(new XAttribute("branchCoverage", moduleBranchCoverage.Percent.ToString()));
coverageSummary.Add(new XAttribute("maxCyclomaticComplexity", "0"));
coverageSummary.Add(new XAttribute("minCyclomaticComplexity", "0"));
coverageSummary.Add(new XAttribute("visitedClasses", visitedClasses.ToString()));
diff --git a/src/coverlet.core/Reporters/ReporterFactory.cs b/src/coverlet.core/Reporters/ReporterFactory.cs
index c61156b08..813d23970 100644
--- a/src/coverlet.core/Reporters/ReporterFactory.cs
+++ b/src/coverlet.core/Reporters/ReporterFactory.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using System.Collections.Generic;
namespace Coverlet.Core.Reporters
{
diff --git a/src/coverlet.core/Symbols/BranchPoint.cs b/src/coverlet.core/Symbols/BranchPoint.cs
new file mode 100644
index 000000000..427aad61b
--- /dev/null
+++ b/src/coverlet.core/Symbols/BranchPoint.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace Coverlet.Core.Symbols
+{
+ ///
+ /// a branch point
+ ///
+ public class BranchPoint
+ {
+ ///
+ /// Line of the branching instruction
+ ///
+ public int StartLine { get; set; }
+
+ ///
+ /// A path that can be taken
+ ///
+ public int Path { get; set; }
+
+ ///
+ /// An order of the point within the method
+ ///
+ public UInt32 Ordinal { get; set; }
+
+ ///
+ /// List of OffsetPoints between Offset and EndOffset (exclusive)
+ ///
+ public System.Collections.Generic.List OffsetPoints { get; set; }
+
+ ///
+ /// The IL offset of the point
+ ///
+ public int Offset { get; set; }
+
+ ///
+ /// Last Offset == EndOffset.
+ /// Can be same as Offset
+ ///
+ public int EndOffset { get; set; }
+
+ ///
+ /// The url to the document if an entry was not mapped to an id
+ ///
+ public string Document { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs
new file mode 100644
index 000000000..e4c9ca3ae
--- /dev/null
+++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs
@@ -0,0 +1,287 @@
+//
+// This class is based heavily on the work of the OpenCover
+// team in OpenCover.Framework.Symbols.CecilSymbolManager
+//
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+using Coverlet.Core.Extensions;
+
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Collections.Generic;
+
+namespace Coverlet.Core.Symbols
+{
+ public static class CecilSymbolHelper
+ {
+ private const int StepOverLineCode = 0xFEEFEE;
+ private static readonly Regex IsMovenext = new Regex(@"\<[^\s>]+\>\w__\w(\w)?::MoveNext\(\)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ public static List GetBranchPoints(MethodDefinition methodDefinition)
+ {
+ var list = new List();
+ if (methodDefinition == null)
+ return list;
+
+ UInt32 ordinal = 0;
+ var instructions = methodDefinition.Body.Instructions;
+
+ // if method is a generated MoveNext skip first branch (could be a switch or a branch)
+ var skipFirstBranch = IsMovenext.IsMatch(methodDefinition.FullName);
+
+ foreach (var instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch))
+ {
+ try
+ {
+ if (skipFirstBranch)
+ {
+ skipFirstBranch = false;
+ continue;
+ }
+
+ if (BranchIsInGeneratedFinallyBlock(instruction, methodDefinition))
+ continue;
+
+ var pathCounter = 0;
+
+ // store branch origin offset
+ var branchOffset = instruction.Offset;
+ var closestSeqPt = FindClosestInstructionWithSequencePoint(methodDefinition.Body, instruction).Maybe(i => methodDefinition.DebugInformation.GetSequencePoint(i));
+ var branchingInstructionLine = closestSeqPt.Maybe(x => x.StartLine, -1);
+ var document = closestSeqPt.Maybe(x => x.Document.Url);
+
+ if (instruction.Next == null)
+ return list;
+
+ if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition))
+ return list;
+ }
+ catch (Exception)
+ {
+ continue;
+ }
+ }
+ return list;
+ }
+
+ private static bool BuildPointsForConditionalBranch(List list, Instruction instruction,
+ int branchingInstructionLine, string document, int branchOffset, int pathCounter,
+ Collection instructions, ref uint ordinal, MethodDefinition methodDefinition)
+ {
+ // Add Default branch (Path=0)
+
+ // Follow else/default instruction
+ var @else = instruction.Next;
+
+ var pathOffsetList = GetBranchPath(@else);
+
+ // add Path 0
+ var path0 = new BranchPoint
+ {
+ StartLine = branchingInstructionLine,
+ Document = document,
+ Offset = branchOffset,
+ Ordinal = ordinal++,
+ Path = pathCounter++,
+ OffsetPoints =
+ pathOffsetList.Count > 1
+ ? pathOffsetList.GetRange(0, pathOffsetList.Count - 1)
+ : new List(),
+ EndOffset = pathOffsetList.Last()
+ };
+
+ // Add Conditional Branch (Path=1)
+ if (instruction.OpCode.Code != Code.Switch)
+ {
+ // Follow instruction at operand
+ var @then = instruction.Operand as Instruction;
+ if (@then == null)
+ return false;
+
+ ordinal = BuildPointsForBranch(list, then, branchingInstructionLine, document, branchOffset,
+ ordinal, pathCounter, path0, instructions, methodDefinition);
+ }
+ else // instruction.OpCode.Code == Code.Switch
+ {
+ var branchInstructions = instruction.Operand as Instruction[];
+ if (branchInstructions == null || branchInstructions.Length == 0)
+ return false;
+
+ ordinal = BuildPointsForSwitchCases(list, path0, branchInstructions, branchingInstructionLine,
+ document, branchOffset, ordinal, ref pathCounter);
+ }
+ return true;
+ }
+
+ private static uint BuildPointsForBranch(List list, Instruction then, int branchingInstructionLine, string document,
+ int branchOffset, uint ordinal, int pathCounter, BranchPoint path0, Collection instructions, MethodDefinition methodDefinition)
+ {
+ var pathOffsetList1 = GetBranchPath(@then);
+
+ // Add path 1
+ var path1 = new BranchPoint
+ {
+ StartLine = branchingInstructionLine,
+ Document = document,
+ Offset = branchOffset,
+ Ordinal = ordinal++,
+ Path = pathCounter,
+ OffsetPoints =
+ pathOffsetList1.Count > 1
+ ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1)
+ : new List(),
+ EndOffset = pathOffsetList1.Last()
+ };
+
+ // only add branch if branch does not match a known sequence
+ // e.g. auto generated field assignment
+ // or encapsulates at least one sequence point
+ var offsets = new[]
+ {
+ path0.Offset,
+ path0.EndOffset,
+ path1.Offset,
+ path1.EndOffset
+ };
+
+ var ignoreSequences = new[]
+ {
+ // we may need other samples
+ new[] {Code.Brtrue_S, Code.Pop, Code.Ldsfld, Code.Ldftn, Code.Newobj, Code.Dup, Code.Stsfld, Code.Newobj}, // CachedAnonymousMethodDelegate field allocation
+ };
+
+ var bs = offsets.Min();
+ var be = offsets.Max();
+
+ var range = instructions.Where(i => (i.Offset >= bs) && (i.Offset <= be)).ToList();
+
+ var match = ignoreSequences
+ .Where(ignoreSequence => range.Count >= ignoreSequence.Length)
+ .Any(ignoreSequence => range.Zip(ignoreSequence, (instruction, code) => instruction.OpCode.Code == code).All(x => x));
+
+ var count = range
+ .Count(i => methodDefinition.DebugInformation.GetSequencePoint(i) != null);
+
+ if (!match || count > 0)
+ {
+ list.Add(path0);
+ list.Add(path1);
+ }
+ return ordinal;
+ }
+
+ private static uint BuildPointsForSwitchCases(List list, BranchPoint path0, Instruction[] branchInstructions,
+ int branchingInstructionLine, string document, int branchOffset, uint ordinal, ref int pathCounter)
+ {
+ var counter = pathCounter;
+ list.Add(path0);
+ // Add Conditional Branches (Path>0)
+ list.AddRange(branchInstructions.Select(GetBranchPath)
+ .Select(pathOffsetList1 => new BranchPoint
+ {
+ StartLine = branchingInstructionLine,
+ Document = document,
+ Offset = branchOffset,
+ Ordinal = ordinal++,
+ Path = counter++,
+ OffsetPoints =
+ pathOffsetList1.Count > 1
+ ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1)
+ : new List(),
+ EndOffset = pathOffsetList1.Last()
+ }));
+ pathCounter = counter;
+ return ordinal;
+ }
+
+ private static bool BranchIsInGeneratedFinallyBlock(Instruction branchInstruction, MethodDefinition methodDefinition)
+ {
+ if (!methodDefinition.Body.HasExceptionHandlers)
+ return false;
+
+ // a generated finally block will have no sequence points in its range
+ var handlers = methodDefinition.Body.ExceptionHandlers
+ .Where(e => e.HandlerType == ExceptionHandlerType.Finally)
+ .ToList();
+
+ return handlers
+ .Where(e => branchInstruction.Offset >= e.HandlerStart.Offset)
+ .Where( e =>branchInstruction.Offset < e.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, e.HandlerStart.Offset)))
+ .OrderByDescending(h => h.HandlerStart.Offset) // we need to work inside out
+ .Any(eh => !(methodDefinition.DebugInformation.GetSequencePointMapping()
+ .Where(i => i.Value.StartLine != StepOverLineCode)
+ .Any(i => i.Value.Offset >= eh.HandlerStart.Offset && i.Value.Offset < eh.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, eh.HandlerStart.Offset)))));
+ }
+
+ private static int GetOffsetOfNextEndfinally(MethodBody body, int startOffset)
+ {
+ var lastOffset = body.Instructions.LastOrDefault().Maybe(i => i.Offset, int.MaxValue);
+ return body.Instructions.FirstOrDefault(i => i.Offset >= startOffset && i.OpCode.Code == Code.Endfinally).Maybe(i => i.Offset, lastOffset);
+ }
+
+ private static List GetBranchPath(Instruction instruction)
+ {
+ var offsetList = new List();
+
+ if (instruction != null)
+ {
+ var point = instruction;
+ offsetList.Add(point.Offset);
+ while ( point.OpCode == OpCodes.Br || point.OpCode == OpCodes.Br_S )
+ {
+ var nextPoint = point.Operand as Instruction;
+ if (nextPoint != null)
+ {
+ point = nextPoint;
+ offsetList.Add(point.Offset);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ return offsetList;
+ }
+
+ private static Instruction FindClosestInstructionWithSequencePoint(MethodBody methodBody, Instruction instruction)
+ {
+ var sequencePointsInMethod = methodBody.Instructions.Where(i => HasValidSequencePoint(i, methodBody.Method)).ToList();
+ if (!sequencePointsInMethod.Any())
+ return null;
+ var idx = sequencePointsInMethod.BinarySearch(instruction, new InstructionByOffsetComparer());
+ Instruction prev;
+ if (idx < 0)
+ {
+ // no exact match, idx corresponds to the next, larger element
+ var lower = Math.Max(~idx - 1, 0);
+ prev = sequencePointsInMethod[lower];
+ }
+ else
+ {
+ // exact match, idx corresponds to the match
+ prev = sequencePointsInMethod[idx];
+ }
+
+ return prev;
+ }
+
+ private static bool HasValidSequencePoint(Instruction instruction, MethodDefinition methodDefinition)
+ {
+ var sp = methodDefinition.DebugInformation.GetSequencePoint(instruction);
+ return sp != null && sp.StartLine != StepOverLineCode;
+ }
+
+ private class InstructionByOffsetComparer : IComparer
+ {
+ public int Compare(Instruction x, Instruction y)
+ {
+ return x.Offset.CompareTo(y.Offset);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj
index b0440d373..f14b73dd3 100644
--- a/src/coverlet.core/coverlet.core.csproj
+++ b/src/coverlet.core/coverlet.core.csproj
@@ -2,7 +2,7 @@
netstandard2.0
- 1.2.0
+ 3.0.0
@@ -12,4 +12,8 @@
+
+
+
+
diff --git a/src/coverlet.core/coverlet.core.snk b/src/coverlet.core/coverlet.core.snk
new file mode 100644
index 000000000..65e3bf493
Binary files /dev/null and b/src/coverlet.core/coverlet.core.snk differ
diff --git a/src/coverlet.msbuild.tasks/CoverageResultTask.cs b/src/coverlet.msbuild.tasks/CoverageResultTask.cs
index e201d7433..e574cb3fa 100644
--- a/src/coverlet.msbuild.tasks/CoverageResultTask.cs
+++ b/src/coverlet.msbuild.tasks/CoverageResultTask.cs
@@ -1,10 +1,10 @@
using System;
using System.IO;
+using System.Linq;
+using System.Text;
using ConsoleTables;
-
using Coverlet.Core;
using Coverlet.Core.Reporters;
-
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -12,15 +12,16 @@ namespace Coverlet.MSbuild.Tasks
{
public class CoverageResultTask : Task
{
- private string _filename;
+ private string _output;
private string _format;
private int _threshold;
+ private string _thresholdType;
[Required]
public string Output
{
- get { return _filename; }
- set { _filename = value; }
+ get { return _output; }
+ set { _output = value; }
}
[Required]
@@ -37,43 +38,82 @@ public int Threshold
set { _threshold = value; }
}
+ [Required]
+ public string ThresholdType
+ {
+ get { return _thresholdType; }
+ set { _thresholdType = value; }
+ }
+
public override bool Execute()
{
try
{
Console.WriteLine("\nCalculating coverage result...");
+
var coverage = InstrumentationTask.Coverage;
- CoverageResult result = coverage.GetCoverageResult();
+ var result = coverage.GetCoverageResult();
- var directory = Path.GetDirectoryName(_filename);
+ var directory = Path.GetDirectoryName(_output);
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);
- IReporter reporter = new ReporterFactory(_format).CreateReporter();
- if (reporter == null)
- throw new Exception($"Specified output format '{_format}' is not supported");
+ var formats = _format.Split(',');
+ foreach (var format in formats)
+ {
+ var reporter = new ReporterFactory(format).CreateReporter();
+ if (reporter == null)
+ throw new Exception($"Specified output format '{format}' is not supported");
+
+ var filename = Path.GetFileName(_output);
+ filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename;
- _filename = _filename + "." + reporter.Extension;
- Console.WriteLine($" Generating report '{_filename}'");
- File.WriteAllText(_filename, reporter.Report(result));
+ var report = Path.Combine(directory, filename);
+ Console.WriteLine($" Generating report '{report}'");
+ File.WriteAllText(report, reporter.Report(result));
+ }
- double total = 0;
- CoverageSummary summary = new CoverageSummary();
- ConsoleTable table = new ConsoleTable("Module", "Coverage");
+ var thresholdFailed = false;
+ var thresholdTypes = _thresholdType.Split(',').Select(t => t.Trim());
+ var summary = new CoverageSummary();
+ var exceptionBuilder = new StringBuilder();
+ var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method");
foreach (var module in result.Modules)
{
- double percent = summary.CalculateLineCoverage(module.Value) * 100;
- table.AddRow(System.IO.Path.GetFileNameWithoutExtension(module.Key), $"{percent}%");
- total += percent;
+ var linePercent = summary.CalculateLineCoverage(module.Value).Percent * 100;
+ var branchPercent = summary.CalculateBranchCoverage(module.Value).Percent * 100;
+ var methodPercent = summary.CalculateMethodCoverage(module.Value).Percent * 100;
+
+ coverageTable.AddRow(Path.GetFileNameWithoutExtension(module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%");
+
+ if (_threshold > 0)
+ {
+ if (linePercent < _threshold && thresholdTypes.Contains("line"))
+ {
+ exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(module.Key)}' has a line coverage '{linePercent}%' below specified threshold '{_threshold}%'");
+ thresholdFailed = true;
+ }
+
+ if (branchPercent < _threshold && thresholdTypes.Contains("branch"))
+ {
+ exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(module.Key)}' has a branch coverage '{branchPercent}%' below specified threshold '{_threshold}%'");
+ thresholdFailed = true;
+ }
+
+ if (methodPercent < _threshold && thresholdTypes.Contains("method"))
+ {
+ exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(module.Key)}' has a method coverage '{methodPercent}%' below specified threshold '{_threshold}%'");
+ thresholdFailed = true;
+ }
+ }
}
Console.WriteLine();
- Console.WriteLine(table.ToMarkDownString());
+ Console.WriteLine(coverageTable.ToStringAlternative());
- double average = total / result.Modules.Count;
- if (average < _threshold)
- throw new Exception($"Overall average coverage '{average}%' is lower than specified threshold '{_threshold}%'");
+ if (thresholdFailed)
+ throw new Exception(exceptionBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray()));
}
catch (Exception ex)
{
diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs
index 2ee289fae..0ddbc2a26 100644
--- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs
+++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs
@@ -1,34 +1,58 @@
-using System;
-using Coverlet.Core;
-using Microsoft.Build.Framework;
+using System;
+using Coverlet.Core;
+using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-namespace Coverlet.MSbuild.Tasks
-{
- public class InstrumentationTask : Task
- {
- internal static Coverage Coverage { get; private set; }
-
- [Required]
- public string Path { get; set; }
-
- public string Exclude { get; set; }
-
- public override bool Execute()
- {
- try
- {
- var excludeRules = Exclude?.Split(',');
- Coverage = new Coverage(Path, Guid.NewGuid().ToString(), excludeRules);
- Coverage.PrepareModules();
- }
- catch(Exception ex)
- {
- Log.LogErrorFromException(ex);
- return false;
- }
-
- return true;
- }
- }
-}
+namespace Coverlet.MSbuild.Tasks
+{
+ public class InstrumentationTask : Task
+ {
+ private static Coverage _coverage;
+ private string _path;
+ private string _exclude;
+ private string _excludeByFile;
+
+ internal static Coverage Coverage
+ {
+ get { return _coverage; }
+ }
+
+ [Required]
+ public string Path
+ {
+ get { return _path; }
+ set { _path = value; }
+ }
+
+ public string Exclude
+ {
+ get { return _exclude; }
+ set { _exclude = value; }
+ }
+
+ public string ExcludeByFile
+ {
+ get { return _excludeByFile; }
+ set { _excludeByFile = value; }
+ }
+
+ public override bool Execute()
+ {
+ try
+ {
+ var excludes = _excludeByFile?.Split(',');
+ var filters = _exclude?.Split(',');
+
+ _coverage = new Coverage(_path, filters, excludes);
+ _coverage.PrepareModules();
+ }
+ catch (Exception ex)
+ {
+ Log.LogErrorFromException(ex);
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/coverlet.msbuild.tasks/Properties/AssemblyInfo.cs b/src/coverlet.msbuild.tasks/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..240956ae9
--- /dev/null
+++ b/src/coverlet.msbuild.tasks/Properties/AssemblyInfo.cs
@@ -0,0 +1 @@
+[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.msbuild.tasks.snk")]
\ No newline at end of file
diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
index 6806737b6..c394fc7fc 100644
--- a/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
+++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
@@ -2,7 +2,7 @@
netstandard2.0
- 1.2.0
+ 2.0.2
diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.snk b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.snk
new file mode 100644
index 000000000..3591ab8d5
Binary files /dev/null and b/src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.snk differ
diff --git a/src/coverlet.msbuild/coverlet.msbuild.props b/src/coverlet.msbuild/coverlet.msbuild.props
index 6f7c3a11b..3dcb69948 100644
--- a/src/coverlet.msbuild/coverlet.msbuild.props
+++ b/src/coverlet.msbuild/coverlet.msbuild.props
@@ -1,15 +1,11 @@
-
-
+ false
json
- $(MSBuildProjectDirectory)
- coverage
- $([MSBuild]::EnsureTrailingSlash('$(CoverletOutputDirectory)'))$(CoverletOutputName)
+ $([MSBuild]::EnsureTrailingSlash('$(MSBuildProjectDirectory)'))
+
0
- false
-
+ line,branch,method
-
-
\ No newline at end of file
+
diff --git a/src/coverlet.msbuild/coverlet.msbuild.targets b/src/coverlet.msbuild/coverlet.msbuild.targets
index 5174fb836..e77b7a0c8 100644
--- a/src/coverlet.msbuild/coverlet.msbuild.targets
+++ b/src/coverlet.msbuild/coverlet.msbuild.targets
@@ -7,6 +7,7 @@
@@ -14,6 +15,7 @@
@@ -22,7 +24,8 @@
Condition="$(CollectCoverage) == 'true'"
Output="$(CoverletOutput)"
OutputFormat="$(CoverletOutputFormat)"
- Threshold="$(Threshold)" />
+ Threshold="$(Threshold)"
+ ThresholdType="$(ThresholdType)" />
diff --git a/src/coverlet.tracker/CoverageTracker.cs b/src/coverlet.tracker/CoverageTracker.cs
new file mode 100644
index 000000000..ce5960808
--- /dev/null
+++ b/src/coverlet.tracker/CoverageTracker.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+
+namespace Coverlet.Tracker
+{
+ public static class CoverageTracker
+ {
+ private static Dictionary> _events;
+
+ [ExcludeFromCodeCoverage]
+ static CoverageTracker()
+ {
+ _events = new Dictionary>();
+ AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
+ AppDomain.CurrentDomain.DomainUnload += new EventHandler(CurrentDomain_ProcessExit);
+ }
+
+ [ExcludeFromCodeCoverage]
+ public static void MarkExecuted(string file, string evt)
+ {
+ lock (_events)
+ {
+ if (!_events.TryGetValue(file, out var fileEvents))
+ {
+ fileEvents = new Dictionary();
+ _events.Add(file, fileEvents);
+ }
+
+ if (!fileEvents.TryGetValue(evt, out var count))
+ {
+ fileEvents.Add(evt, 1);
+ }
+ else if (count < int.MaxValue)
+ {
+ fileEvents[evt] = count + 1;
+ }
+ }
+ }
+
+ [ExcludeFromCodeCoverage]
+ public static void CurrentDomain_ProcessExit(object sender, EventArgs e)
+ {
+ lock (_events)
+ {
+ foreach (var files in _events)
+ {
+ using (var fs = new FileStream(files.Key, FileMode.Create))
+ using (var sw = new StreamWriter(fs))
+ {
+ foreach (var evt in files.Value)
+ {
+ sw.WriteLine($"{evt.Key},{evt.Value}");
+ }
+ }
+ }
+
+ _events.Clear();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/coverlet.core/Extensions/DictionaryExtensions.cs b/src/coverlet.tracker/Extensions/DictionaryExtensions.cs
similarity index 75%
rename from src/coverlet.core/Extensions/DictionaryExtensions.cs
rename to src/coverlet.tracker/Extensions/DictionaryExtensions.cs
index be1bee0df..2b7164025 100644
--- a/src/coverlet.core/Extensions/DictionaryExtensions.cs
+++ b/src/coverlet.tracker/Extensions/DictionaryExtensions.cs
@@ -1,11 +1,11 @@
using System.Collections.Generic;
-using Coverlet.Core.Attributes;
+using System.Diagnostics.CodeAnalysis;
-namespace Coverlet.Core.Extensions
+namespace Coverlet.Tracker.Extensions
{
internal static class DictionaryExtensions
{
- [ExcludeFromCoverage]
+ [ExcludeFromCodeCoverage]
public static bool TryAdd(this Dictionary dictionary, T key, U value)
{
if (dictionary.ContainsKey(key))
diff --git a/src/coverlet.tracker/Properties/AssemblyInfo.cs b/src/coverlet.tracker/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..9495ca62a
--- /dev/null
+++ b/src/coverlet.tracker/Properties/AssemblyInfo.cs
@@ -0,0 +1 @@
+[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.tracker.snk")]
\ No newline at end of file
diff --git a/src/coverlet.tracker/coverlet.tracker.csproj b/src/coverlet.tracker/coverlet.tracker.csproj
new file mode 100644
index 000000000..9f5c4f4ab
--- /dev/null
+++ b/src/coverlet.tracker/coverlet.tracker.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netstandard2.0
+
+
+
diff --git a/src/coverlet.tracker/coverlet.tracker.snk b/src/coverlet.tracker/coverlet.tracker.snk
new file mode 100644
index 000000000..172a9e4b1
Binary files /dev/null and b/src/coverlet.tracker/coverlet.tracker.snk differ
diff --git a/test/coverlet.core.performancetest/PerformanceTest.cs b/test/coverlet.core.performancetest/PerformanceTest.cs
new file mode 100644
index 000000000..849c14a8a
--- /dev/null
+++ b/test/coverlet.core.performancetest/PerformanceTest.cs
@@ -0,0 +1,27 @@
+using coverlet.testsubject;
+using Xunit;
+
+namespace coverlet.core.performancetest
+{
+ ///
+ /// Test the performance of coverlet by running a unit test that calls a reasonably big and complex test class.
+ /// Enable the test, compile, then run the test in the command line:
+ ///
+ /// dotnet test -p:CollectCoverage=true -p:CoverletOutputFormat=opencover test/coverlet.core.performa ncetest/
+ ///
+ ///
+ public class PerformanceTest
+ {
+ [Theory(Skip = "Only enabled when explicitly testing performance.")]
+ [InlineData(150)]
+ public void TestPerformance(int iterations)
+ {
+ var big = new BigClass();
+
+ for (var i = 0; i < iterations; i++)
+ {
+ big.Do(i);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj b/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj
new file mode 100644
index 000000000..9e471b9bf
--- /dev/null
+++ b/test/coverlet.core.performancetest/coverlet.core.performancetest.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+ netcoreapp2.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/coverlet.core.tests/CoverageSummaryTests.cs b/test/coverlet.core.tests/CoverageSummaryTests.cs
index 7e1f29369..04cd2e78f 100644
--- a/test/coverlet.core.tests/CoverageSummaryTests.cs
+++ b/test/coverlet.core.tests/CoverageSummaryTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Coverlet.Core;
@@ -14,11 +15,17 @@ public class CoverageSummaryTests
public CoverageSummaryTests()
{
Lines lines = new Lines();
- lines.Add(1, new LineInfo { Hits = 1, IsBranchPoint = true });
- lines.Add(2, new LineInfo { Hits = 0 });
+ lines.Add(1, 1);
+ lines.Add(2, 0);
+ Branches branches = new Branches();
+ branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 });
+ branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 });
Methods methods = new Methods();
- methods.Add("System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()", lines);
+ var methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()";
+ methods.Add(methodString, new Method());
+ methods[methodString].Lines = lines;
+ methods[methodString].Branches = branches;
Classes classes = new Classes();
classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods);
@@ -40,10 +47,10 @@ public void TestCalculateLineCoverage()
var @class = document.Value.First();
var method = @class.Value.First();
- Assert.Equal(0.5, summary.CalculateLineCoverage(module.Value));
- Assert.Equal(0.5, summary.CalculateLineCoverage(document.Value));
- Assert.Equal(0.5, summary.CalculateLineCoverage(@class.Value));
- Assert.Equal(0.5, summary.CalculateLineCoverage(method.Value));
+ Assert.Equal(0.5, summary.CalculateLineCoverage(module.Value).Percent);
+ Assert.Equal(0.5, summary.CalculateLineCoverage(document.Value).Percent);
+ Assert.Equal(0.5, summary.CalculateLineCoverage(@class.Value).Percent);
+ Assert.Equal(0.5, summary.CalculateLineCoverage(method.Value.Lines).Percent);
}
[Fact]
@@ -56,10 +63,10 @@ public void TestCalculateBranchCoverage()
var @class = document.Value.First();
var method = @class.Value.First();
- Assert.Equal(1, summary.CalculateBranchCoverage(module.Value));
- Assert.Equal(1, summary.CalculateBranchCoverage(document.Value));
- Assert.Equal(1, summary.CalculateBranchCoverage(@class.Value));
- Assert.Equal(1, summary.CalculateBranchCoverage(method.Value));
+ Assert.Equal(1, summary.CalculateBranchCoverage(module.Value).Percent);
+ Assert.Equal(1, summary.CalculateBranchCoverage(document.Value).Percent);
+ Assert.Equal(1, summary.CalculateBranchCoverage(@class.Value).Percent);
+ Assert.Equal(1, summary.CalculateBranchCoverage(method.Value.Branches).Percent);
}
[Fact]
@@ -72,10 +79,10 @@ public void TestCalculateMethodCoverage()
var @class = document.Value.First();
var method = @class.Value.First();
- Assert.Equal(1, summary.CalculateMethodCoverage(module.Value));
- Assert.Equal(1, summary.CalculateMethodCoverage(document.Value));
- Assert.Equal(1, summary.CalculateMethodCoverage(@class.Value));
- Assert.Equal(1, summary.CalculateMethodCoverage(method.Value));
+ Assert.Equal(1, summary.CalculateMethodCoverage(module.Value).Percent);
+ Assert.Equal(1, summary.CalculateMethodCoverage(document.Value).Percent);
+ Assert.Equal(1, summary.CalculateMethodCoverage(@class.Value).Percent);
+ Assert.Equal(1, summary.CalculateMethodCoverage(method.Value.Lines).Percent);
}
}
}
\ No newline at end of file
diff --git a/test/coverlet.core.tests/CoverageTests.cs b/test/coverlet.core.tests/CoverageTests.cs
index e895a47ee..52578a50e 100644
--- a/test/coverlet.core.tests/CoverageTests.cs
+++ b/test/coverlet.core.tests/CoverageTests.cs
@@ -5,6 +5,7 @@
using Moq;
using Coverlet.Core;
+using System.Collections.Generic;
namespace Coverlet.Core.Tests
{
@@ -13,19 +14,25 @@ public class CoverageTests
[Fact]
public void TestCoverage()
{
- string module = typeof(CoverageTests).Assembly.Location;
- string identifier = Guid.NewGuid().ToString();
+ string module = GetType().Assembly.Location;
+ string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
- var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), identifier));
- var tempModule = Path.Combine(directory.FullName, Path.GetFileName(module));
+ var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
- File.Copy(module, tempModule, true);
+ File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true);
+ File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true);
- var coverage = new Coverage(tempModule, identifier);
+ // TODO: Find a way to mimick hits
+
+ // Since Coverage only instruments dependancies, we need a fake module here
+ var testModule = Path.Combine(directory.FullName, "test.module.dll");
+
+ var coverage = new Coverage(testModule, Array.Empty(), Array.Empty());
coverage.PrepareModules();
var result = coverage.GetCoverageResult();
- Assert.Empty(result.Modules);
+
+ Assert.NotEmpty(result.Modules);
directory.Delete(true);
}
diff --git a/test/coverlet.core.tests/Extensions/DictionaryExtensionsTests.cs b/test/coverlet.core.tests/Extensions/DictionaryExtensionsTests.cs
deleted file mode 100644
index 9f22fbb18..000000000
--- a/test/coverlet.core.tests/Extensions/DictionaryExtensionsTests.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-
-using Coverlet.Core.Extensions;
-using Xunit;
-
-namespace Coverlet.Core.Tests.Extensions
-{
- public class DictionaryExtensionsTests
- {
- [Fact]
- public void TestTryAdd()
- {
- var dictionary = new Dictionary();
- Assert.True(DictionaryExtensions.TryAdd(dictionary, "a", "b"));
- Assert.Equal("b", dictionary["a"]);
- Assert.False(DictionaryExtensions.TryAdd(dictionary, "a", "c"));
- }
- }
-}
\ No newline at end of file
diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
index 9e10cb52b..52ade8079 100644
--- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
+++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-
using Xunit;
using System.Collections.Generic;
using System.Linq;
@@ -8,12 +7,12 @@
namespace Coverlet.Core.Helpers.Tests
{
public class InstrumentationHelperTests
- {
+ {
[Fact]
public void TestGetDependencies()
{
string module = typeof(InstrumentationHelperTests).Assembly.Location;
- var modules = InstrumentationHelper.GetDependencies(module);
+ var modules = InstrumentationHelper.GetCoverableModules(module);
Assert.False(Array.Exists(modules, m => m == module));
}
@@ -46,10 +45,25 @@ public void TestCopyCoverletDependency()
var directory = Directory.CreateDirectory(Path.Combine(tempPath, "tempdir"));
InstrumentationHelper.CopyCoverletDependency(Path.Combine(directory.FullName, "somemodule.dll"));
- Assert.True(File.Exists(Path.Combine(directory.FullName, "coverlet.core.dll")));
+ Assert.True(File.Exists(Path.Combine(directory.FullName, "coverlet.tracker.dll")));
Directory.Delete(directory.FullName, true);
}
+ [Fact]
+ public void TestIsValidFilterExpression()
+ {
+ Assert.True(InstrumentationHelper.IsValidFilterExpression("[*]*"));
+ Assert.True(InstrumentationHelper.IsValidFilterExpression("[*]*core"));
+ Assert.True(InstrumentationHelper.IsValidFilterExpression("[assembly]*"));
+ Assert.True(InstrumentationHelper.IsValidFilterExpression("[*]type"));
+ Assert.True(InstrumentationHelper.IsValidFilterExpression("[assembly]type"));
+ Assert.False(InstrumentationHelper.IsValidFilterExpression("[*]"));
+ Assert.False(InstrumentationHelper.IsValidFilterExpression("[-]*"));
+ Assert.False(InstrumentationHelper.IsValidFilterExpression("*"));
+ Assert.False(InstrumentationHelper.IsValidFilterExpression("]["));
+ Assert.False(InstrumentationHelper.IsValidFilterExpression(null));
+ }
+
[Fact]
public void TestDontCopyCoverletDependency()
{
@@ -61,16 +75,6 @@ public void TestDontCopyCoverletDependency()
Directory.Delete(directory.FullName, true);
}
- [Fact]
- public void TestReadHitsFile()
- {
- var tempFile = Path.GetTempFileName();
- Assert.True(File.Exists(tempFile));
-
- var lines = InstrumentationHelper.ReadHitsFile(tempFile);
- Assert.NotNull(lines);
- }
-
[Fact]
public void TestDeleteHitsFile()
{
@@ -80,62 +84,162 @@ public void TestDeleteHitsFile()
InstrumentationHelper.DeleteHitsFile(tempFile);
Assert.False(File.Exists(tempFile));
}
-
-
- public static IEnumerable