Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
35231 views
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
94
function next_uncovered(selector, reverse, scroll_selector) {
95
function visit_element(element) {
96
element.classList.add("seen");
97
element.classList.add("selected");
98
99
if (!scroll_selector) {
100
scroll_selector = "tr:has(.selected) td.line-number"
101
}
102
103
const scroll_to = document.querySelector(scroll_selector);
104
if (scroll_to) {
105
scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"});
106
}
107
108
}
109
110
function select_one() {
111
if (!reverse) {
112
const previously_selected = document.querySelector(".selected");
113
114
if (previously_selected) {
115
previously_selected.classList.remove("selected");
116
}
117
118
return document.querySelector(selector + ":not(.seen)");
119
} else {
120
const previously_selected = document.querySelector(".selected");
121
122
if (previously_selected) {
123
previously_selected.classList.remove("selected");
124
previously_selected.classList.remove("seen");
125
}
126
127
const nodes = document.querySelectorAll(selector + ".seen");
128
if (nodes) {
129
const last = nodes[nodes.length - 1]; // last
130
return last;
131
} else {
132
return undefined;
133
}
134
}
135
}
136
137
function reset_all() {
138
if (!reverse) {
139
const all_seen = document.querySelectorAll(selector + ".seen");
140
141
if (all_seen) {
142
all_seen.forEach(e => e.classList.remove("seen"));
143
}
144
} else {
145
const all_seen = document.querySelectorAll(selector + ":not(.seen)");
146
147
if (all_seen) {
148
all_seen.forEach(e => e.classList.add("seen"));
149
}
150
}
151
152
}
153
154
const uncovered = select_one();
155
156
if (uncovered) {
157
visit_element(uncovered);
158
} else {
159
reset_all();
160
161
162
const uncovered = select_one();
163
164
if (uncovered) {
165
visit_element(uncovered);
166
}
167
}
168
}
169
170
function next_line(reverse) {
171
next_uncovered("td.uncovered-line", reverse)
172
}
173
174
function next_region(reverse) {
175
next_uncovered("span.red.region", reverse);
176
}
177
178
function next_branch(reverse) {
179
next_uncovered("span.red.branch", reverse);
180
}
181
182
document.addEventListener("keypress", function(event) {
183
console.log(event);
184
const reverse = event.shiftKey;
185
if (event.code == "KeyL") {
186
next_line(reverse);
187
}
188
if (event.code == "KeyB") {
189
next_branch(reverse);
190
}
191
if (event.code == "KeyR") {
192
next_region(reverse);
193
}
194
195
});
196
)javascript";
197
198
const char *CSSForCoverage =
199
R"(.red {
200
background-color: #f004;
201
}
202
.cyan {
203
background-color: cyan;
204
}
205
html {
206
scroll-behavior: smooth;
207
}
208
body {
209
font-family: -apple-system, sans-serif;
210
}
211
pre {
212
margin-top: 0px !important;
213
margin-bottom: 0px !important;
214
}
215
.source-name-title {
216
padding: 5px 10px;
217
border-bottom: 1px solid #8888;
218
background-color: #0002;
219
line-height: 35px;
220
}
221
.centered {
222
display: table;
223
margin-left: left;
224
margin-right: auto;
225
border: 1px solid #8888;
226
border-radius: 3px;
227
}
228
.expansion-view {
229
margin-left: 0px;
230
margin-top: 5px;
231
margin-right: 5px;
232
margin-bottom: 5px;
233
border: 1px solid #8888;
234
border-radius: 3px;
235
}
236
table {
237
border-collapse: collapse;
238
}
239
.light-row {
240
border: 1px solid #8888;
241
border-left: none;
242
border-right: none;
243
}
244
.light-row-bold {
245
border: 1px solid #8888;
246
border-left: none;
247
border-right: none;
248
font-weight: bold;
249
}
250
.column-entry {
251
text-align: left;
252
}
253
.column-entry-bold {
254
font-weight: bold;
255
text-align: left;
256
}
257
.column-entry-yellow {
258
text-align: left;
259
background-color: #ff06;
260
}
261
.column-entry-red {
262
text-align: left;
263
background-color: #f004;
264
}
265
.column-entry-gray {
266
text-align: left;
267
background-color: #fff4;
268
}
269
.column-entry-green {
270
text-align: left;
271
background-color: #0f04;
272
}
273
.line-number {
274
text-align: right;
275
}
276
.covered-line {
277
text-align: right;
278
color: #06d;
279
}
280
.uncovered-line {
281
text-align: right;
282
color: #d00;
283
}
284
.uncovered-line.selected {
285
color: #f00;
286
font-weight: bold;
287
}
288
.region.red.selected {
289
background-color: #f008;
290
font-weight: bold;
291
}
292
.branch.red.selected {
293
background-color: #f008;
294
font-weight: bold;
295
}
296
.tooltip {
297
position: relative;
298
display: inline;
299
background-color: #bef;
300
text-decoration: none;
301
}
302
.tooltip span.tooltip-content {
303
position: absolute;
304
width: 100px;
305
margin-left: -50px;
306
color: #FFFFFF;
307
background: #000000;
308
height: 30px;
309
line-height: 30px;
310
text-align: center;
311
visibility: hidden;
312
border-radius: 6px;
313
}
314
.tooltip span.tooltip-content:after {
315
content: '';
316
position: absolute;
317
top: 100%;
318
left: 50%;
319
margin-left: -8px;
320
width: 0; height: 0;
321
border-top: 8px solid #000000;
322
border-right: 8px solid transparent;
323
border-left: 8px solid transparent;
324
}
325
:hover.tooltip span.tooltip-content {
326
visibility: visible;
327
opacity: 0.8;
328
bottom: 30px;
329
left: 50%;
330
z-index: 999;
331
}
332
th, td {
333
vertical-align: top;
334
padding: 2px 8px;
335
border-collapse: collapse;
336
border-right: 1px solid #8888;
337
border-left: 1px solid #8888;
338
text-align: left;
339
}
340
td pre {
341
display: inline-block;
342
text-decoration: inherit;
343
}
344
td:first-child {
345
border-left: none;
346
}
347
td:last-child {
348
border-right: none;
349
}
350
tr:hover {
351
background-color: #eee;
352
}
353
tr:last-child {
354
border-bottom: none;
355
}
356
tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
357
background-color: #8884;
358
}
359
a {
360
color: inherit;
361
}
362
.control {
363
position: fixed;
364
top: 0em;
365
right: 0em;
366
padding: 1em;
367
background: #FFF8;
368
}
369
@media (prefers-color-scheme: dark) {
370
body {
371
background-color: #222;
372
color: whitesmoke;
373
}
374
tr:hover {
375
background-color: #111;
376
}
377
.covered-line {
378
color: #39f;
379
}
380
.uncovered-line {
381
color: #f55;
382
}
383
.tooltip {
384
background-color: #068;
385
}
386
.control {
387
background: #2228;
388
}
389
tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
390
background-color: #8884;
391
}
392
}
393
)";
394
395
const char *EndHeader = "</head>";
396
397
const char *BeginCenteredDiv = "<div class='centered'>";
398
399
const char *EndCenteredDiv = "</div>";
400
401
const char *BeginSourceNameDiv = "<div class='source-name-title'>";
402
403
const char *EndSourceNameDiv = "</div>";
404
405
const char *BeginCodeTD = "<td class='code'>";
406
407
const char *EndCodeTD = "</td>";
408
409
const char *BeginPre = "<pre>";
410
411
const char *EndPre = "</pre>";
412
413
const char *BeginExpansionDiv = "<div class='expansion-view'>";
414
415
const char *EndExpansionDiv = "</div>";
416
417
const char *BeginTable = "<table>";
418
419
const char *EndTable = "</table>";
420
421
const char *ProjectTitleTag = "h1";
422
423
const char *ReportTitleTag = "h2";
424
425
const char *CreatedTimeTag = "h4";
426
427
std::string getPathToStyle(StringRef ViewPath) {
428
std::string PathToStyle;
429
std::string PathSep = std::string(sys::path::get_separator());
430
unsigned NumSeps = ViewPath.count(PathSep);
431
for (unsigned I = 0, E = NumSeps; I < E; ++I)
432
PathToStyle += ".." + PathSep;
433
return PathToStyle + "style.css";
434
}
435
436
std::string getPathToJavaScript(StringRef ViewPath) {
437
std::string PathToJavaScript;
438
std::string PathSep = std::string(sys::path::get_separator());
439
unsigned NumSeps = ViewPath.count(PathSep);
440
for (unsigned I = 0, E = NumSeps; I < E; ++I)
441
PathToJavaScript += ".." + PathSep;
442
return PathToJavaScript + "control.js";
443
}
444
445
void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
446
const std::string &PathToStyle = "",
447
const std::string &PathToJavaScript = "") {
448
OS << "<!doctype html>"
449
"<html>"
450
<< BeginHeader;
451
452
// Link to a stylesheet if one is available. Otherwise, use the default style.
453
if (PathToStyle.empty())
454
OS << "<style>" << CSSForCoverage << "</style>";
455
else
456
OS << "<link rel='stylesheet' type='text/css' href='"
457
<< escape(PathToStyle, Opts) << "'>";
458
459
// Link to a JavaScript if one is available
460
if (PathToJavaScript.empty())
461
OS << "<script>" << JSForCoverage << "</script>";
462
else
463
OS << "<script src='" << escape(PathToJavaScript, Opts) << "'></script>";
464
465
OS << EndHeader << "<body>";
466
}
467
468
void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts,
469
const std::string &FirstCol, const FileCoverageSummary &FCS,
470
bool IsTotals) {
471
SmallVector<std::string, 8> Columns;
472
473
// Format a coverage triple and add the result to the list of columns.
474
auto AddCoverageTripleToColumn =
475
[&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) {
476
std::string S;
477
{
478
raw_string_ostream RSO{S};
479
if (Total)
480
RSO << format("%*.2f", 7, Pctg) << "% ";
481
else
482
RSO << "- ";
483
RSO << '(' << Hit << '/' << Total << ')';
484
}
485
const char *CellClass = "column-entry-yellow";
486
if (!Total)
487
CellClass = "column-entry-gray";
488
else if (Pctg >= Opts.HighCovWatermark)
489
CellClass = "column-entry-green";
490
else if (Pctg < Opts.LowCovWatermark)
491
CellClass = "column-entry-red";
492
Columns.emplace_back(tag("td", tag("pre", S), CellClass));
493
};
494
495
Columns.emplace_back(tag("td", tag("pre", FirstCol)));
496
AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
497
FCS.FunctionCoverage.getNumFunctions(),
498
FCS.FunctionCoverage.getPercentCovered());
499
if (Opts.ShowInstantiationSummary)
500
AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
501
FCS.InstantiationCoverage.getNumFunctions(),
502
FCS.InstantiationCoverage.getPercentCovered());
503
AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
504
FCS.LineCoverage.getNumLines(),
505
FCS.LineCoverage.getPercentCovered());
506
if (Opts.ShowRegionSummary)
507
AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
508
FCS.RegionCoverage.getNumRegions(),
509
FCS.RegionCoverage.getPercentCovered());
510
if (Opts.ShowBranchSummary)
511
AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
512
FCS.BranchCoverage.getNumBranches(),
513
FCS.BranchCoverage.getPercentCovered());
514
if (Opts.ShowMCDCSummary)
515
AddCoverageTripleToColumn(FCS.MCDCCoverage.getCoveredPairs(),
516
FCS.MCDCCoverage.getNumPairs(),
517
FCS.MCDCCoverage.getPercentCovered());
518
519
if (IsTotals)
520
OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");
521
else
522
OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");
523
}
524
525
void emitEpilog(raw_ostream &OS) {
526
OS << "</body>"
527
<< "</html>";
528
}
529
530
} // anonymous namespace
531
532
Expected<CoveragePrinter::OwnedStream>
533
CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
534
auto OSOrErr = createOutputStream(Path, "html", InToplevel);
535
if (!OSOrErr)
536
return OSOrErr;
537
538
OwnedStream OS = std::move(OSOrErr.get());
539
540
if (!Opts.hasOutputDirectory()) {
541
emitPrelude(*OS.get(), Opts);
542
} else {
543
std::string ViewPath = getOutputPath(Path, "html", InToplevel);
544
emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath),
545
getPathToJavaScript(ViewPath));
546
}
547
548
return std::move(OS);
549
}
550
551
void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
552
emitEpilog(*OS.get());
553
}
554
555
/// Emit column labels for the table in the index.
556
static void emitColumnLabelsForIndex(raw_ostream &OS,
557
const CoverageViewOptions &Opts) {
558
SmallVector<std::string, 4> Columns;
559
Columns.emplace_back(tag("td", "Filename", "column-entry-bold"));
560
Columns.emplace_back(tag("td", "Function Coverage", "column-entry-bold"));
561
if (Opts.ShowInstantiationSummary)
562
Columns.emplace_back(
563
tag("td", "Instantiation Coverage", "column-entry-bold"));
564
Columns.emplace_back(tag("td", "Line Coverage", "column-entry-bold"));
565
if (Opts.ShowRegionSummary)
566
Columns.emplace_back(tag("td", "Region Coverage", "column-entry-bold"));
567
if (Opts.ShowBranchSummary)
568
Columns.emplace_back(tag("td", "Branch Coverage", "column-entry-bold"));
569
if (Opts.ShowMCDCSummary)
570
Columns.emplace_back(tag("td", "MC/DC", "column-entry-bold"));
571
OS << tag("tr", join(Columns.begin(), Columns.end(), ""));
572
}
573
574
std::string
575
CoveragePrinterHTML::buildLinkToFile(StringRef SF,
576
const FileCoverageSummary &FCS) const {
577
SmallString<128> LinkTextStr(sys::path::relative_path(FCS.Name));
578
sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
579
sys::path::native(LinkTextStr);
580
std::string LinkText = escape(LinkTextStr, Opts);
581
std::string LinkTarget =
582
escape(getOutputPath(SF, "html", /*InToplevel=*/false), Opts);
583
return a(LinkTarget, LinkText);
584
}
585
586
Error CoveragePrinterHTML::emitStyleSheet() {
587
auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
588
if (Error E = CSSOrErr.takeError())
589
return E;
590
591
OwnedStream CSS = std::move(CSSOrErr.get());
592
CSS->operator<<(CSSForCoverage);
593
594
return Error::success();
595
}
596
597
Error CoveragePrinterHTML::emitJavaScript() {
598
auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);
599
if (Error E = JSOrErr.takeError())
600
return E;
601
602
OwnedStream JS = std::move(JSOrErr.get());
603
JS->operator<<(JSForCoverage);
604
605
return Error::success();
606
}
607
608
void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
609
const std::string &Title) {
610
// Emit some basic information about the coverage report.
611
if (Opts.hasProjectTitle())
612
OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));
613
OSRef << tag(ReportTitleTag, Title);
614
if (Opts.hasCreatedTime())
615
OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));
616
617
// Emit a link to some documentation.
618
OSRef << tag("p", "Click " +
619
a("http://clang.llvm.org/docs/"
620
"SourceBasedCodeCoverage.html#interpreting-reports",
621
"here") +
622
" for information about interpreting this report.");
623
624
// Emit a table containing links to reports for each file in the covmapping.
625
// Exclude files which don't contain any regions.
626
OSRef << BeginCenteredDiv << BeginTable;
627
emitColumnLabelsForIndex(OSRef, Opts);
628
}
629
630
/// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
631
/// false, link the summary to \p SF.
632
void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
633
const FileCoverageSummary &FCS,
634
bool IsTotals) const {
635
// Simplify the display file path, and wrap it in a link if requested.
636
std::string Filename;
637
if (IsTotals) {
638
Filename = std::string(SF);
639
} else {
640
Filename = buildLinkToFile(SF, FCS);
641
}
642
643
emitTableRow(OS, Opts, Filename, FCS, IsTotals);
644
}
645
646
Error CoveragePrinterHTML::createIndexFile(
647
ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
648
const CoverageFiltersMatchAll &Filters) {
649
// Emit the default stylesheet.
650
if (Error E = emitStyleSheet())
651
return E;
652
653
// Emit the JavaScript UI implementation
654
if (Error E = emitJavaScript())
655
return E;
656
657
// Emit a file index along with some coverage statistics.
658
auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
659
if (Error E = OSOrErr.takeError())
660
return E;
661
auto OS = std::move(OSOrErr.get());
662
raw_ostream &OSRef = *OS.get();
663
664
assert(Opts.hasOutputDirectory() && "No output directory for index file");
665
emitPrelude(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));
666
667
emitReportHeader(OSRef, "Coverage Report");
668
669
FileCoverageSummary Totals("TOTALS");
670
auto FileReports = CoverageReport::prepareFileReports(
671
Coverage, Totals, SourceFiles, Opts, Filters);
672
bool EmptyFiles = false;
673
for (unsigned I = 0, E = FileReports.size(); I < E; ++I) {
674
if (FileReports[I].FunctionCoverage.getNumFunctions())
675
emitFileSummary(OSRef, SourceFiles[I], FileReports[I]);
676
else
677
EmptyFiles = true;
678
}
679
emitFileSummary(OSRef, "Totals", Totals, /*IsTotals=*/true);
680
OSRef << EndTable << EndCenteredDiv;
681
682
// Emit links to files which don't contain any functions. These are normally
683
// not very useful, but could be relevant for code which abuses the
684
// preprocessor.
685
if (EmptyFiles && Filters.empty()) {
686
OSRef << tag("p", "Files which contain no functions. (These "
687
"files contain code pulled into other files "
688
"by the preprocessor.)\n");
689
OSRef << BeginCenteredDiv << BeginTable;
690
for (unsigned I = 0, E = FileReports.size(); I < E; ++I)
691
if (!FileReports[I].FunctionCoverage.getNumFunctions()) {
692
std::string Link = buildLinkToFile(SourceFiles[I], FileReports[I]);
693
OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
694
}
695
OSRef << EndTable << EndCenteredDiv;
696
}
697
698
OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts));
699
emitEpilog(OSRef);
700
701
return Error::success();
702
}
703
704
struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
705
CoveragePrinterHTMLDirectory &Printer;
706
707
Reporter(CoveragePrinterHTMLDirectory &Printer,
708
const coverage::CoverageMapping &Coverage,
709
const CoverageFiltersMatchAll &Filters)
710
: DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
711
Printer(Printer) {}
712
713
Error generateSubDirectoryReport(SubFileReports &&SubFiles,
714
SubDirReports &&SubDirs,
715
FileCoverageSummary &&SubTotals) override {
716
auto &LCPath = SubTotals.Name;
717
assert(Options.hasOutputDirectory() &&
718
"No output directory for index file");
719
720
SmallString<128> OSPath = LCPath;
721
sys::path::append(OSPath, "index");
722
auto OSOrErr = Printer.createOutputStream(OSPath, "html",
723
/*InToplevel=*/false);
724
if (auto E = OSOrErr.takeError())
725
return E;
726
auto OS = std::move(OSOrErr.get());
727
raw_ostream &OSRef = *OS.get();
728
729
auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
730
/*InToplevel=*/false);
731
emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),
732
getPathToJavaScript(IndexHtmlPath));
733
734
auto NavLink = buildTitleLinks(LCPath);
735
Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
736
737
std::vector<const FileCoverageSummary *> EmptyFiles;
738
739
// Make directories at the top of the table.
740
for (auto &&SubDir : SubDirs) {
741
auto &Report = SubDir.second.first;
742
if (!Report.FunctionCoverage.getNumFunctions())
743
EmptyFiles.push_back(&Report);
744
else
745
emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
746
/*IsTotals=*/false);
747
}
748
749
for (auto &&SubFile : SubFiles) {
750
auto &Report = SubFile.second;
751
if (!Report.FunctionCoverage.getNumFunctions())
752
EmptyFiles.push_back(&Report);
753
else
754
emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
755
/*IsTotals=*/false);
756
}
757
758
// Emit the totals row.
759
emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);
760
OSRef << EndTable << EndCenteredDiv;
761
762
// Emit links to files which don't contain any functions. These are normally
763
// not very useful, but could be relevant for code which abuses the
764
// preprocessor.
765
if (!EmptyFiles.empty()) {
766
OSRef << tag("p", "Files which contain no functions. (These "
767
"files contain code pulled into other files "
768
"by the preprocessor.)\n");
769
OSRef << BeginCenteredDiv << BeginTable;
770
for (auto FCS : EmptyFiles) {
771
auto Link = buildRelLinkToFile(FCS->Name);
772
OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
773
}
774
OSRef << EndTable << EndCenteredDiv;
775
}
776
777
// Emit epilog.
778
OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));
779
emitEpilog(OSRef);
780
781
return Error::success();
782
}
783
784
/// Make a title with hyperlinks to the index.html files of each hierarchy
785
/// of the report.
786
std::string buildTitleLinks(StringRef LCPath) const {
787
// For each report level in LCPStack, extract the path component and
788
// calculate the number of "../" relative to current LCPath.
789
SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components;
790
791
auto Iter = LCPStack.begin(), IterE = LCPStack.end();
792
SmallString<128> RootPath;
793
if (*Iter == 0) {
794
// If llvm-cov works on relative coverage mapping data, the LCP of
795
// all source file paths can be 0, which makes the title path empty.
796
// As we like adding a slash at the back of the path to indicate a
797
// directory, in this case, we use "." as the root path to make it
798
// not be confused with the root path "/".
799
RootPath = ".";
800
} else {
801
RootPath = LCPath.substr(0, *Iter);
802
sys::path::native(RootPath);
803
sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true);
804
}
805
Components.emplace_back(std::move(RootPath), 0);
806
807
for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
808
SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last);
809
sys::path::native(SubPath);
810
sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true);
811
auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1;
812
Components.back().second += Level;
813
Components.emplace_back(std::move(SubPath), Level);
814
}
815
816
// Then we make the title accroding to Components.
817
std::string S;
818
for (auto I = Components.begin(), E = Components.end();;) {
819
auto &Name = I->first;
820
if (++I == E) {
821
S += a("./index.html", Name);
822
S += sys::path::get_separator();
823
break;
824
}
825
826
SmallString<128> Link;
827
for (unsigned J = I->second; J > 0; --J)
828
Link += "../";
829
Link += "index.html";
830
S += a(Link, Name);
831
S += sys::path::get_separator();
832
}
833
return S;
834
}
835
836
std::string buildRelLinkToFile(StringRef RelPath) const {
837
SmallString<128> LinkTextStr(RelPath);
838
sys::path::native(LinkTextStr);
839
840
// remove_dots will remove trailing slash, so we need to check before it.
841
auto IsDir = LinkTextStr.ends_with(sys::path::get_separator());
842
sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
843
844
SmallString<128> LinkTargetStr(LinkTextStr);
845
if (IsDir) {
846
LinkTextStr += sys::path::get_separator();
847
sys::path::append(LinkTargetStr, "index.html");
848
} else {
849
LinkTargetStr += ".html";
850
}
851
852
auto LinkText = escape(LinkTextStr, Options);
853
auto LinkTarget = escape(LinkTargetStr, Options);
854
return a(LinkTarget, LinkText);
855
}
856
};
857
858
Error CoveragePrinterHTMLDirectory::createIndexFile(
859
ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
860
const CoverageFiltersMatchAll &Filters) {
861
// The createSubIndexFile function only works when SourceFiles is
862
// more than one. So we fallback to CoveragePrinterHTML when it is.
863
if (SourceFiles.size() <= 1)
864
return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters);
865
866
// Emit the default stylesheet.
867
if (Error E = emitStyleSheet())
868
return E;
869
870
// Emit the JavaScript UI implementation
871
if (Error E = emitJavaScript())
872
return E;
873
874
// Emit index files in every subdirectory.
875
Reporter Report(*this, Coverage, Filters);
876
auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
877
if (auto E = TotalsOrErr.takeError())
878
return E;
879
auto &LCPath = TotalsOrErr->Name;
880
881
// Emit the top level index file. Top level index file is just a redirection
882
// to the index file in the LCP directory.
883
auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
884
if (auto E = OSOrErr.takeError())
885
return E;
886
auto OS = std::move(OSOrErr.get());
887
auto LCPIndexFilePath =
888
getOutputPath((LCPath + "index").str(), "html", /*InToplevel=*/false);
889
*OS.get() << R"(<!DOCTYPE html>
890
<html>
891
<head>
892
<meta http-equiv="Refresh" content="0; url=')"
893
<< LCPIndexFilePath << R"('" />
894
</head>
895
<body></body>
896
</html>
897
)";
898
899
return Error::success();
900
}
901
902
void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
903
OS << BeginCenteredDiv << BeginTable;
904
}
905
906
void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
907
OS << EndTable << EndCenteredDiv;
908
}
909
910
void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
911
OS << BeginSourceNameDiv << tag("pre", escape(getSourceName(), getOptions()))
912
<< EndSourceNameDiv;
913
}
914
915
void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
916
OS << "<tr>";
917
}
918
919
void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
920
// If this view has sub-views, renderLine() cannot close the view's cell.
921
// Take care of it here, after all sub-views have been rendered.
922
if (hasSubViews())
923
OS << EndCodeTD;
924
OS << "</tr>";
925
}
926
927
void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
928
// The table-based output makes view dividers unnecessary.
929
}
930
931
void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
932
const LineCoverageStats &LCS,
933
unsigned ExpansionCol, unsigned) {
934
StringRef Line = L.Line;
935
unsigned LineNo = L.LineNo;
936
937
// Steps for handling text-escaping, highlighting, and tooltip creation:
938
//
939
// 1. Split the line into N+1 snippets, where N = |Segments|. The first
940
// snippet starts from Col=1 and ends at the start of the first segment.
941
// The last snippet starts at the last mapped column in the line and ends
942
// at the end of the line. Both are required but may be empty.
943
944
SmallVector<std::string, 8> Snippets;
945
CoverageSegmentArray Segments = LCS.getLineSegments();
946
947
unsigned LCol = 1;
948
auto Snip = [&](unsigned Start, unsigned Len) {
949
Snippets.push_back(std::string(Line.substr(Start, Len)));
950
LCol += Len;
951
};
952
953
Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
954
955
for (unsigned I = 1, E = Segments.size(); I < E; ++I)
956
Snip(LCol - 1, Segments[I]->Col - LCol);
957
958
// |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
959
Snip(LCol - 1, Line.size() + 1 - LCol);
960
961
// 2. Escape all of the snippets.
962
963
for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
964
Snippets[I] = escape(Snippets[I], getOptions());
965
966
// 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment
967
// 1 to set the highlight for snippet 2, segment 2 to set the highlight for
968
// snippet 3, and so on.
969
970
std::optional<StringRef> Color;
971
SmallVector<std::pair<unsigned, unsigned>, 2> HighlightedRanges;
972
auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
973
if (getOptions().Debug)
974
HighlightedRanges.emplace_back(LC, RC);
975
if (Snippet.empty())
976
return tag("span", Snippet, std::string(*Color));
977
else
978
return tag("span", Snippet, "region " + std::string(*Color));
979
};
980
981
auto CheckIfUncovered = [&](const CoverageSegment *S) {
982
return S && (!S->IsGapRegion || (Color && *Color == "red")) &&
983
S->HasCount && S->Count == 0;
984
};
985
986
if (CheckIfUncovered(LCS.getWrappedSegment())) {
987
Color = "red";
988
if (!Snippets[0].empty())
989
Snippets[0] = Highlight(Snippets[0], 1, 1 + Snippets[0].size());
990
}
991
992
for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
993
const auto *CurSeg = Segments[I];
994
if (CheckIfUncovered(CurSeg))
995
Color = "red";
996
else if (CurSeg->Col == ExpansionCol)
997
Color = "cyan";
998
else
999
Color = std::nullopt;
1000
1001
if (Color)
1002
Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
1003
CurSeg->Col + Snippets[I + 1].size());
1004
}
1005
1006
if (Color && Segments.empty())
1007
Snippets.back() = Highlight(Snippets.back(), 1, 1 + Snippets.back().size());
1008
1009
if (getOptions().Debug) {
1010
for (const auto &Range : HighlightedRanges) {
1011
errs() << "Highlighted line " << LineNo << ", " << Range.first << " -> ";
1012
if (Range.second == 0)
1013
errs() << "?";
1014
else
1015
errs() << Range.second;
1016
errs() << "\n";
1017
}
1018
}
1019
1020
// 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
1021
// sub-line region count tooltips if needed.
1022
1023
if (shouldRenderRegionMarkers(LCS)) {
1024
// Just consider the segments which start *and* end on this line.
1025
for (unsigned I = 0, E = Segments.size() - 1; I < E; ++I) {
1026
const auto *CurSeg = Segments[I];
1027
if (!CurSeg->IsRegionEntry)
1028
continue;
1029
if (CurSeg->Count == LCS.getExecutionCount())
1030
continue;
1031
1032
Snippets[I + 1] =
1033
tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count),
1034
"tooltip-content"),
1035
"tooltip");
1036
1037
if (getOptions().Debug)
1038
errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
1039
<< formatCount(CurSeg->Count) << "\n";
1040
}
1041
}
1042
1043
OS << BeginCodeTD;
1044
OS << BeginPre;
1045
for (const auto &Snippet : Snippets)
1046
OS << Snippet;
1047
OS << EndPre;
1048
1049
// If there are no sub-views left to attach to this cell, end the cell.
1050
// Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
1051
if (!hasSubViews())
1052
OS << EndCodeTD;
1053
}
1054
1055
void SourceCoverageViewHTML::renderLineCoverageColumn(
1056
raw_ostream &OS, const LineCoverageStats &Line) {
1057
std::string Count;
1058
if (Line.isMapped())
1059
Count = tag("pre", formatCount(Line.getExecutionCount()));
1060
std::string CoverageClass =
1061
(Line.getExecutionCount() > 0)
1062
? "covered-line"
1063
: (Line.isMapped() ? "uncovered-line" : "skipped-line");
1064
OS << tag("td", Count, CoverageClass);
1065
}
1066
1067
void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
1068
unsigned LineNo) {
1069
std::string LineNoStr = utostr(uint64_t(LineNo));
1070
std::string TargetName = "L" + LineNoStr;
1071
OS << tag("td", a("#" + TargetName, tag("pre", LineNoStr), TargetName),
1072
"line-number");
1073
}
1074
1075
void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
1076
const LineCoverageStats &Line,
1077
unsigned) {
1078
// Region markers are rendered in-line using tooltips.
1079
}
1080
1081
void SourceCoverageViewHTML::renderExpansionSite(raw_ostream &OS, LineRef L,
1082
const LineCoverageStats &LCS,
1083
unsigned ExpansionCol,
1084
unsigned ViewDepth) {
1085
// Render the line containing the expansion site. No extra formatting needed.
1086
renderLine(OS, L, LCS, ExpansionCol, ViewDepth);
1087
}
1088
1089
void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
1090
ExpansionView &ESV,
1091
unsigned ViewDepth) {
1092
OS << BeginExpansionDiv;
1093
ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
1094
/*ShowTitle=*/false, ViewDepth + 1);
1095
OS << EndExpansionDiv;
1096
}
1097
1098
void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
1099
unsigned ViewDepth) {
1100
// Render the child subview.
1101
if (getOptions().Debug)
1102
errs() << "Branch at line " << BRV.getLine() << '\n';
1103
1104
OS << BeginExpansionDiv;
1105
OS << BeginPre;
1106
for (const auto &R : BRV.Regions) {
1107
// Calculate TruePercent and False Percent.
1108
double TruePercent = 0.0;
1109
double FalsePercent = 0.0;
1110
// FIXME: It may overflow when the data is too large, but I have not
1111
// encountered it in actual use, and not sure whether to use __uint128_t.
1112
uint64_t Total = R.ExecutionCount + R.FalseExecutionCount;
1113
1114
if (!getOptions().ShowBranchCounts && Total != 0) {
1115
TruePercent = ((double)(R.ExecutionCount) / (double)Total) * 100.0;
1116
FalsePercent = ((double)(R.FalseExecutionCount) / (double)Total) * 100.0;
1117
}
1118
1119
// Display Line + Column.
1120
std::string LineNoStr = utostr(uint64_t(R.LineStart));
1121
std::string ColNoStr = utostr(uint64_t(R.ColumnStart));
1122
std::string TargetName = "L" + LineNoStr;
1123
1124
OS << " Branch (";
1125
OS << tag("span",
1126
a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
1127
TargetName),
1128
"line-number") +
1129
"): [";
1130
1131
if (R.Folded) {
1132
OS << "Folded - Ignored]\n";
1133
continue;
1134
}
1135
1136
// Display TrueCount or TruePercent.
1137
std::string TrueColor = R.ExecutionCount ? "None" : "red branch";
1138
std::string TrueCovClass =
1139
(R.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
1140
1141
OS << tag("span", "True", TrueColor);
1142
OS << ": ";
1143
if (getOptions().ShowBranchCounts)
1144
OS << tag("span", formatCount(R.ExecutionCount), TrueCovClass) << ", ";
1145
else
1146
OS << format("%0.2f", TruePercent) << "%, ";
1147
1148
// Display FalseCount or FalsePercent.
1149
std::string FalseColor = R.FalseExecutionCount ? "None" : "red branch";
1150
std::string FalseCovClass =
1151
(R.FalseExecutionCount > 0) ? "covered-line" : "uncovered-line";
1152
1153
OS << tag("span", "False", FalseColor);
1154
OS << ": ";
1155
if (getOptions().ShowBranchCounts)
1156
OS << tag("span", formatCount(R.FalseExecutionCount), FalseCovClass);
1157
else
1158
OS << format("%0.2f", FalsePercent) << "%";
1159
1160
OS << "]\n";
1161
}
1162
OS << EndPre;
1163
OS << EndExpansionDiv;
1164
}
1165
1166
void SourceCoverageViewHTML::renderMCDCView(raw_ostream &OS, MCDCView &MRV,
1167
unsigned ViewDepth) {
1168
for (auto &Record : MRV.Records) {
1169
OS << BeginExpansionDiv;
1170
OS << BeginPre;
1171
OS << " MC/DC Decision Region (";
1172
1173
// Display Line + Column information.
1174
const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion();
1175
std::string LineNoStr = Twine(DecisionRegion.LineStart).str();
1176
std::string ColNoStr = Twine(DecisionRegion.ColumnStart).str();
1177
std::string TargetName = "L" + LineNoStr;
1178
OS << tag("span",
1179
a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),
1180
"line-number") +
1181
") to (";
1182
LineNoStr = utostr(uint64_t(DecisionRegion.LineEnd));
1183
ColNoStr = utostr(uint64_t(DecisionRegion.ColumnEnd));
1184
OS << tag("span",
1185
a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),
1186
"line-number") +
1187
")\n\n";
1188
1189
// Display MC/DC Information.
1190
OS << " Number of Conditions: " << Record.getNumConditions() << "\n";
1191
for (unsigned i = 0; i < Record.getNumConditions(); i++) {
1192
OS << " " << Record.getConditionHeaderString(i);
1193
}
1194
OS << "\n";
1195
OS << " Executed MC/DC Test Vectors:\n\n ";
1196
OS << Record.getTestVectorHeaderString();
1197
for (unsigned i = 0; i < Record.getNumTestVectors(); i++)
1198
OS << Record.getTestVectorString(i);
1199
OS << "\n";
1200
for (unsigned i = 0; i < Record.getNumConditions(); i++)
1201
OS << Record.getConditionCoverageString(i);
1202
OS << " MC/DC Coverage for Expression: ";
1203
OS << format("%0.2f", Record.getPercentCovered()) << "%\n";
1204
OS << EndPre;
1205
OS << EndExpansionDiv;
1206
}
1207
return;
1208
}
1209
1210
void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
1211
InstantiationView &ISV,
1212
unsigned ViewDepth) {
1213
OS << BeginExpansionDiv;
1214
if (!ISV.View)
1215
OS << BeginSourceNameDiv
1216
<< tag("pre",
1217
escape("Unexecuted instantiation: " + ISV.FunctionName.str(),
1218
getOptions()))
1219
<< EndSourceNameDiv;
1220
else
1221
ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true,
1222
/*ShowTitle=*/false, ViewDepth);
1223
OS << EndExpansionDiv;
1224
}
1225
1226
void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
1227
if (getOptions().hasProjectTitle())
1228
OS << tag(ProjectTitleTag, escape(getOptions().ProjectTitle, getOptions()));
1229
OS << tag(ReportTitleTag, escape(Title, getOptions()));
1230
if (getOptions().hasCreatedTime())
1231
OS << tag(CreatedTimeTag,
1232
escape(getOptions().CreatedTimeStr, getOptions()));
1233
1234
OS << tag("span",
1235
a("javascript:next_line()", "next uncovered line (L)") + ", " +
1236
a("javascript:next_region()", "next uncovered region (R)") +
1237
", " +
1238
a("javascript:next_branch()", "next uncovered branch (B)"),
1239
"control");
1240
}
1241
1242
void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1243
unsigned ViewDepth) {
1244
std::string Links;
1245
1246
renderLinePrefix(OS, ViewDepth);
1247
OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"));
1248
OS << tag("td", tag("pre", "Source" + Links));
1249
renderLineSuffix(OS, ViewDepth);
1250
}
1251
1252