Skip to content

Commit 28fb5af

Browse files
authored
Merge pull request #271 from tonerdo/threshold-stats
Threshold stats
2 parents 33da1a9 + eac4cfc commit 28fb5af

File tree

14 files changed

+273
-83
lines changed

14 files changed

+273
-83
lines changed

src/coverlet.console/Program.cs

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using ConsoleTables;
77
using Coverlet.Console.Logging;
88
using Coverlet.Core;
9+
using Coverlet.Core.Enums;
910
using Coverlet.Core.Reporters;
1011

1112
using Microsoft.Extensions.CommandLineUtils;
@@ -30,6 +31,7 @@ static int Main(string[] args)
3031
CommandOption formats = app.Option("-f|--format", "Format of the generated coverage report.", CommandOptionType.MultipleValue);
3132
CommandOption threshold = app.Option("--threshold", "Exits with error if the coverage % is below value.", CommandOptionType.SingleValue);
3233
CommandOption thresholdTypes = app.Option("--threshold-type", "Coverage type to apply the threshold to.", CommandOptionType.MultipleValue);
34+
CommandOption thresholdStat = app.Option("--threshold-stat", "Coverage statistic used to enforce the threshold value.", CommandOptionType.SingleValue);
3335
CommandOption excludeFilters = app.Option("--exclude", "Filter expressions to exclude specific modules and types.", CommandOptionType.MultipleValue);
3436
CommandOption includeFilters = app.Option("--include", "Filter expressions to include only specific modules and types.", CommandOptionType.MultipleValue);
3537
CommandOption excludedSourceFiles = app.Option("--exclude-by-file", "Glob patterns specifying source files to exclude.", CommandOptionType.MultipleValue);
@@ -59,8 +61,9 @@ static int Main(string[] args)
5961
process.WaitForExit();
6062

6163
var dOutput = output.HasValue() ? output.Value() : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString();
62-
var dThreshold = threshold.HasValue() ? int.Parse(threshold.Value()) : 0;
64+
var dThreshold = threshold.HasValue() ? double.Parse(threshold.Value()) : 0;
6365
var dThresholdTypes = thresholdTypes.HasValue() ? thresholdTypes.Values : new List<string>(new string[] { "line", "branch", "method" });
66+
var dThresholdStat = thresholdStat.HasValue() ? Enum.Parse<ThresholdStatistic>(thresholdStat.Value(), true) : Enum.Parse<ThresholdStatistic>("minimum", true);
6467

6568
logger.LogInformation("\nCalculating coverage result...");
6669

@@ -79,7 +82,9 @@ static int Main(string[] args)
7982
{
8083
var reporter = new ReporterFactory(format).CreateReporter();
8184
if (reporter == null)
85+
{
8286
throw new Exception($"Specified output format '{format}' is not supported");
87+
}
8388

8489
if (reporter.OutputType == ReporterOutputType.Console)
8590
{
@@ -100,13 +105,31 @@ static int Main(string[] args)
100105
}
101106
}
102107

103-
var summary = new CoverageSummary();
104-
var exceptionBuilder = new StringBuilder();
108+
var thresholdTypeFlags = ThresholdTypeFlags.None;
109+
110+
foreach (var thresholdType in dThresholdTypes)
111+
{
112+
if (thresholdType.Equals("line", StringComparison.OrdinalIgnoreCase))
113+
{
114+
thresholdTypeFlags |= ThresholdTypeFlags.Line;
115+
}
116+
else if (thresholdType.Equals("branch", StringComparison.OrdinalIgnoreCase))
117+
{
118+
thresholdTypeFlags |= ThresholdTypeFlags.Branch;
119+
}
120+
else if (thresholdType.Equals("method", StringComparison.OrdinalIgnoreCase))
121+
{
122+
thresholdTypeFlags |= ThresholdTypeFlags.Method;
123+
}
124+
}
125+
105126
var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method");
106-
var thresholdFailed = false;
107-
var overallLineCoverage = summary.CalculateLineCoverage(result.Modules);
108-
var overallBranchCoverage = summary.CalculateBranchCoverage(result.Modules);
109-
var overallMethodCoverage = summary.CalculateMethodCoverage(result.Modules);
127+
var summary = new CoverageSummary();
128+
int numModules = result.Modules.Count;
129+
130+
var totalLinePercent = summary.CalculateLineCoverage(result.Modules).Percent * 100;
131+
var totalBranchPercent = summary.CalculateBranchCoverage(result.Modules).Percent * 100;
132+
var totalMethodPercent = summary.CalculateMethodCoverage(result.Modules).Percent * 100;
110133

111134
foreach (var _module in result.Modules)
112135
{
@@ -115,37 +138,40 @@ static int Main(string[] args)
115138
var methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent * 100;
116139

117140
coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%");
141+
}
118142

119-
if (dThreshold > 0)
143+
logger.LogInformation(coverageTable.ToStringAlternative());
144+
145+
coverageTable.Columns.Clear();
146+
coverageTable.Rows.Clear();
147+
148+
coverageTable.AddColumn(new[] { "", "Line", "Branch", "Method" });
149+
coverageTable.AddRow("Total", $"{totalLinePercent}%", $"{totalBranchPercent}%", $"{totalMethodPercent}%");
150+
coverageTable.AddRow("Average", $"{totalLinePercent / numModules}%", $"{totalBranchPercent / numModules}%", $"{totalMethodPercent / numModules}%");
151+
152+
logger.LogInformation(coverageTable.ToStringAlternative());
153+
154+
thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, dThreshold, thresholdTypeFlags, dThresholdStat);
155+
if (thresholdTypeFlags != ThresholdTypeFlags.None)
156+
{
157+
var exceptionMessageBuilder = new StringBuilder();
158+
if ((thresholdTypeFlags & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None)
120159
{
121-
if (linePercent < dThreshold && dThresholdTypes.Contains("line"))
122-
{
123-
exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a line coverage '{linePercent}%' below specified threshold '{dThreshold}%'");
124-
thresholdFailed = true;
125-
}
126-
127-
if (branchPercent < dThreshold && dThresholdTypes.Contains("branch"))
128-
{
129-
exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a branch coverage '{branchPercent}%' below specified threshold '{dThreshold}%'");
130-
thresholdFailed = true;
131-
}
132-
133-
if (methodPercent < dThreshold && dThresholdTypes.Contains("method"))
134-
{
135-
exceptionBuilder.AppendLine($"'{Path.GetFileNameWithoutExtension(_module.Key)}' has a method coverage '{methodPercent}%' below specified threshold '{dThreshold}%'");
136-
thresholdFailed = true;
137-
}
160+
exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} line coverage is below the specified {dThreshold}");
138161
}
139-
}
140162

141-
logger.LogInformation(string.Empty);
142-
logger.LogInformation(coverageTable.ToStringAlternative());
143-
logger.LogInformation($"Total Line: {overallLineCoverage.Percent * 100}%");
144-
logger.LogInformation($"Total Branch: {overallBranchCoverage.Percent * 100}%");
145-
logger.LogInformation($"Total Method: {overallMethodCoverage.Percent * 100}%");
163+
if ((thresholdTypeFlags & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None)
164+
{
165+
exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} branch coverage is below the specified {dThreshold}");
166+
}
146167

147-
if (thresholdFailed)
148-
throw new Exception(exceptionBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray()));
168+
if ((thresholdTypeFlags & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None)
169+
{
170+
exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} method coverage is below the specified {dThreshold}");
171+
}
172+
173+
throw new Exception(exceptionMessageBuilder.ToString());
174+
}
149175

150176
return process.ExitCode == 0 ? 0 : process.ExitCode;
151177
});

src/coverlet.core/Coverage.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55

6+
using Coverlet.Core.Enums;
67
using Coverlet.Core.Helpers;
78
using Coverlet.Core.Instrumentation;
89
using Coverlet.Core.Symbols;
@@ -48,7 +49,7 @@ public Coverage(string module, string[] includeFilters, string[] includeDirector
4849
public void PrepareModules()
4950
{
5051
string[] modules = InstrumentationHelper.GetCoverableModules(_module, _includeDirectories);
51-
string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludedSourceFiles);
52+
string[] excludes = InstrumentationHelper.GetExcludedFiles(_excludedSourceFiles);
5253
_excludeFilters = _excludeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
5354
_includeFilters = _includeFilters?.Where(f => InstrumentationHelper.IsValidFilterExpression(f)).ToArray();
5455

@@ -131,14 +132,14 @@ public CoverageResult GetCoverageResult()
131132
if (methods.TryGetValue(branch.Method, out Method method))
132133
{
133134
method.Branches.Add(new BranchInfo
134-
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
135+
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
135136
);
136137
}
137138
else
138139
{
139140
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
140141
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
141-
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
142+
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
142143
);
143144
}
144145
}
@@ -147,7 +148,7 @@ public CoverageResult GetCoverageResult()
147148
documents[doc.Path].Add(branch.Class, new Methods());
148149
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
149150
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
150-
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
151+
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
151152
);
152153
}
153154
}
@@ -157,7 +158,7 @@ public CoverageResult GetCoverageResult()
157158
documents[doc.Path].Add(branch.Class, new Methods());
158159
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
159160
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
160-
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
161+
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
161162
);
162163
}
163164
}
@@ -292,7 +293,7 @@ private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments,
292293
}
293294

294295
relativePathOfBestMatch = relativePathOfBestMatch == "." ? string.Empty : relativePathOfBestMatch;
295-
296+
296297
string replacement = Path.Combine(relativePathOfBestMatch, Path.GetFileName(document));
297298
replacement = replacement.Replace('\\', '/');
298299

src/coverlet.core/CoverageResult.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using Coverlet.Core.Enums;
56

67
namespace Coverlet.Core
78
{
@@ -105,5 +106,101 @@ internal void Merge(Modules modules)
105106
}
106107
}
107108
}
109+
110+
public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summary, double threshold, ThresholdTypeFlags thresholdTypes, ThresholdStatistic thresholdStat)
111+
{
112+
var thresholdTypeFlags = ThresholdTypeFlags.None;
113+
switch (thresholdStat)
114+
{
115+
case ThresholdStatistic.Minimum:
116+
{
117+
foreach (var module in Modules)
118+
{
119+
var line = summary.CalculateLineCoverage(module.Value).Percent * 100;
120+
var branch = summary.CalculateBranchCoverage(module.Value).Percent * 100;
121+
var method = summary.CalculateMethodCoverage(module.Value).Percent * 100;
122+
123+
if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None)
124+
{
125+
if (line < threshold)
126+
thresholdTypeFlags |= ThresholdTypeFlags.Line;
127+
}
128+
129+
if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None)
130+
{
131+
if (branch < threshold)
132+
thresholdTypeFlags |= ThresholdTypeFlags.Branch;
133+
}
134+
135+
if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None)
136+
{
137+
if (method < threshold)
138+
thresholdTypeFlags |= ThresholdTypeFlags.Method;
139+
}
140+
}
141+
}
142+
break;
143+
case ThresholdStatistic.Average:
144+
{
145+
double line = 0;
146+
double branch = 0;
147+
double method = 0;
148+
int numModules = Modules.Count;
149+
150+
foreach (var module in Modules)
151+
{
152+
line += summary.CalculateLineCoverage(module.Value).Percent * 100;
153+
branch += summary.CalculateBranchCoverage(module.Value).Percent * 100;
154+
method += summary.CalculateMethodCoverage(module.Value).Percent * 100;
155+
}
156+
157+
if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None)
158+
{
159+
if ((line / numModules) < threshold)
160+
thresholdTypeFlags |= ThresholdTypeFlags.Line;
161+
}
162+
163+
if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None)
164+
{
165+
if ((branch / numModules) < threshold)
166+
thresholdTypeFlags |= ThresholdTypeFlags.Branch;
167+
}
168+
169+
if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None)
170+
{
171+
if ((method / numModules) < threshold)
172+
thresholdTypeFlags |= ThresholdTypeFlags.Method;
173+
}
174+
}
175+
break;
176+
case ThresholdStatistic.Total:
177+
{
178+
var line = summary.CalculateLineCoverage(Modules).Percent * 100;
179+
var branch = summary.CalculateBranchCoverage(Modules).Percent * 100;
180+
var method = summary.CalculateMethodCoverage(Modules).Percent * 100;
181+
182+
if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None)
183+
{
184+
if (line < threshold)
185+
thresholdTypeFlags |= ThresholdTypeFlags.Line;
186+
}
187+
188+
if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None)
189+
{
190+
if (branch < threshold)
191+
thresholdTypeFlags |= ThresholdTypeFlags.Branch;
192+
}
193+
194+
if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None)
195+
{
196+
if (method < threshold)
197+
thresholdTypeFlags |= ThresholdTypeFlags.Method;
198+
}
199+
}
200+
break;
201+
}
202+
203+
return thresholdTypeFlags;
204+
}
108205
}
109206
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Coverlet.Core.Enums
2+
{
3+
public enum ThresholdStatistic
4+
{
5+
Minimum,
6+
Average,
7+
Total
8+
}
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace Coverlet.Core.Enums
4+
{
5+
[Flags]
6+
public enum ThresholdTypeFlags
7+
{
8+
None = 0,
9+
Line = 2,
10+
Branch = 4,
11+
Method = 8
12+
}
13+
}

src/coverlet.core/Reporters/ReporterFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Linq;
33
using System.Collections.Generic;
4-
using coverlet.core.Reporters;
4+
using Coverlet.Core.Reporters;
55

66
namespace Coverlet.Core.Reporters
77
{

src/coverlet.core/Reporters/TeamCityReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Coverlet.Core.Reporters;
44
using System.Text;
55

6-
namespace coverlet.core.Reporters
6+
namespace Coverlet.Core.Reporters
77
{
88
public class TeamCityReporter : IReporter
99
{

0 commit comments

Comments
 (0)