Exclude code that follows [DoesNotReturn] from code coverage (#904)

Exclude code that follows [DoesNotReturn] from code coverage
This commit is contained in:
kevin-montrose
2020-09-05 05:21:21 -04:00
committed by GitHub
parent 6479f627e4
commit 5ec636bb16
18 changed files with 1286 additions and 59 deletions
+21 -20
View File
@@ -17,26 +17,27 @@ Arguments:
<ASSEMBLY> Path to the test assembly.
Options:
-h|--help Show help information
-v|--version Show version information
-t|--target Path to the test runner application.
-a|--targetargs Arguments to be passed to the test runner.
-o|--output Output of the generated coverage report
-v|--verbosity Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed.
-f|--format Format of the generated coverage report[multiple value].
--threshold Exits with error if the coverage % is below value.
--threshold-type Coverage type to apply the threshold to[multiple value].
--threshold-stat Coverage statistic used to enforce the threshold value.
--exclude Filter expressions to exclude specific modules and types[multiple value].
--include Filter expressions to include specific modules and types[multiple value].
--include-directory Include directories containing additional assemblies to be instrumented[multiple value].
--exclude-by-file Glob patterns specifying source files to exclude[multiple value].
--exclude-by-attribute Attributes to exclude from code coverage[multiple value].
--include-test-assembly Specifies whether to report code coverage of the test assembly.
--single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location.
--merge-with Path to existing coverage result to merge.
--use-source-link Specifies whether to use SourceLink URIs in place of file system paths.
--skipautoprops Neither track nor record auto-implemented properties.
-h|--help Show help information
-v|--version Show version information
-t|--target Path to the test runner application.
-a|--targetargs Arguments to be passed to the test runner.
-o|--output Output of the generated coverage report
-v|--verbosity Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed.
-f|--format Format of the generated coverage report[multiple value].
--threshold Exits with error if the coverage % is below value.
--threshold-type Coverage type to apply the threshold to[multiple value].
--threshold-stat Coverage statistic used to enforce the threshold value.
--exclude Filter expressions to exclude specific modules and types[multiple value].
--include Filter expressions to include specific modules and types[multiple value].
--include-directory Include directories containing additional assemblies to be instrumented[multiple value].
--exclude-by-file Glob patterns specifying source files to exclude[multiple value].
--exclude-by-attribute Attributes to exclude from code coverage[multiple value].
--include-test-assembly Specifies whether to report code coverage of the test assembly.
--single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location.
--merge-with Path to existing coverage result to merge.
--use-source-link Specifies whether to use SourceLink URIs in place of file system paths.
--skipautoprops Neither track nor record auto-implemented properties.
--does-not-return-attribute Attributes that mark methods that do not return[multiple value].
```
NB. For a [multiple value] options you have to specify values multiple times i.e.
+7
View File
@@ -162,6 +162,13 @@ You can also include coverage of the test assembly itself by setting `/p:Include
Neither track nor record auto-implemented properties.
Syntax: `/p:SkipAutoProps=true`
### Methods that do not return
Methods that do not return can be marked with attributes to cause statements after them to be excluded from coverage. `DoesNotReturnAttribute` is included by default.
Attributes can be specified with the following syntax.
Syntax: `/p:DoesNotReturnAttribute="DoesNotReturnAttribute,OtherAttribute"`
### 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 ```,```.
+1
View File
@@ -80,6 +80,7 @@ These are a list of options that are supported by coverlet. These can be specifi
|UseSourceLink | Specifies whether to use SourceLink URIs in place of file system paths. |
|IncludeTestAssembly | Include coverage of the test assembly. |
|SkipAutoProps | Neither track nor record auto-implemented properties. |
|DoesNotReturnAttribute | Methods marked with these attributes are known not to return, statements following them will be excluded from coverage |
How to specify these options via runsettings?
```
@@ -28,7 +28,8 @@ namespace Coverlet.Collector.DataCollection
SingleHit = settings.SingleHit,
MergeWith = settings.MergeWith,
UseSourceLink = settings.UseSourceLink,
SkipAutoProps = settings.SkipAutoProps
SkipAutoProps = settings.SkipAutoProps,
DoesNotReturnAttributes = settings.DoesNotReturnAttributes
};
return new Coverage(
@@ -68,6 +68,11 @@ namespace Coverlet.Collector.DataCollection
/// </summary>
public bool SkipAutoProps { get; set; }
/// <summary>
/// Attributes that mark methods that never return.
/// </summary>
public string[] DoesNotReturnAttributes { get; set; }
public override string ToString()
{
var builder = new StringBuilder();
@@ -83,6 +88,7 @@ namespace Coverlet.Collector.DataCollection
builder.AppendFormat("SingleHit: '{0}'", SingleHit);
builder.AppendFormat("IncludeTestAssembly: '{0}'", IncludeTestAssembly);
builder.AppendFormat("SkipAutoProps: '{0}'", SkipAutoProps);
builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty<string>()));
return builder.ToString();
}
@@ -43,6 +43,7 @@ namespace Coverlet.Collector.DataCollection
coverletSettings.SingleHit = ParseSingleHit(configurationElement);
coverletSettings.IncludeTestAssembly = ParseIncludeTestAssembly(configurationElement);
coverletSettings.SkipAutoProps = ParseSkipAutoProps(configurationElement);
coverletSettings.DoesNotReturnAttributes = ParseDoesNotReturnAttributes(configurationElement);
}
coverletSettings.ReportFormats = ParseReportFormats(configurationElement);
@@ -218,6 +219,17 @@ namespace Coverlet.Collector.DataCollection
return skipAutoProps;
}
/// <summary>
/// Parse attributes that mark methods that do not return.
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>DoesNotReturn attributes</returns>
private string[] ParseDoesNotReturnAttributes(XmlElement configurationElement)
{
XmlElement doesNotReturnAttributesElement = configurationElement[CoverletConstants.DoesNotReturnAttributesElementName];
return this.SplitElement(doesNotReturnAttributesElement);
}
/// <summary>
/// Splits a comma separated elements into an array
/// </summary>
@@ -21,5 +21,6 @@
public const string DefaultExcludeFilter = "[coverlet.*]*";
public const string InProcDataCollectorName = "CoverletInProcDataCollector";
public const string SkipAutoProps = "SkipAutoProps";
public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute";
}
}
+3 -1
View File
@@ -63,6 +63,7 @@ namespace Coverlet.Console
CommandOption skipAutoProp = app.Option("--skipautoprops", "Neither track nor record auto-implemented properties.", CommandOptionType.NoValue);
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue);
CommandOption doesNotReturnAttributes = app.Option("--does-not-return-attribute", "Attributes that mark methods that do not return.", CommandOptionType.MultipleValue);
app.OnExecute(() =>
{
@@ -89,7 +90,8 @@ namespace Coverlet.Console
SingleHit = singleHit.HasValue(),
MergeWith = mergeWith.Value(),
UseSourceLink = useSourceLink.HasValue(),
SkipAutoProps = skipAutoProp.HasValue()
SkipAutoProps = skipAutoProp.HasValue(),
DoesNotReturnAttributes = doesNotReturnAttributes.Values.ToArray()
};
Coverage coverage = new Coverage(module.Value,
@@ -0,0 +1,7 @@
using System;
namespace Coverlet.Core.Attributes
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)]
internal class DoesNotReturnAttribute : Attribute { }
}
+4
View File
@@ -23,6 +23,7 @@ namespace Coverlet.Core
public bool SingleHit { get; set; }
public string MergeWith { get; set; }
public bool UseSourceLink { get; set; }
public string[] DoesNotReturnAttributes { get; set; }
public bool SkipAutoProps { get; set; }
}
@@ -39,6 +40,7 @@ namespace Coverlet.Core
private bool _singleHit;
private string _mergeWith;
private bool _useSourceLink;
private string[] _doesNotReturnAttributes;
private bool _skipAutoProps;
private ILogger _logger;
private IInstrumentationHelper _instrumentationHelper;
@@ -70,6 +72,7 @@ namespace Coverlet.Core
_singleHit = parameters.SingleHit;
_mergeWith = parameters.MergeWith;
_useSourceLink = parameters.UseSourceLink;
_doesNotReturnAttributes = parameters.DoesNotReturnAttributes;
_logger = logger;
_instrumentationHelper = instrumentationHelper;
_fileSystem = fileSystem;
@@ -124,6 +127,7 @@ namespace Coverlet.Core
_includeFilters,
_excludedSourceFiles,
_excludeAttributes,
_doesNotReturnAttributes,
_singleHit,
_skipAutoProps,
_logger,
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Coverlet.Core.Instrumentation.Reachability;
using Coverlet.Core.Abstractions;
using Coverlet.Core.Attributes;
using Coverlet.Core.Symbols;
@@ -45,6 +46,8 @@ namespace Coverlet.Core.Instrumentation
private List<string> _branchesInCompiledGeneratedClass;
private List<(MethodDefinition, int)> _excludedMethods;
private List<string> _excludedCompilerGeneratedTypes;
private readonly string[] _doesNotReturnAttributes;
private ReachabilityHelper _reachabilityHelper;
public bool SkipModule { get; set; } = false;
@@ -55,6 +58,7 @@ namespace Coverlet.Core.Instrumentation
string[] includeFilters,
string[] excludedFiles,
string[] excludedAttributes,
string[] doesNotReturnAttributes,
bool singleHit,
bool skipAutoProps,
ILogger logger,
@@ -68,17 +72,7 @@ namespace Coverlet.Core.Instrumentation
_excludeFilters = excludeFilters;
_includeFilters = includeFilters;
_excludedFilesHelper = new ExcludedFilesHelper(excludedFiles, logger);
_excludedAttributes = (excludedAttributes ?? Array.Empty<string>())
// In case the attribute class ends in "Attribute", but it wasn't specified.
// Both names are included (if it wasn't specified) because the attribute class might not actually end in the prefix.
.SelectMany(a => a.EndsWith("Attribute") ? new[] { a } : new[] { a, $"{a}Attribute" })
// The default custom attributes used to exclude from coverage.
.Union(new List<string>()
{
nameof(ExcludeFromCoverageAttribute),
nameof(ExcludeFromCodeCoverageAttribute)
})
.ToArray();
_excludedAttributes = PrepareAttributes(excludedAttributes, nameof(ExcludeFromCoverageAttribute), nameof(ExcludeFromCodeCoverageAttribute));
_singleHit = singleHit;
_isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib";
_logger = logger;
@@ -86,9 +80,22 @@ namespace Coverlet.Core.Instrumentation
_fileSystem = fileSystem;
_sourceRootTranslator = sourceRootTranslator;
_cecilSymbolHelper = cecilSymbolHelper;
_doesNotReturnAttributes = PrepareAttributes(doesNotReturnAttributes, nameof(DoesNotReturnAttribute));
_skipAutoProps = skipAutoProps;
}
private static string[] PrepareAttributes(IEnumerable<string> providedAttrs, params string[] defaultAttrs)
{
return
(providedAttrs ?? Array.Empty<string>())
// In case the attribute class ends in "Attribute", but it wasn't specified.
// Both names are included (if it wasn't specified) because the attribute class might not actually end in the prefix.
.SelectMany(a => a.EndsWith("Attribute") ? new[] { a } : new[] { a, $"{a}Attribute" })
// The default custom attributes used to exclude from coverage.
.Union(defaultAttrs)
.ToArray();
}
public bool CanInstrument()
{
try
@@ -183,8 +190,31 @@ namespace Coverlet.Core.Instrumentation
return _isCoreLibrary && type.FullName == "System.Threading.Interlocked";
}
// Have to do this before we start writing to a module, as we'll get into file
// locking issues if we do it while writing.
private void CreateReachabilityHelper()
{
using (var stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.Read))
using (var resolver = new NetstandardAwareAssemblyResolver(_module, _logger))
{
resolver.AddSearchDirectory(Path.GetDirectoryName(_module));
var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver };
if (_isCoreLibrary)
{
parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider();
}
using (var module = ModuleDefinition.ReadModule(stream, parameters))
{
_reachabilityHelper = ReachabilityHelper.CreateForModule(module, _doesNotReturnAttributes, _logger);
}
}
}
private void InstrumentModule()
{
CreateReachabilityHelper();
using (var stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.ReadWrite))
using (var resolver = new NetstandardAwareAssemblyResolver(_module, _logger))
{
@@ -509,14 +539,32 @@ namespace Coverlet.Core.Instrumentation
var branchPoints = _cecilSymbolHelper.GetBranchPoints(method);
var unreachableRanges = _reachabilityHelper.FindUnreachableIL(processor.Body.Instructions, processor.Body.ExceptionHandlers);
var currentUnreachableRangeIx = 0;
for (int n = 0; n < count; n++)
{
var instruction = processor.Body.Instructions[index];
var sequencePoint = method.DebugInformation.GetSequencePoint(instruction);
var targetedBranchPoints = branchPoints.Where(p => p.EndOffset == instruction.Offset);
// Check if the instruction is coverable
if (_cecilSymbolHelper.SkipNotCoverableInstruction(method, instruction))
// make sure we're looking at the correct unreachable range (if any)
var instrOffset = instruction.Offset;
while (currentUnreachableRangeIx < unreachableRanges.Length && instrOffset > unreachableRanges[currentUnreachableRangeIx].EndOffset)
{
currentUnreachableRangeIx++;
}
// determine if the unreachable
var isUnreachable = false;
if (currentUnreachableRangeIx < unreachableRanges.Length)
{
var range = unreachableRanges[currentUnreachableRangeIx];
isUnreachable = instrOffset >= range.StartOffset && instrOffset <= range.EndOffset;
}
// Check is both reachable, _and_ coverable
if (isUnreachable || _cecilSymbolHelper.SkipNotCoverableInstruction(method, instruction))
{
index++;
continue;
@@ -0,0 +1,813 @@
using Coverlet.Core.Abstractions;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using System;
using System.Collections.Immutable;
using System.Linq;
namespace Coverlet.Core.Instrumentation.Reachability
{
/// <summary>
/// Helper for find unreachable IL instructions.
/// </summary>
internal class ReachabilityHelper
{
internal readonly struct UnreachableRange
{
/// <summary>
/// Offset of first unreachable instruction.
/// </summary>
public int StartOffset { get; }
/// <summary>
/// Offset of last unreachable instruction.
/// </summary>
public int EndOffset { get; }
public UnreachableRange(int start, int end)
{
StartOffset = start;
EndOffset = end;
}
public override string ToString()
=> $"[IL_{StartOffset:x4}, IL_{EndOffset:x4}]";
}
private class BasicBlock
{
/// <summary>
/// Offset of the instruction that starts the block
/// </summary>
public int StartOffset { get; }
/// <summary>
/// Whether it is possible for control to flow past the end of the block,
/// ie. whether it's tail is reachable
/// </summary>
public bool TailReachable => UnreachableAfter == null;
/// <summary>
/// If control flows to the end of the block, where it can flow to
/// </summary>
public ImmutableArray<int> BranchesTo { get; }
/// <summary>
/// If this block contains a call(i) to a method that does not return
/// this will be the first such call.
/// </summary>
public Instruction UnreachableAfter { get; }
/// <summary>
/// If an exception is raised in this block, where control might branch to.
///
/// Note that this can happen even if the block's end is unreachable.
/// </summary>
public ImmutableArray<int> ExceptionBranchesTo { get; }
/// <summary>
/// Mutable, records whether control can flow into the block,
/// ie. whether it's head is reachable
/// </summary>
public bool HeadReachable { get; set; }
public BasicBlock(int startOffset, Instruction unreachableAfter, ImmutableArray<int> branchesTo, ImmutableArray<int> exceptionBranchesTo)
{
StartOffset = startOffset;
UnreachableAfter = unreachableAfter;
BranchesTo = branchesTo;
ExceptionBranchesTo = exceptionBranchesTo;
}
public override string ToString()
=> $"{nameof(StartOffset)}=IL_{StartOffset:x4}, {nameof(HeadReachable)}={HeadReachable}, {nameof(TailReachable)}={TailReachable}, {nameof(BranchesTo)}=({string.Join(", ", BranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(ExceptionBranchesTo)}=({string.Join(", ", ExceptionBranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(UnreachableAfter)}={(UnreachableAfter != null ? $"IL_{UnreachableAfter:x4}" : "")}";
}
/// <summary>
/// Represents an Instruction that transitions control flow (ie. branches).
///
/// This is _different_ from other branch types, like Branch and BranchPoint
/// because it includes unconditional branches too.
/// </summary>
private readonly struct BranchInstruction
{
/// <summary>
/// Location of the branching instruction
/// </summary>
public int Offset { get; }
/// <summary>
/// Returns true if this branch has multiple targets.
/// </summary>
public bool HasMultiTargets => _TargetOffset == -1;
private readonly int _TargetOffset;
/// <summary>
/// Target of the branch, assuming it has a single target.
///
/// It is illegal to access this if there are multiple targets.
/// </summary>
public int TargetOffset
{
get
{
if (HasMultiTargets)
{
throw new InvalidOperationException($"{HasMultiTargets} is true");
}
return _TargetOffset;
}
}
private readonly ImmutableArray<int> _TargetOffsets;
/// <summary>
/// Targets of the branch, assuming it has multiple targets.
///
/// It is illegal to access this if there is a single target.
/// </summary>
public ImmutableArray<int> TargetOffsets
{
get
{
if (!HasMultiTargets)
{
throw new InvalidOperationException($"{HasMultiTargets} is false");
}
return _TargetOffsets;
}
}
public BranchInstruction(int offset, int targetOffset)
{
Offset = offset;
_TargetOffset = targetOffset;
_TargetOffsets = ImmutableArray<int>.Empty;
}
public BranchInstruction(int offset, ImmutableArray<int> targetOffset)
{
if (targetOffset.Length == 1)
{
throw new ArgumentException("Use single entry constructor for single targets", nameof(targetOffset));
}
Offset = offset;
_TargetOffset = -1;
_TargetOffsets = targetOffset;
}
public override string ToString()
=> $"IL_{Offset:x4}: {(HasMultiTargets ? string.Join(", ", TargetOffsets.Select(x => $"IL_{x:x4}")) : $"IL_{TargetOffset:x4}")}";
}
/// <summary>
/// OpCodes that transfer control code, even if they do not
/// introduce branch points.
/// </summary>
private static readonly ImmutableHashSet<OpCode> BRANCH_OPCODES =
ImmutableHashSet.CreateRange(
new[]
{
OpCodes.Beq,
OpCodes.Beq_S,
OpCodes.Bge,
OpCodes.Bge_S,
OpCodes.Bge_Un,
OpCodes.Bge_Un_S,
OpCodes.Bgt,
OpCodes.Bgt_S,
OpCodes.Bgt_Un,
OpCodes.Bgt_Un_S,
OpCodes.Ble,
OpCodes.Ble_S,
OpCodes.Ble_Un,
OpCodes.Ble_Un_S,
OpCodes.Blt,
OpCodes.Blt_S,
OpCodes.Blt_Un,
OpCodes.Blt_Un_S,
OpCodes.Bne_Un,
OpCodes.Bne_Un_S,
OpCodes.Br,
OpCodes.Br_S,
OpCodes.Brfalse,
OpCodes.Brfalse_S,
OpCodes.Brtrue,
OpCodes.Brtrue_S,
OpCodes.Switch,
// These are forms of Br(_S) that are legal to use to exit
// an exception block
//
// So they're "weird" but not too weird for our purposes
//
// The somewhat nasty subtlety is that, within an exception block,
// it's perfectly legal to replace all normal branches with Leaves
// even if they don't actually exit the block.
OpCodes.Leave,
OpCodes.Leave_S,
// these implicitly branch at the end of a filter or finally block
// their operands do not encode anything interesting, we have to
// look at exception handlers to figure out where they go to
OpCodes.Endfilter,
OpCodes.Endfinally
}
);
/// <summary>
/// OpCodes that unconditionally transfer control, so there
/// is not "fall through" branch target.
/// </summary>
private static readonly ImmutableHashSet<OpCode> UNCONDITIONAL_BRANCH_OPCODES =
ImmutableHashSet.CreateRange(
new[]
{
OpCodes.Br,
OpCodes.Br_S,
OpCodes.Leave,
OpCodes.Leave_S
}
);
private readonly ImmutableHashSet<MetadataToken> DoesNotReturnMethods;
private ReachabilityHelper(ImmutableHashSet<MetadataToken> doesNotReturnMethods)
{
DoesNotReturnMethods = doesNotReturnMethods;
}
/// <summary>
/// Build a ReachabilityHelper for the given module.
///
/// Predetermines methods that will not return, as
/// indicated by the presense of the given attributes.
/// </summary>
public static ReachabilityHelper CreateForModule(ModuleDefinition module, string[] doesNotReturnAttributes, ILogger logger)
{
if (doesNotReturnAttributes.Length == 0)
{
return new ReachabilityHelper(ImmutableHashSet<MetadataToken>.Empty);
}
var processedMethods = ImmutableHashSet<MetadataToken>.Empty;
var doNotReturn = ImmutableHashSet.CreateBuilder<MetadataToken>();
foreach (var type in module.Types)
{
foreach (var mtd in type.Methods)
{
if (mtd.IsNative)
{
continue;
}
MethodBody body;
try
{
if (!mtd.HasBody)
{
continue;
}
body = mtd.Body;
}
catch
{
continue;
}
foreach (var instr in body.Instructions)
{
if (!IsCall(instr, out var calledMtd))
{
continue;
}
var token = calledMtd.MetadataToken;
if (processedMethods.Contains(token))
{
continue;
}
processedMethods = processedMethods.Add(token);
MethodDefinition mtdDef;
try
{
mtdDef = calledMtd.Resolve();
}
catch
{
logger.LogWarning($"Unable to resolve method reference \"{calledMtd.FullName}\", assuming calls to will return");
mtdDef = null;
}
if (mtdDef == null)
{
continue;
}
if (!mtdDef.HasCustomAttributes)
{
continue;
}
var hasDoesNotReturnAttribute = false;
foreach (var attr in mtdDef.CustomAttributes)
{
if (Array.IndexOf(doesNotReturnAttributes, attr.AttributeType.Name) != -1)
{
hasDoesNotReturnAttribute = true;
break;
}
}
if (hasDoesNotReturnAttribute)
{
logger.LogVerbose($"Determined call to \"{calledMtd.FullName}\" will not return");
doNotReturn.Add(token);
}
}
}
}
var doNoReturnTokens = doNotReturn.ToImmutable();
return new ReachabilityHelper(doNoReturnTokens);
}
/// <summary>
/// Calculates which IL instructions are reachable given an instruction stream and branch points extracted from a method.
///
/// The algorithm works like so:
/// 1. determine the "blocks" that make up a function
/// * A block starts with either the start of the method, or a branch _target_
/// * blocks are "where some other code might jump to"
/// * blocks end with either another branch, another branch target, or the end of the method
/// * this means blocks contain no control flow, except (maybe) as the very last instruction
/// 2. blocks have head and tail reachability
/// * a block has head reachablility if some other block that is reachable can branch to it
/// * a block has tail reachability if it contains no calls to methods that never return
/// 4. push the first block onto a stack
/// 5. while the stack is not empty
/// a. pop a block off the stack
/// b. give it head reachability
/// c. if the pop'd block is tail reachable, push the blocks it can branch to onto the stack
/// 6. consider each block
/// * if it is head and tail reachable, all instructions in it are reachable
/// * if it is not head reachable (regardless of tail reachability), no instructions in it are reachable
/// * if it is only head reachable, all instructions up to and including the first call to a method that does not return are reachable
/// </summary>
public ImmutableArray<UnreachableRange> FindUnreachableIL(Collection<Instruction> instrs, Collection<ExceptionHandler> exceptionHandlers)
{
// no instructions, means nothing to... not reach
if (instrs.Count == 0)
{
return ImmutableArray<UnreachableRange>.Empty;
}
// no known methods that do not return, so everything is reachable by definition
if (DoesNotReturnMethods.IsEmpty)
{
return ImmutableArray<UnreachableRange>.Empty;
}
var (mayContainUnreachableCode, branches) = AnalyzeInstructions(instrs, exceptionHandlers);
// no need to do any more work, nothing unreachable here
if (!mayContainUnreachableCode)
{
return ImmutableArray<UnreachableRange>.Empty;
}
var lastInstr = instrs[instrs.Count - 1];
var blocks = CreateBasicBlocks(instrs, exceptionHandlers, branches);
DetermineHeadReachability(blocks);
return DetermineUnreachableRanges(blocks, lastInstr.Offset);
}
/// <summary>
/// Analyzes the instructiona and exception handlers provided to find branches and determine if
/// it is possible for their to be unreachable code.
/// </summary>
private (bool MayContainUnreachableCode, ImmutableArray<BranchInstruction> Branches) AnalyzeInstructions(Collection<Instruction> instrs, Collection<ExceptionHandler> exceptionHandlers)
{
var containsDoesNotReturnCall = false;
var ret = ImmutableArray.CreateBuilder<BranchInstruction>();
foreach (var i in instrs)
{
containsDoesNotReturnCall = containsDoesNotReturnCall || DoesNotReturn(i);
if (BRANCH_OPCODES.Contains(i.OpCode))
{
var (singleTargetOffset, multiTargetOffsets) = GetInstructionTargets(i, exceptionHandlers);
if (singleTargetOffset != null)
{
ret.Add(new BranchInstruction(i.Offset, singleTargetOffset.Value));
}
else
{
ret.Add(new BranchInstruction(i.Offset, multiTargetOffsets));
}
}
}
return (containsDoesNotReturnCall, ret.ToImmutable());
}
/// <summary>
/// For a single instruction, determines all the places it might branch to.
/// </summary>
private static (int? SingleTargetOffset, ImmutableArray<int> MultiTargetOffsets) GetInstructionTargets(Instruction i, Collection<ExceptionHandler> exceptionHandlers)
{
int? singleTargetOffset;
ImmutableArray<int> multiTargetOffsets;
if (i.Operand is Instruction[] multiTarget)
{
// it's a switch
singleTargetOffset = null;
multiTargetOffsets = ImmutableArray.Create(i.Next.Offset);
foreach (var instr in multiTarget)
{
// in practice these are small arrays, so a scan should be fine
if (multiTargetOffsets.Contains(instr.Offset))
{
continue;
}
multiTargetOffsets = multiTargetOffsets.Add(instr.Offset);
}
}
else if (i.Operand is Instruction targetInstr)
{
// it's any of the B.*(_S)? or Leave(_S)? instructions
if (UNCONDITIONAL_BRANCH_OPCODES.Contains(i.OpCode))
{
multiTargetOffsets = ImmutableArray<int>.Empty;
singleTargetOffset = targetInstr.Offset;
}
else
{
singleTargetOffset = null;
multiTargetOffsets = ImmutableArray.Create(i.Next.Offset, targetInstr.Offset);
}
}
else if (i.OpCode == OpCodes.Endfilter)
{
// Endfilter is always the last instruction in a filter block, and no sort of control
// flow is allowed so we can scan backwards to see find the block
ExceptionHandler filterForHandler = null;
foreach (var handler in exceptionHandlers)
{
if (handler.FilterStart == null)
{
continue;
}
var startsAt = handler.FilterStart;
var cur = startsAt;
while (cur != null && cur.Offset < i.Offset)
{
cur = cur.Next;
}
if (cur != null && cur.Offset == i.Offset)
{
filterForHandler = handler;
break;
}
}
if (filterForHandler == null)
{
throw new InvalidOperationException($"Could not find ExceptionHandler associated with {i}");
}
// filter can do one of two things:
// - branch into handler
// - percolate to another catch block, which might not be in this method
//
// so we chose to model this as an unconditional branch into the handler
singleTargetOffset = filterForHandler.HandlerStart.Offset;
multiTargetOffsets = ImmutableArray<int>.Empty;
}
else if (i.OpCode == OpCodes.Endfinally)
{
// Endfinally is very weird
//
// what it does, effectively is "take whatever branch would normally happen after the instruction
// that left the paired try
//
// practically, this makes endfinally a branch with no target
singleTargetOffset = null;
multiTargetOffsets = ImmutableArray<int>.Empty;
}
else
{
throw new InvalidOperationException($"Unexpected operand when processing branch {i}");
}
return (singleTargetOffset, multiTargetOffsets);
}
/// <summary>
/// Calculates which ranges of IL are unreachable, given blocks which have head and tail reachability calculated.
/// </summary>
private ImmutableArray<UnreachableRange> DetermineUnreachableRanges(ImmutableArray<BasicBlock> blocks, int lastInstructionOffset)
{
var ret = ImmutableArray.CreateBuilder<UnreachableRange>();
var endOfMethodOffset = lastInstructionOffset + 1; // add 1 so we point _past_ the end of the method
for (var curBlockIx = 0; curBlockIx < blocks.Length; curBlockIx++)
{
var curBlock = blocks[curBlockIx];
int endOfCurBlockOffset;
if (curBlockIx == blocks.Length - 1)
{
endOfCurBlockOffset = endOfMethodOffset;
}
else
{
endOfCurBlockOffset = blocks[curBlockIx + 1].StartOffset - 1; // minus 1 so we don't include anything of the following block
}
if (curBlock.HeadReachable)
{
if (curBlock.TailReachable)
{
// it's all reachable
continue;
}
// tail isn't reachable, which means there's a call to something that doesn't return...
var doesNotReturnInstr = curBlock.UnreachableAfter;
// and it's everything _after_ the following instruction that is unreachable
// so record the following instruction through the end of the block
var followingInstr = doesNotReturnInstr.Next;
ret.Add(new UnreachableRange(followingInstr.Offset, endOfCurBlockOffset));
}
else
{
// none of it is reachable
ret.Add(new UnreachableRange(curBlock.StartOffset, endOfCurBlockOffset));
}
}
return ret.ToImmutable();
}
/// <summary>
/// Process all the blocks and determine if their first instruction is reachable,
/// that is if they have "head reachability".
///
/// "Tail reachability" will have already been determined in CreateBlocks.
/// </summary>
private void DetermineHeadReachability(ImmutableArray<BasicBlock> blocks)
{
var blockLookup = blocks.ToImmutableDictionary(b => b.StartOffset);
var headBlock = blockLookup[0];
var knownLive = ImmutableStack.Create(headBlock);
while (!knownLive.IsEmpty)
{
knownLive = knownLive.Pop(out var block);
if (block.HeadReachable)
{
// already seen this block
continue;
}
// we can reach this block, clearly
block.HeadReachable = true;
if (block.TailReachable)
{
// we can reach all the blocks it might flow to
foreach (var reachableOffset in block.BranchesTo)
{
var reachableBlock = blockLookup[reachableOffset];
knownLive = knownLive.Push(reachableBlock);
}
}
// if the block is covered by an exception handler, then executing _any_ instruction in it
// could conceivably cause those handlers to be visited
foreach (var exceptionHandlerOffset in block.ExceptionBranchesTo)
{
var reachableHandler = blockLookup[exceptionHandlerOffset];
knownLive = knownLive.Push(reachableHandler);
}
}
}
/// <summary>
/// Create BasicBlocks from an instruction stream, exception blocks, and branches.
///
/// Each block starts either at the start of the method, immediately after a branch or at a target for a branch,
/// and ends with another branch, another branch target, or the end of the method.
///
/// "Tail reachability" is also calculated, which is whether the block can ever actually get past its last instruction.
/// </summary>
private ImmutableArray<BasicBlock> CreateBasicBlocks(Collection<Instruction> instrs, Collection<ExceptionHandler> exceptionHandlers, ImmutableArray<BranchInstruction> branches)
{
// every branch-like instruction starts or stops a block
var branchInstrLocs = branches.ToLookup(i => i.Offset);
var branchInstrOffsets = branchInstrLocs.Select(k => k.Key).ToImmutableHashSet();
// every target that might be branched to starts or stops a block
var branchTargetOffsetsBuilder = ImmutableHashSet.CreateBuilder<int>();
foreach (var branch in branches)
{
if (branch.HasMultiTargets)
{
foreach (var target in branch.TargetOffsets)
{
branchTargetOffsetsBuilder.Add(target);
}
}
else
{
branchTargetOffsetsBuilder.Add(branch.TargetOffset);
}
}
// every exception handler an entry point
// either it's handler, or it's filter (if present)
foreach (var handler in exceptionHandlers)
{
if (handler.FilterStart != null)
{
branchTargetOffsetsBuilder.Add(handler.FilterStart.Offset);
}
else
{
branchTargetOffsetsBuilder.Add(handler.HandlerStart.Offset);
}
}
var branchTargetOffsets = branchTargetOffsetsBuilder.ToImmutable();
// ending the method is also important
var endOfMethodOffset = instrs[instrs.Count - 1].Offset;
var blocks = ImmutableArray<BasicBlock>.Empty;
int? blockStartedAt = null;
Instruction unreachableAfter = null;
foreach (var i in instrs)
{
var offset = i.Offset;
var branchesAtLoc = branchInstrLocs[offset];
if (blockStartedAt == null)
{
blockStartedAt = offset;
unreachableAfter = null;
}
var isBranch = branchInstrOffsets.Contains(offset);
var isFollowedByBranchTarget = i.Next != null && branchTargetOffsets.Contains(i.Next.Offset);
var isEndOfMtd = endOfMethodOffset == offset;
if (unreachableAfter == null && DoesNotReturn(i))
{
unreachableAfter = i;
}
var blockEnds = isBranch || isFollowedByBranchTarget || isEndOfMtd;
if (blockEnds)
{
var nextInstr = i.Next;
// figure out all the different places the basic block could lead to
ImmutableArray<int> goesTo;
if (branchesAtLoc.Any())
{
// it ends in a branch, where all does it branch?
goesTo = ImmutableArray<int>.Empty;
foreach (var branch in branchesAtLoc)
{
if (branch.HasMultiTargets)
{
goesTo = goesTo.AddRange(branch.TargetOffsets);
}
else
{
goesTo = goesTo.Add(branch.TargetOffset);
}
}
}
else if (nextInstr != null)
{
// it falls throw to another instruction
goesTo = ImmutableArray.Create(nextInstr.Offset);
}
else
{
// it ends the method
goesTo = ImmutableArray<int>.Empty;
}
var exceptionSwitchesTo = ImmutableArray<int>.Empty;
// if the block is covered by any exception handlers then
// it is possible that it will branch to its handler block
foreach (var handler in exceptionHandlers)
{
var tryStart = handler.TryStart.Offset;
var tryEnd = handler.TryEnd.Offset;
var containsStartOfTry =
tryStart >= blockStartedAt.Value &&
tryStart <= i.Offset;
var containsEndOfTry =
tryEnd >= blockStartedAt.Value &&
tryEnd <= i.Offset;
var blockInsideTry = blockStartedAt.Value >= tryStart && i.Offset <= tryEnd;
// blocks do not necessarily align to the TRY part of exception handlers, so we need to handle three cases:
// - the try _starts_ in the block
// - the try _ends_ in the block
// - the try complete covers the block, but starts and ends before and after it (respectively)
var tryOverlapsBlock = containsStartOfTry || containsEndOfTry || blockInsideTry;
if (!tryOverlapsBlock)
{
continue;
}
// if there's a filter, that runs first
if (handler.FilterStart != null)
{
exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.FilterStart.Offset);
}
else
{
// otherwise, go straight to the handler
exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.HandlerStart.Offset);
}
}
blocks = blocks.Add(new BasicBlock(blockStartedAt.Value, unreachableAfter, goesTo, exceptionSwitchesTo));
blockStartedAt = null;
unreachableAfter = null;
}
}
return blocks;
}
/// <summary>
/// Returns true if the given instruction will never return,
/// and thus subsequent instructions will never be run.
/// </summary>
private bool DoesNotReturn(Instruction instr)
{
if (!IsCall(instr, out var mtd))
{
return false;
}
return DoesNotReturnMethods.Contains(mtd.MetadataToken);
}
/// <summary>
/// Returns true if the given instruction is a Call or Callvirt.
///
/// If it is a call, extracts the MethodReference that is being called.
/// </summary>
private static bool IsCall(Instruction instr, out MethodReference mtd)
{
var opcode = instr.OpCode;
if (opcode != OpCodes.Call && opcode != OpCodes.Callvirt)
{
mtd = null;
return false;
}
mtd = (MethodReference)instr.Operand;
return true;
}
}
}
@@ -40,6 +40,8 @@ namespace Coverlet.MSbuild.Tasks
public bool SkipAutoProps { get; set; }
public string DoesNotReturnAttribute { get; set; }
[Output]
public ITaskItem InstrumenterState { get; set; }
@@ -98,7 +100,8 @@ namespace Coverlet.MSbuild.Tasks
SingleHit = SingleHit,
MergeWith = MergeWith,
UseSourceLink = UseSourceLink,
SkipAutoProps = SkipAutoProps
SkipAutoProps = SkipAutoProps,
DoesNotReturnAttributes = DoesNotReturnAttribute?.Split(',')
};
Coverage coverage = new Coverage(Path,
@@ -39,7 +39,8 @@
SingleHit="$(SingleHit)"
MergeWith="$(MergeWith)"
UseSourceLink="$(UseSourceLink)"
SkipAutoProps="$(SkipAutoProps)" >
SkipAutoProps="$(SkipAutoProps)"
DoesNotReturnAttribute="$(DoesNotReturnAttribute)">
<Output TaskParameter="InstrumenterState" PropertyName="InstrumenterState"/>
</Coverlet.MSbuild.Tasks.InstrumentationTask>
</Target>
@@ -43,23 +43,24 @@ namespace Coverlet.Collector.Tests
}
[Theory]
[InlineData("[*]*,[coverlet]*", "[coverlet.*.tests?]*,[coverlet.*.tests.*]*", @"E:\temp,/var/tmp", "module1.cs,module2.cs", "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute")]
[InlineData("[*]*,,[coverlet]*", "[coverlet.*.tests?]*,,[coverlet.*.tests.*]*", @"E:\temp,,/var/tmp", "module1.cs,,module2.cs", "Obsolete,,GeneratedCodeAttribute,,CompilerGeneratedAttribute")]
[InlineData("[*]*, ,[coverlet]*", "[coverlet.*.tests?]*, ,[coverlet.*.tests.*]*", @"E:\temp, ,/var/tmp", "module1.cs, ,module2.cs", "Obsolete, ,GeneratedCodeAttribute, ,CompilerGeneratedAttribute")]
[InlineData("[*]*,\t,[coverlet]*", "[coverlet.*.tests?]*,\t,[coverlet.*.tests.*]*", "E:\\temp,\t,/var/tmp", "module1.cs,\t,module2.cs", "Obsolete,\t,GeneratedCodeAttribute,\t,CompilerGeneratedAttribute")]
[InlineData("[*]*, [coverlet]*", "[coverlet.*.tests?]*, [coverlet.*.tests.*]*", @"E:\temp, /var/tmp", "module1.cs, module2.cs", "Obsolete, GeneratedCodeAttribute, CompilerGeneratedAttribute")]
[InlineData("[*]*,\t[coverlet]*", "[coverlet.*.tests?]*,\t[coverlet.*.tests.*]*", "E:\\temp,\t/var/tmp", "module1.cs,\tmodule2.cs", "Obsolete,\tGeneratedCodeAttribute,\tCompilerGeneratedAttribute")]
[InlineData("[*]*, \t[coverlet]*", "[coverlet.*.tests?]*, \t[coverlet.*.tests.*]*", "E:\\temp, \t/var/tmp", "module1.cs, \tmodule2.cs", "Obsolete, \tGeneratedCodeAttribute, \tCompilerGeneratedAttribute")]
[InlineData("[*]*,\r\n[coverlet]*", "[coverlet.*.tests?]*,\r\n[coverlet.*.tests.*]*", "E:\\temp,\r\n/var/tmp", "module1.cs,\r\nmodule2.cs", "Obsolete,\r\nGeneratedCodeAttribute,\r\nCompilerGeneratedAttribute")]
[InlineData("[*]*, \r\n [coverlet]*", "[coverlet.*.tests?]*, \r\n [coverlet.*.tests.*]*", "E:\\temp, \r\n /var/tmp", "module1.cs, \r\n module2.cs", "Obsolete, \r\n GeneratedCodeAttribute, \r\n CompilerGeneratedAttribute")]
[InlineData("[*]*,\t\r\n\t[coverlet]*", "[coverlet.*.tests?]*,\t\r\n\t[coverlet.*.tests.*]*", "E:\\temp,\t\r\n\t/var/tmp", "module1.cs,\t\r\n\tmodule2.cs", "Obsolete,\t\r\n\tGeneratedCodeAttribute,\t\r\n\tCompilerGeneratedAttribute")]
[InlineData("[*]*, \t \r\n \t [coverlet]*", "[coverlet.*.tests?]*, \t \r\n \t [coverlet.*.tests.*]*", "E:\\temp, \t \r\n \t /var/tmp", "module1.cs, \t \r\n \t module2.cs", "Obsolete, \t \r\n \t GeneratedCodeAttribute, \t \r\n \t CompilerGeneratedAttribute")]
[InlineData(" [*]* , [coverlet]* ", " [coverlet.*.tests?]* , [coverlet.*.tests.*]* ", " E:\\temp , /var/tmp ", " module1.cs , module2.cs ", " Obsolete , GeneratedCodeAttribute , CompilerGeneratedAttribute ")]
[InlineData("[*]*,[coverlet]*", "[coverlet.*.tests?]*,[coverlet.*.tests.*]*", @"E:\temp,/var/tmp", "module1.cs,module2.cs", "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute", "DoesNotReturnAttribute,ThrowsAttribute")]
[InlineData("[*]*,,[coverlet]*", "[coverlet.*.tests?]*,,[coverlet.*.tests.*]*", @"E:\temp,,/var/tmp", "module1.cs,,module2.cs", "Obsolete,,GeneratedCodeAttribute,,CompilerGeneratedAttribute", "DoesNotReturnAttribute,,ThrowsAttribute")]
[InlineData("[*]*, ,[coverlet]*", "[coverlet.*.tests?]*, ,[coverlet.*.tests.*]*", @"E:\temp, ,/var/tmp", "module1.cs, ,module2.cs", "Obsolete, ,GeneratedCodeAttribute, ,CompilerGeneratedAttribute", "DoesNotReturnAttribute, ,ThrowsAttribute")]
[InlineData("[*]*,\t,[coverlet]*", "[coverlet.*.tests?]*,\t,[coverlet.*.tests.*]*", "E:\\temp,\t,/var/tmp", "module1.cs,\t,module2.cs", "Obsolete,\t,GeneratedCodeAttribute,\t,CompilerGeneratedAttribute", "DoesNotReturnAttribute,\t,ThrowsAttribute")]
[InlineData("[*]*, [coverlet]*", "[coverlet.*.tests?]*, [coverlet.*.tests.*]*", @"E:\temp, /var/tmp", "module1.cs, module2.cs", "Obsolete, GeneratedCodeAttribute, CompilerGeneratedAttribute", "DoesNotReturnAttribute, ThrowsAttribute")]
[InlineData("[*]*,\t[coverlet]*", "[coverlet.*.tests?]*,\t[coverlet.*.tests.*]*", "E:\\temp,\t/var/tmp", "module1.cs,\tmodule2.cs", "Obsolete,\tGeneratedCodeAttribute,\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\tThrowsAttribute")]
[InlineData("[*]*, \t[coverlet]*", "[coverlet.*.tests?]*, \t[coverlet.*.tests.*]*", "E:\\temp, \t/var/tmp", "module1.cs, \tmodule2.cs", "Obsolete, \tGeneratedCodeAttribute, \tCompilerGeneratedAttribute", "DoesNotReturnAttribute, \tThrowsAttribute")]
[InlineData("[*]*,\r\n[coverlet]*", "[coverlet.*.tests?]*,\r\n[coverlet.*.tests.*]*", "E:\\temp,\r\n/var/tmp", "module1.cs,\r\nmodule2.cs", "Obsolete,\r\nGeneratedCodeAttribute,\r\nCompilerGeneratedAttribute", "DoesNotReturnAttribute,\r\nThrowsAttribute")]
[InlineData("[*]*, \r\n [coverlet]*", "[coverlet.*.tests?]*, \r\n [coverlet.*.tests.*]*", "E:\\temp, \r\n /var/tmp", "module1.cs, \r\n module2.cs", "Obsolete, \r\n GeneratedCodeAttribute, \r\n CompilerGeneratedAttribute", "DoesNotReturnAttribute, \r\n ThrowsAttribute")]
[InlineData("[*]*,\t\r\n\t[coverlet]*", "[coverlet.*.tests?]*,\t\r\n\t[coverlet.*.tests.*]*", "E:\\temp,\t\r\n\t/var/tmp", "module1.cs,\t\r\n\tmodule2.cs", "Obsolete,\t\r\n\tGeneratedCodeAttribute,\t\r\n\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\t\r\n\tThrowsAttribute")]
[InlineData("[*]*, \t \r\n \t [coverlet]*", "[coverlet.*.tests?]*, \t \r\n \t [coverlet.*.tests.*]*", "E:\\temp, \t \r\n \t /var/tmp", "module1.cs, \t \r\n \t module2.cs", "Obsolete, \t \r\n \t GeneratedCodeAttribute, \t \r\n \t CompilerGeneratedAttribute", "DoesNotReturnAttribute, \t \r\n \t ThrowsAttribute")]
[InlineData(" [*]* , [coverlet]* ", " [coverlet.*.tests?]* , [coverlet.*.tests.*]* ", " E:\\temp , /var/tmp ", " module1.cs , module2.cs ", " Obsolete , GeneratedCodeAttribute , CompilerGeneratedAttribute ", "DoesNotReturnAttribute , ThrowsAttribute")]
public void ParseShouldCorrectlyParseConfigurationElement(string includeFilters,
string excludeFilters,
string includeDirectories,
string excludeSourceFiles,
string excludeAttributes)
string excludeAttributes,
string doesNotReturnAttributes)
{
var testModules = new List<string> { "abc.dll" };
var doc = new XmlDocument();
@@ -74,6 +75,7 @@ namespace Coverlet.Collector.Tests
this.CreateCoverletNodes(doc, configElement, CoverletConstants.SingleHitElementName, "true");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeTestAssemblyElementName, "true");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.SkipAutoProps, "true");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.DoesNotReturnAttributesElementName, doesNotReturnAttributes);
CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules);
@@ -91,6 +93,8 @@ namespace Coverlet.Collector.Tests
Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]);
Assert.Equal("[coverlet.*.tests?]*", coverletSettings.ExcludeFilters[1]);
Assert.Equal("[coverlet.*.tests.*]*", coverletSettings.ExcludeFilters[2]);
Assert.Equal("DoesNotReturnAttribute", coverletSettings.DoesNotReturnAttributes[0]);
Assert.Equal("ThrowsAttribute", coverletSettings.DoesNotReturnAttributes[1]);
Assert.False(coverletSettings.UseSourceLink);
Assert.True(coverletSettings.SingleHit);
@@ -336,9 +336,54 @@ namespace Coverlet.Core.Tests
int[] lineRange = Enumerable.Range(from, to - from + 1).ToArray();
if (document.Lines.Select(l => l.Value.Number).Intersect(lineRange).Count() > 0)
return AssertNonInstrumentedLines(document, configuration, lineRange);
}
public static Document AssertNonInstrumentedLines(this Document document, BuildConfiguration configuration, params int[] lines)
{
if (document is null)
{
throw new XunitException($"Unexpected instrumented lines, '{string.Join(',', lineRange)}'");
throw new ArgumentNullException(nameof(document));
}
BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration();
if ((buildConfiguration & configuration) != buildConfiguration)
{
return document;
}
var unexpectedlyInstrumented = document.Lines.Select(l => l.Value.Number).Intersect(lines);
if (unexpectedlyInstrumented.Any())
{
throw new XunitException($"Unexpected instrumented lines, '{string.Join(',', unexpectedlyInstrumented)}'");
}
return document;
}
public static Document AssertInstrumentLines(this Document document, BuildConfiguration configuration, params int[] lines)
{
if (document is null)
{
throw new ArgumentNullException(nameof(document));
}
BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration();
if ((buildConfiguration & configuration) != buildConfiguration)
{
return document;
}
var instrumentedLines = document.Lines.Select(l => l.Value.Number).ToHashSet();
var missing = lines.Where(l => !instrumentedLines.Contains(l));
if (missing.Any())
{
throw new XunitException($"Expected lines to be instrumented, '{string.Join(',', missing)}'");
}
return document;
@@ -18,6 +18,7 @@ using Moq;
using Xunit;
using Microsoft.Extensions.DependencyModel;
using Microsoft.VisualStudio.TestPlatform;
using Coverlet.Core.Tests;
namespace Coverlet.Core.Instrumentation.Tests
{
@@ -77,7 +78,7 @@ namespace Coverlet.Core.Instrumentation.Tests
InstrumentationHelper instrumentationHelper =
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object, _mockLogger.Object, sourceRootTranslator);
Instrumenter instrumenter = new Instrumenter(Path.Combine(directory.FullName, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, _mockLogger.Object, instrumentationHelper, partialMockFileSystem.Object, sourceRootTranslator, new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, _mockLogger.Object, instrumentationHelper, partialMockFileSystem.Object, sourceRootTranslator, new CecilSymbolHelper());
Assert.True(instrumenter.CanInstrument());
InstrumenterResult result = instrumenter.Instrument();
@@ -242,7 +243,7 @@ namespace Coverlet.Core.Instrumentation.Tests
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock<ILogger>().Object, new SourceRootTranslator(new Mock<ILogger>().Object, new FileSystem()));
module = Path.Combine(directory.FullName, destModule);
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore, false, false,
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore, Array.Empty<string>(), false, false,
_mockLogger.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem()), new CecilSymbolHelper());
return new InstrumenterTest
{
@@ -420,7 +421,7 @@ namespace Coverlet.Core.Instrumentation.Tests
new SourceRootTranslator(xunitDll, new Mock<ILogger>().Object, new FileSystem()));
Instrumenter instrumenter = new Instrumenter(xunitDll, "_xunit_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(xunitDll, loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(xunitDll, loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Assert.True(instrumentationHelper.HasPdb(xunitDll, out bool embedded));
Assert.True(embedded);
Assert.False(instrumenter.CanInstrument());
@@ -433,7 +434,7 @@ namespace Coverlet.Core.Instrumentation.Tests
new SourceRootTranslator(sample, new Mock<ILogger>().Object, new FileSystem()));
instrumenter = new Instrumenter(sample, "_coverlet_tests_projectsample_empty", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(sample, loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(sample, loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Assert.True(instrumentationHelper.HasPdb(sample, out embedded));
Assert.False(embedded);
@@ -479,7 +480,8 @@ namespace Coverlet.Core.Instrumentation.Tests
string sample = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), dllFileName).First();
var loggerMock = new Mock<ILogger>();
Instrumenter instrumenter = new Instrumenter(sample, "_75d9f96508d74def860a568f426ea4a4_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Assert.True(instrumentationHelper.HasPdb(sample, out bool embedded));
Assert.False(embedded);
Assert.False(instrumenter.CanInstrument());
@@ -496,7 +498,8 @@ namespace Coverlet.Core.Instrumentation.Tests
new SourceRootTranslator(new Mock<ILogger>().Object, new FileSystem()));
var instrumenter = new Instrumenter("test", "_test_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Assert.False(instrumenter.CanInstrument());
loggerMock.Verify(l => l.LogWarning(It.IsAny<string>()));
}
@@ -519,7 +522,8 @@ namespace Coverlet.Core.Instrumentation.Tests
new SourceRootTranslator(new Mock<ILogger>().Object, new FileSystem()));
Instrumenter instrumenter = new Instrumenter(excludedbyattributeDll, "_xunit_excludedbyattribute", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(),
Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
Array.Empty<string>(), Array.Empty<string>(), false, false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper());
InstrumenterResult result = instrumenter.Instrument();
Assert.Empty(result.Documents);
loggerMock.Verify(l => l.LogVerbose(It.IsAny<string>()));
@@ -638,5 +642,73 @@ namespace Coverlet.Core.Instrumentation.Tests
instrumenterTest.Directory.Delete(true);
}
[Fact]
public void TestReachabilityHelper()
{
var allInstrumentableLines =
new[]
{
// Throws
7, 8,
// NoBranches
12, 13, 14, 15, 16,
// If
19, 20, 22, 23, 24, 25, 26, 27, 29, 30,
// Switch
33, 34, 36, 39, 40, 41, 42, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69,
// Subtle
72, 73, 75, 78, 79, 80, 82, 83, 86, 87, 88, 91, 92, 95, 96, 98, 99, 101, 102, 103,
// UnreachableBranch
106, 107, 108, 110, 111, 112, 113, 114,
// ThrowsGeneric
118, 119,
// CallsGenericMethodDoesNotReturn
124, 125, 126, 127, 128,
// AlsoThrows
134, 135,
// CallsGenericClassDoesNotReturn
140, 141, 142, 143, 144,
// WithLeave
147, 149, 150, 151, 152, 153, 154, 155, 156, 159, 161, 163, 166, 167, 168,
// FiltersAndFinallies
171, 173, 174, 175, 176, 177, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 192, 193, 194, 195, 196, 197
};
var notReachableLines =
new[]
{
// NoBranches
15, 16,
// If
26, 27,
// Switch
41, 42,
// Subtle
79, 80, 88, 96, 98, 99,
// UnreachableBranch
110, 111, 112, 113, 114,
// CallsGenericMethodDoesNotReturn
127, 128,
// CallsGenericClassDoesNotReturn
143, 144,
// WithLeave
163, 164,
// FiltersAndFinallies
176, 177, 183, 184, 189, 190, 195, 196, 197
};
var expectedToBeInstrumented = allInstrumentableLines.Except(notReachableLines).ToArray();
var instrumenterTest = CreateInstrumentor();
var result = instrumenterTest.Instrumenter.Instrument();
var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Instrumentation.DoesNotReturn.cs");
// check for instrumented lines
doc.AssertNonInstrumentedLines(BuildConfiguration.Debug, notReachableLines);
doc.AssertInstrumentLines(BuildConfiguration.Debug, expectedToBeInstrumented);
instrumenterTest.Directory.Delete(true);
}
}
}
@@ -0,0 +1,199 @@
namespace Coverlet.Core.Samples.Tests
{
public class DoesNotReturn
{
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public int Throws()
{
throw new System.Exception();
}
public void NoBranches()
{
System.Console.WriteLine("Before");
Throws();
System.Console.WriteLine("After"); // unreachable
} // unreachable
public void If()
{
System.Console.WriteLine("In-After");
if (System.Console.ReadKey().KeyChar == 'Y')
{
System.Console.WriteLine("In-Before");
Throws();
System.Console.WriteLine("In-After"); // unreachable
} // unreachable
System.Console.WriteLine("Out-After");
}
public void Switch()
{
System.Console.WriteLine("Out-Before");
switch (System.Console.ReadKey().KeyChar)
{
case 'A':
System.Console.WriteLine("In-Before");
Throws();
System.Console.WriteLine("In-After"); // should be unreachable
break; // should be unreachable
case 'B':
System.Console.WriteLine("In-Constant-1");
break;
// need a number of additional, in order, branches to get a Switch generated
case 'C':
System.Console.WriteLine("In-Constant-2");
break;
case 'D':
System.Console.WriteLine("In-Constant-3");
break;
case 'E':
System.Console.WriteLine("In-Constant-4");
break;
case 'F':
System.Console.WriteLine("In-Constant-5");
break;
case 'G':
System.Console.WriteLine("In-Constant-6");
break;
case 'H':
System.Console.WriteLine("In-Constant-7");
break;
}
System.Console.WriteLine("Out-After");
}
public void Subtle()
{
var key = System.Console.ReadKey();
switch (key.KeyChar)
{
case 'A':
Throws();
System.Console.WriteLine("In-Constant-1"); // unreachable
goto case 'B'; // unreachable
case 'B':
System.Console.WriteLine("In-Constant-2");
break;
case 'C':
System.Console.WriteLine("In-Constant-3");
Throws();
goto alwayUnreachable; // unreachable
case 'D':
System.Console.WriteLine("In-Constant-4");
goto subtlyReachable;
}
Throws();
System.Console.WriteLine("Out-Constant-1"); // unreachable
alwayUnreachable: // unreachable
System.Console.WriteLine("Out-Constant-2"); // unreachable
subtlyReachable:
System.Console.WriteLine("Out-Constant-3");
}
public void UnreachableBranch()
{
var key = System.Console.ReadKey();
Throws();
if (key.KeyChar == 'A') // unreachable
{ // unreachable
System.Console.WriteLine("Constant-1"); // unreachable
} // unreachable
} // unreachable
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public void ThrowsGeneric<T>()
{
throw new System.Exception(typeof(T).Name);
}
public void CallsGenericMethodDoesNotReturn()
{
System.Console.WriteLine("Constant-1");
ThrowsGeneric<string>();
System.Console.WriteLine("Constant-2"); // unreachable
}
private class GenericClass<T>
{
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void AlsoThrows()
{
throw new System.Exception(typeof(T).Name);
}
}
public void CallsGenericClassDoesNotReturn()
{
System.Console.WriteLine("Constant-1");
GenericClass<int>.AlsoThrows();
System.Console.WriteLine("Constant-2"); // unreachable
}
public void WithLeave()
{
try
{
System.Console.WriteLine("Constant-1");
}
catch (System.Exception e)
{
if (e is System.InvalidOperationException)
{
goto endOfMethod;
}
System.Console.WriteLine("InCatch-1");
Throws();
System.Console.WriteLine("InCatch-2"); // unreachable
} // unreachable
endOfMethod:
System.Console.WriteLine("Constant-2");
}
public void FiltersAndFinallies()
{
try
{
System.Console.WriteLine("Constant-1");
Throws();
System.Console.WriteLine("Constant-2"); //unreachable
} //unreachable
catch (System.InvalidOperationException e)
when (e.Message != null)
{
System.Console.WriteLine("InCatch-1");
Throws();
System.Console.WriteLine("InCatch-2"); //unreachable
} //unreachable
catch (System.InvalidOperationException)
{
System.Console.WriteLine("InCatch-3");
Throws();
System.Console.WriteLine("InCatch-4"); //unreachable
} //unreachable
finally
{
System.Console.WriteLine("InFinally-1");
Throws();
System.Console.WriteLine("InFinally-2"); //unreachable
} //unreachable
} //unreachable
}
}