New parameter to control automatic assembly exclusion (#1392)
New parameter to control automatic assembly exclusion
This commit is contained in:
@@ -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
|
||||
|
||||
+23
-23
@@ -14,31 +14,31 @@ Cross platform .NET Core code coverage tool 3.0.0.0
|
||||
Usage: coverlet [arguments] [options]
|
||||
|
||||
Arguments:
|
||||
<ASSEMBLY|DIRECTORY> Path to the test assembly or application directory.
|
||||
<ASSEMBLY|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.
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
@@ -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 |
|
||||
|
||||
@@ -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?
|
||||
<IncludeTestAssembly>true</IncludeTestAssembly>
|
||||
<SkipAutoProps>true</SkipAutoProps>
|
||||
<DeterministicReport>false</DeterministicReport>
|
||||
<InstrumentModulesWithoutLocalSources>false</InstrumentModulesWithoutLocalSources>
|
||||
<ExcludeAssembliesWithoutSources>MissingAll,MissingAny,None</ExcludeAssembliesWithoutSources>
|
||||
</Configuration>
|
||||
</DataCollector>
|
||||
</DataCollectors>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -82,9 +82,9 @@ namespace Coverlet.Collector.DataCollection
|
||||
public bool DeterministicReport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instruments modules even if the sources from the PDBs can't be resolved.
|
||||
/// Switch for heuristic to automatically exclude assemblies without source.
|
||||
/// </summary>
|
||||
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<string>()));
|
||||
builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport);
|
||||
builder.AppendFormat("InstrumentModulesWithoutLocalSources: '{0}'", InstrumentModulesWithoutLocalSources);
|
||||
builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources);
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse InstrumentModulesWithoutLocalSources flag
|
||||
/// Parse ExcludeAssembliesWithoutSources flag
|
||||
/// </summary>
|
||||
/// <param name="configurationElement">Configuration element</param>
|
||||
/// <returns>InstrumentModulesWithoutLocalSources flag</returns>
|
||||
private static bool ParseInstrumentModulesWithoutLocalSources(XmlElement configurationElement)
|
||||
/// <returns>ExcludeAssembliesWithoutSources flag</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ISourceRootTranslator>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> _excludedLambdaMethods;
|
||||
private List<string> _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<string> 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<AssemblyVersion>5.8.0</AssemblyVersion>
|
||||
<AssemblyVersion>6.0.0</AssemblyVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -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(',')
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<Threshold Condition="$(Threshold) == ''">0</Threshold>
|
||||
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
|
||||
<ThresholdStat Condition="$(ThresholdStat) == ''">minimum</ThresholdStat>
|
||||
<ExcludeAssembliesWithoutSources Condition="$(ExcludeAssembliesWithoutSources) == ''"></ExcludeAssembliesWithoutSources>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<CoverletToolsPath Condition=" '$(CoverletToolsPath)' == '' ">$(MSBuildThisFileDirectory)</CoverletToolsPath>
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
UseSourceLink="$(UseSourceLink)"
|
||||
SkipAutoProps="$(SkipAutoProps)"
|
||||
DeterministicReport="$(DeterministicReport)"
|
||||
DoesNotReturnAttribute="$(DoesNotReturnAttribute)">
|
||||
DoesNotReturnAttribute="$(DoesNotReturnAttribute)"
|
||||
ExcludeAssembliesWithoutSources="$(ExcludeAssembliesWithoutSources)">
|
||||
<Output TaskParameter="InstrumenterState" PropertyName="InstrumenterState"/>
|
||||
</Coverlet.MSbuild.Tasks.InstrumentationTask>
|
||||
</Target>
|
||||
|
||||
@@ -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<FileSystem> {CallBase = true};
|
||||
fileSystem.Setup(x => x.Exists(It.IsAny<string>())).Returns(false);
|
||||
@@ -42,15 +43,35 @@ namespace Coverlet.Core.Helpers.Tests
|
||||
var instrumentationHelper =
|
||||
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock<ILogger>().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock<ILogger>().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<FileSystem> { CallBase = true };
|
||||
fileSystem.SetupSequence(x => x.Exists(It.IsAny<string>()))
|
||||
.Returns(false)
|
||||
.Returns(() =>
|
||||
{
|
||||
fileSystem.Setup(y => y.Exists(It.IsAny<string>())).Returns(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
var instrumentationHelper =
|
||||
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), fileSystem.Object, new Mock<ILogger>().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock<ILogger>().Object, new FileSystem()));
|
||||
|
||||
Assert.Equal(result, instrumentationHelper.PortablePdbHasLocalSource(typeof(InstrumentationHelperTests).Assembly.Location, (AssemblySearchType) assemblySearchType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -423,7 +423,7 @@ namespace Coverlet.Core.Instrumentation.Tests
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
|
||||
var instrumentationHelper =
|
||||
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock<ILogger>().Object,
|
||||
new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), loggerMock.Object,
|
||||
new SourceRootTranslator(xunitDll, new Mock<ILogger>().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<string>()));
|
||||
_mockLogger.Verify(l => l.LogVerbose(It.IsAny<string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -524,6 +524,20 @@ namespace Coverlet.Core.Instrumentation.Tests
|
||||
Assert.True(instrumenter.CanInstrument());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanInstrument_AssemblySearchTypeNone_ReturnsTrue()
|
||||
{
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
var instrumentationHelper = new Mock<IInstrumentationHelper>();
|
||||
bool embeddedPdb;
|
||||
instrumentationHelper.Setup(x => x.HasPdb(It.IsAny<string>(), out embeddedPdb)).Returns(true);
|
||||
|
||||
var instrumenter = new Instrumenter(It.IsAny<string>(), It.IsAny<string>(), new CoverageParameters{ExcludeAssembliesWithoutSources = "None"},
|
||||
loggerMock.Object, instrumentationHelper.Object, new Mock<IFileSystem>().Object, new Mock<ISourceRootTranslator>().Object, new CecilSymbolHelper());
|
||||
|
||||
Assert.True(instrumenter.CanInstrument());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("NotAMatch", new string[] { }, false)]
|
||||
[InlineData("ExcludeFromCoverageAttribute", new string[] { }, true)]
|
||||
|
||||
+1
-1
@@ -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$"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user