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()