diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 59439a9..459d829 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -164,32 +164,30 @@ namespace Coverlet.Core List documents = result.Documents.Values.ToList(); using (var fs = new FileStream(result.HitsFilePath, FileMode.Open)) - using (var sr = new StreamReader(fs)) + using (var br = new BinaryReader(fs)) { - string row; - while ((row = sr.ReadLine()) != null) + int hitCandidatesCount = br.ReadInt32(); + + // TODO: hitCandidatesCount should be verified against result.HitCandidates.Count + + var documentsList = result.Documents.Values.ToList(); + + for (int i = 0; i < hitCandidatesCount; ++i) { - var info = row.Split(','); - // Ignore malformed lines - if (info.Length != 5) - continue; + var hitLocation = result.HitCandidates[i]; - bool isBranch = info[0] == "B"; - var document = documents[int.Parse(info[1])]; + var document = documentsList[hitLocation.docIndex]; - int start = int.Parse(info[2]); - int hits = int.Parse(info[4]); + int hits = br.ReadInt32(); - if (isBranch) + if (hitLocation.isBranch) { - int ordinal = int.Parse(info[3]); - var branch = document.Branches[(start, ordinal)]; + var branch = document.Branches[(hitLocation.start, hitLocation.end)]; branch.Hits += hits; } else { - int end = int.Parse(info[3]); - for (int j = start; j <= end; j++) + for (int j = hitLocation.start; j <= hitLocation.end; j++) { var line = document.Lines[j]; line.Hits += hits; diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 3462fda..ea1fc19 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -40,17 +40,6 @@ namespace Coverlet.Core.Helpers } } - public static void CopyCoverletDependency(string module) - { - var moduleFileName = Path.GetFileName(module); - if (Path.GetFileName(typeof(Coverage).Assembly.Location) == moduleFileName) - return; - - var directory = Path.GetDirectoryName(module); - var assembly = typeof(Coverlet.Tracker.CoverageTracker).Assembly; - File.Copy(assembly.Location, Path.Combine(directory, Path.GetFileName(assembly.Location)), true); - } - public static void BackupOriginalModule(string module, string identifier) { var backupPath = GetBackupPath(module, identifier); diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index c89c42d..4541c1d 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -7,7 +8,6 @@ using System.Reflection; using Coverlet.Core.Attributes; using Coverlet.Core.Helpers; using Coverlet.Core.Symbols; -using Coverlet.Tracker; using Mono.Cecil; using Mono.Cecil.Cil; @@ -22,8 +22,11 @@ namespace Coverlet.Core.Instrumentation private readonly string[] _excludeFilters; private readonly string[] _includeFilters; private readonly string[] _excludedFiles; - private readonly static Lazy _markExecutedMethodLoader = new Lazy(GetMarkExecutedMethod); private InstrumenterResult _result; + private FieldDefinition _customModuleTrackerHitsArray; + private FieldDefinition _customModuleTrackerHitsFilePath; + private ILProcessor _customModuleTrackerClassConstructorIl; + private MethodReference _cachedInterlockedIncMethod; public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles) { @@ -51,7 +54,6 @@ namespace Coverlet.Core.Instrumentation }; InstrumentModule(); - InstrumentationHelper.CopyCoverletDependency(_module); return _result; } @@ -67,20 +69,109 @@ namespace Coverlet.Core.Instrumentation using (var module = ModuleDefinition.ReadModule(stream, parameters)) { var types = module.GetTypes(); + AddCustomModuleTrackerToModule(module); + foreach (TypeDefinition type in types) { var actualType = type.DeclaringType ?? type; if (!actualType.CustomAttributes.Any(IsExcludeAttribute) + && actualType.Namespace != "Coverlet.Core.Instrumentation.Tracker" && !InstrumentationHelper.IsTypeExcluded(_module, actualType.FullName, _excludeFilters) && InstrumentationHelper.IsTypeIncluded(_module, actualType.FullName, _includeFilters)) InstrumentType(type); } + // Fixup the custom tracker class constructor, according to all instrumented types + Instruction lastInstr = _customModuleTrackerClassConstructorIl.Body.Instructions.Last(); + _customModuleTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count)); + _customModuleTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32)); + _customModuleTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customModuleTrackerHitsArray)); + _customModuleTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath)); + _customModuleTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customModuleTrackerHitsFilePath)); + module.Write(stream); } } } + private void AddCustomModuleTrackerToModule(ModuleDefinition module) + { + using (AssemblyDefinition xad = AssemblyDefinition.ReadAssembly(typeof(Instrumenter).Assembly.Location)) + { + TypeDefinition moduleTrackerTemplate = xad.MainModule.GetType( + "Coverlet.Core.Instrumentation", nameof(ModuleTrackerTemplate)); + + TypeDefinition customTrackerTypeDef = new TypeDefinition( + "Coverlet.Core.Instrumentation.Tracker", Path.GetFileNameWithoutExtension(module.Name) + "_" + _identifier, moduleTrackerTemplate.Attributes); + + customTrackerTypeDef.BaseType = module.TypeSystem.Object; + foreach (FieldDefinition fieldDef in moduleTrackerTemplate.Fields) + { + var fieldClone = new FieldDefinition(fieldDef.Name, fieldDef.Attributes, fieldDef.FieldType); + customTrackerTypeDef.Fields.Add(fieldClone); + + if (fieldClone.Name == "HitsArray") + _customModuleTrackerHitsArray = fieldClone; + else if (fieldClone.Name == "HitsFilePath") + _customModuleTrackerHitsFilePath = fieldClone; + } + + foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) + { + MethodDefinition methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType); + + foreach (var variable in methodDef.Body.Variables) + { + methodOnCustomType.Body.Variables.Add(new VariableDefinition(module.ImportReference(variable.VariableType))); + } + + methodOnCustomType.Body.InitLocals = methodDef.Body.InitLocals; + + ILProcessor ilProcessor = methodOnCustomType.Body.GetILProcessor(); + if (methodDef.Name == ".cctor") + _customModuleTrackerClassConstructorIl = ilProcessor; + + foreach (Instruction instr in methodDef.Body.Instructions) + { + if (instr.Operand is MethodReference methodReference) + { + if (!methodReference.FullName.Contains(moduleTrackerTemplate.Namespace)) + { + // External method references, just import then + instr.Operand = module.ImportReference(methodReference); + } + else + { + // Move to the custom type + instr.Operand = new MethodReference( + methodReference.Name, methodReference.ReturnType, customTrackerTypeDef); + } + } + else if (instr.Operand is FieldReference fieldReference) + { + instr.Operand = customTrackerTypeDef.Fields.Single(fd => fd.Name == fieldReference.Name); + } + else if (instr.Operand is TypeReference typeReference) + { + instr.Operand = module.ImportReference(typeReference); + } + + ilProcessor.Append(instr); + } + + foreach (var handler in methodDef.Body.ExceptionHandlers) + methodOnCustomType.Body.ExceptionHandlers.Add(handler); + + customTrackerTypeDef.Methods.Add(methodOnCustomType); + } + + module.Types.Add(customTrackerTypeDef); + } + + Debug.Assert(_customModuleTrackerHitsArray != null); + Debug.Assert(_customModuleTrackerClassConstructorIl != null); + } + private void InstrumentType(TypeDefinition type) { var methods = type.GetMethods(); @@ -143,7 +234,7 @@ namespace Coverlet.Core.Instrumentation foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) ReplaceExceptionHandlerBoundary(handler, instruction, target); - index += 3; + index += 5; } foreach (var _branchTarget in targetedBranchPoints) @@ -164,7 +255,7 @@ namespace Coverlet.Core.Instrumentation foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) ReplaceExceptionHandlerBoundary(handler, instruction, target); - index += 3; + index += 5; } index++; @@ -188,17 +279,10 @@ namespace Coverlet.Core.Instrumentation document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); } - string marker = $"L,{document.Index},{sequencePoint.StartLine},{sequencePoint.EndLine}"; + var entry = (false, document.Index, sequencePoint.StartLine, sequencePoint.EndLine); + _result.HitCandidates.Add(entry); - 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; + return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count); } private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) @@ -225,17 +309,34 @@ namespace Coverlet.Core.Instrumentation } ); - string marker = $"B,{document.Index},{branchPoint.StartLine},{branchPoint.Ordinal}"; + var entry = (true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal); + _result.HitCandidates.Add(entry); - 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)); + return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count); + } - processor.InsertBefore(instruction, callInstr); - processor.InsertBefore(callInstr, markInstr); - processor.InsertBefore(markInstr, pathInstr); + private Instruction AddInstrumentationInstructions(MethodDefinition method, ILProcessor processor, Instruction instruction, int hitEntryIndex) + { + if (_cachedInterlockedIncMethod == null) + { + _cachedInterlockedIncMethod = new MethodReference( + "Increment", method.Module.TypeSystem.Int32, method.Module.ImportReference(typeof(System.Threading.Interlocked))); + _cachedInterlockedIncMethod.Parameters.Add(new ParameterDefinition(new ByReferenceType(method.Module.TypeSystem.Int32))); + } - return pathInstr; + var sfldInstr = Instruction.Create(OpCodes.Ldsfld, _customModuleTrackerHitsArray); + var indxInstr = Instruction.Create(OpCodes.Ldc_I4, hitEntryIndex); + var arefInstr = Instruction.Create(OpCodes.Ldelema, method.Module.TypeSystem.Int32); + var callInstr = Instruction.Create(OpCodes.Call, _cachedInterlockedIncMethod); + var popInstr = Instruction.Create(OpCodes.Pop); + + processor.InsertBefore(instruction, popInstr); + processor.InsertBefore(popInstr, callInstr); + processor.InsertBefore(callInstr, arefInstr); + processor.InsertBefore(arefInstr, indxInstr); + processor.InsertBefore(indxInstr, sfldInstr); + + return sfldInstr; } private static void ReplaceInstructionTarget(Instruction instruction, Instruction oldTarget, Instruction newTarget) @@ -301,10 +402,5 @@ namespace Coverlet.Core.Instrumentation return null; } } - - private static MethodInfo GetMarkExecutedMethod() - { - return typeof(CoverageTracker).GetMethod(nameof(CoverageTracker.MarkExecuted)); - } } } \ No newline at end of file diff --git a/src/coverlet.core/Instrumentation/InstrumenterResult.cs b/src/coverlet.core/Instrumentation/InstrumenterResult.cs index dcd8269..0060b47 100644 --- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs +++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs @@ -28,7 +28,6 @@ namespace Coverlet.Core.Instrumentation public string Path; public int Index; - public Dictionary Lines { get; private set; } public Dictionary<(int Line, int Ordinal), Branch> Branches { get; private set; } } @@ -38,11 +37,13 @@ namespace Coverlet.Core.Instrumentation public InstrumenterResult() { Documents = new Dictionary(); - } + HitCandidates = new List<(bool isBranch, int docIndex, int start, int end)>(); + } public string Module; public string HitsFilePath; public string ModulePath; public Dictionary Documents { get; private set; } + public List<(bool isBranch, int docIndex, int start, int end)> HitCandidates { get; private set; } } } \ No newline at end of file diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index 783da68..ee02165 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -12,8 +12,4 @@ - - - - diff --git a/src/coverlet.tracker/Extensions/DictionaryExtensions.cs b/src/coverlet.tracker/Extensions/DictionaryExtensions.cs deleted file mode 100644 index 2b71640..0000000 --- a/src/coverlet.tracker/Extensions/DictionaryExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Coverlet.Tracker.Extensions -{ - internal static class DictionaryExtensions - { - [ExcludeFromCodeCoverage] - public static bool TryAdd(this Dictionary dictionary, T key, U value) - { - if (dictionary.ContainsKey(key)) - return false; - - dictionary.Add(key, value); - return true; - } - } -} \ No newline at end of file diff --git a/src/coverlet.tracker/Properties/AssemblyInfo.cs b/src/coverlet.tracker/Properties/AssemblyInfo.cs deleted file mode 100644 index 9495ca6..0000000 --- a/src/coverlet.tracker/Properties/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.tracker.snk")] \ No newline at end of file diff --git a/src/coverlet.tracker/coverlet.tracker.csproj b/src/coverlet.tracker/coverlet.tracker.csproj deleted file mode 100644 index 9f5c4f4..0000000 --- a/src/coverlet.tracker/coverlet.tracker.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netstandard2.0 - - - diff --git a/src/coverlet.tracker/coverlet.tracker.snk b/src/coverlet.tracker/coverlet.tracker.snk deleted file mode 100644 index 172a9e4..0000000 Binary files a/src/coverlet.tracker/coverlet.tracker.snk and /dev/null differ diff --git a/test/coverlet.core.performancetest/PerformanceTest.cs b/test/coverlet.core.performancetest/PerformanceTest.cs index 14c2e28..37aebdb 100644 --- a/test/coverlet.core.performancetest/PerformanceTest.cs +++ b/test/coverlet.core.performancetest/PerformanceTest.cs @@ -14,8 +14,9 @@ namespace coverlet.core.performancetest /// public class PerformanceTest { - [Theory(Skip = "Only enabled when explicitly testing performance.")] + [Theory(/*Skip = "Only enabled when explicitly testing performance."*/)] [InlineData(150)] + // [InlineData(20_000)] public void TestPerformance(int iterations) { var big = new BigClass(); diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 481d191..44e1021 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -38,17 +38,6 @@ namespace Coverlet.Core.Helpers.Tests Assert.True(File.Exists(backupPath)); } - [Fact] - public void TestCopyCoverletDependency() - { - var tempPath = Path.GetTempPath(); - var directory = Directory.CreateDirectory(Path.Combine(tempPath, "tempdir")); - InstrumentationHelper.CopyCoverletDependency(Path.Combine(directory.FullName, "somemodule.dll")); - - Assert.True(File.Exists(Path.Combine(directory.FullName, "coverlet.tracker.dll"))); - Directory.Delete(directory.FullName, true); - } - [Fact] public void TestIsValidFilterExpression() { @@ -64,17 +53,6 @@ namespace Coverlet.Core.Helpers.Tests Assert.False(InstrumentationHelper.IsValidFilterExpression(null)); } - [Fact] - public void TestDontCopyCoverletDependency() - { - var tempPath = Path.GetTempPath(); - var directory = Directory.CreateDirectory(Path.Combine(tempPath, "tempdir")); - InstrumentationHelper.CopyCoverletDependency(Path.Combine(directory.FullName, "coverlet.core.dll")); - - Assert.False(File.Exists(Path.Combine(directory.FullName, "coverlet.core.dll"))); - Directory.Delete(directory.FullName, true); - } - [Fact] public void TestDeleteHitsFile() {