Skip to content

Add option for additional assembly probe dirs #253

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

Merged
merged 10 commits into from
Nov 28, 2018
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,19 @@ Arguments:
<ASSEMBLY> Path to the test assembly.

Options:
-h|--help Show help information
-v|--version Show version information
-t|--target Path to the test runner application.
-a|--targetargs Arguments to be passed to the test runner.
-o|--output Output of the generated coverage report
-f|--format Format of the generated coverage report.
--threshold Exits with error if the coverage % is below value.
--threshold-type Coverage type to apply the threshold to.
--exclude Filter expressions to exclude specific modules and types.
--include Filter expressions to include specific modules and types.
--exclude-by-file Glob patterns specifying source files to exclude.
--merge-with Path to existing coverage result to merge.
-h|--help Show help information
-v|--version Show version information
-t|--target Path to the test runner application.
-a|--targetargs Arguments to be passed to the test runner.
-o|--output Output of the generated coverage report
-f|--format Format of the generated coverage report.
--threshold Exits with error if the coverage % is below value.
--threshold-type Coverage type to apply the threshold to.
--exclude Filter expressions to exclude specific modules and types.
--include Filter expressions to include specific modules and types.
--include-directory Include directories containing additional assemblies to be instrumented.
--exclude-by-file Glob patterns specifying source files to exclude.
--merge-with Path to existing coverage result to merge.
```

#### Code Coverage
Expand Down
341 changes: 171 additions & 170 deletions src/coverlet.console/Program.cs

Large diffs are not rendered by default.

71 changes: 42 additions & 29 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class Coverage
private string _identifier;
private string[] _excludeFilters;
private string[] _includeFilters;
private string[] _includeDirectories;
private string[] _excludedSourceFiles;
private string _mergeWith;
private string[] _excludeAttributes;
Expand All @@ -27,11 +28,12 @@ public string Identifier
get { return _identifier; }
}

public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] includeDirectories, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
{
_module = module;
_excludeFilters = excludeFilters;
_includeFilters = includeFilters;
_includeDirectories = includeDirectories ?? Array.Empty<string>();
_excludedSourceFiles = excludedSourceFiles;
_mergeWith = mergeWith;
_excludeAttributes = excludeAttributes;
Expand All @@ -42,23 +44,33 @@ public Coverage(string module, string[] excludeFilters, string[] includeFilters,

public void PrepareModules()
{
string[] modules = InstrumentationHelper.GetCoverableModules(_module);
string[] modules = InstrumentationHelper.GetCoverableModules(_module, _includeDirectories);
string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludedSourceFiles);
_excludeFilters = _excludeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
_includeFilters = _includeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();

foreach (var module in modules)
{
if (InstrumentationHelper.IsModuleExcluded(module, _excludeFilters)
|| !InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
if (InstrumentationHelper.IsModuleExcluded(module, _excludeFilters) ||
!InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
continue;

var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes);
if (instrumenter.CanInstrument())
{
InstrumentationHelper.BackupOriginalModule(module, _identifier);
var result = instrumenter.Instrument();
_results.Add(result);

// Guard code path and restore if instrumentation fails.
try
{
var result = instrumenter.Instrument();
_results.Add(result);
}
catch (Exception)
{
// TODO: With verbose logging we should note that instrumentation failed.
InstrumentationHelper.RestoreOriginalModule(module, _identifier);
}
}
}
}
Expand Down Expand Up @@ -153,6 +165,7 @@ public CoverageResult GetCoverageResult()
}

var coverageResult = new CoverageResult { Identifier = _identifier, Modules = modules };

if (!string.IsNullOrEmpty(_mergeWith) && !string.IsNullOrWhiteSpace(_mergeWith) && File.Exists(_mergeWith))
{
string json = File.ReadAllText(_mergeWith);
Expand Down Expand Up @@ -207,29 +220,29 @@ private void CalculateCoverage()
}
}

// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
// we'll remove all MoveNext() not covered branch
foreach (var document in result.Documents)
{
List<KeyValuePair<(int, int), Branch>> branchesToRemove = new List<KeyValuePair<(int, int), Branch>>();
foreach (var branch in document.Value.Branches)
{
//if one branch is covered we search the other one only if it's not covered
if (CecilSymbolHelper.IsMoveNext(branch.Value.Method) && branch.Value.Hits > 0)
{
foreach (var moveNextBranch in document.Value.Branches)
{
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
{
branchesToRemove.Add(moveNextBranch);
}
}
}
}
foreach (var branchToRemove in branchesToRemove)
{
document.Value.Branches.Remove(branchToRemove.Key);
}
// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are white-space only changes that are wrong in upstream.

// we'll remove all MoveNext() not covered branch
foreach (var document in result.Documents)
{
List<KeyValuePair<(int, int), Branch>> branchesToRemove = new List<KeyValuePair<(int, int), Branch>>();
foreach (var branch in document.Value.Branches)
{
//if one branch is covered we search the other one only if it's not covered
if (CecilSymbolHelper.IsMoveNext(branch.Value.Method) && branch.Value.Hits > 0)
{
foreach (var moveNextBranch in document.Value.Branches)
{
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
{
branchesToRemove.Add(moveNextBranch);
}
}
}
}
foreach (var branchToRemove in branchesToRemove)
{
document.Value.Branches.Remove(branchToRemove.Key);
}
}

InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
Expand Down
53 changes: 48 additions & 5 deletions src/coverlet.core/Helpers/InstrumentationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,49 @@ namespace Coverlet.Core.Helpers
{
internal static class InstrumentationHelper
{
public static string[] GetCoverableModules(string module)
public static string[] GetCoverableModules(string module, string[] directories)
{
IEnumerable<string> modules = Directory.EnumerateFiles(Path.GetDirectoryName(module)).Where(f => f.EndsWith(".exe") || f.EndsWith(".dll"));
modules = modules.Where(m => IsAssembly(m) && Path.GetFileName(m) != Path.GetFileName(module));
return modules.ToArray();
Debug.Assert(directories != null);

string moduleDirectory = Path.GetDirectoryName(module);
if (moduleDirectory == string.Empty)
{
moduleDirectory = Directory.GetCurrentDirectory();
}

var dirs = new List<string>()
{
// Add the test assembly's directory.
moduleDirectory
};

// Prepare all the directories we probe for modules.
foreach (string directory in directories)
{
if (string.IsNullOrWhiteSpace(directory)) continue;

string fullPath = (!Path.IsPathRooted(directory)
? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), directory))
: directory).TrimEnd('*');

if (!Directory.Exists(fullPath)) continue;

if (directory.EndsWith("*", StringComparison.Ordinal))
dirs.AddRange(Directory.GetDirectories(fullPath));
else
dirs.Add(fullPath);
}

// The module's name must be unique.
// Add the test module itself to exclude it from the files enumeration.
var uniqueModules = new HashSet<string>
{
Path.GetFileName(module)
};

return dirs.SelectMany(d => Directory.EnumerateFiles(d))
.Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m)))
.ToArray();
}

public static bool HasPdb(string module)
Expand All @@ -43,7 +81,7 @@ public static bool HasPdb(string module)
public static void BackupOriginalModule(string module, string identifier)
{
var backupPath = GetBackupPath(module, identifier);
File.Copy(module, backupPath);
File.Copy(module, backupPath, true);
}

public static void RestoreOriginalModule(string module, string identifier)
Expand Down Expand Up @@ -272,6 +310,11 @@ private static string WildcardToRegex(string pattern)

private static bool IsAssembly(string filePath)
{
Debug.Assert(filePath != null);

if (!(filePath.EndsWith(".exe") || filePath.EndsWith(".dll")))
return false;

try
{
AssemblyName.GetAssemblyName(filePath);
Expand Down
10 changes: 9 additions & 1 deletion src/coverlet.msbuild.tasks/InstrumentationTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class InstrumentationTask : Task
private string _path;
private string _exclude;
private string _include;
private string _includeDirectory;
private string _excludeByFile;
private string _mergeWith;
private string _excludeByAttribute;
Expand Down Expand Up @@ -39,6 +40,12 @@ public string Include
set { _include = value; }
}

public string IncludeDirectory
{
get { return _includeDirectory; }
set { _includeDirectory = value; }
}

public string ExcludeByFile
{
get { return _excludeByFile; }
Expand All @@ -64,9 +71,10 @@ public override bool Execute()
var excludedSourceFiles = _excludeByFile?.Split(',');
var excludeFilters = _exclude?.Split(',');
var includeFilters = _include?.Split(',');
var includeDirectories = _includeDirectory?.Split(',');
var excludeAttributes = _excludeByAttribute?.Split(',');

_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith, excludeAttributes);
_coverage = new Coverage(_path, excludeFilters, includeFilters, includeDirectories, excludedSourceFiles, _mergeWith, excludeAttributes);
_coverage.PrepareModules();
}
catch (Exception ex)
Expand Down
1 change: 1 addition & 0 deletions src/coverlet.msbuild/coverlet.msbuild.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
<Threshold Condition="$(Threshold) == ''">0</Threshold>
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
</PropertyGroup>
</Project>
2 changes: 2 additions & 0 deletions src/coverlet.msbuild/coverlet.msbuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<Coverlet.MSbuild.Tasks.InstrumentationTask
Condition="'$(VSTestNoBuild)' == 'true' and $(CollectCoverage) == 'true'"
Include="$(Include)"
IncludeDirectory="$(IncludeDirectory)"
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
Expand All @@ -18,6 +19,7 @@
<Coverlet.MSbuild.Tasks.InstrumentationTask
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
Include="$(Include)"
IncludeDirectory="$(IncludeDirectory)"
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
Expand Down
2 changes: 1 addition & 1 deletion test/coverlet.core.tests/CoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestCoverage()
// Since Coverage only instruments dependancies, we need a fake module here
var testModule = Path.Combine(directory.FullName, "test.module.dll");

var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, Array.Empty<string>());
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, Array.Empty<string>());
coverage.PrepareModules();

var result = coverage.GetCoverageResult();
Expand Down
23 changes: 22 additions & 1 deletion test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class InstrumentationHelperTests
public void TestGetDependencies()
{
string module = typeof(InstrumentationHelperTests).Assembly.Location;
var modules = InstrumentationHelper.GetCoverableModules(module);
var modules = InstrumentationHelper.GetCoverableModules(module, Array.Empty<string>());
Assert.False(Array.Exists(modules, m => m == module));
}

Expand Down Expand Up @@ -230,6 +230,27 @@ public void TestIsTypeExcludedAndIncludedWithMatchingAndMismatchingFilter(string
Assert.True(result);
}

[Fact]
public void TestIncludeDirectories()
{
string module = typeof(InstrumentationHelperTests).Assembly.Location;

var currentDirModules = InstrumentationHelper.GetCoverableModules(module,
new[] {Environment.CurrentDirectory});

var parentDirWildcardModules = InstrumentationHelper.GetCoverableModules(module,
new[] {Path.Combine(Directory.GetParent(Environment.CurrentDirectory).FullName, "*")});

// There are at least as many modules found when searching the parent directory's subdirectories
Assert.True(parentDirWildcardModules.Length >= currentDirModules.Length);

var relativePathModules = InstrumentationHelper.GetCoverableModules(module,
new[] {"."});

// Same number of modules found when using a relative path
Assert.Equal(currentDirModules.Length, relativePathModules.Length);
}

public static IEnumerable<object[]> ValidModuleFilterData =>
new List<object[]>
{
Expand Down