Skip to content

Commit 1dd3b70

Browse files
committed
add preliminary support for excluding assemblies from coverage
1 parent 002c0a6 commit 1dd3b70

File tree

9 files changed

+168
-66
lines changed

9 files changed

+168
-66
lines changed

src/coverlet.core/Coverage.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,30 @@ public class Coverage
1313
{
1414
private string _module;
1515
private string _identifier;
16-
private IEnumerable<string> _excludeRules;
16+
private string[] _filters;
17+
private string[] _excludes;
1718
private List<InstrumenterResult> _results;
1819

19-
public Coverage(string module, string identifier, IEnumerable<string> excludeRules = null)
20+
public Coverage(string module, string identifier, string[] filters, string[] excludes)
2021
{
2122
_module = module;
2223
_identifier = identifier;
23-
_excludeRules = excludeRules;
24+
_filters = filters;
25+
_excludes = excludes;
2426
_results = new List<InstrumenterResult>();
2527
}
2628

2729
public void PrepareModules()
2830
{
29-
string[] modules = InstrumentationHelper.GetDependencies(_module);
30-
var excludedFiles = InstrumentationHelper.GetExcludedFiles(_excludeRules);
31+
string[] modules = InstrumentationHelper.GetCoverableModules(_module);
32+
string[] excludedFiles = InstrumentationHelper.GetExcludedFiles(_excludes);
33+
3134
foreach (var module in modules)
3235
{
33-
var instrumenter = new Instrumenter(module, _identifier, excludedFiles);
36+
if (InstrumentationHelper.IsModuleExcluded(module, _filters))
37+
continue;
38+
39+
var instrumenter = new Instrumenter(module, _identifier, _filters, excludedFiles);
3440
if (instrumenter.CanInstrument())
3541
{
3642
InstrumentationHelper.BackupOriginalModule(module, _identifier);

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Reflection;
66
using System.Reflection.PortableExecutable;
7+
using System.Text.RegularExpressions;
78

89
using Microsoft.Extensions.FileSystemGlobbing;
910
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
@@ -12,10 +13,10 @@ namespace Coverlet.Core.Helpers
1213
{
1314
internal static class InstrumentationHelper
1415
{
15-
public static string[] GetDependencies(string module)
16+
public static string[] GetCoverableModules(string module)
1617
{
1718
IEnumerable<string> modules = Directory.GetFiles(Path.GetDirectoryName(module), "*.dll");
18-
modules = modules.Where(a => IsAssembly(a) && Path.GetFileName(a) != Path.GetFileName(module));
19+
modules = modules.Where(m => IsAssembly(m) && Path.GetFileName(m) != Path.GetFileName(module));
1920
return modules.ToArray();
2021
}
2122

@@ -65,7 +66,8 @@ public static void RestoreOriginalModule(string module, string identifier)
6566
// See: https://github.com/tonerdo/coverlet/issues/25
6667
var retryStrategy = CreateRetryStrategy();
6768

68-
RetryHelper.Retry(() => {
69+
RetryHelper.Retry(() =>
70+
{
6971
File.Copy(backupPath, module, true);
7072
File.Delete(backupPath);
7173
}, retryStrategy, 10);
@@ -76,34 +78,81 @@ public static void DeleteHitsFile(string path)
7678
// Retry hitting the hits file - retry up to 10 times, since the file could be locked
7779
// See: https://github.com/tonerdo/coverlet/issues/25
7880
var retryStrategy = CreateRetryStrategy();
79-
8081
RetryHelper.Retry(() => File.Delete(path), retryStrategy, 10);
8182
}
8283

83-
public static IEnumerable<string> GetExcludedFiles(IEnumerable<string> excludeRules,
84-
string parentDir = null)
84+
public static bool IsModuleExcluded(string module, string[] filters)
85+
{
86+
if (filters == null || !filters.Any())
87+
return false;
88+
89+
module = Path.GetFileNameWithoutExtension(module);
90+
bool isMatch = false;
91+
92+
foreach (var filter in filters)
93+
{
94+
if (!IsValidFilterExpression(filter))
95+
continue;
96+
97+
string pattern = filter.Substring(1, filter.IndexOf(']') - 1);
98+
pattern = WildcardToRegex(pattern);
99+
100+
var regex = new Regex(pattern);
101+
isMatch = regex.IsMatch(module);
102+
}
103+
104+
return isMatch;
105+
}
106+
107+
public static bool IsTypeExcluded(string type, string[] filters)
108+
{
109+
if (filters == null || !filters.Any())
110+
return false;
111+
112+
bool isMatch = false;
113+
114+
foreach (var filter in filters)
115+
{
116+
if (!IsValidFilterExpression(filter))
117+
continue;
118+
119+
string pattern = filter.Substring(filter.IndexOf(']') + 1);
120+
pattern = WildcardToRegex(pattern);
121+
122+
var regex = new Regex(pattern);
123+
isMatch = regex.IsMatch(type);
124+
}
125+
126+
return isMatch;
127+
}
128+
129+
public static string[] GetExcludedFiles(string[] excludes)
85130
{
86131
const string RELATIVE_KEY = nameof(RELATIVE_KEY);
87-
parentDir = string.IsNullOrWhiteSpace(parentDir)? Directory.GetCurrentDirectory() : parentDir;
132+
string parentDir = Directory.GetCurrentDirectory();
88133

89-
if (excludeRules == null || !excludeRules.Any()) return Enumerable.Empty<string>();
134+
if (excludes == null || !excludes.Any()) return Array.Empty<string>();
90135

91-
var matcherDict = new Dictionary<string, Matcher>(){ {RELATIVE_KEY, new Matcher()}};
92-
foreach (var excludeRule in excludeRules)
136+
var matcherDict = new Dictionary<string, Matcher>() { { RELATIVE_KEY, new Matcher() } };
137+
foreach (var excludeRule in excludes)
93138
{
94-
if (Path.IsPathRooted(excludeRule)) {
139+
if (Path.IsPathRooted(excludeRule))
140+
{
95141
var root = Path.GetPathRoot(excludeRule);
96-
if (!matcherDict.ContainsKey(root)) {
142+
if (!matcherDict.ContainsKey(root))
143+
{
97144
matcherDict.Add(root, new Matcher());
98145
}
99146
matcherDict[root].AddInclude(excludeRule.Substring(root.Length));
100-
} else {
147+
}
148+
else
149+
{
101150
matcherDict[RELATIVE_KEY].AddInclude(excludeRule);
102151
}
103152
}
104153

105154
var files = new List<string>();
106-
foreach(var entry in matcherDict)
155+
foreach (var entry in matcherDict)
107156
{
108157
var root = entry.Key;
109158
var matcher = entry.Value;
@@ -114,20 +163,7 @@ public static IEnumerable<string> GetExcludedFiles(IEnumerable<string> excludeRu
114163
files.AddRange(currentFiles);
115164
}
116165

117-
return files.Distinct();
118-
}
119-
120-
private static bool IsAssembly(string filePath)
121-
{
122-
try
123-
{
124-
AssemblyName.GetAssemblyName(filePath);
125-
return true;
126-
}
127-
catch
128-
{
129-
return false;
130-
}
166+
return files.Distinct().ToArray();
131167
}
132168

133169
private static string GetBackupPath(string module, string identifier)
@@ -149,6 +185,52 @@ TimeSpan retryStrategy()
149185

150186
return retryStrategy;
151187
}
188+
189+
private static bool IsValidFilterExpression(string filter)
190+
{
191+
if (!filter.StartsWith("["))
192+
return false;
193+
194+
if (!filter.Contains("]"))
195+
return false;
196+
197+
if (filter.Count(f => f == '[') > 1)
198+
return false;
199+
200+
if (filter.Count(f => f == ']') > 1)
201+
return false;
202+
203+
if (filter.IndexOf(']') < filter.IndexOf('['))
204+
return false;
205+
206+
if (filter.IndexOf(']') - filter.IndexOf('[') == 1)
207+
return false;
208+
209+
if (new Regex(@"[^\w*]").IsMatch(filter.Replace("[", "").Replace("]", "")))
210+
return false;
211+
212+
return true;
213+
}
214+
215+
private static string WildcardToRegex(string pattern)
216+
{
217+
return "^" + Regex.Escape(pattern).
218+
Replace("\\*", ".*").
219+
Replace("\\?", ".") + "$";
220+
}
221+
222+
private static bool IsAssembly(string filePath)
223+
{
224+
try
225+
{
226+
AssemblyName.GetAssemblyName(filePath);
227+
return true;
228+
}
229+
catch
230+
{
231+
return false;
232+
}
233+
}
152234
}
153235
}
154236

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ internal class Instrumenter
1818
{
1919
private readonly string _module;
2020
private readonly string _identifier;
21-
private readonly IEnumerable<string> _excludedFiles;
21+
private readonly string[] _filters;
22+
private readonly string[] _excludedFiles;
2223
private readonly static Lazy<MethodInfo> _markExecutedMethodLoader = new Lazy<MethodInfo>(GetMarkExecutedMethod);
2324
private InstrumenterResult _result;
2425

25-
public Instrumenter(string module, string identifier, IEnumerable<string> excludedFiles = null)
26+
public Instrumenter(string module, string identifier, string[] filters, string[] excludedFiles)
2627
{
2728
_module = module;
2829
_identifier = identifier;
29-
_excludedFiles = excludedFiles ?? Enumerable.Empty<string>();
30+
_filters = filters;
31+
_excludedFiles = excludedFiles ?? Array.Empty<string>();
3032
}
3133

3234
public bool CanInstrument() => InstrumentationHelper.HasPdb(_module);
@@ -72,7 +74,8 @@ private void InstrumentModule()
7274

7375
private void InstrumentType(TypeDefinition type)
7476
{
75-
if (type.CustomAttributes.Any(IsExcludeAttribute))
77+
if (type.CustomAttributes.Any(IsExcludeAttribute)
78+
&& InstrumentationHelper.IsTypeExcluded(type.FullName, _filters))
7679
return;
7780

7881
foreach (var method in type.Methods)

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class InstrumentationTask : Task
99
{
1010
private static Coverage _coverage;
1111
private string _path;
12+
private string _filter;
1213
private string _exclude;
1314

1415
internal static Coverage Coverage
@@ -22,19 +23,27 @@ public string Path
2223
get { return _path; }
2324
set { _path = value; }
2425
}
26+
27+
public string Filter
28+
{
29+
get { return _filter; }
30+
set { _filter = value; }
31+
}
2532

2633
public string Exclude
2734
{
28-
get { return _path; }
29-
set { _path = value; }
35+
get { return _exclude; }
36+
set { _exclude = value; }
3037
}
3138

3239
public override bool Execute()
3340
{
3441
try
3542
{
36-
var excludeRules = _exclude?.Split(',');
37-
_coverage = new Coverage(_path, Guid.NewGuid().ToString(), excludeRules);
43+
var excludes = _exclude?.Split(',');
44+
var filters = _filter?.Split(',');
45+
46+
_coverage = new Coverage(_path, Guid.NewGuid().ToString(), filters, excludes);
3847
_coverage.PrepareModules();
3948
}
4049
catch (Exception ex)

src/coverlet.msbuild/coverlet.msbuild.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<CoverletOutputDirectory Condition="$(CoverletOutputDirectory) == ''">$(MSBuildProjectDirectory)</CoverletOutputDirectory>
66
<CoverletOutputName Condition=" '$(CoverletOutputName)' == '' ">coverage</CoverletOutputName>
77
<CoverletOutput>$([MSBuild]::EnsureTrailingSlash('$(CoverletOutputDirectory)'))$(CoverletOutputName)</CoverletOutput>
8+
<Filter Condition="$(Filter) == ''"></Filter>
89
<Exclude Condition="$(Exclude) == ''"></Exclude>
910
<Threshold Condition="$(Threshold) == ''">0</Threshold>
1011
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Target Name="InstrumentModulesAfterBuild" AfterTargets="BuildProject">
1414
<Coverlet.MSbuild.Tasks.InstrumentationTask
1515
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
16+
Filter="$(Filter)"
1617
Exclude="$(Exclude)"
1718
Path="$(TargetPath)" />
1819
</Target>

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void TestCoverage()
2828
// Since Coverage only instruments dependancies, we need a fake module here
2929
var testModule = Path.Combine(directory.FullName, "test.module.dll");
3030

31-
var coverage = new Coverage(testModule, identifier);
31+
var coverage = new Coverage(testModule, identifier, Array.Empty<string>(), Array.Empty<string>());
3232
coverage.PrepareModules();
3333

3434
var result = coverage.GetCoverageResult();

0 commit comments

Comments
 (0)