From 13ace748d2bccd20c612ad5a75bf1efff9cbc762 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Tue, 1 May 2018 21:25:06 -0400 Subject: [PATCH 1/3] add better branch support, change coverage calculation to be on actual points instead of averages --- .gitignore | 1 + src/coverlet.core/Coverage.cs | 104 +++++-- src/coverlet.core/CoverageResult.cs | 22 +- src/coverlet.core/CoverageSummary.cs | 255 +++++++++------ src/coverlet.core/CoverageTracker.cs | 1 + .../Extensions/HelperExtensions.cs | 16 + .../Instrumentation/Instrumenter.cs | 95 ++++-- .../Instrumentation/InstrumenterResult.cs | 16 +- .../Reporters/CoberturaReporter.cs | 61 ++-- src/coverlet.core/Reporters/LcovReporter.cs | 54 ++-- .../Reporters/OpenCoverReporter.cs | 108 ++++--- src/coverlet.core/Symbols/BranchPoint.cs | 47 +++ .../Symbols/CecilSymbolHelper.cs | 290 ++++++++++++++++++ .../CoverageResultTask.cs | 7 +- .../CoverageSummaryTests.cs | 36 ++- test/coverlet.core.tests/CoverageTests.cs | 18 +- .../InstrumenterResultTests.cs | 3 +- .../Instrumentation/InstrumenterTests.cs | 2 +- .../Reporters/CoberturaReporterTests.cs | 11 +- .../Reporters/JsonReporterTests.cs | 4 +- .../Reporters/LcovReporterTests.cs | 11 +- .../Reporters/OpenCoverReporterTests.cs | 15 +- test/coverlet.core.tests/Samples/Samples.cs | 164 ++++++++++ .../Symbols/CecilSymbolHelperTests.cs | 263 ++++++++++++++++ 24 files changed, 1332 insertions(+), 272 deletions(-) create mode 100644 src/coverlet.core/Extensions/HelperExtensions.cs create mode 100644 src/coverlet.core/Symbols/BranchPoint.cs create mode 100644 src/coverlet.core/Symbols/CecilSymbolHelper.cs create mode 100644 test/coverlet.core.tests/Samples/Samples.cs create mode 100644 test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs diff --git a/.gitignore b/.gitignore index b9830ce42..294c9a4cb 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,7 @@ _TeamCity* # Visual Studio code coverage results *.coverage *.coveragexml +lcov.info # NCrunch _NCrunch_* diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 0ec0b4c84..c68e2950d 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -49,35 +49,90 @@ public CoverageResult GetCoverageResult() Documents documents = new Documents(); foreach (var doc in result.Documents) { + // Construct Line Results foreach (var line in doc.Lines) { if (documents.TryGetValue(doc.Path, out Classes classes)) { if (classes.TryGetValue(line.Class, out Methods methods)) { - if (methods.TryGetValue(line.Method, out Lines lines)) + if (methods.TryGetValue(line.Method, out Method method)) { - documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget }); + documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits }); } else { - documents[doc.Path][line.Class].Add(line.Method, new Lines()); - documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget }); + documents[doc.Path][line.Class].Add(line.Method, new Method()); + documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits }); } } else { documents[doc.Path].Add(line.Class, new Methods()); - documents[doc.Path][line.Class].Add(line.Method, new Lines()); - documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget }); + documents[doc.Path][line.Class].Add(line.Method, new Method()); + documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits }); } } else { documents.Add(doc.Path, new Classes()); documents[doc.Path].Add(line.Class, new Methods()); - documents[doc.Path][line.Class].Add(line.Method, new Lines()); - documents[doc.Path][line.Class][line.Method].Add(line.Number, new LineInfo { Hits = line.Hits, IsBranchPoint = line.IsBranchTarget }); + documents[doc.Path][line.Class].Add(line.Method, new Method()); + documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits }); + } + } + + // Construct Branch Results + foreach (var branch in doc.Branches) + { + if (documents.TryGetValue(doc.Path, out Classes classes)) + { + if (classes.TryGetValue(branch.Class, out Methods methods)) + { + if (methods.TryGetValue(branch.Method, out Method method)) + { + if (method.Branches.TryGetValue(branch.Number, out List branchInfo)) + { + documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo + { Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal } + ); + } + else + { + documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List()); + documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo + { Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal } + ); + } + } + else + { + documents[doc.Path][branch.Class].Add(branch.Method, new Method()); + documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List()); + documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo + { Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal } + ); + } + } + else + { + documents[doc.Path].Add(branch.Class, new Methods()); + documents[doc.Path][branch.Class].Add(branch.Method, new Method()); + documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List()); + documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo + { Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal } + ); + } + } + else + { + documents.Add(doc.Path, new Classes()); + documents[doc.Path].Add(branch.Class, new Methods()); + documents[doc.Path][branch.Class].Add(branch.Method, new Method()); + documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List()); + documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo + { Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal } + ); } } } @@ -99,28 +154,37 @@ private void CalculateCoverage() { if (!File.Exists(result.HitsFilePath)) { continue; } var lines = InstrumentationHelper.ReadHitsFile(result.HitsFilePath); - foreach (var line in lines) + foreach (var row in lines) { - var info = line.Split(','); + var info = row.Split(','); // Ignore malformed lines if (info.Length != 4) continue; - var document = result.Documents.FirstOrDefault(d => d.Path == info[0]); + bool isBranch = info[0] == "B"; + + var document = result.Documents.FirstOrDefault(d => d.Path == info[1]); if (document == null) continue; - int start = int.Parse(info[1]); - int end = int.Parse(info[2]); - bool target = info[3] == "B"; + int start = int.Parse(info[2]); - for (int j = start; j <= end; j++) + if (isBranch) { - var subLine = document.Lines.First(l => l.Number == j); - subLine.Hits = subLine.Hits + 1; - - if (j == start) - subLine.IsBranchTarget = target; + uint ordinal = uint.Parse(info[3]); + var branch = document.Branches.First(b => b.Number == start && b.Ordinal == ordinal); + if (branch.Hits != int.MaxValue) + branch.Hits += branch.Hits + 1; + } + else + { + int end = int.Parse(info[3]); + for (int j = start; j <= end; j++) + { + var line = document.Lines.First(l => l.Number == j); + if (line.Hits != int.MaxValue) + line.Hits = line.Hits + 1; + } } } diff --git a/src/coverlet.core/CoverageResult.cs b/src/coverlet.core/CoverageResult.cs index 302361e66..a9a574ad0 100644 --- a/src/coverlet.core/CoverageResult.cs +++ b/src/coverlet.core/CoverageResult.cs @@ -8,11 +8,29 @@ namespace Coverlet.Core public class LineInfo { public int Hits { get; set; } - public bool IsBranchPoint { get; set; } + } + + public class BranchInfo : LineInfo + { + public int Offset { get; set; } + public int EndOffset { get; set; } + public int Path { get; set; } + public uint Ordinal { get; set; } } public class Lines : SortedDictionary { } - public class Methods : Dictionary { } + public class Branches : SortedDictionary> { } + public class Method + { + internal Method() + { + Lines = new Lines(); + Branches = new Branches(); + } + public Lines Lines; + public Branches Branches; + } + public class Methods : Dictionary { } public class Classes : Dictionary { } public class Documents : Dictionary { } public class Modules : Dictionary { } diff --git a/src/coverlet.core/CoverageSummary.cs b/src/coverlet.core/CoverageSummary.cs index 670bfd51c..4bf5a1ff2 100644 --- a/src/coverlet.core/CoverageSummary.cs +++ b/src/coverlet.core/CoverageSummary.cs @@ -4,149 +4,232 @@ namespace Coverlet.Core { + public class CoverageDetails + { + public double Covered { get; set; } + public int Total { get; set; } + public double Percent { get; set; } + } public class CoverageSummary { - public double CalculateLineCoverage(Lines lines) + public CoverageDetails CalculateLineCoverage(Lines lines) { - double linesCovered = lines.Where(l => l.Value.Hits > 0).Count(); - double coverage = lines.Count == 0 ? lines.Count : linesCovered / lines.Count; - return Math.Round(coverage, 3); + var details = new CoverageDetails(); + details.Covered = lines.Where(l => l.Value.Hits > 0).Count(); + details.Total = lines.Count; + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateLineCoverage(Methods methods) + public CoverageDetails CalculateLineCoverage(Methods methods) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var method in methods) - total += CalculateLineCoverage(method.Value); - - average = total / methods.Count; - return Math.Round(average, 3); + { + var methodCoverage = CalculateLineCoverage(method.Value.Lines); + details.Covered += methodCoverage.Covered; + details.Total += methodCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateLineCoverage(Classes classes) + public CoverageDetails CalculateLineCoverage(Classes classes) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var @class in classes) - total += CalculateLineCoverage(@class.Value); - - average = total / classes.Count; - return Math.Round(average, 3); + { + var classCoverage = CalculateLineCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateLineCoverage(Documents documents) + public CoverageDetails CalculateLineCoverage(Documents documents) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var document in documents) - total += CalculateLineCoverage(document.Value); - - average = total / documents.Count; - return Math.Round(average, 3); + { + var documentCoverage = CalculateLineCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateLineCoverage(Modules modules) + public CoverageDetails CalculateLineCoverage(Modules modules) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var module in modules) - total += CalculateLineCoverage(module.Value); + { + var moduleCoverage = CalculateLineCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; + } - average = total / modules.Count; - return Math.Round(average, 3); + public CoverageDetails CalculateBranchCoverage(List branchInfo) + { + var details = new CoverageDetails(); + details.Covered = branchInfo.Count(bi => bi.Hits > 0); + details.Total = branchInfo.Count; + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateBranchCoverage(Lines lines) + public CoverageDetails CalculateBranchCoverage(Branches branches) { - double pointsCovered = lines.Where(l => l.Value.Hits > 0 && l.Value.IsBranchPoint).Count(); - double totalPoints = lines.Where(l => l.Value.IsBranchPoint).Count(); - double coverage = totalPoints == 0 ? totalPoints : pointsCovered / totalPoints; - return Math.Round(coverage, 3); + var details = new CoverageDetails(); + details.Covered = branches.Sum(b => b.Value.Where(bi => bi.Hits > 0).Count()); + details.Total = branches.Sum(b => b.Value.Count()); + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateBranchCoverage(Methods methods) + public CoverageDetails CalculateBranchCoverage(Methods methods) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var method in methods) - total += CalculateBranchCoverage(method.Value); - - average = total / methods.Count; - return Math.Round(average, 3); + { + var methodCoverage = CalculateBranchCoverage(method.Value.Branches); + details.Covered += methodCoverage.Covered; + details.Total += methodCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateBranchCoverage(Classes classes) + public CoverageDetails CalculateBranchCoverage(Classes classes) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var @class in classes) - total += CalculateBranchCoverage(@class.Value); - - average = total / classes.Count; - return Math.Round(average, 3); + { + var classCoverage = CalculateBranchCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateBranchCoverage(Documents documents) + public CoverageDetails CalculateBranchCoverage(Documents documents) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var document in documents) - total += CalculateBranchCoverage(document.Value); - - average = total / documents.Count; - return Math.Round(average, 3); + { + var documentCoverage = CalculateBranchCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateBranchCoverage(Modules modules) + public CoverageDetails CalculateBranchCoverage(Modules modules) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var module in modules) - total += CalculateBranchCoverage(module.Value); - - average = total / modules.Count; - return Math.Round(average, 3); + { + var moduleCoverage = CalculateBranchCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateMethodCoverage(Lines lines) + public CoverageDetails CalculateMethodCoverage(Lines lines) { - if (lines.Any(l => l.Value.Hits > 0)) - return 1; - - return 0; + var details = new CoverageDetails(); + details.Covered = lines.Any(l => l.Value.Hits > 0) ? 1 : 0; + details.Total = 1; + details.Percent = details.Covered; + return details; } - public double CalculateMethodCoverage(Methods methods) + public CoverageDetails CalculateMethodCoverage(Methods methods) { - double total = 0, average = 0; - foreach (var method in methods) - total += CalculateMethodCoverage(method.Value); - - average = total / methods.Count; - return Math.Round(average, 3); + var details = new CoverageDetails(); + var methodsWithLines = methods.Where(m => m.Value.Lines.Count > 0); + foreach (var method in methodsWithLines) + { + var methodCoverage = CalculateMethodCoverage(method.Value.Lines); + details.Covered += methodCoverage.Covered; + } + details.Total = methodsWithLines.Count(); + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateMethodCoverage(Classes classes) + public CoverageDetails CalculateMethodCoverage(Classes classes) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var @class in classes) - total += CalculateMethodCoverage(@class.Value); - - average = total / classes.Count; - return Math.Round(average, 3); + { + var classCoverage = CalculateMethodCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateMethodCoverage(Documents documents) + public CoverageDetails CalculateMethodCoverage(Documents documents) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var document in documents) - total += CalculateMethodCoverage(document.Value); - - average = total / documents.Count; - return Math.Round(average, 3); + { + var documentCoverage = CalculateMethodCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } - public double CalculateMethodCoverage(Modules modules) + public CoverageDetails CalculateMethodCoverage(Modules modules) { - double total = 0, average = 0; + var details = new CoverageDetails(); foreach (var module in modules) - total += CalculateMethodCoverage(module.Value); - - average = total / modules.Count; - return Math.Round(average, 3); + { + var moduleCoverage = CalculateMethodCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + } + + double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; + details.Percent = Math.Round(coverage, 3); + return details; } } } \ No newline at end of file diff --git a/src/coverlet.core/CoverageTracker.cs b/src/coverlet.core/CoverageTracker.cs index 0ff0b4a69..9a5d64dbf 100644 --- a/src/coverlet.core/CoverageTracker.cs +++ b/src/coverlet.core/CoverageTracker.cs @@ -33,6 +33,7 @@ public static void MarkExecuted(string path, string marker) } } + [ExcludeFromCoverage] public static void CurrentDomain_ProcessExit(object sender, EventArgs e) { foreach (var kvp in _markers) diff --git a/src/coverlet.core/Extensions/HelperExtensions.cs b/src/coverlet.core/Extensions/HelperExtensions.cs new file mode 100644 index 000000000..fe8f45cae --- /dev/null +++ b/src/coverlet.core/Extensions/HelperExtensions.cs @@ -0,0 +1,16 @@ + +using System; +using Coverlet.Core.Attributes; + +namespace Coverlet.Core.Extensions +{ + internal static class HelperExtensions + { + [ExcludeFromCoverage] + public static TRet Maybe(this T value, Func action, TRet defValue = default(TRet)) + where T : class + { + return (value != null) ? action(value) : defValue; + } + } +} \ No newline at end of file diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 1387546d0..72a4b5812 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -6,6 +6,7 @@ using Coverlet.Core.Attributes; using Coverlet.Core.Helpers; +using Coverlet.Core.Symbols; using Mono.Cecil; using Mono.Cecil.Cil; @@ -99,32 +100,59 @@ private void InstrumentMethod(MethodDefinition method) private void InstrumentIL(MethodDefinition method) { + method.Body.SimplifyMacros(); ILProcessor processor = method.Body.GetILProcessor(); var index = 0; var count = processor.Body.Instructions.Count; + var branchPoints = CecilSymbolHelper.GetBranchPoints(method); + for (int n = 0; n < count; n++) { var instruction = processor.Body.Instructions[index]; var sequencePoint = method.DebugInformation.GetSequencePoint(instruction); - if (sequencePoint == null || sequencePoint.IsHidden) + var targetedBranchPoints = branchPoints.Where(p => p.EndOffset == instruction.Offset); + + if (sequencePoint != null && !sequencePoint.IsHidden) { - index++; - continue; - } + var target = AddInstrumentationCode(method, processor, instruction, sequencePoint); + foreach (var _instruction in processor.Body.Instructions) + ReplaceInstructionTarget(_instruction, instruction, target); - var target = AddInstrumentationCode(method, processor, instruction, sequencePoint); - foreach (var _instruction in processor.Body.Instructions) - ReplaceInstructionTarget(_instruction, instruction, target); + foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) + ReplaceExceptionHandlerBoundary(handler, instruction, target); - foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) - ReplaceExceptionHandlerBoundary(handler, instruction, target); + index += 3; + } + + if (targetedBranchPoints.Count() > 0) + { + foreach (var _branchTarget in targetedBranchPoints) + { + /* + * Skip branches with no sequence point reference for now. + * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. + * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which + * I am currently not sure how to handle. + */ + if (_branchTarget.StartLine == -1 || _branchTarget.Document == null) + continue; + + var target = AddInstrumentationCode(method, processor, instruction, _branchTarget); + foreach (var _instruction in processor.Body.Instructions) + ReplaceInstructionTarget(_instruction, instruction, target); + + foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) + ReplaceExceptionHandlerBoundary(handler, instruction, target); + + index += 3; + } + } - index += 4; + index++; } - method.Body.SimplifyMacros(); method.Body.OptimizeMacros(); } @@ -143,8 +171,8 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor document.Lines.Add(new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); } - string flag = IsBranchTarget(processor, instruction) ? "B" : "L"; - string marker = $"{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine},{flag}"; + // string flag = branchPoints.Count > 0 ? "B" : "L"; + string marker = $"L,{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine}"; var pathInstr = Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath); var markInstr = Instruction.Create(OpCodes.Ldstr, marker); @@ -157,21 +185,40 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor return pathInstr; } - private static bool IsBranchTarget(ILProcessor processor, Instruction instruction) + private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) { - foreach (var _instruction in processor.Body.Instructions) + var document = _result.Documents.FirstOrDefault(d => d.Path == branchPoint.Document); + if (document == null) { - if (_instruction.Operand is Instruction target) - { - if (target == instruction) - return true; - } - - if (_instruction.Operand is Instruction[] targets) - return targets.Any(t => t == instruction); + document = new Document { Path = branchPoint.Document }; + _result.Documents.Add(document); } - return false; + if (!document.Branches.Exists(l => l.Number == branchPoint.StartLine && l.Ordinal == branchPoint.Ordinal)) + document.Branches.Add( + new Branch + { + Number = branchPoint.StartLine, + Class = method.DeclaringType.FullName, + Method = method.FullName, + Offset = branchPoint.Offset, + EndOffset = branchPoint.EndOffset, + Path = branchPoint.Path, + Ordinal = branchPoint.Ordinal + } + ); + + string marker = $"B,{document.Path},{branchPoint.StartLine},{branchPoint.Ordinal}"; + + var pathInstr = Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath); + var markInstr = Instruction.Create(OpCodes.Ldstr, marker); + var callInstr = Instruction.Create(OpCodes.Call, processor.Body.Method.Module.ImportReference(_markExecutedMethodLoader.Value)); + + processor.InsertBefore(instruction, callInstr); + processor.InsertBefore(callInstr, markInstr); + processor.InsertBefore(markInstr, pathInstr); + + return pathInstr; } private static void ReplaceInstructionTarget(Instruction instruction, Instruction oldTarget, Instruction newTarget) diff --git a/src/coverlet.core/Instrumentation/InstrumenterResult.cs b/src/coverlet.core/Instrumentation/InstrumenterResult.cs index dd7c9aef0..a2b92cd36 100644 --- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs +++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs @@ -7,16 +7,28 @@ internal class Line public int Number; public string Class; public string Method; - public bool IsBranchTarget; public int Hits; } + internal class Branch : Line + { + public int Offset; + public int EndOffset; + public int Path; + public uint Ordinal; + } + internal class Document { - public Document() => Lines = new List(); + public Document() + { + Lines = new List(); + Branches = new List(); + } public string Path; public List Lines { get; private set; } + public List Branches { get; private set; } } internal class InstrumenterResult diff --git a/src/coverlet.core/Reporters/CoberturaReporter.cs b/src/coverlet.core/Reporters/CoberturaReporter.cs index 16f3c40d1..92048509a 100644 --- a/src/coverlet.core/Reporters/CoberturaReporter.cs +++ b/src/coverlet.core/Reporters/CoberturaReporter.cs @@ -18,12 +18,13 @@ public string Report(CoverageResult result) { CoverageSummary summary = new CoverageSummary(); - int totalLines = 0, coveredLines = 0, totalBranches = 0, coveredBranches = 0; + var lineCoverage = summary.CalculateLineCoverage(result.Modules); + var branchCoverage = summary.CalculateBranchCoverage(result.Modules); XDocument xml = new XDocument(); XElement coverage = new XElement("coverage"); - coverage.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(result.Modules).ToString())); - coverage.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(result.Modules).ToString())); + coverage.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(result.Modules).Percent.ToString())); + coverage.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(result.Modules).Percent.ToString())); coverage.Add(new XAttribute("version", "1.9")); coverage.Add(new XAttribute("timestamp", ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString())); @@ -36,8 +37,8 @@ public string Report(CoverageResult result) { XElement package = new XElement("package"); package.Add(new XAttribute("name", Path.GetFileNameWithoutExtension(module.Key))); - package.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(module.Value).ToString())); - package.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(module.Value).ToString())); + package.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(module.Value).Percent.ToString())); + package.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(module.Value).Percent.ToString())); package.Add(new XAttribute("complexity", "0")); XElement classes = new XElement("classes"); @@ -48,8 +49,8 @@ public string Report(CoverageResult result) XElement @class = new XElement("class"); @class.Add(new XAttribute("name", cls.Key)); @class.Add(new XAttribute("filename", GetRelativePathFromBase(basePath, document.Key))); - @class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).ToString())); - @class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).ToString())); + @class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).Percent.ToString())); + @class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).Percent.ToString())); @class.Add(new XAttribute("complexity", "0")); XElement classLines = new XElement("lines"); @@ -57,37 +58,39 @@ public string Report(CoverageResult result) foreach (var meth in cls.Value) { + // Skip all methods with no lines + if (meth.Value.Lines.Count == 0) + continue; + XElement method = new XElement("method"); method.Add(new XAttribute("name", meth.Key.Split(':')[2].Split('(')[0])); method.Add(new XAttribute("signature", "(" + meth.Key.Split(':')[2].Split('(')[1])); - method.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(meth.Value).ToString())); - method.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(meth.Value).ToString())); + method.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(meth.Value.Lines).Percent.ToString())); + method.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(meth.Value.Branches).Percent.ToString())); XElement lines = new XElement("lines"); - foreach (var ln in meth.Value) + foreach (var ln in meth.Value.Lines) { XElement line = new XElement("line"); line.Add(new XAttribute("number", ln.Key.ToString())); line.Add(new XAttribute("hits", ln.Value.Hits.ToString())); - line.Add(new XAttribute("branch", ln.Value.IsBranchPoint.ToString())); - - totalLines++; - if (ln.Value.Hits > 0) coveredLines++; + line.Add(new XAttribute("branch", meth.Value.Branches.ContainsKey(ln.Key).ToString())); - - if (ln.Value.IsBranchPoint) + if (meth.Value.Branches.TryGetValue(ln.Key, out List branches)) { - line.Add(new XAttribute("condition-coverage", "100% (1/1)")); + var branchInfoCoverage = summary.CalculateBranchCoverage(branches); + line.Add(new XAttribute("condition-coverage", $"{branchInfoCoverage.Percent*100}% ({branchInfoCoverage.Covered}/{branchInfoCoverage.Total})")); XElement conditions = new XElement("conditions"); - XElement condition = new XElement("condition"); - condition.Add(new XAttribute("number", "0")); - condition.Add(new XAttribute("type", "jump")); - condition.Add(new XAttribute("coverage", "100%")); - - totalBranches++; - if (ln.Value.Hits > 0) coveredBranches++; + var byOffset = branches.GroupBy(b => b.Offset).ToDictionary(b => b.Key, b => b.ToList()); + foreach (var entry in byOffset) + { + XElement condition = new XElement("condition"); + condition.Add(new XAttribute("number", entry.Key)); + condition.Add(new XAttribute("type", entry.Value.Count() > 2 ? "switch" : "jump")); // Just guessing here + condition.Add(new XAttribute("coverage", $"{summary.CalculateBranchCoverage(entry.Value).Percent * 100}%")); + conditions.Add(condition); + } - conditions.Add(condition); line.Add(conditions); } @@ -110,10 +113,10 @@ public string Report(CoverageResult result) packages.Add(package); } - coverage.Add(new XAttribute("lines-covered", coveredLines.ToString())); - coverage.Add(new XAttribute("lines-valid", totalLines.ToString())); - coverage.Add(new XAttribute("branches-covered", coveredBranches.ToString())); - coverage.Add(new XAttribute("branches-valid", totalBranches.ToString())); + coverage.Add(new XAttribute("lines-covered", lineCoverage.Covered.ToString())); + coverage.Add(new XAttribute("lines-valid", lineCoverage.Total.ToString())); + coverage.Add(new XAttribute("branches-covered", branchCoverage.Covered.ToString())); + coverage.Add(new XAttribute("branches-valid", branchCoverage.Total.ToString())); coverage.Add(sources); coverage.Add(packages); diff --git a/src/coverlet.core/Reporters/LcovReporter.cs b/src/coverlet.core/Reporters/LcovReporter.cs index 327de30e5..0676d6edc 100644 --- a/src/coverlet.core/Reporters/LcovReporter.cs +++ b/src/coverlet.core/Reporters/LcovReporter.cs @@ -12,58 +12,48 @@ public class LcovReporter : IReporter public string Report(CoverageResult result) { + CoverageSummary summary = new CoverageSummary(); List lcov = new List(); - int numSequencePoints = 0, numBranchPoints = 0, numMethods = 0, numBlockBranch = 1; - int visitedSequencePoints = 0, visitedBranchPoints = 0, visitedMethods = 0; foreach (var module in result.Modules) { foreach (var doc in module.Value) { + var docLineCoverage = summary.CalculateLineCoverage(doc.Value); + var docBranchCoverage = summary.CalculateBranchCoverage(doc.Value); + var docMethodCoverage = summary.CalculateMethodCoverage(doc.Value); + lcov.Add("SF:" + doc.Key); foreach (var @class in doc.Value) { - bool methodVisited = false; foreach (var method in @class.Value) { - lcov.Add($"FN:{method.Value.First().Key - 1},{method.Key}"); - lcov.Add($"FNDA:{method.Value.First().Value.Hits},{method.Key}"); + // Skip all methods with no lines + if (method.Value.Lines.Count == 0) + continue; - foreach (var line in method.Value) - { - lcov.Add($"DA:{line.Key},{line.Value.Hits}"); - numSequencePoints++; + lcov.Add($"FN:{method.Value.Lines.First().Key - 1},{method.Key}"); + lcov.Add($"FNDA:{method.Value.Lines.First().Value.Hits},{method.Key}"); - if (line.Value.IsBranchPoint) - { - lcov.Add($"BRDA:{line.Key},{numBlockBranch},{numBlockBranch},{line.Value.Hits}"); - numBlockBranch++; - numBranchPoints++; - } + foreach (var line in method.Value.Lines) + lcov.Add($"DA:{line.Key},{line.Value.Hits}"); - if (line.Value.Hits > 0) - { - visitedSequencePoints++; - methodVisited = true; - if (line.Value.IsBranchPoint) - visitedBranchPoints++; - } + foreach (var branchs in method.Value.Branches) + { + foreach (var branch in branchs.Value) + lcov.Add($"BRDA:{branchs.Key},{branch.Offset},{branch.Path},{branch.Hits}"); } - - numMethods++; - if (methodVisited) - visitedMethods++; } } - lcov.Add($"LH:{visitedSequencePoints}"); - lcov.Add($"LF:{numSequencePoints}"); + lcov.Add($"LF:{docLineCoverage.Total}"); + lcov.Add($"LH:{docLineCoverage.Covered}"); - lcov.Add($"BRF:{numBranchPoints}"); - lcov.Add($"BRH:{visitedBranchPoints}"); + lcov.Add($"BRF:{docBranchCoverage.Total}"); + lcov.Add($"BRH:{docBranchCoverage.Covered}"); - lcov.Add($"FNF:{numMethods}"); - lcov.Add($"FNH:{visitedMethods}"); + lcov.Add($"FNF:{docMethodCoverage.Total}"); + lcov.Add($"FNH:{docMethodCoverage.Covered}"); lcov.Add("end_of_record"); } diff --git a/src/coverlet.core/Reporters/OpenCoverReporter.cs b/src/coverlet.core/Reporters/OpenCoverReporter.cs index 9e4fb4694..cb2b60d74 100644 --- a/src/coverlet.core/Reporters/OpenCoverReporter.cs +++ b/src/coverlet.core/Reporters/OpenCoverReporter.cs @@ -21,8 +21,8 @@ public string Report(CoverageResult result) XElement coverageSummary = new XElement("Summary"); XElement modules = new XElement("Modules"); - int numSequencePoints = 0, numBranchPoints = 0, numClasses = 0, numMethods = 0; - int visitedSequencePoints = 0, visitedBranchPoints = 0, visitedClasses = 0, visitedMethods = 0; + int numClasses = 0, numMethods = 0; + int visitedClasses = 0, visitedMethods = 0; int i = 1; @@ -62,12 +62,19 @@ public string Report(CoverageResult result) foreach (var meth in cls.Value) { + // Skip all methods with no lines + if (meth.Value.Lines.Count == 0) + continue; + + var methLineCoverage = summary.CalculateLineCoverage(meth.Value.Lines); + var methBranchCoverage = summary.CalculateBranchCoverage(meth.Value.Branches); + XElement method = new XElement("Method"); method.Add(new XAttribute("cyclomaticComplexity", "0")); method.Add(new XAttribute("nPathComplexity", "0")); - method.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(meth.Value).ToString())); - method.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(meth.Value).ToString())); + method.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString())); + method.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString())); method.Add(new XAttribute("isConstructor", meth.Key.Contains("ctor").ToString())); method.Add(new XAttribute("isGetter", meth.Key.Contains("get_").ToString())); method.Add(new XAttribute("isSetter", meth.Key.Contains("set_").ToString())); @@ -79,15 +86,15 @@ public string Report(CoverageResult result) fileRef.Add(new XAttribute("uid", i.ToString())); XElement methodPoint = new XElement("MethodPoint"); - methodPoint.Add(new XAttribute("vc", meth.Value.Select(l => l.Value.Hits).Sum().ToString())); + methodPoint.Add(new XAttribute("vc", methLineCoverage.Covered.ToString())); methodPoint.Add(new XAttribute("upsid", "0")); methodPoint.Add(new XAttribute(XName.Get("type", "xsi"), "SequencePoint")); methodPoint.Add(new XAttribute("ordinal", j.ToString())); methodPoint.Add(new XAttribute("offset", j.ToString())); methodPoint.Add(new XAttribute("sc", "0")); - methodPoint.Add(new XAttribute("sl", meth.Value.First().Key.ToString())); + methodPoint.Add(new XAttribute("sl", meth.Value.Lines.First().Key.ToString())); methodPoint.Add(new XAttribute("ec", "1")); - methodPoint.Add(new XAttribute("el", meth.Value.Last().Key.ToString())); + methodPoint.Add(new XAttribute("el", meth.Value.Lines.Last().Key.ToString())); methodPoint.Add(new XAttribute("bec", "0")); methodPoint.Add(new XAttribute("bev", "0")); methodPoint.Add(new XAttribute("fileid", i.ToString())); @@ -100,7 +107,7 @@ public string Report(CoverageResult result) int kBr = 0; var methodVisited = false; - foreach (var lines in meth.Value) + foreach (var lines in meth.Value.Lines) { XElement sequencePoint = new XElement("SequencePoint"); sequencePoint.Add(new XAttribute("vc", lines.Value.Hits.ToString())); @@ -115,45 +122,43 @@ public string Report(CoverageResult result) sequencePoint.Add(new XAttribute("fileid", i.ToString())); sequencePoints.Add(sequencePoint); - if (lines.Value.IsBranchPoint) - { - XElement branchPoint = new XElement("BranchPoint"); - branchPoint.Add(new XAttribute("vc", lines.Value.Hits.ToString())); - branchPoint.Add(new XAttribute("upsid", lines.Key.ToString())); - branchPoint.Add(new XAttribute("ordinal", kBr.ToString())); - branchPoint.Add(new XAttribute("path", "")); - branchPoint.Add(new XAttribute("offset", kBr.ToString())); - branchPoint.Add(new XAttribute("offsetend", kBr.ToString())); - branchPoint.Add(new XAttribute("sl", lines.Key.ToString())); - branchPoint.Add(new XAttribute("fileid", i.ToString())); - branchPoints.Add(branchPoint); - kBr++; - numBranchPoints++; - } - - numSequencePoints++; if (lines.Value.Hits > 0) { - visitedSequencePoints++; classVisited = true; methodVisited = true; - if (lines.Value.IsBranchPoint) - visitedBranchPoints++; } k++; } + foreach (var branches in meth.Value.Branches) + { + foreach (var branch in branches.Value) + { + XElement branchPoint = new XElement("BranchPoint"); + branchPoint.Add(new XAttribute("vc", branch.Hits.ToString())); + branchPoint.Add(new XAttribute("upsid", branches.Key.ToString())); + branchPoint.Add(new XAttribute("ordinal", branch.Ordinal.ToString())); + branchPoint.Add(new XAttribute("path", branch.Path.ToString())); + branchPoint.Add(new XAttribute("offset", branch.Offset.ToString())); + branchPoint.Add(new XAttribute("offsetend", branch.EndOffset.ToString())); + branchPoint.Add(new XAttribute("sl", branches.Key.ToString())); + branchPoint.Add(new XAttribute("fileid", i.ToString())); + branchPoints.Add(branchPoint); + kBr++; + } + } + numMethods++; if (methodVisited) visitedMethods++; - methodSummary.Add(new XAttribute("numSequencePoints", meth.Value.Count().ToString())); - methodSummary.Add(new XAttribute("visitedSequencePoints", meth.Value.Where(l => l.Value.Hits > 0).Count().ToString())); - methodSummary.Add(new XAttribute("numBranchPoints", meth.Value.Where(l => l.Value.IsBranchPoint).Count().ToString())); - methodSummary.Add(new XAttribute("visitedBranchPoints", meth.Value.Where(l => l.Value.IsBranchPoint && l.Value.Hits > 0).Count().ToString())); - methodSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(meth.Value).ToString())); - methodSummary.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(meth.Value).ToString())); + methodSummary.Add(new XAttribute("numSequencePoints", methLineCoverage.Total.ToString())); + methodSummary.Add(new XAttribute("visitedSequencePoints", methLineCoverage.Covered.ToString())); + methodSummary.Add(new XAttribute("numBranchPoints", methBranchCoverage.Total.ToString())); + methodSummary.Add(new XAttribute("visitedBranchPoints", methBranchCoverage.Covered.ToString())); + methodSummary.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString())); + methodSummary.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString())); methodSummary.Add(new XAttribute("maxCyclomaticComplexity", "0")); methodSummary.Add(new XAttribute("minCyclomaticComplexity", "0")); methodSummary.Add(new XAttribute("visitedClasses", "0")); @@ -176,18 +181,22 @@ public string Report(CoverageResult result) if (classVisited) visitedClasses++; - classSummary.Add(new XAttribute("numSequencePoints", cls.Value.Select(c => c.Value.Count).Sum().ToString())); - classSummary.Add(new XAttribute("visitedSequencePoints", cls.Value.Select(c => c.Value.Where(l => l.Value.Hits > 0).Count()).Sum().ToString())); - classSummary.Add(new XAttribute("numBranchPoints", cls.Value.Select(c => c.Value.Count(l => l.Value.IsBranchPoint)).Sum().ToString())); - classSummary.Add(new XAttribute("visitedBranchPoints", cls.Value.Select(c => c.Value.Where(l => l.Value.Hits > 0 && l.Value.IsBranchPoint).Count()).Sum().ToString())); - classSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(cls.Value).ToString())); - classSummary.Add(new XAttribute("branchCoverage", summary.CalculateBranchCoverage(cls.Value).ToString())); + var classLineCoverage = summary.CalculateLineCoverage(cls.Value); + var classBranchCoverage = summary.CalculateBranchCoverage(cls.Value); + var classMethodCoverage = summary.CalculateMethodCoverage(cls.Value); + + classSummary.Add(new XAttribute("numSequencePoints", classLineCoverage.Total.ToString())); + classSummary.Add(new XAttribute("visitedSequencePoints", classLineCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("numBranchPoints", classBranchCoverage.Total.ToString())); + classSummary.Add(new XAttribute("visitedBranchPoints", classBranchCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("sequenceCoverage", classLineCoverage.Percent.ToString())); + classSummary.Add(new XAttribute("branchCoverage", classBranchCoverage.Percent.ToString())); classSummary.Add(new XAttribute("maxCyclomaticComplexity", "0")); classSummary.Add(new XAttribute("minCyclomaticComplexity", "0")); classSummary.Add(new XAttribute("visitedClasses", classVisited ? "1" : "0")); classSummary.Add(new XAttribute("numClasses", "1")); - classSummary.Add(new XAttribute("visitedMethods", "0")); - classSummary.Add(new XAttribute("numMethods", cls.Value.Count.ToString())); + classSummary.Add(new XAttribute("visitedMethods", classMethodCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("numMethods", classMethodCoverage.Total.ToString())); @class.Add(classSummary); @class.Add(className); @@ -202,12 +211,15 @@ public string Report(CoverageResult result) modules.Add(module); } - coverageSummary.Add(new XAttribute("numSequencePoints", numSequencePoints.ToString())); - coverageSummary.Add(new XAttribute("visitedSequencePoints", visitedSequencePoints.ToString())); - coverageSummary.Add(new XAttribute("numBranchPoints", numBranchPoints.ToString())); - coverageSummary.Add(new XAttribute("visitedBranchPoints", visitedBranchPoints.ToString())); - coverageSummary.Add(new XAttribute("sequenceCoverage", summary.CalculateLineCoverage(result.Modules).ToString())); - coverageSummary.Add(new XAttribute("branchCoverage", summary.CalculateLineCoverage(result.Modules).ToString())); + var moduleLineCoverage = summary.CalculateLineCoverage(result.Modules); + var moduleBranchCoverage = summary.CalculateLineCoverage(result.Modules); + + coverageSummary.Add(new XAttribute("numSequencePoints", moduleLineCoverage.Total.ToString())); + coverageSummary.Add(new XAttribute("visitedSequencePoints", moduleLineCoverage.Covered.ToString())); + coverageSummary.Add(new XAttribute("numBranchPoints", moduleBranchCoverage.Total.ToString())); + coverageSummary.Add(new XAttribute("visitedBranchPoints", moduleBranchCoverage.Covered.ToString())); + coverageSummary.Add(new XAttribute("sequenceCoverage", moduleLineCoverage.Percent.ToString())); + coverageSummary.Add(new XAttribute("branchCoverage", moduleBranchCoverage.Percent.ToString())); coverageSummary.Add(new XAttribute("maxCyclomaticComplexity", "0")); coverageSummary.Add(new XAttribute("minCyclomaticComplexity", "0")); coverageSummary.Add(new XAttribute("visitedClasses", visitedClasses.ToString())); diff --git a/src/coverlet.core/Symbols/BranchPoint.cs b/src/coverlet.core/Symbols/BranchPoint.cs new file mode 100644 index 000000000..427aad61b --- /dev/null +++ b/src/coverlet.core/Symbols/BranchPoint.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.RegularExpressions; + +namespace Coverlet.Core.Symbols +{ + /// + /// a branch point + /// + public class BranchPoint + { + /// + /// Line of the branching instruction + /// + public int StartLine { get; set; } + + /// + /// A path that can be taken + /// + public int Path { get; set; } + + /// + /// An order of the point within the method + /// + public UInt32 Ordinal { get; set; } + + /// + /// List of OffsetPoints between Offset and EndOffset (exclusive) + /// + public System.Collections.Generic.List OffsetPoints { get; set; } + + /// + /// The IL offset of the point + /// + public int Offset { get; set; } + + /// + /// Last Offset == EndOffset. + /// Can be same as Offset + /// + public int EndOffset { get; set; } + + /// + /// The url to the document if an entry was not mapped to an id + /// + public string Document { get; set; } + } +} \ No newline at end of file diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs new file mode 100644 index 000000000..592567331 --- /dev/null +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -0,0 +1,290 @@ +// +// This class is based heavily on the work of the OpenCover +// team in OpenCover.Framework.Symbols.CecilSymbolManager +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +using Coverlet.Core.Extensions; + +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Collections.Generic; + +namespace Coverlet.Core.Symbols +{ + public static class CecilSymbolHelper + { + private const int StepOverLineCode = 0xFEEFEE; + private static readonly Regex IsMovenext = new Regex(@"\<[^\s>]+\>\w__\w(\w)?::MoveNext\(\)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + public static List GetBranchPoints(MethodDefinition methodDefinition) + { + var list = new List(); + GetBranchPoints(methodDefinition, list); + return list; + } + private static void GetBranchPoints(MethodDefinition methodDefinition, List list) + { + if (methodDefinition == null) + return; + try + { + UInt32 ordinal = 0; + var instructions = methodDefinition.Body.Instructions; + + // if method is a generated MoveNext skip first branch (could be a switch or a branch) + var skipFirstBranch = IsMovenext.IsMatch(methodDefinition.FullName); + + foreach (var instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch)) + { + if (skipFirstBranch) + { + skipFirstBranch = false; + continue; + } + + if (BranchIsInGeneratedFinallyBlock(instruction, methodDefinition)) + continue; + + var pathCounter = 0; + + // store branch origin offset + var branchOffset = instruction.Offset; + var closestSeqPt = FindClosestInstructionWithSequencePoint(methodDefinition.Body, instruction).Maybe(i => methodDefinition.DebugInformation.GetSequencePoint(i)); + var branchingInstructionLine = closestSeqPt.Maybe(x => x.StartLine, -1); + var document = closestSeqPt.Maybe(x => x.Document.Url); + + if (null == instruction.Next) + return; + + if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition)) + return; + } + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"An error occurred with 'GetBranchPointsForToken' for method '{methodDefinition.FullName}'", ex); + } + } + + private static bool BuildPointsForConditionalBranch(List list, Instruction instruction, + int branchingInstructionLine, string document, int branchOffset, int pathCounter, + Collection instructions, ref uint ordinal, MethodDefinition methodDefinition) + { + // Add Default branch (Path=0) + + // Follow else/default instruction + var @else = instruction.Next; + + var pathOffsetList = GetBranchPath(@else); + + // add Path 0 + var path0 = new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = pathCounter++, + OffsetPoints = + pathOffsetList.Count > 1 + ? pathOffsetList.GetRange(0, pathOffsetList.Count - 1) + : new List(), + EndOffset = pathOffsetList.Last() + }; + + // Add Conditional Branch (Path=1) + if (instruction.OpCode.Code != Code.Switch) + { + // Follow instruction at operand + var @then = instruction.Operand as Instruction; + if (@then == null) + return false; + + ordinal = BuildPointsForBranch(list, then, branchingInstructionLine, document, branchOffset, + ordinal, pathCounter, path0, instructions, methodDefinition); + } + else // instruction.OpCode.Code == Code.Switch + { + var branchInstructions = instruction.Operand as Instruction[]; + if (branchInstructions == null || branchInstructions.Length == 0) + return false; + + ordinal = BuildPointsForSwitchCases(list, path0, branchInstructions, branchingInstructionLine, + document, branchOffset, ordinal, ref pathCounter); + } + return true; + } + + private static uint BuildPointsForBranch(List list, Instruction then, int branchingInstructionLine, string document, + int branchOffset, uint ordinal, int pathCounter, BranchPoint path0, Collection instructions, MethodDefinition methodDefinition) + { + var pathOffsetList1 = GetBranchPath(@then); + + // Add path 1 + var path1 = new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = pathCounter, + OffsetPoints = + pathOffsetList1.Count > 1 + ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) + : new List(), + EndOffset = pathOffsetList1.Last() + }; + + // only add branch if branch does not match a known sequence + // e.g. auto generated field assignment + // or encapsulates at least one sequence point + var offsets = new[] + { + path0.Offset, + path0.EndOffset, + path1.Offset, + path1.EndOffset + }; + + var ignoreSequences = new[] + { + // we may need other samples + new[] {Code.Brtrue_S, Code.Pop, Code.Ldsfld, Code.Ldftn, Code.Newobj, Code.Dup, Code.Stsfld, Code.Newobj}, // CachedAnonymousMethodDelegate field allocation + }; + + var bs = offsets.Min(); + var be = offsets.Max(); + + var range = instructions.Where(i => (i.Offset >= bs) && (i.Offset <= be)).ToList(); + + var match = ignoreSequences + .Where(ignoreSequence => range.Count >= ignoreSequence.Length) + .Any(ignoreSequence => range.Zip(ignoreSequence, (instruction, code) => instruction.OpCode.Code == code).All(x => x)); + + var count = range + .Count(i => methodDefinition.DebugInformation.GetSequencePoint(i) != null); + + if (!match || count > 0) + { + list.Add(path0); + list.Add(path1); + } + return ordinal; + } + + private static uint BuildPointsForSwitchCases(List list, BranchPoint path0, Instruction[] branchInstructions, + int branchingInstructionLine, string document, int branchOffset, uint ordinal, ref int pathCounter) + { + var counter = pathCounter; + list.Add(path0); + // Add Conditional Branches (Path>0) + list.AddRange(branchInstructions.Select(GetBranchPath) + .Select(pathOffsetList1 => new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = counter++, + OffsetPoints = + pathOffsetList1.Count > 1 + ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) + : new List(), + EndOffset = pathOffsetList1.Last() + })); + pathCounter = counter; + return ordinal; + } + + private static bool BranchIsInGeneratedFinallyBlock(Instruction branchInstruction, MethodDefinition methodDefinition) + { + if (!methodDefinition.Body.HasExceptionHandlers) + return false; + + // a generated finally block will have no sequence points in its range + var handlers = methodDefinition.Body.ExceptionHandlers + .Where(e => e.HandlerType == ExceptionHandlerType.Finally) + .ToList(); + + return handlers + .Where(e => branchInstruction.Offset >= e.HandlerStart.Offset) + .Where( e =>branchInstruction.Offset < e.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, e.HandlerStart.Offset))) + .OrderByDescending(h => h.HandlerStart.Offset) // we need to work inside out + .Any(eh => !(methodDefinition.DebugInformation.GetSequencePointMapping() + .Where(i => i.Value.StartLine != StepOverLineCode) + .Any(i => i.Value.Offset >= eh.HandlerStart.Offset && i.Value.Offset < eh.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, eh.HandlerStart.Offset))))); + } + + private static int GetOffsetOfNextEndfinally(MethodBody body, int startOffset) + { + var lastOffset = body.Instructions.LastOrDefault().Maybe(i => i.Offset, int.MaxValue); + return body.Instructions.FirstOrDefault(i => i.Offset >= startOffset && i.OpCode.Code == Code.Endfinally).Maybe(i => i.Offset, lastOffset); + } + + private static List GetBranchPath(Instruction instruction) + { + var offsetList = new List(); + + if (instruction != null) + { + var point = instruction; + offsetList.Add(point.Offset); + while ( point.OpCode == OpCodes.Br || point.OpCode == OpCodes.Br_S ) + { + var nextPoint = point.Operand as Instruction; + if (nextPoint != null) + { + point = nextPoint; + offsetList.Add(point.Offset); + } + else + { + break; + } + } + } + + return offsetList; + } + + private static Instruction FindClosestInstructionWithSequencePoint(MethodBody methodBody, Instruction instruction) + { + var sequencePointsInMethod = methodBody.Instructions.Where(i => HasValidSequencePoint(i, methodBody.Method)).ToList(); + if (!sequencePointsInMethod.Any()) + return null; + var idx = sequencePointsInMethod.BinarySearch(instruction, new InstructionByOffsetComparer()); + Instruction prev; + if (idx < 0) + { + // no exact match, idx corresponds to the next, larger element + var lower = Math.Max(~idx - 1, 0); + prev = sequencePointsInMethod[lower]; + } + else + { + // exact match, idx corresponds to the match + prev = sequencePointsInMethod[idx]; + } + + return prev; + } + + private static bool HasValidSequencePoint(Instruction instruction, MethodDefinition methodDefinition) + { + var sp = methodDefinition.DebugInformation.GetSequencePoint(instruction); + return sp != null && sp.StartLine != StepOverLineCode; + } + + private class InstructionByOffsetComparer : IComparer + { + public int Compare(Instruction x, Instruction y) + { + return x.Offset.CompareTo(y.Offset); + } + } + } +} \ No newline at end of file diff --git a/src/coverlet.msbuild.tasks/CoverageResultTask.cs b/src/coverlet.msbuild.tasks/CoverageResultTask.cs index e201d7433..dd0621684 100644 --- a/src/coverlet.msbuild.tasks/CoverageResultTask.cs +++ b/src/coverlet.msbuild.tasks/CoverageResultTask.cs @@ -59,12 +59,13 @@ public override bool Execute() double total = 0; CoverageSummary summary = new CoverageSummary(); - ConsoleTable table = new ConsoleTable("Module", "Coverage"); + ConsoleTable table = new ConsoleTable("Module", "Line Coverage", "Branch Coverage"); foreach (var module in result.Modules) { - double percent = summary.CalculateLineCoverage(module.Value) * 100; - table.AddRow(System.IO.Path.GetFileNameWithoutExtension(module.Key), $"{percent}%"); + double percent = summary.CalculateLineCoverage(module.Value).Percent * 100; + double branchPercent = summary.CalculateBranchCoverage(module.Value).Percent * 100; + table.AddRow(System.IO.Path.GetFileNameWithoutExtension(module.Key), $"{percent}%", $"{branchPercent}%"); total += percent; } diff --git a/test/coverlet.core.tests/CoverageSummaryTests.cs b/test/coverlet.core.tests/CoverageSummaryTests.cs index 7e1f29369..fab5f2763 100644 --- a/test/coverlet.core.tests/CoverageSummaryTests.cs +++ b/test/coverlet.core.tests/CoverageSummaryTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Coverlet.Core; @@ -14,11 +15,18 @@ public class CoverageSummaryTests public CoverageSummaryTests() { Lines lines = new Lines(); - lines.Add(1, new LineInfo { Hits = 1, IsBranchPoint = true }); + lines.Add(1, new LineInfo { Hits = 1 }); lines.Add(2, new LineInfo { Hits = 0 }); + Branches branches = new Branches(); + branches.Add(1, new List()); + branches[1].Add(new BranchInfo { Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); + branches[1].Add(new BranchInfo { Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); Methods methods = new Methods(); - methods.Add("System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()", lines); + var methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; Classes classes = new Classes(); classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods); @@ -40,10 +48,10 @@ public void TestCalculateLineCoverage() var @class = document.Value.First(); var method = @class.Value.First(); - Assert.Equal(0.5, summary.CalculateLineCoverage(module.Value)); - Assert.Equal(0.5, summary.CalculateLineCoverage(document.Value)); - Assert.Equal(0.5, summary.CalculateLineCoverage(@class.Value)); - Assert.Equal(0.5, summary.CalculateLineCoverage(method.Value)); + Assert.Equal(0.5, summary.CalculateLineCoverage(module.Value).Percent); + Assert.Equal(0.5, summary.CalculateLineCoverage(document.Value).Percent); + Assert.Equal(0.5, summary.CalculateLineCoverage(@class.Value).Percent); + Assert.Equal(0.5, summary.CalculateLineCoverage(method.Value.Lines).Percent); } [Fact] @@ -56,10 +64,10 @@ public void TestCalculateBranchCoverage() var @class = document.Value.First(); var method = @class.Value.First(); - Assert.Equal(1, summary.CalculateBranchCoverage(module.Value)); - Assert.Equal(1, summary.CalculateBranchCoverage(document.Value)); - Assert.Equal(1, summary.CalculateBranchCoverage(@class.Value)); - Assert.Equal(1, summary.CalculateBranchCoverage(method.Value)); + Assert.Equal(1, summary.CalculateBranchCoverage(module.Value).Percent); + Assert.Equal(1, summary.CalculateBranchCoverage(document.Value).Percent); + Assert.Equal(1, summary.CalculateBranchCoverage(@class.Value).Percent); + Assert.Equal(1, summary.CalculateBranchCoverage(method.Value.Branches).Percent); } [Fact] @@ -72,10 +80,10 @@ public void TestCalculateMethodCoverage() var @class = document.Value.First(); var method = @class.Value.First(); - Assert.Equal(1, summary.CalculateMethodCoverage(module.Value)); - Assert.Equal(1, summary.CalculateMethodCoverage(document.Value)); - Assert.Equal(1, summary.CalculateMethodCoverage(@class.Value)); - Assert.Equal(1, summary.CalculateMethodCoverage(method.Value)); + Assert.Equal(1, summary.CalculateMethodCoverage(module.Value).Percent); + Assert.Equal(1, summary.CalculateMethodCoverage(document.Value).Percent); + Assert.Equal(1, summary.CalculateMethodCoverage(@class.Value).Percent); + Assert.Equal(1, summary.CalculateMethodCoverage(method.Value.Lines).Percent); } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/CoverageTests.cs b/test/coverlet.core.tests/CoverageTests.cs index e895a47ee..e153c474f 100644 --- a/test/coverlet.core.tests/CoverageTests.cs +++ b/test/coverlet.core.tests/CoverageTests.cs @@ -5,6 +5,7 @@ using Moq; using Coverlet.Core; +using System.Collections.Generic; namespace Coverlet.Core.Tests { @@ -13,19 +14,26 @@ public class CoverageTests [Fact] public void TestCoverage() { - string module = typeof(CoverageTests).Assembly.Location; + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); string identifier = Guid.NewGuid().ToString(); var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), identifier)); - var tempModule = Path.Combine(directory.FullName, Path.GetFileName(module)); - File.Copy(module, tempModule, true); + File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true); + File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true); - var coverage = new Coverage(tempModule, identifier); + // TODO: Find a way to mimick hits + + // 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, identifier); coverage.PrepareModules(); var result = coverage.GetCoverageResult(); - Assert.Empty(result.Modules); + + Assert.NotEmpty(result.Modules); directory.Delete(true); } diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs index 4f4058899..4a5b85bbf 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs @@ -14,10 +14,11 @@ public void TestEnsureDocumentsPropertyNotNull() } [Fact] - public void TestEnsureLinesPropertyNotNull() + public void TestEnsureLinesAndBranchesPropertyNotNull() { Document document = new Document(); Assert.NotNull(document.Lines); + Assert.NotNull(document.Branches); } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index e206a7213..83f70a873 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -11,7 +11,7 @@ public class InstrumenterTests [Fact] public void TestInstrument() { - string module = typeof(InstrumenterTests).Assembly.Location; + string module = GetType().Assembly.Location; string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); string identifier = Guid.NewGuid().ToString(); diff --git a/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs b/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs index 540894a5a..be7eba83c 100644 --- a/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Xunit; namespace Coverlet.Core.Reporters.Tests @@ -13,8 +14,16 @@ public void TestReport() Lines lines = new Lines(); lines.Add(1, new LineInfo { Hits = 1 }); lines.Add(2, new LineInfo { Hits = 0 }); + Branches branches = new Branches(); + branches.Add(1, new List { + new BranchInfo{ Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }, + new BranchInfo{ Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 } + }); Methods methods = new Methods(); - methods.Add("System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()", lines); + var methodString = "System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; Classes classes = new Classes(); classes.Add("Coverlet.Core.Reporters.Tests.CoberturaReporterTests", methods); Documents documents = new Documents(); diff --git a/test/coverlet.core.tests/Reporters/JsonReporterTests.cs b/test/coverlet.core.tests/Reporters/JsonReporterTests.cs index bc9511d1e..979bc1108 100644 --- a/test/coverlet.core.tests/Reporters/JsonReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/JsonReporterTests.cs @@ -14,7 +14,9 @@ public void TestReport() lines.Add(1, new LineInfo { Hits = 1 }); lines.Add(2, new LineInfo { Hits = 0 }); Methods methods = new Methods(); - methods.Add("System.Void Coverlet.Core.Reporters.Tests.JsonReporterTests.TestReport()", lines); + var methodString = "System.Void Coverlet.Core.Reporters.Tests.JsonReporterTests.TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; Classes classes = new Classes(); classes.Add("Coverlet.Core.Reporters.Tests.JsonReporterTests", methods); Documents documents = new Documents(); diff --git a/test/coverlet.core.tests/Reporters/LcovReporterTests.cs b/test/coverlet.core.tests/Reporters/LcovReporterTests.cs index c7e972ad4..c30cea209 100644 --- a/test/coverlet.core.tests/Reporters/LcovReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/LcovReporterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Xunit; namespace Coverlet.Core.Reporters.Tests @@ -13,8 +14,16 @@ public void TestReport() Lines lines = new Lines(); lines.Add(1, new LineInfo { Hits = 1 }); lines.Add(2, new LineInfo { Hits = 0 }); + Branches branches = new Branches(); + branches.Add(1, new List { + new BranchInfo{ Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }, + new BranchInfo{ Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 } + }); Methods methods = new Methods(); - methods.Add("System.Void Coverlet.Core.Reporters.Tests.LcovReporterTests.TestReport()", lines); + var methodString = "System.Void Coverlet.Core.Reporters.Tests.LcovReporterTests.TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; Classes classes = new Classes(); classes.Add("Coverlet.Core.Reporters.Tests.LcovReporterTests", methods); Documents documents = new Documents(); diff --git a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs index 4545cec1f..3643480f3 100644 --- a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Xunit; namespace Coverlet.Core.Reporters.Tests @@ -41,8 +42,16 @@ private static Documents CreateFirstDocuments() Lines lines = new Lines(); lines.Add(1, new LineInfo { Hits = 1 }); lines.Add(2, new LineInfo { Hits = 0 }); + Branches branches = new Branches(); + branches.Add(1, new List { + new BranchInfo{ Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }, + new BranchInfo{ Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 } + }); Methods methods = new Methods(); - methods.Add("System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()", lines); + var methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; Classes classes = new Classes(); classes.Add("Coverlet.Core.Reporters.Tests.OpenCoverReporterTests", methods); Documents documents = new Documents(); @@ -57,7 +66,9 @@ private static Documents CreateSecondDocuments() lines.Add(2, new LineInfo { Hits = 0 }); Methods methods = new Methods(); - methods.Add("System.Void Some.Other.Module.TestClass.TestMethod()", lines); + var methodString = "System.Void Some.Other.Module.TestClass.TestMethod()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; Classes classes2 = new Classes(); classes2.Add("Some.Other.Module.TestClass", methods); diff --git a/test/coverlet.core.tests/Samples/Samples.cs b/test/coverlet.core.tests/Samples/Samples.cs new file mode 100644 index 000000000..518a848fb --- /dev/null +++ b/test/coverlet.core.tests/Samples/Samples.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Coverlet.Core.Samples.Tests +{ + class ConstructorNotDeclaredClass + { + } + class DeclaredConstructorClass + { + DeclaredConstructorClass() { } + + public bool HasSingleDecision(string input) + { + if (input.Contains("test")) return true; + return false; + } + + public bool HasTwoDecisions(string input) + { + if (input.Contains("test")) return true; + if (input.Contains("xxx")) return true; + return false; + } + + public bool HasCompleteIf(string input) + { + if (input.Contains("test")) + { + return true; + } + else + { + return false; + } + } + + public bool HasSwitch(int input) + { + switch (input) + { + case 0: + return true; + case 1: + return false; + case 2: + return true; + } + return false; + } + + public bool HasSwitchWithDefault(int input) + { + switch (input) + { + case 1: + return true; + case 2: + return false; + case 3: + return true; + default: + return false; + } + } + + public bool HasSwitchWithBreaks(int input) + { + bool ret = false; + switch (input) + { + case 1: + ret = true; + break; + case 2: + ret = false; + break; + case 3: + ret = true; + break; + } + + return ret; + } + + public int HasSwitchWithMultipleCases(int input) + { + switch (input) + { + case 1: + return -1; + case 2: + return 2001; + case 3: + return -5001; + default: + return 7; + } + } + + public string HasSimpleUsingStatement() + { + string value; + try + { + + } + finally + { + using (var stream = new MemoryStream()) + { + var x = stream.Length; + value = x > 1000 ? "yes" : "no"; + } + } + return value; + } + + public void HasSimpleTaskWithLambda() + { + var t = new Task(() => { }); + } + + public string UsingWithException_Issue243() + { + using (var ms = new MemoryStream()) // IL generates a finally block for using to dispose the stream + { + throw new Exception(); + } + } + } + + public class LinqIssue + { + public void Method() + { + var s = new ObservableCollection(); + var x = (from a in s select new {a}); + } + + public object Property + { + get + { + var s = new ObservableCollection(); + var x = (from a in s select new { a }); + return x; + } + } + } + + public class Iterator + { + public IEnumerable Fetch() + { + yield return "one"; + yield return "two"; + } + } +} \ No newline at end of file diff --git a/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs new file mode 100644 index 000000000..0cd079683 --- /dev/null +++ b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs @@ -0,0 +1,263 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +using Xunit; +using Coverlet.Core.Samples.Tests; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Coverlet.Core.Symbols.Tests +{ + public class CecilSymbolHelperTests + { + private ModuleDefinition _module; + public CecilSymbolHelperTests() + { + var location = GetType().Assembly.Location; + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(Path.GetDirectoryName(location)); + var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver }; + _module = ModuleDefinition.ReadModule(location, parameters); + } + + [Fact] + public void GetBranchPoints_OneBranch() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSingleDecision")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(2, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(0, points[0].Path); + Assert.Equal(1, points[1].Path); + Assert.Equal(19, points[0].StartLine); + Assert.Equal(19, points[1].StartLine); + Assert.NotNull(points[1].Document); + Assert.Equal(points[0].Document, points[1].Document); + } + + [Fact] + public void GetBranchPoints_Using_Where_GeneratedBranchesIgnored() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSimpleUsingStatement")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + Assert.Equal(2, points.Count()); + } + + [Fact] + public void GetBranchPoints_GeneratedBranches_DueToCachedAnonymousMethodDelegate_Ignored() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSimpleTaskWithLambda")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_TwoBranch() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasTwoDecisions")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[2].Offset, points[3].Offset); + Assert.Equal(25, points[0].StartLine); + Assert.Equal(26, points[2].StartLine); + } + + [Fact] + public void GetBranchPoints_CompleteIf() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasCompleteIf")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(2, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(32, points[0].StartLine); + Assert.Equal(32, points[1].StartLine); + } + + [Fact] + public void GetBranchPoints_Switch() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSwitch")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(44, points[0].StartLine); + Assert.Equal(44, points[1].StartLine); + Assert.Equal(44, points[2].StartLine); + Assert.Equal(44, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithDefault() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithDefault")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(58, points[0].StartLine); + Assert.Equal(58, points[1].StartLine); + Assert.Equal(58, points[2].StartLine); + Assert.Equal(58, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithBreaks() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithBreaks")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(74, points[0].StartLine); + Assert.Equal(74, points[1].StartLine); + Assert.Equal(74, points[2].StartLine); + Assert.Equal(74, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithMultipleCases() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithMultipleCases")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count()); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(points[0].Offset, points[3].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(92, points[0].StartLine); + Assert.Equal(92, points[1].StartLine); + Assert.Equal(92, points[2].StartLine); + Assert.Equal(92, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_AssignsNegativeLineNumberToBranchesInMethodsThatHaveNoInstrumentablePoints() + { + /* + * Yes these actually exist - the compiler is very inventive + * in this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. + */ + // arrange + var type = _module.Types.First(x => x.FullName.Contains("f__AnonymousType")); + var method = type.Methods.First(x => x.FullName.Contains("::Equals")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + foreach (var branchPoint in points) + Assert.Equal(-1, branchPoint.StartLine); + } + + [Fact] + public void GetBranchPoints_UsingWithException_Issue243_IgnoresBranchInFinallyBlock() + { + // arrange + var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + var method = type.Methods.First(x => x.FullName.Contains("::UsingWithException_Issue243")); + + // check that the method is laid out the way we discovered it to be during the defect + // @see https://github.com/OpenCover/opencover/issues/243 + Assert.Single(method.Body.ExceptionHandlers); + Assert.NotNull(method.Body.ExceptionHandlers[0].HandlerStart); + Assert.Null(method.Body.ExceptionHandlers[0].HandlerEnd); + Assert.Equal(1, method.Body.Instructions.Count(i => i.OpCode.FlowControl == FlowControl.Cond_Branch)); + Assert.True(method.Body.Instructions.First(i => i.OpCode.FlowControl == FlowControl.Cond_Branch).Offset > method.Body.ExceptionHandlers[0].HandlerStart.Offset); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresSwitchIn_GeneratedMoveNext() + { + // arrange + var nestedName = typeof (Iterator).GetNestedTypes(BindingFlags.NonPublic).First().Name; + var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(Iterator).FullName); + var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + var points = CecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + + } + } +} \ No newline at end of file From a5e8534afa0488a950f6e1b91e09662bc3c017e1 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sat, 5 May 2018 12:51:30 -0400 Subject: [PATCH 2/3] changes per feedback --- src/coverlet.core/CoverageDetails.cs | 14 ++++++ src/coverlet.core/CoverageSummary.cs | 49 ------------------- .../Instrumentation/Instrumenter.cs | 40 +++++++-------- .../Symbols/CecilSymbolHelper.cs | 3 +- 4 files changed, 34 insertions(+), 72 deletions(-) create mode 100644 src/coverlet.core/CoverageDetails.cs diff --git a/src/coverlet.core/CoverageDetails.cs b/src/coverlet.core/CoverageDetails.cs new file mode 100644 index 000000000..e83cfdb44 --- /dev/null +++ b/src/coverlet.core/CoverageDetails.cs @@ -0,0 +1,14 @@ +using System; + +namespace Coverlet.Core +{ + public class CoverageDetails + { + public double Covered { get; internal set; } + public int Total { get; internal set; } + public double Percent + { + get => Math.Round(Total == 0 ? Total : Covered / Total, 3); + } + } +} \ No newline at end of file diff --git a/src/coverlet.core/CoverageSummary.cs b/src/coverlet.core/CoverageSummary.cs index 4bf5a1ff2..81155c636 100644 --- a/src/coverlet.core/CoverageSummary.cs +++ b/src/coverlet.core/CoverageSummary.cs @@ -4,12 +4,6 @@ namespace Coverlet.Core { - public class CoverageDetails - { - public double Covered { get; set; } - public int Total { get; set; } - public double Percent { get; set; } - } public class CoverageSummary { public CoverageDetails CalculateLineCoverage(Lines lines) @@ -17,8 +11,6 @@ public CoverageDetails CalculateLineCoverage(Lines lines) var details = new CoverageDetails(); details.Covered = lines.Where(l => l.Value.Hits > 0).Count(); details.Total = lines.Count; - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -31,9 +23,6 @@ public CoverageDetails CalculateLineCoverage(Methods methods) details.Covered += methodCoverage.Covered; details.Total += methodCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -46,9 +35,6 @@ public CoverageDetails CalculateLineCoverage(Classes classes) details.Covered += classCoverage.Covered; details.Total += classCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -61,9 +47,6 @@ public CoverageDetails CalculateLineCoverage(Documents documents) details.Covered += documentCoverage.Covered; details.Total += documentCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -76,9 +59,6 @@ public CoverageDetails CalculateLineCoverage(Modules modules) details.Covered += moduleCoverage.Covered; details.Total += moduleCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -87,8 +67,6 @@ public CoverageDetails CalculateBranchCoverage(List branchInfo) var details = new CoverageDetails(); details.Covered = branchInfo.Count(bi => bi.Hits > 0); details.Total = branchInfo.Count; - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -97,8 +75,6 @@ public CoverageDetails CalculateBranchCoverage(Branches branches) var details = new CoverageDetails(); details.Covered = branches.Sum(b => b.Value.Where(bi => bi.Hits > 0).Count()); details.Total = branches.Sum(b => b.Value.Count()); - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -111,9 +87,6 @@ public CoverageDetails CalculateBranchCoverage(Methods methods) details.Covered += methodCoverage.Covered; details.Total += methodCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -126,9 +99,6 @@ public CoverageDetails CalculateBranchCoverage(Classes classes) details.Covered += classCoverage.Covered; details.Total += classCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -141,9 +111,6 @@ public CoverageDetails CalculateBranchCoverage(Documents documents) details.Covered += documentCoverage.Covered; details.Total += documentCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -156,9 +123,6 @@ public CoverageDetails CalculateBranchCoverage(Modules modules) details.Covered += moduleCoverage.Covered; details.Total += moduleCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -167,7 +131,6 @@ public CoverageDetails CalculateMethodCoverage(Lines lines) var details = new CoverageDetails(); details.Covered = lines.Any(l => l.Value.Hits > 0) ? 1 : 0; details.Total = 1; - details.Percent = details.Covered; return details; } @@ -181,9 +144,6 @@ public CoverageDetails CalculateMethodCoverage(Methods methods) details.Covered += methodCoverage.Covered; } details.Total = methodsWithLines.Count(); - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -196,9 +156,6 @@ public CoverageDetails CalculateMethodCoverage(Classes classes) details.Covered += classCoverage.Covered; details.Total += classCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -211,9 +168,6 @@ public CoverageDetails CalculateMethodCoverage(Documents documents) details.Covered += documentCoverage.Covered; details.Total += documentCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } @@ -226,9 +180,6 @@ public CoverageDetails CalculateMethodCoverage(Modules modules) details.Covered += moduleCoverage.Covered; details.Total += moduleCoverage.Total; } - - double coverage = details.Total == 0 ? details.Total : details.Covered / details.Total; - details.Percent = Math.Round(coverage, 3); return details; } } diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 72a4b5812..d6dd0527f 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -126,28 +126,25 @@ private void InstrumentIL(MethodDefinition method) index += 3; } - if (targetedBranchPoints.Count() > 0) + foreach (var _branchTarget in targetedBranchPoints) { - foreach (var _branchTarget in targetedBranchPoints) - { - /* - * Skip branches with no sequence point reference for now. - * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. - * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which - * I am currently not sure how to handle. - */ - if (_branchTarget.StartLine == -1 || _branchTarget.Document == null) - continue; - - var target = AddInstrumentationCode(method, processor, instruction, _branchTarget); - foreach (var _instruction in processor.Body.Instructions) - ReplaceInstructionTarget(_instruction, instruction, target); - - foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) - ReplaceExceptionHandlerBoundary(handler, instruction, target); - - index += 3; - } + /* + * Skip branches with no sequence point reference for now. + * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. + * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which + * I am currently not sure how to handle. + */ + if (_branchTarget.StartLine == -1 || _branchTarget.Document == null) + continue; + + var target = AddInstrumentationCode(method, processor, instruction, _branchTarget); + foreach (var _instruction in processor.Body.Instructions) + ReplaceInstructionTarget(_instruction, instruction, target); + + foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) + ReplaceExceptionHandlerBoundary(handler, instruction, target); + + index += 3; } index++; @@ -171,7 +168,6 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor document.Lines.Add(new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); } - // string flag = branchPoints.Count > 0 ? "B" : "L"; string marker = $"L,{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine}"; var pathInstr = Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath); diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index 592567331..7b36e57df 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -19,6 +19,7 @@ public static class CecilSymbolHelper { private const int StepOverLineCode = 0xFEEFEE; private static readonly Regex IsMovenext = new Regex(@"\<[^\s>]+\>\w__\w(\w)?::MoveNext\(\)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + public static List GetBranchPoints(MethodDefinition methodDefinition) { var list = new List(); @@ -56,7 +57,7 @@ private static void GetBranchPoints(MethodDefinition methodDefinition, List x.StartLine, -1); var document = closestSeqPt.Maybe(x => x.Document.Url); - if (null == instruction.Next) + if (instruction.Next == null) return; if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition)) From 6e9b50129dcba9b71de428e4e6081fc75944fedd Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sat, 5 May 2018 13:29:38 -0400 Subject: [PATCH 3/3] make requested changes to symbol helper --- .../Symbols/CecilSymbolHelper.cs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index 7b36e57df..e4c9ca3ae 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -23,22 +23,18 @@ public static class CecilSymbolHelper public static List GetBranchPoints(MethodDefinition methodDefinition) { var list = new List(); - GetBranchPoints(methodDefinition, list); - return list; - } - private static void GetBranchPoints(MethodDefinition methodDefinition, List list) - { if (methodDefinition == null) - return; - try - { - UInt32 ordinal = 0; - var instructions = methodDefinition.Body.Instructions; - - // if method is a generated MoveNext skip first branch (could be a switch or a branch) - var skipFirstBranch = IsMovenext.IsMatch(methodDefinition.FullName); + return list; + + UInt32 ordinal = 0; + var instructions = methodDefinition.Body.Instructions; + + // if method is a generated MoveNext skip first branch (could be a switch or a branch) + var skipFirstBranch = IsMovenext.IsMatch(methodDefinition.FullName); - foreach (var instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch)) + foreach (var instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch)) + { + try { if (skipFirstBranch) { @@ -58,17 +54,17 @@ private static void GetBranchPoints(MethodDefinition methodDefinition, List x.Document.Url); if (instruction.Next == null) - return; + return list; if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition)) - return; + return list; + } + catch (Exception) + { + continue; } } - catch (Exception ex) - { - throw new InvalidOperationException( - $"An error occurred with 'GetBranchPointsForToken' for method '{methodDefinition.FullName}'", ex); - } + return list; } private static bool BuildPointsForConditionalBranch(List list, Instruction instruction,