Skip to content

Commit 1a28fcd

Browse files
authored
Merge pull request #233 from amweiss/feature/exclude-by-attribute
feat: Add ExcludeByAttribute option (#232)
2 parents 3454f21 + 643e1a5 commit 1a28fcd

File tree

10 files changed

+89
-23
lines changed

10 files changed

+89
-23
lines changed

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --threshold 80 -
176176
177177
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.
178178
179+
You can also ignore additional attributes by using the `ExcludeByAttribute` property (short name or full name supported):
180+
181+
```bash
182+
coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --exclude-by-attribute "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute"
183+
```
184+
179185
##### Source Files
180186
181187
You can also ignore specific source files from code coverage using the `--exclude-by-file` option
@@ -208,7 +214,7 @@ Examples
208214
coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --exclude "[coverlet.*]Coverlet.Core.Coverage"
209215
```
210216
211-
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `--include` option.
217+
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `--include` option.
212218
213219
Examples
214220
- `--include "[*]*"` => Includes all types in all assemblies (everything is instrumented)
@@ -224,7 +230,7 @@ In this mode, Coverlet doesn't require any additional setup other than including
224230
If a property takes multiple comma-separated values please note that [you will have to add escaped quotes around the string](https://github.com/Microsoft/msbuild/issues/2999#issuecomment-366078677) like this: `/p:Exclude=\"[coverlet.*]*,[*]Coverlet.Core*\"`, `/p:Include=\"[coverlet.*]*,[*]Coverlet.Core*\"`, or `/p:CoverletOutputFormat=\"json,opencover\"`.
225231
226232
##### Note for Powershell / VSTS users
227-
To exclude or include multiple assemblies when using Powershell scripts or creating a .yaml file for a VSTS build ```%2c``` should be used as a separator. Msbuild will translate this symbol to ```,```.
233+
To exclude or include multiple assemblies when using Powershell scripts or creating a .yaml file for a VSTS build ```%2c``` should be used as a separator. Msbuild will translate this symbol to ```,```.
228234
229235
```/p:Exclude="[*]*Examples?%2c[*]*Startup"```
230236
@@ -305,6 +311,12 @@ You can specify multiple values for `ThresholdType` by separating them with comm
305311
306312
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.
307313
314+
You can also ignore additional attributes by using the `ExcludeByAttribute` property (short name or full name supported):
315+
316+
```bash
317+
dotnet test /p:CollectCoverage=true /p:ExcludeByAttribute="Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute"
318+
```
319+
308320
#### Source Files
309321
You can also ignore specific source files from code coverage using the `ExcludeByFile` property
310322
- Use single or multiple paths (separate by comma)
@@ -315,7 +327,7 @@ You can also ignore specific source files from code coverage using the `ExcludeB
315327
dotnet test /p:CollectCoverage=true /p:ExcludeByFile=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
316328
```
317329
318-
##### Filters
330+
##### Filters
319331
Coverlet gives the ability to have fine grained control over what gets excluded using "filter expressions".
320332
321333
Syntax: `/p:Exclude=[Assembly-Filter]Type-Filter`
@@ -335,7 +347,7 @@ Examples
335347
dotnet test /p:CollectCoverage=true /p:Exclude="[coverlet.*]Coverlet.Core.Coverage"
336348
```
337349
338-
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `Include` property.
350+
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `Include` property.
339351
340352
Examples
341353
- `/p:Include="[*]*"` => Includes all types in all assemblies (everything is instrumented)

src/coverlet.console/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ static int Main(string[] args)
3434
CommandOption includeFilters = app.Option("--include", "Filter expressions to include only specific modules and types.", CommandOptionType.MultipleValue);
3535
CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue);
3636
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
37+
CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue);
3738

3839
app.OnExecute(() =>
3940
{
@@ -43,7 +44,7 @@ static int Main(string[] args)
4344
if (!target.HasValue())
4445
throw new CommandParsingException(app, "Target must be specified.");
4546

46-
Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value());
47+
Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value(), excludeAttributes.Values.ToArray());
4748
coverage.PrepareModules();
4849

4950
Process process = new Process();

src/coverlet.core/Coverage.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ public class Coverage
1919
private string[] _includeFilters;
2020
private string[] _excludedSourceFiles;
2121
private string _mergeWith;
22+
private string[] _excludeAttributes;
2223
private List<InstrumenterResult> _results;
2324

2425
public string Identifier
2526
{
2627
get { return _identifier; }
2728
}
2829

29-
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith)
30+
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
3031
{
3132
_module = module;
3233
_excludeFilters = excludeFilters;
3334
_includeFilters = includeFilters;
3435
_excludedSourceFiles = excludedSourceFiles;
3536
_mergeWith = mergeWith;
37+
_excludeAttributes = excludeAttributes;
3638

3739
_identifier = Guid.NewGuid().ToString();
3840
_results = new List<InstrumenterResult>();
@@ -51,7 +53,7 @@ public void PrepareModules()
5153
|| !InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
5254
continue;
5355

54-
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes);
56+
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes);
5557
if (instrumenter.CanInstrument())
5658
{
5759
InstrumentationHelper.BackupOriginalModule(module, _identifier);

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.Diagnostics.CodeAnalysis;
45
using System.IO;
@@ -22,20 +23,22 @@ internal class Instrumenter
2223
private readonly string[] _excludeFilters;
2324
private readonly string[] _includeFilters;
2425
private readonly string[] _excludedFiles;
26+
private readonly string[] _excludedAttributes;
2527
private InstrumenterResult _result;
2628
private FieldDefinition _customTrackerHitsArray;
2729
private FieldDefinition _customTrackerHitsFilePath;
2830
private ILProcessor _customTrackerClassConstructorIl;
2931
private TypeDefinition _customTrackerTypeDef;
3032
private MethodReference _customTrackerRecordHitMethod;
3133

32-
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles)
34+
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes)
3335
{
3436
_module = module;
3537
_identifier = identifier;
3638
_excludeFilters = excludeFilters;
3739
_includeFilters = includeFilters;
3840
_excludedFiles = excludedFiles ?? Array.Empty<string>();
41+
_excludedAttributes = excludedAttributes;
3942
}
4043

4144
public bool CanInstrument() => InstrumentationHelper.HasPdb(_module);
@@ -179,7 +182,7 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
179182
{
180183
handler.CatchType = module.ImportReference(handler.CatchType);
181184
}
182-
185+
183186
methodOnCustomType.Body.ExceptionHandlers.Add(handler);
184187
}
185188

@@ -392,19 +395,24 @@ private static void ReplaceExceptionHandlerBoundary(ExceptionHandler handler, In
392395
handler.TryStart = newTarget;
393396
}
394397

395-
private static bool IsExcludeAttribute(CustomAttribute customAttribute)
398+
private bool IsExcludeAttribute(CustomAttribute customAttribute)
396399
{
397-
var excludeAttributeNames = new[]
400+
// The default custom attributes used to exclude from coverage.
401+
IEnumerable<string> excludeAttributeNames = new List<string>()
398402
{
399403
nameof(ExcludeFromCoverageAttribute),
400-
"ExcludeFromCoverage",
401-
nameof(ExcludeFromCodeCoverageAttribute),
402-
"ExcludeFromCodeCoverage"
404+
nameof(ExcludeFromCodeCoverageAttribute)
403405
};
404406

405-
var attributeName = customAttribute.AttributeType.Name;
406-
return excludeAttributeNames.Any(a => a.Equals(attributeName));
407-
}
407+
// Include the other attributes to exclude based on incoming parameters.
408+
if (_excludedAttributes != null)
409+
{
410+
excludeAttributeNames = _excludedAttributes.Union(excludeAttributeNames);
411+
}
412+
413+
return excludeAttributeNames.Any(a =>
414+
customAttribute.AttributeType.Name.Equals(a.EndsWith("Attribute")? a : $"{a}Attribute"));
415+
}
408416

409417
private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method)
410418
{

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class InstrumentationTask : Task
1313
private string _include;
1414
private string _excludeByFile;
1515
private string _mergeWith;
16+
private string _excludeByAttribute;
1617

1718
internal static Coverage Coverage
1819
{
@@ -25,7 +26,7 @@ public string Path
2526
get { return _path; }
2627
set { _path = value; }
2728
}
28-
29+
2930
public string Exclude
3031
{
3132
get { return _exclude; }
@@ -50,15 +51,22 @@ public string MergeWith
5051
set { _mergeWith = value; }
5152
}
5253

54+
public string ExcludeByAttribute
55+
{
56+
get { return _excludeByAttribute; }
57+
set { _excludeByAttribute = value; }
58+
}
59+
5360
public override bool Execute()
5461
{
5562
try
5663
{
5764
var excludedSourceFiles = _excludeByFile?.Split(',');
5865
var excludeFilters = _exclude?.Split(',');
5966
var includeFilters = _include?.Split(',');
67+
var excludeAttributes = _excludeByAttribute?.Split(',');
6068

61-
_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith);
69+
_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith, excludeAttributes);
6270
_coverage.PrepareModules();
6371
}
6472
catch (Exception ex)

src/coverlet.msbuild/coverlet.msbuild.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
1010
<Threshold Condition="$(Threshold) == ''">0</Threshold>
1111
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
12+
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
1213
</PropertyGroup>
1314
</Project>

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Exclude="$(Exclude)"
1111
ExcludeByFile="$(ExcludeByFile)"
1212
MergeWith="$(MergeWith)"
13+
ExcludeByAttribute="$(ExcludeByAttribute)"
1314
Path="$(TargetPath)" />
1415
</Target>
1516

@@ -20,6 +21,7 @@
2021
Exclude="$(Exclude)"
2122
ExcludeByFile="$(ExcludeByFile)"
2223
MergeWith="$(MergeWith)"
24+
ExcludeByAttribute="$(ExcludeByAttribute)"
2325
Path="$(TargetPath)" />
2426
</Target>
2527

test/coverlet.core.tests/CoverageTests.cs

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

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

3333
var result = coverage.GetCoverageResult();

test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void TestCoreLibInstrumentation()
2727
foreach (var file in files)
2828
File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true);
2929

30-
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
30+
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
3131
Assert.True(instrumenter.CanInstrument());
3232
var result = instrumenter.Instrument();
3333
Assert.NotNull(result);
@@ -76,7 +76,26 @@ public void TestInstrument_ClassesWithExcludeAttributeAreExcluded(Type excludedT
7676
instrumenterTest.Directory.Delete(true);
7777
}
7878

79-
private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false)
79+
80+
[Theory]
81+
[InlineData(nameof(ObsoleteAttribute))]
82+
[InlineData("Obsolete")]
83+
public void TestInstrument_ClassesWithCustomExcludeAttributeAreExcluded(string excludedAttribute)
84+
{
85+
var instrumenterTest = CreateInstrumentor(attributesToIgnore: new string[] { excludedAttribute });
86+
var result = instrumenterTest.Instrumenter.Instrument();
87+
88+
var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs");
89+
Assert.NotNull(doc);
90+
#pragma warning disable CS0612 // Type or member is obsolete
91+
var found = doc.Lines.Values.Any(l => l.Class.Equals(nameof(ClassExcludedByObsoleteAttr)));
92+
#pragma warning restore CS0612 // Type or member is obsolete
93+
Assert.False(found, "Class decorated with with exclude attribute should be excluded");
94+
95+
instrumenterTest.Directory.Delete(true);
96+
}
97+
98+
private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, string[] attributesToIgnore = null)
8099
{
81100
string module = GetType().Assembly.Location;
82101
string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
@@ -100,7 +119,7 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false)
100119
File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true);
101120

102121
module = Path.Combine(directory.FullName, destModule);
103-
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
122+
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore);
104123
return new InstrumenterTest
105124
{
106125
Instrumenter = instrumenter,

test/coverlet.core.tests/Samples/Samples.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,17 @@ public string Method(string input)
189189
return input;
190190
}
191191
}
192+
193+
[Obsolete]
194+
public class ClassExcludedByObsoleteAttr
195+
{
196+
197+
public string Method(string input)
198+
{
199+
if (string.IsNullOrEmpty(input))
200+
throw new ArgumentException("Cannot be empty", nameof(input));
201+
202+
return input;
203+
}
204+
}
192205
}

0 commit comments

Comments
 (0)