diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 608786fcb..0dc76a4cb 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using ConsoleTables; @@ -102,7 +103,6 @@ static int Main(string[] args) process.WaitForExit(); var dOutput = output.HasValue() ? output.Value() : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); - var dThreshold = threshold.HasValue() ? double.Parse(threshold.Value()) : 0; var dThresholdTypes = thresholdTypes.HasValue() ? thresholdTypes.Values : new List(new string[] { "line", "branch", "method" }); var dThresholdStat = thresholdStat.HasValue() ? Enum.Parse(thresholdStat.Value(), true) : Enum.Parse("minimum", true); @@ -147,20 +147,47 @@ static int Main(string[] args) } var thresholdTypeFlags = ThresholdTypeFlags.None; - + var thresholdTypeFlagQueue = new Queue(); foreach (var thresholdType in dThresholdTypes) { if (thresholdType.Equals("line", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Line; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Line); } else if (thresholdType.Equals("branch", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Branch; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Branch); } else if (thresholdType.Equals("method", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Method; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Method); + } + } + + Dictionary thresholdTypeFlagValues = new Dictionary(); + if (threshold.HasValue() && threshold.Value().Contains(',')) + { + var thresholdValues = threshold.Value().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()); + if (thresholdValues.Count() != thresholdTypeFlagQueue.Count()) + { + throw new Exception($"Threshold type flag count ({thresholdTypeFlagQueue.Count()}) and values count ({thresholdValues.Count()}) doesnt match"); + } + + foreach (var thresholdValue in thresholdValues) + { + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = double.Parse(thresholdValue); + } + } + else + { + double thresholdValue = threshold.HasValue() ? double.Parse(threshold.Value()) : 0; + + while (thresholdTypeFlagQueue.Any()) + { + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = thresholdValue; } } @@ -199,28 +226,30 @@ static int Main(string[] args) coverageTable.AddRow("Average", $"{averageLinePercent}%", $"{averageBranchPercent}%", $"{averageMethodPercent}%"); logger.LogInformation(coverageTable.ToStringAlternative()); + if (process.ExitCode > 0) { exitCode += (int)CommandExitCodes.TestFailed; } - thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, dThreshold, thresholdTypeFlags, dThresholdStat); + + thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, dThresholdStat); if (thresholdTypeFlags != ThresholdTypeFlags.None) { exitCode += (int)CommandExitCodes.CoverageBelowThreshold; var exceptionMessageBuilder = new StringBuilder(); if ((thresholdTypeFlags & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} line coverage is below the specified {dThreshold}"); + exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} line coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Line]}"); } if ((thresholdTypeFlags & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} branch coverage is below the specified {dThreshold}"); + exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} branch coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Branch]}"); } if ((thresholdTypeFlags & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} method coverage is below the specified {dThreshold}"); + exceptionMessageBuilder.AppendLine($"The {dThresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); } throw new Exception(exceptionMessageBuilder.ToString()); diff --git a/src/coverlet.core/CoverageResult.cs b/src/coverlet.core/CoverageResult.cs index 00f9cfabe..2ba8b4126 100644 --- a/src/coverlet.core/CoverageResult.cs +++ b/src/coverlet.core/CoverageResult.cs @@ -110,7 +110,7 @@ internal void Merge(Modules modules) } } - public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summary, double threshold, ThresholdTypeFlags thresholdTypes, ThresholdStatistic thresholdStat) + public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summary, Dictionary thresholdTypeFlagValues, ThresholdTypeFlags thresholdTypes, ThresholdStatistic thresholdStat) { var thresholdTypeFlags = ThresholdTypeFlags.None; switch (thresholdStat) @@ -125,19 +125,19 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) { - if (line < threshold) + if (line < thresholdTypeFlagValues[ThresholdTypeFlags.Line]) thresholdTypeFlags |= ThresholdTypeFlags.Line; } if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) { - if (branch < threshold) + if (branch < thresholdTypeFlagValues[ThresholdTypeFlags.Branch]) thresholdTypeFlags |= ThresholdTypeFlags.Branch; } if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) { - if (method < threshold) + if (method < thresholdTypeFlagValues[ThresholdTypeFlags.Method]) thresholdTypeFlags |= ThresholdTypeFlags.Method; } } @@ -151,19 +151,19 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) { - if (line < threshold) + if ((line / numModules) < thresholdTypeFlagValues[ThresholdTypeFlags.Line]) thresholdTypeFlags |= ThresholdTypeFlags.Line; } if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) { - if (branch < threshold) + if ((branch / numModules) < thresholdTypeFlagValues[ThresholdTypeFlags.Branch]) thresholdTypeFlags |= ThresholdTypeFlags.Branch; } if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) { - if (method < threshold) + if ((method / numModules) < thresholdTypeFlagValues[ThresholdTypeFlags.Method]) thresholdTypeFlags |= ThresholdTypeFlags.Method; } } @@ -176,19 +176,19 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar if ((thresholdTypes & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) { - if (line < threshold) + if (line < thresholdTypeFlagValues[ThresholdTypeFlags.Line]) thresholdTypeFlags |= ThresholdTypeFlags.Line; } if ((thresholdTypes & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) { - if (branch < threshold) + if (branch < thresholdTypeFlagValues[ThresholdTypeFlags.Branch]) thresholdTypeFlags |= ThresholdTypeFlags.Branch; } if ((thresholdTypes & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) { - if (method < threshold) + if (method < thresholdTypeFlagValues[ThresholdTypeFlags.Method]) thresholdTypeFlags |= ThresholdTypeFlags.Method; } } diff --git a/src/coverlet.msbuild.tasks/CoverageResultTask.cs b/src/coverlet.msbuild.tasks/CoverageResultTask.cs index c37e59859..0552207fa 100644 --- a/src/coverlet.msbuild.tasks/CoverageResultTask.cs +++ b/src/coverlet.msbuild.tasks/CoverageResultTask.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -17,7 +18,7 @@ public class CoverageResultTask : Task { private string _output; private string _format; - private double _threshold; + private string _threshold; private string _thresholdType; private string _thresholdStat; private ITaskItem _instrumenterState; @@ -38,7 +39,7 @@ public string OutputFormat } [Required] - public double Threshold + public string Threshold { get { return _threshold; } set { _threshold = value; } @@ -130,24 +131,51 @@ public override bool Execute() } var thresholdTypeFlags = ThresholdTypeFlags.None; - var thresholdStat = ThresholdStatistic.Minimum; - + var thresholdTypeFlagQueue = new Queue(); foreach (var thresholdType in _thresholdType.Split(',').Select(t => t.Trim())) { if (thresholdType.Equals("line", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Line; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Line); } else if (thresholdType.Equals("branch", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Branch; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Branch); } else if (thresholdType.Equals("method", StringComparison.OrdinalIgnoreCase)) { thresholdTypeFlags |= ThresholdTypeFlags.Method; + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Method ); + } + } + + Dictionary thresholdTypeFlagValues = new Dictionary(); + if (_threshold.Contains(',')) + { + var thresholdValues = _threshold.Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()); + if(thresholdValues.Count() != thresholdTypeFlagQueue.Count()) + { + throw new Exception($"Threshold type flag count ({thresholdTypeFlagQueue.Count()}) and values count ({thresholdValues.Count()}) doesnt match"); + } + + foreach (var threshold in thresholdValues) + { + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = double.Parse(threshold); + } + } + else + { + double thresholdValue = double.Parse(_threshold); + + while (thresholdTypeFlagQueue.Any()) + { + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = thresholdValue; } } + var thresholdStat = ThresholdStatistic.Minimum; if (_thresholdStat.Equals("average", StringComparison.OrdinalIgnoreCase)) { thresholdStat = ThresholdStatistic.Average; @@ -194,23 +222,23 @@ public override bool Execute() Console.WriteLine(coverageTable.ToStringAlternative()); - thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, _threshold, thresholdTypeFlags, thresholdStat); + thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStat); if (thresholdTypeFlags != ThresholdTypeFlags.None) { var exceptionMessageBuilder = new StringBuilder(); if ((thresholdTypeFlags & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} line coverage is below the specified {_threshold}"); + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} line coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Line]}"); } if ((thresholdTypeFlags & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} branch coverage is below the specified {_threshold}"); + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} branch coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Branch]}"); } if ((thresholdTypeFlags & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {_threshold}"); + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); } throw new Exception(exceptionMessageBuilder.ToString()); diff --git a/test/coverlet.core.tests/CoverageResultTests.cs b/test/coverlet.core.tests/CoverageResultTests.cs new file mode 100644 index 000000000..06809a91d --- /dev/null +++ b/test/coverlet.core.tests/CoverageResultTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Coverlet.Core; +using Coverlet.Core.Enums; +using Moq; +using Xunit; + +namespace Coverlet.Core.Tests +{ + public class CoverageResultTests + { + private Modules _modules; + + public CoverageResultTests() + { + Lines lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 1); + lines.Add(3, 1); + Branches branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); + branches.Add(new BranchInfo { Line = 2, Hits = 0, Offset = 1, Path = 0, Ordinal = 1 }); + + // System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests - 3/3 100% line 2/3 66.7% branch coverage + Methods methods = new Methods(); + var methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + // System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold - 0/2 0% line + methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = new Lines() + { + {1, 0}, + {2, 0}, + }; + + Classes classes = new Classes(); + classes.Add("Coverlet.Core.Tests.CoverageResultTests", methods); + // Methods - 1/2 (50%) + // Lines - 3/5 (60%) + // Branches - 2/3 (66.67%) + + Documents documents = new Documents(); + documents.Add("doc.cs", classes); + + _modules = new Modules(); + _modules.Add("module", documents); + } + + [Fact] + public void TestGetThresholdTypesBelowThresholdLine() + { + CoverageResult result = new CoverageResult(); + result.Modules = _modules; + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 90 }, + { ThresholdTypeFlags.Method, 10 }, + { ThresholdTypeFlags.Branch, 10 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Line, resThresholdTypeFlags); + } + + [Fact] + public void TestGetThresholdTypesBelowThresholdMethod() + { + CoverageResult result = new CoverageResult(); + result.Modules = _modules; + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 50 }, + { ThresholdTypeFlags.Method, 75 }, + { ThresholdTypeFlags.Branch, 10 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Method, resThresholdTypeFlags); + } + + [Fact] + public void TestGetThresholdTypesBelowThresholdBranch() + { + CoverageResult result = new CoverageResult(); + result.Modules = _modules; + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 50 }, + { ThresholdTypeFlags.Method, 50 }, + { ThresholdTypeFlags.Branch, 90 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Total; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Branch, resThresholdTypeFlags); + } + + [Fact] + public void TestGetThresholdTypesBelowThresholdAllGood() + { + CoverageResult result = new CoverageResult(); + result.Modules = _modules; + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 50 }, + { ThresholdTypeFlags.Method, 50 }, + { ThresholdTypeFlags.Branch, 50 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Average; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.None, resThresholdTypeFlags); + } + + [Fact] + public void TestGetThresholdTypesBelowThresholdAllFail() + { + CoverageResult result = new CoverageResult(); + result.Modules = _modules; + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 100 }, + { ThresholdTypeFlags.Method, 100 }, + { ThresholdTypeFlags.Branch, 100 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdTypeFlags, thresholdStatic); + Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); + } + } +} \ No newline at end of file