diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index be50c97c1..1028bc831 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -37,6 +37,7 @@ static int Main(string[] args) CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue); CommandOption includeDirectories = app.Option("--include-directory", "Include directories containing additional assemblies to be instrumented.", CommandOptionType.MultipleValue); CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue); + CommandOption singleHit = app.Option("--single-hit", "Specifies whether to limit code coverage hit reporting to a single hit for each location", CommandOptionType.NoValue); CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue); CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue); @@ -48,7 +49,7 @@ static int Main(string[] args) if (!target.HasValue()) throw new CommandParsingException(app, "Target must be specified."); - 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()); + 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()); coverage.PrepareModules(); Process process = new Process(); diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index cd1cf6af1..e72a63992 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -22,6 +22,7 @@ public class Coverage private string[] _excludeFilters; private string[] _excludedSourceFiles; private string[] _excludeAttributes; + private bool _singleHit; private string _mergeWith; private bool _useSourceLink; private List _results; @@ -31,7 +32,7 @@ public string Identifier get { return _identifier; } } - public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith, bool useSourceLink) + public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, bool singleHit, string mergeWith, bool useSourceLink) { _module = module; _includeFilters = includeFilters; @@ -39,6 +40,7 @@ public Coverage(string module, string[] includeFilters, string[] includeDirector _excludeFilters = excludeFilters; _excludedSourceFiles = excludedSourceFiles; _excludeAttributes = excludeAttributes; + _singleHit = singleHit; _mergeWith = mergeWith; _useSourceLink = useSourceLink; @@ -59,7 +61,7 @@ public void PrepareModules() !InstrumentationHelper.IsModuleIncluded(module, _includeFilters)) continue; - var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes); + var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes, _singleHit); if (instrumenter.CanInstrument()) { InstrumentationHelper.BackupOriginalModule(module, _identifier); diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 58fc5f9f9..2bece3356 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -23,17 +23,19 @@ internal class Instrumenter private readonly string[] _includeFilters; private readonly string[] _excludedFiles; private readonly string[] _excludedAttributes; + private readonly bool _singleHit; private readonly bool _isCoreLibrary; private InstrumenterResult _result; private FieldDefinition _customTrackerHitsArray; private FieldDefinition _customTrackerHitsFilePath; + private FieldDefinition _customTrackerSingleHit; private ILProcessor _customTrackerClassConstructorIl; private TypeDefinition _customTrackerTypeDef; private MethodReference _customTrackerRegisterUnloadEventsMethod; private MethodReference _customTrackerRecordHitMethod; private List _asyncMachineStateMethod; - public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes) + public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes, bool singleHit) { _module = module; _identifier = identifier; @@ -41,6 +43,7 @@ public Instrumenter(string module, string identifier, string[] excludeFilters, s _includeFilters = includeFilters; _excludedFiles = excludedFiles ?? Array.Empty(); _excludedAttributes = excludedAttributes; + _singleHit = singleHit; _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; } @@ -125,6 +128,8 @@ private void InstrumentModule() _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(_singleHit ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerSingleHit)); if (containsAppContext) { @@ -174,6 +179,8 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module) _customTrackerHitsArray = fieldClone; else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsFilePath)) _customTrackerHitsFilePath = fieldClone; + else if (fieldClone.Name == nameof(ModuleTrackerTemplate.SingleHit)) + _customTrackerSingleHit = fieldClone; } foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) @@ -426,9 +433,20 @@ private Instruction AddInstrumentationInstructions(MethodDefinition method, ILPr { if (_customTrackerRecordHitMethod == null) { - var recordHitMethodName = _isCoreLibrary - ? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary) - : nameof(ModuleTrackerTemplate.RecordHit); + string recordHitMethodName; + if (_singleHit) + { + recordHitMethodName = _isCoreLibrary + ? nameof(ModuleTrackerTemplate.RecordSingleHitInCoreLibrary) + : nameof(ModuleTrackerTemplate.RecordSingleHit); + } + else + { + recordHitMethodName = _isCoreLibrary + ? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary) + : nameof(ModuleTrackerTemplate.RecordHit); + } + _customTrackerRecordHitMethod = new MethodReference( recordHitMethodName, method.Module.TypeSystem.Void, _customTrackerTypeDef); _customTrackerRecordHitMethod.Parameters.Add(new ParameterDefinition("hitLocationIndex", ParameterAttributes.None, method.Module.TypeSystem.Int32)); diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs index 542f98c1f..4883586bf 100644 --- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs +++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs @@ -14,6 +14,7 @@ public class InstrumentationTask : Task private string _exclude; private string _excludeByFile; private string _excludeByAttribute; + private bool _singleHit; private string _mergeWith; private bool _useSourceLink; @@ -59,6 +60,12 @@ public string ExcludeByAttribute set { _excludeByAttribute = value; } } + public bool SingleHit + { + get { return _singleHit; } + set { _singleHit = value; } + } + public string MergeWith { get { return _mergeWith; } @@ -81,7 +88,7 @@ public override bool Execute() var excludedSourceFiles = _excludeByFile?.Split(','); var excludeAttributes = _excludeByAttribute?.Split(','); - _coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith, _useSourceLink); + _coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _singleHit, _mergeWith, _useSourceLink); _coverage.PrepareModules(); } catch (Exception ex) diff --git a/src/coverlet.msbuild/coverlet.msbuild.props b/src/coverlet.msbuild/coverlet.msbuild.props index 4be3b8011..9910db827 100644 --- a/src/coverlet.msbuild/coverlet.msbuild.props +++ b/src/coverlet.msbuild/coverlet.msbuild.props @@ -6,6 +6,7 @@ + false false json diff --git a/src/coverlet.msbuild/coverlet.msbuild.targets b/src/coverlet.msbuild/coverlet.msbuild.targets index ee5cd1c82..c673586a1 100644 --- a/src/coverlet.msbuild/coverlet.msbuild.targets +++ b/src/coverlet.msbuild/coverlet.msbuild.targets @@ -12,6 +12,7 @@ Exclude="$(Exclude)" ExcludeByFile="$(ExcludeByFile)" ExcludeByAttribute="$(ExcludeByAttribute)" + SingleHit="$(CoverletSingleHit)" MergeWith="$(MergeWith)" UseSourceLink="$(UseSourceLink)" /> @@ -25,6 +26,7 @@ Exclude="$(Exclude)" ExcludeByFile="$(ExcludeByFile)" ExcludeByAttribute="$(ExcludeByAttribute)" + SingleHit="$(CoverletSingleHit)" MergeWith="$(MergeWith)" UseSourceLink="$(UseSourceLink)" /> diff --git a/src/coverlet.template/ModuleTrackerTemplate.cs b/src/coverlet.template/ModuleTrackerTemplate.cs index 2bb959971..4689a56f3 100644 --- a/src/coverlet.template/ModuleTrackerTemplate.cs +++ b/src/coverlet.template/ModuleTrackerTemplate.cs @@ -18,6 +18,7 @@ public static class ModuleTrackerTemplate { public static string HitsFilePath; public static int[] HitsArray; + public static bool SingleHit; static ModuleTrackerTemplate() { @@ -50,6 +51,25 @@ public static void RecordHit(int hitLocationIndex) Interlocked.Increment(ref HitsArray[hitLocationIndex]); } + public static void RecordSingleHitInCoreLibrary(int hitLocationIndex) + { + // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an + // instrumented build of System.Private.CoreLib. + if (HitsArray is null) + return; + + ref int location = ref HitsArray[hitLocationIndex]; + if (location == 0) + location = 1; + } + + public static void RecordSingleHit(int hitLocationIndex) + { + ref int location = ref HitsArray[hitLocationIndex]; + if (location == 0) + location = 1; + } + public static void UnloadModule(object sender, EventArgs e) { // Claim the current hits array and reset it to prevent double-counting scenarios. @@ -99,7 +119,10 @@ public static void UnloadModule(object sender, EventArgs e) { int oldHitCount = br.ReadInt32(); bw.Seek(-sizeof(int), SeekOrigin.Current); - bw.Write(hitsArray[i] + oldHitCount); + if (SingleHit) + bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0); + else + bw.Write(hitsArray[i] + oldHitCount); } } } diff --git a/test/coverlet.core.tests/CoverageTests.cs b/test/coverlet.core.tests/CoverageTests.cs index b757fe664..f02d7fed2 100644 --- a/test/coverlet.core.tests/CoverageTests.cs +++ b/test/coverlet.core.tests/CoverageTests.cs @@ -24,7 +24,7 @@ public void TestCoverage() // TODO: Find a way to mimick hits - var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), string.Empty, false); + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, string.Empty, false); coverage.PrepareModules(); var result = coverage.GetCoverageResult(); diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 721a0edf3..e67bc2084 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -27,7 +27,7 @@ public void TestCoreLibInstrumentation() foreach (var file in files) File.Copy(Path.Combine(OriginalFilesDir, file), Path.Combine(TestFilesDir, file), overwrite: true); - Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); + Instrumenter instrumenter = new Instrumenter(Path.Combine(TestFilesDir, files[0]), "_coverlet_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false); Assert.True(instrumenter.CanInstrument()); var result = instrumenter.Instrument(); Assert.NotNull(result); @@ -119,7 +119,7 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, stri File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true); module = Path.Combine(directory.FullName, destModule); - Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty(), Array.Empty(), Array.Empty(), attributesToIgnore); + Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty(), Array.Empty(), Array.Empty(), attributesToIgnore, false); return new InstrumenterTest { Instrumenter = instrumenter,