Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable the independent unload of instrumented modules without generating final report #1688

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ static int Main(string[] args)
var doesNotReturnAttributes = new Option<string[]>("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true };
var excludeAssembliesWithoutSources = new Option<string>("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne };
var sourceMappingFile = new Option<string>("--source-mapping-file", "Specifies the path to a SourceRootsMappings file.") { Arity = ArgumentArity.ZeroOrOne };
var unloadCoverletFromModulesOnly = new Option<bool>("---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()
{
Expand All @@ -73,7 +74,8 @@ static int Main(string[] args)
useSourceLink,
doesNotReturnAttributes,
excludeAssembliesWithoutSources,
sourceMappingFile
sourceMappingFile,
unloadCoverletFromModulesOnly
};

rootCommand.Description = "Cross platform .NET Core code coverage tool";
Expand Down Expand Up @@ -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.");
Expand All @@ -127,7 +130,8 @@ static int Main(string[] args)
useSourceLinkValue,
doesNotReturnAttributesValue,
excludeAssembliesWithoutSourcesValue,
sourceMappingFileValue);
sourceMappingFileValue,
unloadCoverletFromModulesOnlyBool);
context.ExitCode = taskStatus;

});
Expand All @@ -154,7 +158,8 @@ private static Task<int> HandleCommand(string moduleOrAppDirectory,
bool useSourceLink,
string[] doesNotReturnAttributes,
string excludeAssembliesWithoutSources,
string sourceMappingFile
string sourceMappingFile,
bool unloadCoverletFromModulesOnly
)
{

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -385,7 +396,6 @@ string sourceMappingFile

return Task.FromResult(exitCode);


}

catch (Win32Exception we) when (we.Source == "System.Diagnostics.Process")
Expand Down
48 changes: 47 additions & 1 deletion src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -326,6 +326,52 @@ public CoverageResult GetCoverageResult()
return coverageResult;
}

/// <summary>
/// unloads all modules that were instrumented
/// </summary>
/// <returns> exit code of module unloading </returns>
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;
}

/// <summary>
/// Invoke the unloading of modules and restoration of the original assembly files
/// </summary>
/// <param name="modulePath"></param>
/// <returns> exist code of unloading modules </returns>
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)
Expand Down
73 changes: 73 additions & 0 deletions test/coverlet.core.tests/Coverage/CoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IInstrumentationHelper>();
mockInstrumentationHelper.Setup(x => x.RestoreOriginalModule(It.IsAny<string>(), It.IsAny<string>()));

var parameters = new CoverageParameters
{
IncludeFilters = new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" },
IncludeDirectories = Array.Empty<string>(),
ExcludeFilters = Array.Empty<string>(),
ExcludedSourceFiles = Array.Empty<string>(),
ExcludeAttributes = Array.Empty<string>(),
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<string>(v => v.Equals(Path.Combine(directory.FullName, Path.GetFileName(module)))), It.IsAny<string>()), Times.Once);
_mockLogger.Verify(l => l.LogVerbose(It.Is<string>(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<IInstrumentationHelper>();
mockInstrumentationHelper
.Setup(x => x.SelectModules(It.IsAny<IEnumerable<string>>(), It.IsAny<string[]>(), It.IsAny<string[]>()))
.Returns(new List<string>(){"ModuleX"});
mockInstrumentationHelper.Setup(x => x.RestoreOriginalModule(It.IsAny<string>(), It.IsAny<string>()));

var parameters = new CoverageParameters
{
IncludeFilters = new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" },
IncludeDirectories = Array.Empty<string>(),
ExcludeFilters = Array.Empty<string>(),
ExcludedSourceFiles = Array.Empty<string>(),
ExcludeAttributes = Array.Empty<string>(),
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<string>(v => v.Equals("ModuleX")), It.IsAny<string>()), Times.Once);
_mockLogger.Verify(l => l.LogVerbose(It.Is<string>(v => v.Equals("All Modules unloaded."))), Times.Once);
}
}
}

Expand Down