Skip to content

Commit a3c76ed

Browse files
authored
Merge pull request #253 from ViktorHofer/ProbePath
Add option for additional assembly probe dirs
2 parents 1a28fcd + d4450ac commit a3c76ed

File tree

9 files changed

+309
-219
lines changed

9 files changed

+309
-219
lines changed

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,19 @@ Arguments:
5656
<ASSEMBLY> Path to the test assembly.
5757

5858
Options:
59-
-h|--help Show help information
60-
-v|--version Show version information
61-
-t|--target Path to the test runner application.
62-
-a|--targetargs Arguments to be passed to the test runner.
63-
-o|--output Output of the generated coverage report
64-
-f|--format Format of the generated coverage report.
65-
--threshold Exits with error if the coverage % is below value.
66-
--threshold-type Coverage type to apply the threshold to.
67-
--exclude Filter expressions to exclude specific modules and types.
68-
--include Filter expressions to include specific modules and types.
69-
--exclude-by-file Glob patterns specifying source files to exclude.
70-
--merge-with Path to existing coverage result to merge.
59+
-h|--help Show help information
60+
-v|--version Show version information
61+
-t|--target Path to the test runner application.
62+
-a|--targetargs Arguments to be passed to the test runner.
63+
-o|--output Output of the generated coverage report
64+
-f|--format Format of the generated coverage report.
65+
--threshold Exits with error if the coverage % is below value.
66+
--threshold-type Coverage type to apply the threshold to.
67+
--exclude Filter expressions to exclude specific modules and types.
68+
--include Filter expressions to include specific modules and types.
69+
--include-directory Include directories containing additional assemblies to be instrumented.
70+
--exclude-by-file Glob patterns specifying source files to exclude.
71+
--merge-with Path to existing coverage result to merge.
7172
```
7273
7374
#### Code Coverage

src/coverlet.console/Program.cs

Lines changed: 171 additions & 170 deletions
Large diffs are not rendered by default.

src/coverlet.core/Coverage.cs

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class Coverage
1717
private string _identifier;
1818
private string[] _excludeFilters;
1919
private string[] _includeFilters;
20+
private string[] _includeDirectories;
2021
private string[] _excludedSourceFiles;
2122
private string _mergeWith;
2223
private string[] _excludeAttributes;
@@ -27,11 +28,12 @@ public string Identifier
2728
get { return _identifier; }
2829
}
2930

30-
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
31+
public Coverage(string module, string[] excludeFilters, string[] includeFilters, string[] includeDirectories, string[] excludedSourceFiles, string mergeWith, string[] excludeAttributes)
3132
{
3233
_module = module;
3334
_excludeFilters = excludeFilters;
3435
_includeFilters = includeFilters;
36+
_includeDirectories = includeDirectories ?? Array.Empty<string>();
3537
_excludedSourceFiles = excludedSourceFiles;
3638
_mergeWith = mergeWith;
3739
_excludeAttributes = excludeAttributes;
@@ -42,23 +44,33 @@ public Coverage(string module, string[] excludeFilters, string[] includeFilters,
4244

4345
public void PrepareModules()
4446
{
45-
string[] modules = InstrumentationHelper.GetCoverableModules(_module);
47+
string[] modules = InstrumentationHelper.GetCoverableModules(_module, _includeDirectories);
4648
string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludedSourceFiles);
4749
_excludeFilters = _excludeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
4850
_includeFilters = _includeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
4951

5052
foreach (var module in modules)
5153
{
52-
if (InstrumentationHelper.IsModuleExcluded(module, _excludeFilters)
53-
|| !InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
54+
if (InstrumentationHelper.IsModuleExcluded(module, _excludeFilters) ||
55+
!InstrumentationHelper.IsModuleIncluded(module, _includeFilters))
5456
continue;
5557

5658
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes);
5759
if (instrumenter.CanInstrument())
5860
{
5961
InstrumentationHelper.BackupOriginalModule(module, _identifier);
60-
var result = instrumenter.Instrument();
61-
_results.Add(result);
62+
63+
// Guard code path and restore if instrumentation fails.
64+
try
65+
{
66+
var result = instrumenter.Instrument();
67+
_results.Add(result);
68+
}
69+
catch (Exception)
70+
{
71+
// TODO: With verbose logging we should note that instrumentation failed.
72+
InstrumentationHelper.RestoreOriginalModule(module, _identifier);
73+
}
6274
}
6375
}
6476
}
@@ -153,6 +165,7 @@ public CoverageResult GetCoverageResult()
153165
}
154166

155167
var coverageResult = new CoverageResult { Identifier = _identifier, Modules = modules };
168+
156169
if (!string.IsNullOrEmpty(_mergeWith) && !string.IsNullOrWhiteSpace(_mergeWith) && File.Exists(_mergeWith))
157170
{
158171
string json = File.ReadAllText(_mergeWith);
@@ -207,29 +220,29 @@ private void CalculateCoverage()
207220
}
208221
}
209222

210-
// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
211-
// we'll remove all MoveNext() not covered branch
212-
foreach (var document in result.Documents)
213-
{
214-
List<KeyValuePair<(int, int), Branch>> branchesToRemove = new List<KeyValuePair<(int, int), Branch>>();
215-
foreach (var branch in document.Value.Branches)
216-
{
217-
//if one branch is covered we search the other one only if it's not covered
218-
if (CecilSymbolHelper.IsMoveNext(branch.Value.Method) && branch.Value.Hits > 0)
219-
{
220-
foreach (var moveNextBranch in document.Value.Branches)
221-
{
222-
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
223-
{
224-
branchesToRemove.Add(moveNextBranch);
225-
}
226-
}
227-
}
228-
}
229-
foreach (var branchToRemove in branchesToRemove)
230-
{
231-
document.Value.Branches.Remove(branchToRemove.Key);
232-
}
223+
// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
224+
// we'll remove all MoveNext() not covered branch
225+
foreach (var document in result.Documents)
226+
{
227+
List<KeyValuePair<(int, int), Branch>> branchesToRemove = new List<KeyValuePair<(int, int), Branch>>();
228+
foreach (var branch in document.Value.Branches)
229+
{
230+
//if one branch is covered we search the other one only if it's not covered
231+
if (CecilSymbolHelper.IsMoveNext(branch.Value.Method) && branch.Value.Hits > 0)
232+
{
233+
foreach (var moveNextBranch in document.Value.Branches)
234+
{
235+
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
236+
{
237+
branchesToRemove.Add(moveNextBranch);
238+
}
239+
}
240+
}
241+
}
242+
foreach (var branchToRemove in branchesToRemove)
243+
{
244+
document.Value.Branches.Remove(branchToRemove.Key);
245+
}
233246
}
234247

235248
InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);

src/coverlet.core/Helpers/InstrumentationHelper.cs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,49 @@ namespace Coverlet.Core.Helpers
1414
{
1515
internal static class InstrumentationHelper
1616
{
17-
public static string[] GetCoverableModules(string module)
17+
public static string[] GetCoverableModules(string module, string[] directories)
1818
{
19-
IEnumerable<string> modules = Directory.EnumerateFiles(Path.GetDirectoryName(module)).Where(f => f.EndsWith(".exe") || f.EndsWith(".dll"));
20-
modules = modules.Where(m => IsAssembly(m) && Path.GetFileName(m) != Path.GetFileName(module));
21-
return modules.ToArray();
19+
Debug.Assert(directories != null);
20+
21+
string moduleDirectory = Path.GetDirectoryName(module);
22+
if (moduleDirectory == string.Empty)
23+
{
24+
moduleDirectory = Directory.GetCurrentDirectory();
25+
}
26+
27+
var dirs = new List<string>()
28+
{
29+
// Add the test assembly's directory.
30+
moduleDirectory
31+
};
32+
33+
// Prepare all the directories we probe for modules.
34+
foreach (string directory in directories)
35+
{
36+
if (string.IsNullOrWhiteSpace(directory)) continue;
37+
38+
string fullPath = (!Path.IsPathRooted(directory)
39+
? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), directory))
40+
: directory).TrimEnd('*');
41+
42+
if (!Directory.Exists(fullPath)) continue;
43+
44+
if (directory.EndsWith("*", StringComparison.Ordinal))
45+
dirs.AddRange(Directory.GetDirectories(fullPath));
46+
else
47+
dirs.Add(fullPath);
48+
}
49+
50+
// The module's name must be unique.
51+
// Add the test module itself to exclude it from the files enumeration.
52+
var uniqueModules = new HashSet<string>
53+
{
54+
Path.GetFileName(module)
55+
};
56+
57+
return dirs.SelectMany(d => Directory.EnumerateFiles(d))
58+
.Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m)))
59+
.ToArray();
2260
}
2361

2462
public static bool HasPdb(string module)
@@ -43,7 +81,7 @@ public static bool HasPdb(string module)
4381
public static void BackupOriginalModule(string module, string identifier)
4482
{
4583
var backupPath = GetBackupPath(module, identifier);
46-
File.Copy(module, backupPath);
84+
File.Copy(module, backupPath, true);
4785
}
4886

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

273311
private static bool IsAssembly(string filePath)
274312
{
313+
Debug.Assert(filePath != null);
314+
315+
if (!(filePath.EndsWith(".exe") || filePath.EndsWith(".dll")))
316+
return false;
317+
275318
try
276319
{
277320
AssemblyName.GetAssemblyName(filePath);

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class InstrumentationTask : Task
1111
private string _path;
1212
private string _exclude;
1313
private string _include;
14+
private string _includeDirectory;
1415
private string _excludeByFile;
1516
private string _mergeWith;
1617
private string _excludeByAttribute;
@@ -39,6 +40,12 @@ public string Include
3940
set { _include = value; }
4041
}
4142

43+
public string IncludeDirectory
44+
{
45+
get { return _includeDirectory; }
46+
set { _includeDirectory = value; }
47+
}
48+
4249
public string ExcludeByFile
4350
{
4451
get { return _excludeByFile; }
@@ -64,9 +71,10 @@ public override bool Execute()
6471
var excludedSourceFiles = _excludeByFile?.Split(',');
6572
var excludeFilters = _exclude?.Split(',');
6673
var includeFilters = _include?.Split(',');
74+
var includeDirectories = _includeDirectory?.Split(',');
6775
var excludeAttributes = _excludeByAttribute?.Split(',');
6876

69-
_coverage = new Coverage(_path, excludeFilters, includeFilters, excludedSourceFiles, _mergeWith, excludeAttributes);
77+
_coverage = new Coverage(_path, excludeFilters, includeFilters, includeDirectories, excludedSourceFiles, _mergeWith, excludeAttributes);
7078
_coverage.PrepareModules();
7179
}
7280
catch (Exception ex)

src/coverlet.msbuild/coverlet.msbuild.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
1010
<Threshold Condition="$(Threshold) == ''">0</Threshold>
1111
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
12+
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
1213
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
1314
</PropertyGroup>
1415
</Project>

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<Coverlet.MSbuild.Tasks.InstrumentationTask
88
Condition="'$(VSTestNoBuild)' == 'true' and $(CollectCoverage) == 'true'"
99
Include="$(Include)"
10+
IncludeDirectory="$(IncludeDirectory)"
1011
Exclude="$(Exclude)"
1112
ExcludeByFile="$(ExcludeByFile)"
1213
MergeWith="$(MergeWith)"
@@ -18,6 +19,7 @@
1819
<Coverlet.MSbuild.Tasks.InstrumentationTask
1920
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
2021
Include="$(Include)"
22+
IncludeDirectory="$(IncludeDirectory)"
2123
Exclude="$(Exclude)"
2224
ExcludeByFile="$(ExcludeByFile)"
2325
MergeWith="$(MergeWith)"

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void TestCoverage()
2727
// Since Coverage only instruments dependancies, we need a fake module here
2828
var testModule = Path.Combine(directory.FullName, "test.module.dll");
2929

30-
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, Array.Empty<string>());
30+
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, Array.Empty<string>());
3131
coverage.PrepareModules();
3232

3333
var result = coverage.GetCoverageResult();

test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class InstrumentationHelperTests
1212
public void TestGetDependencies()
1313
{
1414
string module = typeof(InstrumentationHelperTests).Assembly.Location;
15-
var modules = InstrumentationHelper.GetCoverableModules(module);
15+
var modules = InstrumentationHelper.GetCoverableModules(module, Array.Empty<string>());
1616
Assert.False(Array.Exists(modules, m => m == module));
1717
}
1818

@@ -230,6 +230,27 @@ public void TestIsTypeExcludedAndIncludedWithMatchingAndMismatchingFilter(string
230230
Assert.True(result);
231231
}
232232

233+
[Fact]
234+
public void TestIncludeDirectories()
235+
{
236+
string module = typeof(InstrumentationHelperTests).Assembly.Location;
237+
238+
var currentDirModules = InstrumentationHelper.GetCoverableModules(module,
239+
new[] {Environment.CurrentDirectory});
240+
241+
var parentDirWildcardModules = InstrumentationHelper.GetCoverableModules(module,
242+
new[] {Path.Combine(Directory.GetParent(Environment.CurrentDirectory).FullName, "*")});
243+
244+
// There are at least as many modules found when searching the parent directory's subdirectories
245+
Assert.True(parentDirWildcardModules.Length >= currentDirModules.Length);
246+
247+
var relativePathModules = InstrumentationHelper.GetCoverableModules(module,
248+
new[] {"."});
249+
250+
// Same number of modules found when using a relative path
251+
Assert.Equal(currentDirModules.Length, relativePathModules.Length);
252+
}
253+
233254
public static IEnumerable<object[]> ValidModuleFilterData =>
234255
new List<object[]>
235256
{

0 commit comments

Comments
 (0)