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()
{