brintos

brintos / llvm-project-archived public Read only

0
0
Text · 38.0 KiB · bdef099 Raw
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