Description
Given that have update ms code coverage which has this new setting and have updated Coverlet ( which may have already had this setting ).
Note that both require MSBuild targets that use
the target InitializeSourceRootMappedPaths and have similar behaviour.
The targets write files that are used for this functionality.
From Coverlet
ExcludeAssembliesWithoutSource
Specifies whether to exclude assemblies without source. Options are either MissingAll, MissingAny or None. Default is MissingAll.
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
...
<ExcludeAssembliesWithoutSources>MissingAll,MissingAny,None</ExcludeAssembliesWithoutSources>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
https://github.com/coverlet-coverage/coverlet/blob/master/src/coverlet.collector/build/coverlet.collector.targets
https://github.com/coverlet-coverage/coverlet/blob/master/src/coverlet.msbuild.tasks/coverlet.msbuild.targets
** But what about console ? **
Like ms code coverage there is a
SourceRootTranslator that reads files output from msbuild.
ctor
string mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}";
_sourceRootMapping = LoadSourceRootMapping(Path.Combine(Path.GetDirectoryName(moduleTestPath), mappingFileName));
The Instrumenter
determines the AssemblySearchType in the ctor
public Instrumenter(
string module,
string identifier,
CoverageParameters parameters,
ILogger logger,
IInstrumentationHelper instrumentationHelper,
IFileSystem fileSystem,
ISourceRootTranslator sourceRootTranslator,
ICecilSymbolHelper cecilSymbolHelper)
{
_module = module;
_identifier = identifier;
_parameters = parameters;
_excludedFilesHelper = new ExcludedFilesHelper(parameters.ExcludedSourceFiles, logger);
_excludedAttributes = PrepareAttributes(parameters.ExcludeAttributes, nameof(ExcludeFromCoverageAttribute), nameof(ExcludeFromCodeCoverageAttribute));
_isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib";
_logger = logger;
_instrumentationHelper = instrumentationHelper;
_fileSystem = fileSystem;
_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;
}
and it is used in CanInstrument
public bool CanInstrument()
{
try
{
if (_instrumentationHelper.HasPdb(_module, out bool embeddedPdb))
{
if (_excludeAssembliesWithoutSources.Equals(AssemblySearchType.None))
{
return true;
}
if (embeddedPdb)
{
return _instrumentationHelper.EmbeddedPortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources);
}
else
{
return _instrumentationHelper.PortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources);
}
}
else
{
return false;
}
}
catch (Exception ex)
{
_logger.LogWarning($"Unable to instrument module: '{_module}'\n{ex}");
return false;
}
}
For ms code coverage there is an MSBuild targets file that writes a .msCoverageSourceRootsMapping_$(AssemblyName) file that can be used to exclude an assembly from coverage.
( Where AnnotatedProjects comes from https://github.com/dotnet/msbuild/blob/main/src/Tasks/Microsoft.Common.CurrentVersion.targets )
How this works is determined by the ExcludeAssemblliesWithoutSources setting in the runsettings ( or the similar setting in the new Testing Platform) - ExcludeAssembliesWithoutSources.
Note this setting has not been documented
If used it is a child element of the Configuration element
<?xml version="1.0" encoding="utf-8"?>
<!-- File name extension must be .runsettings -->
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage">
<Configuration>
<ExcludeAssembliesWithoutSource>....</IncludeTestAssembly>
...
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
Internally this setting is represented by an enumeration and defaults to AssemblySearchType.NotSpecified.
The values are
MissingAny
MissingAll
None
these are the same as for Coverlet.
The setting is used by the AssemblyMetadataProcessor and the sourceRootTranslator has the information written out by the build task ( provided by the SourceRootTranslatorFactory )
AssemblyMetadataProcessor
private bool IsIncludedBySources(AssemblyMetadata assemblyMetadata, IPdbReader pdbReader)
{
if (this._configuration.CodeCoverage.SymbolsRestrictOriginalPathAccess)
return true;
if (this._configuration.ExcludeAssembliesWithoutSources == AssemblySearchType.MissingAny)
{
foreach (string source in pdbReader.GetSources())
{
if (!this._sourceRootTranslator.Exists(source))
{
...
assemblyMetadata.IsCodeCoverageEnabled = false;
assemblyMetadata.ModuleSkipReason = ModuleSkipReason.no_symbols;
return false;
}
}
}
if (this._configuration.ExcludeAssembliesWithoutSources != AssemblySearchType.MissingAll || !pdbReader.GetSources().All<string>((Func<string, bool>) (source => !this._sourceRootTranslator.Exists(source))))
return true;
this._logger.LogInfo("AssemblyMetadataProcessor.IsIncludedBySources: skipping " + assemblyMetadata.Path + " as all sources do not exist");
assemblyMetadata.IsCodeCoverageEnabled = false;
assemblyMetadata.ModuleSkipReason = ModuleSkipReason.no_symbols;
return false;
}
SourceRootTranslatorFactory
private static readonly string[] Prefixes = new string[2]
{
".msCoverageSourceRootsMapping_",
".msCoverageExtensionSourceRootsMapping_"
};
private IReadOnlyDictionary<string, Dictionary<string, List<string>>> LoadSourceRootMapping(
IEnumerable<string> paths,
ILogger logger)
{
Dictionary<string, Dictionary<string, List<string>>> dictionary = new Dictionary<string, Dictionary<string, List<string>>>();
foreach (string path1 in paths)
{
string str1 = string.Empty;
DefaultInterpolatedStringHandler interpolatedStringHandler;
try
{
str1 = this._libraryHelper.GetAssemblyName(path1);
}
catch (Exception ex)
{....
}
if (!string.IsNullOrEmpty(str1))
{
foreach (string prefix in SourceRootTranslatorFactory.Prefixes)
{
string path2 = Path.Combine(Path.GetDirectoryName(path1), prefix + str1);
if (this._fileHelper.Exists(path2))
{
foreach (string readAllLine in this._fileHelper.ReadAllLines(path2))
{
int length = readAllLine.IndexOf('|');
int num = readAllLine.IndexOf('=');
if (length == -1 || num == -1)
{
logger.LogWarning("SourceRootTranslator.LoadSourceRootMapping Malformed mapping '" + readAllLine + "'");
}
else
{
string str2 = readAllLine.Substring(0, length);
string str3 = readAllLine.Substring(length + 1, num - length - 1);
string key1 = readAllLine.Substring(num + 1);
if (!dictionary.ContainsKey(key1))
dictionary.Add(key1, new Dictionary<string, List<string>>());
foreach (string key2 in str3.Split(';'))
{
if (!dictionary[key1].ContainsKey(key2))
dictionary[key1].Add(key2, new List<string>());
....
dictionary[key1][key2].Add(str2);
}
}
}
}
}
}
}
return (IReadOnlyDictionary<string, Dictionary<string, List<string>>>) dictionary;
}
SourceRootTranslator.Exists =>
private bool InternalResolveFilePath(string path)
{
foreach (KeyValuePair<string, Dictionary<string, List<string>>> keyValuePair in (IEnumerable<KeyValuePair<string, Dictionary<string, List<string>>>>) this._sourceRootMapping)
{
if (path.StartsWith(keyValuePair.Key))
{
string str = path.Substring(keyValuePair.Key.Length);
foreach (string key in keyValuePair.Value.Keys)
{
string fullPath = Path.GetFullPath(key + str);
if (this._inclusionChecker.IsIncluded(fullPath) || this._fileHelper.Exists(fullPath))
{
this._cachedMappings.TryAdd(path, (fullPath, keyValuePair.Key, key));
this._cachedPaths.TryAdd(fullPath, false);
...
return true;
}
}
}
}
return false;
}
and for good measure
internal class ConfigurationInclusionChecker : IInclusionChecker
{
private readonly DataCollectorConfiguration _configuration;
public ConfigurationInclusionChecker(DataCollectorConfiguration configuration) => this._configuration = configuration;
public bool IsIncluded(string path) => this._configuration.CodeCoverage.ModulePaths.IncludeAll || this._configuration.CodeCoverage.ModulePaths.IsIncluded(path);
}
So what should FCC do ?