Skip to content

Commit b3d108a

Browse files
authored
Merge pull request #64 from ido-namely/exclude-files-by-path
Implementation exclusion of files according to source file paths
2 parents 7355d9f + 23dca2a commit b3d108a

File tree

11 files changed

+151
-33
lines changed

11 files changed

+151
-33
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,24 @@ The above command will automatically fail the build if the average code coverage
7878

7979
### Excluding From Coverage
8080

81+
#### Attributes
8182
You can ignore a method or an entire class from code coverage by creating and applying any of the following attributes:
8283

8384
* ExcludeFromCoverage
8485
* ExcludeFromCoverageAttribute
8586

8687
Coverlet just uses the type name, so the attributes can be created under any namespace of your choosing.
8788

89+
#### File Path
90+
You can also ignore specific source files from code coverage using the `Exclude` property
91+
- Use single or multiple paths (separate by comma)
92+
- Use absolute or relative paths (relative to the project directory)
93+
- Use file path or directory path with globbing (e.g `dir1/*.cs`)
94+
95+
```bash
96+
dotnet test /p:CollectCoverage=true /p:Exclude=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
97+
```
98+
8899
## Roadmap
89100

90101
* Filter modules to be instrumented

build.proj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@
2424
</Target>
2525

2626
<Target Name="RunTests" AfterTargets="CopyMSBuildScripts">
27-
<Exec Command="dotnet test $(MSBuildThisFileDirectory)test\coverlet.core.tests\coverlet.core.tests.csproj -c $(Configuration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover" />
27+
<Exec Command="dotnet test $(MSBuildThisFileDirectory)test\coverlet.core.tests\coverlet.core.tests.csproj -c $(Configuration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover"/>
28+
</Target>
29+
30+
<Target Name="RunTestsWithExclude" AfterTargets="RunTests">
31+
<Exec Command="dotnet test --no-build $(MSBuildThisFileDirectory)test\coverlet.core.tests\coverlet.core.tests.csproj -c $(Configuration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude=\&quot;../../src/coverlet.core/Instrumentation/**/*.cs,$(MSBuildThisFileDirectory)/src/coverlet.core/Reporters/**/*.cs\&quot;"/>
2832
</Target>
2933

30-
<Target Name="CreateNuGetPackage" AfterTargets="RunTests" Condition="$(Configuration) == 'Release'">
34+
<Target Name="CreateNuGetPackage" AfterTargets="RunTestsWithExclude" Condition="$(Configuration) == 'Release'">
3135
<Exec Command="dotnet pack $(MSBuildThisFileDirectory)src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj -c $(Configuration) -o $(OutputPath) /p:NuspecFile=$(NuspecFile)" />
3236
</Target>
3337

34-
</Project>
38+
</Project>

src/coverlet.core/Coverage.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,24 @@ public class Coverage
1212
{
1313
private string _module;
1414
private string _identifier;
15+
private IEnumerable<string> _excludeRules;
1516
private List<InstrumenterResult> _results;
1617

17-
public Coverage(string module, string identifier)
18+
public Coverage(string module, string identifier, IEnumerable<string> excludeRules = null)
1819
{
1920
_module = module;
2021
_identifier = identifier;
22+
_excludeRules = excludeRules;
2123
_results = new List<InstrumenterResult>();
2224
}
2325

2426
public void PrepareModules()
2527
{
2628
string[] modules = InstrumentationHelper.GetDependencies(_module);
29+
var excludedFiles = InstrumentationHelper.GetExcludedFiles(_excludeRules);
2730
foreach (var module in modules)
2831
{
29-
var instrumenter = new Instrumenter(module, _identifier);
32+
var instrumenter = new Instrumenter(module, _identifier, excludedFiles);
3033
if (instrumenter.CanInstrument())
3134
{
3235
InstrumentationHelper.BackupOriginalModule(module, _identifier);

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
using System.IO;
44
using System.Linq;
55
using System.Reflection.PortableExecutable;
6+
using Microsoft.Extensions.FileSystemGlobbing;
67

78
using Coverlet.Core.Instrumentation;
9+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
810

911
namespace Coverlet.Core.Helpers
1012
{
@@ -107,5 +109,44 @@ public static void DeleteHitsFile(string path)
107109

108110
RetryHelper.Retry(() => File.Delete(path), retryStrategy, 10);
109111
}
112+
113+
public static IEnumerable<string> GetExcludedFiles(IEnumerable<string> excludeRules,
114+
string parentDir = null)
115+
{
116+
const string RELATIVE_KEY = nameof(RELATIVE_KEY);
117+
parentDir = string.IsNullOrWhiteSpace(parentDir)? Directory.GetCurrentDirectory() : parentDir;
118+
119+
if (excludeRules == null || !excludeRules.Any()) return Enumerable.Empty<string>();
120+
121+
var matcherDict = new Dictionary<string, Matcher>(){ {RELATIVE_KEY, new Matcher()}};
122+
foreach (var excludeRule in excludeRules)
123+
{
124+
if (Path.IsPathRooted(excludeRule)) {
125+
var root = Path.GetPathRoot(excludeRule);
126+
if (!matcherDict.ContainsKey(root)) {
127+
matcherDict.Add(root, new Matcher());
128+
}
129+
matcherDict[root].AddInclude(excludeRule.Substring(root.Length));
130+
} else {
131+
matcherDict[RELATIVE_KEY].AddInclude(excludeRule);
132+
}
133+
134+
}
135+
136+
var files = new List<string>();
137+
foreach(var entry in matcherDict)
138+
{
139+
var root = entry.Key;
140+
var matcher = entry.Value;
141+
var directoryInfo = new DirectoryInfo(root.Equals(RELATIVE_KEY) ? parentDir : root);
142+
var fileMatchResult = matcher.Execute(new DirectoryInfoWrapper(directoryInfo));
143+
var currentFiles = fileMatchResult.Files
144+
.Select(f => Path.GetFullPath(Path.Combine(directoryInfo.ToString(), f.Path)));
145+
files.AddRange(currentFiles);
146+
}
147+
148+
return files.Distinct();
149+
}
110150
}
111-
}
151+
}
152+

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66
using Mono.Cecil;
77
using Mono.Cecil.Cil;
88
using Mono.Cecil.Rocks;
9+
using System.Collections.Generic;
910

1011
namespace Coverlet.Core.Instrumentation
1112
{
1213
internal class Instrumenter
1314
{
1415
private readonly string _module;
1516
private readonly string _identifier;
17+
private IEnumerable<string> _excludedFiles;
1618
private InstrumenterResult _result;
1719

18-
public Instrumenter(string module, string identifier)
20+
public Instrumenter(string module, string identifier, IEnumerable<string> excludedFiles = null)
1921
{
2022
_module = module;
2123
_identifier = identifier;
24+
_excludedFiles = excludedFiles ?? Enumerable.Empty<string>();
2225
}
2326

2427
public bool CanInstrument() => InstrumentationHelper.HasPdb(_module);
@@ -59,6 +62,10 @@ private void InstrumentModule()
5962

6063
foreach (var method in type.Methods)
6164
{
65+
var sourceFile = method.DebugInformation.SequencePoints.Select(s => s.Document.Url).FirstOrDefault();
66+
if (!string.IsNullOrEmpty(sourceFile) && _excludedFiles.Contains(sourceFile)) {
67+
continue;
68+
}
6269
if (!method.CustomAttributes.Any(a => a.AttributeType.Name == "ExcludeFromCoverageAttribute" || a.AttributeType.Name == "ExcludeFromCoverage"))
6370
InstrumentMethod(method);
6471
}

src/coverlet.core/coverlet.core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<PackageReference Include="Jil" Version="2.15.4" />
1010
<PackageReference Include="Mono.Cecil" Version="0.10.0" />
1111
<PackageReference Include="System.Reflection.Metadata" Version="1.5.0" />
12+
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="2.0.1" />
1213
</ItemGroup>
1314

1415
</Project>

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
using System;
22
using Coverlet.Core;
3-
43
using Microsoft.Build.Framework;
5-
using Microsoft.Build.Utilities;
6-
4+
using Microsoft.Build.Utilities;
5+
76
namespace Coverlet.MSbuild.Tasks
87
{
98
public class InstrumentationTask : Task
10-
{
11-
private string _path;
12-
private static Coverage _coverage;
13-
14-
internal static Coverage Coverage
15-
{
16-
get { return _coverage; }
17-
private set { _coverage = value; }
18-
}
9+
{
10+
internal static Coverage Coverage { get; private set; }
1911

2012
[Required]
21-
public string Path
22-
{
23-
get { return _path; }
24-
set { _path = value; }
25-
}
26-
13+
public string Path { get; set; }
14+
15+
public string Exclude { get; set; }
16+
2717
public override bool Execute()
2818
{
2919
try
3020
{
31-
_coverage = new Coverage(_path, Guid.NewGuid().ToString());
32-
_coverage.PrepareModules();
21+
var excludeRules = Exclude?.Split(',');
22+
Coverage = new Coverage(Path, Guid.NewGuid().ToString(), excludeRules);
23+
Coverage.PrepareModules();
3324
}
3425
catch(Exception ex)
3526
{

src/coverlet.msbuild/coverlet.msbuild.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<CoverletOutputDirectory Condition="$(CoverletOutputDirectory) == ''">$(MSBuildProjectDirectory)</CoverletOutputDirectory>
77
<CoverletOutputName Condition=" '$(CoverletOutputName)' == '' ">coverage</CoverletOutputName>
88
<CoverletOutput>$([MSBuild]::EnsureTrailingSlash('$(CoverletOutputDirectory)'))$(CoverletOutputName)</CoverletOutput>
9-
9+
<Exclude Condition="$(Exclude) == ''"></Exclude>
1010
<Threshold Condition="$(Threshold) == ''">0</Threshold>
1111
<CollectCoverage Condition="$(CollectCoverage) == ''">false</CollectCoverage>
1212

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
<Target Name="InstrumentModulesNoBuild" BeforeTargets="VSTest">
77
<Coverlet.MSbuild.Tasks.InstrumentationTask
88
Condition="'$(VSTestNoBuild)' == 'true' and $(CollectCoverage) == 'true'"
9+
Exclude="$(Exclude)"
910
Path="$(TargetPath)" />
1011
</Target>
1112

1213
<Target Name="InstrumentModulesAfterBuild" AfterTargets="BuildProject">
1314
<Coverlet.MSbuild.Tasks.InstrumentationTask
1415
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
16+
Exclude="$(Exclude)"
1517
Path="$(TargetPath)" />
1618
</Target>
1719

@@ -23,4 +25,4 @@
2325
Threshold="$(Threshold)" />
2426
</Target>
2527

26-
</Project>
28+
</Project>

test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
using System.IO;
33

44
using Xunit;
5-
using Coverlet.Core.Helpers;
5+
using System.Collections.Generic;
6+
using System.Linq;
67

78
namespace Coverlet.Core.Helpers.Tests
89
{
910
public class InstrumentationHelperTests
10-
{
11+
{
1112
[Fact]
1213
public void TestGetDependencies()
1314
{
@@ -79,5 +80,63 @@ public void TestDeleteHitsFile()
7980
InstrumentationHelper.DeleteHitsFile(tempFile);
8081
Assert.False(File.Exists(tempFile));
8182
}
83+
84+
85+
public static IEnumerable<object[]> GetExcludedFilesReturnsEmptyArgs =>
86+
new[]
87+
{
88+
new object[]{null},
89+
new object[]{new List<string>()},
90+
new object[]{new List<string>(){ Path.GetRandomFileName() }},
91+
new object[]{new List<string>(){Path.GetRandomFileName(),
92+
Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName())}
93+
}
94+
};
95+
96+
[Theory]
97+
[MemberData(nameof(GetExcludedFilesReturnsEmptyArgs))]
98+
public void TestGetExcludedFilesReturnsEmpty(IEnumerable<string> excludedFiles)
99+
{
100+
Assert.False(InstrumentationHelper.GetExcludedFiles(excludedFiles)?.Any());
101+
}
102+
103+
[Fact]
104+
public void TestGetExcludedFilesUsingAbsFile()
105+
{
106+
var file = Path.GetRandomFileName();
107+
File.Create(file).Dispose();
108+
var excludeFiles = InstrumentationHelper.GetExcludedFiles(
109+
new List<string>() {Path.Combine(Directory.GetCurrentDirectory(), file)}
110+
);
111+
File.Delete(file);
112+
Assert.Single(excludeFiles);
113+
}
114+
115+
[Fact]
116+
public void TestGetExcludedFilesUsingGlobbing()
117+
{
118+
var fileExtension = Path.GetRandomFileName();
119+
var paths = new string[]{
120+
$"{Path.GetRandomFileName()}.{fileExtension}",
121+
$"{Path.GetRandomFileName()}.{fileExtension}"
122+
};
123+
124+
foreach (var path in paths)
125+
{
126+
File.Create(path).Dispose();
127+
}
128+
129+
var excludeFiles = InstrumentationHelper.GetExcludedFiles(
130+
new List<string>(){$"*.{fileExtension}"});
131+
132+
foreach (var path in paths)
133+
{
134+
File.Delete(path);
135+
}
136+
137+
Assert.Equal(paths.Length, excludeFiles.Count());
138+
}
82139
}
83-
}
140+
}
141+
142+

0 commit comments

Comments
 (0)