diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index a89b62000..4778fa765 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -74,11 +74,9 @@ public Coverage(CoveragePrepareResult prepareResult, ILogger logger) public CoveragePrepareResult PrepareModules() { string[] modules = InstrumentationHelper.GetCoverableModules(_module, _includeDirectories, _includeTestAssembly); - string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludedSourceFiles); Array.ForEach(_excludeFilters ?? Array.Empty(), filter => _logger.LogVerbose($"Excluded module filter '{filter}'")); Array.ForEach(_includeFilters ?? Array.Empty(), filter => _logger.LogVerbose($"Included module filter '{filter}'")); - Array.ForEach(excludes ?? Array.Empty(), filter => _logger.LogVerbose($"Excluded source files '{filter}'")); _excludeFilters = _excludeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray(); _includeFilters = _includeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray(); @@ -92,7 +90,7 @@ public CoveragePrepareResult PrepareModules() continue; } - var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, excludes, _excludeAttributes, _singleHit, _logger); + var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, _excludedSourceFiles, _excludeAttributes, _singleHit, _logger); if (instrumenter.CanInstrument()) { InstrumentationHelper.BackupOriginalModule(module, _identifier); diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 3c6ac984e..90b30707e 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -318,46 +318,6 @@ public static bool IsTypeIncluded(string module, string type, string[] includeFi public static bool IsLocalMethod(string method) => new Regex(WildcardToRegex("<*>*__*|*")).IsMatch(method); - public static string[] GetExcludedFiles(string[] excludes) - { - const string RELATIVE_KEY = nameof(RELATIVE_KEY); - string parentDir = Directory.GetCurrentDirectory(); - - if (excludes == null || !excludes.Any()) return Array.Empty(); - - var matcherDict = new Dictionary() { { RELATIVE_KEY, new Matcher() } }; - foreach (var excludeRule in excludes) - { - if (Path.IsPathRooted(excludeRule)) - { - var root = Path.GetPathRoot(excludeRule); - if (!matcherDict.ContainsKey(root)) - { - matcherDict.Add(root, new Matcher()); - } - matcherDict[root].AddInclude(Path.GetFullPath(excludeRule).Substring(root.Length)); - } - else - { - matcherDict[RELATIVE_KEY].AddInclude(excludeRule); - } - } - - var files = new List(); - foreach (var entry in matcherDict) - { - var root = entry.Key; - var matcher = entry.Value; - var directoryInfo = new DirectoryInfo(root.Equals(RELATIVE_KEY) ? parentDir : root); - var fileMatchResult = matcher.Execute(new DirectoryInfoWrapper(directoryInfo)); - var currentFiles = fileMatchResult.Files - .Select(f => Path.GetFullPath(Path.Combine(directoryInfo.ToString(), f.Path))); - files.AddRange(currentFiles); - } - - return files.Distinct().ToArray(); - } - private static bool IsTypeFilterMatch(string module, string type, string[] filters) { Debug.Assert(module != null); diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 2d591be88..0e1ccc9d4 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -9,7 +9,7 @@ using Coverlet.Core.Helpers; using Coverlet.Core.Logging; using Coverlet.Core.Symbols; - +using Microsoft.Extensions.FileSystemGlobbing; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -22,7 +22,7 @@ internal class Instrumenter private readonly string _identifier; private readonly string[] _excludeFilters; private readonly string[] _includeFilters; - private readonly string[] _excludedFiles; + private readonly ExcludedFilesHelper _excludedFilesHelper; private readonly string[] _excludedAttributes; private readonly bool _singleHit; private readonly bool _isCoreLibrary; @@ -36,6 +36,7 @@ internal class Instrumenter private MethodReference _customTrackerRegisterUnloadEventsMethod; private MethodReference _customTrackerRecordHitMethod; private List _asyncMachineStateMethod; + private List _excludedSourceFiles; public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes, bool singleHit, ILogger logger) { @@ -43,7 +44,7 @@ public Instrumenter(string module, string identifier, string[] excludeFilters, s _identifier = identifier; _excludeFilters = excludeFilters; _includeFilters = includeFilters; - _excludedFiles = excludedFiles ?? Array.Empty(); + _excludedFilesHelper = new ExcludedFilesHelper(excludedFiles, logger); _excludedAttributes = excludedAttributes; _singleHit = singleHit; _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; @@ -103,6 +104,14 @@ public InstrumenterResult Instrument() _result.AsyncMachineStateMethod = _asyncMachineStateMethod == null ? Array.Empty() : _asyncMachineStateMethod.ToArray(); + if (_excludedSourceFiles != null) + { + foreach (string sourceFile in _excludedSourceFiles) + { + _logger.LogVerbose($"Excluded source file: '{sourceFile}'"); + } + } + return _result; } @@ -321,9 +330,12 @@ private void InstrumentType(TypeDefinition type) private void InstrumentMethod(MethodDefinition method) { var sourceFile = method.DebugInformation.SequencePoints.Select(s => s.Document.Url).FirstOrDefault(); - if (!string.IsNullOrEmpty(sourceFile) && _excludedFiles.Contains(sourceFile)) + if (!string.IsNullOrEmpty(sourceFile) && _excludedFilesHelper.Exclude(sourceFile)) { - _logger.LogVerbose($"Excluded source file: '{sourceFile}'"); + if (!(_excludedSourceFiles ??= new List()).Contains(sourceFile)) + { + _excludedSourceFiles.Add(sourceFile); + } return; } @@ -562,7 +574,7 @@ private bool IsExcludeAttribute(CustomAttribute customAttribute) customAttribute.AttributeType.Name.Equals(a.EndsWith("Attribute") ? a : $"{a}Attribute")); } - private static Mono.Cecil.Cil.MethodBody GetMethodBody(MethodDefinition method) + private static MethodBody GetMethodBody(MethodDefinition method) { try { @@ -731,4 +743,36 @@ public override AssemblyDefinition Resolve(AssemblyNameReference name) } } } + + // Exclude files helper https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.filesystemglobbing.matcher?view=aspnetcore-2.2 + internal class ExcludedFilesHelper + { + Matcher _matcher; + + public ExcludedFilesHelper(string[] excludes, ILogger logger) + { + if (excludes != null && excludes.Length > 0) + { + _matcher = new Matcher(); + foreach (var excludeRule in excludes) + { + if (excludeRule is null) + { + continue; + } + logger.LogVerbose($"Excluded source file rule '{excludeRule}'"); + _matcher.AddInclude(Path.IsPathRooted(excludeRule) ? excludeRule.Substring(Path.GetPathRoot(excludeRule).Length) : excludeRule); + } + } + } + + public bool Exclude(string sourceFile) + { + if (_matcher is null || sourceFile is null) + return false; + + // We strip out drive because it doesn't work with globbing + return _matcher.Match(Path.IsPathRooted(sourceFile) ? sourceFile.Substring(Path.GetPathRoot(sourceFile).Length) : sourceFile).HasMatches; + } + } } diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index acabab30b..3c985d31c 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -5,6 +5,7 @@ netstandard2.0 5.1.1 false + preview diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 3ad0df5fd..202f4127f 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -72,61 +72,6 @@ public void TestDeleteHitsFile() Assert.False(File.Exists(tempFile)); } - public static IEnumerable GetExcludedFilesReturnsEmptyArgs => - new[] - { - new object[]{null}, - new object[]{new string[0]}, - new object[]{new string[]{ Path.GetRandomFileName() }}, - new object[]{new string[]{Path.GetRandomFileName(), - Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName())} - } - }; - - [Theory] - [MemberData(nameof(GetExcludedFilesReturnsEmptyArgs))] - public void TestGetExcludedFilesReturnsEmpty(string[] excludedFiles) - { - Assert.False(InstrumentationHelper.GetExcludedFiles(excludedFiles)?.Any()); - } - - [Fact] - public void TestGetExcludedFilesUsingAbsFile() - { - var file = Path.GetRandomFileName(); - File.Create(file).Dispose(); - var excludedFiles = InstrumentationHelper.GetExcludedFiles( - new string[] { Path.Combine(Directory.GetCurrentDirectory(), file) } - ); - File.Delete(file); - Assert.Single(excludedFiles); - } - - [Fact] - public void TestGetExcludedFilesUsingGlobbing() - { - var fileExtension = Path.GetRandomFileName(); - var paths = new string[]{ - $"{Path.GetRandomFileName()}.{fileExtension}", - $"{Path.GetRandomFileName()}.{fileExtension}" - }; - - foreach (var path in paths) - { - File.Create(path).Dispose(); - } - - var excludedFiles = InstrumentationHelper.GetExcludedFiles( - new string[] { $"*.{fileExtension}" }); - - foreach (var path in paths) - { - File.Delete(path); - } - - Assert.Equal(paths.Length, excludedFiles.Count()); - } - [Fact] public void TestIsModuleExcludedWithoutFilter() { diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 3f7c592ad..1b31d50a6 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Coverlet.Core.Helpers; using Coverlet.Core.Logging; @@ -238,6 +240,107 @@ public void TestInstrument_NetStandardAwareAssemblyResolver_FromFolder() Assert.Equal(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "netstandard.dll"), Path.GetFullPath(resolved.MainModule.FileName)); } + public static IEnumerable TestInstrument_ExcludedFilesHelper_Data() + { + yield return new object[] { new string[]{ @"one.txt" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\one.txt", false, true), + (@"dir/one.txt", false, false) + }}; + yield return new object[] { new string[]{ @"*one.txt" }, new ValueTuple[] + { + (@"one.txt", true , false), + (@"c:\dir\one.txt", false, true), + (@"dir/one.txt", false, false) + }}; + yield return new object[] { new string[]{ @"*.txt" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\one.txt", false, true), + (@"dir/one.txt", false, false) + }}; + yield return new object[] { new string[]{ @"*.*" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\one.txt", false, true), + (@"dir/one.txt", false, false) + }}; + yield return new object[] { new string[]{ @"one.*" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\one.txt", false, true), + (@"dir/one.txt", false, false) + }}; + yield return new object[] { new string[]{ @"dir/*.txt" }, new ValueTuple[] + { + (@"one.txt", false, false), + (@"c:\dir\one.txt", true, true), + (@"dir/one.txt", true, false) + }}; + yield return new object[] { new string[]{ @"dir\*.txt" }, new ValueTuple[] + { + (@"one.txt", false, false), + (@"c:\dir\one.txt", true, true), + (@"dir/one.txt", true, false) + }}; + yield return new object[] { new string[]{ @"**/*" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\one.txt", true, true), + (@"dir/one.txt", true, false) + }}; + yield return new object[] { new string[]{ @"dir/**/*" }, new ValueTuple[] + { + (@"one.txt", false, false), + (@"c:\dir\one.txt", true, true), + (@"dir/one.txt", true, false), + (@"c:\dir\dir2\one.txt", true, true), + (@"dir/dir2/one.txt", true, false) + }}; + yield return new object[] { new string[]{ @"one.txt", @"dir\*two.txt" }, new ValueTuple[] + { + (@"one.txt", true, false), + (@"c:\dir\imtwo.txt", true, true), + (@"dir/one.txt", false, false) + }}; + + // This is a special case test different drive same path + // We strip out drive from path to check for globbing + // BTW I don't know if makes sense add a filter with full path maybe we should forbid + yield return new object[] { new string[]{ @"c:\dir\one.txt" }, new ValueTuple[] + { + (@"c:\dir\one.txt", true, true), + (@"d:\dir\one.txt", true, true) // maybe should be false? + }}; + + yield return new object[] { new string[]{ null }, new ValueTuple[] + { + (null, false, false), + }}; + } + + [Theory] + [MemberData(nameof(TestInstrument_ExcludedFilesHelper_Data))] + public void TestInstrument_ExcludedFilesHelper(string[] excludeFilterHelper, ValueTuple[] result) + { + var exludeFilterHelper = new ExcludedFilesHelper(excludeFilterHelper, new Mock().Object); + foreach (ValueTuple checkFile in result) + { + if (checkFile.Item3) // run test only on windows platform + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Equal(checkFile.Item2, exludeFilterHelper.Exclude(checkFile.Item1)); + } + } + else + { + Assert.Equal(checkFile.Item2, exludeFilterHelper.Exclude(checkFile.Item1)); + } + } + } + [Fact] public void SkipEmbeddedPpdbWithoutLocalSource() {