diff --git a/action.yml b/action.yml index ad67fd2..9096f18 100644 --- a/action.yml +++ b/action.yml @@ -9,17 +9,29 @@ inputs: description: 'Code coverage file to analyse.' required: true badge: - description: 'Include a badge in the output - true / false (default).' + description: 'Include a Line Rate coverage badge in the output using shields.io - true / false (default).' + required: false + default: 'false' + fail_below_min: + description: 'Fail if overall Line Rate below lower threshold - true / false (default).' required: false default: 'false' format: description: 'Output Format - markdown or text (default).' required: false default: 'text' + indicators: + description: 'Include health indicators in the output - true (default) / false.' + required: false + default: 'true' output: description: 'Output Type - console (default), file or both.' required: false default: 'console' + thresholds: + description: 'Threshold percentages for badge and health indicators, lower threshold can also be used to fail the action.' + required: false + default: '50 75' runs: using: 'docker' image: 'docker://ghcr.io/irongut/codecoveragesummary:v1.0.5' @@ -27,7 +39,13 @@ runs: - ${{ inputs.filename }} - '--badge' - ${{ inputs.badge }} + - '--fail' + - ${{ inputs.fail_below_min }} - '--format' - ${{ inputs.format }} + - '--indicators' + - ${{ inputs.indicators }} - '--output' - ${{ inputs.output }} + - '--thresholds' + - ${{ inputs.thresholds }} diff --git a/src/CodeCoverageSummary/CommandLineOptions.cs b/src/CodeCoverageSummary/CommandLineOptions.cs index 1b588c9..6ca33ec 100644 --- a/src/CodeCoverageSummary/CommandLineOptions.cs +++ b/src/CodeCoverageSummary/CommandLineOptions.cs @@ -1,4 +1,5 @@ using CommandLine; +using System; namespace CodeCoverageSummary { @@ -7,13 +8,28 @@ namespace CodeCoverageSummary [Value(index: 0, Required = true, HelpText = "Code coverage file to analyse.")] public string Filename { get; set; } - [Option(longName: "badge", Required = false, HelpText = "Include a badge reporting the Line Rate coverage in the output using shields.io - true or false.", Default = false)] - public bool Badge { get; set; } + [Option(longName: "badge", Required = false, HelpText = "Include a Line Rate coverage badge in the output using shields.io - true or false.", Default = "false")] + public string BadgeString { get; set; } + + public bool Badge => BadgeString.Equals("true", StringComparison.OrdinalIgnoreCase); + + [Option(longName: "fail", Required = false, HelpText = "Fail if overall Line Rate below lower threshold - true or false.", Default = "false")] + public string FailString { get; set; } + + public bool FailBelowThreshold => FailString.Equals("true", StringComparison.OrdinalIgnoreCase); [Option(longName: "format", Required = false, HelpText = "Output Format - markdown or text.", Default = "text")] public string Format { get; set; } + [Option(longName: "indicators", Required = false, HelpText = "Include health indicators in the output - true or false.", Default = "true")] + public string IndicatorsString { get; set; } + + public bool Indicators => IndicatorsString.Equals("true", StringComparison.OrdinalIgnoreCase); + [Option(longName: "output", Required = false, HelpText = "Output Type - console, file or both.", Default = "console")] public string Output { get; set; } + + [Option(longName: "thresholds", Required = false, HelpText = "Threshold percentages for badge and health indicators, lower threshold can also be used to fail the action.", Default = "50 75")] + public string Thresholds { get; set; } } } diff --git a/src/CodeCoverageSummary/GlobalSuppressions.cs b/src/CodeCoverageSummary/GlobalSuppressions.cs index 920e60b..8163dc6 100644 --- a/src/CodeCoverageSummary/GlobalSuppressions.cs +++ b/src/CodeCoverageSummary/GlobalSuppressions.cs @@ -6,3 +6,4 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Performance", "RCS1197:Optimize StringBuilder.Append/AppendLine call.", Scope = "type", Target = "~T:CodeCoverageSummary.Program")] +[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Scope = "member", Target = "~M:CodeCoverageSummary.Program.SetThresholds(System.String)")] diff --git a/src/CodeCoverageSummary/Program.cs b/src/CodeCoverageSummary/Program.cs index 27b4c2b..ad0e30b 100644 --- a/src/CodeCoverageSummary/Program.cs +++ b/src/CodeCoverageSummary/Program.cs @@ -9,6 +9,9 @@ namespace CodeCoverageSummary { internal static class Program { + private static double lowerThreshold = 0.5; + private static double upperThreshold = 0.75; + private static int Main(string[] args) { return Parser.Default.ParseArguments(args) @@ -32,6 +35,10 @@ namespace CodeCoverageSummary } else { + // set health badge thresholds + if (!string.IsNullOrWhiteSpace(o.Thresholds)) + SetThresholds(o.Thresholds); + // generate badge string badgeUrl = o.Badge ? GenerateBadge(summary) : null; @@ -41,12 +48,16 @@ namespace CodeCoverageSummary if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase)) { fileExt = "txt"; - output = GenerateTextOutput(summary, badgeUrl); + output = GenerateTextOutput(summary, badgeUrl, o.Indicators); + if (o.FailBelowThreshold) + output += $"Minimum allowed line rate is {lowerThreshold * 100:N0}%{Environment.NewLine}"; } else if (o.Format.Equals("md", StringComparison.OrdinalIgnoreCase) || o.Format.Equals("markdown", StringComparison.OrdinalIgnoreCase)) { fileExt = "md"; - output = GenerateMarkdownOutput(summary, badgeUrl); + output = GenerateMarkdownOutput(summary, badgeUrl, o.Indicators); + if (o.FailBelowThreshold) + output += $"{Environment.NewLine}_Minimum allowed line rate is `{lowerThreshold * 100:N0}%`_{Environment.NewLine}"; } else { @@ -74,6 +85,12 @@ namespace CodeCoverageSummary return -2; // error } + if (o.FailBelowThreshold && summary.LineRate < lowerThreshold) + { + Console.WriteLine($"FAIL: Overall line rate below minimum threshold of {lowerThreshold * 100:N0}%."); + return -2; + } + return 0; // success } } @@ -158,14 +175,49 @@ namespace CodeCoverageSummary } } + private static void SetThresholds(string thresholds) + { + int lowerPercentage; + int upperPercentage = (int)(upperThreshold * 100); + int s = thresholds.IndexOf(" "); + if (s == 0) + { + throw new ArgumentException("Threshold parameter set incorrectly."); + } + else if (s < 0) + { + if (!int.TryParse(thresholds, out lowerPercentage)) + throw new ArgumentException("Threshold parameter set incorrectly."); + } + else + { + if (!int.TryParse(thresholds.Substring(0, s), out lowerPercentage)) + throw new ArgumentException("Threshold parameter set incorrectly."); + + if (!int.TryParse(thresholds.Substring(s + 1), out upperPercentage)) + throw new ArgumentException("Threshold parameter set incorrectly."); + } + lowerThreshold = lowerPercentage / 100.0; + upperThreshold = upperPercentage / 100.0; + + if (lowerThreshold > 1.0) + lowerThreshold = 1.0; + + if (lowerThreshold > upperThreshold) + upperThreshold = lowerThreshold + 0.1; + + if (upperThreshold > 1.0) + upperThreshold = 1.0; + } + private static string GenerateBadge(CodeSummary summary) { string colour; - if (summary.LineRate < 0.5) + if (summary.LineRate < lowerThreshold) { colour = "critical"; } - else if (summary.LineRate < 0.75) + else if (summary.LineRate < upperThreshold) { colour = "yellow"; } @@ -176,78 +228,75 @@ namespace CodeCoverageSummary return $"https://img.shields.io/badge/Code%20Coverage-{summary.LineRate * 100:N0}%25-{colour}?style=flat"; } - private static string GenerateTextOutput(CodeSummary summary, string badgeUrl) + private static string GenerateHealthIndicator(double rate) + { + if (rate < lowerThreshold) + { + return "❌"; + } + else if (rate < upperThreshold) + { + return "➖"; + } + else + { + return "✔"; + } + } + + private static string GenerateTextOutput(CodeSummary summary, string badgeUrl, bool indicators) { StringBuilder textOutput = new(); if (!string.IsNullOrWhiteSpace(badgeUrl)) { - textOutput.AppendLine(badgeUrl); - } - - textOutput.AppendLine($"Line Rate = {summary.LineRate * 100:N0}%, Lines Covered = {summary.LinesCovered} / {summary.LinesValid}") - .AppendLine($"Branch Rate = {summary.BranchRate * 100:N0}%, Branches Covered = {summary.BranchesCovered} / {summary.BranchesValid}"); - - if (summary.Complexity % 1 == 0) - { - textOutput.AppendLine($"Complexity = {summary.Complexity}"); - } - else - { - textOutput.AppendLine($"Complexity = {summary.Complexity:N4}"); + textOutput.AppendLine(badgeUrl) + .AppendLine(); } foreach (CodeCoverage package in summary.Packages) { - if (package.Complexity % 1 == 0) - { - textOutput.AppendLine($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%, Branch Rate = {package.BranchRate * 100:N0}%, Complexity = {package.Complexity}"); - } - else - { - textOutput.AppendLine($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%, Branch Rate = {package.BranchRate * 100:N0}%, Complexity = {package.Complexity:N4}"); - } + textOutput.Append($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%") + .Append($", Branch Rate = {package.BranchRate * 100:N0}%") + .Append((package.Complexity % 1 == 0) ? $", Complexity = {package.Complexity}" : $", Complexity = {package.Complexity:N4}") + .AppendLine(indicators ? $", {GenerateHealthIndicator(package.LineRate)}" : string.Empty); } + textOutput.Append($"Summary: Line Rate = {summary.LineRate * 100:N0}% ({summary.LinesCovered} / {summary.LinesValid})") + .Append($", Branch Rate = {summary.BranchRate * 100:N0}% ({summary.BranchesCovered} / {summary.BranchesValid})") + .Append((summary.Complexity % 1 == 0) ? $", Complexity = {summary.Complexity}" : $", Complexity = {summary.Complexity:N4}") + .AppendLine(indicators ? $", {GenerateHealthIndicator(summary.LineRate)}" : string.Empty); + return textOutput.ToString(); } - private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl) + private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl, bool indicators) { StringBuilder markdownOutput = new(); if (!string.IsNullOrWhiteSpace(badgeUrl)) { - markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})"); - markdownOutput.AppendLine(""); + markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})") + .AppendLine(); } - markdownOutput.AppendLine("Package | Line Rate | Branch Rate | Complexity") - .AppendLine("-------- | --------- | ----------- | ----------"); + markdownOutput.Append("Package | Line Rate | Branch Rate | Complexity") + .AppendLine(indicators ? " | Health" : string.Empty) + .Append("-------- | --------- | ----------- | ----------") + .AppendLine(indicators ? " | ------" : string.Empty); foreach (CodeCoverage package in summary.Packages) { - if (package.Complexity % 1 == 0) - { - markdownOutput.AppendLine($"{package.Name} | {package.LineRate * 100:N0}% | {package.BranchRate * 100:N0}% | {package.Complexity}"); - } - else - { - markdownOutput.AppendLine($"{package.Name} | {package.LineRate * 100:N0}% | {package.BranchRate * 100:N0}% | {package.Complexity:N4}"); - } + markdownOutput.Append($"{package.Name} | {package.LineRate * 100:N0}%") + .Append($" | {package.BranchRate * 100:N0}%") + .Append((package.Complexity % 1 == 0) ? $" | {package.Complexity}" : $" | {package.Complexity:N4}" ) + .AppendLine(indicators ? $" | {GenerateHealthIndicator(package.LineRate)}" : string.Empty); } - markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid}) | ") - .Append($"**{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid}) | "); - - if (summary.Complexity % 1 == 0) - { - markdownOutput.AppendLine(summary.Complexity.ToString()); - } - else - { - markdownOutput.AppendLine(summary.Complexity.ToString("N4")); - } + markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid})") + .Append($" | **{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid})") + .Append((summary.Complexity % 1 == 0) ? $" | {summary.Complexity}" : $" | {summary.Complexity:N4}") + .AppendLine(indicators ? $" | {GenerateHealthIndicator(summary.LineRate)}" : string.Empty); return markdownOutput.ToString(); } diff --git a/src/CodeCoverageSummary/Properties/launchSettings.json b/src/CodeCoverageSummary/Properties/launchSettings.json index 77ec29b..25f7867 100644 --- a/src/CodeCoverageSummary/Properties/launchSettings.json +++ b/src/CodeCoverageSummary/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CodeCoverageSummary": { "commandName": "Project", - "commandLineArgs": "../../../../coverage.gcovr.xml --format=md --badge=true" + "commandLineArgs": "../../../../coverage.cobertura.xml --format=md --badge true --thresholds=\"85 90\" --fail true" }, "Docker": { "commandName": "Docker",