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 22 using namespace llvm; 23 24 namespace { 25 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. 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 else 41 ++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. 53 std::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. 70 std::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 86 const char *BeginHeader = 87 "<head>" 88 "<meta name='viewport' content='width=device-width,initial-scale=1'>" 89 "<meta charset='UTF-8'>"; 90 91 const char *JSForCoverage = 92 R"javascript( 93 function 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 167 function next_line(reverse) { 168 next_uncovered("td.uncovered-line", reverse) 169 } 170 171 function next_region(reverse) { 172 next_uncovered("span.red.region", reverse); 173 } 174 175 function next_branch(reverse) { 176 next_uncovered("span.red.branch", reverse); 177 } 178 179 document.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 193 const char *CSSForCoverage = 194 R"(.red { 195 background-color: #f004; 196 } 197 .cyan { 198 background-color: cyan; 199 } 200 html { 201 scroll-behavior: smooth; 202 } 203 body { 204 font-family: -apple-system, sans-serif; 205 } 206 pre { 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 } 231 table { 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 } 327 th, 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 } 335 td pre { 336 display: inline-block; 337 text-decoration: inherit; 338 } 339 td:first-child { 340 border-left: none; 341 } 342 td:last-child { 343 border-right: none; 344 } 345 tr:hover { 346 background-color: #eee; 347 } 348 tr:last-child { 349 border-bottom: none; 350 } 351 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { 352 background-color: #8884; 353 } 354 a { 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 390 const char *EndHeader = "</head>"; 391 392 const char *BeginCenteredDiv = "<div class='centered'>"; 393 394 const char *EndCenteredDiv = "</div>"; 395 396 const char *BeginSourceNameDiv = "<div class='source-name-title'>"; 397 398 const char *EndSourceNameDiv = "</div>"; 399 400 const char *BeginCodeTD = "<td class='code'>"; 401 402 const char *EndCodeTD = "</td>"; 403 404 const char *BeginPre = "<pre>"; 405 406 const char *EndPre = "</pre>"; 407 408 const char *BeginExpansionDiv = "<div class='expansion-view'>"; 409 410 const char *EndExpansionDiv = "</div>"; 411 412 const char *BeginTable = "<table>"; 413 414 const char *EndTable = "</table>"; 415 416 const char *ProjectTitleTag = "h1"; 417 418 const char *ReportTitleTag = "h2"; 419 420 const char *CreatedTimeTag = "h4"; 421 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"; 429 } 430 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"; 438 } 439 440 void 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(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(PathToJavaScript, Opts) << "'></script>"; 459 460 OS << EndHeader << "<body>"; 461 } 462 463 void 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 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(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 else 517 OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row"); 518 } 519 520 void emitEpilog(raw_ostream &OS) { 521 OS << "</body>" 522 << "</html>"; 523 } 524 525 } // anonymous namespace 526 527 Expected<CoveragePrinter::OwnedStream> 528 CoveragePrinterHTML::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 546 void CoveragePrinterHTML::closeViewFile(OwnedStream OS) { 547 emitEpilog(*OS.get()); 548 } 549 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(), "")); 567 } 568 569 std::string 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); 579 } 580 581 Error 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 592 Error 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 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)); 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 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; 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 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()) 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("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 else 672 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 normally 678 // not very useful, but could be relevant for code which abuses the 679 // 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 699 struct 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 else 740 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 else 749 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 normally 758 // not very useful, but could be relevant for code which abuses the 759 // 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 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(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 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); 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("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 897 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) { 898 OS << BeginCenteredDiv << BeginTable; 899 } 900 901 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) { 902 OS << EndTable << EndCenteredDiv; 903 } 904 905 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) { 906 OS << BeginSourceNameDiv << tag("pre", escape(getSourceName(), getOptions())) 907 << EndSourceNameDiv; 908 } 909 910 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) { 911 OS << "<tr>"; 912 } 913 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. 917 if (hasSubViews()) 918 OS << EndCodeTD; 919 OS << "</tr>"; 920 } 921 922 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) { 923 // The table-based output makes view dividers unnecessary. 924 } 925 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; 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(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 (unsigned I = 0, E = Snippets.size(); I < E; ++I) 959 Snippets[I] = escape(Snippets[I], 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(LC, RC); 970 if (Snippet.empty()) 971 return tag("span", Snippet, std::string(*Color)); 972 else 973 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 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(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 1053 void 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 1065 void 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 1073 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &, 1074 const LineCoverageStats &Line, 1075 unsigned) { 1076 // Region markers are rendered in-line using tooltips. 1077 } 1078 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); 1085 } 1086 1087 void 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 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'; 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 else 1115 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 1153 void 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 1196 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS, 1197 InstantiationView &ISV, 1198 unsigned ViewDepth) { 1199 OS << BeginExpansionDiv; 1200 if (!ISV.View) 1201 OS << BeginSourceNameDiv 1202 << tag("pre", 1203 escape("Unexecuted instantiation: " + ISV.FunctionName.str(), 1204 getOptions())) 1205 << EndSourceNameDiv; 1206 else 1207 ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, 1208 /*ShowTitle=*/false, ViewDepth); 1209 OS << EndExpansionDiv; 1210 } 1211 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())); 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 1228 void 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