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 support for merging results #123

Closed
wants to merge 2 commits into from
Closed
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ dotnet test /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line

You can specify multiple values for `ThresholdType` by separating them with commas. Valid values include `line`, `branch` and `method`.

### Intermediate Result

For combining the results of multiple projects, it is possible to use an intermediate result by using the `CoverletIntermediateResult` property. Coverage output will be merged with an intermediate result before generating the report(s). Ensure that all test runs point to the same intermediate result file.

```bash
dotnet test /p:CollectCoverage=true /p:CoverletIntermediateResult=intermediate.json
```

_Note: When using build automation, ensure that this intermediate result file is removed first. It doesn't make sense to merge with an intermediate result from a different build!_

### Excluding From Coverage

#### Attributes
Expand Down
4 changes: 4 additions & 0 deletions src/coverlet.console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ static int Main(string[] args)

CommandArgument project = app.Argument("<PROJECT>", "The project to test. Defaults to the current directory.");
CommandOption config = app.Option("-c|--configuration", "Configuration to use for building the project.", CommandOptionType.SingleValue);
CommandOption intermediateResult = app.Option("-i|--coverage-intermediate-result", "The output path of intermediate result (for merging multiple runs).", CommandOptionType.SingleValue);
CommandOption output = app.Option("-o|--coverage-output", "The output path of the generated coverage report", CommandOptionType.SingleValue);
CommandOption format = app.Option("-f|--coverage-format", "The format of the coverage report", CommandOptionType.SingleValue);

Expand All @@ -35,6 +36,9 @@ static int Main(string[] args)

dotnetTestArgs.Add("/p:CollectCoverage=true");

if (intermediateResult.HasValue())
dotnetTestArgs.Add($"/p:CoverletIntermediateResult={intermediateResult.Value()}");

if (output.HasValue())
dotnetTestArgs.Add($"/p:CoverletOutput={output.Value()}");

Expand Down
11 changes: 11 additions & 0 deletions src/coverlet.core/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" culture="Neutral" publicKeyToken="30ad4fe6b2a6aeed" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
42 changes: 12 additions & 30 deletions src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,86 +66,68 @@ public CoverageResult GetCoverageResult()
{
if (methods.TryGetValue(line.Method, out Method method))
{
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits });
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new HitInfo { Hits = line.Hits });
}
else
{
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits });
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new HitInfo { Hits = line.Hits });
}
}
else
{
documents[doc.Path].Add(line.Class, new Methods());
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits });
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new HitInfo { Hits = line.Hits });
}
}
else
{
documents.Add(doc.Path, new Classes());
documents[doc.Path].Add(line.Class, new Methods());
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new LineInfo { Hits = line.Hits });
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, new HitInfo { Hits = line.Hits });
}
}

// Construct Branch Results
foreach (var branch in doc.Branches)
{
var key = (branch.Number, branch.Offset, branch.EndOffset, branch.Path, branch.Ordinal);

if (documents.TryGetValue(doc.Path, out Classes classes))
{
if (classes.TryGetValue(branch.Class, out Methods methods))
{
if (methods.TryGetValue(branch.Method, out Method method))
{
if (method.Branches.TryGetValue(branch.Number, out List<BranchInfo> branchInfo))
{
documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo
{ Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
else
{
documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List<BranchInfo>());
documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo
{ Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
documents[doc.Path][branch.Class][branch.Method].Branches[key] = new HitInfo { Hits = branch.Hits };
}
else
{
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List<BranchInfo>());
documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo
{ Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
documents[doc.Path][branch.Class][branch.Method].Branches[key] = new HitInfo { Hits = branch.Hits };
}
}
else
{
documents[doc.Path].Add(branch.Class, new Methods());
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List<BranchInfo>());
documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo
{ Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
documents[doc.Path][branch.Class][branch.Method].Branches[key] = new HitInfo { Hits = branch.Hits };
}
}
else
{
documents.Add(doc.Path, new Classes());
documents[doc.Path].Add(branch.Class, new Methods());
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(branch.Number, new List<BranchInfo>());
documents[doc.Path][branch.Class][branch.Method].Branches[branch.Number].Add(new BranchInfo
{ Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
documents[doc.Path][branch.Class][branch.Method].Branches[key] = new HitInfo { Hits = branch.Hits };
}
}
}

modules.Add(result.ModulePath, documents);
// TODO: Module path is not generic across multiple projects referencing the same assemblies.
modules.Add(Path.GetFileName(result.ModulePath), documents);
InstrumentationHelper.RestoreOriginalModule(result.ModulePath, _identifier);
}

Expand Down
17 changes: 3 additions & 14 deletions src/coverlet.core/CoverageResult.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
using System.Collections.Generic;
using System.IO;

using Jil;

namespace Coverlet.Core
{
public class LineInfo
public class HitInfo
{
public int Hits { get; set; }
}

public class BranchInfo : LineInfo
{
public int Offset { get; set; }
public int EndOffset { get; set; }
public int Path { get; set; }
public uint Ordinal { get; set; }
}

public class Lines : SortedDictionary<int, LineInfo> { }
public class Branches : SortedDictionary<int, List<BranchInfo>> { }
public class Lines : SortedDictionary<int, HitInfo> { }
public class Branches : SortedDictionary<(int Number, int Offset, int EndOffset, int Path, uint Ordinal), HitInfo> { }
public class Method
{
internal Method()
Expand Down
10 changes: 5 additions & 5 deletions src/coverlet.core/CoverageSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,19 @@ public CoverageDetails CalculateLineCoverage(Modules modules)
return details;
}

public CoverageDetails CalculateBranchCoverage(List<BranchInfo> branchInfo)
public CoverageDetails CalculateBranchCoverage(List<KeyValuePair<(int Number, int Offset, int EndOffset, int Path, uint Ordinal), HitInfo>> branches)
{
var details = new CoverageDetails();
details.Covered = branchInfo.Count(bi => bi.Hits > 0);
details.Total = branchInfo.Count;
details.Covered = branches.Count(kv => kv.Value.Hits > 0);
details.Total = branches.Count;
return details;
}

public CoverageDetails CalculateBranchCoverage(Branches branches)
{
var details = new CoverageDetails();
details.Covered = branches.Sum(b => b.Value.Where(bi => bi.Hits > 0).Count());
details.Total = branches.Sum(b => b.Value.Count());
details.Covered = branches.Count(kv => kv.Value.Hits > 0);
details.Total = branches.Count;
return details;
}

Expand Down
108 changes: 108 additions & 0 deletions src/coverlet.core/Extensions/CoverageResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Coverlet.Core;

namespace coverlet.core.Extensions
{
public static class CoverageResultHelper
{
public static void Merge(this CoverageResult result, CoverageResult other)
{
MergeModules(result.Modules, other.Modules);
}

private static void MergeModules(Modules result, Modules other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
MergeDocuments(result[keyValuePair.Key], keyValuePair.Value);
}
}
}

private static void MergeDocuments(Documents result, Documents other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
MergeClasses(result[keyValuePair.Key], keyValuePair.Value);
}
}
}

private static void MergeClasses(Classes result, Classes other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
MergeMethods(result[keyValuePair.Key], keyValuePair.Value);
}
}
}

private static void MergeMethods(Methods result, Methods other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
MergeMethod(result[keyValuePair.Key], keyValuePair.Value);
}
}
}

private static void MergeMethod(Method result, Method other)
{
MergeLines(result.Lines, other.Lines);
MergeBranches(result.Branches, other.Branches);
}

private static void MergeBranches(Branches result, Branches other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
result[keyValuePair.Key].Hits += keyValuePair.Value.Hits;
}
}
}

private static void MergeLines(Lines result, Lines other)
{
foreach (var keyValuePair in other)
{
if (!result.ContainsKey(keyValuePair.Key))
{
result[keyValuePair.Key] = keyValuePair.Value;
}
else
{
result[keyValuePair.Key].Hits += keyValuePair.Value.Hits;
}
}
}
}
}
13 changes: 10 additions & 3 deletions src/coverlet.core/Reporters/CoberturaReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,18 @@ public string Report(CoverageResult result)
foreach (var ln in meth.Value.Lines)
{
XElement line = new XElement("line");
var branches = meth.Value.Branches.Where(kv => kv.Key.Number == ln.Key).ToList();

line.Add(new XAttribute("number", ln.Key.ToString()));
line.Add(new XAttribute("hits", ln.Value.Hits.ToString()));
line.Add(new XAttribute("branch", meth.Value.Branches.ContainsKey(ln.Key).ToString()));
line.Add(new XAttribute("branch", branches.Any().ToString()));

if (meth.Value.Branches.TryGetValue(ln.Key, out List<BranchInfo> branches))
if (branches.Any())
{
var branchInfoCoverage = summary.CalculateBranchCoverage(branches);
line.Add(new XAttribute("condition-coverage", $"{branchInfoCoverage.Percent*100}% ({branchInfoCoverage.Covered}/{branchInfoCoverage.Total})"));
XElement conditions = new XElement("conditions");
var byOffset = branches.GroupBy(b => b.Offset).ToDictionary(b => b.Key, b => b.ToList());
var byOffset = branches.GroupBy(kv => kv.Key.Offset).ToDictionary(b => b.Key, b => b.ToList());
foreach (var entry in byOffset)
{
XElement condition = new XElement("condition");
Expand Down Expand Up @@ -128,6 +130,11 @@ public string Report(CoverageResult result)
return Encoding.UTF8.GetString(stream.ToArray());
}

public CoverageResult Read(string data)
{
throw new NotSupportedException("Not supported by this reporter.");
}

private string GetBasePath(Modules modules)
{
List<string> sources = new List<string>();
Expand Down
1 change: 1 addition & 0 deletions src/coverlet.core/Reporters/IReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public interface IReporter
string Format { get; }
string Extension { get; }
string Report(CoverageResult result);
CoverageResult Read(string data);
}
}
Loading