Skip to content

Commit 71f95f5

Browse files
committed
Add support for an intermediate result.
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.
1 parent 4d6e1d4 commit 71f95f5

File tree

14 files changed

+496
-14
lines changed

14 files changed

+496
-14
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ dotnet test /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line
8282

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

85+
### Intermediate Result
86+
87+
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.
88+
89+
```bash
90+
dotnet test /p:CollectCoverage=true /p:CoverletIntermediateResult=intermediate.json
91+
```
92+
93+
_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!_
94+
8595
### Excluding From Coverage
8696

8797
#### Attributes

src/coverlet.console/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ static int Main(string[] args)
1818

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

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

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

39+
if (intermediateResult.HasValue())
40+
dotnetTestArgs.Add($"/p:CoverletIntermediateResult={intermediateResult.Value()}");
41+
3842
if (output.HasValue())
3943
dotnetTestArgs.Add($"/p:CoverletOutput={output.Value()}");
4044

src/coverlet.core/Coverage.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ public CoverageResult GetCoverageResult()
126126
}
127127
}
128128

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

src/coverlet.core/CoverageResult.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
using System;
21
using System.Collections.Generic;
3-
using System.IO;
4-
5-
using Jil;
62

73
namespace Coverlet.Core
84
{
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using Coverlet.Core;
2+
3+
namespace coverlet.core.Extensions
4+
{
5+
public static class CoverageResultHelper
6+
{
7+
public static void Merge(this CoverageResult result, CoverageResult other)
8+
{
9+
MergeModules(result.Modules, other.Modules);
10+
}
11+
12+
private static void MergeModules(Modules result, Modules other)
13+
{
14+
foreach (var keyValuePair in other)
15+
{
16+
if (!result.ContainsKey(keyValuePair.Key))
17+
{
18+
result[keyValuePair.Key] = keyValuePair.Value;
19+
}
20+
else
21+
{
22+
MergeDocuments(result[keyValuePair.Key], keyValuePair.Value);
23+
}
24+
}
25+
}
26+
27+
private static void MergeDocuments(Documents result, Documents other)
28+
{
29+
foreach (var keyValuePair in other)
30+
{
31+
if (!result.ContainsKey(keyValuePair.Key))
32+
{
33+
result[keyValuePair.Key] = keyValuePair.Value;
34+
}
35+
else
36+
{
37+
MergeClasses(result[keyValuePair.Key], keyValuePair.Value);
38+
}
39+
}
40+
}
41+
42+
private static void MergeClasses(Classes result, Classes other)
43+
{
44+
foreach (var keyValuePair in other)
45+
{
46+
if (!result.ContainsKey(keyValuePair.Key))
47+
{
48+
result[keyValuePair.Key] = keyValuePair.Value;
49+
}
50+
else
51+
{
52+
MergeMethods(result[keyValuePair.Key], keyValuePair.Value);
53+
}
54+
}
55+
}
56+
57+
private static void MergeMethods(Methods result, Methods other)
58+
{
59+
foreach (var keyValuePair in other)
60+
{
61+
if (!result.ContainsKey(keyValuePair.Key))
62+
{
63+
result[keyValuePair.Key] = keyValuePair.Value;
64+
}
65+
else
66+
{
67+
MergeMethod(result[keyValuePair.Key], keyValuePair.Value);
68+
}
69+
}
70+
}
71+
72+
private static void MergeMethod(Method result, Method other)
73+
{
74+
MergeLines(result.Lines, other.Lines);
75+
MergeBranches(result.Branches, other.Branches);
76+
}
77+
78+
private static void MergeBranches(Branches result, Branches other)
79+
{
80+
foreach (var keyValuePair in other)
81+
{
82+
if (!result.ContainsKey(keyValuePair.Key))
83+
{
84+
result[keyValuePair.Key] = keyValuePair.Value;
85+
}
86+
else
87+
{
88+
result[keyValuePair.Key].Hits += keyValuePair.Value.Hits;
89+
}
90+
}
91+
}
92+
93+
private static void MergeLines(Lines result, Lines other)
94+
{
95+
foreach (var keyValuePair in other)
96+
{
97+
if (!result.ContainsKey(keyValuePair.Key))
98+
{
99+
result[keyValuePair.Key] = keyValuePair.Value;
100+
}
101+
else
102+
{
103+
result[keyValuePair.Key].Hits += keyValuePair.Value.Hits;
104+
}
105+
}
106+
}
107+
}
108+
}

src/coverlet.core/Reporters/CoberturaReporter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ public string Report(CoverageResult result)
130130
return Encoding.UTF8.GetString(stream.ToArray());
131131
}
132132

133+
public CoverageResult Read(string data)
134+
{
135+
throw new NotSupportedException("Not supported by this reporter.");
136+
}
137+
133138
private string GetBasePath(Modules modules)
134139
{
135140
List<string> sources = new List<string>();

src/coverlet.core/Reporters/IReporter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ public interface IReporter
55
string Format { get; }
66
string Extension { get; }
77
string Report(CoverageResult result);
8+
CoverageResult Read(string data);
89
}
910
}

src/coverlet.core/Reporters/JsonReporter.cs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System;
12
using Newtonsoft.Json;
3+
using Newtonsoft.Json.Linq;
24

35
namespace Coverlet.Core.Reporters
46
{
@@ -10,7 +12,119 @@ public class JsonReporter : IReporter
1012

1113
public string Report(CoverageResult result)
1214
{
13-
return JsonConvert.SerializeObject(result.Modules, Formatting.Indented);
15+
return JsonConvert.SerializeObject(result.Modules, Formatting.Indented, new LinesConverter(), new BranchesConverter());
16+
}
17+
18+
public CoverageResult Read(string data)
19+
{
20+
return new CoverageResult
21+
{
22+
Identifier = Guid.NewGuid().ToString(),
23+
Modules = JsonConvert.DeserializeObject<Modules>(data, new LinesConverter(), new BranchesConverter())
24+
};
25+
}
26+
27+
private class BranchesConverter : JsonConverter
28+
{
29+
public override bool CanConvert(Type objectType)
30+
{
31+
return objectType == typeof(Branches);
32+
}
33+
34+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
35+
{
36+
var array = JArray.Load(reader);
37+
var branches = new Branches();
38+
39+
foreach (var item in array)
40+
{
41+
var obj = (JObject)item;
42+
43+
var key = (
44+
(int)obj["Key"]["Number"],
45+
(int)obj["Key"]["Offset"],
46+
(int)obj["Key"]["EndOffset"],
47+
(int)obj["Key"]["Path"],
48+
(uint)obj["Key"]["Ordinal"]);
49+
var value = new HitInfo { Hits = (int)obj["Value"]["Hits"] };
50+
51+
branches.Add(key, value);
52+
}
53+
54+
return branches;
55+
}
56+
57+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
58+
{
59+
var branches = (Branches) value;
60+
var array = new JArray();
61+
62+
foreach (var kv in branches)
63+
{
64+
dynamic obj = new JObject();
65+
66+
obj.Key = new JObject();
67+
obj.Key.Number = kv.Key.Number;
68+
obj.Key.Offset = kv.Key.Offset;
69+
obj.Key.EndOffset = kv.Key.EndOffset;
70+
obj.Key.Path = kv.Key.Path;
71+
obj.Key.Ordinal = kv.Key.Ordinal;
72+
73+
obj.Value = new JObject();
74+
obj.Value.Hits = kv.Value.Hits;
75+
76+
array.Add(obj);
77+
}
78+
79+
array.WriteTo(writer);
80+
}
81+
}
82+
83+
private class LinesConverter : JsonConverter
84+
{
85+
public override bool CanConvert(Type objectType)
86+
{
87+
return objectType == typeof(Lines);
88+
}
89+
90+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
91+
{
92+
var array = JArray.Load(reader);
93+
var lines = new Lines();
94+
95+
foreach (var item in array)
96+
{
97+
var obj = (JObject) item;
98+
99+
var key = (int)obj["Key"]["Line"];
100+
var value = new HitInfo { Hits = (int)obj["Value"]["Hits"] };
101+
102+
lines.Add(key, value);
103+
}
104+
105+
return lines;
106+
}
107+
108+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
109+
{
110+
var lines = (Lines)value;
111+
var array = new JArray();
112+
113+
foreach (var kv in lines)
114+
{
115+
dynamic obj = new JObject();
116+
117+
obj.Key = new JObject();
118+
obj.Key.Line = kv.Key;
119+
120+
obj.Value = new JObject();
121+
obj.Value.Hits = kv.Value.Hits;
122+
123+
array.Add(obj);
124+
}
125+
126+
array.WriteTo(writer);
127+
}
14128
}
15129
}
16130
}

src/coverlet.core/Reporters/LcovReporter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,10 @@ public string Report(CoverageResult result)
6060

6161
return string.Join(Environment.NewLine, lcov);
6262
}
63+
64+
public CoverageResult Read(string data)
65+
{
66+
throw new NotSupportedException("Not supported by this reporter.");
67+
}
6368
}
6469
}

src/coverlet.core/Reporters/OpenCoverReporter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,5 +233,10 @@ public string Report(CoverageResult result)
233233

234234
return Encoding.UTF8.GetString(stream.ToArray());
235235
}
236+
237+
public CoverageResult Read(string data)
238+
{
239+
throw new NotSupportedException("Not supported by this reporter.");
240+
}
236241
}
237242
}

0 commit comments

Comments
 (0)