diff --git a/README.md b/README.md index 9a10853..e961ea7 100644 --- a/README.md +++ b/README.md @@ -136,12 +136,12 @@ The currently supported [TeamCity statistics](https://confluence.jetbrains.com/d | TeamCity Statistic Key | Description | | :--- | :--- | | CodeCoverageL | Line-level code coverage | -| CodeCoverageC | Class-level code coverage | +| CodeCoverageR | Branch-level code coverage | | CodeCoverageM | Method-level code coverage | | CodeCoverageAbsLTotal | The total number of lines | | CodeCoverageAbsLCovered | The number of covered lines | -| CodeCoverageAbsCTotal | The total number of classes | -| CodeCoverageAbsCCovered | The number of covered classes | +| CodeCoverageAbsRTotal | The total number of branches | +| CodeCoverageAbsRCovered | The number of covered branches | | CodeCoverageAbsMTotal | The total number of methods | | CodeCoverageAbsMCovered | The number of covered methods | diff --git a/coverlet.sln b/coverlet.sln index 6f42d08..910bd9e 100644 --- a/coverlet.sln +++ b/coverlet.sln @@ -24,97 +24,37 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.ActiveCfg = Debug|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x64.Build.0 = Debug|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.ActiveCfg = Debug|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Debug|x86.Build.0 = Debug|Any CPU {31084026-D563-4B91-BE71-174C4270CCF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {31084026-D563-4B91-BE71-174C4270CCF4}.Release|Any CPU.Build.0 = Release|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.ActiveCfg = Release|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x64.Build.0 = Release|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.ActiveCfg = Release|Any CPU - {31084026-D563-4B91-BE71-174C4270CCF4}.Release|x86.Build.0 = Release|Any CPU {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.ActiveCfg = Debug|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x64.Build.0 = Debug|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.ActiveCfg = Debug|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Debug|x86.Build.0 = Debug|Any CPU {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|Any CPU.Build.0 = Release|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.ActiveCfg = Release|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x64.Build.0 = Release|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.ActiveCfg = Release|Any CPU - {FA73E423-9790-4F35-B018-3C4E3CA338BA}.Release|x86.Build.0 = Release|Any CPU {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.ActiveCfg = Debug|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x64.Build.0 = Debug|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.ActiveCfg = Debug|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Debug|x86.Build.0 = Debug|Any CPU {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|Any CPU.Build.0 = Release|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.ActiveCfg = Release|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x64.Build.0 = Release|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.ActiveCfg = Release|Any CPU - {E7637CC6-43F7-461A-A0BF-3C14562419BD}.Release|x86.Build.0 = Release|Any CPU {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.ActiveCfg = Debug|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x64.Build.0 = Debug|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.ActiveCfg = Debug|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Debug|x86.Build.0 = Debug|Any CPU {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|Any CPU.Build.0 = Release|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.ActiveCfg = Release|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x64.Build.0 = Release|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.ActiveCfg = Release|Any CPU - {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}.Release|x86.Build.0 = Release|Any CPU {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.ActiveCfg = Debug|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.Build.0 = Debug|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.ActiveCfg = Debug|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.Build.0 = Debug|Any CPU {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.Build.0 = Release|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.ActiveCfg = Release|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.Build.0 = Release|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.ActiveCfg = Release|Any CPU - {AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.Build.0 = Release|Any CPU {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.ActiveCfg = Debug|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.Build.0 = Debug|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.Build.0 = Debug|Any CPU {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.Build.0 = Release|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.ActiveCfg = Release|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.Build.0 = Release|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.ActiveCfg = Release|Any CPU - {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.Build.0 = Release|Any CPU {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|x64.ActiveCfg = Debug|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|x64.Build.0 = Debug|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|x86.ActiveCfg = Debug|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Debug|x86.Build.0 = Debug|Any CPU {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|Any CPU.Build.0 = Release|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|x64.ActiveCfg = Release|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|x64.Build.0 = Release|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|x86.ActiveCfg = Release|Any CPU - {FF16BD00-4BE7-41F3-95AE-F69B56E5E254}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index be50c97..1028bc8 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -37,6 +37,7 @@ namespace Coverlet.Console 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 @@ namespace Coverlet.Console 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 6f80780..6cc2896 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -22,6 +22,7 @@ namespace Coverlet.Core 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 @@ namespace Coverlet.Core 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 @@ namespace Coverlet.Core _excludeFilters = excludeFilters; _excludedSourceFiles = excludedSourceFiles; _excludeAttributes = excludeAttributes; + _singleHit = singleHit; _mergeWith = mergeWith; _useSourceLink = useSourceLink; @@ -59,7 +61,7 @@ namespace Coverlet.Core !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/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index fcc54d5..8e9bdc3 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -48,11 +48,7 @@ namespace Coverlet.Core.Helpers } // The module's name must be unique. - // Add the test module itself to exclude it from the files enumeration. - var uniqueModules = new HashSet - { - Path.GetFileName(module) - }; + var uniqueModules = new HashSet(); return dirs.SelectMany(d => Directory.EnumerateFiles(d)) .Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m))) @@ -86,12 +82,20 @@ namespace Coverlet.Core.Helpers public static void BackupOriginalModule(string module, string identifier) { var backupPath = GetBackupPath(module, identifier); + var backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); File.Copy(module, backupPath, true); + + var symbolFile = Path.ChangeExtension(module, ".pdb"); + if (File.Exists(symbolFile)) + { + File.Copy(symbolFile, backupSymbolPath, true); + } } public static void RestoreOriginalModule(string module, string identifier) { var backupPath = GetBackupPath(module, identifier); + var backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); // Restore the original module - retry up to 10 times, since the destination file could be locked // See: https://github.com/tonerdo/coverlet/issues/25 @@ -102,6 +106,12 @@ namespace Coverlet.Core.Helpers File.Copy(backupPath, module, true); File.Delete(backupPath); }, retryStrategy, 10); + + RetryHelper.Retry(() => + { + File.Copy(backupSymbolPath, Path.ChangeExtension(module, ".pdb"), true); + File.Delete(backupSymbolPath); + }, retryStrategy, 10); } public static void DeleteHitsFile(string path) diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 82ed6f9..2bece33 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -23,17 +23,19 @@ namespace Coverlet.Core.Instrumentation 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 @@ namespace Coverlet.Core.Instrumentation _includeFilters = includeFilters; _excludedFiles = excludedFiles ?? Array.Empty(); _excludedAttributes = excludedAttributes; + _singleHit = singleHit; _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; } @@ -125,6 +128,8 @@ namespace Coverlet.Core.Instrumentation _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) { @@ -147,7 +152,7 @@ namespace Coverlet.Core.Instrumentation onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, customTrackerUnloadModule)); } - module.Write(stream); + module.Write(stream, new WriterParameters { WriteSymbols = true }); } } } @@ -170,10 +175,12 @@ namespace Coverlet.Core.Instrumentation _customTrackerTypeDef.Fields.Add(fieldClone); - if (fieldClone.Name == "HitsArray") + if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsArray)) _customTrackerHitsArray = fieldClone; - else if (fieldClone.Name == "HitsFilePath") + 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 @@ namespace Coverlet.Core.Instrumentation { 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.core/Reporters/OpenCoverReporter.cs b/src/coverlet.core/Reporters/OpenCoverReporter.cs index 28698bd..2d397c3 100644 --- a/src/coverlet.core/Reporters/OpenCoverReporter.cs +++ b/src/coverlet.core/Reporters/OpenCoverReporter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -76,8 +77,8 @@ namespace Coverlet.Core.Reporters method.Add(new XAttribute("cyclomaticComplexity", methCyclomaticComplexity.ToString())); method.Add(new XAttribute("nPathComplexity", "0")); - method.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString())); - method.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString())); + method.Add(new XAttribute("sequenceCoverage", Math.Round(methLineCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); + method.Add(new XAttribute("branchCoverage", Math.Round(methBranchCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); 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())); @@ -157,8 +158,8 @@ namespace Coverlet.Core.Reporters 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("sequenceCoverage", Math.Round(methLineCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); + methodSummary.Add(new XAttribute("branchCoverage", Math.Round(methBranchCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); methodSummary.Add(new XAttribute("maxCyclomaticComplexity", methCyclomaticComplexity.ToString())); methodSummary.Add(new XAttribute("minCyclomaticComplexity", methCyclomaticComplexity.ToString())); methodSummary.Add(new XAttribute("visitedClasses", "0")); @@ -191,8 +192,8 @@ namespace Coverlet.Core.Reporters 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("sequenceCoverage", Math.Round(classLineCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); + classSummary.Add(new XAttribute("branchCoverage", Math.Round(classBranchCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); classSummary.Add(new XAttribute("maxCyclomaticComplexity", classMaxCyclomaticComplexity.ToString())); classSummary.Add(new XAttribute("minCyclomaticComplexity", classMinCyclomaticComplexity.ToString())); classSummary.Add(new XAttribute("visitedClasses", classVisited ? "1" : "0")); @@ -214,7 +215,7 @@ namespace Coverlet.Core.Reporters } var moduleLineCoverage = summary.CalculateLineCoverage(result.Modules); - var moduleBranchCoverage = summary.CalculateLineCoverage(result.Modules); + var moduleBranchCoverage = summary.CalculateBranchCoverage(result.Modules); var moduleMaxCyclomaticComplexity = summary.CalculateMaxCyclomaticComplexity(result.Modules); var moduleMinCyclomaticComplexity = summary.CalculateMinCyclomaticComplexity(result.Modules); @@ -222,8 +223,8 @@ namespace Coverlet.Core.Reporters 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("sequenceCoverage", Math.Round(moduleLineCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); + coverageSummary.Add(new XAttribute("branchCoverage", Math.Round(moduleBranchCoverage.Percent * 100, 2).ToString("G", CultureInfo.InvariantCulture))); coverageSummary.Add(new XAttribute("maxCyclomaticComplexity", moduleMaxCyclomaticComplexity.ToString())); coverageSummary.Add(new XAttribute("minCyclomaticComplexity", moduleMinCyclomaticComplexity.ToString())); coverageSummary.Add(new XAttribute("visitedClasses", visitedClasses.ToString())); diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs index 542f98c..4883586 100644 --- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs +++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs @@ -14,6 +14,7 @@ namespace Coverlet.MSbuild.Tasks private string _exclude; private string _excludeByFile; private string _excludeByAttribute; + private bool _singleHit; private string _mergeWith; private bool _useSourceLink; @@ -59,6 +60,12 @@ namespace Coverlet.MSbuild.Tasks set { _excludeByAttribute = value; } } + public bool SingleHit + { + get { return _singleHit; } + set { _singleHit = value; } + } + public string MergeWith { get { return _mergeWith; } @@ -81,7 +88,7 @@ namespace Coverlet.MSbuild.Tasks 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.tasks/coverlet.msbuild.props b/src/coverlet.msbuild.tasks/coverlet.msbuild.props index 327d932..197a6e9 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.props +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.props @@ -6,6 +6,7 @@ + false false json diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets index 00ae252..f23b89c 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets +++ b/src/coverlet.msbuild.tasks/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 2bb9599..4689a56 100644 --- a/src/coverlet.template/ModuleTrackerTemplate.cs +++ b/src/coverlet.template/ModuleTrackerTemplate.cs @@ -18,6 +18,7 @@ namespace Coverlet.Core.Instrumentation { public static string HitsFilePath; public static int[] HitsArray; + public static bool SingleHit; static ModuleTrackerTemplate() { @@ -50,6 +51,25 @@ namespace Coverlet.Core.Instrumentation 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 @@ namespace Coverlet.Core.Instrumentation { 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 5aba981..f02d7fe 100644 --- a/test/coverlet.core.tests/CoverageTests.cs +++ b/test/coverlet.core.tests/CoverageTests.cs @@ -24,10 +24,7 @@ namespace Coverlet.Core.Tests // 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, 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/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index af6fa73..82d2d60 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -13,7 +13,7 @@ namespace Coverlet.Core.Helpers.Tests { string module = typeof(InstrumentationHelperTests).Assembly.Location; var modules = InstrumentationHelper.GetCoverableModules(module, Array.Empty()); - Assert.False(Array.Exists(modules, m => m == module)); + Assert.True(Array.Exists(modules, m => m == module)); } [Fact] diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 721a0ed..e67bc20 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -27,7 +27,7 @@ namespace Coverlet.Core.Instrumentation.Tests 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 @@ namespace Coverlet.Core.Instrumentation.Tests 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, diff --git a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs index f17452b..7fb3717 100644 --- a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs @@ -1,5 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; using Xunit; namespace Coverlet.Core.Reporters.Tests @@ -16,7 +20,11 @@ namespace Coverlet.Core.Reporters.Tests result.Modules.Add("Coverlet.Core.Reporters.Tests", CreateFirstDocuments()); OpenCoverReporter reporter = new OpenCoverReporter(); - Assert.NotEqual(string.Empty, reporter.Report(result)); + string report = reporter.Report(result); + Assert.NotEmpty(report); + XDocument doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); + Assert.Empty(doc.Descendants().Attributes("sequenceCoverage").Where(v => v.Value != "33.3")); + Assert.Empty(doc.Descendants().Attributes("branchCoverage").Where(v => v.Value != "25")); } [Fact] @@ -42,10 +50,13 @@ namespace Coverlet.Core.Reporters.Tests Lines lines = new Lines(); lines.Add(1, 1); lines.Add(2, 0); + lines.Add(3, 0); Branches branches = new Branches(); branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 41, Path = 0, Ordinal = 3 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 44, Path = 1, Ordinal = 4 }); Methods methods = new Methods(); var methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; diff --git a/test/coverlet.core.tests/Samples/Samples.cs b/test/coverlet.core.tests/Samples/Samples.cs index 43b9bcb..2c178c9 100644 --- a/test/coverlet.core.tests/Samples/Samples.cs +++ b/test/coverlet.core.tests/Samples/Samples.cs @@ -122,6 +122,14 @@ namespace Coverlet.Core.Samples.Tests return value; } + /// + /// This method is used by a unit test that verifies the behavior of the instrumentation process on an assembly + /// which is not instrumented. Excluding this method from code coverage prevents the bytecode for this reference + /// method from getting modified prior to test execution so it retains its original form for the test. This is + /// not a problem for the test because the instrumentation process only runs on assemblies which have not + /// already been instrumented. + /// + [ExcludeFromCodeCoverage] public void HasSimpleTaskWithLambda() { var t = new Task(() => { }); diff --git a/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs index c37a1d2..c2831fa 100644 --- a/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs +++ b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs @@ -28,7 +28,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSingleDecision")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSingleDecision)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -50,7 +50,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSimpleUsingStatement")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleUsingStatement)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -63,7 +63,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSimpleTaskWithLambda")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleTaskWithLambda)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -76,7 +76,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasTwoDecisions")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasTwoDecisions)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -95,7 +95,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasCompleteIf")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasCompleteIf)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -113,7 +113,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSwitch")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitch)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -136,7 +136,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithDefault")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithDefault)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -159,7 +159,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithBreaks")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithBreaks)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -182,7 +182,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::HasSwitchWithMultipleCases")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithMultipleCases)}")); // act var points = CecilSymbolHelper.GetBranchPoints(method); @@ -226,7 +226,7 @@ namespace Coverlet.Core.Symbols.Tests { // arrange var type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - var method = type.Methods.First(x => x.FullName.Contains("::UsingWithException_Issue243")); + var method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.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