diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs index 990274af3..164d73ec0 100644 --- a/src/coverlet.collector/DataCollection/CoverageWrapper.cs +++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs @@ -31,7 +31,8 @@ public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger settings.UseSourceLink, coverletLogger, DependencyInjection.Current.GetService(), - DependencyInjection.Current.GetService()); + DependencyInjection.Current.GetService(), + true); } /// diff --git a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs index c48c2dd86..51f3c0df6 100644 --- a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs +++ b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs @@ -53,10 +53,10 @@ public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs) try { - _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 }); - _eqtTrace.Verbose($"Called ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); + _eqtTrace.Verbose($"Calling ModuleTrackerTemplate.InProcessCollectorFlush for '{injectedInstrumentationClass.Assembly.FullName}'"); + var unloadModule = injectedInstrumentationClass.GetMethod(nameof(ModuleTrackerTemplate.InProcessCollectorFlush)); + unloadModule.Invoke(null, new object[0]); + _eqtTrace.Verbose($"Called ModuleTrackerTemplate.InProcessCollectorFlush for '{injectedInstrumentationClass.Assembly.FullName}'"); } catch (Exception ex) { diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 1a2050824..2a7469618 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -21,6 +21,7 @@ internal class Coverage private string[] _excludeAttributes; private bool _includeTestAssembly; private bool _singleHit; + private bool _isInstrumentedByOutOfProcessCollector; private string _mergeWith; private bool _useSourceLink; private ILogger _logger; @@ -45,7 +46,8 @@ public Coverage(string module, bool useSourceLink, ILogger logger, IInstrumentationHelper instrumentationHelper, - IFileSystem fileSystem) + IFileSystem fileSystem, + bool isInstrumentedByOutOfProcessCollector = false) { _module = module; _includeFilters = includeFilters; @@ -55,6 +57,7 @@ public Coverage(string module, _excludeAttributes = excludeAttributes; _includeTestAssembly = includeTestAssembly; _singleHit = singleHit; + _isInstrumentedByOutOfProcessCollector = isInstrumentedByOutOfProcessCollector; _mergeWith = mergeWith; _useSourceLink = useSourceLink; _logger = logger; @@ -97,7 +100,18 @@ public CoveragePrepareResult PrepareModules() continue; } - var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, _excludedSourceFiles, _excludeAttributes, _singleHit, _logger, _instrumentationHelper, _fileSystem); + var instrumenter = new Instrumenter(module, + _identifier, + _excludeFilters, + _includeFilters, + _excludedSourceFiles, + _excludeAttributes, + _singleHit, + _logger, + _instrumentationHelper, + _fileSystem, + _isInstrumentedByOutOfProcessCollector); + if (instrumenter.CanInstrument()) { _instrumentationHelper.BackupOriginalModule(module, _identifier); diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 4b84edd8b..b91410415 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -25,6 +25,7 @@ internal class Instrumenter private readonly ExcludedFilesHelper _excludedFilesHelper; private readonly string[] _excludedAttributes; private readonly bool _singleHit; + private readonly bool _isInstrumentedByOutOfProcessCollector; private readonly bool _isCoreLibrary; private readonly ILogger _logger; private readonly IInstrumentationHelper _instrumentationHelper; @@ -33,6 +34,7 @@ internal class Instrumenter private FieldDefinition _customTrackerHitsArray; private FieldDefinition _customTrackerHitsFilePath; private FieldDefinition _customTrackerSingleHit; + private FieldDefinition _customIsCalledByInProcessCollector; private ILProcessor _customTrackerClassConstructorIl; private TypeDefinition _customTrackerTypeDef; private MethodReference _customTrackerRegisterUnloadEventsMethod; @@ -54,7 +56,8 @@ public Instrumenter( bool singleHit, ILogger logger, IInstrumentationHelper instrumentationHelper, - IFileSystem fileSystem) + IFileSystem fileSystem, + bool isInstrumentedByOutOfProcessCollector = false) { _module = module; _identifier = identifier; @@ -63,6 +66,7 @@ public Instrumenter( _excludedFilesHelper = new ExcludedFilesHelper(excludedFiles, logger); _excludedAttributes = excludedAttributes; _singleHit = singleHit; + _isInstrumentedByOutOfProcessCollector = isInstrumentedByOutOfProcessCollector; _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; _logger = logger; _instrumentationHelper = instrumentationHelper; @@ -240,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(_isInstrumentedByOutOfProcessCollector ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customIsCalledByInProcessCollector)); if (containsAppContext) { @@ -248,7 +254,7 @@ private void InstrumentModule() // initialization of the custom tracker and the static initialization of the hosting AppDomain // (which for the core library case will be instrumented code). var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary); - var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef); + var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.AppContextOnProcessExitEvent), module.TypeSystem.Void, _customTrackerTypeDef); customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType)); @@ -291,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.IsCalledByInProcessCollector)) + _customIsCalledByInProcessCollector = 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..5573ca0bb 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 IsCalledByInProcessCollector; private static readonly bool _enableLog = int.TryParse(Environment.GetEnvironmentVariable("COVERLET_ENABLETRACKERLOG"), out int result) ? result == 1 : false; static ModuleTrackerTemplate() @@ -35,8 +36,8 @@ static ModuleTrackerTemplate() // to UnloadModule will be injected in System.AppContext.OnProcessExit. public static void RegisterUnloadEvents() { - AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); - AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); + AppDomain.CurrentDomain.ProcessExit += new EventHandler(ProcessExitEvent); + AppDomain.CurrentDomain.DomainUnload += new EventHandler(DomainUnloadEvent); } public static void RecordHitInCoreLibrary(int hitLocationIndex) @@ -73,11 +74,37 @@ public static void RecordSingleHit(int hitLocationIndex) location = 1; } - public static void UnloadModule(object sender, EventArgs e) + private static bool IsDotNetCore() + { + // object for .NET Framework is inside mscorlib.dll + return Path.GetFileName(typeof(object).Assembly.Location) == "System.Private.CoreLib.dll"; + } + + public static void ProcessExitEvent(object sender, EventArgs e) + { + Flush(nameof(ProcessExitEvent)); + } + + public static void DomainUnloadEvent(object sender, EventArgs e) + { + Flush(nameof(DomainUnloadEvent)); + } + + public static void AppContextOnProcessExitEvent(object sender, EventArgs e) + { + Flush(nameof(AppContextOnProcessExitEvent)); + } + + public static void InProcessCollectorFlush() + { + Flush(nameof(InProcessCollectorFlush)); + } + + private static void Flush(string flushType) { try { - WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}'"); + WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}' FlushType: {flushType}"); // Claim the current hits array and reset it to prevent double-counting scenarios. int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); @@ -108,7 +135,7 @@ public static void UnloadModule(object sender, EventArgs e) failedToCreateNewHitsFile = true; } - if (failedToCreateNewHitsFile) + if (failedToCreateNewHitsFile && !IsDotNetCore()) { // 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. diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs index 579e6caf3..8d3e9e300 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs @@ -9,6 +9,7 @@ using Coverlet.Core.Abstracts; using Coverlet.Core.Helpers; +using Coverlet.Core.Instrumentation; using Coverlet.Core.Reporters; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -117,7 +118,7 @@ async public static Task Run(Func callM // string hitsFilePath = (string)tracker.GetField("HitsFilePath").GetValue(null); // Void UnloadModule(System.Object, System.EventArgs) - tracker.GetTypeInfo().GetMethod("UnloadModule").Invoke(null, new object[2] { null, null }); + tracker.GetTypeInfo().GetMethod(nameof(ModuleTrackerTemplate.InProcessCollectorFlush)).Invoke(null, new object[0]); // Persist CoveragePrepareResult using (FileStream fs = new FileStream(persistPrepareResultToFile, FileMode.Open)) diff --git a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs index e0c23da31..a4bca734c 100644 --- a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs +++ b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -19,8 +19,8 @@ public TrackerContext() public void Dispose() { File.Delete(ModuleTrackerTemplate.HitsFilePath); - AppDomain.CurrentDomain.ProcessExit -= ModuleTrackerTemplate.UnloadModule; - AppDomain.CurrentDomain.DomainUnload -= ModuleTrackerTemplate.UnloadModule; + AppDomain.CurrentDomain.ProcessExit -= ModuleTrackerTemplate.ProcessExitEvent; + AppDomain.CurrentDomain.DomainUnload -= ModuleTrackerTemplate.DomainUnloadEvent; } } @@ -36,7 +36,7 @@ public void HitsFileCorrectlyWritten() { using var ctx = new TrackerContext(); ModuleTrackerTemplate.HitsArray = new[] { 1, 2, 0, 3 }; - ModuleTrackerTemplate.UnloadModule(null, null); + ModuleTrackerTemplate.InProcessCollectorFlush(); var expectedHitsArray = new[] { 1, 2, 0, 3 }; Assert.Equal(expectedHitsArray, ReadHitsFile()); @@ -54,7 +54,7 @@ public void HitsFileWithDifferentNumberOfEntriesCausesExceptionOnUnload() using var ctx = new TrackerContext(); WriteHitsFile(new[] { 1, 2, 3 }); ModuleTrackerTemplate.HitsArray = new[] { 1 }; - Assert.Throws(() => ModuleTrackerTemplate.UnloadModule(null, null)); + Assert.Throws(() => ModuleTrackerTemplate.InProcessCollectorFlush()); return _success; }); } @@ -73,7 +73,7 @@ public void HitsOnMultipleThreadsCorrectlyCounted() t.Start(i); } - ModuleTrackerTemplate.UnloadModule(null, null); + ModuleTrackerTemplate.InProcessCollectorFlush(); var expectedHitsArray = new[] { 4, 3, 2, 1 }; Assert.Equal(expectedHitsArray, ReadHitsFile()); @@ -98,10 +98,10 @@ public void MultipleSequentialUnloadsHaveCorrectTotalData() { using var ctx = new TrackerContext(); ModuleTrackerTemplate.HitsArray = new[] { 0, 3, 2, 1 }; - ModuleTrackerTemplate.UnloadModule(null, null); + ModuleTrackerTemplate.InProcessCollectorFlush(); ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 }; - ModuleTrackerTemplate.UnloadModule(null, null); + ModuleTrackerTemplate.InProcessCollectorFlush(); var expectedHitsArray = new[] { 0, 4, 4, 4 }; Assert.Equal(expectedHitsArray, ReadHitsFile()); @@ -123,7 +123,7 @@ public void MutexBlocksMultipleWriters() Assert.True(createdNew); ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 }; - var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null)); + var unloadTask = Task.Run(() => ModuleTrackerTemplate.InProcessCollectorFlush()); Assert.False(unloadTask.Wait(5)); diff --git a/test/coverlet.integration.tests/BaseTest.cs b/test/coverlet.integration.tests/BaseTest.cs index a937a52b1..43bf3bb8b 100644 --- a/test/coverlet.integration.tests/BaseTest.cs +++ b/test/coverlet.integration.tests/BaseTest.cs @@ -92,6 +92,7 @@ private protected bool RunCommand(string command, string arguments, out string s psi.WorkingDirectory = workingDirectory; psi.RedirectStandardError = true; psi.RedirectStandardOutput = true; + psi.EnvironmentVariables.Add("COVERLET_ENABLETRACKERLOG", "1"); Process commandProcess = Process.Start(psi); if (!commandProcess.WaitForExit((int)TimeSpan.FromMinutes(5).TotalMilliseconds)) {