Skip to content

Add support for Sourcelink #260

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

Merged
merged 5 commits into from
Dec 9, 2018
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
3 changes: 2 additions & 1 deletion src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ static int Main(string[] args)
CommandOption includeDirectories = app.Option("--include-directory", "Include directories containing additional assemblies to be instrumented.", CommandOptionType.MultipleValue);
CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue);
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue);

app.OnExecute(() =>
{
Expand All @@ -45,7 +46,7 @@ static int Main(string[] args)
if (!target.HasValue())
throw new CommandParsingException(app, "Target must be specified.");

Coverage coverage = new Coverage(module.Value, includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), excludeAttributes.Values.ToArray(), mergeWith.Value());
Coverage coverage = new Coverage(module.Value, includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), excludeAttributes.Values.ToArray(), mergeWith.Value(), useSourceLink.HasValue());
coverage.PrepareModules();

Process process = new Process();
Expand Down
65 changes: 58 additions & 7 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,38 @@
using Coverlet.Core.Symbols;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Coverlet.Core
{
public class Coverage
{
private string _module;
private string _identifier;
private string[] _excludeFilters;
private string[] _includeFilters;
private string[] _includeDirectories;
private string[] _excludeFilters;
private string[] _excludedSourceFiles;
private string _mergeWith;
private string[] _excludeAttributes;
private string _mergeWith;
private bool _useSourceLink;
private List<InstrumenterResult> _results;

public string Identifier
{
get { return _identifier; }
}

public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith)
public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith, bool useSourceLink)
{
_module = module;
_excludeFilters = excludeFilters;
_includeFilters = includeFilters;
_includeDirectories = includeDirectories ?? Array.Empty<string>();
_excludeFilters = excludeFilters;
_excludedSourceFiles = excludedSourceFiles;
_mergeWith = mergeWith;
_excludeAttributes = excludeAttributes;
_mergeWith = mergeWith;
_useSourceLink = useSourceLink;

_identifier = Guid.NewGuid().ToString();
_results = new List<InstrumenterResult>();
Expand Down Expand Up @@ -186,6 +189,15 @@ private void CalculateCoverage()
}

List<Document> documents = result.Documents.Values.ToList();
if (_useSourceLink && result.SourceLink != null)
{
var jObject = JObject.Parse(result.SourceLink)["documents"];
var sourceLinkDocuments = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
foreach (var document in documents)
{
document.Path = GetSourceLinkUrl(sourceLinkDocuments, document.Path);
}
}

using (var fs = new FileStream(result.HitsFilePath, FileMode.Open))
using (var br = new BinaryReader(fs))
Expand All @@ -199,9 +211,7 @@ private void CalculateCoverage()
for (int i = 0; i < hitCandidatesCount; ++i)
{
var hitLocation = result.HitCandidates[i];

var document = documentsList[hitLocation.docIndex];

int hits = br.ReadInt32();

if (hitLocation.isBranch)
Expand Down Expand Up @@ -248,5 +258,46 @@ private void CalculateCoverage()
InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
}
}

private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments, string document)
{
if (sourceLinkDocuments.TryGetValue(document, out string url))
{
return url;
}

var keyWithBestMatch = string.Empty;
var relativePathOfBestMatch = string.Empty;

foreach (var sourceLinkDocument in sourceLinkDocuments)
{
string key = sourceLinkDocument.Key;
if (Path.GetFileName(key) != "*") continue;

string relativePath = Path.GetRelativePath(Path.GetDirectoryName(key), Path.GetDirectoryName(document));

if (relativePath.Contains("..")) continue;

if (relativePathOfBestMatch.Length == 0)
{
keyWithBestMatch = sourceLinkDocument.Key;
relativePathOfBestMatch = relativePath;
}

if (relativePath.Length < relativePathOfBestMatch.Length)
{
keyWithBestMatch = sourceLinkDocument.Key;
relativePathOfBestMatch = relativePath;
}
}

relativePathOfBestMatch = relativePathOfBestMatch == "." ? string.Empty : relativePathOfBestMatch;

string replacement = Path.Combine(relativePathOfBestMatch, Path.GetFileName(document));
replacement = replacement.Replace('\\', '/');

url = sourceLinkDocuments[keyWithBestMatch];
return url.Replace("*", replacement);
}
}
}
6 changes: 6 additions & 0 deletions src/coverlet.core/Instrumentation/Instrumenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ private void InstrumentModule()
var types = module.GetTypes();
AddCustomModuleTrackerToModule(module);

var sourceLinkDebugInfo = module.CustomDebugInformations.FirstOrDefault(c => c.Kind == CustomDebugInformationKind.SourceLink);
if (sourceLinkDebugInfo != null)
{
_result.SourceLink = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
}

foreach (TypeDefinition type in types)
{
var actualType = type.DeclaringType ?? type;
Expand Down
1 change: 1 addition & 0 deletions src/coverlet.core/Instrumentation/InstrumenterResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public InstrumenterResult()
public string Module;
public string HitsFilePath;
public string ModulePath;
public string SourceLink;
public Dictionary<string, Document> Documents { get; private set; }
public List<(bool isBranch, int docIndex, int start, int end)> HitCandidates { get; private set; }
}
Expand Down
33 changes: 1 addition & 32 deletions src/coverlet.core/Reporters/CoberturaReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ public string Report(CoverageResult result)
coverage.Add(new XAttribute("timestamp", ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString()));

XElement sources = new XElement("sources");
var basePath = GetBasePath(result.Modules);
sources.Add(new XElement("source", basePath));

XElement packages = new XElement("packages");
foreach (var module in result.Modules)
Expand All @@ -50,7 +48,7 @@ public string Report(CoverageResult result)
{
XElement @class = new XElement("class");
@class.Add(new XAttribute("name", cls.Key));
@class.Add(new XAttribute("filename", GetRelativePathFromBase(basePath, document.Key)));
@class.Add(new XAttribute("filename", document.Key));
@class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).Percent.ToString()));
@class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).Percent.ToString()));
@class.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(cls.Value).ToString()));
Expand Down Expand Up @@ -131,34 +129,5 @@ public string Report(CoverageResult result)

return Encoding.UTF8.GetString(stream.ToArray());
}

private string GetBasePath(Modules modules)
{
List<string> sources = new List<string>();
string path = string.Empty;

foreach (var module in modules)
{
sources.AddRange(
module.Value.Select(d => Path.GetDirectoryName(d.Key)));
}

sources = sources.Distinct().ToList();
var segments = sources[0].Split(Path.DirectorySeparatorChar);

foreach (var segment in segments)
{
var startsWith = sources.All(s => s.StartsWith(path + segment));
if (!startsWith)
break;

path += segment + Path.DirectorySeparatorChar;
}

return path;
}

private string GetRelativePathFromBase(string basePath, string path)
=> basePath == string.Empty ? path : path.Replace(basePath, string.Empty);
}
}
4 changes: 2 additions & 2 deletions src/coverlet.core/coverlet.core.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyVersion>4.0.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.10.1" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Reflection.Metadata" Version="1.5.0" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="2.0.1" />
</ItemGroup>

Expand Down
35 changes: 21 additions & 14 deletions src/coverlet.msbuild.tasks/InstrumentationTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ public class InstrumentationTask : Task
{
private static Coverage _coverage;
private string _path;
private string _exclude;
private string _include;
private string _includeDirectory;
private string _exclude;
private string _excludeByFile;
private string _mergeWith;
private string _excludeByAttribute;
private string _mergeWith;
private bool _useSourceLink;

internal static Coverage Coverage
{
Expand All @@ -28,12 +29,6 @@ public string Path
set { _path = value; }
}

public string Exclude
{
get { return _exclude; }
set { _exclude = value; }
}

public string Include
{
get { return _include; }
Expand All @@ -46,35 +41,47 @@ public string IncludeDirectory
set { _includeDirectory = value; }
}

public string Exclude
{
get { return _exclude; }
set { _exclude = value; }
}

public string ExcludeByFile
{
get { return _excludeByFile; }
set { _excludeByFile = value; }
}

public string ExcludeByAttribute
{
get { return _excludeByAttribute; }
set { _excludeByAttribute = value; }
}

public string MergeWith
{
get { return _mergeWith; }
set { _mergeWith = value; }
}

public string ExcludeByAttribute
public bool UseSourceLink
{
get { return _excludeByAttribute; }
set { _excludeByAttribute = value; }
get { return _useSourceLink; }
set { _useSourceLink = value; }
}

public override bool Execute()
{
try
{
var excludedSourceFiles = _excludeByFile?.Split(',');
var excludeFilters = _exclude?.Split(',');
var includeFilters = _include?.Split(',');
var includeDirectories = _includeDirectory?.Split(',');
var excludeFilters = _exclude?.Split(',');
var excludedSourceFiles = _excludeByFile?.Split(',');
var excludeAttributes = _excludeByAttribute?.Split(',');

_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith);
_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith, _useSourceLink);
_coverage.PrepareModules();
}
catch (Exception ex)
Expand Down
3 changes: 2 additions & 1 deletion src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyVersion>2.3.0</AssemblyVersion>
</PropertyGroup>

Expand Down
9 changes: 5 additions & 4 deletions src/coverlet.msbuild/coverlet.msbuild.props
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CollectCoverage Condition="$(CollectCoverage) == ''">false</CollectCoverage>
<CoverletOutputFormat Condition="$(CoverletOutputFormat) == ''">json</CoverletOutputFormat>
<CoverletOutput Condition="$(CoverletOutput) == ''">$([MSBuild]::EnsureTrailingSlash('$(MSBuildProjectDirectory)'))</CoverletOutput>
<Include Condition="$(Include) == ''"></Include>
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
<Exclude Condition="$(Exclude) == ''"></Exclude>
<ExcludeByFile Condition="$(ExcludeByFile) == ''"></ExcludeByFile>
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
<UseSourceLink Condition="$(UseSourceLink) == ''">false</UseSourceLink>
<CoverletOutputFormat Condition="$(CoverletOutputFormat) == ''">json</CoverletOutputFormat>
<CoverletOutput Condition="$(CoverletOutput) == ''">$([MSBuild]::EnsureTrailingSlash('$(MSBuildProjectDirectory)'))</CoverletOutput>
<Threshold Condition="$(Threshold) == ''">0</Threshold>
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
</PropertyGroup>
</Project>
10 changes: 6 additions & 4 deletions src/coverlet.msbuild/coverlet.msbuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@
<Target Name="InstrumentModulesNoBuild" BeforeTargets="VSTest">
<Coverlet.MSbuild.Tasks.InstrumentationTask
Condition="'$(VSTestNoBuild)' == 'true' and $(CollectCoverage) == 'true'"
Path="$(TargetPath)"
Include="$(Include)"
IncludeDirectory="$(IncludeDirectory)"
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
ExcludeByAttribute="$(ExcludeByAttribute)"
Path="$(TargetPath)" />
MergeWith="$(MergeWith)"
UseSourceLink="$(UseSourceLink)" />
</Target>

<Target Name="InstrumentModulesAfterBuild" AfterTargets="BuildProject">
<Coverlet.MSbuild.Tasks.InstrumentationTask
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
Path="$(TargetPath)"
Include="$(Include)"
IncludeDirectory="$(IncludeDirectory)"
Exclude="$(Exclude)"
ExcludeByFile="$(ExcludeByFile)"
MergeWith="$(MergeWith)"
ExcludeByAttribute="$(ExcludeByAttribute)"
Path="$(TargetPath)" />
MergeWith="$(MergeWith)"
UseSourceLink="$(UseSourceLink)" />
</Target>

<Target Name="GenerateCoverageResult" AfterTargets="VSTest">
Expand Down
2 changes: 1 addition & 1 deletion test/coverlet.core.tests/CoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestCoverage()
// Since Coverage only instruments dependancies, we need a fake module here
var testModule = Path.Combine(directory.FullName, "test.module.dll");

var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty);
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, false);
coverage.PrepareModules();

var result = coverage.GetCoverageResult();
Expand Down