Skip to content

Commit b819588

Browse files
authored
Merge pull request #260 from tonerdo/sourcelink
Add support for Sourcelink
2 parents 4ead796 + 1b2d8ba commit b819588

File tree

11 files changed

+105
-66
lines changed

11 files changed

+105
-66
lines changed

src/coverlet.console/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ static int Main(string[] args)
3636
CommandOption includeDirectories = app.Option("--include-directory", "Include directories containing additional assemblies to be instrumented.", CommandOptionType.MultipleValue);
3737
CommandOption excludeAttributes = app.Option("--exclude-by-attribute", "Attributes to exclude from code coverage.", CommandOptionType.MultipleValue);
3838
CommandOption mergeWith = app.Option("--merge-with", "Path to existing coverage result to merge.", CommandOptionType.SingleValue);
39+
CommandOption useSourceLink = app.Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.", CommandOptionType.NoValue);
3940

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

48-
Coverage coverage = new Coverage(module.Value, includeFilters.Values.ToArray(), includeDirectories.Values.ToArray(), excludeFilters.Values.ToArray(), excludedSourceFiles.Values.ToArray(), excludeAttributes.Values.ToArray(), mergeWith.Value());
49+
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());
4950
coverage.PrepareModules();
5051

5152
Process process = new Process();

src/coverlet.core/Coverage.cs

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,38 @@
88
using Coverlet.Core.Symbols;
99

1010
using Newtonsoft.Json;
11+
using Newtonsoft.Json.Linq;
1112

1213
namespace Coverlet.Core
1314
{
1415
public class Coverage
1516
{
1617
private string _module;
1718
private string _identifier;
18-
private string[] _excludeFilters;
1919
private string[] _includeFilters;
2020
private string[] _includeDirectories;
21+
private string[] _excludeFilters;
2122
private string[] _excludedSourceFiles;
22-
private string _mergeWith;
2323
private string[] _excludeAttributes;
24+
private string _mergeWith;
25+
private bool _useSourceLink;
2426
private List<InstrumenterResult> _results;
2527

2628
public string Identifier
2729
{
2830
get { return _identifier; }
2931
}
3032

31-
public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith)
33+
public Coverage(string module, string[] includeFilters, string[] includeDirectories, string[] excludeFilters, string[] excludedSourceFiles, string[] excludeAttributes, string mergeWith, bool useSourceLink)
3234
{
3335
_module = module;
34-
_excludeFilters = excludeFilters;
3536
_includeFilters = includeFilters;
3637
_includeDirectories = includeDirectories ?? Array.Empty<string>();
38+
_excludeFilters = excludeFilters;
3739
_excludedSourceFiles = excludedSourceFiles;
38-
_mergeWith = mergeWith;
3940
_excludeAttributes = excludeAttributes;
41+
_mergeWith = mergeWith;
42+
_useSourceLink = useSourceLink;
4043

4144
_identifier = Guid.NewGuid().ToString();
4245
_results = new List<InstrumenterResult>();
@@ -186,6 +189,15 @@ private void CalculateCoverage()
186189
}
187190

188191
List<Document> documents = result.Documents.Values.ToList();
192+
if (_useSourceLink && result.SourceLink != null)
193+
{
194+
var jObject = JObject.Parse(result.SourceLink)["documents"];
195+
var sourceLinkDocuments = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
196+
foreach (var document in documents)
197+
{
198+
document.Path = GetSourceLinkUrl(sourceLinkDocuments, document.Path);
199+
}
200+
}
189201

190202
using (var fs = new FileStream(result.HitsFilePath, FileMode.Open))
191203
using (var br = new BinaryReader(fs))
@@ -199,9 +211,7 @@ private void CalculateCoverage()
199211
for (int i = 0; i < hitCandidatesCount; ++i)
200212
{
201213
var hitLocation = result.HitCandidates[i];
202-
203214
var document = documentsList[hitLocation.docIndex];
204-
205215
int hits = br.ReadInt32();
206216

207217
if (hitLocation.isBranch)
@@ -248,5 +258,46 @@ private void CalculateCoverage()
248258
InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
249259
}
250260
}
261+
262+
private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments, string document)
263+
{
264+
if (sourceLinkDocuments.TryGetValue(document, out string url))
265+
{
266+
return url;
267+
}
268+
269+
var keyWithBestMatch = string.Empty;
270+
var relativePathOfBestMatch = string.Empty;
271+
272+
foreach (var sourceLinkDocument in sourceLinkDocuments)
273+
{
274+
string key = sourceLinkDocument.Key;
275+
if (Path.GetFileName(key) != "*") continue;
276+
277+
string relativePath = Path.GetRelativePath(Path.GetDirectoryName(key), Path.GetDirectoryName(document));
278+
279+
if (relativePath.Contains("..")) continue;
280+
281+
if (relativePathOfBestMatch.Length == 0)
282+
{
283+
keyWithBestMatch = sourceLinkDocument.Key;
284+
relativePathOfBestMatch = relativePath;
285+
}
286+
287+
if (relativePath.Length < relativePathOfBestMatch.Length)
288+
{
289+
keyWithBestMatch = sourceLinkDocument.Key;
290+
relativePathOfBestMatch = relativePath;
291+
}
292+
}
293+
294+
relativePathOfBestMatch = relativePathOfBestMatch == "." ? string.Empty : relativePathOfBestMatch;
295+
296+
string replacement = Path.Combine(relativePathOfBestMatch, Path.GetFileName(document));
297+
replacement = replacement.Replace('\\', '/');
298+
299+
url = sourceLinkDocuments[keyWithBestMatch];
300+
return url.Replace("*", replacement);
301+
}
251302
}
252303
}

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ private void InstrumentModule()
7979
var types = module.GetTypes();
8080
AddCustomModuleTrackerToModule(module);
8181

82+
var sourceLinkDebugInfo = module.CustomDebugInformations.FirstOrDefault(c => c.Kind == CustomDebugInformationKind.SourceLink);
83+
if (sourceLinkDebugInfo != null)
84+
{
85+
_result.SourceLink = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
86+
}
87+
8288
foreach (TypeDefinition type in types)
8389
{
8490
var actualType = type.DeclaringType ?? type;

src/coverlet.core/Instrumentation/InstrumenterResult.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public InstrumenterResult()
4343
public string Module;
4444
public string HitsFilePath;
4545
public string ModulePath;
46+
public string SourceLink;
4647
public Dictionary<string, Document> Documents { get; private set; }
4748
public List<(bool isBranch, int docIndex, int start, int end)> HitCandidates { get; private set; }
4849
}

src/coverlet.core/Reporters/CoberturaReporter.cs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ public string Report(CoverageResult result)
3131
coverage.Add(new XAttribute("timestamp", ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString()));
3232

3333
XElement sources = new XElement("sources");
34-
var basePath = GetBasePath(result.Modules);
35-
sources.Add(new XElement("source", basePath));
3634

3735
XElement packages = new XElement("packages");
3836
foreach (var module in result.Modules)
@@ -50,7 +48,7 @@ public string Report(CoverageResult result)
5048
{
5149
XElement @class = new XElement("class");
5250
@class.Add(new XAttribute("name", cls.Key));
53-
@class.Add(new XAttribute("filename", GetRelativePathFromBase(basePath, document.Key)));
51+
@class.Add(new XAttribute("filename", document.Key));
5452
@class.Add(new XAttribute("line-rate", summary.CalculateLineCoverage(cls.Value).Percent.ToString()));
5553
@class.Add(new XAttribute("branch-rate", summary.CalculateBranchCoverage(cls.Value).Percent.ToString()));
5654
@class.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(cls.Value).ToString()));
@@ -131,34 +129,5 @@ public string Report(CoverageResult result)
131129

132130
return Encoding.UTF8.GetString(stream.ToArray());
133131
}
134-
135-
private string GetBasePath(Modules modules)
136-
{
137-
List<string> sources = new List<string>();
138-
string path = string.Empty;
139-
140-
foreach (var module in modules)
141-
{
142-
sources.AddRange(
143-
module.Value.Select(d => Path.GetDirectoryName(d.Key)));
144-
}
145-
146-
sources = sources.Distinct().ToList();
147-
var segments = sources[0].Split(Path.DirectorySeparatorChar);
148-
149-
foreach (var segment in segments)
150-
{
151-
var startsWith = sources.All(s => s.StartsWith(path + segment));
152-
if (!startsWith)
153-
break;
154-
155-
path += segment + Path.DirectorySeparatorChar;
156-
}
157-
158-
return path;
159-
}
160-
161-
private string GetRelativePathFromBase(string basePath, string path)
162-
=> basePath == string.Empty ? path : path.Replace(basePath, string.Empty);
163132
}
164133
}

src/coverlet.core/coverlet.core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netstandard2.0</TargetFramework>
4+
<OutputType>Library</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
56
<AssemblyVersion>4.0.0</AssemblyVersion>
67
</PropertyGroup>
78

89
<ItemGroup>
910
<PackageReference Include="Mono.Cecil" Version="0.10.1" />
1011
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
11-
<PackageReference Include="System.Reflection.Metadata" Version="1.5.0" />
1212
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="2.0.1" />
1313
</ItemGroup>
1414

src/coverlet.msbuild.tasks/InstrumentationTask.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ public class InstrumentationTask : Task
99
{
1010
private static Coverage _coverage;
1111
private string _path;
12-
private string _exclude;
1312
private string _include;
1413
private string _includeDirectory;
14+
private string _exclude;
1515
private string _excludeByFile;
16-
private string _mergeWith;
1716
private string _excludeByAttribute;
17+
private string _mergeWith;
18+
private bool _useSourceLink;
1819

1920
internal static Coverage Coverage
2021
{
@@ -28,12 +29,6 @@ public string Path
2829
set { _path = value; }
2930
}
3031

31-
public string Exclude
32-
{
33-
get { return _exclude; }
34-
set { _exclude = value; }
35-
}
36-
3732
public string Include
3833
{
3934
get { return _include; }
@@ -46,35 +41,47 @@ public string IncludeDirectory
4641
set { _includeDirectory = value; }
4742
}
4843

44+
public string Exclude
45+
{
46+
get { return _exclude; }
47+
set { _exclude = value; }
48+
}
49+
4950
public string ExcludeByFile
5051
{
5152
get { return _excludeByFile; }
5253
set { _excludeByFile = value; }
5354
}
5455

56+
public string ExcludeByAttribute
57+
{
58+
get { return _excludeByAttribute; }
59+
set { _excludeByAttribute = value; }
60+
}
61+
5562
public string MergeWith
5663
{
5764
get { return _mergeWith; }
5865
set { _mergeWith = value; }
5966
}
6067

61-
public string ExcludeByAttribute
68+
public bool UseSourceLink
6269
{
63-
get { return _excludeByAttribute; }
64-
set { _excludeByAttribute = value; }
70+
get { return _useSourceLink; }
71+
set { _useSourceLink = value; }
6572
}
6673

6774
public override bool Execute()
6875
{
6976
try
7077
{
71-
var excludedSourceFiles = _excludeByFile?.Split(',');
72-
var excludeFilters = _exclude?.Split(',');
7378
var includeFilters = _include?.Split(',');
7479
var includeDirectories = _includeDirectory?.Split(',');
80+
var excludeFilters = _exclude?.Split(',');
81+
var excludedSourceFiles = _excludeByFile?.Split(',');
7582
var excludeAttributes = _excludeByAttribute?.Split(',');
7683

77-
_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith);
84+
_coverage = new Coverage(_path, includeFilters, includeDirectories, excludeFilters, excludedSourceFiles, excludeAttributes, _mergeWith, _useSourceLink);
7885
_coverage.PrepareModules();
7986
}
8087
catch (Exception ex)

src/coverlet.msbuild.tasks/coverlet.msbuild.tasks.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netstandard2.0</TargetFramework>
4+
<OutputType>Library</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
56
<AssemblyVersion>2.3.0</AssemblyVersion>
67
</PropertyGroup>
78

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22
<PropertyGroup>
33
<CollectCoverage Condition="$(CollectCoverage) == ''">false</CollectCoverage>
4-
<CoverletOutputFormat Condition="$(CoverletOutputFormat) == ''">json</CoverletOutputFormat>
5-
<CoverletOutput Condition="$(CoverletOutput) == ''">$([MSBuild]::EnsureTrailingSlash('$(MSBuildProjectDirectory)'))</CoverletOutput>
64
<Include Condition="$(Include) == ''"></Include>
5+
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
76
<Exclude Condition="$(Exclude) == ''"></Exclude>
87
<ExcludeByFile Condition="$(ExcludeByFile) == ''"></ExcludeByFile>
8+
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
99
<MergeWith Condition="$(MergeWith) == ''"></MergeWith>
10+
<UseSourceLink Condition="$(UseSourceLink) == ''">false</UseSourceLink>
11+
<CoverletOutputFormat Condition="$(CoverletOutputFormat) == ''">json</CoverletOutputFormat>
12+
<CoverletOutput Condition="$(CoverletOutput) == ''">$([MSBuild]::EnsureTrailingSlash('$(MSBuildProjectDirectory)'))</CoverletOutput>
1013
<Threshold Condition="$(Threshold) == ''">0</Threshold>
1114
<ThresholdType Condition="$(ThresholdType) == ''">line,branch,method</ThresholdType>
12-
<IncludeDirectory Condition="$(IncludeDirectory) == ''"></IncludeDirectory>
13-
<ExcludeByAttribute Condition="$(ExcludeByAttribute) == ''"></ExcludeByAttribute>
1415
</PropertyGroup>
1516
</Project>

src/coverlet.msbuild/coverlet.msbuild.targets

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,27 @@
66
<Target Name="InstrumentModulesNoBuild" BeforeTargets="VSTest">
77
<Coverlet.MSbuild.Tasks.InstrumentationTask
88
Condition="'$(VSTestNoBuild)' == 'true' and $(CollectCoverage) == 'true'"
9+
Path="$(TargetPath)"
910
Include="$(Include)"
1011
IncludeDirectory="$(IncludeDirectory)"
1112
Exclude="$(Exclude)"
1213
ExcludeByFile="$(ExcludeByFile)"
13-
MergeWith="$(MergeWith)"
1414
ExcludeByAttribute="$(ExcludeByAttribute)"
15-
Path="$(TargetPath)" />
15+
MergeWith="$(MergeWith)"
16+
UseSourceLink="$(UseSourceLink)" />
1617
</Target>
1718

1819
<Target Name="InstrumentModulesAfterBuild" AfterTargets="BuildProject">
1920
<Coverlet.MSbuild.Tasks.InstrumentationTask
2021
Condition="'$(VSTestNoBuild)' != 'true' and $(CollectCoverage) == 'true'"
22+
Path="$(TargetPath)"
2123
Include="$(Include)"
2224
IncludeDirectory="$(IncludeDirectory)"
2325
Exclude="$(Exclude)"
2426
ExcludeByFile="$(ExcludeByFile)"
25-
MergeWith="$(MergeWith)"
2627
ExcludeByAttribute="$(ExcludeByAttribute)"
27-
Path="$(TargetPath)" />
28+
MergeWith="$(MergeWith)"
29+
UseSourceLink="$(UseSourceLink)" />
2830
</Target>
2931

3032
<Target Name="GenerateCoverageResult" AfterTargets="VSTest">

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void TestCoverage()
2727
// Since Coverage only instruments dependancies, we need a fake module here
2828
var testModule = Path.Combine(directory.FullName, "test.module.dll");
2929

30-
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty);
30+
var coverage = new Coverage(testModule, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), string.Empty, false);
3131
coverage.PrepareModules();
3232

3333
var result = coverage.GetCoverageResult();

0 commit comments

Comments
 (0)