Files
code-coverage-summary/src/CodeCoverageSummary/Program.cs
T
2022-07-26 01:39:53 +01:00

368 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommandLine;
using Microsoft.Extensions.FileSystemGlobbing;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
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<CommandLineOptions>(args)
.MapResult(o =>
{
try
{
// use glob patterns to match files
Matcher matcher = new();
matcher.AddIncludePatterns(o.Files.ToArray());
IEnumerable<string> matchingFiles = matcher.GetResultsInFullPath(".");
if (matchingFiles?.Any() == false)
{
Console.WriteLine("Error: No files found matching glob pattern.");
return -2; // error
}
// check files exist
foreach (var file in matchingFiles)
{
if (!File.Exists(file))
{
Console.WriteLine($"Error: Coverage file not found - {file}.");
return -2; // error
}
}
// parse code coverage file
CodeSummary summary = new();
foreach (var file in matchingFiles)
{
Console.WriteLine($"Coverage File: {file}");
summary = ParseTestResults(file, summary);
}
if (summary == null)
return -2; // error
summary.LineRate /= matchingFiles.Count();
summary.BranchRate /= matchingFiles.Count();
if (summary.Packages.Count == 0)
{
Console.WriteLine("Parsing Error: No packages found in coverage files.");
return -2; // error
}
else
{
// hide branch rate if metrics missing
bool hideBranchRate = o.HideBranchRate;
if (summary.BranchRate == 0 && summary.BranchesCovered == 0 && summary.BranchesValid == 0)
hideBranchRate = true;
// set health badge thresholds
if (!string.IsNullOrWhiteSpace(o.Thresholds))
SetThresholds(o.Thresholds);
// generate badge
string badgeUrl = o.Badge ? GenerateBadge(summary) : null;
// generate output
string output;
string fileExt;
if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase))
{
fileExt = "txt";
output = GenerateTextOutput(summary, badgeUrl, o.Indicators, hideBranchRate, o.HideComplexity);
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, o.Indicators, hideBranchRate, o.HideComplexity);
if (o.FailBelowThreshold)
output += $"{Environment.NewLine}_Minimum allowed line rate is `{lowerThreshold * 100:N0}%`_{Environment.NewLine}";
}
else
{
Console.WriteLine("Error: Unknown output format.");
return -2; // error
}
// output
if (o.Output.Equals("console", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine(output);
}
else if (o.Output.Equals("file", StringComparison.OrdinalIgnoreCase))
{
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else if (o.Output.Equals("both", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine(output);
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else
{
Console.WriteLine("Error: Unknown output type.");
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
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.GetType()} - {ex.Message}");
return -3; // unhandled error
}
},
_ => -1); // invalid arguments
}
private static CodeSummary ParseTestResults(string filename, CodeSummary summary)
{
if (summary == null)
return null;
try
{
string rss = File.ReadAllText(filename);
var xdoc = XDocument.Parse(rss);
// test coverage for solution
var coverage = from item in xdoc.Descendants("coverage")
select item;
if (!coverage.Any())
throw new Exception("Coverage file invalid, data not found");
var lineR = from item in coverage.Attributes()
where item.Name == "line-rate"
select item;
if (!lineR.Any())
throw new Exception("Overall line rate not found");
summary.LineRate += double.Parse(lineR.First().Value);
var linesCovered = from item in coverage.Attributes()
where item.Name == "lines-covered"
select item;
if (!linesCovered.Any())
throw new Exception("Overall lines covered not found");
summary.LinesCovered += int.Parse(linesCovered.First().Value);
var linesValid = from item in coverage.Attributes()
where item.Name == "lines-valid"
select item;
if (!linesValid.Any())
throw new Exception("Overall lines valid not found");
summary.LinesValid += int.Parse(linesValid.First().Value);
var branchR = from item in coverage.Attributes()
where item.Name == "branch-rate"
select item;
if (branchR.Any())
{
summary.BranchRate += double.TryParse(branchR.First().Value, out double bRate) ? bRate : 0;
var branchesCovered = from item in coverage.Attributes()
where item.Name == "branches-covered"
select item;
summary.BranchesCovered += int.TryParse(branchesCovered?.First().Value ?? "0", out int bCovered) ? bCovered : 0;
var branchesValid = from item in coverage.Attributes()
where item.Name == "branches-valid"
select item;
summary.BranchesValid += int.TryParse(branchesValid?.First().Value ?? "0", out int bValid) ? bValid : 0;
}
// test coverage for individual packages
var packages = from item in coverage.Descendants("package")
select item;
if (!packages.Any())
throw new Exception("No package data found");
int i = 1;
foreach (var item in packages)
{
CodeCoverage packageCoverage = new()
{
Name = string.IsNullOrWhiteSpace(item.Attribute("name")?.Value) ? $"{Path.GetFileNameWithoutExtension(filename)} Package {i}" : item.Attribute("name").Value,
LineRate = double.Parse(item.Attribute("line-rate")?.Value ?? "0"),
BranchRate = double.TryParse(item.Attribute("branch-rate")?.Value ?? "0", out double bRate) ? bRate : 0,
Complexity = double.TryParse(item.Attribute("complexity")?.Value ?? "0", out double complex) ? complex : 0
};
summary.Packages.Add(packageCoverage);
summary.Complexity += packageCoverage.Complexity;
i++;
}
return summary;
}
catch (Exception ex)
{
Console.WriteLine($"Parsing Error: {ex.Message} - {filename}");
return null;
}
}
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.AsSpan(0, s), out lowerPercentage))
throw new ArgumentException("Threshold parameter set incorrectly.");
if (!int.TryParse(thresholds.AsSpan(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 < lowerThreshold)
{
colour = "critical";
}
else if (summary.LineRate < upperThreshold)
{
colour = "yellow";
}
else
{
colour = "success";
}
return $"https://img.shields.io/badge/Code%20Coverage-{summary.LineRate * 100:N0}%25-{colour}?style=flat";
}
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, bool hideBranchRate, bool hideComplexity)
{
StringBuilder textOutput = new();
if (!string.IsNullOrWhiteSpace(badgeUrl))
{
textOutput.AppendLine(badgeUrl)
.AppendLine();
}
foreach (CodeCoverage package in summary.Packages)
{
textOutput.Append($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%")
.Append(hideBranchRate ? string.Empty : $", Branch Rate = {package.BranchRate * 100:N0}%")
.Append(hideComplexity ? string.Empty : (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(hideBranchRate ? string.Empty : $", Branch Rate = {summary.BranchRate * 100:N0}% ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append(hideComplexity ? string.Empty : (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, bool indicators, bool hideBranchRate, bool hideComplexity)
{
StringBuilder markdownOutput = new();
if (!string.IsNullOrWhiteSpace(badgeUrl))
{
markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})")
.AppendLine();
}
markdownOutput.Append("Package | Line Rate")
.Append(hideBranchRate ? string.Empty : " | Branch Rate")
.Append(hideComplexity ? string.Empty : " | Complexity")
.AppendLine(indicators ? " | Health" : string.Empty)
.Append("-------- | ---------")
.Append(hideBranchRate ? string.Empty : " | -----------")
.Append(hideComplexity ? string.Empty : " | ----------")
.AppendLine(indicators ? " | ------" : string.Empty);
foreach (CodeCoverage package in summary.Packages)
{
markdownOutput.Append($"{package.Name} | {package.LineRate * 100:N0}%")
.Append(hideBranchRate ? string.Empty : $" | {package.BranchRate * 100:N0}%")
.Append(hideComplexity ? string.Empty : (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(hideBranchRate ? string.Empty : $" | **{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append(hideComplexity ? string.Empty : (summary.Complexity % 1 == 0) ? $" | **{summary.Complexity}**" : $" | **{summary.Complexity:N4}**")
.AppendLine(indicators ? $" | {GenerateHealthIndicator(summary.LineRate)}" : string.Empty);
return markdownOutput.ToString();
}
}
}