1 //===- SourceCoverageViewHTML.cpp - A html code coverage view -------------===//
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
7 //===----------------------------------------------------------------------===//
9 /// \file This file implements the html coverage renderer.
11 //===----------------------------------------------------------------------===//
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"
26 // Return a string with the special characters in \p Str escaped.
27 std::string
escape(StringRef Str
, const CoverageViewOptions
&Opts
) {
28 std::string TabExpandedResult
;
29 unsigned ColNum
= 0; // Record the column number.
32 // Replace '\t' with up to TabSize spaces.
33 unsigned NumSpaces
= Opts
.TabSize
- (ColNum
% Opts
.TabSize
);
34 TabExpandedResult
.append(NumSpaces
, ' ');
37 TabExpandedResult
+= C
;
38 if (C
== '\n' || C
== '\r')
44 std::string EscapedHTML
;
46 raw_string_ostream OS
{EscapedHTML
};
47 printHTMLEscaped(TabExpandedResult
, OS
);
52 // Create a \p Name tag around \p Str, and optionally set its \p ClassName.
53 std::string
tag(StringRef Name
, StringRef Str
, StringRef ClassName
= "") {
54 std::string Tag
= "<";
56 if (!ClassName
.empty()) {
69 // Create an anchor to \p Link with the label \p Str.
70 std::string
a(StringRef Link
, StringRef Str
, StringRef TargetName
= "") {
73 if (!TargetName
.empty()) {
86 const char *BeginHeader
=
88 "<meta name='viewport' content='width=device-width,initial-scale=1'>"
89 "<meta charset='UTF-8'>";
91 const char *JSForCoverage
=
93 function next_uncovered(selector, reverse, scroll_selector) {
94 function visit_element(element) {
95 element.classList.add("seen
");
96 element.classList.add("selected
");
98 if (!scroll_selector) {
99 scroll_selector = "tr
:has(.selected
) td
.line
-number
"
102 const scroll_to = document.querySelector(scroll_selector);
104 scroll_to.scrollIntoView({behavior: "smooth
", block: "center
", inline: "end
"});
108 function select_one() {
110 const previously_selected = document.querySelector(".selected
");
112 if (previously_selected) {
113 previously_selected.classList.remove("selected
");
116 return document.querySelector(selector + ":not(.seen
)");
118 const previously_selected = document.querySelector(".selected
");
120 if (previously_selected) {
121 previously_selected.classList.remove("selected
");
122 previously_selected.classList.remove("seen
");
125 const nodes = document.querySelectorAll(selector + ".seen
");
127 const last = nodes[nodes.length - 1]; // last
135 function reset_all() {
137 const all_seen = document.querySelectorAll(selector + ".seen
");
140 all_seen.forEach(e => e.classList.remove("seen
"));
143 const all_seen = document.querySelectorAll(selector + ":not(.seen
)");
146 all_seen.forEach(e => e.classList.add("seen
"));
152 const uncovered = select_one();
155 visit_element(uncovered);
159 const uncovered = select_one();
162 visit_element(uncovered);
167 function next_line(reverse) {
168 next_uncovered("td
.uncovered
-line
", reverse)
171 function next_region(reverse) {
172 next_uncovered("span
.red
.region
", reverse);
175 function next_branch(reverse) {
176 next_uncovered("span
.red
.branch
", reverse);
179 document.addEventListener("keypress
", function(event) {
180 const reverse = event.shiftKey;
181 if (event.code == "KeyL
") {
184 if (event.code == "KeyB
") {
185 next_branch(reverse);
187 if (event.code == "KeyR
") {
188 next_region(reverse);
193 const char *CSSForCoverage
=
195 background-color: #f004;
198 background-color: cyan;
201 scroll-behavior: smooth;
204 font-family: -apple-system, sans-serif;
207 margin-top: 0px !important;
208 margin-bottom: 0px !important;
212 border-bottom: 1px solid #8888;
213 background-color: #0002;
220 border: 1px solid #8888;
228 border: 1px solid #8888;
232 border-collapse: collapse;
235 border: 1px solid #8888;
240 border: 1px solid #8888;
252 .column-entry-yellow {
254 background-color: #ff06;
258 background-color: #f004;
262 background-color: #fff4;
264 .column-entry-green {
266 background-color: #0f04;
279 .uncovered-line.selected {
283 .region.red.selected {
284 background-color: #f008;
287 .branch.red.selected {
288 background-color: #f008;
294 background-color: #bef;
295 text-decoration: none;
297 .tooltip span.tooltip-content {
309 .tooltip span.tooltip-content:after {
316 border-top: 8px solid #000000;
317 border-right: 8px solid transparent;
318 border-left: 8px solid transparent;
320 :hover.tooltip span.tooltip-content {
330 border-collapse: collapse;
331 border-right: 1px solid #8888;
332 border-left: 1px solid #8888;
336 display: inline-block;
337 text-decoration: inherit;
346 background-color: #eee;
351 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
352 background-color: #8884;
364 @media (prefers-color-scheme: dark) {
366 background-color: #222;
370 background-color: #111;
379 background-color: #068;
384 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
385 background-color: #8884;
390 const char *EndHeader
= "</head>";
392 const char *BeginCenteredDiv
= "<div class='centered'>";
394 const char *EndCenteredDiv
= "</div>";
396 const char *BeginSourceNameDiv
= "<div class='source-name-title'>";
398 const char *EndSourceNameDiv
= "</div>";
400 const char *BeginCodeTD
= "<td class='code'>";
402 const char *EndCodeTD
= "</td>";
404 const char *BeginPre
= "<pre>";
406 const char *EndPre
= "</pre>";
408 const char *BeginExpansionDiv
= "<div class='expansion-view'>";
410 const char *EndExpansionDiv
= "</div>";
412 const char *BeginTable
= "<table>";
414 const char *EndTable
= "</table>";
416 const char *ProjectTitleTag
= "h1";
418 const char *ReportTitleTag
= "h2";
420 const char *CreatedTimeTag
= "h4";
422 std::string
getPathToStyle(StringRef ViewPath
) {
423 std::string PathToStyle
;
424 std::string PathSep
= std::string(sys::path::get_separator());
425 unsigned NumSeps
= ViewPath
.count(PathSep
);
426 for (unsigned I
= 0, E
= NumSeps
; I
< E
; ++I
)
427 PathToStyle
+= ".." + PathSep
;
428 return PathToStyle
+ "style.css";
431 std::string
getPathToJavaScript(StringRef ViewPath
) {
432 std::string PathToJavaScript
;
433 std::string PathSep
= std::string(sys::path::get_separator());
434 unsigned NumSeps
= ViewPath
.count(PathSep
);
435 for (unsigned I
= 0, E
= NumSeps
; I
< E
; ++I
)
436 PathToJavaScript
+= ".." + PathSep
;
437 return PathToJavaScript
+ "control.js";
440 void emitPrelude(raw_ostream
&OS
, const CoverageViewOptions
&Opts
,
441 const std::string
&PathToStyle
= "",
442 const std::string
&PathToJavaScript
= "") {
443 OS
<< "<!doctype html>"
447 // Link to a stylesheet if one is available. Otherwise, use the default style.
448 if (PathToStyle
.empty())
449 OS
<< "<style>" << CSSForCoverage
<< "</style>";
451 OS
<< "<link rel='stylesheet' type='text/css' href='"
452 << escape(PathToStyle
, Opts
) << "'>";
454 // Link to a JavaScript if one is available
455 if (PathToJavaScript
.empty())
456 OS
<< "<script>" << JSForCoverage
<< "</script>";
458 OS
<< "<script src='" << escape(PathToJavaScript
, Opts
) << "'></script>";
460 OS
<< EndHeader
<< "<body>";
463 void emitTableRow(raw_ostream
&OS
, const CoverageViewOptions
&Opts
,
464 const std::string
&FirstCol
, const FileCoverageSummary
&FCS
,
466 SmallVector
<std::string
, 8> Columns
;
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
) {
473 raw_string_ostream RSO
{S
};
475 RSO
<< format("%*.2f", 7, Pctg
) << "% ";
478 RSO
<< '(' << Hit
<< '/' << Total
<< ')';
480 const char *CellClass
= "column-entry-yellow";
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(tag("td", tag("pre", S
), CellClass
));
490 Columns
.emplace_back(tag("td", tag("pre", 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());
515 OS
<< tag("tr", join(Columns
.begin(), Columns
.end(), ""), "light-row-bold");
517 OS
<< tag("tr", join(Columns
.begin(), Columns
.end(), ""), "light-row");
520 void emitEpilog(raw_ostream
&OS
) {
525 } // anonymous namespace
527 Expected
<CoveragePrinter::OwnedStream
>
528 CoveragePrinterHTML::createViewFile(StringRef Path
, bool InToplevel
) {
529 auto OSOrErr
= createOutputStream(Path
, "html", InToplevel
);
533 OwnedStream OS
= std::move(OSOrErr
.get());
535 if (!Opts
.hasOutputDirectory()) {
536 emitPrelude(*OS
.get(), Opts
);
538 std::string ViewPath
= getOutputPath(Path
, "html", InToplevel
);
539 emitPrelude(*OS
.get(), Opts
, getPathToStyle(ViewPath
),
540 getPathToJavaScript(ViewPath
));
543 return std::move(OS
);
546 void CoveragePrinterHTML::closeViewFile(OwnedStream OS
) {
547 emitEpilog(*OS
.get());
550 /// Emit column labels for the table in the index.
551 static void emitColumnLabelsForIndex(raw_ostream
&OS
,
552 const CoverageViewOptions
&Opts
) {
553 SmallVector
<std::string
, 4> Columns
;
554 Columns
.emplace_back(tag("td", "Filename", "column-entry-bold"));
555 Columns
.emplace_back(tag("td", "Function Coverage", "column-entry-bold"));
556 if (Opts
.ShowInstantiationSummary
)
557 Columns
.emplace_back(
558 tag("td", "Instantiation Coverage", "column-entry-bold"));
559 Columns
.emplace_back(tag("td", "Line Coverage", "column-entry-bold"));
560 if (Opts
.ShowRegionSummary
)
561 Columns
.emplace_back(tag("td", "Region Coverage", "column-entry-bold"));
562 if (Opts
.ShowBranchSummary
)
563 Columns
.emplace_back(tag("td", "Branch Coverage", "column-entry-bold"));
564 if (Opts
.ShowMCDCSummary
)
565 Columns
.emplace_back(tag("td", "MC/DC", "column-entry-bold"));
566 OS
<< tag("tr", join(Columns
.begin(), Columns
.end(), ""));
570 CoveragePrinterHTML::buildLinkToFile(StringRef SF
,
571 const FileCoverageSummary
&FCS
) const {
572 SmallString
<128> LinkTextStr(sys::path::relative_path(FCS
.Name
));
573 sys::path::remove_dots(LinkTextStr
, /*remove_dot_dot=*/true);
574 sys::path::native(LinkTextStr
);
575 std::string LinkText
= escape(LinkTextStr
, Opts
);
576 std::string LinkTarget
=
577 escape(getOutputPath(SF
, "html", /*InToplevel=*/false), Opts
);
578 return a(LinkTarget
, LinkText
);
581 Error
CoveragePrinterHTML::emitStyleSheet() {
582 auto CSSOrErr
= createOutputStream("style", "css", /*InToplevel=*/true);
583 if (Error E
= CSSOrErr
.takeError())
586 OwnedStream CSS
= std::move(CSSOrErr
.get());
587 CSS
->operator<<(CSSForCoverage
);
589 return Error::success();
592 Error
CoveragePrinterHTML::emitJavaScript() {
593 auto JSOrErr
= createOutputStream("control", "js", /*InToplevel=*/true);
594 if (Error E
= JSOrErr
.takeError())
597 OwnedStream JS
= std::move(JSOrErr
.get());
598 JS
->operator<<(JSForCoverage
);
600 return Error::success();
603 void 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(ProjectTitleTag
, escape(Opts
.ProjectTitle
, Opts
));
608 OSRef
<< tag(ReportTitleTag
, Title
);
609 if (Opts
.hasCreatedTime())
610 OSRef
<< tag(CreatedTimeTag
, escape(Opts
.CreatedTimeStr
, Opts
));
612 // Emit a link to some documentation.
613 OSRef
<< tag("p", "Click " +
614 a("http://clang.llvm.org/docs/"
615 "SourceBasedCodeCoverage.html#interpreting-reports",
617 " for information about interpreting this report.");
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(OSRef
, Opts
);
625 /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
626 /// false, link the summary to \p SF.
627 void 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
;
633 Filename
= std::string(SF
);
635 Filename
= buildLinkToFile(SF
, FCS
);
638 emitTableRow(OS
, Opts
, Filename
, FCS
, IsTotals
);
641 Error
CoveragePrinterHTML::createIndexFile(
642 ArrayRef
<std::string
> SourceFiles
, const CoverageMapping
&Coverage
,
643 const CoverageFiltersMatchAll
&Filters
) {
644 // Emit the default stylesheet.
645 if (Error E
= emitStyleSheet())
648 // Emit the JavaScript UI implementation
649 if (Error E
= emitJavaScript())
652 // Emit a file index along with some coverage statistics.
653 auto OSOrErr
= createOutputStream("index", "html", /*InToplevel=*/true);
654 if (Error E
= OSOrErr
.takeError())
656 auto OS
= std::move(OSOrErr
.get());
657 raw_ostream
&OSRef
= *OS
.get();
659 assert(Opts
.hasOutputDirectory() && "No output directory for index file");
660 emitPrelude(OSRef
, Opts
, getPathToStyle(""), getPathToJavaScript(""));
662 emitReportHeader(OSRef
, "Coverage Report");
664 FileCoverageSummary
Totals("TOTALS");
665 auto FileReports
= CoverageReport::prepareFileReports(
666 Coverage
, Totals
, SourceFiles
, 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(OSRef
, SourceFiles
[I
], FileReports
[I
]);
674 emitFileSummary(OSRef
, "Totals", Totals
, /*IsTotals=*/true);
675 OSRef
<< EndTable
<< EndCenteredDiv
;
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
680 if (EmptyFiles
&& Filters
.empty()) {
681 OSRef
<< tag("p", "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(SourceFiles
[I
], FileReports
[I
]);
688 OSRef
<< tag("tr", tag("td", tag("pre", Link
)), "light-row") << '\n';
690 OSRef
<< EndTable
<< EndCenteredDiv
;
693 OSRef
<< tag("h5", escape(Opts
.getLLVMVersionString(), Opts
));
696 return Error::success();
699 struct CoveragePrinterHTMLDirectory::Reporter
: public DirectoryCoverageReport
{
700 CoveragePrinterHTMLDirectory
&Printer
;
702 Reporter(CoveragePrinterHTMLDirectory
&Printer
,
703 const coverage::CoverageMapping
&Coverage
,
704 const CoverageFiltersMatchAll
&Filters
)
705 : DirectoryCoverageReport(Printer
.Opts
, Coverage
, Filters
),
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");
715 SmallString
<128> OSPath
= LCPath
;
716 sys::path::append(OSPath
, "index");
717 auto OSOrErr
= Printer
.createOutputStream(OSPath
, "html",
718 /*InToplevel=*/false);
719 if (auto E
= OSOrErr
.takeError())
721 auto OS
= std::move(OSOrErr
.get());
722 raw_ostream
&OSRef
= *OS
.get();
724 auto IndexHtmlPath
= Printer
.getOutputPath((LCPath
+ "index").str(), "html",
725 /*InToplevel=*/false);
726 emitPrelude(OSRef
, Options
, getPathToStyle(IndexHtmlPath
),
727 getPathToJavaScript(IndexHtmlPath
));
729 auto NavLink
= buildTitleLinks(LCPath
);
730 Printer
.emitReportHeader(OSRef
, "Coverage Report (" + NavLink
+ ")");
732 std::vector
<const FileCoverageSummary
*> EmptyFiles
;
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(&Report
);
740 emitTableRow(OSRef
, Options
, buildRelLinkToFile(Report
.Name
), Report
,
744 for (auto &&SubFile
: SubFiles
) {
745 auto &Report
= SubFile
.second
;
746 if (!Report
.FunctionCoverage
.getNumFunctions())
747 EmptyFiles
.push_back(&Report
);
749 emitTableRow(OSRef
, Options
, buildRelLinkToFile(Report
.Name
), Report
,
753 // Emit the totals row.
754 emitTableRow(OSRef
, Options
, "Totals", SubTotals
, /*IsTotals=*/false);
755 OSRef
<< EndTable
<< EndCenteredDiv
;
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
760 if (!EmptyFiles
.empty()) {
761 OSRef
<< tag("p", "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(FCS
->Name
);
767 OSRef
<< tag("tr", tag("td", tag("pre", Link
)), "light-row") << '\n';
769 OSRef
<< EndTable
<< EndCenteredDiv
;
773 OSRef
<< tag("h5", escape(Options
.getLLVMVersionString(), Options
));
776 return Error::success();
779 /// Make a title with hyperlinks to the index.html files of each hierarchy
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
;
786 auto Iter
= LCPStack
.begin(), IterE
= LCPStack
.end();
787 SmallString
<128> RootPath
;
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 "/".
796 RootPath
= LCPath
.substr(0, *Iter
);
797 sys::path::native(RootPath
);
798 sys::path::remove_dots(RootPath
, /*remove_dot_dot=*/true);
800 Components
.emplace_back(std::move(RootPath
), 0);
802 for (auto Last
= *Iter
; ++Iter
!= IterE
; Last
= *Iter
) {
803 SmallString
<128> SubPath
= LCPath
.substr(Last
, *Iter
- Last
);
804 sys::path::native(SubPath
);
805 sys::path::remove_dots(SubPath
, /*remove_dot_dot=*/true);
806 auto Level
= unsigned(SubPath
.count(sys::path::get_separator())) + 1;
807 Components
.back().second
+= Level
;
808 Components
.emplace_back(std::move(SubPath
), Level
);
811 // Then we make the title accroding to Components.
813 for (auto I
= Components
.begin(), E
= Components
.end();;) {
814 auto &Name
= I
->first
;
816 S
+= a("./index.html", Name
);
817 S
+= sys::path::get_separator();
821 SmallString
<128> Link
;
822 for (unsigned J
= I
->second
; J
> 0; --J
)
824 Link
+= "index.html";
826 S
+= sys::path::get_separator();
831 std::string
buildRelLinkToFile(StringRef RelPath
) const {
832 SmallString
<128> LinkTextStr(RelPath
);
833 sys::path::native(LinkTextStr
);
835 // remove_dots will remove trailing slash, so we need to check before it.
836 auto IsDir
= LinkTextStr
.ends_with(sys::path::get_separator());
837 sys::path::remove_dots(LinkTextStr
, /*remove_dot_dot=*/true);
839 SmallString
<128> LinkTargetStr(LinkTextStr
);
841 LinkTextStr
+= sys::path::get_separator();
842 sys::path::append(LinkTargetStr
, "index.html");
844 LinkTargetStr
+= ".html";
847 auto LinkText
= escape(LinkTextStr
, Options
);
848 auto LinkTarget
= escape(LinkTargetStr
, Options
);
849 return a(LinkTarget
, LinkText
);
853 Error
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
);
861 // Emit the default stylesheet.
862 if (Error E
= emitStyleSheet())
865 // Emit the JavaScript UI implementation
866 if (Error E
= emitJavaScript())
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())
874 auto &LCPath
= TotalsOrErr
->Name
;
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("index", "html", /*InToplevel=*/true);
879 if (auto E
= OSOrErr
.takeError())
881 auto OS
= std::move(OSOrErr
.get());
882 auto LCPIndexFilePath
=
883 getOutputPath((LCPath
+ "index").str(), "html", /*InToplevel=*/false);
884 *OS
.get() << R
"(<!DOCTYPE html>
887 <meta http-equiv="Refresh
" content="0; url
=')"
888 << LCPIndexFilePath << R"('" />
894 return Error::success();
897 void SourceCoverageViewHTML::renderViewHeader(raw_ostream
&OS
) {
898 OS
<< BeginCenteredDiv
<< BeginTable
;
901 void SourceCoverageViewHTML::renderViewFooter(raw_ostream
&OS
) {
902 OS
<< EndTable
<< EndCenteredDiv
;
905 void SourceCoverageViewHTML::renderSourceName(raw_ostream
&OS
, bool WholeFile
) {
906 OS
<< BeginSourceNameDiv
<< tag("pre", escape(getSourceName(), getOptions()))
910 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream
&OS
, unsigned) {
914 void 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.
922 void SourceCoverageViewHTML::renderViewDivider(raw_ostream
&, unsigned) {
923 // The table-based output makes view dividers unnecessary.
926 void 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
;
932 // Steps for handling text-escaping, highlighting, and tooltip creation:
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.
939 SmallVector
<std::string
, 8> Snippets
;
940 CoverageSegmentArray Segments
= LCS
.getLineSegments();
943 auto Snip
= [&](unsigned Start
, unsigned Len
) {
944 Snippets
.push_back(std::string(Line
.substr(Start
, Len
)));
948 Snip(LCol
- 1, Segments
.empty() ? 0 : (Segments
.front()->Col
- 1));
950 for (unsigned I
= 1, E
= Segments
.size(); I
< E
; ++I
)
951 Snip(LCol
- 1, Segments
[I
]->Col
- LCol
);
953 // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
954 Snip(LCol
- 1, Line
.size() + 1 - LCol
);
956 // 2. Escape all of the snippets.
958 for (unsigned I
= 0, E
= Snippets
.size(); I
< E
; ++I
)
959 Snippets
[I
] = escape(Snippets
[I
], getOptions());
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.
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(LC
, RC
);
971 return tag("span", Snippet
, std::string(*Color
));
973 return tag("span", Snippet
, "region " + std::string(*Color
));
976 auto CheckIfUncovered
= [&](const CoverageSegment
*S
) {
977 return S
&& (!S
->IsGapRegion
|| (Color
&& *Color
== "red")) &&
978 S
->HasCount
&& S
->Count
== 0;
981 if (CheckIfUncovered(LCS
.getWrappedSegment())) {
983 if (!Snippets
[0].empty())
984 Snippets
[0] = Highlight(Snippets
[0], 1, 1 + Snippets
[0].size());
987 for (unsigned I
= 0, E
= Segments
.size(); I
< E
; ++I
) {
988 const auto *CurSeg
= Segments
[I
];
989 if (CheckIfUncovered(CurSeg
))
991 else if (CurSeg
->Col
== ExpansionCol
)
994 Color
= std::nullopt
;
997 Snippets
[I
+ 1] = Highlight(Snippets
[I
+ 1], CurSeg
->Col
,
998 CurSeg
->Col
+ Snippets
[I
+ 1].size());
1001 if (Color
&& Segments
.empty())
1002 Snippets
.back() = Highlight(Snippets
.back(), 1, 1 + Snippets
.back().size());
1004 if (getOptions().Debug
) {
1005 for (const auto &Range
: HighlightedRanges
) {
1006 errs() << "Highlighted line " << LineNo
<< ", " << Range
.first
<< " -> ";
1007 if (Range
.second
== 0)
1010 errs() << Range
.second
;
1015 // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
1016 // sub-line region count tooltips if needed.
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(CurSeg
->Count
);
1023 auto LCSCount
= BinaryCount(LCS
.getExecutionCount());
1024 if (!CurSeg
->IsRegionEntry
)
1026 if (CurSegCount
== LCSCount
)
1032 tag("span", formatCount(CurSegCount
), "tooltip-content"),
1035 if (getOptions().Debug
)
1036 errs() << "Marker at " << CurSeg
->Line
<< ":" << CurSeg
->Col
<< " = "
1037 << formatCount(CurSegCount
) << "\n";
1043 for (const auto &Snippet
: Snippets
)
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()).
1053 void SourceCoverageViewHTML::renderLineCoverageColumn(
1054 raw_ostream
&OS
, const LineCoverageStats
&Line
) {
1056 if (Line
.isMapped())
1057 Count
= tag("pre", formatBinaryCount(Line
.getExecutionCount()));
1058 std::string CoverageClass
=
1059 (Line
.getExecutionCount() > 0)
1061 : (Line
.isMapped() ? "uncovered-line" : "skipped-line");
1062 OS
<< tag("td", Count
, CoverageClass
);
1065 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream
&OS
,
1067 std::string LineNoStr
= utostr(uint64_t(LineNo
));
1068 std::string TargetName
= "L" + LineNoStr
;
1069 OS
<< tag("td", a("#" + TargetName
, tag("pre", LineNoStr
), TargetName
),
1073 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream
&,
1074 const LineCoverageStats
&Line
,
1076 // Region markers are rendered in-line using tooltips.
1079 void 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
);
1087 void SourceCoverageViewHTML::renderExpansionView(raw_ostream
&OS
,
1089 unsigned ViewDepth
) {
1090 OS
<< BeginExpansionDiv
;
1091 ESV
.View
->print(OS
, /*WholeFile=*/false, /*ShowSourceName=*/false,
1092 /*ShowTitle=*/false, ViewDepth
+ 1);
1093 OS
<< EndExpansionDiv
;
1096 void 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';
1102 auto BranchCount
= [&](StringRef Label
, uint64_t Count
, bool Folded
,
1105 return std::string
{"Folded"};
1108 raw_string_ostream
OS(Str
);
1110 OS
<< tag("span", Label
, (Count
? "None" : "red branch")) << ": ";
1111 if (getOptions().ShowBranchCounts
)
1112 OS
<< tag("span", formatBinaryCount(Count
),
1113 (Count
? "covered-line" : "uncovered-line"));
1115 OS
<< format("%0.2f", (Total
!= 0 ? 100.0 * Count
/ Total
: 0.0)) << "%";
1120 OS
<< BeginExpansionDiv
;
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).
1126 static_cast<double>(R
.ExecutionCount
) + R
.FalseExecutionCount
;
1128 // Display Line + Column.
1129 std::string LineNoStr
= utostr(uint64_t(R
.LineStart
));
1130 std::string ColNoStr
= utostr(uint64_t(R
.ColumnStart
));
1131 std::string TargetName
= "L" + LineNoStr
;
1135 a("#" + TargetName
, tag("span", LineNoStr
+ ":" + ColNoStr
),
1140 if (R
.TrueFolded
&& R
.FalseFolded
) {
1141 OS
<< "Folded - Ignored]\n";
1145 OS
<< BranchCount("True", R
.ExecutionCount
, R
.TrueFolded
, Total
) << ", "
1146 << BranchCount("False", R
.FalseExecutionCount
, R
.FalseFolded
, Total
)
1150 OS
<< EndExpansionDiv
;
1153 void SourceCoverageViewHTML::renderMCDCView(raw_ostream
&OS
, MCDCView
&MRV
,
1154 unsigned ViewDepth
) {
1155 for (auto &Record
: MRV
.Records
) {
1156 OS
<< BeginExpansionDiv
;
1158 OS
<< " MC/DC Decision Region (";
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
;
1166 a("#" + TargetName
, tag("span", LineNoStr
+ ":" + ColNoStr
)),
1169 LineNoStr
= utostr(uint64_t(DecisionRegion
.LineEnd
));
1170 ColNoStr
= utostr(uint64_t(DecisionRegion
.ColumnEnd
));
1172 a("#" + TargetName
, tag("span", LineNoStr
+ ":" + ColNoStr
)),
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(i
);
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(i
);
1187 for (unsigned i
= 0; i
< Record
.getNumConditions(); i
++)
1188 OS
<< Record
.getConditionCoverageString(i
);
1189 OS
<< " MC/DC Coverage for Expression: ";
1190 OS
<< format("%0.2f", Record
.getPercentCovered()) << "%\n";
1192 OS
<< EndExpansionDiv
;
1196 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream
&OS
,
1197 InstantiationView
&ISV
,
1198 unsigned ViewDepth
) {
1199 OS
<< BeginExpansionDiv
;
1201 OS
<< BeginSourceNameDiv
1203 escape("Unexecuted instantiation: " + ISV
.FunctionName
.str(),
1205 << EndSourceNameDiv
;
1207 ISV
.View
->print(OS
, /*WholeFile=*/false, /*ShowSourceName=*/true,
1208 /*ShowTitle=*/false, ViewDepth
);
1209 OS
<< EndExpansionDiv
;
1212 void SourceCoverageViewHTML::renderTitle(raw_ostream
&OS
, StringRef Title
) {
1213 if (getOptions().hasProjectTitle())
1214 OS
<< tag(ProjectTitleTag
, escape(getOptions().ProjectTitle
, getOptions()));
1215 OS
<< tag(ReportTitleTag
, escape(Title
, getOptions()));
1216 if (getOptions().hasCreatedTime())
1217 OS
<< tag(CreatedTimeTag
,
1218 escape(getOptions().CreatedTimeStr
, getOptions()));
1221 a("javascript:next_line()", "next uncovered line (L)") + ", " +
1222 a("javascript:next_region()", "next uncovered region (R)") +
1224 a("javascript:next_branch()", "next uncovered branch (B)"),
1228 void SourceCoverageViewHTML::renderTableHeader(raw_ostream
&OS
,
1229 unsigned ViewDepth
) {
1232 renderLinePrefix(OS
, ViewDepth
);
1233 OS
<< tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"));
1234 OS
<< tag("td", tag("pre", "Source" + Links
));
1235 renderLineSuffix(OS
, ViewDepth
);