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