| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.IO; |
| | | 4 | | using System.Linq; |
| | | 5 | | using DirectSight.Parser.Analysis; |
| | | 6 | | using DirectSight.Reporting.Builders.Rendering; |
| | | 7 | | |
| | | 8 | | namespace DirectSight.Reporting.Builders; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Implementation of <see cref="IReportBuilder"/> that uses <see cref="IHtmlRenderer"/> to create reports. |
| | | 12 | | /// </summary> |
| | | 13 | | internal class HtmlReportBuilder : IReportBuilder |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Dictionary containing the filenames of the class reports by class. |
| | | 17 | | /// </summary> |
| | 3 | 18 | | protected readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>(); |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Gets the report type. |
| | | 22 | | /// </summary> |
| | | 23 | | /// <value> |
| | | 24 | | /// The report type. |
| | | 25 | | /// </value> |
| | 1 | 26 | | public string ReportType => "Html"; |
| | | 27 | | |
| | | 28 | | /// <summary> |
| | | 29 | | /// Gets or sets the report context. |
| | | 30 | | /// </summary> |
| | | 31 | | /// <value> |
| | | 32 | | /// The report context. |
| | | 33 | | /// </value> |
| | 88 | 34 | | public ReportContext ReportContext { get; set; } |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Creates a class report. |
| | | 38 | | /// </summary> |
| | | 39 | | /// <param name="class">The class.</param> |
| | | 40 | | /// <param name="fileAnalyses">The file analyses that correspond to the class.</param> |
| | | 41 | | public void CreateClassReport(Class @class, IEnumerable<FileAnalysis> fileAnalyses) |
| | 36 | 42 | | { |
| | 36 | 43 | | ArgumentNullException.ThrowIfNull(@class); |
| | | 44 | | |
| | 36 | 45 | | ArgumentNullException.ThrowIfNull(fileAnalyses); |
| | | 46 | | |
| | 36 | 47 | | using var reportRenderer = new HtmlRenderer( |
| | 36 | 48 | | this.fileNameByClass, |
| | 36 | 49 | | false, |
| | 36 | 50 | | ["custom_adaptive.css", "custom_bluered.css"], |
| | 36 | 51 | | "custom.css"); |
| | | 52 | | |
| | 36 | 53 | | reportRenderer.BeginClassReport(this.CreateTargetDirectory(), @class.Assembly, @class.Name, @class.DisplayName, |
| | | 54 | | |
| | 36 | 55 | | reportRenderer.HeaderWithBackLink("Summary"); |
| | | 56 | | |
| | 36 | 57 | | var infoCardItems = new List<CardLineItem>() |
| | 36 | 58 | | { |
| | 36 | 59 | | new("Class:", @class.DisplayName, null, CardLineItemAlignment.Left), |
| | 36 | 60 | | new("Assembly", @class.Assembly.ShortName, null, CardLineItemAlignment.Left), |
| | 36 | 61 | | new("File(s):", @class.Files.Select(f => f.Path).ToArray()) |
| | 36 | 62 | | }; |
| | | 63 | | |
| | 36 | 64 | | var infoCard = new Card( |
| | 36 | 65 | | "Information", |
| | 36 | 66 | | string.Empty, |
| | 36 | 67 | | null, |
| | 36 | 68 | | infoCardItems.ToArray()); |
| | | 69 | | |
| | 36 | 70 | | reportRenderer.Cards([infoCard]); |
| | | 71 | | |
| | 36 | 72 | | var cards = new List<Card>() |
| | 36 | 73 | | { |
| | 36 | 74 | | new( |
| | 36 | 75 | | "Line coverage", |
| | 36 | 76 | | @class.CoverageQuota.HasValue ? $"{Math.Floor(@class.CoverageQuota.Value)}%" : "N/A", |
| | 36 | 77 | | @class.CoverageQuota, |
| | 36 | 78 | | new CardLineItem("Covered lines:", @class.CoveredLines.ToString(), null), |
| | 36 | 79 | | new CardLineItem("Uncovered lines:", (@class.CoverableLines - @class.CoveredLines).ToString(), null), |
| | 36 | 80 | | new CardLineItem("Coverable lines:", @class.CoverableLines.ToString(), null), |
| | 36 | 81 | | new CardLineItem("Total lines:", @class.TotalLines.GetValueOrDefault().ToString(), null), |
| | 36 | 82 | | new CardLineItem( |
| | 36 | 83 | | "Line coverage:", |
| | 36 | 84 | | @class.CoverageQuota.HasValue ? $"{@class.CoverageQuota.Value}%" : "N/A", |
| | 36 | 85 | | @class.CoverageQuota.HasValue ? $"{@class.CoveredLines} of {@class.CoverableLines}" : "N/A")) |
| | 36 | 86 | | }; |
| | | 87 | | |
| | 36 | 88 | | if (@class.CoveredBranches.HasValue && @class.TotalBranches.HasValue) |
| | 36 | 89 | | { |
| | 36 | 90 | | cards.Add(new Card( |
| | 36 | 91 | | "Branch coverage", |
| | 36 | 92 | | @class.BranchCoverageQuota.HasValue ? $"{Math.Floor(@class.BranchCoverageQuota.Value)}%" : "N/A", |
| | 36 | 93 | | @class.BranchCoverageQuota, |
| | 36 | 94 | | new CardLineItem("Covered branches:", @class.CoveredBranches.GetValueOrDefault().ToString(), null), |
| | 36 | 95 | | new CardLineItem("Total branches:", @class.TotalBranches.GetValueOrDefault().ToString(), null), |
| | 36 | 96 | | new CardLineItem( |
| | 36 | 97 | | "Branch coverage:", |
| | 36 | 98 | | @class.BranchCoverageQuota.HasValue ? $"{@class.BranchCoverageQuota.Value}%" : "N/A", |
| | 36 | 99 | | @class.BranchCoverageQuota.HasValue |
| | 36 | 100 | | ? $"{@class.CoveredBranches.GetValueOrDefault()} of {@class.TotalBranches.GetValueOrDefault()}" |
| | 36 | 101 | | : "N/A"))); |
| | 36 | 102 | | } |
| | | 103 | | |
| | 36 | 104 | | cards.Add(new Card("Method coverage")); |
| | | 105 | | |
| | 36 | 106 | | reportRenderer.Cards(cards); |
| | | 107 | | |
| | 70 | 108 | | if (@class.Files.Any(f => f.MethodMetrics.Any())) |
| | 28 | 109 | | { |
| | 28 | 110 | | reportRenderer.Header("Metrics"); |
| | 28 | 111 | | reportRenderer.MetricsTable(@class); |
| | 28 | 112 | | } |
| | | 113 | | |
| | 36 | 114 | | reportRenderer.Header("File(s)"); |
| | | 115 | | |
| | 36 | 116 | | if (fileAnalyses.Any()) |
| | 32 | 117 | | { |
| | 32 | 118 | | int fileIndex = 0; |
| | 168 | 119 | | foreach (var fileAnalysis in fileAnalyses) |
| | 36 | 120 | | { |
| | 36 | 121 | | reportRenderer.File(fileAnalysis.Path); |
| | | 122 | | |
| | 36 | 123 | | if (!string.IsNullOrEmpty(fileAnalysis.Error)) |
| | 0 | 124 | | { |
| | 0 | 125 | | reportRenderer.Paragraph(fileAnalysis.Error); |
| | 0 | 126 | | } |
| | | 127 | | else |
| | 36 | 128 | | { |
| | 36 | 129 | | reportRenderer.BeginLineAnalysisTable([string.Empty, "#", "Line", string.Empty, "Line coverage"]); |
| | | 130 | | |
| | 2768 | 131 | | foreach (var line in fileAnalysis.Lines) |
| | 1330 | 132 | | { |
| | 1330 | 133 | | reportRenderer.LineAnalysis(fileIndex, line); |
| | 1330 | 134 | | } |
| | | 135 | | |
| | 36 | 136 | | reportRenderer.FinishTable(); |
| | 36 | 137 | | } |
| | | 138 | | |
| | 36 | 139 | | fileIndex++; |
| | 36 | 140 | | } |
| | 32 | 141 | | } |
| | | 142 | | else |
| | 4 | 143 | | { |
| | 4 | 144 | | reportRenderer.Paragraph( |
| | 4 | 145 | | "No files found. " + |
| | 4 | 146 | | "This usually happens if a file isn't covered by a test or the class does not contain any sequence point |
| | 4 | 147 | | "(e.g. a class that only contains auto properties)."); |
| | 4 | 148 | | } |
| | | 149 | | |
| | 36 | 150 | | reportRenderer.AddFooter(); |
| | | 151 | | |
| | 36 | 152 | | if (fileAnalyses.Any()) |
| | 32 | 153 | | { |
| | 32 | 154 | | var testMethods = @class.Files |
| | 102 | 155 | | .SelectMany(f => f.TestMethods) |
| | 32 | 156 | | .Distinct() |
| | 188 | 157 | | .OrderBy(l => l.ShortName); |
| | | 158 | | |
| | 32 | 159 | | var codeElementsByFileIndex = new Dictionary<int, IEnumerable<CodeElement>>(); |
| | | 160 | | |
| | 32 | 161 | | int fileIndex = 0; |
| | 168 | 162 | | foreach (var file in @class.Files) |
| | 36 | 163 | | { |
| | 146 | 164 | | codeElementsByFileIndex.Add(fileIndex++, file.CodeElements.OrderBy(c => c.FirstLine)); |
| | 36 | 165 | | } |
| | | 166 | | |
| | 32 | 167 | | reportRenderer.TestMethods(testMethods, fileAnalyses, codeElementsByFileIndex); |
| | 32 | 168 | | } |
| | | 169 | | |
| | 36 | 170 | | reportRenderer.SaveClassReport(this.CreateTargetDirectory(), @class.Name); |
| | 72 | 171 | | } |
| | | 172 | | |
| | | 173 | | /// <summary> |
| | | 174 | | /// Creates the summary report. |
| | | 175 | | /// </summary> |
| | | 176 | | /// <param name="summaryResult">The summary result.</param> |
| | | 177 | | public void CreateSummaryReport(SummaryResult summaryResult) |
| | 2 | 178 | | { |
| | 2 | 179 | | ArgumentNullException.ThrowIfNull(summaryResult); |
| | | 180 | | |
| | 2 | 181 | | using var reportRenderer = new HtmlRenderer( |
| | 2 | 182 | | this.fileNameByClass, |
| | 2 | 183 | | false, |
| | 2 | 184 | | ["custom_adaptive.css", "custom_bluered.css"], |
| | 2 | 185 | | "custom.css"); |
| | | 186 | | |
| | 2 | 187 | | string title = "Summary"; |
| | | 188 | | |
| | 2 | 189 | | reportRenderer.BeginSummaryReport(this.CreateTargetDirectory(), null, title); |
| | 2 | 190 | | reportRenderer.HeaderWithGithubLinks(title); |
| | | 191 | | |
| | 2 | 192 | | var assembliesWithClasses = summaryResult.Assemblies |
| | 2 | 193 | | .Where(a => a.Classes.Any()) |
| | 2 | 194 | | .ToArray(); |
| | | 195 | | |
| | 2 | 196 | | var infoCardItems = new List<CardLineItem>() |
| | 2 | 197 | | { |
| | 2 | 198 | | new("Parser:", summaryResult.UsedParser, null, CardLineItemAlignment.Left), |
| | 2 | 199 | | new("Assemblies:", assembliesWithClasses.Length.ToString(), null), |
| | 2 | 200 | | new("Classes:", assembliesWithClasses.SelectMany(a => a.Classes).Count().ToString(), null), |
| | 38 | 201 | | new("Files:", assembliesWithClasses.SelectMany(a => a.Classes).SelectMany(a => a.Files).Distinct().Count().T |
| | 2 | 202 | | }; |
| | | 203 | | |
| | 2 | 204 | | if (summaryResult.MinimumTimeStamp.HasValue || summaryResult.MaximumTimeStamp.HasValue) |
| | 0 | 205 | | { |
| | 0 | 206 | | infoCardItems.Add(new CardLineItem("Coverage date:", summaryResult.CoverageDate(), null, CardLineItemAlignme |
| | 0 | 207 | | } |
| | | 208 | | |
| | 2 | 209 | | var cards = new List<Card>() |
| | 2 | 210 | | { |
| | 2 | 211 | | new( |
| | 2 | 212 | | "Information", |
| | 2 | 213 | | string.Empty, |
| | 2 | 214 | | null, |
| | 2 | 215 | | infoCardItems.ToArray()), |
| | 2 | 216 | | new( |
| | 2 | 217 | | "Line coverage", |
| | 2 | 218 | | summaryResult.CoverageQuota.HasValue ? $"{Math.Floor(summaryResult.CoverageQuota.Value)}%" : "N/A", |
| | 2 | 219 | | summaryResult.CoverageQuota, |
| | 2 | 220 | | new CardLineItem("Covered lines:", summaryResult.CoveredLines.ToString(), null), |
| | 2 | 221 | | new CardLineItem("Uncovered lines:", (summaryResult.CoverableLines - summaryResult.CoveredLines).ToStrin |
| | 2 | 222 | | new CardLineItem("Coverable lines:", summaryResult.CoverableLines.ToString(), null), |
| | 2 | 223 | | new CardLineItem("Total lines:", summaryResult.TotalLines.GetValueOrDefault().ToString(), null), |
| | 2 | 224 | | new CardLineItem( |
| | 2 | 225 | | "Line coverage:", |
| | 2 | 226 | | summaryResult.CoverageQuota.HasValue ? $"{summaryResult.CoverageQuota.Value}%" : "N/A", |
| | 2 | 227 | | summaryResult.CoverageQuota.HasValue ? $"{summaryResult.CoveredLines} of {summaryResult.CoverableLin |
| | 2 | 228 | | }; |
| | | 229 | | |
| | 2 | 230 | | if (summaryResult.CoveredBranches.HasValue && summaryResult.TotalBranches.HasValue) |
| | 2 | 231 | | { |
| | 2 | 232 | | cards.Add(new Card( |
| | 2 | 233 | | "Branch coverage", |
| | 2 | 234 | | summaryResult.BranchCoverageQuota.HasValue ? $"{Math.Floor(summaryResult.BranchCoverageQuota.Value)}%" : |
| | 2 | 235 | | summaryResult.BranchCoverageQuota, |
| | 2 | 236 | | new CardLineItem("Covered branches:", summaryResult.CoveredBranches.GetValueOrDefault().ToString(), null |
| | 2 | 237 | | new CardLineItem("Total branches:", summaryResult.TotalBranches.GetValueOrDefault().ToString(), null), |
| | 2 | 238 | | new CardLineItem( |
| | 2 | 239 | | "Branch coverage:", |
| | 2 | 240 | | summaryResult.BranchCoverageQuota.HasValue ? $"{summaryResult.BranchCoverageQuota.Value}%" : "N/A", |
| | 2 | 241 | | summaryResult.BranchCoverageQuota.HasValue |
| | 2 | 242 | | ? $"{summaryResult.CoveredBranches.GetValueOrDefault()} of {summaryResult.TotalBranches.GetValueOrDe |
| | 2 | 243 | | : "N/A"))); |
| | 2 | 244 | | } |
| | | 245 | | |
| | 2 | 246 | | cards.Add(new Card("Method coverage")); |
| | | 247 | | |
| | 2 | 248 | | reportRenderer.Cards(cards); |
| | | 249 | | |
| | 2 | 250 | | if (this.ReportContext.RiskHotspotAnalysisResult != null |
| | 2 | 251 | | && this.ReportContext.RiskHotspotAnalysisResult.CodeCodeQualityMetricsAvailable) |
| | 2 | 252 | | { |
| | 2 | 253 | | reportRenderer.Header("Risk Hotspots"); |
| | | 254 | | |
| | 2 | 255 | | if (this.ReportContext.RiskHotspotAnalysisResult.RiskHotspots.Count > 0) |
| | 0 | 256 | | { |
| | 0 | 257 | | reportRenderer.BeginRiskHotspots(); |
| | 0 | 258 | | reportRenderer.RiskHotspots(this.ReportContext.RiskHotspotAnalysisResult.RiskHotspots); |
| | 0 | 259 | | reportRenderer.FinishRiskHotspots(); |
| | 0 | 260 | | } |
| | | 261 | | else |
| | 2 | 262 | | { |
| | | 263 | | // Angular element has to be present |
| | 2 | 264 | | reportRenderer.BeginRiskHotspots(); |
| | 2 | 265 | | reportRenderer.FinishRiskHotspots(); |
| | | 266 | | |
| | 2 | 267 | | reportRenderer.Paragraph("No risk hotspots found."); |
| | 2 | 268 | | } |
| | 2 | 269 | | } |
| | | 270 | | else |
| | 0 | 271 | | { |
| | | 272 | | // Angular element has to be present |
| | 0 | 273 | | reportRenderer.BeginRiskHotspots(); |
| | 0 | 274 | | reportRenderer.FinishRiskHotspots(); |
| | 0 | 275 | | } |
| | | 276 | | |
| | 2 | 277 | | reportRenderer.Header("Coverage"); |
| | | 278 | | |
| | 2 | 279 | | if (assembliesWithClasses.Length != 0) |
| | 2 | 280 | | { |
| | 2 | 281 | | reportRenderer.BeginSummaryTable(); |
| | 2 | 282 | | reportRenderer.BeginSummaryTable(summaryResult.SupportsBranchCoverage); |
| | | 283 | | |
| | 10 | 284 | | foreach (var assembly in assembliesWithClasses) |
| | 2 | 285 | | { |
| | 2 | 286 | | reportRenderer.SummaryAssembly(assembly, summaryResult.SupportsBranchCoverage); |
| | | 287 | | |
| | 78 | 288 | | foreach (var @class in assembly.Classes) |
| | 36 | 289 | | { |
| | 36 | 290 | | reportRenderer.SummaryClass(@class, summaryResult.SupportsBranchCoverage); |
| | 36 | 291 | | } |
| | 2 | 292 | | } |
| | | 293 | | |
| | 2 | 294 | | reportRenderer.FinishTable(); |
| | 2 | 295 | | reportRenderer.FinishSummaryTable(); |
| | 2 | 296 | | } |
| | | 297 | | else |
| | 0 | 298 | | { |
| | | 299 | | // Angular element has to be present |
| | 0 | 300 | | reportRenderer.BeginSummaryTable(); |
| | 0 | 301 | | reportRenderer.FinishSummaryTable(); |
| | | 302 | | |
| | 0 | 303 | | reportRenderer.Paragraph("No assemblies have been covered."); |
| | 0 | 304 | | } |
| | | 305 | | |
| | 2 | 306 | | reportRenderer.CustomSummary( |
| | 2 | 307 | | assembliesWithClasses, |
| | 2 | 308 | | this.ReportContext.RiskHotspotAnalysisResult.RiskHotspots, |
| | 2 | 309 | | summaryResult.SupportsBranchCoverage); |
| | | 310 | | |
| | 2 | 311 | | reportRenderer.AddFooter(); |
| | 2 | 312 | | reportRenderer.SaveSummaryReport(this.CreateTargetDirectory()); |
| | | 313 | | |
| | 2 | 314 | | string targetDirectory = this.CreateTargetDirectory(); |
| | | 315 | | |
| | 2 | 316 | | File.Copy( |
| | 2 | 317 | | Path.Combine(targetDirectory, "index.html"), |
| | 2 | 318 | | Path.Combine(targetDirectory, "index.htm"), |
| | 2 | 319 | | true); |
| | 4 | 320 | | } |
| | | 321 | | |
| | | 322 | | /// <summary> |
| | | 323 | | /// Creates the target directory. |
| | | 324 | | /// </summary> |
| | | 325 | | /// <returns>The target directory.</returns> |
| | | 326 | | protected string CreateTargetDirectory() |
| | 78 | 327 | | { |
| | 78 | 328 | | return this.ReportContext.ReportConfiguration.TargetDirectory; |
| | 78 | 329 | | } |
| | | 330 | | } |