Skip to content

Commit 76be6c1

Browse files
committed
Add a "single hit" collection mode
Fixes #306
1 parent c3bc94a commit 76be6c1

File tree

9 files changed

+66
-12
lines changed

9 files changed

+66
-12
lines changed

src/coverlet.console/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ static int Main(string[] args)
3737
CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue);
3838
CommandOption includeDirectories = app.Option("--include-directory", "Include directories containing additional assemblies to be instrumented.", CommandOptionType.MultipleValue);
3939
CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue);
40+
CommandOption singleHit = app.Option("--single-hit", "Specifies whether to limit code coverage hit reporting to a single hit for each location", CommandOptionType.NoValue);
4041
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
4142
CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue);
4243

@@ -48,7 +49,7 @@ static int Main(string[] args)
4849
if (!target.HasValue())
4950
throw new CommandParsingException(app, "Target must be specified.");
5051

51-
Coverage coverage = new Coverage(module.Value, includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), excludeAttributes.Values.ToArray(), mergeWith.Value(), useSourceLink.HasValue());
52+
Coverage coverage = new Coverage(module.Value, includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), excludeAttributes.Values.ToArray(), singleHit.HasValue(), mergeWith.Value(), useSourceLink.HasValue());
5253
coverage.PrepareModules();
5354

5455
Process process = new Process();

src/coverlet.core/Coverage.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class Coverage
2323
private string[] _excludeFilters;
2424
private string[] _excludedSourceFiles;
2525
private string[] _excludeAttributes;
26+
private bool _singleHit;
2627
private string _mergeWith;
2728
private bool _useSourceLink;
2829
private List<InstrumenterResult> _results;
@@ -36,14 +37,15 @@ public string Identifier
3637

3738
internal IEnumerable<InstrumenterResult> Results => _results;
3839

39-
public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith, bool useSourceLink)
40+
public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, bool singleHit, string mergeWith, bool useSourceLink)
4041
{
4142
_module = module;
4243
_includeFilters = includeFilters;
4344
_includeDirectories = includeDirectories ?? Array.Empty<string>();
4445
_excludeFilters = excludeFilters;
4546
_excludedSourceFiles = excludedSourceFiles;
4647
_excludeAttributes = excludeAttributes;
48+
_singleHit = singleHit;
4749
_mergeWith = mergeWith;
4850
_useSourceLink = useSourceLink;
4951

@@ -64,7 +66,7 @@ public void PrepareModules()
6466
!InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
6567
continue;
6668

67-
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes);
69+
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes, _singleHit);
6870
if (instrumenter.CanInstrument())
6971
{
7072
InstrumentationHelper.BackupOriginalModule(module, _identifier);

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,28 @@ internal class Instrumenter
2323
private readonly string[] _includeFilters;
2424
private readonly string[] _excludedFiles;
2525
private readonly string[] _excludedAttributes;
26+
private readonly bool _singleHit;
2627
private readonly bool _isCoreLibrary;
2728
private InstrumenterResult _result;
2829
private FieldDefinition _customTrackerHitsFilePath;
2930
private FieldDefinition _customTrackerHitsArray;
3031
private FieldDefinition _customTrackerHitsMemoryMapName;
32+
private FieldDefinition _customTrackerSingleHit;
3133
private ILProcessor _customTrackerClassConstructorIl;
3234
private TypeDefinition _customTrackerTypeDef;
3335
private MethodReference _customTrackerRegisterUnloadEventsMethod;
3436
private MethodReference _customTrackerRecordHitMethod;
3537
private List<string> _asyncMachineStateMethod;
3638

37-
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes)
39+
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes, bool singleHit)
3840
{
3941
_module = module;
4042
_identifier = identifier;
4143
_excludeFilters = excludeFilters;
4244
_includeFilters = includeFilters;
4345
_excludedFiles = excludedFiles ?? Array.Empty<string>();
4446
_excludedAttributes = excludedAttributes;
47+
_singleHit = singleHit;
4548

4649
_isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib";
4750
}
@@ -129,6 +132,8 @@ private void InstrumentModule()
129132
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath));
130133
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsResultGuid));
131134
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsMemoryMapName));
135+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(_singleHit ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0));
136+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerSingleHit));
132137

133138
if (containsAppContext)
134139
{
@@ -180,6 +185,8 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
180185
_customTrackerHitsMemoryMapName = fieldClone;
181186
else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsArray))
182187
_customTrackerHitsArray = fieldClone;
188+
else if (fieldClone.Name == nameof(ModuleTrackerTemplate.SingleHit))
189+
_customTrackerSingleHit = fieldClone;
183190
}
184191

185192
foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods)
@@ -432,9 +439,20 @@ private Instruction AddInstrumentationInstructions(MethodDefinition method, ILPr
432439
{
433440
if (_customTrackerRecordHitMethod == null)
434441
{
435-
var recordHitMethodName = _isCoreLibrary
436-
? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary)
437-
: nameof(ModuleTrackerTemplate.RecordHit);
442+
string recordHitMethodName;
443+
if (_singleHit)
444+
{
445+
recordHitMethodName = _isCoreLibrary
446+
? nameof(ModuleTrackerTemplate.RecordSingleHitInCoreLibrary)
447+
: nameof(ModuleTrackerTemplate.RecordSingleHit);
448+
}
449+
else
450+
{
451+
recordHitMethodName = _isCoreLibrary
452+
? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary)
453+
: nameof(ModuleTrackerTemplate.RecordHit);
454+
}
455+
438456
_customTrackerRecordHitMethod = new MethodReference(
439457
recordHitMethodName, method.Module.TypeSystem.Void, _customTrackerTypeDef);
440458
_customTrackerRecordHitMethod.Parameters.Add(new ParameterDefinition("hitLocationIndex", ParameterAttributes.None, method.Module.TypeSystem.Int32));

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class InstrumentationTask : Task
1414
private string _exclude;
1515
private string _excludeByFile;
1616
private string _excludeByAttribute;
17+
private bool _singleHit;
1718
private string _mergeWith;
1819
private bool _useSourceLink;
1920

@@ -59,6 +60,12 @@ public string ExcludeByAttribute
5960
set { _excludeByAttribute = value; }
6061
}
6162

63+
public bool SingleHit
64+
{
65+
get { return _singleHit; }
66+
set { _singleHit = value; }
67+
}
68+
6269
public string MergeWith
6370
{
6471
get { return _mergeWith; }
@@ -81,7 +88,7 @@ public override bool Execute()
8188
var excludedSourceFiles = _excludeByFile?.Split(',');
8289
var excludeAttributes = _excludeByAttribute?.Split(',');
8390

84-
_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith, _useSourceLink);
91+
_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _singleHit, _mergeWith, _useSourceLink);
8592
_coverage.PrepareModules();
8693
}
8794
catch (Exception ex)

src/coverlet.msbuild/coverlet.msbuild.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<Exclude Condition="$(Exclude) == ''"></Exclude>
77
<ExcludeByFile Condition="$(ExcludeByFile) == ''"></ExcludeByFile>
88
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
9+
<CoverletSingleHit Condition="'$(CoverletSingleHit)' == ''">false</CoverletSingleHit>
910
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
1011
<UseSourceLink Condition="$(UseSourceLink) == ''">false</UseSourceLink>
1112
<CoverletOutputFormat Condition="$(CoverletOutputFormat) == ''">json</CoverletOutputFormat>

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Exclude="$(Exclude)"
1313
ExcludeByFile="$(ExcludeByFile)"
1414
ExcludeByAttribute="$(ExcludeByAttribute)"
15+
SingleHit="$(CoverletSingleHit)"
1516
MergeWith="$(MergeWith)"
1617
UseSourceLink="$(UseSourceLink)" />
1718
</Target>
@@ -25,6 +26,7 @@
2526
Exclude="$(Exclude)"
2627
ExcludeByFile="$(ExcludeByFile)"
2728
ExcludeByAttribute="$(ExcludeByAttribute)"
29+
SingleHit="$(CoverletSingleHit)"
2830
MergeWith="$(MergeWith)"
2931
UseSourceLink="$(UseSourceLink)" />
3032
</Target>

src/coverlet.template/ModuleTrackerTemplate.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static class ModuleTrackerTemplate
2424
public static string HitsFilePath;
2525
public static string HitsMemoryMapName;
2626
public static int[] HitsArray;
27+
public static bool SingleHit;
2728

2829
static ModuleTrackerTemplate()
2930
{
@@ -56,6 +57,25 @@ public static void RecordHit(int hitLocationIndex)
5657
Interlocked.Increment(ref HitsArray[hitLocationIndex]);
5758
}
5859

60+
public static void RecordSingleHitInCoreLibrary(int hitLocationIndex)
61+
{
62+
// Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an
63+
// instrumented build of System.Private.CoreLib.
64+
if (HitsArray is null)
65+
return;
66+
67+
ref int location = ref HitsArray[hitLocationIndex];
68+
if (location == 0)
69+
location = 1;
70+
}
71+
72+
public static void RecordSingleHit(int hitLocationIndex)
73+
{
74+
ref int location = ref HitsArray[hitLocationIndex];
75+
if (location == 0)
76+
location = 1;
77+
}
78+
5979
public static void UnloadModule(object sender, EventArgs e)
6080
{
6181
// Claim the current hits array and reset it to prevent double-counting scenarios.
@@ -110,7 +130,10 @@ public static void UnloadModule(object sender, EventArgs e)
110130

111131
// No need to use Interlocked here since the mutex ensures only one thread updates
112132
// the shared memory map.
113-
*hitLocationArrayOffset += count;
133+
if (SingleHit)
134+
*hitLocationArrayOffset = count;
135+
else
136+
*hitLocationArrayOffset += count;
114137
}
115138
}
116139

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void TestCoverage()
3131
// Since Coverage only instruments dependancies, we need a fake module here
3232
var testModule = Path.Combine(directory.FullName, "test.module.dll");
3333

34-
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, false);
34+
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, string.Empty, false);
3535
coverage.PrepareModules();
3636

3737
// The module hit tracker must signal to Coverage that it has done its job, so call it manually

test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void TestCoreLibInstrumentation()
2727
foreach (var file in files)
2828
File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true);
2929

30-
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
30+
Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false);
3131
Assert.True(instrumenter.CanInstrument());
3232
var result = instrumenter.Instrument();
3333
Assert.NotNull(result);
@@ -119,7 +119,7 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, stri
119119
File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true);
120120

121121
module = Path.Combine(directory.FullName, destModule);
122-
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore);
122+
Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), attributesToIgnore, false);
123123
return new InstrumenterTest
124124
{
125125
Instrumenter = instrumenter,

0 commit comments

Comments
 (0)