diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 864df117f..5ecd4c1a8 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -49,6 +49,7 @@ static int Main(string[] args) var doesNotReturnAttributes = new Option("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true }; var excludeAssembliesWithoutSources = new Option("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne }; var sourceMappingFile = new Option("--source-mapping-file", "Specifies the path to a SourceRootsMappings file.") { Arity = ArgumentArity.ZeroOrOne }; + var unloadCoverletFromModulesOnly = new Option("---only-unload-modules", "Specifies Whether or not coverlet will only unload after unit tests are finished and skip coverage calculation"){ Arity = ArgumentArity.ZeroOrOne }; RootCommand rootCommand = new() { @@ -73,7 +74,8 @@ static int Main(string[] args) useSourceLink, doesNotReturnAttributes, excludeAssembliesWithoutSources, - sourceMappingFile + sourceMappingFile, + unloadCoverletFromModulesOnly }; rootCommand.Description = "Cross platform .NET Core code coverage tool"; @@ -102,6 +104,7 @@ static int Main(string[] args) string[] doesNotReturnAttributesValue = context.ParseResult.GetValueForOption(doesNotReturnAttributes); string excludeAssembliesWithoutSourcesValue = context.ParseResult.GetValueForOption(excludeAssembliesWithoutSources); string sourceMappingFileValue = context.ParseResult.GetValueForOption(sourceMappingFile); + bool unloadCoverletFromModulesOnlyBool = context.ParseResult.GetValueForOption(unloadCoverletFromModulesOnly); if (string.IsNullOrEmpty(moduleOrAppDirectoryValue) || string.IsNullOrWhiteSpace(moduleOrAppDirectoryValue)) throw new ArgumentException("No test assembly or application directory specified."); @@ -127,7 +130,8 @@ static int Main(string[] args) useSourceLinkValue, doesNotReturnAttributesValue, excludeAssembliesWithoutSourcesValue, - sourceMappingFileValue); + sourceMappingFileValue, + unloadCoverletFromModulesOnlyBool); context.ExitCode = taskStatus; }); @@ -154,7 +158,8 @@ private static Task HandleCommand(string moduleOrAppDirectory, bool useSourceLink, string[] doesNotReturnAttributes, string excludeAssembliesWithoutSources, - string sourceMappingFile + string sourceMappingFile, + bool unloadCoverletFromModulesOnly ) { @@ -232,6 +237,12 @@ string sourceMappingFile string dOutput = output != null ? output : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); + if (unloadCoverletFromModulesOnly) + { + int unloadModuleExitCode = coverage.UnloadModules(); + return Task.FromResult(unloadModuleExitCode); + } + logger.LogInformation("\nCalculating coverage result..."); CoverageResult result = coverage.GetCoverageResult(); @@ -385,7 +396,6 @@ string sourceMappingFile return Task.FromResult(exitCode); - } catch (Win32Exception we) when (we.Source == "System.Diagnostics.Process") diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 07c0297fa..70809cd6a 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -241,7 +241,7 @@ public CoverageResult GetCoverageResult() } modules.Add(Path.GetFileName(result.ModulePath), documents); - _instrumentationHelper.RestoreOriginalModule(result.ModulePath, Identifier); + UnloadModule(result.ModulePath); } // In case of anonymous delegate compiler generate a custom class and passes it as type.method delegate. @@ -326,6 +326,52 @@ public CoverageResult GetCoverageResult() return coverageResult; } + /// + /// unloads all modules that were instrumented + /// + /// exit code of module unloading + public int UnloadModules() + { + string[] modules = _instrumentationHelper.GetCoverableModules(_moduleOrAppDirectory, + _parameters.IncludeDirectories, _parameters.IncludeTestAssembly); + + var validModules = _instrumentationHelper + .SelectModules(modules, _parameters.IncludeFilters, _parameters.ExcludeFilters); + var validModulesAsList = validModules.ToList(); + foreach (string modulePath in validModulesAsList) { + try + { + _instrumentationHelper.RestoreOriginalModule(modulePath, Identifier); + _logger.LogVerbose("All Modules unloaded."); + } + catch (Exception e) + { + _logger.LogVerbose($"{e.InnerException} occured, module unloading aborted."); + return -1; + } + } + + return 0; + } + + /// + /// Invoke the unloading of modules and restoration of the original assembly files + /// + /// + /// exist code of unloading modules + public void UnloadModule(string modulePath) + { + try + { + _instrumentationHelper.RestoreOriginalModule(modulePath, Identifier); + _logger.LogVerbose($"Module at {modulePath} is unloaded."); + } + catch (Exception e) + { + _logger.LogVerbose($"{e.InnerException} occured, module unloading aborted."); + } + } + private bool BranchInCompilerGeneratedClass(string methodName) { foreach (InstrumenterResult instrumentedResult in _results) diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.cs b/test/coverlet.core.tests/Coverage/CoverageTests.cs index 701df8f30..6d996e832 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.cs @@ -177,6 +177,79 @@ public void TestCoverageMergeWithWrongParameter() directory.Delete(true); } + + [Fact] + public void TestCoverageUnloadWithParameters() + { + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); + + DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + + File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true); + File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true); + + var mockInstrumentationHelper = new Mock(); + mockInstrumentationHelper.Setup(x => x.RestoreOriginalModule(It.IsAny(), It.IsAny())); + + var parameters = new CoverageParameters + { + IncludeFilters = new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" }, + IncludeDirectories = Array.Empty(), + ExcludeFilters = Array.Empty(), + ExcludedSourceFiles = Array.Empty(), + ExcludeAttributes = Array.Empty(), + IncludeTestAssembly = false, + SingleHit = false, + MergeWith = string.Empty, + UseSourceLink = false + }; + + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), parameters, _mockLogger.Object, mockInstrumentationHelper.Object, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem()), new CecilSymbolHelper()); + coverage.PrepareModules(); + coverage.UnloadModule(Path.Combine(directory.FullName, Path.GetFileName(module))); + + mockInstrumentationHelper.Verify(i => i.RestoreOriginalModule(It.Is(v => v.Equals(Path.Combine(directory.FullName, Path.GetFileName(module)))), It.IsAny()), Times.Once); + _mockLogger.Verify(l => l.LogVerbose(It.Is(v => v.Equals($"Module at {Path.Combine(directory.FullName, Path.GetFileName(module))} is unloaded."))), Times.Once); + } + + [Fact] + public void TestCoverageUnloadWithNoParameters() + { + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); + + DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + + File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true); + File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true); + + var mockInstrumentationHelper = new Mock(); + mockInstrumentationHelper + .Setup(x => x.SelectModules(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(new List(){"ModuleX"}); + mockInstrumentationHelper.Setup(x => x.RestoreOriginalModule(It.IsAny(), It.IsAny())); + + var parameters = new CoverageParameters + { + IncludeFilters = new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" }, + IncludeDirectories = Array.Empty(), + ExcludeFilters = Array.Empty(), + ExcludedSourceFiles = Array.Empty(), + ExcludeAttributes = Array.Empty(), + IncludeTestAssembly = false, + SingleHit = false, + MergeWith = string.Empty, + UseSourceLink = false + }; + + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), parameters, _mockLogger.Object, mockInstrumentationHelper.Object, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem()), new CecilSymbolHelper()); + coverage.PrepareModules(); + coverage.UnloadModules(); + + mockInstrumentationHelper.Verify(i => i.RestoreOriginalModule(It.Is(v => v.Equals("ModuleX")), It.IsAny()), Times.Once); + _mockLogger.Verify(l => l.LogVerbose(It.Is(v => v.Equals("All Modules unloaded."))), Times.Once); + } } }