diff --git a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj index 7657ba024..3000c34e8 100644 --- a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj +++ b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs index c48c2dd86..8c4dd05ef 100644 --- a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs +++ b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs @@ -55,7 +55,8 @@ public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs) { _eqtTrace.Verbose($"Calling ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); var unloadModule = injectedInstrumentationClass.GetMethod(nameof(ModuleTrackerTemplate.UnloadModule), new[] { typeof(object), typeof(EventArgs) }); - unloadModule.Invoke(null, new[] { null, EventArgs.Empty }); + unloadModule.Invoke(null, new[] { (object)this, EventArgs.Empty }); + injectedInstrumentationClass.GetField("FlushHitFile", BindingFlags.Static | BindingFlags.Public).SetValue(null, false); _eqtTrace.Verbose($"Called ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); } catch (Exception ex) diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 376bfb43f..86e32bca2 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -34,6 +34,7 @@ internal class Instrumenter private FieldDefinition _customTrackerHitsArray; private FieldDefinition _customTrackerHitsFilePath; private FieldDefinition _customTrackerSingleHit; + private FieldDefinition _customTrackerFlushHitFile; private ILProcessor _customTrackerClassConstructorIl; private TypeDefinition _customTrackerTypeDef; private MethodReference _customTrackerRegisterUnloadEventsMethod; @@ -243,6 +244,8 @@ private void InstrumentModule() _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)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4_1)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerFlushHitFile)); if (containsAppContext) { @@ -294,6 +297,8 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module) _customTrackerHitsFilePath = fieldClone; else if (fieldClone.Name == nameof(ModuleTrackerTemplate.SingleHit)) _customTrackerSingleHit = fieldClone; + else if (fieldClone.Name == nameof(ModuleTrackerTemplate.FlushHitFile)) + _customTrackerFlushHitFile = fieldClone; } foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) diff --git a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs index b3ae4e302..251dcac24 100644 --- a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -21,6 +21,7 @@ internal static class ModuleTrackerTemplate public static string HitsFilePath; public static int[] HitsArray; public static bool SingleHit; + public static bool FlushHitFile; private static readonly bool _enableLog = int.TryParse(Environment.GetEnvironmentVariable("COVERLET_ENABLETRACKERLOG"), out int result) ? result == 1 : false; static ModuleTrackerTemplate() @@ -75,84 +76,95 @@ public static void RecordSingleHit(int hitLocationIndex) public static void UnloadModule(object sender, EventArgs e) { - try + // 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)) { - WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}'"); - // Claim the current hits array and reset it to prevent double-counting scenarios. - int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); - - // 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) { - WriteLog($"Flushing hit file '{HitsFilePath}'"); - if (!createdNew) - mutex.WaitOne(); + mutex.WaitOne(); + } - bool failedToCreateNewHitsFile = false; + if (FlushHitFile) + { try { - using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew)) - using (var bw = new BinaryWriter(fs)) + // Claim the current hits array and reset it to prevent double-counting scenarios. + int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); + + WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); + WriteLog($"Flushing hit file '{HitsFilePath}'"); + + bool failedToCreateNewHitsFile = false; + try { - bw.Write(hitsArray.Length); - foreach (int hitCount in hitsArray) + using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew)) + using (var bw = new BinaryWriter(fs)) { - bw.Write(hitCount); + bw.Write(hitsArray.Length); + foreach (int hitCount in hitsArray) + { + bw.Write(hitCount); + } } } - } - catch (Exception ex) - { - WriteLog($"Failed to create new hits file '{HitsFilePath}'\n{ex}"); - failedToCreateNewHitsFile = true; - } - - if (failedToCreateNewHitsFile) - { - // 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 = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - using (var br = new BinaryReader(fs)) - using (var bw = new BinaryWriter(fs)) + catch (Exception ex) { - int hitsLength = br.ReadInt32(); - WriteLog($"Current hits found '{hitsLength}'"); - - if (hitsLength != hitsArray.Length) - { - throw new InvalidOperationException( - $"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}"); - } + WriteLog($"Failed to create new hits file '{HitsFilePath}' -> '{ex.Message}'"); + failedToCreateNewHitsFile = true; + } - for (int i = 0; i < hitsLength; ++i) + if (failedToCreateNewHitsFile) + { + // 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 = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + using (var br = new BinaryReader(fs)) + using (var bw = new BinaryWriter(fs)) { - int oldHitCount = br.ReadInt32(); - bw.Seek(-sizeof(int), SeekOrigin.Current); - if (SingleHit) - bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0); - else - bw.Write(hitsArray[i] + oldHitCount); + int hitsLength = br.ReadInt32(); + WriteLog($"Current hits found '{hitsLength}'"); + + if (hitsLength != hitsArray.Length) + { + throw new InvalidOperationException($"{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); + if (SingleHit) + { + bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0); + } + else + { + bw.Write(hitsArray[i] + oldHitCount); + } + } } } - } - WriteHits(); + WriteHits(sender); - // On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file - // this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll. - mutex.ReleaseMutex(); - WriteLog($"Hit file '{HitsFilePath}' flushed, size {new FileInfo(HitsFilePath).Length}"); + WriteLog($"Hit file '{HitsFilePath}' flushed, size {new FileInfo(HitsFilePath).Length}"); + WriteLog("--------------------------------"); + } + catch (Exception ex) + { + WriteLog(ex.ToString()); + throw; + } } - } - catch (Exception ex) - { - WriteLog(ex.ToString()); - throw; + + // On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file + // this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll. + mutex.ReleaseMutex(); } } - private static void WriteHits() + private static void WriteHits(object sender) { if (_enableLog) { @@ -172,7 +184,7 @@ private static void WriteHits() } } - File.AppendAllText(logFile, "Hits flushed"); + File.AppendAllText(logFile, $"Hits flushed file path {HitsFilePath} location '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); } } diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs index e8d17ee82..fb7a63c28 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs @@ -26,7 +26,7 @@ static class TestInstrumentationHelper /// caller sample: TestInstrumentationHelper.GenerateHtmlReport(result, sourceFileFilter: @"+**\Samples\Instrumentation.cs"); /// TestInstrumentationHelper.GenerateHtmlReport(result); /// - public static void GenerateHtmlReport(CoverageResult coverageResult, IReporter reporter = null, string sourceFileFilter = "", [CallerMemberName]string directory = "") + public static void GenerateHtmlReport(CoverageResult coverageResult, IReporter reporter = null, string sourceFileFilter = "", [CallerMemberName] string directory = "") { JsonReporter defaultReporter = new JsonReporter(); reporter ??= new CoberturaReporter(); @@ -290,5 +290,10 @@ public static void RunInProcess(this FunctionExecutor executor, Func> func) + { + Assert.Equal(0, func().Result); + } } } diff --git a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs index 4bbaccbe8..bda2ff8ea 100644 --- a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs +++ b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Coverlet.Core.Instrumentation; -using Coverlet.Tests.Xunit.Extensions; using Xunit; namespace Coverlet.Core.Tests.Instrumentation @@ -14,6 +13,7 @@ class TrackerContext : IDisposable public TrackerContext() { ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + ModuleTrackerTemplate.FlushHitFile = true; } public void Dispose()