diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index c71be1a..9945361 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking changes +- New parameter `ExcludeAssembliesWithoutSources` to control automatic assembly exclusion [1164](https://github.com/coverlet-coverage/coverlet/issues/1164). The parameter `InstrumentModulesWithoutLocalSources` has been removed. since it can be handled by setting `ExcludeAssembliesWithoutSources` to `None`. +- The default heuristics for determining whether to instrument an assembly has been changed. In previous versions any missing source file was taken as a signal that it was a third-party project that shouldn't be instrumented, with exceptions for some common file name patterns for source generators. Now only assemblies where no source files at all can be found are excluded from instrumentation, and the code for detecting source generator files have been removed. To get back to the behaviour that at least one missing file is sufficient to exclude an assembly, set `ExcludeAssembliesWithoutSources` to `MissingAny`, or use assembly exclusion filters for more fine-grained control. + ## Release date 2022-10-29 ### Packages coverlet.msbuild 3.2.0 diff --git a/Documentation/GlobalTool.md b/Documentation/GlobalTool.md index 64d659d..455f34b 100644 --- a/Documentation/GlobalTool.md +++ b/Documentation/GlobalTool.md @@ -14,31 +14,31 @@ Cross platform .NET Core code coverage tool 3.0.0.0 Usage: coverlet [arguments] [options] Arguments: - Path to the test assembly or application directory. + Path to the test assembly or application directory. 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 - -v|--verbosity Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed. - -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. - --threshold-stat Coverage statistic used to enforce the threshold value. - --exclude Filter expressions to exclude specific modules and types. - --include Filter expressions to include only specific modules and types. - --exclude-by-file Glob patterns specifying source files to exclude. - --include-directory Include directories containing additional assemblies to be instrumented. - --exclude-by-attribute Attributes to exclude from code coverage. - --include-test-assembly Specifies whether to report code coverage of the test assembly. - --single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location - --skipautoprops Neither track nor record auto-implemented properties. - --merge-with Path to existing coverage result to merge. - --use-source-link Specifies whether to use SourceLink URIs in place of file system paths. - --does-not-return-attribute Attributes that mark methods that do not return. - --instrument-modules-without-local-sources Specifies whether modules should be instrumented even if the sources from the PDBs can't be found locally. + -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 + -v|--verbosity Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed. + -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. + --threshold-stat Coverage statistic used to enforce the threshold value. + --exclude Filter expressions to exclude specific modules and types. + --include Filter expressions to include only specific modules and types. + --exclude-by-file Glob patterns specifying source files to exclude. + --include-directory Include directories containing additional assemblies to be instrumented. + --exclude-by-attribute Attributes to exclude from code coverage. + --include-test-assembly Specifies whether to report code coverage of the test assembly. + --single-hit Specifies whether to limit code coverage hit reporting to a single hit for each location + --skipautoprops Neither track nor record auto-implemented properties. + --merge-with Path to existing coverage result to merge. + --use-source-link Specifies whether to use SourceLink URIs in place of file system paths. + --does-not-return-attribute Attributes that mark methods that do not return. + --exclude-assemblies-without-sources Specifies behaviour of heuristic to ignore assemblies with missing source documents. ``` NB. For a [multiple value] options you have to specify values multiple times i.e. diff --git a/Documentation/MSBuildIntegration.md b/Documentation/MSBuildIntegration.md index 1848c3e..a937665 100644 --- a/Documentation/MSBuildIntegration.md +++ b/Documentation/MSBuildIntegration.md @@ -228,3 +228,19 @@ To generate deterministc report the parameter is: ``` /p:DeterministicReport=true ``` + +## Exclude assemblies without sources from coverage + +The heuristic coverlet uses to determine if an assembly is a third-party dependency is based on the matching of the assembly`s source documents and the corresponding source files. +This parameter has three different values to control the automatic assembly exclusion. + +| Parameter | Description | +|-----------|-------------| +| MissingAll | Includes the assembly if at least one document is matching. In case the `ExcludeAssembliesWithoutSources` parameter is not specified the default value is `MissingAll`. | +| MissingAny | Includes the assembly only if all documents can be matched to corresponding source files. | +| None | No assembly is excluded. | + +Here is an example of how to specifiy the parameter: +``` +/p:ExcludeAssembliesWithoutSources="MissingAny" +``` \ No newline at end of file diff --git a/Documentation/ReleasePlan.md b/Documentation/ReleasePlan.md index 9a268be..f5b885d 100644 --- a/Documentation/ReleasePlan.md +++ b/Documentation/ReleasePlan.md @@ -23,9 +23,9 @@ We release 3 components as NuGet packages: | Package | Version | |:----------------------|:--------| -|**coverlet.msbuild** | 3.1.2 | -|**coverlet.console** | 3.1.2 | -|**coverlet.collector** | 3.1.2 | +|**coverlet.msbuild** | 3.2.0 | +|**coverlet.console** | 3.2.0 | +|**coverlet.collector** | 3.2.0 | | Release Date | coverlet.msbuild | coverlet.console | coverlet.collector| commit hash | notes | diff --git a/Documentation/VSTestIntegration.md b/Documentation/VSTestIntegration.md index b01e4ee..279250b 100644 --- a/Documentation/VSTestIntegration.md +++ b/Documentation/VSTestIntegration.md @@ -97,8 +97,8 @@ These are a list of options that are supported by coverlet. These can be specifi | IncludeTestAssembly | Include coverage of the test assembly. | | SkipAutoProps | Neither track nor record auto-implemented properties. | | DoesNotReturnAttribute | Methods marked with these attributes are known not to return, statements following them will be excluded from coverage | -| DeterministicReport | Generates deterministic report in context of deterministic build. Take a look at [documentation](DeterministicBuild.md) for further informations. | -| InstrumentModulesWithoutLocalSources | Specifies whether modules should be instrumented even if the sources from the PDBs can't be found locally. | +| DeterministicReport | Generates deterministic report in context of deterministic build. Take a look at [documentation](DeterministicBuild.md) for further informations. +| ExcludeAssembliesWithoutSources | Specifies whether to exclude assemblies without source. Options are either MissingAll, MissingAny or None. Default is MissingAll.| How to specify these options via runsettings? @@ -120,7 +120,7 @@ How to specify these options via runsettings? true true false - false + MissingAll,MissingAny,None diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs index 0f8dc42..0569ab2 100644 --- a/src/coverlet.collector/DataCollection/CoverageWrapper.cs +++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs @@ -34,7 +34,7 @@ namespace Coverlet.Collector.DataCollection SkipAutoProps = settings.SkipAutoProps, DoesNotReturnAttributes = settings.DoesNotReturnAttributes, DeterministicReport = settings.DeterministicReport, - InstrumentModulesWithoutLocalSources = settings.InstrumentModulesWithoutLocalSources + ExcludeAssembliesWithoutSources = settings.ExcludeAssembliesWithoutSources }; return new Coverage( diff --git a/src/coverlet.collector/DataCollection/CoverletSettings.cs b/src/coverlet.collector/DataCollection/CoverletSettings.cs index 7a23e0a..2ebde79 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettings.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettings.cs @@ -82,9 +82,9 @@ namespace Coverlet.Collector.DataCollection public bool DeterministicReport { get; set; } /// - /// Instruments modules even if the sources from the PDBs can't be resolved. + /// Switch for heuristic to automatically exclude assemblies without source. /// - public bool InstrumentModulesWithoutLocalSources { get; set; } + public string ExcludeAssembliesWithoutSources { get; set; } public override string ToString() { @@ -103,7 +103,7 @@ namespace Coverlet.Collector.DataCollection builder.AppendFormat("SkipAutoProps: '{0}'", SkipAutoProps); builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty())); builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport); - builder.AppendFormat("InstrumentModulesWithoutLocalSources: '{0}'", InstrumentModulesWithoutLocalSources); + builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources); return builder.ToString(); } diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs index b1a77b1..3776c98 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs @@ -48,7 +48,7 @@ namespace Coverlet.Collector.DataCollection coverletSettings.SkipAutoProps = ParseSkipAutoProps(configurationElement); coverletSettings.DoesNotReturnAttributes = ParseDoesNotReturnAttributes(configurationElement); coverletSettings.DeterministicReport = ParseDeterministicReport(configurationElement); - coverletSettings.InstrumentModulesWithoutLocalSources = ParseInstrumentModulesWithoutLocalSources(configurationElement); + coverletSettings.ExcludeAssembliesWithoutSources = ParseExcludeAssembliesWithoutSources(configurationElement); } coverletSettings.ReportFormats = ParseReportFormats(configurationElement); @@ -213,15 +213,14 @@ namespace Coverlet.Collector.DataCollection } /// - /// Parse InstrumentModulesWithoutLocalSources flag + /// Parse ExcludeAssembliesWithoutSources flag /// /// Configuration element - /// InstrumentModulesWithoutLocalSources flag - private static bool ParseInstrumentModulesWithoutLocalSources(XmlElement configurationElement) + /// ExcludeAssembliesWithoutSources flag + private static string ParseExcludeAssembliesWithoutSources(XmlElement configurationElement) { - XmlElement instrumentModulesWithoutLocalSourcesElement = configurationElement[CoverletConstants.InstrumentModulesWithoutLocalSources]; - bool.TryParse(instrumentModulesWithoutLocalSourcesElement?.InnerText, out bool instrumentModulesWithoutLocalSources); - return instrumentModulesWithoutLocalSources; + XmlElement instrumentModulesWithoutLocalSourcesElement = configurationElement[CoverletConstants.ExcludeAssembliesWithoutSources]; + return instrumentModulesWithoutLocalSourcesElement?.InnerText; } /// diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index a8bd770..3a7bfa9 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -26,6 +26,6 @@ namespace Coverlet.Collector.Utilities public const string SkipAutoProps = "SkipAutoProps"; public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute"; public const string DeterministicReport = "DeterministicReport"; - public const string InstrumentModulesWithoutLocalSources = "InstrumentModulesWithoutLocalSources"; + public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources"; } } diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index a80f6ff..d967f90 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -70,7 +70,7 @@ namespace Coverlet.Console CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue); CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue); CommandOption doesNotReturnAttributes = app.Option("--does-not-return-attribute", "Attributes that mark methods that do not return.", CommandOptionType.MultipleValue); - CommandOption instrumentModulesWithoutLocalSources = app.Option("--instrument-modules-without-local-sources", "Specifies whether modules should be instrumented even if the sources from the PDBs can't be found locally.", CommandOptionType.NoValue); + CommandOption excludeAssembliesWithoutSources = app.Option("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.", CommandOptionType.SingleValue); app.OnExecute(() => { @@ -99,7 +99,7 @@ namespace Coverlet.Console UseSourceLink = useSourceLink.HasValue(), SkipAutoProps = skipAutoProp.HasValue(), DoesNotReturnAttributes = doesNotReturnAttributes.Values.ToArray(), - InstrumentModulesWithoutLocalSources = instrumentModulesWithoutLocalSources.HasValue(), + ExcludeAssembliesWithoutSources = excludeAssembliesWithoutSources.Value() }; ISourceRootTranslator sourceRootTranslator = serviceProvider.GetRequiredService(); diff --git a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs index ed41dcc..65af400 100644 --- a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs +++ b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs @@ -1,6 +1,8 @@ // Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Coverlet.Core.Enums; + namespace Coverlet.Core.Abstractions { internal interface IInstrumentationHelper @@ -15,8 +17,8 @@ namespace Coverlet.Core.Abstractions bool IsTypeExcluded(string module, string type, string[] excludeFilters); bool IsTypeIncluded(string module, string type, string[] includeFilters); void RestoreOriginalModule(string module, string identifier); - bool EmbeddedPortablePdbHasLocalSource(string module, out string firstNotFoundDocument); - bool PortablePdbHasLocalSource(string module, out string firstNotFoundDocument); + bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); + bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); bool IsLocalMethod(string method); void SetLogger(ILogger logger); } diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index d035799..8a3def3 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -44,7 +44,7 @@ namespace Coverlet.Core [DataMember] public bool DeterministicReport { get; set; } [DataMember] - public bool InstrumentModulesWithoutLocalSources { get; set; } + public string ExcludeAssembliesWithoutSources { get; set; } } internal class Coverage diff --git a/src/coverlet.core/Enums/AssemblySearchType.cs b/src/coverlet.core/Enums/AssemblySearchType.cs new file mode 100644 index 0000000..099e542 --- /dev/null +++ b/src/coverlet.core/Enums/AssemblySearchType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Coverlet.Core.Enums +{ + internal enum AssemblySearchType + { + MissingAny, + MissingAll, + None + } +} diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index fcf9f4f..bbd234a 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -12,6 +12,7 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; using Coverlet.Core.Abstractions; +using Coverlet.Core.Enums; namespace Coverlet.Core.Helpers { @@ -122,9 +123,8 @@ namespace Coverlet.Core.Helpers return false; } - public bool EmbeddedPortablePdbHasLocalSource(string module, out string firstNotFoundDocument) + public bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) { - firstNotFoundDocument = ""; using (Stream moduleStream = _fileSystem.OpenRead(module)) using (var peReader = new PEReader(moduleStream)) { @@ -135,11 +135,8 @@ namespace Coverlet.Core.Helpers using MetadataReaderProvider embeddedMetadataProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); MetadataReader metadataReader = embeddedMetadataProvider.GetMetadataReader(); - (bool allDocumentsMatch, string notFoundDocument) = MatchDocumentsWithSources(metadataReader); - - if (!allDocumentsMatch) + if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) { - firstNotFoundDocument = notFoundDocument; return false; } } @@ -151,9 +148,8 @@ namespace Coverlet.Core.Helpers return true; } - public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDocument) + public bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) { - firstNotFoundDocument = ""; using (Stream moduleStream = _fileSystem.OpenRead(module)) using (var peReader = new PEReader(moduleStream)) { @@ -175,11 +171,8 @@ namespace Coverlet.Core.Helpers return true; } - (bool allDocumentsMatch, string notFoundDocument) = MatchDocumentsWithSources(metadataReader); - - if (!allDocumentsMatch) + if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) { - firstNotFoundDocument = notFoundDocument; return false; } } @@ -189,25 +182,57 @@ namespace Coverlet.Core.Helpers return true; } - private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSources(MetadataReader metadataReader) + private bool MatchDocumentsWithSources(string module, AssemblySearchType excludeAssembliesWithoutSources, + MetadataReader metadataReader) { - foreach (DocumentHandle docHandle in metadataReader.Documents) + if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAll)) + { + bool anyDocumentMatches = MatchDocumentsWithSourcesMissingAll(metadataReader); + if (!anyDocumentMatches) + { + _logger.LogVerbose($"Excluding module from instrumentation: {module}, pdb without any local source files"); + return false; + } + } + + if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAny)) + { + (bool allDocumentsMatch, string notFoundDocument) = MatchDocumentsWithSourcesMissingAny(metadataReader); + + if (!allDocumentsMatch) + { + _logger.LogVerbose( + $"Excluding module from instrumentation: {module}, pdb without local source files, [{FileSystem.EscapeFileName(notFoundDocument)}]"); + return false; + } + } + + return true; + } + + private IEnumerable<(string documentName, bool documentExists)> DocumentSourceMap(MetadataReader metadataReader) + { + return metadataReader.Documents.Select(docHandle => { Document document = metadataReader.GetDocument(docHandle); string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); - Guid languageGuid = metadataReader.GetGuid(document.Language); + return (docName, _fileSystem.Exists(docName)); + }); + } + + private bool MatchDocumentsWithSourcesMissingAll(MetadataReader metadataReader) + { + return DocumentSourceMap(metadataReader).Any(x => x.documentExists); + } + + private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSourcesMissingAny( + MetadataReader metadataReader) + { + var documentSourceMap = DocumentSourceMap(metadataReader).ToList(); + + if (documentSourceMap.Any(x => !x.documentExists)) + return (false, documentSourceMap.FirstOrDefault(x => !x.documentExists).documentName); - // We verify all docs and return false if not all are present in local - // We could have false negative if doc is not a source - // Btw check for all possible extension could be weak approach - // We exlude from the check the autogenerated source file(i.e. source generators) - // We exclude special F# construct https://github.com/coverlet-coverage/coverlet/issues/1145 - if (!_fileSystem.Exists(docName) && !IsGeneratedDocumentName(docName) && - !IsUnknownModuleInFSharpAssembly(languageGuid, docName)) - { - return (false, docName); - } - } return (true, string.Empty); } @@ -467,41 +492,5 @@ namespace Coverlet.Core.Helpers return false; } } - - // Follow the same rules that exist in Microsoft.CodeAnalysis - // https://sourceroslyn.io/#Microsoft.CodeAnalysis/InternalUtilities/GeneratedCodeUtilities.cs,55bff725ec9f1338,references - private static bool IsGeneratedDocumentName(string docPath) - { - if (!string.IsNullOrEmpty(docPath)) - { - string fileName = Path.GetFileName(docPath); - if (fileName.StartsWith("TemporaryGeneratedFile_", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - string extension = Path.GetExtension(fileName); - if (!string.IsNullOrEmpty(extension)) - { - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(docPath); - if (fileNameWithoutExtension.EndsWith(".designer", StringComparison.OrdinalIgnoreCase) || - fileNameWithoutExtension.EndsWith(".generated", StringComparison.OrdinalIgnoreCase) || - fileNameWithoutExtension.EndsWith(".g", StringComparison.OrdinalIgnoreCase) || - fileNameWithoutExtension.EndsWith(".g.i", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - private static bool IsUnknownModuleInFSharpAssembly(Guid languageGuid, string docName) - { - // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#document-table-0x30 - return languageGuid.Equals(new Guid("ab4f38c9-b6e6-43ba-be3b-58080b2ccce3")) - && docName.EndsWith("unknown"); - } } } diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index e3ff94a..502a356 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Coverlet.Core.Abstractions; using Coverlet.Core.Attributes; +using Coverlet.Core.Enums; using Coverlet.Core.Helpers; using Coverlet.Core.Instrumentation.Reachability; using Coverlet.Core.Symbols; @@ -33,6 +34,8 @@ namespace Coverlet.Core.Instrumentation private readonly IFileSystem _fileSystem; private readonly ISourceRootTranslator _sourceRootTranslator; private readonly ICecilSymbolHelper _cecilSymbolHelper; + private readonly string[] _doesNotReturnAttributes; + private readonly AssemblySearchType _excludeAssembliesWithoutSources; private InstrumenterResult _result; private FieldDefinition _customTrackerHitsArray; private FieldDefinition _customTrackerHitsFilePath; @@ -47,7 +50,6 @@ namespace Coverlet.Core.Instrumentation private List<(MethodDefinition, int)> _excludedMethods; private List _excludedLambdaMethods; private List _excludedCompilerGeneratedTypes; - private readonly string[] _doesNotReturnAttributes; private ReachabilityHelper _reachabilityHelper; public bool SkipModule { get; set; } @@ -74,6 +76,16 @@ namespace Coverlet.Core.Instrumentation _sourceRootTranslator = sourceRootTranslator; _cecilSymbolHelper = cecilSymbolHelper; _doesNotReturnAttributes = PrepareAttributes(parameters.DoesNotReturnAttributes); + _excludeAssembliesWithoutSources = DetermineHeuristics(parameters.ExcludeAssembliesWithoutSources); + } + + private AssemblySearchType DetermineHeuristics(string parametersExcludeAssembliesWithoutSources) + { + if (Enum.TryParse(parametersExcludeAssembliesWithoutSources, true, out AssemblySearchType option)) + { + return option; + } + return AssemblySearchType.MissingAll; } private static string[] PrepareAttributes(IEnumerable providedAttrs, params string[] defaultAttrs) @@ -94,34 +106,18 @@ namespace Coverlet.Core.Instrumentation { if (_instrumentationHelper.HasPdb(_module, out bool embeddedPdb)) { - if (this._parameters.InstrumentModulesWithoutLocalSources) + if (_excludeAssembliesWithoutSources.Equals(AssemblySearchType.None)) { return true; } if (embeddedPdb) { - if (_instrumentationHelper.EmbeddedPortablePdbHasLocalSource(_module, out string firstNotFoundDocument)) - { - return true; - } - else - { - _logger.LogVerbose($"Unable to instrument module: {_module}, embedded pdb without local source files, [{FileSystem.EscapeFileName(firstNotFoundDocument)}]"); - return false; - } + return _instrumentationHelper.EmbeddedPortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); } else { - if (_instrumentationHelper.PortablePdbHasLocalSource(_module, out string firstNotFoundDocument)) - { - return true; - } - else - { - _logger.LogVerbose($"Unable to instrument module: {_module}, pdb without local source files, [{FileSystem.EscapeFileName(firstNotFoundDocument)}]"); - return false; - } + return _instrumentationHelper.PortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); } } else diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index cb48401..f30498d 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -3,7 +3,7 @@ Library netstandard2.0 - 5.8.0 + 6.0.0 false diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs index b43f152..fa0868e 100644 --- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs +++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs @@ -47,7 +47,7 @@ namespace Coverlet.MSbuild.Tasks public bool DeterministicReport { get; set; } - public bool InstrumentModulesWithoutLocalSources { get; set; } + public string ExcludeAssembliesWithoutSources { get; set; } [Output] public ITaskItem InstrumenterState { get; set; } @@ -101,7 +101,7 @@ namespace Coverlet.MSbuild.Tasks UseSourceLink = UseSourceLink, SkipAutoProps = SkipAutoProps, DeterministicReport = DeterministicReport, - InstrumentModulesWithoutLocalSources = InstrumentModulesWithoutLocalSources, + ExcludeAssembliesWithoutSources = ExcludeAssembliesWithoutSources, DoesNotReturnAttributes = DoesNotReturnAttribute?.Split(',') }; diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.props b/src/coverlet.msbuild.tasks/coverlet.msbuild.props index 3821845..9356dbd 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.props +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.props @@ -16,6 +16,7 @@ 0 line,branch,method minimum + $(MSBuildThisFileDirectory) diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets index c271ec2..6e71373 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets @@ -49,7 +49,8 @@ UseSourceLink="$(UseSourceLink)" SkipAutoProps="$(SkipAutoProps)" DeterministicReport="$(DeterministicReport)" - DoesNotReturnAttribute="$(DoesNotReturnAttribute)"> + DoesNotReturnAttribute="$(DoesNotReturnAttribute)" + ExcludeAssembliesWithoutSources="$(ExcludeAssembliesWithoutSources)"> diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index c6feee2..8f5a055 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -9,6 +9,7 @@ using System.Linq; using Castle.Core.Internal; using Moq; using Coverlet.Core.Abstractions; +using Coverlet.Core.Enums; namespace Coverlet.Core.Helpers.Tests { @@ -34,7 +35,7 @@ namespace Coverlet.Core.Helpers.Tests } [Fact] - public void EmbeddedPortablePDPHasLocalSource_DocumentDoesNotExist_ReturnsFalse() + public void EmbeddedPortablePDPHasLocalSource_NoDocumentsExist_ReturnsFalse() { var fileSystem = new Mock {CallBase = true}; fileSystem.Setup(x => x.Exists(It.IsAny())).Returns(false); @@ -42,15 +43,35 @@ namespace Coverlet.Core.Helpers.Tests var instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock().Object, new FileSystem())); - Assert.False(instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument)); - Assert.False(notFoundDocument.IsNullOrEmpty()); + Assert.False(instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, AssemblySearchType.MissingAny)); + Assert.False(instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, AssemblySearchType.MissingAll)); } [Fact] public void EmbeddedPortablePDPHasLocalSource_AllDocumentsExist_ReturnsTrue() { - Assert.True(_instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, out string notFoundDocument)); - Assert.True(notFoundDocument.IsNullOrEmpty()); + Assert.True(_instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, AssemblySearchType.MissingAny)); + Assert.True(_instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, AssemblySearchType.MissingAll)); + } + + [Theory] + [InlineData(AssemblySearchType.MissingAny, false)] + [InlineData(AssemblySearchType.MissingAll, true)] + public void EmbeddedPortablePDPHasLocalSource_FirstDocumentDoesNotExist_ReturnsExpectedValue(object assemblySearchType, bool result) + { + var fileSystem = new Mock { CallBase = true }; + fileSystem.SetupSequence(x => x.Exists(It.IsAny())) + .Returns(false) + .Returns(() => + { + fileSystem.Setup(y => y.Exists(It.IsAny())).Returns(true); + return true; + }); + + var instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock().Object, new FileSystem())); + + Assert.Equal(result, instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, (AssemblySearchType) assemblySearchType)); } [Fact] diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 1896488..4dc155b 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -423,7 +423,7 @@ namespace Coverlet.Core.Instrumentation.Tests var loggerMock = new Mock(); var instrumentationHelper = - new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), loggerMock.Object, new SourceRootTranslator(xunitDll, new Mock().Object, new FileSystem())); var instrumenter = new Instrumenter(xunitDll, "_xunit_instrumented", new CoverageParameters(), loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(xunitDll, loggerMock.Object, new FileSystem()), new CecilSymbolHelper()); @@ -488,7 +488,7 @@ namespace Coverlet.Core.Instrumentation.Tests Assert.True(instrumentationHelper.HasPdb(sample, out bool embedded)); Assert.False(embedded); Assert.False(instrumenter.CanInstrument()); - loggerMock.Verify(l => l.LogVerbose(It.IsAny())); + _mockLogger.Verify(l => l.LogVerbose(It.IsAny())); } [Fact] @@ -524,6 +524,20 @@ namespace Coverlet.Core.Instrumentation.Tests Assert.True(instrumenter.CanInstrument()); } + [Fact] + public void CanInstrument_AssemblySearchTypeNone_ReturnsTrue() + { + var loggerMock = new Mock(); + var instrumentationHelper = new Mock(); + bool embeddedPdb; + instrumentationHelper.Setup(x => x.HasPdb(It.IsAny(), out embeddedPdb)).Returns(true); + + var instrumenter = new Instrumenter(It.IsAny(), It.IsAny(), new CoverageParameters{ExcludeAssembliesWithoutSources = "None"}, + loggerMock.Object, instrumentationHelper.Object, new Mock().Object, new Mock().Object, new CecilSymbolHelper()); + + Assert.True(instrumenter.CanInstrument()); + } + [Theory] [InlineData("NotAMatch", new string[] { }, false)] [InlineData("ExcludeFromCoverageAttribute", new string[] { }, true)] diff --git a/version.json b/version.json index 2aa0bb4..eb46a76 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.2.1-preview.{height}", + "version": "4.0.0-preview.{height}", "publicReleaseRefSpec": [ "^refs/heads/master$" ],