From a0bd87d70f899d955891956ce4173e8bf31c16b0 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 16 Jan 2024 19:41:22 +0000 Subject: [PATCH] Add use-mapping-file to coverlet.console (#1568) Adds the option --use-mapping-file to coverlet.console that allows the caller to specify a custom source mapping file to use. This is used to then maps paths located in an assembly's debug symbols to local path when collecting coverage. 8 --- Documentation/GlobalTool.md | 13 +++++++++++++ src/coverlet.console/Program.cs | 13 +++++++++---- .../Helpers/SourceRootTranslator.cs | 17 +++++++++++------ .../Helpers/SourceRootTranslatorTests.cs | 19 +++++++++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Documentation/GlobalTool.md b/Documentation/GlobalTool.md index b87964a2d..4dc47e652 100644 --- a/Documentation/GlobalTool.md +++ b/Documentation/GlobalTool.md @@ -37,6 +37,7 @@ Options: --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. + --use-mapping-file Specifies the path to a SourceRootsMappings file. --version Show version information -?, -h, --help Show help and usage information ``` @@ -237,6 +238,18 @@ You can also include coverage of the test assembly itself by specifying the `--i Coverlet supports [SourceLink](https://github.com/dotnet/sourcelink) custom debug information contained in PDBs. When you specify the `--use-source-link` flag, Coverlet will generate results that contain the URL to the source files in your source control instead of local file paths. +## Path Mappings + +Coverlet has the ability to map the paths contained inside the debug sources into a local path where the source is currently located using the option `--source-mapping-file`. This is useful if the source was built using a deterministic build which sets the path to `/_/` or if it was built on a different host where the source is located in a different path. + +The value for `--source-mapping-file` should be a file with each line being in the format `|path to map to=path in debug symbol`. For example to map the local checkout of a project `C:\git\coverlet` to project that was built with `true` which sets the sources to `/_/*` the following line must be in the mapping file. + +``` +|C:\git\coverlet\=/_/ +``` + +During coverage collection, Coverlet will translate any path that starts with `/_/` to `C:\git\coverlet\` allowing the collector to find the source file. + ## Exit Codes Coverlet outputs specific exit codes to better support build automation systems for determining the kind of failure so the appropriate action can be taken. diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 1a4bde021..7bf4dad77 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -48,6 +48,7 @@ static int Main(string[] args) var useSourceLink = new Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.") { Arity = ArgumentArity.Zero }; var doesNotReturnAttributes = new Option("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore }; var excludeAssembliesWithoutSources = new Option("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne }; + var sourceMappingFile = new Option("--source-mapping-file", "Specifies the path to a SourceRootsMappings file.") { Arity = ArgumentArity.ZeroOrOne }; RootCommand rootCommand = new() { @@ -71,7 +72,8 @@ static int Main(string[] args) mergeWith, useSourceLink, doesNotReturnAttributes, - excludeAssembliesWithoutSources + excludeAssembliesWithoutSources, + sourceMappingFile }; rootCommand.Description = "Cross platform .NET Core code coverage tool"; @@ -99,6 +101,7 @@ static int Main(string[] args) bool useSourceLinkValue = context.ParseResult.GetValueForOption(useSourceLink); string[] doesNotReturnAttributesValue = context.ParseResult.GetValueForOption(doesNotReturnAttributes); string excludeAssembliesWithoutSourcesValue = context.ParseResult.GetValueForOption(excludeAssembliesWithoutSources); + string sourceMappingFileValue = context.ParseResult.GetValueForOption(sourceMappingFile); if (string.IsNullOrEmpty(moduleOrAppDirectoryValue) || string.IsNullOrWhiteSpace(moduleOrAppDirectoryValue)) throw new ArgumentException("No test assembly or application directory specified."); @@ -123,7 +126,8 @@ static int Main(string[] args) mergeWithValue, useSourceLinkValue, doesNotReturnAttributesValue, - excludeAssembliesWithoutSourcesValue); + excludeAssembliesWithoutSourcesValue, + sourceMappingFileValue); context.ExitCode = taskStatus; }); @@ -149,7 +153,8 @@ private static Task HandleCommand(string moduleOrAppDirectory, string mergeWith, bool useSourceLink, string[] doesNotReturnAttributes, - string excludeAssembliesWithoutSources + string excludeAssembliesWithoutSources, + string sourceMappingFile ) { @@ -160,7 +165,7 @@ string excludeAssembliesWithoutSources serviceCollection.AddTransient(); // We need to keep singleton/static semantics serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(provider => new SourceRootTranslator(provider.GetRequiredService(), provider.GetRequiredService())); + serviceCollection.AddSingleton(provider => new SourceRootTranslator(sourceMappingFile, provider.GetRequiredService(), provider.GetRequiredService())); serviceCollection.AddSingleton(); ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/src/coverlet.core/Helpers/SourceRootTranslator.cs b/src/coverlet.core/Helpers/SourceRootTranslator.cs index 7c2cfeb36..d5a0dfcc7 100644 --- a/src/coverlet.core/Helpers/SourceRootTranslator.cs +++ b/src/coverlet.core/Helpers/SourceRootTranslator.cs @@ -22,7 +22,6 @@ internal class SourceRootTranslator : ISourceRootTranslator private readonly IFileSystem _fileSystem; private readonly Dictionary> _sourceRootMapping; private readonly Dictionary> _sourceToDeterministicPathMapping; - private readonly string _mappingFileName; private Dictionary _resolutionCacheFiles; public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) @@ -32,6 +31,13 @@ public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) _sourceRootMapping = new Dictionary>(); } + public SourceRootTranslator(string sourceMappingFile, ILogger logger, IFileSystem fileSystem) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _sourceRootMapping = LoadSourceRootMapping(sourceMappingFile); + } + public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem fileSystem, IAssemblyAdapter assemblyAdapter) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -46,11 +52,11 @@ public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem f } string assemblyName = assemblyAdapter.GetAssemblyName(moduleTestPath); - _mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}"; + string mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}"; - _logger.LogInformation($"_mapping file name: '{_mappingFileName}'", true); + _logger.LogInformation($"_mapping file name: '{mappingFileName}'", true); - _sourceRootMapping = LoadSourceRootMapping(Path.GetDirectoryName(moduleTestPath)); + _sourceRootMapping = LoadSourceRootMapping(Path.Combine(Path.GetDirectoryName(moduleTestPath), mappingFileName)); _sourceToDeterministicPathMapping = LoadSourceToDeterministicPathMapping(_sourceRootMapping); } @@ -77,11 +83,10 @@ private static Dictionary> LoadSourceToDeterministicPathMap return sourceToDeterministicPathMapping; } - private Dictionary> LoadSourceRootMapping(string directory) + private Dictionary> LoadSourceRootMapping(string mappingFilePath) { var mapping = new Dictionary>(); - string mappingFilePath = Path.Combine(directory, _mappingFileName); if (!_fileSystem.Exists(mappingFilePath)) { return mapping; diff --git a/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs index c09a988f5..c860678c8 100644 --- a/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs +++ b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs @@ -51,6 +51,25 @@ public void TranslatePathRoot_Success() Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath); } + [ConditionalFact] + [SkipOnOS(OS.Linux, "Windows path format only")] + [SkipOnOS(OS.MacOS, "Windows path format only")] + public void TranslateWithDirectFile_Success() + { + var logger = new Mock(); + var assemblyAdapter = new Mock(); + assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); + var translator = new SourceRootTranslator("CoverletSourceRootsMapping_testLib", logger.Object, fileSystem.Object); + Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath); + } + [Fact] public void Translate_EmptyFile() {