Skip to content

Commit

Permalink
Add support for an intermediate result.
Browse files Browse the repository at this point in the history
This commits adds the option `IntermediateResult`, which will write the
raw coverage result to an intermediate file, and merge the result of the
next run with that file. This is useful in multi-project solutions.

Eventually, the last coverage run will produce a report with the
combined results of all the runs.
  • Loading branch information
basilfx committed Jun 20, 2018
1 parent 4d6e1d4 commit 71f95f5
Show file tree
Hide file tree
Showing 14 changed files with 496 additions and 14 deletions.
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
3 changes: 2 additions & 1 deletion src/coverlet.core/Coverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ public CoverageResult GetCoverageResult()
}
}

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
4 changes: 0 additions & 4 deletions src/coverlet.core/CoverageResult.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;

using Jil;

namespace Coverlet.Core
{
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;
}
}
}
}
}
5 changes: 5 additions & 0 deletions src/coverlet.core/Reporters/CoberturaReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,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);
}
}
116 changes: 115 additions & 1 deletion src/coverlet.core/Reporters/JsonReporter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Coverlet.Core.Reporters
{
Expand All @@ -10,7 +12,119 @@ public class JsonReporter : IReporter

public string Report(CoverageResult result)
{
return JsonConvert.SerializeObject(result.Modules, Formatting.Indented);
return JsonConvert.SerializeObject(result.Modules, Formatting.Indented, new LinesConverter(), new BranchesConverter());
}

public CoverageResult Read(string data)
{
return new CoverageResult
{
Identifier = Guid.NewGuid().ToString(),
Modules = JsonConvert.DeserializeObject<Modules>(data, new LinesConverter(), new BranchesConverter())
};
}

private class BranchesConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Branches);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var array = JArray.Load(reader);
var branches = new Branches();

foreach (var item in array)
{
var obj = (JObject)item;

var key = (
(int)obj["Key"]["Number"],
(int)obj["Key"]["Offset"],
(int)obj["Key"]["EndOffset"],
(int)obj["Key"]["Path"],
(uint)obj["Key"]["Ordinal"]);
var value = new HitInfo { Hits = (int)obj["Value"]["Hits"] };

branches.Add(key, value);
}

return branches;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var branches = (Branches) value;
var array = new JArray();

foreach (var kv in branches)
{
dynamic obj = new JObject();

obj.Key = new JObject();
obj.Key.Number = kv.Key.Number;
obj.Key.Offset = kv.Key.Offset;
obj.Key.EndOffset = kv.Key.EndOffset;
obj.Key.Path = kv.Key.Path;
obj.Key.Ordinal = kv.Key.Ordinal;

obj.Value = new JObject();
obj.Value.Hits = kv.Value.Hits;

array.Add(obj);
}

array.WriteTo(writer);
}
}

private class LinesConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Lines);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var array = JArray.Load(reader);
var lines = new Lines();

foreach (var item in array)
{
var obj = (JObject) item;

var key = (int)obj["Key"]["Line"];
var value = new HitInfo { Hits = (int)obj["Value"]["Hits"] };

lines.Add(key, value);
}

return lines;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var lines = (Lines)value;
var array = new JArray();

foreach (var kv in lines)
{
dynamic obj = new JObject();

obj.Key = new JObject();
obj.Key.Line = kv.Key;

obj.Value = new JObject();
obj.Value.Hits = kv.Value.Hits;

array.Add(obj);
}

array.WriteTo(writer);
}
}
}
}
5 changes: 5 additions & 0 deletions src/coverlet.core/Reporters/LcovReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,10 @@ public string Report(CoverageResult result)

return string.Join(Environment.NewLine, lcov);
}

public CoverageResult Read(string data)
{
throw new NotSupportedException("Not supported by this reporter.");
}
}
}
5 changes: 5 additions & 0 deletions src/coverlet.core/Reporters/OpenCoverReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,5 +233,10 @@ public string Report(CoverageResult result)

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

public CoverageResult Read(string data)
{
throw new NotSupportedException("Not supported by this reporter.");
}
}
}
38 changes: 38 additions & 0 deletions src/coverlet.msbuild.tasks/CoverageResultTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Text;
using coverlet.core.Extensions;
using ConsoleTables;
using Coverlet.Core;
using Coverlet.Core.Reporters;
Expand All @@ -13,10 +14,17 @@ namespace Coverlet.MSbuild.Tasks
public class CoverageResultTask : Task
{
private string _filename;
private string _intermediateResult;
private string _format;
private int _threshold;
private string _thresholdType;

public string IntermediateResult
{
get { return _intermediateResult; }
set { _intermediateResult = value; }
}

[Required]
public string Output
{
Expand Down Expand Up @@ -59,6 +67,36 @@ public override bool Execute()
Directory.CreateDirectory(directory);
}

if (!string.IsNullOrEmpty(_intermediateResult))
{
Console.WriteLine("\nMerging with intermediate result...");
var reporter = new ReporterFactory("json").CreateReporter();

if (File.Exists(_intermediateResult))
{
try
{
result.Merge(reporter.Read(File.ReadAllText(_intermediateResult)));
}
catch (Exception)
{
Console.WriteLine(" Unable to read intermediate results, ignoring");
}
}

try
{
File.WriteAllText(_intermediateResult, reporter.Report(result));
}
catch (IOException e)
{
throw new Exception("Unable to write intermediate results", e);
}

Console.WriteLine($" Intermediate result written to '{_intermediateResult}'");
}

Console.WriteLine($"\nWriting report(s)...");
var formats = _format.Split(',');
foreach (var format in formats)
{
Expand Down
Loading

0 comments on commit 71f95f5

Please sign in to comment.