Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --threshold 80 -

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.

You can also ignore additional attributes by using the `ExcludeByAttribute` property (short name or full name supported):

```bash
coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --exclude-by-attributes "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute"
```

##### Source Files

You can also ignore specific source files from code coverage using the `--exclude-by-file` option
Expand Down Expand Up @@ -185,7 +191,7 @@ Examples
coverlet <ASSEMBLY> --target <TARGET> --targetargs <TARGETARGS> --exclude "[coverlet.*]Coverlet.Core.Coverage"
```

Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `--include` option.
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `--include` option.

Examples
- `--include "[*]*"` => INcludes all types in all assemblies (nothing is instrumented)
Expand All @@ -201,7 +207,7 @@ In this mode, Coverlet doesn't require any additional setup other than including
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\"`.

##### Note for Powershell / VSTS users
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 ```,```.
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 ```,```.

```/p:Exclude="[*]*Examples?%2c[*]*Startup"```

Expand Down Expand Up @@ -281,6 +287,12 @@ You can specify multiple values for `ThresholdType` by separating them with comm

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.

You can also ignore additional attributes by using the `ExcludeByAttribute` property (short name or full name supported):

```bash
dotnet test /p:CollectCoverage=true /p:ExcludeByAttribute="Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute"
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should mention that the Attribute suffix is optional. See my other comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a sight fear since it wasn't using the fully qualified name that it could cause issues, but I can make the it try to do the short name resolution like the compiler as well.


#### Source Files
You can also ignore specific source files from code coverage using the `ExcludeByFile` property
- Use single or multiple paths (separate by comma)
Expand All @@ -291,7 +303,7 @@ You can also ignore specific source files from code coverage using the `ExcludeB
dotnet test /p:CollectCoverage=true /p:ExcludeByFile=\"../dir1/class1.cs,../dir2/*.cs,../dir3/**/*.cs,\"
```

##### Filters
##### Filters
Coverlet gives the ability to have fine grained control over what gets excluded using "filter expressions".

Syntax: `/p:Exclude=[Assembly-Filter]Type-Filter`
Expand All @@ -311,7 +323,7 @@ Examples
dotnet test /p:CollectCoverage=true /p:Exclude="[coverlet.*]Coverlet.Core.Coverage"
```

Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `Include` property.
Coverlet goes a step in the other direction by also letting you explicitly set what can be included using the `Include` property.

Examples
- `/p:Include="[*]*"` => Includes all types in all assemblies (everything is instrumented)
Expand Down
3 changes: 2 additions & 1 deletion src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ static int Main(string[] args)
CommandOption includeFilters = app.Option("--include", "Filter expressions to include only specific modules and types.", CommandOptionType.MultipleValue);
CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue);
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue);

app.OnExecute(() =>
{
Expand All @@ -44,7 +45,7 @@ static int Main(string[] args)
if (!target.HasValue())
throw new CommandParsingException(app, "Target must be specified.");

Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value());
Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value(), excludeAttributes.Values.ToArray());
coverage.PrepareModules();

Process process = new Process();
Expand Down
6 changes: 4 additions & 2 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ public class Coverage
private string[] _includeFilters;
private string[] _excludedSourceFiles;
private string _mergeWith;
private string[] _excludeAttributes;
private List<InstrumenterResult> _results;

public string Identifier
{
get { return _identifier; }
}

public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith)
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
{
_module = module;
_excludeFilters = excludeFilters;
_includeFilters = includeFilters;
_excludedSourceFiles = excludedSourceFiles;
_mergeWith = mergeWith;
_excludeAttributes = excludeAttributes;

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

var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes);
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes);
if (instrumenter.CanInstrument())
{
InstrumentationHelper.BackupOriginalModule(module, _identifier);
Expand Down
28 changes: 18 additions & 10 deletions src/coverlet.core/Instrumentation/Instrumenter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
Expand All @@ -22,20 +23,22 @@ internal class Instrumenter
private readonly string[] _excludeFilters;
private readonly string[] _includeFilters;
private readonly string[] _excludedFiles;
private readonly string[] _excludedAttributes;
private InstrumenterResult _result;
private FieldDefinition _customTrackerHitsArray;
private FieldDefinition _customTrackerHitsFilePath;
private ILProcessor _customTrackerClassConstructorIl;
private TypeDefinition _customTrackerTypeDef;
private MethodReference _customTrackerRecordHitMethod;

public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles)
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes)
{
_module = module;
_identifier = identifier;
_excludeFilters = excludeFilters;
_includeFilters = includeFilters;
_excludedFiles = excludedFiles ?? Array.Empty<string>();
_excludedAttributes = excludedAttributes;
}

public bool CanInstrument() => InstrumentationHelper.HasPdb(_module);
Expand Down Expand Up @@ -179,7 +182,7 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
{
handler.CatchType = module.ImportReference(handler.CatchType);
}

methodOnCustomType.Body.ExceptionHandlers.Add(handler);
}

Expand Down Expand Up @@ -392,19 +395,24 @@ private static void ReplaceExceptionHandlerBoundary(ExceptionHandler handler, In
handler.TryStart = newTarget;
}

private static bool IsExcludeAttribute(CustomAttribute customAttribute)
private bool IsExcludeAttribute(CustomAttribute customAttribute)
{
var excludeAttributeNames = new[]
// The default custom attributes used to exclude from coverage.
IEnumerable<string> excludeAttributeNames = new List<string>()
{
nameof(ExcludeFromCoverageAttribute),
"ExcludeFromCoverage",
nameof(ExcludeFromCodeCoverageAttribute),
"ExcludeFromCodeCoverage"
nameof(ExcludeFromCodeCoverageAttribute)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a backward incompatible change. We should make specifying the Attribute suffix optional

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually incompatible? From what I saw of the usage even if you use the short name attribute like [ExcludeFromCoverage] the attribute name still comes across with the full name ExcludeFromCoverageAttribute. Were you trying to account for people writing custom attributes to handle excluding from coverage that were named the same as your attributes?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more of a user experience thing. Since the compiler doesn't enforce the Attribute suffix I don't want us to as well. I want the user to be able to specify Obsolete and still be fine. You can simply append Attribute to the name if it's not specified

};

var attributeName = customAttribute.AttributeType.Name;
return excludeAttributeNames.Any(a => a.Equals(attributeName));
}
// Include the other attributes to exclude based on incoming parameters.
if (_excludedAttributes != null)
{
excludeAttributeNames = _excludedAttributes.Union(excludeAttributeNames);
}

return excludeAttributeNames.Any(a =>
customAttribute.AttributeType.Name.Equals(a.EndsWith("Attribute")? a : $"{a}Attribute"));
}

private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method)
{
Expand Down
12 changes: 10 additions & 2 deletions src/coverlet.msbuild.tasks/InstrumentationTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class InstrumentationTask : Task
private string _include;
private string _excludeByFile;
private string _mergeWith;
private string _excludeByAttributes;

internal static Coverage Coverage
{
Expand All @@ -25,7 +26,7 @@ public string Path
get { return _path; }
set { _path = value; }
}

public string Exclude
{
get { return _exclude; }
Expand All @@ -50,15 +51,22 @@ public string MergeWith
set { _mergeWith = value; }
}

public string ExcludeByAttributes
{
get { return _excludeByAttributes; }
set { _excludeByAttributes = value; }
}

public override bool Execute()
{
try
{
var excludedSourceFiles = _excludeByFile?.Split(',');
var excludeFilters = _exclude?.Split(',');
var includeFilters = _include?.Split(',');
var excludeAttributes = _excludeByAttributes?.Split(',');

_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith);
_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith, excludeAttributes);
_coverage.PrepareModules();
}
catch (Exception ex)
Expand Down
1 change: 1 addition & 0 deletions src/coverlet.msbuild/coverlet.msbuild.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
<Threshold Condition="$(Threshold) == ''">0</Threshold>
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
<ExcludeByAttributes Condition="$(ExcludeByAttributes) == ''"></ExcludeByAttributes>
</PropertyGroup>
</Project>
2 changes: 2 additions & 0 deletions src/coverlet.msbuild/coverlet.msbuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
ExcludeByAttributes="$(ExcludeByAttributes)"
Path="$(TargetPath)" />
</Target>

Expand All @@ -20,6 +21,7 @@
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
ExcludeByAttributes="$(ExcludeByAttributes)"
Path="$(TargetPath)" />
</Target>

Expand Down
2 changes: 1 addition & 1 deletion test/coverlet.core.tests/CoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestCoverage()
// Since Coverage only instruments dependancies, we need a fake module here
var testModule = Path.Combine(directory.FullName, "test.module.dll");

var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty);
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, Array.Empty<string>());
coverage.PrepareModules();

var result = coverage.GetCoverageResult();
Expand Down
25 changes: 22 additions & 3 deletions test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestCoreLibInstrumentation()
foreach (var file in files)
File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true);

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

private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false)

[Theory]
[InlineData(nameof(ObsoleteAttribute))]
[InlineData("Obsolete")]
public void TestInstrument_ClassesWithCustomExcludeAttributeAreExcluded(string excludedAttribute)
{
var instrumenterTest = CreateInstrumentor(attributesToIgnore: new string[] { excludedAttribute });
var result = instrumenterTest.Instrumenter.Instrument();

var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs");
Assert.NotNull(doc);
#pragma warning disable CS0612 // Type or member is obsolete
var found = doc.Lines.Values.Any(l => l.Class.Equals(nameof(ClassExcludedByObsoleteAttr)));
#pragma warning restore CS0612 // Type or member is obsolete
Assert.False(found, "Class decorated with with exclude attribute should be excluded");

instrumenterTest.Directory.Delete(true);
}

private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, string[] attributesToIgnore = null)
{
string module = GetType().Assembly.Location;
string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
Expand All @@ -100,7 +119,7 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false)
File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true);

module = Path.Combine(directory.FullName, destModule);
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore);
return new InstrumenterTest
{
Instrumenter = instrumenter,
Expand Down
13 changes: 13 additions & 0 deletions test/coverlet.core.tests/Samples/Samples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,17 @@ public string Method(string input)
return input;
}
}

[Obsolete]
public class ClassExcludedByObsoleteAttr
{

public string Method(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("Cannot be empty", nameof(input));

return input;
}
}
}