diff --git a/README.md b/README.md index 47c0f0373..af55ebe17 100644 --- a/README.md +++ b/README.md @@ -56,18 +56,19 @@ Arguments: 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 diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 3929d901c..6d16d042a 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -1,170 +1,171 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using ConsoleTables; -using Coverlet.Console.Logging; -using Coverlet.Core; -using Coverlet.Core.Reporters; - -using Microsoft.Extensions.CommandLineUtils; - -namespace Coverlet.Console -{ - class Program - { - static int Main(string[] args) - { - var logger = new ConsoleLogger(); - var app = new CommandLineApplication(); - app.Name = "coverlet"; - app.FullName = "Cross platform .NET Core code coverage tool"; - app.HelpOption("-h|--help"); - app.VersionOption("-v|--version", GetAssemblyVersion()); - - CommandArgument module = app.Argument("", "Path to the test assembly."); - CommandOption target = app.Option("-t|--target", "Path to the test runner application.", CommandOptionType.SingleValue); - CommandOption targs = app.Option("-a|--targetargs", "Arguments to be passed to the test runner.", CommandOptionType.SingleValue); - CommandOption output = app.Option("-o|--output", "Output of the generated coverage report", CommandOptionType.SingleValue); - CommandOption formats = app.Option("-f|--format", "Format of the generated coverage report.", CommandOptionType.MultipleValue); - CommandOption threshold = app.Option("--threshold", "Exits with error if the coverage % is below value.", CommandOptionType.SingleValue); - CommandOption thresholdTypes = app.Option("--threshold-type", "Coverage type to apply the threshold to.", CommandOptionType.MultipleValue); - CommandOption excludeFilters = app.Option("--exclude", "Filter expressions to exclude specific modules and types.", CommandOptionType.MultipleValue); - CommandOption includeFilters = app.Option("--include", "Filter expressions to include only specific modules and types.", CommandOptionType.MultipleValue); - CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue); - CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue); - CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue); - - app.OnExecute(() => - { - if (string.IsNullOrEmpty(module.Value) || string.IsNullOrWhiteSpace(module.Value)) - throw new CommandParsingException(app, "No test assembly specified."); - - if (!target.HasValue()) - throw new CommandParsingException(app, "Target must be specified."); - - Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value(), excludeAttributes.Values.ToArray()); - coverage.PrepareModules(); - - Process process = new Process(); - process.StartInfo.FileName = target.Value(); - process.StartInfo.Arguments = targs.HasValue() ? targs.Value() : string.Empty; - process.StartInfo.CreateNoWindow = true; - process.StartInfo.RedirectStandardOutput = true; - process.Start(); - logger.LogInformation(process.StandardOutput.ReadToEnd()); - process.WaitForExit(); - - var dOutput = output.HasValue() ? output.Value() : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); - var dThreshold = threshold.HasValue() ? int.Parse(threshold.Value()) : 0; - var dThresholdTypes = thresholdTypes.HasValue() ? thresholdTypes.Values : new List(new string[] { "line", "branch", "method" }); - - logger.LogInformation("\nCalculating coverage result..."); - - var result = coverage.GetCoverageResult(); - var directory = Path.GetDirectoryName(dOutput); - if (directory == string.Empty) - { - directory = Directory.GetCurrentDirectory(); - } - else if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - foreach (var format in (formats.HasValue() ? formats.Values : new List(new string[] { "json" }))) - { - var reporter = new ReporterFactory(format).CreateReporter(); - if (reporter == null) - throw new Exception($"Specified output format '{format}' is not supported"); - - if (reporter.OutputType == ReporterOutputType.Console) - { - // Output to console - logger.LogInformation(" Outputting results to console"); - logger.LogInformation(reporter.Report(result)); - } - else - { - // Output to file - var filename = Path.GetFileName(dOutput); - filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename; - filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}"; - - var report = Path.Combine(directory, filename); - logger.LogInformation($" Generating report '{report}'"); - File.WriteAllText(report, reporter.Report(result)); - } - } - - var summary = new CoverageSummary(); - var exceptionBuilder = new StringBuilder(); - var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method"); - var thresholdFailed = false; - var overallLineCoverage = summary.CalculateLineCoverage(result.Modules); - var overallBranchCoverage = summary.CalculateBranchCoverage(result.Modules); - var overallMethodCoverage = summary.CalculateMethodCoverage(result.Modules); - - foreach (var _module in result.Modules) - { - var linePercent = summary.CalculateLineCoverage(_module.Value).Percent * 100; - var branchPercent = summary.CalculateBranchCoverage(_module.Value).Percent * 100; - var methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent * 100; - - coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%"); - - if (dThreshold > 0) - { - if (linePercent < dThreshold && dThresholdTypes.Contains("line")) - { - exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a line coverage '{linePercent}%' below specified threshold '{dThreshold}%'"); - thresholdFailed = true; - } - - if (branchPercent < dThreshold && dThresholdTypes.Contains("branch")) - { - exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a branch coverage '{branchPercent}%' below specified threshold '{dThreshold}%'"); - thresholdFailed = true; - } - - if (methodPercent < dThreshold && dThresholdTypes.Contains("method")) - { - exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a method coverage '{methodPercent}%' below specified threshold '{dThreshold}%'"); - thresholdFailed = true; - } - } - } - - logger.LogInformation(string.Empty); - logger.LogInformation(coverageTable.ToStringAlternative()); - logger.LogInformation($"Total Line: {overallLineCoverage.Percent * 100}%"); - logger.LogInformation($"Total Branch: {overallBranchCoverage.Percent * 100}%"); - logger.LogInformation($"Total Method: {overallMethodCoverage.Percent * 100}%"); - - if (thresholdFailed) - throw new Exception(exceptionBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray())); - - return process.ExitCode == 0 ? 0 : process.ExitCode; - }); - - try - { - return app.Execute(args); - } - catch (CommandParsingException ex) - { - logger.LogError(ex.Message); - app.ShowHelp(); - return 1; - } - catch (Exception ex) - { - logger.LogError(ex.Message); - return 1; - } - } - - static string GetAssemblyVersion() => typeof(Program).Assembly.GetName().Version.ToString(); - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using ConsoleTables; +using Coverlet.Console.Logging; +using Coverlet.Core; +using Coverlet.Core.Reporters; + +using Microsoft.Extensions.CommandLineUtils; + +namespace Coverlet.Console +{ + class Program + { + static int Main(string[] args) + { + var logger = new ConsoleLogger(); + var app = new CommandLineApplication(); + app.Name = "coverlet"; + app.FullName = "Cross platform .NET Core code coverage tool"; + app.HelpOption("-h|--help"); + app.VersionOption("-v|--version", GetAssemblyVersion()); + + CommandArgument module = app.Argument("", "Path to the test assembly."); + CommandOption target = app.Option("-t|--target", "Path to the test runner application.", CommandOptionType.SingleValue); + CommandOption targs = app.Option("-a|--targetargs", "Arguments to be passed to the test runner.", CommandOptionType.SingleValue); + CommandOption output = app.Option("-o|--output", "Output of the generated coverage report", CommandOptionType.SingleValue); + CommandOption formats = app.Option("-f|--format", "Format of the generated coverage report.", CommandOptionType.MultipleValue); + CommandOption threshold = app.Option("--threshold", "Exits with error if the coverage % is below value.", CommandOptionType.SingleValue); + CommandOption thresholdTypes = app.Option("--threshold-type", "Coverage type to apply the threshold to.", CommandOptionType.MultipleValue); + CommandOption excludeFilters = app.Option("--exclude", "Filter expressions to exclude specific modules and types.", CommandOptionType.MultipleValue); + CommandOption includeFilters = app.Option("--include", "Filter expressions to include only specific modules and types.", CommandOptionType.MultipleValue); + CommandOption includeDirectories = app.Option("--include-directory", "Include directories containing additional assemblies to be instrumented.", CommandOptionType.MultipleValue); + CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue); + CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue); + CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue); + + app.OnExecute(() => + { + if (string.IsNullOrEmpty(module.Value) || string.IsNullOrWhiteSpace(module.Value)) + throw new CommandParsingException(app, "No test assembly specified."); + + if (!target.HasValue()) + throw new CommandParsingException(app, "Target must be specified."); + + Coverage coverage = new Coverage(module.Value, excludeFilters.Values.ToArray(), includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludedSourceFiles.Values.ToArray(), mergeWith.Value(), excludeAttributes.Values.ToArray()); + coverage.PrepareModules(); + + Process process = new Process(); + process.StartInfo.FileName = target.Value(); + process.StartInfo.Arguments = targs.HasValue() ? targs.Value() : string.Empty; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + logger.LogInformation(process.StandardOutput.ReadToEnd()); + process.WaitForExit(); + + var dOutput = output.HasValue() ? output.Value() : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); + var dThreshold = threshold.HasValue() ? int.Parse(threshold.Value()) : 0; + var dThresholdTypes = thresholdTypes.HasValue() ? thresholdTypes.Values : new List(new string[] { "line", "branch", "method" }); + + logger.LogInformation("\nCalculating coverage result..."); + + var result = coverage.GetCoverageResult(); + var directory = Path.GetDirectoryName(dOutput); + if (directory == string.Empty) + { + directory = Directory.GetCurrentDirectory(); + } + else if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + foreach (var format in (formats.HasValue() ? formats.Values : new List(new string[] { "json" }))) + { + var reporter = new ReporterFactory(format).CreateReporter(); + if (reporter == null) + throw new Exception($"Specified output format '{format}' is not supported"); + + if (reporter.OutputType == ReporterOutputType.Console) + { + // Output to console + logger.LogInformation(" Outputting results to console"); + logger.LogInformation(reporter.Report(result)); + } + else + { + // Output to file + var filename = Path.GetFileName(dOutput); + filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename; + filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}"; + + var report = Path.Combine(directory, filename); + logger.LogInformation($" Generating report '{report}'"); + File.WriteAllText(report, reporter.Report(result)); + } + } + + var summary = new CoverageSummary(); + var exceptionBuilder = new StringBuilder(); + var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method"); + var thresholdFailed = false; + var overallLineCoverage = summary.CalculateLineCoverage(result.Modules); + var overallBranchCoverage = summary.CalculateBranchCoverage(result.Modules); + var overallMethodCoverage = summary.CalculateMethodCoverage(result.Modules); + + foreach (var _module in result.Modules) + { + var linePercent = summary.CalculateLineCoverage(_module.Value).Percent * 100; + var branchPercent = summary.CalculateBranchCoverage(_module.Value).Percent * 100; + var methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent * 100; + + coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%"); + + if (dThreshold > 0) + { + if (linePercent < dThreshold && dThresholdTypes.Contains("line")) + { + exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a line coverage '{linePercent}%' below specified threshold '{dThreshold}%'"); + thresholdFailed = true; + } + + if (branchPercent < dThreshold && dThresholdTypes.Contains("branch")) + { + exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a branch coverage '{branchPercent}%' below specified threshold '{dThreshold}%'"); + thresholdFailed = true; + } + + if (methodPercent < dThreshold && dThresholdTypes.Contains("method")) + { + exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a method coverage '{methodPercent}%' below specified threshold '{dThreshold}%'"); + thresholdFailed = true; + } + } + } + + logger.LogInformation(string.Empty); + logger.LogInformation(coverageTable.ToStringAlternative()); + logger.LogInformation($"Total Line: {overallLineCoverage.Percent * 100}%"); + logger.LogInformation($"Total Branch: {overallBranchCoverage.Percent * 100}%"); + logger.LogInformation($"Total Method: {overallMethodCoverage.Percent * 100}%"); + + if (thresholdFailed) + throw new Exception(exceptionBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray())); + + return process.ExitCode == 0 ? 0 : process.ExitCode; + }); + + try + { + return app.Execute(args); + } + catch (CommandParsingException ex) + { + logger.LogError(ex.Message); + app.ShowHelp(); + return 1; + } + catch (Exception ex) + { + logger.LogError(ex.Message); + return 1; + } + } + + static string GetAssemblyVersion() => typeof(Program).Assembly.GetName().Version.ToString(); + } +} diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 2fe85a45d..2eddbab0f 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -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; @@ -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(); _excludedSourceFiles = excludedSourceFiles; _mergeWith = mergeWith; _excludeAttributes = excludeAttributes; @@ -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); + } } } } @@ -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); @@ -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> branchesToRemove = new List>(); - 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) + // we'll remove all MoveNext() not covered branch + foreach (var document in result.Documents) + { + List> branchesToRemove = new List>(); + 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); diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index ea1fc19e1..426b76c7e 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -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 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() + { + // 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 + { + 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) @@ -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) @@ -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); diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs index 7cb7e4ba6..09541972e 100644 --- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs +++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs @@ -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; @@ -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; } @@ -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) diff --git a/src/coverlet.msbuild/coverlet.msbuild.props b/src/coverlet.msbuild/coverlet.msbuild.props index 34b59eae4..58121f036 100644 --- a/src/coverlet.msbuild/coverlet.msbuild.props +++ b/src/coverlet.msbuild/coverlet.msbuild.props @@ -9,6 +9,7 @@ 0 line,branch,method + diff --git a/src/coverlet.msbuild/coverlet.msbuild.targets b/src/coverlet.msbuild/coverlet.msbuild.targets index b125f5a9c..c5e26000c 100644 --- a/src/coverlet.msbuild/coverlet.msbuild.targets +++ b/src/coverlet.msbuild/coverlet.msbuild.targets @@ -7,6 +7,7 @@ (), Array.Empty(), Array.Empty(), string.Empty, Array.Empty()); + var coverage = new Coverage(testModule, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), string.Empty, Array.Empty()); coverage.PrepareModules(); var result = coverage.GetCoverageResult(); diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 44e1021c8..c2147f50b 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -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()); Assert.False(Array.Exists(modules, m => m == module)); } @@ -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 ValidModuleFilterData => new List {