diff --git a/coverlet.sln b/coverlet.sln index c14d01a85..98a8d4d5c 100644 --- a/coverlet.sln +++ b/coverlet.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.tests", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.console", "src\coverlet.console\coverlet.console.csproj", "{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tracker", "src\coverlet.tracker\coverlet.tracker.csproj", "{F4273009-536D-4999-A126-B0A2E3AA3E70}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.testsubject", "test\coverlet.testsubject\coverlet.testsubject.csproj", "{AE117FAA-C21D-4F23-917E-0C8050614750}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.performancetest", "test\coverlet.core.performancetest\coverlet.core.performancetest.csproj", "{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}" @@ -79,18 +77,6 @@ Global {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 - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x64.ActiveCfg = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x64.Build.0 = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x86.ActiveCfg = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Debug|x86.Build.0 = Debug|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|Any CPU.Build.0 = Release|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x64.ActiveCfg = Release|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x64.Build.0 = Release|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x86.ActiveCfg = Release|Any CPU - {F4273009-536D-4999-A126-B0A2E3AA3E70}.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 @@ -124,7 +110,6 @@ Global {FA73E423-9790-4F35-B018-3C4E3CA338BA} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD} {E7637CC6-43F7-461A-A0BF-3C14562419BD} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD} - {F4273009-536D-4999-A126-B0A2E3AA3E70} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD} {AE117FAA-C21D-4F23-917E-0C8050614750} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} EndGlobalSection diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 59439a91d..459d82914 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -164,32 +164,30 @@ private void CalculateCoverage() 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 3462fda9e..ea1fc19e1 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -40,17 +40,6 @@ public static bool HasPdb(string module) } } - 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 c89c42d75..7b0744e96 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 Coverlet.Core.Attributes; using Coverlet.Core.Helpers; using Coverlet.Core.Symbols; -using Coverlet.Tracker; using Mono.Cecil; using Mono.Cecil.Cil; @@ -22,8 +22,12 @@ internal class Instrumenter 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 _customTrackerHitsArray; + private FieldDefinition _customTrackerHitsFilePath; + private ILProcessor _customTrackerClassConstructorIl; + private TypeDefinition _customTrackerTypeDef; + private MethodReference _customTrackerRecordHitMethod; public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles) { @@ -51,7 +55,6 @@ public InstrumenterResult Instrument() }; InstrumentModule(); - InstrumentationHelper.CopyCoverletDependency(_module); return _result; } @@ -67,20 +70,119 @@ private void InstrumentModule() 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 = _customTrackerClassConstructorIl.Body.Instructions.Last(); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32)); + _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)); + module.Write(stream); } } } + private void AddCustomModuleTrackerToModule(ModuleDefinition module) + { + using (AssemblyDefinition coverletInstrumentationAssembly = AssemblyDefinition.ReadAssembly(typeof(Instrumenter).Assembly.Location)) + { + TypeDefinition moduleTrackerTemplate = coverletInstrumentationAssembly.MainModule.GetType( + "Coverlet.Core.Instrumentation", nameof(ModuleTrackerTemplate)); + + _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); + fieldClone.FieldType = module.ImportReference(fieldClone.FieldType); + + _customTrackerTypeDef.Fields.Add(fieldClone); + + if (fieldClone.Name == "HitsArray") + _customTrackerHitsArray = fieldClone; + else if (fieldClone.Name == "HitsFilePath") + _customTrackerHitsFilePath = fieldClone; + } + + foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) + { + MethodDefinition methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType); + + if (methodDef.Name == "RecordHit") + { + foreach (var parameter in methodDef.Parameters) + { + methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType))); + } + } + + 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") + _customTrackerClassConstructorIl = 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(_customTrackerHitsArray != null); + Debug.Assert(_customTrackerClassConstructorIl != null); + } + private void InstrumentType(TypeDefinition type) { var methods = type.GetMethods(); @@ -143,7 +245,7 @@ private void InstrumentIL(MethodDefinition method) foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) ReplaceExceptionHandlerBoundary(handler, instruction, target); - index += 3; + index += 2; } foreach (var _branchTarget in targetedBranchPoints) @@ -164,7 +266,7 @@ private void InstrumentIL(MethodDefinition method) foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) ReplaceExceptionHandlerBoundary(handler, instruction, target); - index += 3; + index += 2; } index++; @@ -188,17 +290,10 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor 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 - 1); } private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) @@ -225,17 +320,28 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor } ); - string marker = $"B,{document.Index},{branchPoint.StartLine},{branchPoint.Ordinal}"; + var entry = (true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal); + _result.HitCandidates.Add(entry); + + return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1); + } + + private Instruction AddInstrumentationInstructions(MethodDefinition method, ILProcessor processor, Instruction instruction, int hitEntryIndex) + { + if (_customTrackerRecordHitMethod == null) + { + _customTrackerRecordHitMethod = new MethodReference( + "RecordHit", method.Module.TypeSystem.Void, _customTrackerTypeDef); + _customTrackerRecordHitMethod.Parameters.Add(new ParameterDefinition(method.Module.TypeSystem.Int32)); + } - 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)); + var indxInstr = Instruction.Create(OpCodes.Ldc_I4, hitEntryIndex); + var callInstr = Instruction.Create(OpCodes.Call, _customTrackerRecordHitMethod); processor.InsertBefore(instruction, callInstr); - processor.InsertBefore(callInstr, markInstr); - processor.InsertBefore(markInstr, pathInstr); + processor.InsertBefore(callInstr, indxInstr); - return pathInstr; + return indxInstr; } private static void ReplaceInstructionTarget(Instruction instruction, Instruction oldTarget, Instruction newTarget) @@ -301,10 +407,5 @@ private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method) 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 dcd82694a..0060b47a8 100644 --- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs +++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs @@ -28,7 +28,6 @@ public Document() 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 @@ internal class InstrumenterResult 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/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs new file mode 100644 index 000000000..381119f33 --- /dev/null +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Coverlet.Core.Instrumentation +{ + /// + /// This static class will be injected on a module being instrumented in order to direct on module hits + /// to a single location. + /// + /// + /// As this type is going to be customized for each instrumeted module it doesn't follow typical practices + /// regarding visibility of members, etc. + /// + public static class ModuleTrackerTemplate + { + public static string HitsFilePath; + public static int[] HitsArray; + + [ThreadStatic] + private static int[] t_threadHits; + + private static List _threads; + + static ModuleTrackerTemplate() + { + _threads = new List(2 * Environment.ProcessorCount); + + AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); + AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); + // At the end of the instrumentation of a module, the instrumenter needs to add code here + // to initialize the static fields according to the values derived from the instrumentation of + // the module. + } + + public static void RecordHit(int hitLocationIndex) + { + if (t_threadHits == null) + { + lock (_threads) + { + if (t_threadHits == null) + { + t_threadHits = new int[HitsArray.Length]; + _threads.Add(t_threadHits); + } + } + } + + ++t_threadHits[hitLocationIndex]; + } + + public static void UnloadModule(object sender, EventArgs e) + { + // Update the global hits array from data from all the threads + lock (_threads) + { + foreach (var threadHits in _threads) + { + for (int i = 0; i < HitsArray.Length; ++i) + HitsArray[i] += threadHits[i]; + } + + // Prevent any double counting scenario, i.e.: UnloadModule called twice (not sure if this can happen in practice ...) + // Only an issue if DomainUnload and ProcessExit can both happens, perhaps can be removed... + _threads.Clear(); + } + + // The same module can be unloaded multiple times in the same process via different app domains. + // Use a global mutex to ensure no concurrent access. + using (var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew)) + { + if (!createdNew) + mutex.WaitOne(); + + if (!File.Exists(HitsFilePath)) + { + // File not created yet, just write it + using (var fs = new FileStream(HitsFilePath, FileMode.Create)) + using (var bw = new BinaryWriter(fs)) + { + bw.Write(HitsArray.Length); + foreach (int hitCount in HitsArray) + { + bw.Write(hitCount); + } + } + } + else + { + // Update the number of hits by adding value on disk with the ones on memory. + // This path should be triggered only in the case of multiple AppDomain unloads. + using (var fs = File.Open(HitsFilePath, FileMode.Open)) + using (var br = new BinaryReader(fs)) + using (var bw = new BinaryWriter(fs)) + { + int hitsLength = br.ReadInt32(); + if (hitsLength != HitsArray.Length) + { + throw new InvalidDataException( + $"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {HitsArray.Length}"); + } + + for (int i = 0; i < hitsLength; ++i) + { + int oldHitCount = br.ReadInt32(); + bw.Seek(-sizeof(int), SeekOrigin.Current); + bw.Write(HitsArray[i] + oldHitCount); + } + } + } + + // Prevent any double counting scenario, i.e.: UnloadModule called twice (not sure if this can happen in practice ...) + // Only an issue if DomainUnload and ProcessExit can both happens, perhaps can be removed... + Array.Clear(HitsArray, 0, HitsArray.Length); + } + } + } +} diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index 783da684f..ee0216533 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 2b7164025..000000000 --- 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 9495ca62a..000000000 --- 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 9f5c4f4ab..000000000 --- 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 172a9e4b1..000000000 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 14c2e28cf..a1cfed586 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.")] - [InlineData(150)] + [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 481d1915a..44e1021c8 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -38,17 +38,6 @@ public void TestBackupOriginalModule() 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 @@ public void TestIsValidFilterExpression() 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() { diff --git a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs new file mode 100644 index 000000000..c6539d6c3 --- /dev/null +++ b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -0,0 +1,145 @@ +using Coverlet.Core.Instrumentation; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace coverlet.core.tests.Instrumentation +{ + public class ModuleTrackerTemplateTestsFixture : IDisposable + { + public ModuleTrackerTemplateTestsFixture() + { + ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), nameof(ModuleTrackerTemplateTests)); + } + + public void Dispose() + { + AppDomain.CurrentDomain.ProcessExit -= ModuleTrackerTemplate.UnloadModule; + AppDomain.CurrentDomain.DomainUnload -= ModuleTrackerTemplate.UnloadModule; + } + } + + public class ModuleTrackerTemplateTests : IClassFixture, IDisposable + { + + public ModuleTrackerTemplateTests() + { + File.Delete(ModuleTrackerTemplate.HitsFilePath); + } + + public void Dispose() + { + File.Delete(ModuleTrackerTemplate.HitsFilePath); + } + + [Fact] + public void HitsFileCorrectlyWritten() + { + ModuleTrackerTemplate.HitsArray = new[] { 1, 2, 0, 3 }; + ModuleTrackerTemplate.UnloadModule(null, null); + + var expectedHitsArray = new[] { 1, 2, 0, 3 }; + Assert.Equal(expectedHitsArray, ReadHitsFile()); + } + + [Fact] + public void HitsFileWithDifferentNumberOfEntriesCausesExceptionOnUnload() + { + WriteHitsFile(new[] { 1, 2, 3 }); + ModuleTrackerTemplate.HitsArray = new[] { 1 }; + Assert.Throws(() => ModuleTrackerTemplate.UnloadModule(null, null)); + } + + [Fact] + public void HitsOnMultipleThreadsCorrectlyCounted() + { + ModuleTrackerTemplate.HitsArray = new[] { 0, 0, 0, 0 }; + for (int i = 0; i < ModuleTrackerTemplate.HitsArray.Length; ++i) + { + var t = new Thread(HitIndex); + t.Start(i); + } + + ModuleTrackerTemplate.UnloadModule(null, null); + var expectedHitsArray = new[] { 4, 3, 2, 1 }; + Assert.Equal(expectedHitsArray, ReadHitsFile()); + + void HitIndex(object index) + { + var hitIndex = (int)index; + for (int i = 0; i <= hitIndex; ++i) + { + ModuleTrackerTemplate.RecordHit(i); + } + } + } + + [Fact] + public void MultipleSequentialUnloadsHaveCorrectTotalData() + { + ModuleTrackerTemplate.HitsArray = new[] { 0, 3, 2, 1 }; + ModuleTrackerTemplate.UnloadModule(null, null); + + ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 }; + ModuleTrackerTemplate.UnloadModule(null, null); + + var expectedHitsArray = new[] { 0, 4, 4, 4 }; + Assert.Equal(expectedHitsArray, ReadHitsFile()); + } + + [Fact] + public async void MutexBlocksMultipleWriters() + { + using (var mutex = new Mutex( + true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew)) + { + Assert.True(createdNew); + + ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 }; + var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null)); + + Assert.False(unloadTask.Wait(5)); + + WriteHitsFile(new[] { 0, 3, 2, 1 }); + + Assert.False(unloadTask.Wait(5)); + + mutex.ReleaseMutex(); + await unloadTask; + + var expectedHitsArray = new[] { 0, 4, 4, 4 }; + Assert.Equal(expectedHitsArray, ReadHitsFile()); + } + } + + private void WriteHitsFile(int[] hitsArray) + { + using (var fs = new FileStream(ModuleTrackerTemplate.HitsFilePath, FileMode.Create)) + using (var bw = new BinaryWriter(fs)) + { + bw.Write(hitsArray.Length); + foreach (int hitCount in hitsArray) + { + bw.Write(hitCount); + } + } + } + + private int[] ReadHitsFile() + { + using (var fs = new FileStream(ModuleTrackerTemplate.HitsFilePath, FileMode.Open)) + using (var br = new BinaryReader(fs)) + { + var hitsArray = new int[br.ReadInt32()]; + for (int i = 0; i < hitsArray.Length; ++i) + { + hitsArray[i] = br.ReadInt32(); + } + + return hitsArray; + } + } + } +}