Skip to content

Commit 928f05d

Browse files
authored
Merge pull request #94 from tonerdo/filter-assemblies
Add ability to exclude assemblies by expression
2 parents 2b1e56b + 371112c commit 928f05d

File tree

10 files changed

+245
-103
lines changed

10 files changed

+245
-103
lines changed

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,34 @@ You can ignore a method or an entire class from code coverage by creating and ap
8787
Coverlet just uses the type name, so the attributes can be created under any namespace of your choosing.
8888

8989
#### Source Files
90-
You can also ignore specific source files from code coverage using the `Exclude` property
90+
You can also ignore specific source files from code coverage using the `ExcludeByFile` property
9191
- Use single or multiple paths (separate by comma)
9292
- Use absolute or relative paths (relative to the project directory)
9393
- Use file path or directory path with globbing (e.g `dir1/*.cs`)
9494

9595
```bash
96-
dotnet test /p:CollectCoverage=true /p:Exclude=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
96+
dotnet test /p:CollectCoverage=true /p:ExcludeByFile=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
9797
```
9898

99+
#### Filters
100+
Coverlet gives the ability to have fine grained control over what gets excluded using "filter expressions".
101+
102+
Syntax: `/p:Exclude=[Assembly-Filter]Type-Filter`
103+
104+
Examples
105+
- `/p:Exclude="[*]*"` => Excludes all types in all assemblies (nothing is instrumented)
106+
- `/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`)
107+
- `/p:Exclude="[*]Coverlet.Core.Instrumentation.*"` => Excludes all types belonging to `Coverlet.Core.Instrumentation` namespace in any assembly
108+
109+
```bash
110+
dotnet test /p:CollectCoverage=true /p:Exclude="[coverlet.*]Coverlet.Core.Coverage"
111+
```
112+
113+
You can specify multiple fiter expressions by separting them with a comma (`,`).
114+
99115
## Roadmap
100116

101-
* Filter modules to be instrumented
117+
* Merging outputs (multiple test projects, one coverage result)
102118
* Support for more output formats (e.g. JaCoCo)
103119
* Console runner (removes the need for requiring a NuGet package)
104120

src/coverlet.core/Coverage.cs

Lines changed: 13 additions & 7 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[] _rules;
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[] rules)
2021
{
2122
_module = module;
2223
_identifier = identifier;
23-
_excludeRules = excludeRules;
24+
_filters = filters;
25+
_rules = rules;
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(_rules);
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);
@@ -205,4 +211,4 @@ private void CalculateCoverage()
205211
}
206212
}
207213
}
208-
}
214+
}

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 118 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,86 @@ 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)
87+
return false;
88+
89+
bool isMatch = false;
90+
module = Path.GetFileNameWithoutExtension(module);
91+
92+
foreach (var filter in filters)
93+
{
94+
if (!IsValidFilterExpression(filter))
95+
continue;
96+
97+
string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1);
98+
string typePattern = filter.Substring(filter.IndexOf(']') + 1);
99+
100+
modulePattern = WildcardToRegex(modulePattern);
101+
102+
var regex = new Regex(modulePattern);
103+
isMatch = regex.IsMatch(module) && typePattern == "*";
104+
}
105+
106+
return isMatch;
107+
}
108+
109+
public static bool IsTypeExcluded(string module, string type, string[] filters)
110+
{
111+
if (filters == null)
112+
return false;
113+
114+
bool isMatch = false;
115+
module = Path.GetFileNameWithoutExtension(module);
116+
117+
foreach (var filter in filters)
118+
{
119+
if (!IsValidFilterExpression(filter))
120+
continue;
121+
122+
string typePattern = filter.Substring(filter.IndexOf(']') + 1);
123+
string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1);
124+
125+
typePattern = WildcardToRegex(typePattern);
126+
modulePattern = WildcardToRegex(modulePattern);
127+
128+
isMatch = new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module);
129+
}
130+
131+
return isMatch;
132+
}
133+
134+
public static string[] GetExcludedFiles(string[] rules)
85135
{
86136
const string RELATIVE_KEY = nameof(RELATIVE_KEY);
87-
parentDir = string.IsNullOrWhiteSpace(parentDir)? Directory.GetCurrentDirectory() : parentDir;
137+
string parentDir = Directory.GetCurrentDirectory();
88138

89-
if (excludeRules == null || !excludeRules.Any()) return Enumerable.Empty<string>();
139+
if (rules == null || !rules.Any()) return Array.Empty<string>();
90140

91-
var matcherDict = new Dictionary<string, Matcher>(){ {RELATIVE_KEY, new Matcher()}};
92-
foreach (var excludeRule in excludeRules)
141+
var matcherDict = new Dictionary<string, Matcher>() { { RELATIVE_KEY, new Matcher() } };
142+
foreach (var excludeRule in rules)
93143
{
94-
if (Path.IsPathRooted(excludeRule)) {
144+
if (Path.IsPathRooted(excludeRule))
145+
{
95146
var root = Path.GetPathRoot(excludeRule);
96-
if (!matcherDict.ContainsKey(root)) {
147+
if (!matcherDict.ContainsKey(root))
148+
{
97149
matcherDict.Add(root, new Matcher());
98150
}
99151
matcherDict[root].AddInclude(excludeRule.Substring(root.Length));
100-
} else {
152+
}
153+
else
154+
{
101155
matcherDict[RELATIVE_KEY].AddInclude(excludeRule);
102156
}
103157
}
104158

105159
var files = new List<string>();
106-
foreach(var entry in matcherDict)
160+
foreach (var entry in matcherDict)
107161
{
108162
var root = entry.Key;
109163
var matcher = entry.Value;
@@ -114,20 +168,7 @@ public static IEnumerable<string> GetExcludedFiles(IEnumerable<string> excludeRu
114168
files.AddRange(currentFiles);
115169
}
116170

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-
}
171+
return files.Distinct().ToArray();
131172
}
132173

133174
private static string GetBackupPath(string module, string identifier)
@@ -149,6 +190,55 @@ TimeSpan retryStrategy()
149190

150191
return retryStrategy;
151192
}
193+
194+
private static bool IsValidFilterExpression(string filter)
195+
{
196+
if (!filter.StartsWith("["))
197+
return false;
198+
199+
if (!filter.Contains("]"))
200+
return false;
201+
202+
if (filter.Count(f => f == '[') > 1)
203+
return false;
204+
205+
if (filter.Count(f => f == ']') > 1)
206+
return false;
207+
208+
if (filter.IndexOf(']') < filter.IndexOf('['))
209+
return false;
210+
211+
if (filter.IndexOf(']') - filter.IndexOf('[') == 1)
212+
return false;
213+
214+
if (filter.EndsWith("]"))
215+
return false;
216+
217+
if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("[", "").Replace("]", "")))
218+
return false;
219+
220+
return true;
221+
}
222+
223+
private static string WildcardToRegex(string pattern)
224+
{
225+
return "^" + Regex.Escape(pattern).
226+
Replace("\\*", ".*").
227+
Replace("\\?", ".") + "$";
228+
}
229+
230+
private static bool IsAssembly(string filePath)
231+
{
232+
try
233+
{
234+
AssemblyName.GetAssemblyName(filePath);
235+
return true;
236+
}
237+
catch
238+
{
239+
return false;
240+
}
241+
}
152242
}
153243
}
154244

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(_module, type.FullName, _filters))
7679
return;
7780

7881
foreach (var method in type.Methods)

0 commit comments

Comments
 (0)