Skip to content

Commit e091967

Browse files
committed
[llvm-cov] HTML generated code now contains JavaScript code to quickly navigate between uncovered parts of code
1 parent 47f8b85 commit e091967

File tree

7 files changed

+190
-31
lines changed

7 files changed

+190
-31
lines changed

llvm/docs/ReleaseNotes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ Changes to the LLVM tools
292292
now has a map for the mapped files. (`#92835
293293
<https://github.com/llvm/llvm-project/pull/92835>`).
294294

295+
* llvm-cov now generates HTML report with JavaScript code to allow simple
296+
jumping between uncovered parts (lines/regions/branches) of code
297+
using buttons on top-right corner of the page or using keys (L/R/B or
298+
jumping in reverse direction with shift+L/R/B). (`#95662
299+
<https://github.com/llvm/llvm-project/pull/95662>`).
300+
295301
Changes to LLDB
296302
---------------------------------
297303

llvm/tools/llvm-cov/SourceCoverageView.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,7 @@ void SourceCoverageView::print(raw_ostream &OS, bool WholeFile,
203203
if (ShowSourceName)
204204
renderSourceName(OS, WholeFile);
205205

206-
renderTableHeader(OS, (ViewDepth > 0) ? 0 : getFirstUncoveredLineNo(),
207-
ViewDepth);
206+
renderTableHeader(OS, ViewDepth);
208207

209208
// We need the expansions, instantiations, and branches sorted so we can go
210209
// through them while we iterate lines.

llvm/tools/llvm-cov/SourceCoverageView.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,7 @@ class SourceCoverageView {
262262
virtual void renderTitle(raw_ostream &OS, StringRef CellText) = 0;
263263

264264
/// Render the table header for a given source file.
265-
virtual void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
266-
unsigned IndentLevel) = 0;
265+
virtual void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) = 0;
267266

268267
/// @}
269268

llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp

Lines changed: 178 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,113 @@ const char *BeginHeader =
8888
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
8989
"<meta charset='UTF-8'>";
9090

91+
const char *JSForCoverage =
92+
R"javascript(
93+
function next_uncovered(selector, reverse) {
94+
function visit_element(element) {
95+
element.scrollIntoView({behavior: "smooth", block: "center"});
96+
element.classList.add("seen");
97+
element.classList.add("selected");
98+
}
99+
100+
function select_one() {
101+
if (!reverse) {
102+
const previously_selected = document.querySelector(".selected");
103+
104+
if (previously_selected) {
105+
previously_selected.classList.remove("selected");
106+
}
107+
108+
return document.querySelector(selector + ":not(.seen)");
109+
} else {
110+
const previously_selected = document.querySelector(".selected");
111+
112+
if (previously_selected) {
113+
previously_selected.classList.remove("selected");
114+
previously_selected.classList.remove("seen");
115+
}
116+
117+
const nodes = document.querySelectorAll(selector + ".seen");
118+
if (nodes) {
119+
const last = nodes[nodes.length - 1]; // last
120+
return last;
121+
} else {
122+
return undefined;
123+
}
124+
}
125+
}
126+
127+
function reset_all() {
128+
if (!reverse) {
129+
const all_seen = document.querySelectorAll(selector + ".seen");
130+
131+
if (all_seen) {
132+
all_seen.forEach(e => e.classList.remove("seen"));
133+
}
134+
} else {
135+
const all_seen = document.querySelectorAll(selector + ":not(.seen)");
136+
137+
if (all_seen) {
138+
all_seen.forEach(e => e.classList.add("seen"));
139+
}
140+
}
141+
142+
}
143+
144+
const uncovered = select_one();
145+
146+
if (uncovered) {
147+
visit_element(uncovered);
148+
} else {
149+
reset_all();
150+
151+
152+
const uncovered = select_one();
153+
154+
if (uncovered) {
155+
visit_element(uncovered);
156+
}
157+
}
158+
}
159+
160+
function next_line(reverse) {
161+
next_uncovered("td.uncovered-line", reverse)
162+
}
163+
164+
function next_region(reverse) {
165+
next_uncovered("span.red.region", reverse);
166+
}
167+
168+
function next_branch(reverse) {
169+
next_uncovered("span.red.branch", reverse);
170+
}
171+
172+
document.addEventListener("keypress", function(event) {
173+
console.log(event);
174+
const reverse = event.shiftKey;
175+
if (event.code == "KeyL") {
176+
next_line(reverse);
177+
}
178+
if (event.code == "KeyB") {
179+
next_branch(reverse);
180+
}
181+
if (event.code == "KeyR") {
182+
next_region(reverse);
183+
}
184+
185+
});
186+
)javascript";
187+
91188
const char *CSSForCoverage =
92189
R"(.red {
93190
background-color: #f004;
94191
}
95192
.cyan {
96193
background-color: cyan;
97194
}
195+
html {
196+
scroll-behavior: smooth;
197+
}
98198
body {
99199
font-family: -apple-system, sans-serif;
100200
}
@@ -171,6 +271,18 @@ table {
171271
text-align: right;
172272
color: #d00;
173273
}
274+
.uncovered-line.selected {
275+
color: #f00;
276+
font-weight: bold;
277+
}
278+
.region.red.selected {
279+
background-color: #f008;
280+
font-weight: bold;
281+
}
282+
.branch.red.selected {
283+
background-color: #f008;
284+
font-weight: bold;
285+
}
174286
.tooltip {
175287
position: relative;
176288
display: inline;
@@ -237,6 +349,13 @@ tr:has(> td >a:target) {
237349
a {
238350
color: inherit;
239351
}
352+
.control {
353+
position: fixed;
354+
top: 0em;
355+
right: 0em;
356+
padding: 1em;
357+
background: #FFF8;
358+
}
240359
@media (prefers-color-scheme: dark) {
241360
body {
242361
background-color: #222;
@@ -254,6 +373,9 @@ a {
254373
.tooltip {
255374
background-color: #068;
256375
}
376+
.control {
377+
background: #2228;
378+
}
257379
}
258380
)";
259381

@@ -298,8 +420,18 @@ std::string getPathToStyle(StringRef ViewPath) {
298420
return PathToStyle + "style.css";
299421
}
300422

423+
std::string getPathToJavaScript(StringRef ViewPath) {
424+
std::string PathToJavaScript;
425+
std::string PathSep = std::string(sys::path::get_separator());
426+
unsigned NumSeps = ViewPath.count(PathSep);
427+
for (unsigned I = 0, E = NumSeps; I < E; ++I)
428+
PathToJavaScript += ".." + PathSep;
429+
return PathToJavaScript + "control.js";
430+
}
431+
301432
void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
302-
const std::string &PathToStyle = "") {
433+
const std::string &PathToStyle = "",
434+
const std::string &PathToJavaScript = "") {
303435
OS << "<!doctype html>"
304436
"<html>"
305437
<< BeginHeader;
@@ -311,6 +443,13 @@ void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
311443
OS << "<link rel='stylesheet' type='text/css' href='"
312444
<< escape(PathToStyle, Opts) << "'>";
313445

446+
// Link to a JavaScript if one is available
447+
if (PathToJavaScript.empty())
448+
;
449+
// OS << "<style>" << CSSForCoverage << "</style>";
450+
else
451+
OS << "<script src='" << escape(PathToJavaScript, Opts) << "'></script>";
452+
314453
OS << EndHeader << "<body>";
315454
}
316455

@@ -390,7 +529,8 @@ CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
390529
emitPrelude(*OS.get(), Opts);
391530
} else {
392531
std::string ViewPath = getOutputPath(Path, "html", InToplevel);
393-
emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath));
532+
emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath),
533+
getPathToJavaScript(ViewPath));
394534
}
395535

396536
return std::move(OS);
@@ -442,6 +582,17 @@ Error CoveragePrinterHTML::emitStyleSheet() {
442582
return Error::success();
443583
}
444584

585+
Error CoveragePrinterHTML::emitJavaScript() {
586+
auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);
587+
if (Error E = JSOrErr.takeError())
588+
return E;
589+
590+
OwnedStream JS = std::move(JSOrErr.get());
591+
JS->operator<<(JSForCoverage);
592+
593+
return Error::success();
594+
}
595+
445596
void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
446597
const std::string &Title) {
447598
// Emit some basic information about the coverage report.
@@ -487,6 +638,9 @@ Error CoveragePrinterHTML::createIndexFile(
487638
if (Error E = emitStyleSheet())
488639
return E;
489640

641+
if (Error E = emitJavaScript())
642+
return E;
643+
490644
// Emit a file index along with some coverage statistics.
491645
auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
492646
if (Error E = OSOrErr.takeError())
@@ -495,7 +649,7 @@ Error CoveragePrinterHTML::createIndexFile(
495649
raw_ostream &OSRef = *OS.get();
496650

497651
assert(Opts.hasOutputDirectory() && "No output directory for index file");
498-
emitPrelude(OSRef, Opts, getPathToStyle(""));
652+
emitPrelude(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));
499653

500654
emitReportHeader(OSRef, "Coverage Report");
501655

@@ -561,7 +715,8 @@ struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
561715

562716
auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
563717
/*InToplevel=*/false);
564-
emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath));
718+
emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),
719+
getPathToJavaScript(IndexHtmlPath));
565720

566721
auto NavLink = buildTitleLinks(LCPath);
567722
Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
@@ -800,7 +955,10 @@ void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
800955
auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
801956
if (getOptions().Debug)
802957
HighlightedRanges.emplace_back(LC, RC);
803-
return tag("span", Snippet, std::string(*Color));
958+
if (Snippet.empty())
959+
return tag("span", Snippet, std::string(*Color));
960+
else
961+
return tag("span", Snippet, "region " + std::string(*Color));
804962
};
805963

806964
auto CheckIfUncovered = [&](const CoverageSegment *S) {
@@ -883,7 +1041,9 @@ void SourceCoverageViewHTML::renderLineCoverageColumn(
8831041
if (Line.isMapped())
8841042
Count = tag("pre", formatCount(Line.getExecutionCount()));
8851043
std::string CoverageClass =
886-
(Line.getExecutionCount() > 0) ? "covered-line" : "uncovered-line";
1044+
(Line.getExecutionCount() > 0)
1045+
? "covered-line"
1046+
: (Line.isMapped() ? "uncovered-line" : "skipped-line");
8871047
OS << tag("td", Count, CoverageClass);
8881048
}
8891049

@@ -957,7 +1117,7 @@ void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
9571117
}
9581118

9591119
// Display TrueCount or TruePercent.
960-
std::string TrueColor = R.ExecutionCount ? "None" : "red";
1120+
std::string TrueColor = R.ExecutionCount ? "None" : "red branch";
9611121
std::string TrueCovClass =
9621122
(R.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
9631123

@@ -969,7 +1129,7 @@ void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
9691129
OS << format("%0.2f", TruePercent) << "%, ";
9701130

9711131
// Display FalseCount or FalsePercent.
972-
std::string FalseColor = R.FalseExecutionCount ? "None" : "red";
1132+
std::string FalseColor = R.FalseExecutionCount ? "None" : "red branch";
9731133
std::string FalseCovClass =
9741134
(R.FalseExecutionCount > 0) ? "covered-line" : "uncovered-line";
9751135

@@ -1053,24 +1213,21 @@ void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
10531213
if (getOptions().hasCreatedTime())
10541214
OS << tag(CreatedTimeTag,
10551215
escape(getOptions().CreatedTimeStr, getOptions()));
1216+
1217+
OS << tag("span",
1218+
a("javascript:next_line()", "next uncovered line (L)") + ", " +
1219+
a("javascript:next_region()", "next uncovered region (R)") +
1220+
", " +
1221+
a("javascript:next_branch()", "next uncovered branch (B)"),
1222+
"control");
10561223
}
10571224

10581225
void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1059-
unsigned FirstUncoveredLineNo,
10601226
unsigned ViewDepth) {
1061-
std::string SourceLabel;
1062-
if (FirstUncoveredLineNo == 0) {
1063-
SourceLabel = tag("td", tag("pre", "Source"));
1064-
} else {
1065-
std::string LinkTarget = "#L" + utostr(uint64_t(FirstUncoveredLineNo));
1066-
SourceLabel =
1067-
tag("td", tag("pre", "Source (" +
1068-
a(LinkTarget, "jump to first uncovered line") +
1069-
")"));
1070-
}
1227+
std::string Links;
10711228

10721229
renderLinePrefix(OS, ViewDepth);
1073-
OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"))
1074-
<< SourceLabel;
1230+
OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"));
1231+
OS << tag("td", tag("pre", "Source" + Links));
10751232
renderLineSuffix(OS, ViewDepth);
10761233
}

llvm/tools/llvm-cov/SourceCoverageViewHTML.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class CoveragePrinterHTML : public CoveragePrinter {
3838

3939
protected:
4040
Error emitStyleSheet();
41+
Error emitJavaScript();
4142
void emitReportHeader(raw_ostream &OSRef, const std::string &Title);
4243

4344
private:
@@ -105,8 +106,7 @@ class SourceCoverageViewHTML : public SourceCoverageView {
105106

106107
void renderTitle(raw_ostream &OS, StringRef Title) override;
107108

108-
void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
109-
unsigned IndentLevel) override;
109+
void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) override;
110110

111111
public:
112112
SourceCoverageViewHTML(StringRef SourceName, const MemoryBuffer &File,

llvm/tools/llvm-cov/SourceCoverageViewText.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,5 +414,4 @@ void SourceCoverageViewText::renderTitle(raw_ostream &OS, StringRef Title) {
414414
<< getOptions().CreatedTimeStr << "\n";
415415
}
416416

417-
void SourceCoverageViewText::renderTableHeader(raw_ostream &, unsigned,
418-
unsigned) {}
417+
void SourceCoverageViewText::renderTableHeader(raw_ostream &, unsigned) {}

llvm/tools/llvm-cov/SourceCoverageViewText.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ class SourceCoverageViewText : public SourceCoverageView {
9393

9494
void renderTitle(raw_ostream &OS, StringRef Title) override;
9595

96-
void renderTableHeader(raw_ostream &OS, unsigned FirstUncoveredLineNo,
97-
unsigned IndentLevel) override;
96+
void renderTableHeader(raw_ostream &OS, unsigned IndentLevel) override;
9897

9998
public:
10099
SourceCoverageViewText(StringRef SourceName, const MemoryBuffer &File,

0 commit comments

Comments
 (0)