1237 lines · cpp
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-exception6//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(NumSpaces, ' ');35 ColNum += NumSpaces;36 } else {37 TabExpandedResult += C;38 if (C == '\n' || C == '\r')39 ColNum = 0;40 else41 ++ColNum;42 }43 }44 std::string EscapedHTML;45 {46 raw_string_ostream OS{EscapedHTML};47 printHTMLEscaped(TabExpandedResult, 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]; // last128 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(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(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 else451 OS << "<link rel='stylesheet' type='text/css' href='"452 << escape(PathToStyle, Opts) << "'>";453 454 // Link to a JavaScript if one is available455 if (PathToJavaScript.empty())456 OS << "<script>" << JSForCoverage << "</script>";457 else458 OS << "<script src='" << escape(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("%*.2f", 7, Pctg) << "% ";476 else477 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(tag("td", tag("pre", S), CellClass));488 };489 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());513 514 if (IsTotals)515 OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");516 else517 OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");518}519 520void emitEpilog(raw_ostream &OS) {521 OS << "</body>"522 << "</html>";523}524 525} // anonymous namespace526 527Expected<CoveragePrinter::OwnedStream>528CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {529 auto OSOrErr = createOutputStream(Path, "html", InToplevel);530 if (!OSOrErr)531 return OSOrErr;532 533 OwnedStream OS = std::move(OSOrErr.get());534 535 if (!Opts.hasOutputDirectory()) {536 emitPrelude(*OS.get(), Opts);537 } else {538 std::string ViewPath = getOutputPath(Path, "html", InToplevel);539 emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath),540 getPathToJavaScript(ViewPath));541 }542 543 return std::move(OS);544}545 546void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {547 emitEpilog(*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(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(), ""));567}568 569std::string570CoveragePrinterHTML::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);579}580 581Error CoveragePrinterHTML::emitStyleSheet() {582 auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);583 if (Error E = CSSOrErr.takeError())584 return E;585 586 OwnedStream CSS = std::move(CSSOrErr.get());587 CSS->operator<<(CSSForCoverage);588 589 return Error::success();590}591 592Error CoveragePrinterHTML::emitJavaScript() {593 auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);594 if (Error E = JSOrErr.takeError())595 return E;596 597 OwnedStream JS = std::move(JSOrErr.get());598 JS->operator<<(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(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));608 OSRef << tag(ReportTitleTag, Title);609 if (Opts.hasCreatedTime())610 OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));611 612 // Emit a link to some documentation.613 OSRef << tag("p", "Click " +614 a("http://clang.llvm.org/docs/"615 "SourceBasedCodeCoverage.html#interpreting-reports",616 "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(OSRef, Opts);623}624 625/// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is626/// 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, 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 implementation649 if (Error E = emitJavaScript())650 return E;651 652 // Emit a file index along with some coverage statistics.653 auto OSOrErr = createOutputStream("index", "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(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));661 662 emitReportHeader(OSRef, "Coverage Report");663 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]);671 else672 EmptyFiles = true;673 }674 emitFileSummary(OSRef, "Totals", Totals, /*IsTotals=*/true);675 OSRef << EndTable << EndCenteredDiv;676 677 // Emit links to files which don't contain any functions. These are normally678 // not very useful, but could be relevant for code which abuses the679 // preprocessor.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';689 }690 OSRef << EndTable << EndCenteredDiv;691 }692 693 OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts));694 emitEpilog(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(OSPath, "index");717 auto OSOrErr = Printer.createOutputStream(OSPath, "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((LCPath + "index").str(), "html",725 /*InToplevel=*/false);726 emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),727 getPathToJavaScript(IndexHtmlPath));728 729 auto NavLink = buildTitleLinks(LCPath);730 Printer.emitReportHeader(OSRef, "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(&Report);739 else740 emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), 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(&Report);748 else749 emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,750 /*IsTotals=*/false);751 }752 753 // Emit the totals row.754 emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);755 OSRef << EndTable << EndCenteredDiv;756 757 // Emit links to files which don't contain any functions. These are normally758 // not very useful, but could be relevant for code which abuses the759 // preprocessor.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';768 }769 OSRef << EndTable << EndCenteredDiv;770 }771 772 // Emit epilog.773 OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));774 emitEpilog(OSRef);775 776 return Error::success();777 }778 779 /// Make a title with hyperlinks to the index.html files of each hierarchy780 /// of the report.781 std::string buildTitleLinks(StringRef LCPath) const {782 // For each report level in LCPStack, extract the path component and783 // 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 of790 // 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 a792 // directory, in this case, we use "." as the root path to make it793 // not be confused with the root path "/".794 RootPath = ".";795 } else {796 RootPath = LCPath.substr(0, *Iter);797 sys::path::native(RootPath);798 sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true);799 }800 Components.emplace_back(std::move(RootPath), 0);801 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);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("./index.html", 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, 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(LinkTextStr);834 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);838 839 SmallString<128> LinkTargetStr(LinkTextStr);840 if (IsDir) {841 LinkTextStr += sys::path::get_separator();842 sys::path::append(LinkTargetStr, "index.html");843 } else {844 LinkTargetStr += ".html";845 }846 847 auto LinkText = escape(LinkTextStr, Options);848 auto LinkTarget = escape(LinkTargetStr, Options);849 return a(LinkTarget, 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 is857 // 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 implementation866 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 redirection877 // to the index file in the LCP directory.878 auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);879 if (auto E = OSOrErr.takeError())880 return E;881 auto OS = std::move(OSOrErr.get());882 auto LCPIndexFilePath =883 getOutputPath((LCPath + "index").str(), "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("pre", escape(getSourceName(), 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 first935 // 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 ends937 // 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(std::string(Line.substr(Start, 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 (std::string &Snippet : Snippets)959 Snippet = escape(Snippet, getOptions());960 961 // 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment962 // 1 to set the highlight for snippet 2, segment 2 to set the highlight for963 // 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(LC, RC);970 if (Snippet.empty())971 return tag("span", Snippet, std::string(*Color));972 else973 return tag("span", Snippet, "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 else994 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 else1010 errs() << Range.second;1011 errs() << "\n";1012 }1013 }1014 1015 // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate1016 // 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(CurSeg->Count);1023 auto LCSCount = BinaryCount(LCS.getExecutionCount());1024 if (!CurSeg->IsRegionEntry)1025 continue;1026 if (CurSegCount == LCSCount)1027 continue;1028 1029 Snippets[I + 1] =1030 tag("div",1031 Snippets[I + 1] +1032 tag("span", formatCount(CurSegCount), "tooltip-content"),1033 "tooltip");1034 1035 if (getOptions().Debug)1036 errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "1037 << formatCount(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("pre", formatBinaryCount(Line.getExecutionCount()));1058 std::string CoverageClass =1059 (Line.getExecutionCount() > 0)1060 ? "covered-line"1061 : (Line.isMapped() ? "uncovered-line" : "skipped-line");1062 OS << tag("td", Count, CoverageClass);1063}1064 1065void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,1066 unsigned LineNo) {1067 std::string LineNoStr = utostr(uint64_t(LineNo));1068 std::string TargetName = "L" + LineNoStr;1069 OS << tag("td", a("#" + TargetName, tag("pre", LineNoStr), TargetName),1070 "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 + 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("span", Label, (Count ? "None" : "red branch")) << ": ";1111 if (getOptions().ShowBranchCounts)1112 OS << tag("span", formatBinaryCount(Count),1113 (Count ? "covered-line" : "uncovered-line"));1114 else1115 OS << format("%0.2f", (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(uint64_t(R.LineStart));1130 std::string ColNoStr = utostr(uint64_t(R.ColumnStart));1131 std::string TargetName = "L" + LineNoStr;1132 1133 OS << " Branch (";1134 OS << tag("span",1135 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),1136 TargetName),1137 "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("span",1166 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),1167 "line-number") +1168 ") to (";1169 LineNoStr = utostr(uint64_t(DecisionRegion.LineEnd));1170 ColNoStr = utostr(uint64_t(DecisionRegion.ColumnEnd));1171 OS << tag("span",1172 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),1173 "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(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(i);1186 OS << "\n";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";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 << BeginSourceNameDiv1202 << tag("pre",1203 escape("Unexecuted instantiation: " + ISV.FunctionName.str(),1204 getOptions()))1205 << EndSourceNameDiv;1206 else1207 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(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()));1219 1220 OS << tag("span",1221 a("javascript:next_line()", "next uncovered line (L)") + ", " +1222 a("javascript:next_region()", "next uncovered region (R)") +1223 ", " +1224 a("javascript:next_branch()", "next uncovered branch (B)"),1225 "control");1226}1227 1228void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,1229 unsigned ViewDepth) {1230 std::string Links;1231 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);1236}1237