Skip to content

Commit f3cce5d

Browse files
Improve lambda scenario coverage (#583)
Improve lambda scenario coverage
1 parent 9321bd7 commit f3cce5d

File tree

7 files changed

+350
-18
lines changed

7 files changed

+350
-18
lines changed

src/coverlet.core/Coverage.cs

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,63 @@ public CoverageResult GetCoverageResult()
217217
_instrumentationHelper.RestoreOriginalModule(result.ModulePath, _identifier);
218218
}
219219

220+
// In case of anonymous delegate compiler generate a custom class and passes it as type.method delegate.
221+
// If in delegate method we've a branches we need to move these to "actual" class/method that use it.
222+
// We search "method" with same "Line" of closure class method and add missing branches to it,
223+
// in this way we correctly report missing branch inside compiled generated anonymous delegate.
224+
List<string> compileGeneratedClassToRemove = null;
225+
foreach (var module in modules)
226+
{
227+
foreach (var document in module.Value)
228+
{
229+
foreach (var @class in document.Value)
230+
{
231+
foreach (var method in @class.Value)
232+
{
233+
foreach (var branch in method.Value.Branches)
234+
{
235+
if (BranchInCompilerGeneratedClass(method.Key))
236+
{
237+
Method actualMethod = GetMethodWithSameLineInSameDocument(document.Value, @class.Key, branch.Line);
238+
239+
if (actualMethod is null)
240+
{
241+
continue;
242+
}
243+
244+
actualMethod.Branches.Add(branch);
245+
246+
if (compileGeneratedClassToRemove is null)
247+
{
248+
compileGeneratedClassToRemove = new List<string>();
249+
}
250+
251+
if (!compileGeneratedClassToRemove.Contains(@class.Key))
252+
{
253+
compileGeneratedClassToRemove.Add(@class.Key);
254+
}
255+
}
256+
}
257+
}
258+
}
259+
}
260+
}
261+
262+
// After method/branches analysis of compiled generated class we can remove noise from reports
263+
if (!(compileGeneratedClassToRemove is null))
264+
{
265+
foreach (var module in modules)
266+
{
267+
foreach (var document in module.Value)
268+
{
269+
foreach (var classToRemove in compileGeneratedClassToRemove)
270+
{
271+
document.Value.Remove(classToRemove);
272+
}
273+
}
274+
}
275+
}
276+
220277
var coverageResult = new CoverageResult { Identifier = _identifier, Modules = modules, InstrumentedResults = _results };
221278

222279
if (!string.IsNullOrEmpty(_mergeWith) && !string.IsNullOrWhiteSpace(_mergeWith) && _fileSystem.Exists(_mergeWith))
@@ -228,6 +285,41 @@ public CoverageResult GetCoverageResult()
228285
return coverageResult;
229286
}
230287

288+
private bool BranchInCompilerGeneratedClass(string methodName)
289+
{
290+
foreach (var instrumentedResult in _results)
291+
{
292+
if (instrumentedResult.BranchesInCompiledGeneratedClass.Contains(methodName))
293+
{
294+
return true;
295+
}
296+
}
297+
return false;
298+
}
299+
300+
private Method GetMethodWithSameLineInSameDocument(Classes documentClasses, string compilerGeneratedClassName, int branchLine)
301+
{
302+
foreach (var @class in documentClasses)
303+
{
304+
if (@class.Key == compilerGeneratedClassName)
305+
{
306+
continue;
307+
}
308+
309+
foreach (var method in @class.Value)
310+
{
311+
foreach (var line in method.Value.Lines)
312+
{
313+
if (line.Key == branchLine)
314+
{
315+
return method.Value;
316+
}
317+
}
318+
}
319+
}
320+
return null;
321+
}
322+
231323
private void CalculateCoverage()
232324
{
233325
foreach (var result in _results)
@@ -253,15 +345,15 @@ private void CalculateCoverage()
253345
}
254346
}
255347

348+
List<(int docIndex, int line)> zeroHitsLines = new List<(int docIndex, int line)>();
349+
var documentsList = result.Documents.Values.ToList();
256350
using (var fs = _fileSystem.NewFileStream(result.HitsFilePath, FileMode.Open))
257351
using (var br = new BinaryReader(fs))
258352
{
259353
int hitCandidatesCount = br.ReadInt32();
260354

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

263-
var documentsList = result.Documents.Values.ToList();
264-
265357
for (int i = 0; i < hitCandidatesCount; ++i)
266358
{
267359
var hitLocation = result.HitCandidates[i];
@@ -279,10 +371,29 @@ private void CalculateCoverage()
279371
{
280372
var line = document.Lines[j];
281373
line.Hits += hits;
374+
375+
// We register 0 hit lines for later cleanup false positive of nested lambda closures
376+
if (hits == 0)
377+
{
378+
zeroHitsLines.Add((hitLocation.docIndex, line.Number));
379+
}
282380
}
283381
}
284382
}
285383
}
384+
385+
// Cleanup nested state machine false positive hits
386+
foreach (var (docIndex, line) in zeroHitsLines)
387+
{
388+
foreach (var lineToCheck in documentsList[docIndex].Lines)
389+
{
390+
if (lineToCheck.Key == line)
391+
{
392+
lineToCheck.Value.Hits = 0;
393+
}
394+
}
395+
}
396+
286397
_instrumentationHelper.DeleteHitsFile(result.HitsFilePath);
287398
_logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted");
288399
}

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public void BackupOriginalModule(string module, string identifier)
198198
}
199199
}
200200

201-
public void RestoreOriginalModule(string module, string identifier)
201+
public virtual void RestoreOriginalModule(string module, string identifier)
202202
{
203203
var backupPath = GetBackupPath(module, identifier);
204204
var backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb");
@@ -226,7 +226,7 @@ public void RestoreOriginalModule(string module, string identifier)
226226
}, retryStrategy, 10);
227227
}
228228

229-
public void RestoreOriginalModules()
229+
public virtual void RestoreOriginalModules()
230230
{
231231
// Restore the original module - retry up to 10 times, since the destination file could be locked
232232
// See: https://github.com/tonerdo/coverlet/issues/25

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.IO;
66
using System.Linq;
7+
using System.Runtime.CompilerServices;
78
using Coverlet.Core.Abstracts;
89
using Coverlet.Core.Attributes;
910
using Coverlet.Core.Logging;
@@ -37,6 +38,7 @@ internal class Instrumenter
3738
private MethodReference _customTrackerRegisterUnloadEventsMethod;
3839
private MethodReference _customTrackerRecordHitMethod;
3940
private List<string> _excludedSourceFiles;
41+
private List<string> _branchesInCompiledGeneratedClass;
4042

4143
public Instrumenter(
4244
string module,
@@ -130,6 +132,8 @@ public InstrumenterResult Instrument()
130132
}
131133
}
132134

135+
_result.BranchesInCompiledGeneratedClass = _branchesInCompiledGeneratedClass == null ? Array.Empty<string>() : _branchesInCompiledGeneratedClass.ToArray();
136+
133137
return _result;
134138
}
135139

@@ -454,7 +458,8 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor
454458
BranchKey key = new BranchKey(branchPoint.StartLine, (int)branchPoint.Ordinal);
455459
if (!document.Branches.ContainsKey(key))
456460
{
457-
document.Branches.Add(key,
461+
document.Branches.Add(
462+
key,
458463
new Branch
459464
{
460465
Number = branchPoint.StartLine,
@@ -466,6 +471,19 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor
466471
Ordinal = branchPoint.Ordinal
467472
}
468473
);
474+
475+
if (method.DeclaringType.CustomAttributes.Any(x => x.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName))
476+
{
477+
if (_branchesInCompiledGeneratedClass == null)
478+
{
479+
_branchesInCompiledGeneratedClass = new List<string>();
480+
}
481+
482+
if (!_branchesInCompiledGeneratedClass.Contains(method.FullName))
483+
{
484+
_branchesInCompiledGeneratedClass.Add(method.FullName);
485+
}
486+
}
469487
}
470488

471489
_result.HitCandidates.Add(new HitCandidate(true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal));

src/coverlet.core/Instrumentation/InstrumenterResult.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
namespace Coverlet.Core.Instrumentation
77
{
8+
[DebuggerDisplay("Number = {Number} Hits = {Hits} Class = {Class} Method = {Method}")]
89
[DataContract]
910
public class Line
1011
{
@@ -73,6 +74,7 @@ public Document()
7374
public Dictionary<BranchKey, Branch> Branches { get; private set; }
7475
}
7576

77+
[DebuggerDisplay("isBranch = {isBranch} docIndex = {docIndex} start = {start} end = {end}")]
7678
[DataContract]
7779
public class HitCandidate
7880
{
@@ -100,6 +102,8 @@ public InstrumenterResult()
100102
[DataMember]
101103
public string Module;
102104
[DataMember]
105+
public string[] BranchesInCompiledGeneratedClass;
106+
[DataMember]
103107
public string HitsFilePath;
104108
[DataMember]
105109
public string ModulePath;

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ public void SelectionStatements_If()
9292
// Similar to msbuild coverage result task
9393
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
9494

95+
// Generate html report to check
96+
// TestInstrumentationHelper.GenerateHtmlReport(result);
97+
9598
// Asserts on doc/lines/branches
9699
result.Document("Instrumentation.SelectionStatements.cs")
97100
// (line, hits)
98101
.AssertLinesCovered((11, 1), (15, 0))
99102
// (line,ordinal,hits)
100103
.AssertBranchesCovered((9, 0, 1), (9, 1, 0));
101-
102-
// if need to generate html report for debugging purpose
103-
// TestInstrumentationHelper.GenerateHtmlReport(result);
104104
}
105105
finally
106106
{
@@ -163,6 +163,7 @@ public void AsyncAwait()
163163
}, path).Dispose();
164164

165165
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
166+
166167
result.Document("Instrumentation.AsyncAwait.cs")
167168
.AssertLinesCovered(BuildConfiguration.Debug,
168169
// AsyncExecution(bool)
@@ -192,5 +193,51 @@ public void AsyncAwait()
192193
File.Delete(path);
193194
}
194195
}
196+
197+
[Fact]
198+
public void Lambda_Issue343()
199+
{
200+
string path = Path.GetTempFileName();
201+
try
202+
{
203+
RemoteExecutor.Invoke(async pathSerialize =>
204+
{
205+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Lambda_Issue343>(instance =>
206+
{
207+
instance.InvokeAnonymous_Test();
208+
((Task<bool>)instance.InvokeAnonymousAsync_Test()).ConfigureAwait(false).GetAwaiter().GetResult();
209+
return Task.CompletedTask;
210+
}, pathSerialize);
211+
return 0;
212+
}, path).Dispose();
213+
214+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
215+
216+
result.Document("Instrumentation.Lambda.cs")
217+
.AssertLinesCoveredAllBut(BuildConfiguration.Debug, 23, 51)
218+
.AssertBranchesCovered(BuildConfiguration.Debug,
219+
// Expected branches
220+
(22, 0, 0),
221+
(22, 1, 1),
222+
(50, 2, 0),
223+
(50, 3, 1),
224+
// Unexpected branches
225+
(20, 0, 1),
226+
(20, 1, 1),
227+
(49, 0, 1),
228+
(49, 1, 0),
229+
(54, 4, 0),
230+
(54, 5, 1),
231+
(39, 0, 1),
232+
(39, 1, 0),
233+
(48, 0, 1),
234+
(48, 1, 1)
235+
);
236+
}
237+
finally
238+
{
239+
File.Delete(path);
240+
}
241+
}
195242
}
196243
}

0 commit comments

Comments
 (0)