< Summary

Information
Class: DirectSight.Reporting.Builders.Rendering.HtmlRenderer
Assembly DirectSight
File(s): /home/runner/work/DirectSight/DirectSight/DirectSight/Reporting/Builders/Rendering/HtmlRenderer.cs
Line coverage
82%
Covered lines: 830
Uncovered lines: 171
Coverable lines: 1001
Total lines: 1466
Line coverage: 82.9%
Branch coverage
71%
Covered branches: 197
Total branches: 274
Branch coverage: 71.8%
Method coverage

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
.ctor(...)0%220%
.ctor(...)50%22100%
BeginSummaryReport(...)50%4470%
BeginClassReport(...)100%11100%
Cards(...)100%1818100%
Header(...)100%11100%
HeaderWithGithubLinks(...)100%11100%
HeaderWithBackLink(...)100%11100%
TestMethods(...)71.42%565697.46%
File(...)100%11100%
Paragraph(...)100%11100%
BeginSummaryTable()100%11100%
FinishSummaryTable()100%11100%
BeginSummaryTable(...)100%66100%
CustomSummary(...)42.85%282862.6%
BeginLineAnalysisTable(...)100%22100%
MetricsTable(...)83.33%242484.81%
LineAnalysis(...)100%1818100%
FinishTable()100%11100%
BeginRiskHotspots()100%11100%
FinishRiskHotspots()100%11100%
RiskHotspots(...)0%20200%
SummaryAssembly(...)60%1010100%
SummaryClass(...)100%1212100%
AddFooter()100%11100%
SaveSummaryReport(...)100%11100%
SaveClassReport(...)100%22100%
Dispose()100%11100%
Dispose(...)75%44100%
WriteCoverageTable(...)100%66100%
ConvertToCssClass(...)100%1212100%
GetTooltip(...)100%1212100%
WriteHtmlStart(...)100%11100%
GetClassReportFilename(...)43.75%161648.78%
SaveCss(...)75%4490.47%
SaveJavaScript(...)100%44100%
WriteCss(...)83.33%66100%
WriteCombinedJavascript(...)100%22100%
CreateTextWriter(...)100%11100%
SaveReport()100%11100%
FinishReport()100%22100%
.cctor()100%11100%
Get(...)100%11100%
.cctor()100%11100%
Get()100%11100%
Return(...)100%11100%
ToStringAndReturnToPool(...)100%11100%

File(s)

/home/runner/work/DirectSight/DirectSight/DirectSight/Reporting/Builders/Rendering/HtmlRenderer.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Linq;
 6using System.Net;
 7using System.Text;
 8using System.Text.Encodings.Web;
 9using System.Text.RegularExpressions;
 10using Microsoft.Extensions.ObjectPool;
 11using DirectSight.CodeAnalysis;
 12using DirectSight.Common;
 13using DirectSight.Logging;
 14using DirectSight.Parser.Analysis;
 15
 16namespace DirectSight.Reporting.Builders.Rendering;
 17
 18/// <summary>
 19/// HTML report renderer.
 20/// </summary>
 21internal class HtmlRenderer : IHtmlRenderer, IDisposable
 22{
 23    /// <summary>
 24    /// The link to the static CSS file.
 25    /// </summary>
 26    private const string CssLink = "<link rel=\"stylesheet\" type=\"text/css\" href=\"report.css\" />";
 27
 28    /// <summary>
 29    /// Indicates whether which CSS and Javascript files have already been written.
 30    /// </summary>
 131    private static readonly HashSet<string> WrittenCssAndJavascriptFiles = [];
 32
 33    /// <summary>
 34    /// Dictionary containing the filenames of the class reports by class.
 35    /// </summary>
 36    private readonly IDictionary<string, string> fileNameByClass;
 37
 38    /// <summary>
 39    /// Indicates that only a summary report is created (no class reports).
 40    /// </summary>
 41    private readonly bool onlySummary;
 42
 43    /// <summary>
 44    /// Contains report specific JavaScript content.
 45    /// </summary>
 46    private readonly StringBuilder javaScriptContent;
 47
 48    /// <summary>
 49    /// The css file resource.
 50    /// </summary>
 51    private readonly string cssFileResource;
 52
 53    /// <summary>
 54    /// Optional additional CSS file resources.
 55    /// </summary>
 56    private readonly string[] additionalCssFileResources;
 57
 58    /// <summary>
 59    /// Indicates that JavaScript was generated.
 60    /// </summary>
 61    private bool javaScriptGenerated;
 62
 63    /// <summary>
 64    /// The report builder.
 65    /// </summary>
 66    private TextWriter reportTextWriter;
 67
 68    /// <summary>
 69    /// Indicates that that a class report is created (not the summary page).
 70    /// </summary>
 71    private bool classReport;
 72
 73    /// <summary>
 74    /// Initializes a new instance of the <see cref="HtmlRenderer" /> class.
 75    /// </summary>
 76    /// <param name="fileNameByClass">Dictionary containing the filenames of the class reports by class.</param>
 77    /// <param name="onlySummary">if set to <c>true</c> only a summary report is created (no class reports).</param>
 78    /// <param name="cssFileResource">Optional CSS file resource.</param>
 79    /// <param name="additionalCssFileResource">Optional additional CSS file resource.</param>
 080    internal HtmlRenderer(
 081        IDictionary<string, string> fileNameByClass,
 082        bool onlySummary,
 083        string cssFileResource = "custom.css",
 084        string additionalCssFileResource = "custom_adaptive.css")
 085    {
 086        this.fileNameByClass = fileNameByClass;
 087        this.onlySummary = onlySummary;
 088        this.javaScriptContent = StringBuilderCache.Get();
 089        this.cssFileResource = cssFileResource;
 090        this.additionalCssFileResources = additionalCssFileResource == null ? [] : [additionalCssFileResource];
 091    }
 92
 93    /// <summary>
 94    /// Initializes a new instance of the <see cref="HtmlRenderer" /> class.
 95    /// </summary>
 96    /// <param name="fileNameByClass">Dictionary containing the filenames of the class reports by class.</param>
 97    /// <param name="onlySummary">if set to <c>true</c> only a summary report is created (no class reports).</param>
 98    /// <param name="additionalCssFileResources">Optional additional CSS file resources.</param>
 99    /// <param name="cssFileResource">Optional CSS file resource.</param>
 38100    internal HtmlRenderer(
 38101        IDictionary<string, string> fileNameByClass,
 38102        bool onlySummary,
 38103        string[] additionalCssFileResources,
 38104        string cssFileResource = "custom.css")
 38105    {
 38106        this.fileNameByClass = fileNameByClass;
 38107        this.onlySummary = onlySummary;
 38108        this.javaScriptContent = StringBuilderCache.Get();
 38109        this.additionalCssFileResources = additionalCssFileResources ?? [];
 38110        this.cssFileResource = cssFileResource;
 38111    }
 112
 113    /// <inheritdoc />
 114    public void BeginSummaryReport(string targetDirectory, string fileName, string title)
 2115    {
 2116        string targetPath = Path.Combine(targetDirectory, this.onlySummary ? "summary.html" : "index.html");
 117
 2118        if (fileName != null)
 0119        {
 0120            targetPath = Path.Combine(targetDirectory, fileName);
 0121        }
 122
 2123        ConsoleLogger.Info("Writing report file '{0}'", targetPath);
 2124        this.CreateTextWriter(targetPath);
 125
 2126        WriteHtmlStart(this.reportTextWriter, title, "Coverage Report");
 2127    }
 128
 129    /// <inheritdoc />
 130    public void BeginClassReport(string targetDirectory, Assembly assembly, string className, string classDisplayName, s
 36131    {
 36132        this.classReport = true;
 133
 36134        string targetPath = this.GetClassReportFilename(assembly, className);
 135
 36136        ConsoleLogger.Debug("Writing report file '{0}'", targetPath);
 36137        this.CreateTextWriter(Path.Combine(targetDirectory, targetPath));
 138
 36139        WriteHtmlStart(this.reportTextWriter, classDisplayName, additionalTitle + "Coverage Report");
 36140    }
 141
 142    /// <inheritdoc />
 143    public void Cards(IEnumerable<Card> cards)
 74144    {
 74145        this.reportTextWriter.WriteLine("<div class=\"card-group\">");
 146
 526147        foreach (var card in cards)
 152148        {
 152149            this.reportTextWriter.WriteLine("<div class=\"card\">");
 150
 152151            if (!string.IsNullOrWhiteSpace(card.Title))
 152152            {
 152153                this.reportTextWriter.WriteLine("<div class=\"card-header\">{0}</div>", card.Title);
 152154            }
 155
 152156            this.reportTextWriter.WriteLine("<div class=\"card-body\">");
 152157            if (!string.IsNullOrWhiteSpace(card.SubTitle))
 76158            {
 76159                string clazz = string.Empty;
 160
 76161                if (card.SubTitlePercentage.HasValue)
 44162                {
 44163                    int uncovered = 100 - (int)Math.Round(card.SubTitlePercentage.Value, 0);
 164
 44165                    clazz = $" cardpercentagebar cardpercentagebar{uncovered}";
 44166                }
 167
 76168                this.reportTextWriter.WriteLine("<div class=\"large{0}\">{1}</div>", clazz, WebUtility.HtmlEncode(card.S
 76169            }
 170
 152171            this.reportTextWriter.WriteLine("<div class=\"table\">");
 152172            this.reportTextWriter.WriteLine("<table>");
 173
 1296174            foreach (var row in card.Rows)
 420175            {
 420176                this.reportTextWriter.WriteLine("<tr>");
 420177                this.reportTextWriter.WriteLine("<th>{0}</th>", WebUtility.HtmlEncode(row.Header));
 420178                if (row.Links != null)
 36179                {
 36180                    int fileNumber = 1;
 181
 36182                    bool usePrefix = row.Links.Count > 1;
 183
 36184                    string value = string.Join(
 36185                        "<br />",
 72186                        row.Links.Select(v => string.Format(
 72187                            "<a href=\"#{0}\" class=\"navigatetohash\">{1}</a>",
 72188                            WebUtility.HtmlEncode(StringHelper.ReplaceNonLetterChars(v)),
 72189                            WebUtility.HtmlEncode((usePrefix ? $"File {fileNumber++}: " : string.Empty) + v))));
 190
 36191                    this.reportTextWriter.WriteLine(
 36192                        "<td class=\"overflow-wrap\">{0}</td>",
 36193                        value);
 36194                }
 195                else
 384196                {
 384197                    this.reportTextWriter.WriteLine(
 384198                        "<td class=\"limit-width {0}\" title=\"{1}\">{2}</td>",
 384199                        row.Alignment == CardLineItemAlignment.Right ? "right" : string.Empty,
 384200                        WebUtility.HtmlEncode(row.Tooltip ?? row.Text),
 384201                        WebUtility.HtmlEncode(row.Text));
 384202                }
 203
 420204                this.reportTextWriter.WriteLine("</tr>");
 420205            }
 206
 152207            this.reportTextWriter.WriteLine("</table>");
 152208            this.reportTextWriter.WriteLine("</div>");
 209
 152210            this.reportTextWriter.WriteLine("</div>");
 152211            this.reportTextWriter.WriteLine("</div>");
 152212        }
 213
 74214        this.reportTextWriter.WriteLine("</div>");
 74215    }
 216
 217    /// <inheritdoc />
 218    public void Header(string text)
 68219    {
 68220        this.reportTextWriter.WriteLine("<h1>{0}</h1>", WebUtility.HtmlEncode(text));
 68221    }
 222
 223    /// <inheritdoc />
 224    public void HeaderWithGithubLinks(string text)
 2225    {
 2226        this.reportTextWriter.WriteLine(
 2227            "<h1>{0}<a class=\"button\" href=\"https://github.com/joharasmus/DirectSight\" title=\"{1}\"><i class=\"icon
 2228            WebUtility.HtmlEncode(text),
 2229            WebUtility.HtmlEncode("Star on GitHub"),
 2230            WebUtility.HtmlEncode("Star"));
 2231    }
 232
 233    /// <inheritdoc />
 234    public void HeaderWithBackLink(string text)
 36235    {
 36236        this.reportTextWriter.WriteLine("<h1><a href=\"index.html\" class=\"back\">&lt;</a> {0}</h1>", WebUtility.HtmlEn
 36237    }
 238
 239    /// <inheritdoc />
 240    public void TestMethods(
 241        IEnumerable<TestMethod> testMethods,
 242        IEnumerable<FileAnalysis> fileAnalyses,
 243        IDictionary<int, IEnumerable<CodeElement>> codeElementsByFileIndex)
 32244    {
 32245        ArgumentNullException.ThrowIfNull(testMethods);
 246
 32247        if (!testMethods.Any() && codeElementsByFileIndex.Count == 0)
 0248        {
 0249            return;
 250        }
 251
 252        // Close 'containerleft' and begin 'containerright'
 32253        this.reportTextWriter.WriteLine("</div>");
 32254        this.reportTextWriter.WriteLine("<div class=\"containerright\">");
 32255        this.reportTextWriter.WriteLine("<div class=\"containerrightfixed\">");
 256
 32257        if (testMethods.Any())
 26258        {
 26259            this.reportTextWriter.WriteLine("<h1>{0}</h1>", WebUtility.HtmlEncode("Coverage by test methods"));
 260
 1160261            int coverableLines = fileAnalyses.SafeSum(f => f.Lines.Count(l => l.LineVisitStatus != LineVisitStatus.NotCo
 1160262            int coveredLines = fileAnalyses.SafeSum(f => f.Lines.Count(l => l.LineVisitStatus > LineVisitStatus.NotCover
 26263            decimal? coverage = (coverableLines == 0) ? null : MathExtensions.CalculatePercentage(coveredLines, coverabl
 264
 26265            int? coverageRounded = null;
 266
 26267            if (coverage.HasValue)
 26268            {
 26269                coverageRounded = (int)coverage.Value;
 26270                coverageRounded -= coverageRounded % 10;
 26271            }
 272
 26273            this.reportTextWriter.WriteLine(
 26274                "<label id=\"AllTestMethods\" class=\"testmethod percentagebar percentagebar{0}\" title=\"{1}{2}\">" +
 26275                "<input type=\"radio\" name=\"method\" value=\"AllTestMethods\" class=\"switchtestmethod\" checked=\"che
 26276                coverage.HasValue ? coverageRounded.ToString() : "undefined",
 26277                coverage.HasValue ? "Line coverage: " + coverage.Value.ToString() + "% - " : string.Empty,
 26278                WebUtility.HtmlEncode("All"));
 279
 182280            foreach (var testMethod in testMethods)
 52281            {
 52282                coveredLines = fileAnalyses.SafeSum(
 112283                    f => f.Lines.Count(
 2320284                        l => l.LineCoverageByTestMethod.ContainsKey(testMethod)
 2320285                        && l.LineCoverageByTestMethod[testMethod].LineVisitStatus > LineVisitStatus.NotCovered));
 286
 52287                coverage = (coverableLines == 0) ? null : MathExtensions.CalculatePercentage(coveredLines, coverableLine
 288
 52289                coverageRounded = null;
 290
 52291                if (coverage.HasValue)
 52292                {
 52293                    coverageRounded = (int)coverage.Value;
 52294                    coverageRounded -= coverageRounded % 10;
 52295                }
 296
 52297                this.reportTextWriter.WriteLine(
 52298                    "<br /><label id=\"M{3}\" class=\"testmethod percentagebar percentagebar{0}\" title=\"{1}{2}\">"
 52299                    + "<input type=\"radio\" name=\"method\" value=\"M{3}\" class=\"switchtestmethod\" />{4}</label>",
 52300                    coverage.HasValue ? coverageRounded.ToString() : "undefined",
 52301                    coverage.HasValue ? "Line coverage: " + coverage.Value.ToString() + "% - " : string.Empty,
 52302                    WebUtility.HtmlEncode(testMethod.Name),
 52303                    testMethod.Id,
 52304                    WebUtility.HtmlEncode(testMethod.ShortName));
 52305            }
 26306        }
 307
 32308        if (codeElementsByFileIndex.Count > 0)
 32309        {
 32310            this.reportTextWriter.WriteLine("<h1>{0}</h1>", WebUtility.HtmlEncode("Methods/Properties"));
 311
 168312            foreach (var item in codeElementsByFileIndex)
 36313            {
 328314                foreach (var codeElement in item.Value)
 110315                {
 110316                    int? coverageRounded = null;
 317
 110318                    if (codeElement.CoverageQuota.HasValue)
 110319                    {
 110320                        coverageRounded = (int)codeElement.CoverageQuota.Value;
 110321                        coverageRounded -= coverageRounded % 10;
 110322                    }
 323
 110324                    string prefix = fileAnalyses.Count() > 1 ? $"File {item.Key + 1}: " : string.Empty;
 110325                    this.reportTextWriter.WriteLine(
 110326                        "<a href=\"#file{0}_line{1}\" class=\"navigatetohash percentagebar percentagebar{2}\" title=\"{3
 110327                        + "<i class=\"icon-{5}\"></i>{4}</a><br />",
 110328                        item.Key,
 110329                        codeElement.FirstLine,
 110330                        codeElement.CoverageQuota.HasValue ? coverageRounded.ToString() : "undefined",
 110331                        prefix + (codeElement.CoverageQuota.HasValue
 110332                            ? "Line coverage: " + codeElement.CoverageQuota.Value.ToString() + "% - "
 110333                            : string.Empty),
 110334                        WebUtility.HtmlEncode(codeElement.Name),
 110335                        codeElement.CodeElementType == CodeElementType.Method ? "cube" : "wrench");
 110336                }
 36337            }
 32338        }
 339
 32340        this.reportTextWriter.WriteLine("<br/></div>");
 32341    }
 342
 343    /// <inheritdoc />
 344    public void File(string path)
 36345    {
 36346        this.reportTextWriter.WriteLine(
 36347            "<h2 id=\"{0}\">{1}</h2>",
 36348            WebUtility.HtmlEncode(StringHelper.ReplaceNonLetterChars(path)),
 36349            WebUtility.HtmlEncode(path));
 36350    }
 351
 352    /// <inheritdoc />
 353    public void Paragraph(string text)
 6354    {
 6355        this.reportTextWriter.WriteLine("<p>{0}</p>", WebUtility.HtmlEncode(text));
 6356    }
 357
 358    /// <inheritdoc />
 359    public void BeginSummaryTable()
 2360    {
 2361        this.reportTextWriter.WriteLine("<coverage-info>");
 2362    }
 363
 364    /// <inheritdoc />
 365    public void FinishSummaryTable()
 2366    {
 2367        this.reportTextWriter.WriteLine("</coverage-info>");
 2368    }
 369
 370    /// <inheritdoc />
 371    public void BeginSummaryTable(bool branchCoverageAvailable)
 2372    {
 2373        this.reportTextWriter.WriteLine("<div class=\"table-responsive\">");
 2374        this.reportTextWriter.WriteLine("<table class=\"overview table-fixed stripped\">");
 2375        this.reportTextWriter.WriteLine("<colgroup>");
 2376        this.reportTextWriter.WriteLine("<col class=\"column-min-200\" />");
 2377        this.reportTextWriter.WriteLine("<col class=\"column90\" />");
 2378        this.reportTextWriter.WriteLine("<col class=\"column105\" />");
 2379        this.reportTextWriter.WriteLine("<col class=\"column100\" />");
 2380        this.reportTextWriter.WriteLine("<col class=\"column70\" />");
 2381        this.reportTextWriter.WriteLine("<col class=\"column98\" />");
 2382        this.reportTextWriter.WriteLine("<col class=\"column112\" />");
 2383        if (branchCoverageAvailable)
 2384        {
 2385            this.reportTextWriter.WriteLine("<col class=\"column90\" />");
 2386            this.reportTextWriter.WriteLine("<col class=\"column70\" />");
 2387            this.reportTextWriter.WriteLine("<col class=\"column98\" />");
 2388            this.reportTextWriter.WriteLine("<col class=\"column112\" />");
 2389        }
 390
 2391        this.reportTextWriter.WriteLine("</colgroup>");
 392
 2393        this.reportTextWriter.WriteLine("<thead>");
 2394        this.reportTextWriter.Write("<tr class=\"header\">");
 2395        this.reportTextWriter.Write(
 2396            "<th></th><th colspan=\"6\" class=\"center\">{0}</th>",
 2397            WebUtility.HtmlEncode("Line coverage"));
 398
 2399        if (branchCoverageAvailable)
 2400        {
 2401            this.reportTextWriter.Write(
 2402                "<th colspan=\"4\" class=\"center\">{0}</th>",
 2403                WebUtility.HtmlEncode("Branch coverage"));
 2404        }
 405
 2406        this.reportTextWriter.WriteLine("</tr>");
 407
 2408        this.reportTextWriter.Write(
 2409            "<tr>"
 2410            + "<th>{0}</th>"
 2411            + "<th class=\"right\">{1}</th>"
 2412            + "<th class=\"right\">{2}</th>"
 2413            + "<th class=\"right\">{3}</th>"
 2414            + "<th class=\"right\">{4}</th>"
 2415            + "<th class=\"center\" colspan=\"2\">{5}</th>",
 2416            WebUtility.HtmlEncode("Name"),
 2417            WebUtility.HtmlEncode("Covered"),
 2418            WebUtility.HtmlEncode("Uncovered"),
 2419            WebUtility.HtmlEncode("Coverable"),
 2420            WebUtility.HtmlEncode("Total"),
 2421            WebUtility.HtmlEncode("Percentage"));
 422
 2423        if (branchCoverageAvailable)
 2424        {
 2425            this.reportTextWriter.Write(
 2426            "<th class=\"right\">{0}</th><th class=\"right\">{1}</th><th class=\"center\" colspan=\"2\">{2}</th>",
 2427            WebUtility.HtmlEncode("Covered"),
 2428            WebUtility.HtmlEncode("Total"),
 2429            WebUtility.HtmlEncode("Percentage"));
 2430        }
 431
 2432        this.reportTextWriter.WriteLine("</tr></thead>");
 2433        this.reportTextWriter.WriteLine("<tbody>");
 2434    }
 435
 436    /// <inheritdoc />
 437    public void CustomSummary(
 438        IEnumerable<Assembly> assemblies,
 439        IEnumerable<RiskHotspot> riskHotspots,
 440        bool branchCoverageAvailable)
 2441    {
 2442        ArgumentNullException.ThrowIfNull(assemblies);
 443
 2444        ArgumentNullException.ThrowIfNull(riskHotspots);
 445
 2446        if (this.classReport)
 0447        {
 0448            return;
 449        }
 450
 2451        this.javaScriptContent.AppendLine("var assemblies = [");
 452
 2453        var tagsByBistoricCoverageExecutionTime = new Dictionary<DateTime, string>();
 2454        var metricsByName = new Dictionary<string, Metric>();
 455
 10456        foreach (var assembly in assemblies)
 2457        {
 2458            this.javaScriptContent.AppendLine("  {");
 2459            this.javaScriptContent.AppendFormat("    \"name\": \"{0}\",", JavaScriptEncoder.Default.Encode(assembly.Name
 2460            this.javaScriptContent.AppendLine();
 2461            this.javaScriptContent.AppendLine("    \"classes\": [");
 462
 78463            foreach (var @class in assembly.Classes)
 36464            {
 465                void WriteMetricsCoverage()
 36466                {
 36467                    this.javaScriptContent.Append('{');
 468
 856469                    foreach (var metricGroup in @class.Files.SelectMany(f => f.MethodMetrics).SelectMany(m => m.Metrics)
 140470                    {
 140471                        var firstMetric = metricGroup.First();
 140472                        metricsByName[firstMetric.Name] = firstMetric;
 140473                    }
 474
 36475                    this.javaScriptContent.Append(" }");
 36476                }
 477
 36478                this.javaScriptContent.Append("      { ");
 36479                this.javaScriptContent.AppendFormat("\"name\": \"{0}\",", JavaScriptEncoder.Default.Encode(@class.Displa
 36480                this.javaScriptContent.AppendFormat(
 36481                    " \"rp\": \"{0}\",",
 36482                    this.onlySummary ? string.Empty : JavaScriptEncoder.Default.Encode(this.GetClassReportFilename(@clas
 36483                this.javaScriptContent.AppendFormat(" \"cl\": {0},", @class.CoveredLines.ToString());
 36484                this.javaScriptContent.AppendFormat(" \"ucl\": {0},", (@class.CoverableLines - @class.CoveredLines).ToSt
 36485                this.javaScriptContent.AppendFormat(" \"cal\": {0},", @class.CoverableLines.ToString());
 36486                this.javaScriptContent.AppendFormat(" \"tl\": {0},", @class.TotalLines.GetValueOrDefault().ToString());
 487
 36488                this.javaScriptContent.AppendFormat(" \"cb\": {0},", @class.CoveredBranches.GetValueOrDefault().ToString
 36489                this.javaScriptContent.AppendFormat(" \"tb\": {0},", @class.TotalBranches.GetValueOrDefault().ToString()
 36490                this.javaScriptContent.AppendFormat(" \"cm\": {0},", "0");
 36491                this.javaScriptContent.AppendFormat(" \"fcm\": {0},","0");
 36492                this.javaScriptContent.AppendFormat(" \"tm\": {0},", "0");
 493
 36494                this.javaScriptContent.Append(" \"hc\": ");
 36495                this.javaScriptContent.Append(',');
 496
 36497                this.javaScriptContent.Append(" \"metrics\": ");
 36498                WriteMetricsCoverage();
 499
 36500                this.javaScriptContent.AppendLine(" },");
 36501            }
 502
 2503            this.javaScriptContent.AppendLine("    ]},");
 2504        }
 505
 2506        this.javaScriptContent.AppendLine("];");
 507
 2508        this.javaScriptContent.AppendLine();
 509
 2510        this.javaScriptContent.Append("var metrics = [");
 2511        int metricAbbreviationCounter = 0;
 512
 26513        foreach (var item in metricsByName)
 10514        {
 10515            if (metricAbbreviationCounter++ > 0)
 8516            {
 8517                this.javaScriptContent.Append(", ");
 8518            }
 519
 10520            this.javaScriptContent.AppendFormat(
 10521                "{{ \"name\": \"{0}\", \"abbreviation\": \"{1}\", \"explanationUrl\": \"{2}\" }}",
 10522                JavaScriptEncoder.Default.Encode(item.Key),
 10523                JavaScriptEncoder.Default.Encode(item.Value.Abbreviation),
 10524                JavaScriptEncoder.Default.Encode(item.Value.ExplanationUrl.ToString()));
 10525        }
 526
 2527        this.javaScriptContent.AppendLine("];");
 528
 2529        this.javaScriptContent.AppendLine();
 530
 2531        this.javaScriptContent.AppendLine("];");
 532
 2533        this.javaScriptContent.AppendLine();
 534
 2535        this.javaScriptContent.AppendLine("var riskHotspotMetrics = [");
 536
 2537        if (riskHotspots.Any())
 0538        {
 0539            foreach (var metric in riskHotspots.First().StatusMetrics)
 0540            {
 0541                this.javaScriptContent.Append("      { ");
 0542                this.javaScriptContent.AppendFormat("\"name\": \"{0}\",", JavaScriptEncoder.Default.Encode(metric.Metric
 0543                this.javaScriptContent.AppendFormat(
 0544                    " \"explanationUrl\": \"{0}\"",
 0545                    JavaScriptEncoder.Default.Encode(metric.Metric.ExplanationUrl.ToString()));
 0546                this.javaScriptContent.AppendLine(" },");
 0547            }
 0548        }
 549
 2550        this.javaScriptContent.AppendLine("];");
 551
 2552        this.javaScriptContent.AppendLine();
 553
 2554        this.javaScriptContent.AppendLine("var riskHotspots = [");
 555
 6556        foreach (var riskHotspot in riskHotspots)
 0557        {
 0558            this.javaScriptContent.AppendLine("  {");
 0559            this.javaScriptContent.AppendFormat("    \"assembly\": \"{0}\",", JavaScriptEncoder.Default.Encode(riskHotsp
 0560            this.javaScriptContent.AppendFormat(" \"class\": \"{0}\",", JavaScriptEncoder.Default.Encode(riskHotspot.Cla
 0561            this.javaScriptContent.AppendFormat(
 0562                " \"reportPath\": \"{0}\",",
 0563                this.onlySummary
 0564                ? string.Empty
 0565                : JavaScriptEncoder.Default.Encode(this.GetClassReportFilename(riskHotspot.Assembly, riskHotspot.Class.N
 0566            this.javaScriptContent.AppendFormat(" \"methodName\": \"{0}\",", JavaScriptEncoder.Default.Encode(riskHotspo
 0567            this.javaScriptContent.AppendFormat(" \"methodShortName\": \"{0}\",", JavaScriptEncoder.Default.Encode(riskH
 0568            this.javaScriptContent.AppendFormat(" \"fileIndex\": {0},", riskHotspot.FileIndex);
 0569            this.javaScriptContent.AppendFormat(
 0570                " \"line\": {0},",
 0571                !this.onlySummary && riskHotspot.MethodMetric.Line.HasValue
 0572                ? riskHotspot.MethodMetric.Line.Value.ToString()
 0573                : "null");
 0574            this.javaScriptContent.AppendLine();
 0575            this.javaScriptContent.AppendLine("    \"metrics\": [");
 576
 0577            foreach (var metric in riskHotspot.StatusMetrics)
 0578            {
 0579                this.javaScriptContent.Append("      { ");
 0580                this.javaScriptContent.AppendFormat(
 0581                    "\"value\": {0},",
 0582                    metric.Metric.Value.HasValue ? metric.Metric.Value.Value.ToString("0.##") : "null");
 0583                this.javaScriptContent.AppendFormat(" \"exceeded\": {0}", metric.Exceeded.ToString().ToLowerInvariant())
 0584                this.javaScriptContent.AppendLine(" },");
 0585            }
 586
 0587            this.javaScriptContent.AppendLine("    ]},");
 0588        }
 589
 2590        this.javaScriptContent.AppendLine("];");
 591
 2592        this.javaScriptContent.AppendLine();
 593
 2594        this.javaScriptContent.AppendLine("var branchCoverageAvailable = " + branchCoverageAvailable.ToString().ToLowerI
 2595        this.javaScriptContent.AppendLine("var methodCoverageAvailable = false;");
 2596        this.javaScriptContent.AppendLine("var applyQueryStringToAllLinks = false;");
 2597        this.javaScriptContent.AppendLine("var applyMaximumGroupingLevel = false;");
 2598        this.javaScriptContent.AppendLine("var maximumDecimalPlacesForCoverageQuotas = " + MathExtensions.MaximumDecimal
 2599        this.javaScriptContent.AppendLine();
 2600    }
 601
 602    /// <inheritdoc />
 603    public void BeginLineAnalysisTable(IEnumerable<string> headers)
 36604    {
 36605        ArgumentNullException.ThrowIfNull(headers);
 606
 36607        this.reportTextWriter.WriteLine("<div class=\"table-responsive\">");
 36608        this.reportTextWriter.WriteLine("<table class=\"lineAnalysis\">");
 36609        this.reportTextWriter.Write("<thead><tr>");
 610
 468611        foreach (var header in headers)
 180612        {
 180613            this.reportTextWriter.Write("<th>{0}</th>", WebUtility.HtmlEncode(header));
 180614        }
 615
 36616        this.reportTextWriter.WriteLine("</tr></thead>");
 36617        this.reportTextWriter.WriteLine("<tbody>");
 36618    }
 619
 620    /// <inheritdoc />
 621    public void MetricsTable(Class @class)
 28622    {
 28623        ArgumentNullException.ThrowIfNull(@class);
 624
 28625        var metrics = @class.Files
 30626            .SelectMany(f => f.MethodMetrics)
 72627            .SelectMany(m => m.Metrics)
 28628            .Distinct()
 140629            .OrderBy(m => m.Name)
 28630            .ToArray();
 631
 28632        this.reportTextWriter.WriteLine("<div class=\"table-responsive\">");
 28633        this.reportTextWriter.WriteLine("<table class=\"overview table-fixed\">");
 634
 28635        this.reportTextWriter.WriteLine("<colgroup>");
 28636        this.reportTextWriter.WriteLine("<col class=\"column-min-200\" />");
 637
 364638        foreach (var met in metrics)
 140639        {
 140640            this.reportTextWriter.WriteLine("<col class=\"column105\" />");
 140641        }
 642
 28643        this.reportTextWriter.WriteLine("</colgroup>");
 644
 28645        this.reportTextWriter.Write("<thead><tr>");
 646
 28647        this.reportTextWriter.Write("<th>{0}</th>", WebUtility.HtmlEncode("Method"));
 648
 364649        foreach (var met in metrics)
 140650        {
 140651            if (met.ExplanationUrl == null)
 0652            {
 0653                this.reportTextWriter.Write("<th>{0}</th>", WebUtility.HtmlEncode(met.Name));
 0654            }
 655            else
 140656            {
 140657                this.reportTextWriter.Write(
 140658                    "<th>{0} <a href=\"{1}\" target=\"_blank\"><i class=\"icon-info-circled\"></i></a></th>",
 140659                    WebUtility.HtmlEncode(met.Name),
 140660                    WebUtility.HtmlEncode(met.ExplanationUrl.OriginalString));
 140661            }
 140662        }
 663
 28664        this.reportTextWriter.WriteLine("</tr></thead>");
 28665        this.reportTextWriter.WriteLine("<tbody>");
 666
 28667        int fileIndex = 0;
 28668        bool usePrefix = @class.Files.Count() > 1;
 669
 144670        foreach (var file in @class.Files)
 30671        {
 306672            foreach (var methodMetric in file.MethodMetrics.OrderBy(c => c.Line))
 72673            {
 72674                this.reportTextWriter.Write("<tr>");
 675
 72676                if (methodMetric.Line.HasValue)
 72677                {
 72678                    this.reportTextWriter.Write(
 72679                        "<td title=\"{0}\"><a href=\"#file{1}_line{2}\" class=\"navigatetohash\">{3}</a></td>",
 72680                        WebUtility.HtmlEncode(methodMetric.FullName),
 72681                        fileIndex,
 72682                        methodMetric.Line,
 72683                        WebUtility.HtmlEncode(
 72684                            (usePrefix ? $"File {fileIndex + 1}: " : string.Empty) + methodMetric.ShortName));
 72685                }
 686                else
 0687                {
 0688                    this.reportTextWriter.Write(
 0689                        "<td title=\"{0}\">{1}</td>",
 0690                        WebUtility.HtmlEncode(methodMetric.FullName),
 0691                        WebUtility.HtmlEncode(file.Path + " => " + methodMetric.ShortName));
 0692                }
 693
 936694                foreach (var metric in metrics)
 360695                {
 1440696                    var metricValue = methodMetric.Metrics.FirstOrDefault(m => m.Equals(metric));
 697
 360698                    if (metricValue != null)
 360699                    {
 360700                        this.reportTextWriter.Write(
 360701                        "<td>{0}{1}</td>",
 360702                        metricValue.Value.HasValue ? metricValue.Value.Value.ToString("0.##") : "-",
 360703                        metricValue.Value.HasValue && metricValue.MetricType == MetricType.CoveragePercentual ? "%" : st
 360704                    }
 705                    else
 0706                    {
 0707                        this.reportTextWriter.Write("<td>-</td>");
 0708                    }
 360709                }
 710
 72711                this.reportTextWriter.WriteLine("</tr>");
 72712            }
 713
 30714            fileIndex++;
 30715        }
 716
 28717        this.reportTextWriter.WriteLine("</tbody>");
 28718        this.reportTextWriter.WriteLine("</table>");
 28719        this.reportTextWriter.WriteLine("</div>");
 28720    }
 721
 722    /// <inheritdoc />
 723    public void LineAnalysis(int fileIndex, LineAnalysis analysis)
 1330724    {
 1330725        ArgumentNullException.ThrowIfNull(analysis);
 726
 1330727        string formattedLine = analysis.LineContent
 1330728            .Replace(((char)11).ToString(), "  ") // replace tab
 1330729            .Replace(((char)9).ToString(), "  "); // replace tab
 730
 1330731        if (formattedLine.Length > 120)
 4732        {
 4733            formattedLine = formattedLine[..120];
 4734        }
 735
 1330736        formattedLine = WebUtility.HtmlEncode(formattedLine);
 1330737        formattedLine = formattedLine.Replace(" ", "&nbsp;");
 738
 1330739        string lineVisitStatus = ConvertToCssClass(analysis.LineVisitStatus, false);
 740
 1330741        this.reportTextWriter.Write(
 1330742            "<tr class=\"{0}\" title=\"{1}\" data-coverage=\"{{",
 1330743            analysis.LineVisitStatus > LineVisitStatus.NotCoverable ? "coverableline" : string.Empty,
 1330744            WebUtility.HtmlEncode(GetTooltip(analysis)));
 745
 1330746        this.reportTextWriter.Write(
 1330747            "'AllTestMethods': {{'VC': '{0}', 'LVS': '{1}'}}",
 1330748            analysis.LineVisitStatus != LineVisitStatus.NotCoverable ? analysis.LineVisits.ToString() : string.Empty,
 1330749            lineVisitStatus);
 750
 8406751        foreach (var coverageByTestMethod in analysis.LineCoverageByTestMethod)
 2208752        {
 2208753            this.reportTextWriter.Write(
 2208754                ", 'M{0}': {{'VC': '{1}', 'LVS': '{2}'}}",
 2208755                coverageByTestMethod.Key.Id.ToString(),
 2208756                coverageByTestMethod.Value.LineVisitStatus != LineVisitStatus.NotCoverable
 2208757                    ? coverageByTestMethod.Value.LineVisits.ToString()
 2208758                    : string.Empty,
 2208759                ConvertToCssClass(coverageByTestMethod.Value.LineVisitStatus, false));
 2208760        }
 761
 1330762        this.reportTextWriter.Write("}\">");
 763
 1330764        this.reportTextWriter.Write(
 1330765            "<td class=\"{0}\">&nbsp;</td>",
 1330766            lineVisitStatus);
 1330767        this.reportTextWriter.Write(
 1330768            "<td class=\"leftmargin rightmargin right\">{0}</td>",
 1330769            analysis.LineVisitStatus != LineVisitStatus.NotCoverable ? analysis.LineVisits.ToString() : string.Empty);
 1330770        this.reportTextWriter.Write(
 1330771            "<td class=\"rightmargin right\"><a id=\"file{0}_line{1}\"></a><code>{1}</code></td>",
 1330772            fileIndex,
 1330773            analysis.LineNumber);
 774
 1330775        if (analysis.CoveredBranches.HasValue && analysis.TotalBranches.HasValue && analysis.TotalBranches.Value > 0)
 8776        {
 8777            int branchCoverage = (int)(100 * (double)analysis.CoveredBranches.Value / analysis.TotalBranches.Value);
 8778            branchCoverage -= branchCoverage % 10;
 8779            this.reportTextWriter.Write("<td class=\"percentagebar percentagebar{0}\"><i class=\"icon-fork\"></i></td>",
 8780        }
 781        else
 1322782        {
 1322783            this.reportTextWriter.Write("<td></td>");
 1322784        }
 785
 1330786        this.reportTextWriter.Write(
 1330787            "<td class=\"{0}\"><code>{1}</code></td>",
 1330788            ConvertToCssClass(analysis.LineVisitStatus, true),
 1330789            formattedLine);
 790
 1330791        this.reportTextWriter.WriteLine("</tr>");
 1330792    }
 793
 794    /// <inheritdoc />
 795    public void FinishTable()
 38796    {
 38797        this.reportTextWriter.WriteLine("</tbody>");
 38798        this.reportTextWriter.WriteLine("</table>");
 38799        this.reportTextWriter.WriteLine("</div>");
 38800    }
 801
 802    /// <inheritdoc />
 803    public void BeginRiskHotspots()
 2804    {
 2805        this.reportTextWriter.WriteLine("<risk-hotspots>");
 2806    }
 807
 808    /// <inheritdoc />
 809    public void FinishRiskHotspots()
 2810    {
 2811        this.reportTextWriter.WriteLine("</risk-hotspots>");
 2812    }
 813
 814    /// <inheritdoc />
 815    public void RiskHotspots(IEnumerable<RiskHotspot> riskHotspots)
 0816    {
 0817        var codeQualityMetrics = riskHotspots.First().MethodMetric.Metrics
 0818            .Where(m => m.MetricType == MetricType.CodeQuality)
 0819            .ToArray();
 820
 0821        this.reportTextWriter.WriteLine("<div class=\"table-responsive\">");
 0822        this.reportTextWriter.WriteLine("<table class=\"overview table-fixed stripped\">");
 823
 0824        this.reportTextWriter.WriteLine("<colgroup>");
 0825        this.reportTextWriter.WriteLine("<col class=\"column-min-200\" />");
 0826        this.reportTextWriter.WriteLine("<col class=\"column-min-200\" />");
 0827        this.reportTextWriter.WriteLine("<col class=\"column-min-200\" />");
 828
 0829        foreach (var met in codeQualityMetrics)
 0830        {
 0831            this.reportTextWriter.WriteLine("<col class=\"column105\" />");
 0832        }
 833
 0834        this.reportTextWriter.WriteLine("</colgroup>");
 835
 0836        this.reportTextWriter.Write("<thead><tr>");
 837
 0838        this.reportTextWriter.WriteLine("<th>{0}</th>", WebUtility.HtmlEncode("Assembly"));
 0839        this.reportTextWriter.WriteLine("<th>{0}</th>", WebUtility.HtmlEncode("Class"));
 0840        this.reportTextWriter.WriteLine("<th>{0}</th>", WebUtility.HtmlEncode("Method"));
 841
 0842        foreach (var metric in codeQualityMetrics)
 0843        {
 0844            if (metric.ExplanationUrl == null)
 0845            {
 0846                this.reportTextWriter.WriteLine("<th>{0}</th>", WebUtility.HtmlEncode(metric.Name));
 0847            }
 848            else
 0849            {
 0850                this.reportTextWriter.WriteLine(
 0851                    "<th>{0} <a href=\"{1}\" target=\"_blank\"><i class=\"icon-info-circled\"></i></a></th>",
 0852                    WebUtility.HtmlEncode(metric.Name),
 0853                    WebUtility.HtmlEncode(metric.ExplanationUrl.OriginalString));
 0854            }
 0855        }
 856
 0857        this.reportTextWriter.WriteLine("</tr></thead>");
 858
 0859        this.reportTextWriter.WriteLine("<tbody>");
 860
 0861        foreach (var riskHotspot in riskHotspots.Take(20))
 0862        {
 0863            string filenameColumn = riskHotspot.Class.Name;
 864
 0865            if (!this.onlySummary)
 0866            {
 0867                filenameColumn = string.Format(
 0868                    "<a href=\"{0}\">{1}</a>",
 0869                    WebUtility.HtmlEncode(this.GetClassReportFilename(riskHotspot.Assembly, riskHotspot.Class.Name)),
 0870                    WebUtility.HtmlEncode(riskHotspot.Class.DisplayName));
 0871            }
 872
 0873            this.reportTextWriter.WriteLine("<tr>");
 0874            this.reportTextWriter.WriteLine("<td>{0}</td>", WebUtility.HtmlEncode(riskHotspot.Assembly.ShortName));
 0875            this.reportTextWriter.WriteLine("<td>{0}</td>", filenameColumn);
 876
 0877            if (!this.onlySummary && riskHotspot.MethodMetric.Line.HasValue)
 0878            {
 0879                this.reportTextWriter.Write(
 0880                    "<td title=\"{0}\"><a href=\"{1}#file{2}_line{3}\">{4}</a></td>",
 0881                    WebUtility.HtmlEncode(riskHotspot.MethodMetric.FullName),
 0882                    WebUtility.HtmlEncode(this.GetClassReportFilename(riskHotspot.Assembly, riskHotspot.Class.Name)),
 0883                    riskHotspot.FileIndex,
 0884                    riskHotspot.MethodMetric.Line,
 0885                    WebUtility.HtmlEncode(riskHotspot.MethodMetric.ShortName));
 0886            }
 887            else
 0888            {
 0889                this.reportTextWriter.Write(
 0890                    "<td title=\"{0}\">{1}</td>",
 0891                    WebUtility.HtmlEncode(riskHotspot.MethodMetric.FullName),
 0892                    WebUtility.HtmlEncode(riskHotspot.MethodMetric.ShortName));
 0893            }
 894
 0895            foreach (var statusMetric in riskHotspot.StatusMetrics)
 0896            {
 0897                this.reportTextWriter.WriteLine(
 0898                    "<td class=\"{0} right\">{1}</td>",
 0899                    statusMetric.Exceeded ? "lightred" : "lightgreen",
 0900                    statusMetric.Metric.Value.HasValue ? statusMetric.Metric.Value.Value.ToString("0.##") : "-");
 0901            }
 902
 0903            this.reportTextWriter.WriteLine("</tr>");
 0904        }
 905
 0906        this.reportTextWriter.WriteLine("</tbody>");
 0907        this.reportTextWriter.WriteLine("</table>");
 0908        this.reportTextWriter.WriteLine("</div>");
 0909    }
 910
 911    /// <inheritdoc />
 912    public void SummaryAssembly(Assembly assembly, bool branchCoverageAvailable)
 2913    {
 2914        ArgumentNullException.ThrowIfNull(assembly);
 915
 2916        this.reportTextWriter.Write("<tr>");
 2917        this.reportTextWriter.Write("<th>{0}</th>", WebUtility.HtmlEncode(assembly.Name));
 2918        this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.CoveredLines);
 2919        this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.CoverableLines - assembly.CoveredLines);
 2920        this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.CoverableLines);
 2921        this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.TotalLines.GetValueOrDefault());
 2922        this.reportTextWriter.Write(
 2923            "<th title=\"{0}\" class=\"right\">{1}</th>",
 2924            assembly.CoverageQuota.HasValue ? $"{assembly.CoveredLines}/{assembly.CoverableLines}" : string.Empty,
 2925            assembly.CoverageQuota.HasValue ? assembly.CoverageQuota.Value.ToString() + "%" : string.Empty);
 2926        WriteCoverageTable(this.reportTextWriter, "th", assembly.CoverageQuota);
 927
 2928        if (branchCoverageAvailable)
 2929        {
 2930            this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.CoveredBranches);
 2931            this.reportTextWriter.Write("<th class=\"right\">{0}</th>", assembly.TotalBranches);
 2932            this.reportTextWriter.Write(
 2933            "<th class=\"right\" title=\"{0}\">{1}</th>",
 2934            assembly.BranchCoverageQuota.HasValue ? $"{assembly.CoveredBranches}/{assembly.TotalBranches}" : "-",
 2935            assembly.BranchCoverageQuota.HasValue ? assembly.BranchCoverageQuota.Value.ToString() + "%" : string.Empty);
 2936            WriteCoverageTable(this.reportTextWriter, "th", assembly.BranchCoverageQuota);
 2937        }
 938
 2939        this.reportTextWriter.WriteLine("</tr>");
 2940    }
 941
 942    /// <inheritdoc />
 943    public void SummaryClass(Class @class, bool branchCoverageAvailable)
 36944    {
 36945        ArgumentNullException.ThrowIfNull(@class);
 946
 36947        string filenameColumn = @class.Name;
 948
 36949        if (!this.onlySummary)
 36950        {
 36951            filenameColumn = string.Format(
 36952                "<a href=\"{0}\">{1}</a>",
 36953                WebUtility.HtmlEncode(this.GetClassReportFilename(@class.Assembly, @class.Name)),
 36954                WebUtility.HtmlEncode(@class.DisplayName));
 36955        }
 956
 36957        this.reportTextWriter.Write("<tr>");
 36958        this.reportTextWriter.Write("<td>{0}</td>", filenameColumn);
 36959        this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.CoveredLines);
 36960        this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.CoverableLines - @class.CoveredLines);
 36961        this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.CoverableLines);
 36962        this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.TotalLines.GetValueOrDefault());
 36963        this.reportTextWriter.Write(
 36964            "<td title=\"{0}\" class=\"right\">{1}{2}</td>",
 36965            @class.CoverageQuota.HasValue ? $"{@class.CoveredLines}/{@class.CoverableLines}" : string.Empty,
 36966            string.Empty,
 36967            @class.CoverageQuota.HasValue ? @class.CoverageQuota.Value.ToString() + "%" : string.Empty);
 968
 36969        WriteCoverageTable(this.reportTextWriter, "td", @class.CoverageQuota);
 970
 36971        if (branchCoverageAvailable)
 36972        {
 36973            this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.CoveredBranches);
 36974            this.reportTextWriter.Write("<td class=\"right\">{0}</td>", @class.TotalBranches);
 36975            this.reportTextWriter.Write(
 36976                "<td class=\"right\" title=\"{0}\">{1}{2}</td>",
 36977                @class.BranchCoverageQuota.HasValue ? $"{@class.CoveredBranches}/{@class.TotalBranches}" : "-",
 36978                string.Empty,
 36979                @class.BranchCoverageQuota.HasValue ? @class.BranchCoverageQuota.Value.ToString() + "%" : string.Empty);
 36980            WriteCoverageTable(this.reportTextWriter, "td", @class.BranchCoverageQuota);
 36981        }
 982
 36983        this.reportTextWriter.WriteLine("</tr>");
 36984    }
 985
 986    /// <inheritdoc />
 987    public void AddFooter()
 38988    {
 38989        this.reportTextWriter.Write(string.Format(
 38990            "<div class=\"footer\">Generated by DirectSight {0}<br />{1}<br /><a href=\"https://github.com/joharasmus/Di
 38991            typeof(HtmlRenderer).Assembly.GetName().Version.ToString(3),
 38992            DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss 'UTC'zzz")));
 38993    }
 994
 995    /// <inheritdoc />
 996    public void SaveSummaryReport(string targetDirectory)
 2997    {
 2998        this.SaveReport();
 2999        this.SaveCss(targetDirectory);
 21000        this.SaveJavaScript(targetDirectory);
 21001    }
 1002
 1003    /// <inheritdoc />
 1004    public void SaveClassReport(string targetDirectory, string className)
 361005    {
 361006        this.SaveReport();
 1007
 361008        if (!this.javaScriptGenerated)
 361009        {
 361010            this.SaveJavaScript(targetDirectory);
 361011            this.javaScriptGenerated = true;
 361012        }
 361013    }
 1014
 1015    /// <summary>
 1016    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 1017    /// </summary>
 1018    public void Dispose()
 381019    {
 381020        this.Dispose(true);
 381021        GC.SuppressFinalize(this);
 381022    }
 1023
 1024    /// <summary>
 1025    /// Releases unmanaged and - optionally - managed resources.
 1026    /// </summary>
 1027    /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release onl
 1028    protected virtual void Dispose(bool disposing)
 381029    {
 381030        if (disposing)
 381031        {
 381032            this.reportTextWriter?.Dispose();
 381033            StringBuilderCache.Return(this.javaScriptContent);
 381034        }
 381035    }
 1036
 1037    /// <summary>
 1038    /// Writes a table showing the coverage quota with red and green bars to the TextWriter.
 1039    /// </summary>
 1040    /// <param name="writer"><see cref="TextWriter"/> to write to.</param>
 1041    /// <param name="surroundingElement">Html element to write around the table (e.g. 'td', or 'th').</param>
 1042    /// <param name="coverage">The coverage quota.</param>
 1043    private static void WriteCoverageTable(TextWriter writer, string surroundingElement, decimal? coverage)
 761044    {
 761045        writer.Write("<{0}><table class=\"coverage\"><tr>", surroundingElement);
 1046
 761047        if (coverage.HasValue)
 441048        {
 441049            int covered = (int)Math.Round(coverage.Value, 0);
 441050            int uncovered = 100 - covered;
 1051
 441052            if (covered > 0)
 381053            {
 381054                writer.Write("<td class=\"green covered{0}\">&nbsp;</td>", covered);
 381055            }
 1056
 441057            if (uncovered > 0)
 301058            {
 301059                writer.Write("<td class=\"red covered{0}\">&nbsp;</td>", uncovered);
 301060            }
 441061        }
 1062        else
 321063        {
 321064            writer.Write("<td class=\"gray covered100\">&nbsp;</td>");
 321065        }
 1066
 761067        writer.Write("</tr></table></{0}>", surroundingElement);
 761068    }
 1069
 1070    /// <summary>
 1071    /// Converts the <see cref="LineVisitStatus" /> to the corresponding CSS class.
 1072    /// </summary>
 1073    /// <param name="lineVisitStatus">The line visit status.</param>
 1074    /// <param name="lightcolor">if set to <c>true</c> a CSS class representing a light color is returned.</param>
 1075    /// <returns>The corresponding CSS class.</returns>
 1076    private static string ConvertToCssClass(LineVisitStatus lineVisitStatus, bool lightcolor)
 48681077    {
 48681078        return lineVisitStatus switch
 48681079        {
 10121080            LineVisitStatus.Covered => lightcolor ? "lightgreen" : "green",
 4441081            LineVisitStatus.NotCovered => lightcolor ? "lightred" : "red",
 241082            LineVisitStatus.PartiallyCovered => lightcolor ? "lightorange" : "orange",
 33881083            _ => lightcolor ? "lightgray" : "gray",
 48681084        };
 1085
 48681086    }
 1087
 1088    private static string GetTooltip(LineAnalysis analysis)
 13301089    {
 13301090        string branchRate = string.Empty;
 1091
 13301092        if (analysis.CoveredBranches.HasValue && analysis.TotalBranches.HasValue && analysis.TotalBranches.Value > 0)
 81093        {
 81094            branchRate = ", " + string.Format("{0} of {1} branches are covered", analysis.CoveredBranches, analysis.Tota
 81095        }
 1096
 1097        string result;
 1098
 13301099        if (analysis.LineVisitStatus == LineVisitStatus.Covered)
 2561100        {
 2561101            result = string.Format("Covered ({0} visits{1})", analysis.LineVisits, branchRate);
 2561102        }
 10741103        else if (analysis.LineVisitStatus == LineVisitStatus.PartiallyCovered)
 61104        {
 61105            result = string.Format("Partially covered ({0} visits{1})", analysis.LineVisits, branchRate);
 61106        }
 10681107        else if (analysis.LineVisitStatus == LineVisitStatus.NotCovered)
 1261108        {
 1261109            result = string.Format("Not covered ({0} visits{1})", analysis.LineVisits, branchRate);
 1261110        }
 1111        else
 9421112        {
 9421113            result = "Not coverable";
 9421114            return result;
 1115        }
 1116
 3881117        return result;
 13301118    }
 1119
 1120    private static void WriteHtmlStart(TextWriter writer, string title, string subtitle)
 381121    {
 381122        writer.WriteLine($@"<!DOCTYPE html>
 381123<html>
 381124<head>
 381125<meta charset=""utf-8"" />
 381126<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
 381127<meta http-equiv=""X-UA-Compatible"" content=""IE=EDGE,chrome=1"" />
 381128<title>{WebUtility.HtmlEncode(title)} - {WebUtility.HtmlEncode(subtitle)}</title>");
 1129
 381130        writer.WriteLine(CssLink);
 1131
 381132        writer.WriteLine("</head><body><div class=\"container\"><div class=\"containerleft\">");
 381133    }
 1134
 1135    /// <summary>
 1136    /// Gets the file name of the report file for the given class.
 1137    /// </summary>
 1138    /// <param name="assembly">The assembly.</param>
 1139    /// <param name="className">Name of the class.</param>
 1140    /// <returns>The file name.</returns>
 1141    private string GetClassReportFilename(Assembly assembly, string className)
 1081142    {
 1081143        string assemblyName = assembly.ShortName;
 1144
 1081145        string key = assembly.Name + "_" + className;
 1146
 1081147        if (!this.fileNameByClass.TryGetValue(key, out string fileName))
 361148        {
 361149            string shortClassName = null;
 1150
 361151            if (assemblyName == "Default" && className.Contains(Path.DirectorySeparatorChar))
 01152            {
 01153                assemblyName = className[..className.LastIndexOf(Path.DirectorySeparatorChar)];
 01154                shortClassName = className[(className.LastIndexOf(Path.DirectorySeparatorChar) + 1)..];
 01155            }
 1156            else
 361157            {
 361158                if (className.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
 01159                {
 01160                    shortClassName = className[..className.LastIndexOf('.')];
 01161                }
 1162                else
 361163                {
 361164                    shortClassName = className[(className.LastIndexOf('.') + 1)..];
 361165                }
 361166            }
 1167
 361168            fileName = StringHelper.ReplaceInvalidPathChars(assemblyName + "_" + shortClassName) + ".html";
 1169
 361170            if (fileName.Length > 100)
 01171            {
 01172                string firstPart = fileName[..50];
 01173                string lastPart = fileName.Substring(fileName.Length - 45, 45);
 1174
 01175                fileName = firstPart + lastPart;
 01176            }
 1177
 3421178            if (this.fileNameByClass.Values.Any(v => v.Equals(fileName, StringComparison.OrdinalIgnoreCase)))
 01179            {
 01180                int counter = 2;
 01181                string fileNameWithoutExtension = fileName[..^4];
 1182
 1183                do
 01184                {
 01185                    fileName = fileNameWithoutExtension + counter + ".html";
 01186                    counter++;
 01187                }
 01188                while (this.fileNameByClass.Values.Any(v => v.Equals(fileName, StringComparison.OrdinalIgnoreCase)));
 01189            }
 1190
 361191            this.fileNameByClass.Add(key, fileName);
 361192        }
 1193
 1081194        return fileName;
 1081195    }
 1196
 1197    /// <summary>
 1198    /// Saves the CSS.
 1199    /// </summary>
 1200    /// <param name="targetDirectory">The target directory.</param>
 1201    private void SaveCss(string targetDirectory)
 21202    {
 21203        string targetPath = Path.Combine(targetDirectory, "report.css");
 1204
 21205        if (WrittenCssAndJavascriptFiles.Contains(targetPath))
 01206        {
 01207            return;
 1208        }
 1209
 21210        var builder = StringBuilderCache.Get();
 21211        using (var writer = new StringWriter(builder))
 21212        {
 21213            this.WriteCss(writer);
 21214        }
 1215
 21216        string css = StringBuilderCache.ToStringAndReturnToPool(builder);
 1217
 21218        System.IO.File.WriteAllText(targetPath, css);
 1219
 21220        var matches = Regex.Matches(css, @"url\(icon_(?<filename>.+).svg\),\surl\(data:image/svg\+xml;base64,(?<base64im
 1221
 1061222        foreach (Match match in matches)
 501223        {
 501224            System.IO.File.WriteAllBytes(
 501225                Path.Combine(targetDirectory, "icon_" + match.Groups["filename"].Value + ".svg"),
 501226                Convert.FromBase64String(match.Groups["base64image"].Value));
 501227        }
 1228
 21229        WrittenCssAndJavascriptFiles.Add(targetPath);
 21230    }
 1231
 1232    /// <summary>
 1233    /// Saves the javascript.
 1234    /// </summary>
 1235    /// <param name="targetDirectory">The target directory.</param>
 1236    private void SaveJavaScript(string targetDirectory)
 381237    {
 381238        string targetPath = Path.Combine(targetDirectory, this.classReport ? "class.js" : "main.js");
 1239
 381240        if (WrittenCssAndJavascriptFiles.Contains(targetPath))
 341241        {
 341242            return;
 1243        }
 1244
 41245        using (var fs = new FileStream(targetPath, FileMode.Create))
 41246        using (var writer = new StreamWriter(fs))
 41247        {
 41248            this.WriteCombinedJavascript(writer);
 41249        }
 1250
 41251        WrittenCssAndJavascriptFiles.Add(targetPath);
 381252    }
 1253
 1254    /// <summary>
 1255    /// Writes CSS to the provided Text Writer.
 1256    /// </summary>
 1257    private void WriteCss(TextWriter writer)
 21258    {
 1259        void WriteWithFilteredUrls(string resourceName)
 81260        {
 81261            var resource = ResourceStreamCache.Get(resourceName);
 81262            writer.Write(resource);
 81263        }
 1264
 21265        WriteWithFilteredUrls(this.cssFileResource);
 21266        writer.WriteLine();
 21267        writer.WriteLine();
 1268
 21269        if (this.additionalCssFileResources != null && this.additionalCssFileResources.Length > 0)
 21270        {
 141271            foreach (var additionalCssFileResource in this.additionalCssFileResources)
 41272            {
 41273                writer.WriteLine();
 41274                writer.WriteLine();
 41275                WriteWithFilteredUrls(additionalCssFileResource);
 41276            }
 1277
 21278            writer.WriteLine();
 21279            writer.WriteLine();
 21280        }
 1281
 21282        WriteWithFilteredUrls("chartist.min.css");
 21283    }
 1284
 1285    /// <summary>
 1286    /// Writes combined javascript to the provided TextWriter.
 1287    /// </summary>
 1288    private void WriteCombinedJavascript(TextWriter writer)
 41289    {
 41290        writer.WriteLine(ResourceStreamCache.Get("chartist.min.js"));
 41291        writer.WriteLine();
 1292
 41293        writer.Write(ResourceStreamCache.Get("custom.js"));
 1294
 41295        if (this.classReport)
 21296        {
 21297            return;
 1298        }
 1299
 21300        writer.WriteLine();
 21301        writer.WriteLine();
 21302        writer.Write(this.javaScriptContent);
 21303        writer.WriteLine();
 1304
 21305        writer.WriteLine("var translations = {");
 21306        writer.Write("'top': '{0}'", WebUtility.HtmlEncode("Top:"));
 21307        writer.WriteLine(",");
 21308        writer.Write("'all': '{0}'", WebUtility.HtmlEncode("All"));
 21309        writer.WriteLine(",");
 21310        writer.Write("'assembly': '{0}'", WebUtility.HtmlEncode("Assembly"));
 21311        writer.WriteLine(",");
 21312        writer.Write("'class': '{0}'", WebUtility.HtmlEncode("Class"));
 21313        writer.WriteLine(",");
 21314        writer.Write("'method': '{0}'", WebUtility.HtmlEncode("Method"));
 21315        writer.WriteLine(",");
 21316        writer.Write("'lineCoverage': '{0}'", WebUtility.HtmlEncode("Line coverage"));
 21317        writer.WriteLine(",");
 21318        writer.Write("'noGrouping': '{0}'", WebUtility.HtmlEncode("No grouping"));
 21319        writer.WriteLine(",");
 21320        writer.Write("'byAssembly': '{0}'", WebUtility.HtmlEncode("By assembly"));
 21321        writer.WriteLine(",");
 21322        writer.Write("'byNamespace': '{0}'", WebUtility.HtmlEncode("By namespace, Level:"));
 21323        writer.WriteLine(",");
 21324        writer.Write("'all': '{0}'", WebUtility.HtmlEncode("All"));
 21325        writer.WriteLine(",");
 21326        writer.Write("'collapseAll': '{0}'", WebUtility.HtmlEncode("Collapse all"));
 21327        writer.WriteLine(",");
 21328        writer.Write("'expandAll': '{0}'", WebUtility.HtmlEncode("Expand all"));
 21329        writer.WriteLine(",");
 21330        writer.Write("'grouping': '{0}'", WebUtility.HtmlEncode("Grouping:"));
 21331        writer.WriteLine(",");
 21332        writer.Write("'filter': '{0}'", WebUtility.HtmlEncode("Filter:"));
 21333        writer.WriteLine(",");
 21334        writer.Write("'name': '{0}'", WebUtility.HtmlEncode("Name"));
 21335        writer.WriteLine(",");
 21336        writer.Write("'covered': '{0}'", WebUtility.HtmlEncode("Covered"));
 21337        writer.WriteLine(",");
 21338        writer.Write("'uncovered': '{0}'", WebUtility.HtmlEncode("Uncovered"));
 21339        writer.WriteLine(",");
 21340        writer.Write("'coverable': '{0}'", WebUtility.HtmlEncode("Coverable"));
 21341        writer.WriteLine(",");
 21342        writer.Write("'total': '{0}'", WebUtility.HtmlEncode("Total"));
 21343        writer.WriteLine(",");
 21344        writer.Write("'coverage': '{0}'", WebUtility.HtmlEncode("Line coverage"));
 21345        writer.WriteLine(",");
 21346        writer.Write("'branchCoverage': '{0}'", WebUtility.HtmlEncode("Branch coverage"));
 21347        writer.WriteLine(",");
 21348        writer.Write("'methodCoverage': '{0}'", WebUtility.HtmlEncode("Method coverage"));
 21349        writer.WriteLine(",");
 21350        writer.Write("'fullMethodCoverage': '{0}'", WebUtility.HtmlEncode("Full method coverage"));
 21351        writer.WriteLine(",");
 21352        writer.Write("'percentage': '{0}'", WebUtility.HtmlEncode("Percentage"));
 21353        writer.WriteLine(",");
 21354        writer.Write("'date': '{0}'", WebUtility.HtmlEncode("Date"));
 21355        writer.WriteLine(",");
 21356        writer.Write("'allChanges': '{0}'", WebUtility.HtmlEncode("All changes"));
 21357        writer.WriteLine(",");
 21358        writer.Write("'selectCoverageTypes': '{0}'", WebUtility.HtmlEncode("Select coverage types"));
 21359        writer.WriteLine(",");
 21360        writer.Write("'selectCoverageTypesAndMetrics': '{0}'", "Select coverage types and metrics");
 21361        writer.WriteLine(",");
 21362        writer.Write("'coverageTypes': '{0}'", WebUtility.HtmlEncode("Coverage types"));
 21363        writer.WriteLine(",");
 21364        writer.Write("'metrics': '{0}'", WebUtility.HtmlEncode("Metrics"));
 21365        writer.WriteLine(",");
 21366        writer.Write("'lineCoverageIncreaseOnly': '{0}'", WebUtility.HtmlEncode("Line coverage: Increase only"));
 21367        writer.WriteLine(",");
 21368        writer.Write("'lineCoverageDecreaseOnly': '{0}'", WebUtility.HtmlEncode("Line coverage: Decrease only"));
 21369        writer.WriteLine(",");
 21370        writer.Write("'branchCoverageIncreaseOnly': '{0}'", WebUtility.HtmlEncode("Branch coverage: Increase only"));
 21371        writer.WriteLine(",");
 21372        writer.Write("'branchCoverageDecreaseOnly': '{0}'", WebUtility.HtmlEncode("Branch coverage: Decrease only"));
 21373        writer.WriteLine(",");
 21374        writer.Write("'methodCoverageIncreaseOnly': '{0}'", WebUtility.HtmlEncode("Method coverage: Increase only"));
 21375        writer.WriteLine(",");
 21376        writer.Write("'methodCoverageDecreaseOnly': '{0}'", WebUtility.HtmlEncode("Method coverage: Decrease only"));
 21377        writer.WriteLine(",");
 21378        writer.Write("'fullMethodCoverageIncreaseOnly': '{0}'", WebUtility.HtmlEncode("Full method coverage: Increase on
 21379        writer.WriteLine(",");
 21380        writer.Write("'fullMethodCoverageDecreaseOnly': '{0}'", WebUtility.HtmlEncode("Full method coverage: Decrease on
 21381        writer.WriteLine();
 21382        writer.WriteLine("};");
 1383
 21384        writer.WriteLine();
 21385        writer.WriteLine();
 1386
 21387        writer.WriteLine(ResourceStreamCache.Get("runtime.js"));
 21388        writer.WriteLine();
 1389
 21390        writer.WriteLine(ResourceStreamCache.Get("polyfills.js"));
 21391        writer.WriteLine();
 1392
 21393        writer.Write(ResourceStreamCache.Get("main.js"));
 41394    }
 1395
 1396    /// <summary>
 1397    /// Initializes the text writer.
 1398    /// </summary>
 1399    /// <param name="targetPath">The target path.</param>
 1400    private void CreateTextWriter(string targetPath)
 381401    {
 381402        this.reportTextWriter = new StreamWriter(new FileStream(targetPath, FileMode.Create));
 381403    }
 1404
 1405    /// <summary>
 1406    /// Saves the report.
 1407    /// </summary>
 1408    private void SaveReport()
 381409    {
 381410        this.FinishReport();
 1411
 381412        this.reportTextWriter.Flush();
 381413        this.reportTextWriter.Dispose();
 1414
 381415        this.reportTextWriter = null;
 381416    }
 1417
 1418    /// <summary>
 1419    /// Finishes the report.
 1420    /// </summary>
 1421    private void FinishReport()
 381422    {
 381423        string fileName = this.classReport ? "class.js" : "main.js";
 1424
 381425        this.reportTextWriter.WriteLine("</div></div>");
 1426
 381427        this.reportTextWriter.WriteLine($"<script type=\"text/javascript\" src=\"{fileName}\"></script>");
 1428
 381429        this.reportTextWriter.Write("</body></html>");
 381430    }
 1431
 1432    private static class ResourceStreamCache
 1433    {
 11434        private static readonly ConcurrentDictionary<string, string> Cache = new();
 1435
 221436        public static string Get(string resourceName) => Cache.GetOrAdd(resourceName, v =>
 91437        {
 91438            using Stream stream =
 91439                typeof(HtmlRenderer)
 91440                .Assembly
 91441                .GetManifestResourceStream("DirectSight.Reporting.Builders.Rendering.resources." + resourceName);
 91442            using var reader = new StreamReader(stream);
 91443            return reader.ReadToEnd();
 311444        });
 1445    }
 1446
 1447    private static class StringBuilderCache
 1448    {
 11449        private static readonly DefaultObjectPool<StringBuilder> Pool = new(new StringBuilderPooledObjectPolicy
 11450        {
 11451            InitialCapacity = 4096,
 11452            MaximumRetainedCapacity = 1 * 1024 * 1024
 11453        });
 1454
 401455        internal static StringBuilder Get() => Pool.Get();
 1456
 381457        internal static void Return(StringBuilder builder) => Pool.Return(builder);
 1458
 1459        internal static string ToStringAndReturnToPool(StringBuilder builder)
 21460        {
 21461            var result = builder.ToString();
 21462            Pool.Return(builder);
 21463            return result;
 21464        }
 1465    }
 1466}

Methods/Properties

.cctor()
.ctor(System.Collections.Generic.IDictionary`2<System.String,System.String>,System.Boolean,System.String,System.String)
.ctor(System.Collections.Generic.IDictionary`2<System.String,System.String>,System.Boolean,System.String[],System.String)
BeginSummaryReport(System.String,System.String,System.String)
BeginClassReport(System.String,DirectSight.Parser.Analysis.Assembly,System.String,System.String,System.String)
Cards(System.Collections.Generic.IEnumerable`1<DirectSight.Reporting.Builders.Rendering.Card>)
Header(System.String)
HeaderWithGithubLinks(System.String)
HeaderWithBackLink(System.String)
TestMethods(System.Collections.Generic.IEnumerable`1<DirectSight.Parser.Analysis.TestMethod>,System.Collections.Generic.IEnumerable`1<DirectSight.Parser.Analysis.FileAnalysis>,System.Collections.Generic.IDictionary`2<System.Int32,System.Collections.Generic.IEnumerable`1<DirectSight.Parser.Analysis.CodeElement>>)
File(System.String)
Paragraph(System.String)
BeginSummaryTable()
FinishSummaryTable()
BeginSummaryTable(System.Boolean)
CustomSummary(System.Collections.Generic.IEnumerable`1<DirectSight.Parser.Analysis.Assembly>,System.Collections.Generic.IEnumerable`1<DirectSight.CodeAnalysis.RiskHotspot>,System.Boolean)
BeginLineAnalysisTable(System.Collections.Generic.IEnumerable`1<System.String>)
MetricsTable(DirectSight.Parser.Analysis.Class)
LineAnalysis(System.Int32,DirectSight.Parser.Analysis.LineAnalysis)
FinishTable()
BeginRiskHotspots()
FinishRiskHotspots()
RiskHotspots(System.Collections.Generic.IEnumerable`1<DirectSight.CodeAnalysis.RiskHotspot>)
SummaryAssembly(DirectSight.Parser.Analysis.Assembly,System.Boolean)
SummaryClass(DirectSight.Parser.Analysis.Class,System.Boolean)
AddFooter()
SaveSummaryReport(System.String)
SaveClassReport(System.String,System.String)
Dispose()
Dispose(System.Boolean)
WriteCoverageTable(System.IO.TextWriter,System.String,System.Nullable`1<System.Decimal>)
ConvertToCssClass(DirectSight.Parser.Analysis.LineVisitStatus,System.Boolean)
GetTooltip(DirectSight.Parser.Analysis.LineAnalysis)
WriteHtmlStart(System.IO.TextWriter,System.String,System.String)
GetClassReportFilename(DirectSight.Parser.Analysis.Assembly,System.String)
SaveCss(System.String)
SaveJavaScript(System.String)
WriteCss(System.IO.TextWriter)
WriteCombinedJavascript(System.IO.TextWriter)
CreateTextWriter(System.String)
SaveReport()
FinishReport()
.cctor()
Get(System.String)
.cctor()
Get()
Return(System.Text.StringBuilder)
ToStringAndReturnToPool(System.Text.StringBuilder)