Skip to content

Commit 66fee67

Browse files
committed
Add use-mapping-file to coverlet.console
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
1 parent 1981476 commit 66fee67

File tree

4 files changed

+52
-10
lines changed

4 files changed

+52
-10
lines changed

Documentation/GlobalTool.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Options:
3737
--use-source-link Specifies whether to use SourceLink URIs in place of file system paths.
3838
--does-not-return-attribute Attributes that mark methods that do not return.
3939
--exclude-assemblies-without-sources Specifies behaviour of heuristic to ignore assemblies with missing source documents.
40+
--use-mapping-file Specifies the path to a SourceRootsMappings file.
4041
--version Show version information
4142
-?, -h, --help Show help and usage information
4243
```
@@ -237,6 +238,18 @@ You can also include coverage of the test assembly itself by specifying the `--i
237238

238239
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.
239240

241+
## Path Mappings
242+
243+
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.
244+
245+
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 `<Deterministic>true</Deterministic>` which sets the sources to `/_/*` the following line must be in the mapping file.
246+
247+
```
248+
|C:\git\coverlet\=/_/
249+
```
250+
251+
During coverage collection, Coverlet will translate any path that starts with `/_/` to `C:\git\coverlet\` allowing the collector to find the source file.
252+
240253
## Exit Codes
241254

242255
Coverlet outputs specific exit codes to better support build automation systems for determining the kind of failure so the appropriate action can be taken.

src/coverlet.console/Program.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ static int Main(string[] args)
4848
var useSourceLink = new Option<bool>("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.") { Arity = ArgumentArity.Zero };
4949
var doesNotReturnAttributes = new Option<string[]>("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore };
5050
var excludeAssembliesWithoutSources = new Option<string>("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne };
51+
var sourceMappingFile = new Option<string>("--source-mapping-file", "Specifies the path to a SourceRootsMappings file.") { Arity = ArgumentArity.ZeroOrOne };
5152

5253
RootCommand rootCommand = new()
5354
{
@@ -71,7 +72,8 @@ static int Main(string[] args)
7172
mergeWith,
7273
useSourceLink,
7374
doesNotReturnAttributes,
74-
excludeAssembliesWithoutSources
75+
excludeAssembliesWithoutSources,
76+
sourceMappingFile
7577
};
7678

7779
rootCommand.Description = "Cross platform .NET Core code coverage tool";
@@ -99,6 +101,7 @@ static int Main(string[] args)
99101
bool useSourceLinkValue = context.ParseResult.GetValueForOption(useSourceLink);
100102
string[] doesNotReturnAttributesValue = context.ParseResult.GetValueForOption(doesNotReturnAttributes);
101103
string excludeAssembliesWithoutSourcesValue = context.ParseResult.GetValueForOption(excludeAssembliesWithoutSources);
104+
string sourceMappingFileValue = context.ParseResult.GetValueForOption(sourceMappingFile);
102105

103106
if (string.IsNullOrEmpty(moduleOrAppDirectoryValue) || string.IsNullOrWhiteSpace(moduleOrAppDirectoryValue))
104107
throw new ArgumentException("No test assembly or application directory specified.");
@@ -123,7 +126,8 @@ static int Main(string[] args)
123126
mergeWithValue,
124127
useSourceLinkValue,
125128
doesNotReturnAttributesValue,
126-
excludeAssembliesWithoutSourcesValue);
129+
excludeAssembliesWithoutSourcesValue,
130+
sourceMappingFileValue);
127131
context.ExitCode = taskStatus;
128132

129133
});
@@ -149,7 +153,8 @@ private static Task<int> HandleCommand(string moduleOrAppDirectory,
149153
string mergeWith,
150154
bool useSourceLink,
151155
string[] doesNotReturnAttributes,
152-
string excludeAssembliesWithoutSources
156+
string excludeAssembliesWithoutSources,
157+
string sourceMappingFile
153158
)
154159
{
155160

@@ -160,7 +165,7 @@ string excludeAssembliesWithoutSources
160165
serviceCollection.AddTransient<ILogger, ConsoleLogger>();
161166
// We need to keep singleton/static semantics
162167
serviceCollection.AddSingleton<IInstrumentationHelper, InstrumentationHelper>();
163-
serviceCollection.AddSingleton<ISourceRootTranslator, SourceRootTranslator>(provider => new SourceRootTranslator(provider.GetRequiredService<ILogger>(), provider.GetRequiredService<IFileSystem>()));
168+
serviceCollection.AddSingleton<ISourceRootTranslator, SourceRootTranslator>(provider => new SourceRootTranslator(sourceMappingFile, provider.GetRequiredService<ILogger>(), provider.GetRequiredService<IFileSystem>()));
164169
serviceCollection.AddSingleton<ICecilSymbolHelper, CecilSymbolHelper>();
165170

166171
ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

src/coverlet.core/Helpers/SourceRootTranslator.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ internal class SourceRootTranslator : ISourceRootTranslator
2222
private readonly IFileSystem _fileSystem;
2323
private readonly Dictionary<string, List<SourceRootMapping>> _sourceRootMapping;
2424
private readonly Dictionary<string, List<string>> _sourceToDeterministicPathMapping;
25-
private readonly string _mappingFileName;
2625
private Dictionary<string, string> _resolutionCacheFiles;
2726

2827
public SourceRootTranslator(ILogger logger, IFileSystem fileSystem)
@@ -32,6 +31,13 @@ public SourceRootTranslator(ILogger logger, IFileSystem fileSystem)
3231
_sourceRootMapping = new Dictionary<string, List<SourceRootMapping>>();
3332
}
3433

34+
public SourceRootTranslator(string sourceMappingFile, ILogger logger, IFileSystem fileSystem)
35+
{
36+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
37+
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
38+
_sourceRootMapping = LoadSourceRootMapping(sourceMappingFile);
39+
}
40+
3541
public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem fileSystem, IAssemblyAdapter assemblyAdapter)
3642
{
3743
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -46,11 +52,11 @@ public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem f
4652
}
4753

4854
string assemblyName = assemblyAdapter.GetAssemblyName(moduleTestPath);
49-
_mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}";
55+
string mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}";
5056

51-
_logger.LogInformation($"_mapping file name: '{_mappingFileName}'", true);
57+
_logger.LogInformation($"_mapping file name: '{mappingFileName}'", true);
5258

53-
_sourceRootMapping = LoadSourceRootMapping(Path.GetDirectoryName(moduleTestPath));
59+
_sourceRootMapping = LoadSourceRootMapping(Path.Combine(Path.GetDirectoryName(moduleTestPath), mappingFileName));
5460
_sourceToDeterministicPathMapping = LoadSourceToDeterministicPathMapping(_sourceRootMapping);
5561
}
5662

@@ -77,11 +83,10 @@ private static Dictionary<string, List<string>> LoadSourceToDeterministicPathMap
7783
return sourceToDeterministicPathMapping;
7884
}
7985

80-
private Dictionary<string, List<SourceRootMapping>> LoadSourceRootMapping(string directory)
86+
private Dictionary<string, List<SourceRootMapping>> LoadSourceRootMapping(string mappingFilePath)
8187
{
8288
var mapping = new Dictionary<string, List<SourceRootMapping>>();
8389

84-
string mappingFilePath = Path.Combine(directory, _mappingFileName);
8590
if (!_fileSystem.Exists(mappingFilePath))
8691
{
8792
return mapping;

test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ public void TranslatePathRoot_Success()
5151
Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath);
5252
}
5353

54+
[ConditionalFact]
55+
[SkipOnOS(OS.Linux, "Windows path format only")]
56+
[SkipOnOS(OS.MacOS, "Windows path format only")]
57+
public void TranslateWithDirectFile_Success()
58+
{
59+
var logger = new Mock<ILogger>();
60+
var assemblyAdapter = new Mock<IAssemblyAdapter>();
61+
assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny<string>())).Returns("testLib");
62+
var fileSystem = new Mock<IFileSystem>();
63+
fileSystem.Setup(f => f.Exists(It.IsAny<string>())).Returns((string p) =>
64+
{
65+
if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true;
66+
return false;
67+
});
68+
fileSystem.Setup(f => f.ReadAllLines(It.IsAny<string>())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest"));
69+
var translator = new SourceRootTranslator("CoverletSourceRootsMapping_testLib", logger.Object, fileSystem.Object);
70+
Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath);
71+
}
72+
5473
[Fact]
5574
public void Translate_EmptyFile()
5675
{

0 commit comments

Comments
 (0)