Skip to content

Commit

Permalink
fix anonymous delegate
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoRossignoli committed May 8, 2019
1 parent 6fec36d commit 00435c2
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 104 deletions.
144 changes: 107 additions & 37 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,63 @@ public CoverageResult GetCoverageResult()
InstrumentationHelper.RestoreOriginalModule(result.ModulePath, _identifier);
}

// In case of anonymous delegate compiler generate a custom class and passes it as type.method delegate.
// If in delegate method we've a branches we need to move these to "actual" class/method that use it.
// We search "method" with same "Line" of closure class method and add missing branches to it,
// in this way we correctly report missing branch inside compiled generated anonymous delegate.
List<string> compileGeneratedClassToRemove = null;
foreach (var module in modules)
{
foreach (var document in module.Value)
{
foreach (var @class in document.Value)
{
foreach (var method in @class.Value)
{
foreach (var branch in method.Value.Branches)
{
if (BranchInCompilerGeneratedClass(method.Key))
{
Method actualMethod = GetMethodWithSameLineInSameDocument(document.Value, @class.Key, branch.Line);

if (actualMethod is null)
{
continue;
}

actualMethod.Branches.Add(branch);

if (compileGeneratedClassToRemove is null)
{
compileGeneratedClassToRemove = new List<string>();
}

if (!compileGeneratedClassToRemove.Contains(@class.Key))
{
compileGeneratedClassToRemove.Add(@class.Key);
}
}
}
}
}
}
}

// After method/branches analysis of compiled generated class we can remove noise from reports
if (!(compileGeneratedClassToRemove is null))
{
foreach (var module in modules)
{
foreach (var document in module.Value)
{
foreach (var classToRemove in compileGeneratedClassToRemove)
{
document.Value.Remove(classToRemove);
}
}
}
}

var coverageResult = new CoverageResult { Identifier = _identifier, Modules = modules, InstrumentedResults = _results };

if (!string.IsNullOrEmpty(_mergeWith) && !string.IsNullOrWhiteSpace(_mergeWith) && File.Exists(_mergeWith))
Expand All @@ -200,6 +257,41 @@ public CoverageResult GetCoverageResult()
return coverageResult;
}

private Method GetMethodWithSameLineInSameDocument(Classes documentClasses, string compilerGeneratedClassName, int branchLine)
{
foreach (var @class in documentClasses)
{
if (@class.Key == compilerGeneratedClassName)
{
continue;
}

foreach (var method in @class.Value)
{
foreach (var line in method.Value.Lines)
{
if (line.Key == branchLine)
{
return method.Value;
}
}
}
}
return null;
}

private bool BranchInCompilerGeneratedClass(string methodName)
{
foreach (var instrumentedResult in _results)
{
if (instrumentedResult.BranchesInCompiledGeneratedClass.Contains(methodName))
{
return true;
}
}
return false;
}

private void CalculateCoverage()
{
foreach (var result in _results)
Expand All @@ -221,15 +313,17 @@ private void CalculateCoverage()
}
}

List<(int docIndex, int line)> zeroHitsLines = new List<(int docIndex, int line)>();

var documentsList = result.Documents.Values.ToList();

using (var fs = new FileStream(result.HitsFilePath, FileMode.Open))
using (var br = new BinaryReader(fs))
{
int hitCandidatesCount = br.ReadInt32();

// TODO: hitCandidatesCount should be verified against result.HitCandidates.Count

var documentsList = result.Documents.Values.ToList();

for (int i = 0; i < hitCandidatesCount; ++i)
{
var hitLocation = result.HitCandidates[i];
Expand All @@ -247,57 +341,33 @@ private void CalculateCoverage()
{
var line = document.Lines[j];
line.Hits += hits;

// We register 0 hit lines for later cleanup false positive
if (hits == 0)
{
zeroHitsLines.Add((hitLocation.docIndex, line.Number));
}
}
}
}
}

// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
// we'll remove all MoveNext() not covered branch
foreach (var document in result.Documents)
// Cleanup nested state machine false positive hits
foreach (var (docIndex, line) in zeroHitsLines)
{
List<KeyValuePair<(int, int), Branch>> branchesToRemove = new List<KeyValuePair<(int, int), Branch>>();
foreach (var branch in document.Value.Branches)
foreach (var lineToCheck in documentsList[docIndex].Lines)
{
//if one branch is covered we search the other one only if it's not covered
if (IsAsyncStateMachineMethod(branch.Value.Method) && branch.Value.Hits > 0)
if (lineToCheck.Key == line)
{
foreach (var moveNextBranch in document.Value.Branches)
{
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
{
branchesToRemove.Add(moveNextBranch);
}
}
lineToCheck.Value.Hits = 0;
}
}
foreach (var branchToRemove in branchesToRemove)
{
document.Value.Branches.Remove(branchToRemove.Key);
}
}

InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
}
}

private bool IsAsyncStateMachineMethod(string method)
{
if (!method.EndsWith("::MoveNext()"))
{
return false;
}

foreach (var instrumentationResult in _results)
{
if (instrumentationResult.AsyncMachineStateMethod.Contains(method))
{
return true;
}
}
return false;
}

private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments, string document)
{
if (sourceLinkDocuments.TryGetValue(document, out string url))
Expand Down
51 changes: 0 additions & 51 deletions src/coverlet.core/CoverageResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,57 +108,6 @@ internal void Merge(Modules modules)
}
}
}

// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
// we'll remove all MoveNext() not covered branch
List<BranchInfo> branchesToRemove = new List<BranchInfo>();
foreach (var module in this.Modules)
{
foreach (var document in module.Value)
{
foreach (var @class in document.Value)
{
foreach (var method in @class.Value)
{
foreach (var branch in method.Value.Branches)
{
//if one branch is covered we search the other one only if it's not covered
if (IsAsyncStateMachineMethod(method.Key) && branch.Hits > 0)
{
foreach (var moveNextBranch in method.Value.Branches)
{
if (moveNextBranch != branch && moveNextBranch.Hits == 0)
{
branchesToRemove.Add(moveNextBranch);
}
}
}
}
foreach (var branchToRemove in branchesToRemove)
{
method.Value.Branches.Remove(branchToRemove);
}
}
}
}
}
}

private bool IsAsyncStateMachineMethod(string method)
{
if (!method.EndsWith("::MoveNext()"))
{
return false;
}

foreach (var instrumentedResult in InstrumentedResults)
{
if (instrumentedResult.AsyncMachineStateMethod.Contains(method))
{
return true;
}
}
return false;
}

public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summary, double threshold, ThresholdTypeFlags thresholdTypes, ThresholdStatistic thresholdStat)
Expand Down
Loading

0 comments on commit 00435c2

Please sign in to comment.