Skip to content

Improve cobertura absolute/relative path report generation #661

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
60 changes: 51 additions & 9 deletions src/coverlet.core/Reporters/CoberturaReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public string Report(CoverageResult result)
coverage.Add(new XAttribute("timestamp", (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds));

XElement sources = new XElement("sources");
var rootDirs = GetRootDirs(result.Modules, result.UseSourceLink).ToList();
rootDirs.ForEach(x => sources.Add(new XElement("source", x)));
var absolutePaths = GetBasePaths(result.Modules, result.UseSourceLink).ToList();
absolutePaths.ForEach(x => sources.Add(new XElement("source", x)));

XElement packages = new XElement("packages");
foreach (var module in result.Modules)
Expand All @@ -51,7 +51,7 @@ public string Report(CoverageResult result)
{
XElement @class = new XElement("class");
@class.Add(new XAttribute("name", cls.Key));
@class.Add(new XAttribute("filename", GetRelativePathFromBase(rootDirs, document.Key, result.UseSourceLink)));
@class.Add(new XAttribute("filename", GetRelativePathFromBase(absolutePaths, document.Key, result.UseSourceLink)));
@class.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture)));
@class.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture)));
@class.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(cls.Value)));
Expand Down Expand Up @@ -133,28 +133,70 @@ public string Report(CoverageResult result)
return Encoding.UTF8.GetString(stream.ToArray());
}

private static IEnumerable<string> GetRootDirs(Modules modules, bool useSourceLink)
private static IEnumerable<string> GetBasePaths(Modules modules, bool useSourceLink)
{
/*
Workflow

Path1 c:\dir1\dir2\file1.cs
Path2 c:\dir1\file2.cs
Path3 e:\dir1\file2.cs

1) Search for root dir
c:\ -> c:\dir1\dir2\file1.cs
c:\dir1\file2.cs
e:\ -> e:\dir1\file2.cs

2) Split path on directory separator i.e. for record c:\ ordered ascending by fragment elements
Path1 = [c:|dir1|file2.cs]
Path2 = [c:|dir1|dir2|file1.cs]

3) Find longest shared path comparing indexes
Path1[0] = Path2[0], ..., PathY[0] -> add to final fragment list
Path1[n] = Path2[n], ..., PathY[n] -> add to final fragment list
Path1[n+1] != Path2[n+1], ..., PathY[n+1] -> break, Path1[n] was last shared fragment

4) Concat created fragment list
*/
if (useSourceLink)
{
return new[] { string.Empty };
}

return modules.Values.SelectMany(k => k.Keys).Select(Directory.GetDirectoryRoot).Distinct();
return modules.Values.SelectMany(k => k.Keys).GroupBy(Directory.GetDirectoryRoot).Select(group =>
{
var splittedPaths = group.Select(absolutePath => absolutePath.Split(Path.DirectorySeparatorChar))
.OrderBy(absolutePath => absolutePath.Length).ToList();
if (splittedPaths.Count == 1)
{
return group.Key;
}

var basePathFragments = new List<string>();

splittedPaths[0].Select((value, index) => (value, index)).ToList().ForEach(fragmentIndexPair =>
{
if (splittedPaths.All(sp => fragmentIndexPair.value.Equals(sp[fragmentIndexPair.index])))
{
basePathFragments.Add(fragmentIndexPair.value);
}
});
return string.Concat(string.Join(Path.DirectorySeparatorChar.ToString(), basePathFragments), Path.DirectorySeparatorChar);
});
}

private static string GetRelativePathFromBase(IEnumerable<string> rootPaths, string path, bool useSourceLink)
private static string GetRelativePathFromBase(IEnumerable<string> basePaths, string path, bool useSourceLink)
{
if (useSourceLink)
{
return path;
}

foreach (var root in rootPaths)
foreach (var basePath in basePaths)
{
if (path.StartsWith(root))
if (path.StartsWith(basePath))
{
return path.Substring(root.Length);
return path.Substring(basePath.Length);
}
}

Expand Down
53 changes: 39 additions & 14 deletions test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,52 +137,77 @@ public void TestEnsureParseMethodStringCorrectly(
}

[Fact]
public void TestReportWithTwoDifferentDirectories()
public void TestReportWithDifferentDirectories()
{
CoverageResult result = new CoverageResult();
result.Identifier = Guid.NewGuid().ToString();

var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

string absolutePath1;
string absolutePath2;
string absolutePath3;
string absolutePath4;
string absolutePath5;
string absolutePath6;
string absolutePath7;

if (isWindows)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
absolutePath1 = @"C:\projA\file.cs";
absolutePath2 = @"E:\projB\file.cs";
absolutePath1 = @"C:\projA\dir1\dir10\file1.cs";
absolutePath2 = @"C:\projA\dir1\dir10\file2.cs";
absolutePath3 = @"C:\projA\dir1\file3.cs";
absolutePath4 = @"E:\projB\dir1\dir10\file4.cs";
absolutePath5 = @"E:\projB\dir2\file5.cs";
absolutePath6 = @"F:\file6.cs";
absolutePath7 = @"F:\";
}
else
{
absolutePath1 = @"/projA/file.cs";
absolutePath2 = @"/projB/file.cs";
absolutePath1 = @"/projA/dir1/dir10/file1.cs";
absolutePath2 = @"/projA/dir1/file2.cs";
absolutePath3 = @"/projA/dir1/file3.cs";
absolutePath4 = @"/projA/dir2/file4.cs";
absolutePath5 = @"/projA/dir2/file5.cs";
absolutePath6 = @"/file1.cs";
absolutePath7 = @"/";
}

var classes = new Classes {{"Class", new Methods()}};
var documents = new Documents {{absolutePath1, classes}, {absolutePath2, classes}};
var classes = new Classes { { "Class", new Methods() } };
var documents = new Documents { { absolutePath1, classes },
{ absolutePath2, classes },
{ absolutePath3, classes },
{ absolutePath4, classes },
{ absolutePath5, classes },
{ absolutePath6, classes },
{ absolutePath7, classes }
};

result.Modules = new Modules {{"Module", documents}};
result.Modules = new Modules { { "Module", documents } };

CoberturaReporter reporter = new CoberturaReporter();
string report = reporter.Report(result);

var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report)));

List<string> rootPaths = doc.Element("coverage").Element("sources").Elements().Select(e => e.Value).ToList();
List<string> basePaths = doc.Element("coverage").Element("sources").Elements().Select(e => e.Value).ToList();
List<string> relativePaths = doc.Element("coverage").Element("packages").Element("package")
.Element("classes").Elements().Select(e => e.Attribute("filename").Value).ToList();

List<string> possiblePaths = new List<string>();
foreach (string root in rootPaths)
foreach (string basePath in basePaths)
{
foreach (string relativePath in relativePaths)
{
possiblePaths.Add(Path.Combine(root, relativePath));
possiblePaths.Add(Path.Combine(basePath, relativePath));
}
}

Assert.Contains(absolutePath1, possiblePaths);
Assert.Contains(absolutePath2, possiblePaths);
Assert.Contains(absolutePath3, possiblePaths);
Assert.Contains(absolutePath4, possiblePaths);
Assert.Contains(absolutePath5, possiblePaths);
Assert.Contains(absolutePath6, possiblePaths);
Assert.Contains(absolutePath7, possiblePaths);
}

[Fact]
Expand Down