[llvm] [llvm-cov] Coverage report HTML UI to jump between uncovered parts of code (PR #95662)

Hana Dusíková via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 15 08:39:51 PDT 2024


https://github.com/hanickadot updated https://github.com/llvm/llvm-project/pull/95662

>From c340cbc5cfb0ece217c19ae3b81df381a4bacf6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hana=20Dusi=CC=81kova=CC=81?= <hanicka at hanicka.net>
Date: Sat, 15 Jun 2024 17:11:39 +0200
Subject: [PATCH] [llvm-cov] HTML generated code now contains JavaScript code
 to quickly navigate between uncovered parts of code

---
 llvm/docs/ReleaseNotes.rst                    |   6 +
 llvm/tools/llvm-cov/SourceCoverageView.cpp    |   3 +-
 llvm/tools/llvm-cov/SourceCoverageView.h      |   3 +-
 .../tools/llvm-cov/SourceCoverageViewHTML.cpp | 199 ++++++++++++++++--
 llvm/tools/llvm-cov/SourceCoverageViewHTML.h  |   4 +-
 .../tools/llvm-cov/SourceCoverageViewText.cpp |   3 +-
 llvm/tools/llvm-cov/SourceCoverageViewText.h  |   3 +-
 7 files changed, 190 insertions(+), 31 deletions(-)

diff --git a/llvm/docs/ReleaseNotes.rst b/llvm/docs/ReleaseNotes.rst
index 5fdbc9f349af4..7b46cdd9050db 100644
--- a/llvm/docs/ReleaseNotes.rst
+++ b/llvm/docs/ReleaseNotes.rst
@@ -292,6 +292,12 @@ Changes to the LLVM tools
   now has a map for the mapped files. (`#92835
   <https://github.com/llvm/llvm-project/pull/92835>`).
 
+* llvm-cov now generates HTML report with JavaScript code to allow simple
+  jumping between uncovered parts (lines/regions/branches) of code 
+  using buttons on top-right corner of the page or using keys (L/R/B or 
+  jumping in reverse direction with shift+L/R/B). (`#?????
+  <https://github.com/llvm/llvm-project/pull/?????>`).
+
 Changes to LLDB
 ---------------------------------
 
diff --git a/llvm/tools/llvm-cov/SourceCoverageView.cpp b/llvm/tools/llvm-cov/SourceCoverageView.cpp
index 45bddd7284461..ce55e3abf23bd 100644
--- a/llvm/tools/llvm-cov/SourceCoverageView.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageView.cpp
@@ -203,8 +203,7 @@ void SourceCoverageView::print(raw_ostream &OS, bool WholeFile,
   if (ShowSourceName)
     renderSourceName(OS, WholeFile);
 
-  renderTableHeader(OS, (ViewDepth > 0) ? 0 : getFirstUncoveredLineNo(),
-                    ViewDepth);
+  renderTableHeader(OS, ViewDepth);
 
   // We need the expansions, instantiations, and branches sorted so we can go
   // through them while we iterate lines.
diff --git a/llvm/tools/llvm-cov/SourceCoverageView.h b/llvm/tools/llvm-cov/SourceCoverageView.h
index a874f7c6820d2..d255f8c200b24 100644
--- a/llvm/tools/llvm-cov/SourceCoverageView.h
+++ b/llvm/tools/llvm-cov/SourceCoverageView.h
@@ -262,8 +262,7 @@ class SourceCoverageView {
   virtual void renderTitle(raw_ostream &OS, StringRef CellText) = 0;
 
   /// Render the table header for a given source file.
-  virtual void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
-                                 unsigned IndentLevel) = 0;
+  virtual void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) = 0;
 
   /// @}
 
diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
index d4b2ea3594fc5..1a79fd3a115c7 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
@@ -88,6 +88,103 @@ const char *BeginHeader =
     "<meta name='viewport' content='width=device-width,initial-scale=1'>"
     "<meta charset='UTF-8'>";
 
+const char *JSForCoverage =
+    R"javascript(
+function next_uncovered(selector, reverse) {
+  function visit_element(element) {
+    element.scrollIntoView({behavior: "smooth", block: "center"});
+    element.classList.add("seen");
+    element.classList.add("selected");
+  }
+  
+  function select_one() {
+    if (!reverse) {
+      const previously_selected = document.querySelector(".selected");
+      
+      if (previously_selected) {
+        previously_selected.classList.remove("selected");
+      }
+      
+      return document.querySelector(selector + ":not(.seen)");
+    } else {      
+      const previously_selected = document.querySelector(".selected");
+      
+      if (previously_selected) {
+        previously_selected.classList.remove("selected");
+        previously_selected.classList.remove("seen");
+      }
+      
+      const nodes = document.querySelectorAll(selector + ".seen");
+      if (nodes) {
+        const last = nodes[nodes.length - 1]; // last
+        return last;
+      } else {
+        return undefined;
+      }
+    }
+  }
+  
+  function reset_all() {
+    if (!reverse) {
+      const all_seen = document.querySelectorAll(selector + ".seen");
+  
+      if (all_seen) {
+        all_seen.forEach(e => e.classList.remove("seen"));
+      }
+    } else {
+      const all_seen = document.querySelectorAll(selector + ":not(.seen)");
+  
+      if (all_seen) {
+        all_seen.forEach(e => e.classList.add("seen"));
+      }
+    }
+    
+  }
+  
+  const uncovered = select_one();
+
+  if (uncovered) {
+    visit_element(uncovered);
+  } else {
+    reset_all();
+    
+    
+    const uncovered = select_one();
+    
+    if (uncovered) {
+      visit_element(uncovered);
+    }
+  }
+}
+
+function next_line(reverse) { 
+  next_uncovered("td.uncovered-line", reverse)
+}
+
+function next_region(reverse) { 
+  next_uncovered("span.red.region", reverse);
+}
+
+function next_branch(reverse) { 
+  next_uncovered("span.red.branch", reverse);
+}
+
+document.addEventListener("keypress", function(event) {
+  console.log(event);
+  const reverse = event.shiftKey;
+  if (event.code == "KeyL") {
+    next_line(reverse);
+  }
+  if (event.code == "KeyB") {
+    next_branch(reverse);
+  }
+  if (event.code == "KeyR") {
+    next_region(reverse);
+  }
+  
+});
+)javascript";
+
 const char *CSSForCoverage =
     R"(.red {
   background-color: #f004;
@@ -95,6 +192,9 @@ const char *CSSForCoverage =
 .cyan {
   background-color: cyan;
 }
+html {
+  scroll-behavior: smooth;
+}
 body {
   font-family: -apple-system, sans-serif;
 }
@@ -171,6 +271,18 @@ table {
   text-align: right;
   color: #d00;
 }
+.uncovered-line.selected {
+  color: #f00;
+  font-weight: bold;
+}
+.region.red.selected {
+  background-color: #f008;
+  font-weight: bold;
+}
+.branch.red.selected {
+  background-color: #f008;
+  font-weight: bold;
+}
 .tooltip {
   position: relative;
   display: inline;
@@ -237,6 +349,13 @@ tr:has(> td >a:target) {
 a {
   color: inherit;
 }
+.control {
+  position: fixed;
+  top: 0em;
+  right: 0em;
+  padding: 1em;
+  background: #FFF8;
+}
 @media (prefers-color-scheme: dark) {
   body {
     background-color: #222;
@@ -254,6 +373,9 @@ a {
   .tooltip {
     background-color: #068;
   }
+  .control {
+    background: #2228;
+  }
 }
 )";
 
@@ -298,8 +420,18 @@ std::string getPathToStyle(StringRef ViewPath) {
   return PathToStyle + "style.css";
 }
 
+std::string getPathToJavaScript(StringRef ViewPath) {
+  std::string PathToJavaScript;
+  std::string PathSep = std::string(sys::path::get_separator());
+  unsigned NumSeps = ViewPath.count(PathSep);
+  for (unsigned I = 0, E = NumSeps; I < E; ++I)
+    PathToJavaScript += ".." + PathSep;
+  return PathToJavaScript + "control.js";
+}
+
 void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
-                 const std::string &PathToStyle = "") {
+                 const std::string &PathToStyle = "",
+                 const std::string &PathToJavaScript = "") {
   OS << "<!doctype html>"
         "<html>"
      << BeginHeader;
@@ -311,6 +443,13 @@ void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
     OS << "<link rel='stylesheet' type='text/css' href='"
        << escape(PathToStyle, Opts) << "'>";
 
+  // Link to a JavaScript if one is available
+  if (PathToJavaScript.empty())
+    ;
+  // OS << "<style>" << CSSForCoverage << "</style>";
+  else
+    OS << "<script src='" << escape(PathToJavaScript, Opts) << "'></script>";
+
   OS << EndHeader << "<body>";
 }
 
@@ -390,7 +529,8 @@ CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
     emitPrelude(*OS.get(), Opts);
   } else {
     std::string ViewPath = getOutputPath(Path, "html", InToplevel);
-    emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath));
+    emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath),
+                getPathToJavaScript(ViewPath));
   }
 
   return std::move(OS);
@@ -442,6 +582,17 @@ Error CoveragePrinterHTML::emitStyleSheet() {
   return Error::success();
 }
 
+Error CoveragePrinterHTML::emitJavaScript() {
+  auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);
+  if (Error E = JSOrErr.takeError())
+    return E;
+
+  OwnedStream JS = std::move(JSOrErr.get());
+  JS->operator<<(JSForCoverage);
+
+  return Error::success();
+}
+
 void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
                                            const std::string &Title) {
   // Emit some basic information about the coverage report.
@@ -487,6 +638,9 @@ Error CoveragePrinterHTML::createIndexFile(
   if (Error E = emitStyleSheet())
     return E;
 
+  if (Error E = emitJavaScript())
+    return E;
+
   // Emit a file index along with some coverage statistics.
   auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
   if (Error E = OSOrErr.takeError())
@@ -495,7 +649,7 @@ Error CoveragePrinterHTML::createIndexFile(
   raw_ostream &OSRef = *OS.get();
 
   assert(Opts.hasOutputDirectory() && "No output directory for index file");
-  emitPrelude(OSRef, Opts, getPathToStyle(""));
+  emitPrelude(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));
 
   emitReportHeader(OSRef, "Coverage Report");
 
@@ -561,7 +715,8 @@ struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
 
     auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
                                                /*InToplevel=*/false);
-    emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath));
+    emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),
+                getPathToJavaScript(IndexHtmlPath));
 
     auto NavLink = buildTitleLinks(LCPath);
     Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
@@ -800,7 +955,10 @@ void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
   auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
     if (getOptions().Debug)
       HighlightedRanges.emplace_back(LC, RC);
-    return tag("span", Snippet, std::string(*Color));
+    if (Snippet.empty())
+      return tag("span", Snippet, std::string(*Color));
+    else
+      return tag("span", Snippet, "region " + std::string(*Color));
   };
 
   auto CheckIfUncovered = [&](const CoverageSegment *S) {
@@ -883,7 +1041,9 @@ void SourceCoverageViewHTML::renderLineCoverageColumn(
   if (Line.isMapped())
     Count = tag("pre", formatCount(Line.getExecutionCount()));
   std::string CoverageClass =
-      (Line.getExecutionCount() > 0) ? "covered-line" : "uncovered-line";
+      (Line.getExecutionCount() > 0)
+          ? "covered-line"
+          : (Line.isMapped() ? "uncovered-line" : "skipped-line");
   OS << tag("td", Count, CoverageClass);
 }
 
@@ -957,7 +1117,7 @@ void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
     }
 
     // Display TrueCount or TruePercent.
-    std::string TrueColor = R.ExecutionCount ? "None" : "red";
+    std::string TrueColor = R.ExecutionCount ? "None" : "red branch";
     std::string TrueCovClass =
         (R.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
 
@@ -969,7 +1129,7 @@ void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
       OS << format("%0.2f", TruePercent) << "%, ";
 
     // Display FalseCount or FalsePercent.
-    std::string FalseColor = R.FalseExecutionCount ? "None" : "red";
+    std::string FalseColor = R.FalseExecutionCount ? "None" : "red branch";
     std::string FalseCovClass =
         (R.FalseExecutionCount > 0) ? "covered-line" : "uncovered-line";
 
@@ -1053,24 +1213,21 @@ void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
   if (getOptions().hasCreatedTime())
     OS << tag(CreatedTimeTag,
               escape(getOptions().CreatedTimeStr, getOptions()));
+
+  OS << tag("span",
+            a("javascript:next_line()", "next uncovered line (L)") + ", " +
+                a("javascript:next_region()", "next uncovered region (R)") +
+                ", " +
+                a("javascript:next_branch()", "next uncovered branch (B)"),
+            "control");
 }
 
 void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
-                                               unsigned FirstUncoveredLineNo,
                                                unsigned ViewDepth) {
-  std::string SourceLabel;
-  if (FirstUncoveredLineNo == 0) {
-    SourceLabel = tag("td", tag("pre", "Source"));
-  } else {
-    std::string LinkTarget = "#L" + utostr(uint64_t(FirstUncoveredLineNo));
-    SourceLabel =
-        tag("td", tag("pre", "Source (" +
-                                 a(LinkTarget, "jump to first uncovered line") +
-                                 ")"));
-  }
+  std::string Links;
 
   renderLinePrefix(OS, ViewDepth);
-  OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"))
-     << SourceLabel;
+  OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"));
+  OS << tag("td", tag("pre", "Source" + Links));
   renderLineSuffix(OS, ViewDepth);
 }
diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
index 32313a3963c43..9b7391d0043ec 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
+++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
@@ -38,6 +38,7 @@ class CoveragePrinterHTML : public CoveragePrinter {
 
 protected:
   Error emitStyleSheet();
+  Error emitJavaScript();
   void emitReportHeader(raw_ostream &OSRef, const std::string &Title);
 
 private:
@@ -105,8 +106,7 @@ class SourceCoverageViewHTML : public SourceCoverageView {
 
   void renderTitle(raw_ostream &OS, StringRef Title) override;
 
-  void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
-                         unsigned IndentLevel) override;
+  void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) override;
 
 public:
   SourceCoverageViewHTML(StringRef SourceName, const MemoryBuffer &File,
diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
index 580da45ecfc0d..cab60c2d9034e 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
@@ -414,5 +414,4 @@ void SourceCoverageViewText::renderTitle(raw_ostream &OS, StringRef Title) {
         << getOptions().CreatedTimeStr << "\n";
 }
 
-void SourceCoverageViewText::renderTableHeader(raw_ostream &, unsigned,
-                                               unsigned) {}
+void SourceCoverageViewText::renderTableHeader(raw_ostream &, unsigned) {}
diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.h b/llvm/tools/llvm-cov/SourceCoverageViewText.h
index 7cb47fcbf42bd..25a161b096200 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewText.h
+++ b/llvm/tools/llvm-cov/SourceCoverageViewText.h
@@ -93,8 +93,7 @@ class SourceCoverageViewText : public SourceCoverageView {
 
   void renderTitle(raw_ostream &OS, StringRef Title) override;
 
-  void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
-                         unsigned IndentLevel) override;
+  void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) override;
 
 public:
   SourceCoverageViewText(StringRef SourceName, const MemoryBuffer &File,



More information about the llvm-commits mailing list