Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --source-mapping-file to coverlet.console #1568

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Documentation/GlobalTool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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 `<Deterministic>true</Deterministic>` 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.
Expand Down
13 changes: 9 additions & 4 deletions src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ static int Main(string[] args)
var useSourceLink = new Option<bool>("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.") { Arity = ArgumentArity.Zero };
var doesNotReturnAttributes = new Option<string[]>("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore };
var excludeAssembliesWithoutSources = new Option<string>("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne };
var sourceMappingFile = new Option<string>("--source-mapping-file", "Specifies the path to a SourceRootsMappings file.") { Arity = ArgumentArity.ZeroOrOne };

RootCommand rootCommand = new()
{
Expand All @@ -71,7 +72,8 @@ static int Main(string[] args)
mergeWith,
useSourceLink,
doesNotReturnAttributes,
excludeAssembliesWithoutSources
excludeAssembliesWithoutSources,
sourceMappingFile
};

rootCommand.Description = "Cross platform .NET Core code coverage tool";
Expand Down Expand Up @@ -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.");
Expand All @@ -123,7 +126,8 @@ static int Main(string[] args)
mergeWithValue,
useSourceLinkValue,
doesNotReturnAttributesValue,
excludeAssembliesWithoutSourcesValue);
excludeAssembliesWithoutSourcesValue,
sourceMappingFileValue);
context.ExitCode = taskStatus;

});
Expand All @@ -149,7 +153,8 @@ private static Task<int> HandleCommand(string moduleOrAppDirectory,
string mergeWith,
bool useSourceLink,
string[] doesNotReturnAttributes,
string excludeAssembliesWithoutSources
string excludeAssembliesWithoutSources,
string sourceMappingFile
)
{

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

ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
Expand Down
17 changes: 11 additions & 6 deletions src/coverlet.core/Helpers/SourceRootTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ internal class SourceRootTranslator : ISourceRootTranslator
private readonly IFileSystem _fileSystem;
private readonly Dictionary<string, List<SourceRootMapping>> _sourceRootMapping;
private readonly Dictionary<string, List<string>> _sourceToDeterministicPathMapping;
private readonly string _mappingFileName;
private Dictionary<string, string> _resolutionCacheFiles;

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

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));
Expand All @@ -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);
}

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

private Dictionary<string, List<SourceRootMapping>> LoadSourceRootMapping(string directory)
private Dictionary<string, List<SourceRootMapping>> LoadSourceRootMapping(string mappingFilePath)
{
var mapping = new Dictionary<string, List<SourceRootMapping>>();

string mappingFilePath = Path.Combine(directory, _mappingFileName);
if (!_fileSystem.Exists(mappingFilePath))
{
return mapping;
Expand Down
19 changes: 19 additions & 0 deletions test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILogger>();
var assemblyAdapter = new Mock<IAssemblyAdapter>();
assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny<string>())).Returns("testLib");
var fileSystem = new Mock<IFileSystem>();
fileSystem.Setup(f => f.Exists(It.IsAny<string>())).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<string>())).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()
{
Expand Down