1//===- SourceCoverageViewHTML.cpp - A html code coverage view -------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8///
9/// \file This file implements the html coverage renderer.
10///
11//===----------------------------------------------------------------------===//
12
13#include "SourceCoverageViewHTML.h"
14#include "CoverageReport.h"
15#include "llvm/ADT/SmallString.h"
16#include "llvm/ADT/StringExtras.h"
17#include "llvm/Support/Format.h"
18#include "llvm/Support/Path.h"
19#include "llvm/Support/ThreadPool.h"
20#include <optional>
21
22using namespace llvm;
23
24namespace {
25
26// Return a string with the special characters in \p Str escaped.
27std::string escape(StringRef Str, const CoverageViewOptions &Opts) {
28 std::string TabExpandedResult;
29 unsigned ColNum = 0; // Record the column number.
30 for (char C : Str) {
31 if (C == '\t') {
32 // Replace '\t' with up to TabSize spaces.
33 unsigned NumSpaces = Opts.TabSize - (ColNum % Opts.TabSize);
34 TabExpandedResult.append(n: NumSpaces, c: ' ');
35 ColNum += NumSpaces;
36 } else {
37 TabExpandedResult += C;
38 if (C == '\n' || C == '\r')
39 ColNum = 0;
40 else
41 ++ColNum;
42 }
43 }
44 std::string EscapedHTML;
45 {
46 raw_string_ostream OS{EscapedHTML};
47 printHTMLEscaped(String: TabExpandedResult, Out&: OS);
48 }
49 return EscapedHTML;
50}
51
52// Create a \p Name tag around \p Str, and optionally set its \p ClassName.
53std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") {
54 std::string Tag = "<";
55 Tag += Name;
56 if (!ClassName.empty()) {
57 Tag += " class='";
58 Tag += ClassName;
59 Tag += "'";
60 }
61 Tag += ">";
62 Tag += Str;
63 Tag += "</";
64 Tag += Name;
65 Tag += ">";
66 return Tag;
67}
68
69// Create an anchor to \p Link with the label \p Str.
70std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") {
71 std::string Tag;
72 Tag += "<a ";
73 if (!TargetName.empty()) {
74 Tag += "name='";
75 Tag += TargetName;
76 Tag += "' ";
77 }
78 Tag += "href='";
79 Tag += Link;
80 Tag += "'>";
81 Tag += Str;
82 Tag += "</a>";
83 return Tag;
84}
85
86const char *BeginHeader =
87 "<head>"
88 "<meta name='viewport' content='width=device-width,initial-scale=1'>"
89 "<meta charset='UTF-8'>";
90
91const char *JSForCoverage =
92 R"javascript(
93function next_uncovered(selector, reverse, scroll_selector) {
94 function visit_element(element) {
95 element.classList.add("seen");
96 element.classList.add("selected");
97
98 if (!scroll_selector) {
99 scroll_selector = "tr:has(.selected) td.line-number"
100 }
101
102 const scroll_to = document.querySelector(scroll_selector);
103 if (scroll_to) {
104 scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"});
105 }
106 }
107
108 function select_one() {
109 if (!reverse) {
110 const previously_selected = document.querySelector(".selected");
111
112 if (previously_selected) {
113 previously_selected.classList.remove("selected");
114 }
115
116 return document.querySelector(selector + ":not(.seen)");
117 } else {
118 const previously_selected = document.querySelector(".selected");
119
120 if (previously_selected) {
121 previously_selected.classList.remove("selected");
122 previously_selected.classList.remove("seen");
123 }
124
125 const nodes = document.querySelectorAll(selector + ".seen");
126 if (nodes) {
127 const last = nodes[nodes.length - 1]; // last
128 return last;
129 } else {
130 return undefined;
131 }
132 }
133 }
134
135 function reset_all() {
136 if (!reverse) {
137 const all_seen = document.querySelectorAll(selector + ".seen");
138
139 if (all_seen) {
140 all_seen.forEach(e => e.classList.remove("seen"));
141 }
142 } else {
143 const all_seen = document.querySelectorAll(selector + ":not(.seen)");
144
145 if (all_seen) {
146 all_seen.forEach(e => e.classList.add("seen"));
147 }
148 }
149
150 }
151
152 const uncovered = select_one();
153
154 if (uncovered) {
155 visit_element(uncovered);
156 } else {
157 reset_all();
158
159 const uncovered = select_one();
160
161 if (uncovered) {
162 visit_element(uncovered);
163 }
164 }
165}
166
167function next_line(reverse) {
168 next_uncovered("td.uncovered-line", reverse)
169}
170
171function next_region(reverse) {
172 next_uncovered("span.red.region", reverse);
173}
174
175function next_branch(reverse) {
176 next_uncovered("span.red.branch", reverse);
177}
178
179document.addEventListener("keypress", function(event) {
180 const reverse = event.shiftKey;
181 if (event.code == "KeyL") {
182 next_line(reverse);
183 }
184 if (event.code == "KeyB") {
185 next_branch(reverse);
186 }
187 if (event.code == "KeyR") {
188 next_region(reverse);
189 }
190});
191)javascript";
192
193const char *CSSForCoverage =
194 R"(.red {
195 background-color: #f004;
196}
197.cyan {
198 background-color: cyan;
199}
200html {
201 scroll-behavior: smooth;
202}
203body {
204 font-family: -apple-system, sans-serif;
205}
206pre {
207 margin-top: 0px !important;
208 margin-bottom: 0px !important;
209}
210.source-name-title {
211 padding: 5px 10px;
212 border-bottom: 1px solid #8888;
213 background-color: #0002;
214 line-height: 35px;
215}
216.centered {
217 display: table;
218 margin-left: left;
219 margin-right: auto;
220 border: 1px solid #8888;
221 border-radius: 3px;
222}
223.expansion-view {
224 margin-left: 0px;
225 margin-top: 5px;
226 margin-right: 5px;
227 margin-bottom: 5px;
228 border: 1px solid #8888;
229 border-radius: 3px;
230}
231table {
232 border-collapse: collapse;
233}
234.light-row {
235 border: 1px solid #8888;
236 border-left: none;
237 border-right: none;
238}
239.light-row-bold {
240 border: 1px solid #8888;
241 border-left: none;
242 border-right: none;
243 font-weight: bold;
244}
245.column-entry {
246 text-align: left;
247}
248.column-entry-bold {
249 font-weight: bold;
250 text-align: left;
251}
252.column-entry-yellow {
253 text-align: left;
254 background-color: #ff06;
255}
256.column-entry-red {
257 text-align: left;
258 background-color: #f004;
259}
260.column-entry-gray {
261 text-align: left;
262 background-color: #fff4;
263}
264.column-entry-green {
265 text-align: left;
266 background-color: #0f04;
267}
268.line-number {
269 text-align: right;
270}
271.covered-line {
272 text-align: right;
273 color: #06d;
274}
275.uncovered-line {
276 text-align: right;
277 color: #d00;
278}
279.uncovered-line.selected {
280 color: #f00;
281 font-weight: bold;
282}
283.region.red.selected {
284 background-color: #f008;
285 font-weight: bold;
286}
287.branch.red.selected {
288 background-color: #f008;
289 font-weight: bold;
290}
291.tooltip {
292 position: relative;
293 display: inline;
294 background-color: #bef;
295 text-decoration: none;
296}
297.tooltip span.tooltip-content {
298 position: absolute;
299 width: 100px;
300 margin-left: -50px;
301 color: #FFFFFF;
302 background: #000000;
303 height: 30px;
304 line-height: 30px;
305 text-align: center;
306 visibility: hidden;
307 border-radius: 6px;
308}
309.tooltip span.tooltip-content:after {
310 content: '';
311 position: absolute;
312 top: 100%;
313 left: 50%;
314 margin-left: -8px;
315 width: 0; height: 0;
316 border-top: 8px solid #000000;
317 border-right: 8px solid transparent;
318 border-left: 8px solid transparent;
319}
320:hover.tooltip span.tooltip-content {
321 visibility: visible;
322 opacity: 0.8;
323 bottom: 30px;
324 left: 50%;
325 z-index: 999;
326}
327th, td {
328 vertical-align: top;
329 padding: 2px 8px;
330 border-collapse: collapse;
331 border-right: 1px solid #8888;
332 border-left: 1px solid #8888;
333 text-align: left;
334}
335td pre {
336 display: inline-block;
337 text-decoration: inherit;
338}
339td:first-child {
340 border-left: none;
341}
342td:last-child {
343 border-right: none;
344}
345tr:hover {
346 background-color: #eee;
347}
348tr:last-child {
349 border-bottom: none;
350}
351tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
352 background-color: #8884;
353}
354a {
355 color: inherit;
356}
357.control {
358 position: fixed;
359 top: 0em;
360 right: 0em;
361 padding: 1em;
362 background: #FFF8;
363}
364@media (prefers-color-scheme: dark) {
365 body {
366 background-color: #222;
367 color: whitesmoke;
368 }
369 tr:hover {
370 background-color: #111;
371 }
372 .covered-line {
373 color: #39f;
374 }
375 .uncovered-line {
376 color: #f55;
377 }
378 .tooltip {
379 background-color: #068;
380 }
381 .control {
382 background: #2228;
383 }
384 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
385 background-color: #8884;
386 }
387}
388)";
389
390const char *EndHeader = "</head>";
391
392const char *BeginCenteredDiv = "<div class='centered'>";
393
394const char *EndCenteredDiv = "</div>";
395
396const char *BeginSourceNameDiv = "<div class='source-name-title'>";
397
398const char *EndSourceNameDiv = "</div>";
399
400const char *BeginCodeTD = "<td class='code'>";
401
402const char *EndCodeTD = "</td>";
403
404const char *BeginPre = "<pre>";
405
406const char *EndPre = "</pre>";
407
408const char *BeginExpansionDiv = "<div class='expansion-view'>";
409
410const char *EndExpansionDiv = "</div>";
411
412const char *BeginTable = "<table>";
413
414const char *EndTable = "</table>";
415
416const char *ProjectTitleTag = "h1";
417
418const char *ReportTitleTag = "h2";
419
420const char *CreatedTimeTag = "h4";
421
422std::string getPathToStyle(StringRef ViewPath) {
423 std::string PathToStyle;
424 std::string PathSep = std::string(sys::path::get_separator());
425 unsigned NumSeps = ViewPath.count(Str: PathSep);
426 for (unsigned I = 0, E = NumSeps; I < E; ++I)
427 PathToStyle += ".." + PathSep;
428 return PathToStyle + "style.css";
429}
430
431std::string getPathToJavaScript(StringRef ViewPath) {
432 std::string PathToJavaScript;
433 std::string PathSep = std::string(sys::path::get_separator());
434 unsigned NumSeps = ViewPath.count(Str: PathSep);
435 for (unsigned I = 0, E = NumSeps; I < E; ++I)
436 PathToJavaScript += ".." + PathSep;
437 return PathToJavaScript + "control.js";
438}
439
440void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
441 const std::string &PathToStyle = "",
442 const std::string &PathToJavaScript = "") {
443 OS << "<!doctype html>"
444 "<html>"
445 << BeginHeader;
446
447 // Link to a stylesheet if one is available. Otherwise, use the default style.
448 if (PathToStyle.empty())
449 OS << "<style>" << CSSForCoverage << "</style>";
450 else
451 OS << "<link rel='stylesheet' type='text/css' href='"
452 << escape(Str: PathToStyle, Opts) << "'>";
453
454 // Link to a JavaScript if one is available
455 if (PathToJavaScript.empty())
456 OS << "<script>" << JSForCoverage << "</script>";
457 else
458 OS << "<script src='" << escape(Str: PathToJavaScript, Opts) << "'></script>";
459
460 OS << EndHeader << "<body>";
461}
462
463void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts,
464 const std::string &FirstCol, const FileCoverageSummary &FCS,
465 bool IsTotals) {
466 SmallVector<std::string, 8> Columns;
467
468 // Format a coverage triple and add the result to the list of columns.
469 auto AddCoverageTripleToColumn =
470 [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) {
471 std::string S;
472 {
473 raw_string_ostream RSO{S};
474 if (Total)
475 RSO << format(Fmt: "%*.2f", Vals: 7, Vals: Pctg) << "% ";
476 else
477 RSO << "- ";
478 RSO << '(' << Hit << '/' << Total << ')';
479 }
480 const char *CellClass = "column-entry-yellow";
481 if (!Total)
482 CellClass = "column-entry-gray";
483 else if (Pctg >= Opts.HighCovWatermark)
484 CellClass = "column-entry-green";
485 else if (Pctg < Opts.LowCovWatermark)
486 CellClass = "column-entry-red";
487 Columns.emplace_back(Args: tag(Name: "td", Str: tag(Name: "pre", Str: S), ClassName: CellClass));
488 };
489
490 Columns.emplace_back(Args: tag(Name: "td", Str: tag(Name: "pre", Str: FirstCol)));
491 AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
492 FCS.FunctionCoverage.getNumFunctions(),
493 FCS.FunctionCoverage.getPercentCovered());
494 if (Opts.ShowInstantiationSummary)
495 AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
496 FCS.InstantiationCoverage.getNumFunctions(),
497 FCS.InstantiationCoverage.getPercentCovered());
498 AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
499 FCS.LineCoverage.getNumLines(),
500 FCS.LineCoverage.getPercentCovered());
501 if (Opts.ShowRegionSummary)
502 AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
503 FCS.RegionCoverage.getNumRegions(),
504 FCS.RegionCoverage.getPercentCovered());
505 if (Opts.ShowBranchSummary)
506 AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
507 FCS.BranchCoverage.getNumBranches(),
508 FCS.BranchCoverage.getPercentCovered());
509 if (Opts.ShowMCDCSummary)
510 AddCoverageTripleToColumn(FCS.MCDCCoverage.getCoveredPairs(),
511 FCS.MCDCCoverage.getNumPairs(),
512 FCS.MCDCCoverage.getPercentCovered());
513
514 if (IsTotals)
515 OS << tag(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""), ClassName: "light-row-bold");
516 else
517 OS << tag(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""), ClassName: "light-row");
518}
519
520void emitEpilog(raw_ostream &OS) {
521 OS << "</body>"
522 << "</html>";
523}
524
525} // anonymous namespace
526
527Expected<CoveragePrinter::OwnedStream>
528CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
529 auto OSOrErr = createOutputStream(Path, Extension: "html", InToplevel);
530 if (!OSOrErr)
531 return OSOrErr;
532
533 OwnedStream OS = std::move(OSOrErr.get());
534
535 if (!Opts.hasOutputDirectory()) {
536 emitPrelude(OS&: *OS.get(), Opts);
537 } else {
538 std::string ViewPath = getOutputPath(Path, Extension: "html", InToplevel);
539 emitPrelude(OS&: *OS.get(), Opts, PathToStyle: getPathToStyle(ViewPath),
540 PathToJavaScript: getPathToJavaScript(ViewPath));
541 }
542
543 return std::move(OS);
544}
545
546void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
547 emitEpilog(OS&: *OS.get());
548}
549
550/// Emit column labels for the table in the index.
551static void emitColumnLabelsForIndex(raw_ostream &OS,
552 const CoverageViewOptions &Opts) {
553 SmallVector<std::string, 4> Columns;
554 Columns.emplace_back(Args: tag(Name: "td", Str: "Filename", ClassName: "column-entry-bold"));
555 Columns.emplace_back(Args: tag(Name: "td", Str: "Function Coverage", ClassName: "column-entry-bold"));
556 if (Opts.ShowInstantiationSummary)
557 Columns.emplace_back(
558 Args: tag(Name: "td", Str: "Instantiation Coverage", ClassName: "column-entry-bold"));
559 Columns.emplace_back(Args: tag(Name: "td", Str: "Line Coverage", ClassName: "column-entry-bold"));
560 if (Opts.ShowRegionSummary)
561 Columns.emplace_back(Args: tag(Name: "td", Str: "Region Coverage", ClassName: "column-entry-bold"));
562 if (Opts.ShowBranchSummary)
563 Columns.emplace_back(Args: tag(Name: "td", Str: "Branch Coverage", ClassName: "column-entry-bold"));
564 if (Opts.ShowMCDCSummary)
565 Columns.emplace_back(Args: tag(Name: "td", Str: "MC/DC", ClassName: "column-entry-bold"));
566 OS << tag(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""));
567}
568
569std::string
570CoveragePrinterHTML::buildLinkToFile(StringRef SF,
571 const FileCoverageSummary &FCS) const {
572 SmallString<128> LinkTextStr(sys::path::relative_path(path: FCS.Name));
573 sys::path::remove_dots(path&: LinkTextStr, /*remove_dot_dot=*/true);
574 sys::path::native(path&: LinkTextStr);
575 std::string LinkText = escape(Str: LinkTextStr, Opts);
576 std::string LinkTarget =
577 escape(Str: getOutputPath(Path: SF, Extension: "html", /*InToplevel=*/false), Opts);
578 return a(Link: LinkTarget, Str: LinkText);
579}
580
581Error CoveragePrinterHTML::emitStyleSheet() {
582 auto CSSOrErr = createOutputStream(Path: "style", Extension: "css", /*InToplevel=*/true);
583 if (Error E = CSSOrErr.takeError())
584 return E;
585
586 OwnedStream CSS = std::move(CSSOrErr.get());
587 CSS->operator<<(Str: CSSForCoverage);
588
589 return Error::success();
590}
591
592Error CoveragePrinterHTML::emitJavaScript() {
593 auto JSOrErr = createOutputStream(Path: "control", Extension: "js", /*InToplevel=*/true);
594 if (Error E = JSOrErr.takeError())
595 return E;
596
597 OwnedStream JS = std::move(JSOrErr.get());
598 JS->operator<<(Str: JSForCoverage);
599
600 return Error::success();
601}
602
603void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
604 const std::string &Title) {
605 // Emit some basic information about the coverage report.
606 if (Opts.hasProjectTitle())
607 OSRef << tag(Name: ProjectTitleTag, Str: escape(Str: Opts.ProjectTitle, Opts));
608 OSRef << tag(Name: ReportTitleTag, Str: Title);
609 if (Opts.hasCreatedTime())
610 OSRef << tag(Name: CreatedTimeTag, Str: escape(Str: Opts.CreatedTimeStr, Opts));
611
612 // Emit a link to some documentation.
613 OSRef << tag(Name: "p", Str: "Click " +
614 a(Link: "http://clang.llvm.org/docs/"
615 "SourceBasedCodeCoverage.html#interpreting-reports",
616 Str: "here") +
617 " for information about interpreting this report.");
618
619 // Emit a table containing links to reports for each file in the covmapping.
620 // Exclude files which don't contain any regions.
621 OSRef << BeginCenteredDiv << BeginTable;
622 emitColumnLabelsForIndex(OS&: OSRef, Opts);
623}
624
625/// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
626/// false, link the summary to \p SF.
627void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
628 const FileCoverageSummary &FCS,
629 bool IsTotals) const {
630 // Simplify the display file path, and wrap it in a link if requested.
631 std::string Filename;
632 if (IsTotals) {
633 Filename = std::string(SF);
634 } else {
635 Filename = buildLinkToFile(SF, FCS);
636 }
637
638 emitTableRow(OS, Opts, FirstCol: Filename, FCS, IsTotals);
639}
640
641Error CoveragePrinterHTML::createIndexFile(
642 ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
643 const CoverageFiltersMatchAll &Filters) {
644 // Emit the default stylesheet.
645 if (Error E = emitStyleSheet())
646 return E;
647
648 // Emit the JavaScript UI implementation
649 if (Error E = emitJavaScript())
650 return E;
651
652 // Emit a file index along with some coverage statistics.
653 auto OSOrErr = createOutputStream(Path: "index", Extension: "html", /*InToplevel=*/true);
654 if (Error E = OSOrErr.takeError())
655 return E;
656 auto OS = std::move(OSOrErr.get());
657 raw_ostream &OSRef = *OS.get();
658
659 assert(Opts.hasOutputDirectory() && "No output directory for index file");
660 emitPrelude(OS&: OSRef, Opts, PathToStyle: getPathToStyle(ViewPath: ""), PathToJavaScript: getPathToJavaScript(ViewPath: ""));
661
662 emitReportHeader(OSRef, Title: "Coverage Report");
663
664 FileCoverageSummary Totals("TOTALS");
665 auto FileReports = CoverageReport::prepareFileReports(
666 Coverage, Totals, Files: SourceFiles, Options: Opts, Filters);
667 bool EmptyFiles = false;
668 for (unsigned I = 0, E = FileReports.size(); I < E; ++I) {
669 if (FileReports[I].FunctionCoverage.getNumFunctions())
670 emitFileSummary(OS&: OSRef, SF: SourceFiles[I], FCS: FileReports[I]);
671 else
672 EmptyFiles = true;
673 }
674 emitFileSummary(OS&: OSRef, SF: "Totals", FCS: Totals, /*IsTotals=*/true);
675 OSRef << EndTable << EndCenteredDiv;
676
677 // Emit links to files which don't contain any functions. These are normally
678 // not very useful, but could be relevant for code which abuses the
679 // preprocessor.
680 if (EmptyFiles && Filters.empty()) {
681 OSRef << tag(Name: "p", Str: "Files which contain no functions. (These "
682 "files contain code pulled into other files "
683 "by the preprocessor.)\n");
684 OSRef << BeginCenteredDiv << BeginTable;
685 for (unsigned I = 0, E = FileReports.size(); I < E; ++I)
686 if (!FileReports[I].FunctionCoverage.getNumFunctions()) {
687 std::string Link = buildLinkToFile(SF: SourceFiles[I], FCS: FileReports[I]);
688 OSRef << tag(Name: "tr", Str: tag(Name: "td", Str: tag(Name: "pre", Str: Link)), ClassName: "light-row") << '\n';
689 }
690 OSRef << EndTable << EndCenteredDiv;
691 }
692
693 OSRef << tag(Name: "h5", Str: escape(Str: Opts.getLLVMVersionString(), Opts));
694 emitEpilog(OS&: OSRef);
695
696 return Error::success();
697}
698
699struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
700 CoveragePrinterHTMLDirectory &Printer;
701
702 Reporter(CoveragePrinterHTMLDirectory &Printer,
703 const coverage::CoverageMapping &Coverage,
704 const CoverageFiltersMatchAll &Filters)
705 : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
706 Printer(Printer) {}
707
708 Error generateSubDirectoryReport(SubFileReports &&SubFiles,
709 SubDirReports &&SubDirs,
710 FileCoverageSummary &&SubTotals) override {
711 auto &LCPath = SubTotals.Name;
712 assert(Options.hasOutputDirectory() &&
713 "No output directory for index file");
714
715 SmallString<128> OSPath = LCPath;
716 sys::path::append(path&: OSPath, a: "index");
717 auto OSOrErr = Printer.createOutputStream(Path: OSPath, Extension: "html",
718 /*InToplevel=*/false);
719 if (auto E = OSOrErr.takeError())
720 return E;
721 auto OS = std::move(OSOrErr.get());
722 raw_ostream &OSRef = *OS.get();
723
724 auto IndexHtmlPath = Printer.getOutputPath(Path: (LCPath + "index").str(), Extension: "html",
725 /*InToplevel=*/false);
726 emitPrelude(OS&: OSRef, Opts: Options, PathToStyle: getPathToStyle(ViewPath: IndexHtmlPath),
727 PathToJavaScript: getPathToJavaScript(ViewPath: IndexHtmlPath));
728
729 auto NavLink = buildTitleLinks(LCPath);
730 Printer.emitReportHeader(OSRef, Title: "Coverage Report (" + NavLink + ")");
731
732 std::vector<const FileCoverageSummary *> EmptyFiles;
733
734 // Make directories at the top of the table.
735 for (auto &&SubDir : SubDirs) {
736 auto &Report = SubDir.second.first;
737 if (!Report.FunctionCoverage.getNumFunctions())
738 EmptyFiles.push_back(x: &Report);
739 else
740 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: buildRelLinkToFile(RelPath: Report.Name), FCS: Report,
741 /*IsTotals=*/false);
742 }
743
744 for (auto &&SubFile : SubFiles) {
745 auto &Report = SubFile.second;
746 if (!Report.FunctionCoverage.getNumFunctions())
747 EmptyFiles.push_back(x: &Report);
748 else
749 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: buildRelLinkToFile(RelPath: Report.Name), FCS: Report,
750 /*IsTotals=*/false);
751 }
752
753 // Emit the totals row.
754 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: "Totals", FCS: SubTotals, /*IsTotals=*/false);
755 OSRef << EndTable << EndCenteredDiv;
756
757 // Emit links to files which don't contain any functions. These are normally
758 // not very useful, but could be relevant for code which abuses the
759 // preprocessor.
760 if (!EmptyFiles.empty()) {
761 OSRef << tag(Name: "p", Str: "Files which contain no functions. (These "
762 "files contain code pulled into other files "
763 "by the preprocessor.)\n");
764 OSRef << BeginCenteredDiv << BeginTable;
765 for (auto FCS : EmptyFiles) {
766 auto Link = buildRelLinkToFile(RelPath: FCS->Name);
767 OSRef << tag(Name: "tr", Str: tag(Name: "td", Str: tag(Name: "pre", Str: Link)), ClassName: "light-row") << '\n';
768 }
769 OSRef << EndTable << EndCenteredDiv;
770 }
771
772 // Emit epilog.
773 OSRef << tag(Name: "h5", Str: escape(Str: Options.getLLVMVersionString(), Opts: Options));
774 emitEpilog(OS&: OSRef);
775
776 return Error::success();
777 }
778
779 /// Make a title with hyperlinks to the index.html files of each hierarchy
780 /// of the report.
781 std::string buildTitleLinks(StringRef LCPath) const {
782 // For each report level in LCPStack, extract the path component and
783 // calculate the number of "../" relative to current LCPath.
784 SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components;
785
786 auto Iter = LCPStack.begin(), IterE = LCPStack.end();
787 SmallString<128> RootPath;
788 if (*Iter == 0) {
789 // If llvm-cov works on relative coverage mapping data, the LCP of
790 // all source file paths can be 0, which makes the title path empty.
791 // As we like adding a slash at the back of the path to indicate a
792 // directory, in this case, we use "." as the root path to make it
793 // not be confused with the root path "/".
794 RootPath = ".";
795 } else {
796 RootPath = LCPath.substr(Start: 0, N: *Iter);
797 sys::path::native(path&: RootPath);
798 sys::path::remove_dots(path&: RootPath, /*remove_dot_dot=*/true);
799 }
800 Components.emplace_back(Args: std::move(RootPath), Args: 0);
801
802 for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
803 SmallString<128> SubPath = LCPath.substr(Start: Last, N: *Iter - Last);
804 sys::path::native(path&: SubPath);
805 sys::path::remove_dots(path&: SubPath, /*remove_dot_dot=*/true);
806 auto Level = unsigned(SubPath.count(Str: sys::path::get_separator())) + 1;
807 Components.back().second += Level;
808 Components.emplace_back(Args: std::move(SubPath), Args&: Level);
809 }
810
811 // Then we make the title accroding to Components.
812 std::string S;
813 for (auto I = Components.begin(), E = Components.end();;) {
814 auto &Name = I->first;
815 if (++I == E) {
816 S += a(Link: "./index.html", Str: Name);
817 S += sys::path::get_separator();
818 break;
819 }
820
821 SmallString<128> Link;
822 for (unsigned J = I->second; J > 0; --J)
823 Link += "../";
824 Link += "index.html";
825 S += a(Link, Str: Name);
826 S += sys::path::get_separator();
827 }
828 return S;
829 }
830
831 std::string buildRelLinkToFile(StringRef RelPath) const {
832 SmallString<128> LinkTextStr(RelPath);
833 sys::path::native(path&: LinkTextStr);
834
835 // remove_dots will remove trailing slash, so we need to check before it.
836 auto IsDir = LinkTextStr.ends_with(Suffix: sys::path::get_separator());
837 sys::path::remove_dots(path&: LinkTextStr, /*remove_dot_dot=*/true);
838
839 SmallString<128> LinkTargetStr(LinkTextStr);
840 if (IsDir) {
841 LinkTextStr += sys::path::get_separator();
842 sys::path::append(path&: LinkTargetStr, a: "index.html");
843 } else {
844 LinkTargetStr += ".html";
845 }
846
847 auto LinkText = escape(Str: LinkTextStr, Opts: Options);
848 auto LinkTarget = escape(Str: LinkTargetStr, Opts: Options);
849 return a(Link: LinkTarget, Str: LinkText);
850 }
851};
852
853Error CoveragePrinterHTMLDirectory::createIndexFile(
854 ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
855 const CoverageFiltersMatchAll &Filters) {
856 // The createSubIndexFile function only works when SourceFiles is
857 // more than one. So we fallback to CoveragePrinterHTML when it is.
858 if (SourceFiles.size() <= 1)
859 return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters);
860
861 // Emit the default stylesheet.
862 if (Error E = emitStyleSheet())
863 return E;
864
865 // Emit the JavaScript UI implementation
866 if (Error E = emitJavaScript())
867 return E;
868
869 // Emit index files in every subdirectory.
870 Reporter Report(*this, Coverage, Filters);
871 auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
872 if (auto E = TotalsOrErr.takeError())
873 return E;
874 auto &LCPath = TotalsOrErr->Name;
875
876 // Emit the top level index file. Top level index file is just a redirection
877 // to the index file in the LCP directory.
878 auto OSOrErr = createOutputStream(Path: "index", Extension: "html", /*InToplevel=*/true);
879 if (auto E = OSOrErr.takeError())
880 return E;
881 auto OS = std::move(OSOrErr.get());
882 auto LCPIndexFilePath =
883 getOutputPath(Path: (LCPath + "index").str(), Extension: "html", /*InToplevel=*/false);
884 *OS.get() << R"(<!DOCTYPE html>
885 <html>
886 <head>
887 <meta http-equiv="Refresh" content="0; url=')"
888 << LCPIndexFilePath << R"('" />
889 </head>
890 <body></body>
891 </html>
892 )";
893
894 return Error::success();
895}
896
897void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
898 OS << BeginCenteredDiv << BeginTable;
899}
900
901void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
902 OS << EndTable << EndCenteredDiv;
903}
904
905void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
906 OS << BeginSourceNameDiv << tag(Name: "pre", Str: escape(Str: getSourceName(), Opts: getOptions()))
907 << EndSourceNameDiv;
908}
909
910void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
911 OS << "<tr>";
912}
913
914void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
915 // If this view has sub-views, renderLine() cannot close the view's cell.
916 // Take care of it here, after all sub-views have been rendered.
917 if (hasSubViews())
918 OS << EndCodeTD;
919 OS << "</tr>";
920}
921
922void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
923 // The table-based output makes view dividers unnecessary.
924}
925
926void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
927 const LineCoverageStats &LCS,
928 unsigned ExpansionCol, unsigned) {
929 StringRef Line = L.Line;
930 unsigned LineNo = L.LineNo;
931
932 // Steps for handling text-escaping, highlighting, and tooltip creation:
933 //
934 // 1. Split the line into N+1 snippets, where N = |Segments|. The first
935 // snippet starts from Col=1 and ends at the start of the first segment.
936 // The last snippet starts at the last mapped column in the line and ends
937 // at the end of the line. Both are required but may be empty.
938
939 SmallVector<std::string, 8> Snippets;
940 CoverageSegmentArray Segments = LCS.getLineSegments();
941
942 unsigned LCol = 1;
943 auto Snip = [&](unsigned Start, unsigned Len) {
944 Snippets.push_back(Elt: std::string(Line.substr(Start, N: Len)));
945 LCol += Len;
946 };
947
948 Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
949
950 for (unsigned I = 1, E = Segments.size(); I < E; ++I)
951 Snip(LCol - 1, Segments[I]->Col - LCol);
952
953 // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
954 Snip(LCol - 1, Line.size() + 1 - LCol);
955
956 // 2. Escape all of the snippets.
957
958 for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
959 Snippets[I] = escape(Str: Snippets[I], Opts: getOptions());
960
961 // 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment
962 // 1 to set the highlight for snippet 2, segment 2 to set the highlight for
963 // snippet 3, and so on.
964
965 std::optional<StringRef> Color;
966 SmallVector<std::pair<unsigned, unsigned>, 2> HighlightedRanges;
967 auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
968 if (getOptions().Debug)
969 HighlightedRanges.emplace_back(Args&: LC, Args&: RC);
970 if (Snippet.empty())
971 return tag(Name: "span", Str: Snippet, ClassName: std::string(*Color));
972 else
973 return tag(Name: "span", Str: Snippet, ClassName: "region " + std::string(*Color));
974 };
975
976 auto CheckIfUncovered = [&](const CoverageSegment *S) {
977 return S && (!S->IsGapRegion || (Color && *Color == "red")) &&
978 S->HasCount && S->Count == 0;
979 };
980
981 if (CheckIfUncovered(LCS.getWrappedSegment())) {
982 Color = "red";
983 if (!Snippets[0].empty())
984 Snippets[0] = Highlight(Snippets[0], 1, 1 + Snippets[0].size());
985 }
986
987 for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
988 const auto *CurSeg = Segments[I];
989 if (CheckIfUncovered(CurSeg))
990 Color = "red";
991 else if (CurSeg->Col == ExpansionCol)
992 Color = "cyan";
993 else
994 Color = std::nullopt;
995
996 if (Color)
997 Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
998 CurSeg->Col + Snippets[I + 1].size());
999 }
1000
1001 if (Color && Segments.empty())
1002 Snippets.back() = Highlight(Snippets.back(), 1, 1 + Snippets.back().size());
1003
1004 if (getOptions().Debug) {
1005 for (const auto &Range : HighlightedRanges) {
1006 errs() << "Highlighted line " << LineNo << ", " << Range.first << " -> ";
1007 if (Range.second == 0)
1008 errs() << "?";
1009 else
1010 errs() << Range.second;
1011 errs() << "\n";
1012 }
1013 }
1014
1015 // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
1016 // sub-line region count tooltips if needed.
1017
1018 if (shouldRenderRegionMarkers(LCS)) {
1019 // Just consider the segments which start *and* end on this line.
1020 for (unsigned I = 0, E = Segments.size() - 1; I < E; ++I) {
1021 const auto *CurSeg = Segments[I];
1022 auto CurSegCount = BinaryCount(N: CurSeg->Count);
1023 auto LCSCount = BinaryCount(N: LCS.getExecutionCount());
1024 if (!CurSeg->IsRegionEntry)
1025 continue;
1026 if (CurSegCount == LCSCount)
1027 continue;
1028
1029 Snippets[I + 1] =
1030 tag(Name: "div",
1031 Str: Snippets[I + 1] +
1032 tag(Name: "span", Str: formatCount(N: CurSegCount), ClassName: "tooltip-content"),
1033 ClassName: "tooltip");
1034
1035 if (getOptions().Debug)
1036 errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
1037 << formatCount(N: CurSegCount) << "\n";
1038 }
1039 }
1040
1041 OS << BeginCodeTD;
1042 OS << BeginPre;
1043 for (const auto &Snippet : Snippets)
1044 OS << Snippet;
1045 OS << EndPre;
1046
1047 // If there are no sub-views left to attach to this cell, end the cell.
1048 // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
1049 if (!hasSubViews())
1050 OS << EndCodeTD;
1051}
1052
1053void SourceCoverageViewHTML::renderLineCoverageColumn(
1054 raw_ostream &OS, const LineCoverageStats &Line) {
1055 std::string Count;
1056 if (Line.isMapped())
1057 Count = tag(Name: "pre", Str: formatBinaryCount(N: Line.getExecutionCount()));
1058 std::string CoverageClass =
1059 (Line.getExecutionCount() > 0)
1060 ? "covered-line"
1061 : (Line.isMapped() ? "uncovered-line" : "skipped-line");
1062 OS << tag(Name: "td", Str: Count, ClassName: CoverageClass);
1063}
1064
1065void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
1066 unsigned LineNo) {
1067 std::string LineNoStr = utostr(X: uint64_t(LineNo));
1068 std::string TargetName = "L" + LineNoStr;
1069 OS << tag(Name: "td", Str: a(Link: "#" + TargetName, Str: tag(Name: "pre", Str: LineNoStr), TargetName),
1070 ClassName: "line-number");
1071}
1072
1073void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
1074 const LineCoverageStats &Line,
1075 unsigned) {
1076 // Region markers are rendered in-line using tooltips.
1077}
1078
1079void SourceCoverageViewHTML::renderExpansionSite(raw_ostream &OS, LineRef L,
1080 const LineCoverageStats &LCS,
1081 unsigned ExpansionCol,
1082 unsigned ViewDepth) {
1083 // Render the line containing the expansion site. No extra formatting needed.
1084 renderLine(OS, L, LCS, ExpansionCol, ViewDepth);
1085}
1086
1087void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
1088 ExpansionView &ESV,
1089 unsigned ViewDepth) {
1090 OS << BeginExpansionDiv;
1091 ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
1092 /*ShowTitle=*/false, ViewDepth: ViewDepth + 1);
1093 OS << EndExpansionDiv;
1094}
1095
1096void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
1097 unsigned ViewDepth) {
1098 // Render the child subview.
1099 if (getOptions().Debug)
1100 errs() << "Branch at line " << BRV.getLine() << '\n';
1101
1102 auto BranchCount = [&](StringRef Label, uint64_t Count, bool Folded,
1103 double Total) {
1104 if (Folded)
1105 return std::string{"Folded"};
1106
1107 std::string Str;
1108 raw_string_ostream OS(Str);
1109
1110 OS << tag(Name: "span", Str: Label, ClassName: (Count ? "None" : "red branch")) << ": ";
1111 if (getOptions().ShowBranchCounts)
1112 OS << tag(Name: "span", Str: formatBinaryCount(N: Count),
1113 ClassName: (Count ? "covered-line" : "uncovered-line"));
1114 else
1115 OS << format(Fmt: "%0.2f", Vals: (Total != 0 ? 100.0 * Count / Total : 0.0)) << "%";
1116
1117 return Str;
1118 };
1119
1120 OS << BeginExpansionDiv;
1121 OS << BeginPre;
1122 for (const auto &R : BRV.Regions) {
1123 // This can be `double` since it is only used as a denominator.
1124 // FIXME: It is still inaccurate if Count is greater than (1LL << 53).
1125 double Total =
1126 static_cast<double>(R.ExecutionCount) + R.FalseExecutionCount;
1127
1128 // Display Line + Column.
1129 std::string LineNoStr = utostr(X: uint64_t(R.LineStart));
1130 std::string ColNoStr = utostr(X: uint64_t(R.ColumnStart));
1131 std::string TargetName = "L" + LineNoStr;
1132
1133 OS << " Branch (";
1134 OS << tag(Name: "span",
1135 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr),
1136 TargetName),
1137 ClassName: "line-number") +
1138 "): [";
1139
1140 if (R.TrueFolded && R.FalseFolded) {
1141 OS << "Folded - Ignored]\n";
1142 continue;
1143 }
1144
1145 OS << BranchCount("True", R.ExecutionCount, R.TrueFolded, Total) << ", "
1146 << BranchCount("False", R.FalseExecutionCount, R.FalseFolded, Total)
1147 << "]\n";
1148 }
1149 OS << EndPre;
1150 OS << EndExpansionDiv;
1151}
1152
1153void SourceCoverageViewHTML::renderMCDCView(raw_ostream &OS, MCDCView &MRV,
1154 unsigned ViewDepth) {
1155 for (auto &Record : MRV.Records) {
1156 OS << BeginExpansionDiv;
1157 OS << BeginPre;
1158 OS << " MC/DC Decision Region (";
1159
1160 // Display Line + Column information.
1161 const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion();
1162 std::string LineNoStr = Twine(DecisionRegion.LineStart).str();
1163 std::string ColNoStr = Twine(DecisionRegion.ColumnStart).str();
1164 std::string TargetName = "L" + LineNoStr;
1165 OS << tag(Name: "span",
1166 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr)),
1167 ClassName: "line-number") +
1168 ") to (";
1169 LineNoStr = utostr(X: uint64_t(DecisionRegion.LineEnd));
1170 ColNoStr = utostr(X: uint64_t(DecisionRegion.ColumnEnd));
1171 OS << tag(Name: "span",
1172 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr)),
1173 ClassName: "line-number") +
1174 ")\n\n";
1175
1176 // Display MC/DC Information.
1177 OS << " Number of Conditions: " << Record.getNumConditions() << "\n";
1178 for (unsigned i = 0; i < Record.getNumConditions(); i++) {
1179 OS << " " << Record.getConditionHeaderString(Condition: i);
1180 }
1181 OS << "\n";
1182 OS << " Executed MC/DC Test Vectors:\n\n ";
1183 OS << Record.getTestVectorHeaderString();
1184 for (unsigned i = 0; i < Record.getNumTestVectors(); i++)
1185 OS << Record.getTestVectorString(TestVectorIndex: i);
1186 OS << "\n";
1187 for (unsigned i = 0; i < Record.getNumConditions(); i++)
1188 OS << Record.getConditionCoverageString(Condition: i);
1189 OS << " MC/DC Coverage for Expression: ";
1190 OS << format(Fmt: "%0.2f", Vals: Record.getPercentCovered()) << "%\n";
1191 OS << EndPre;
1192 OS << EndExpansionDiv;
1193 }
1194}
1195
1196void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
1197 InstantiationView &ISV,
1198 unsigned ViewDepth) {
1199 OS << BeginExpansionDiv;
1200 if (!ISV.View)
1201 OS << BeginSourceNameDiv
1202 << tag(Name: "pre",
1203 Str: escape(Str: "Unexecuted instantiation: " + ISV.FunctionName.str(),
1204 Opts: getOptions()))
1205 << EndSourceNameDiv;
1206 else
1207 ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true,
1208 /*ShowTitle=*/false, ViewDepth);
1209 OS << EndExpansionDiv;
1210}
1211
1212void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
1213 if (getOptions().hasProjectTitle())
1214 OS << tag(Name: ProjectTitleTag, Str: escape(Str: getOptions().ProjectTitle, Opts: getOptions()));
1215 OS << tag(Name: ReportTitleTag, Str: escape(Str: Title, Opts: getOptions()));
1216 if (getOptions().hasCreatedTime())
1217 OS << tag(Name: CreatedTimeTag,
1218 Str: escape(Str: getOptions().CreatedTimeStr, Opts: getOptions()));
1219
1220 OS << tag(Name: "span",
1221 Str: a(Link: "javascript:next_line()", Str: "next uncovered line (L)") + ", " +
1222 a(Link: "javascript:next_region()", Str: "next uncovered region (R)") +
1223 ", " +
1224 a(Link: "javascript:next_branch()", Str: "next uncovered branch (B)"),
1225 ClassName: "control");
1226}
1227
1228void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1229 unsigned ViewDepth) {
1230 std::string Links;
1231
1232 renderLinePrefix(OS, ViewDepth);
1233 OS << tag(Name: "td", Str: tag(Name: "pre", Str: "Line")) << tag(Name: "td", Str: tag(Name: "pre", Str: "Count"));
1234 OS << tag(Name: "td", Str: tag(Name: "pre", Str: "Source" + Links));
1235 renderLineSuffix(OS, ViewDepth);
1236}
1237