diff --git a/README.md b/README.md index 845499398..f8298c0f9 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,16 @@ Coverlet gives the ability to have fine grained control over what gets excluded 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" diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 7e8171657..5319da9a2 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -107,7 +107,7 @@ public static bool IsValidFilterExpression(string filter) if (filter.EndsWith("]")) return false; - if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("[", "").Replace("]", ""))) + if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", ""))) return false; return true; @@ -118,21 +118,27 @@ public static bool IsModuleExcluded(string module, string[] filters) if (filters == null) return false; - bool isMatch = false; 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); - isMatch = regex.IsMatch(module) && typePattern == "*"; + + if (regex.IsMatch(module)) + return true; } - return isMatch; + return false; } public static bool IsTypeExcluded(string module, string type, string[] filters) @@ -140,8 +146,9 @@ public static bool IsTypeExcluded(string module, string type, string[] filters) if (filters == null) return false; - bool isMatch = false; module = Path.GetFileNameWithoutExtension(module); + if (module == null) + return false; foreach (var filter in filters) { @@ -151,10 +158,11 @@ public static bool IsTypeExcluded(string module, string type, string[] filters) typePattern = WildcardToRegex(typePattern); modulePattern = WildcardToRegex(modulePattern); - isMatch = new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module); + if (new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module)) + return true; } - return isMatch; + return false; } public static string[] GetExcludedFiles(string[] rules) @@ -221,7 +229,7 @@ private static string WildcardToRegex(string pattern) { return "^" + Regex.Escape(pattern). Replace("\\*", ".*"). - Replace("\\?", ".") + "$"; + Replace("\\?", "?") + "$"; } private static bool IsAssembly(string filePath) diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 4b6740944..a41c4646c 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -141,6 +141,106 @@ public void TestGetExcludedFilesUsingGlobbing() Assert.Equal(paths.Length, excludedFiles.Count()); } + + [Fact] + public void TestIsModuleExcludedWithoutFilter() + { + var result = InstrumentationHelper.IsModuleExcluded("Module.dll", new string[0]); + + Assert.False(result); + } + + [Theory] + [InlineData("[Module]mismatch")] + [InlineData("[Mismatch]*")] + public void TestIsModuleExcludedWithSingleMismatchFilter(string filter) + { + var result = InstrumentationHelper.IsModuleExcluded("Module.dll", new[] { filter }); + + Assert.False(result); + } + + [Theory] + [MemberData(nameof(ValidModuleFilterData))] + public void TestIsModuleExcludedWithFilter(string filter) + { + var result = InstrumentationHelper.IsModuleExcluded("Module.dll", new[] { filter }); + + Assert.True(result); + } + + [Theory] + [MemberData(nameof(ValidModuleFilterData))] + public void TestIsModuleExcludedWithMatchingAndMismatchingFilter(string filter) + { + var filters = new[] { "[Mismatch]*", filter, "[Mismatch]*" }; + + var result = InstrumentationHelper.IsModuleExcluded("Module.dll", filters); + + Assert.True(result); + } + + [Fact] + public void TestIsTypeExcludedWithoutFilter() + { + var result = InstrumentationHelper.IsTypeExcluded("Module.dll", "a.b.Dto", new string[0]); + + Assert.False(result); + } + + [Theory] + [InlineData("[Module]mismatch")] + [InlineData("[Mismatch]*")] + [InlineData("[Mismatch]a.b.Dto")] + public void TestIsTypeExcludedWithSingleMismatchFilter(string filter) + { + var result = InstrumentationHelper.IsTypeExcluded("Module.dll", "a.b.Dto", new[] { filter }); + + Assert.False(result); + } + + [Theory] + [MemberData(nameof(ValidModuleAndNamespaceFilterData))] + public void TestIsTypeExcludedWithFilter(string filter) + { + var result = InstrumentationHelper.IsTypeExcluded("Module.dll", "a.b.Dto", new[] { filter }); + + Assert.True(result); + } + + [Theory] + [MemberData(nameof(ValidModuleAndNamespaceFilterData))] + public void TestIsTypeExcludedWithMatchingAndMismatchingFilter(string filter) + { + var filters = new[] { "[Mismatch]*", filter, "[Mismatch]*" }; + + var result = InstrumentationHelper.IsTypeExcluded("Module.dll", "a.b.Dto", filters); + + Assert.True(result); + } + + public static IEnumerable ValidModuleFilterData => + new List + { + new object[] { "[Module]*" }, + new object[] { "[Module*]*" }, + new object[] { "[Mod*ule]*" }, + new object[] { "[M*e]*" }, + new object[] { "[Mod*le*]*" }, + new object[] { "[Module?]*" }, + new object[] { "[ModuleX?]*" }, + }; + + public static IEnumerable ValidModuleAndNamespaceFilterData => + new List + { + new object[] { "[Module]a.b.Dto" }, + new object[] { "[Module]a.b.Dtos?" }, + new object[] { "[Module]a.*" }, + new object[] { "[Module]a*" }, + new object[] { "[Module]*b.*" }, + } + .Concat(ValidModuleFilterData); } }