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
22using namespace llvm;
23
24namespace {
25
26// Return a string with the special characters in \p Str escaped.
27std::string escape(StringRef Str, const CoverageViewOptions &Opts) {
28 std::string TabExpandedResult;
29 unsigned ColNum = 0; // Record the column number.
30 for (char C : Str) {
31 if (C == '\t') {
32 // Replace '\t' with up to TabSize spaces.
33 unsigned NumSpaces = Opts.TabSize - (ColNum % Opts.TabSize);
34 TabExpandedResult.append(n: NumSpaces, c: ' ');
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(String: TabExpandedResult, Out&: OS);
48 }
49 return EscapedHTML;
50}
51
52// Create a \p Name tag around \p Str, and optionally set its \p ClassName.
53std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") {
54 std::string Tag = "<";
55 Tag += Name;
56 if (!ClassName.empty()) {
57 Tag += " class='";
58 Tag += ClassName;
59 Tag += "'";
60 }
61 Tag += ">";
62 Tag += Str;
63 Tag += "</";
64 Tag += Name;
65 Tag += ">";
66 return Tag;
67}
68
69// Create an anchor to \p Link with the label \p Str.
70std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") {
71 std::string Tag;
72 Tag += "<a ";
73 if (!TargetName.empty()) {
74 Tag += "name='";
75 Tag += TargetName;
76 Tag += "' ";
77 }
78 Tag += "href='";
79 Tag += Link;
80 Tag += "'>";
81 Tag += Str;
82 Tag += "</a>";
83 return Tag;
84}
85
86const char *BeginHeader =
87 "<head>"
88 "<meta name='viewport' content='width=device-width,initial-scale=1'>"
89 "<meta charset='UTF-8'>";
90
91const char *CSSForCoverage =
92 R"(.red {
93 background-color: #ffd0d0;
94}
95.cyan {
96 background-color: cyan;
97}
98body {
99 font-family: -apple-system, sans-serif;
100}
101pre {
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}
127table {
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}
226th, 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}
234td pre {
235 display: inline-block;
236}
237td:first-child {
238 border-left: none;
239}
240td:last-child {
241 border-right: none;
242}
243tr:hover {
244 background-color: #f0f0f0;
245}
246tr:last-child {
247 border-bottom: none;
248}
249tr:has(> td >a:target) > td.code > pre {
250 background-color: #ffa;
251}
252)";
253
254const char *EndHeader = "</head>";
255
256const char *BeginCenteredDiv = "<div class='centered'>";
257
258const char *EndCenteredDiv = "</div>";
259
260const char *BeginSourceNameDiv = "<div class='source-name-title'>";
261
262const char *EndSourceNameDiv = "</div>";
263
264const char *BeginCodeTD = "<td class='code'>";
265
266const char *EndCodeTD = "</td>";
267
268const char *BeginPre = "<pre>";
269
270const char *EndPre = "</pre>";
271
272const char *BeginExpansionDiv = "<div class='expansion-view'>";
273
274const char *EndExpansionDiv = "</div>";
275
276const char *BeginTable = "<table>";
277
278const char *EndTable = "</table>";
279
280const char *ProjectTitleTag = "h1";
281
282const char *ReportTitleTag = "h2";
283
284const char *CreatedTimeTag = "h4";
285
286std::string getPathToStyle(StringRef ViewPath) {
287 std::string PathToStyle;
288 std::string PathSep = std::string(sys::path::get_separator());
289 unsigned NumSeps = ViewPath.count(Str: PathSep);
290 for (unsigned I = 0, E = NumSeps; I < E; ++I)
291 PathToStyle += ".." + PathSep;
292 return PathToStyle + "style.css";
293}
294
295void 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(Str: PathToStyle, Opts) << "'>";
307
308 OS << EndHeader << "<body>";
309}
310
311void 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(Fmt: "%*.2f", Vals: 7, Vals: 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(Args: tag(Name: "td", Str: tag(Name: "pre", Str: S), ClassName: CellClass));
336 };
337
338 Columns.emplace_back(Args: tag(Name: "td", Str: tag(Name: "pre", Str: 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(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""), ClassName: "light-row-bold");
364 else
365 OS << tag(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""), ClassName: "light-row");
366}
367
368void emitEpilog(raw_ostream &OS) {
369 OS << "</body>"
370 << "</html>";
371}
372
373} // anonymous namespace
374
375Expected<CoveragePrinter::OwnedStream>
376CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
377 auto OSOrErr = createOutputStream(Path, Extension: "html", InToplevel);
378 if (!OSOrErr)
379 return OSOrErr;
380
381 OwnedStream OS = std::move(OSOrErr.get());
382
383 if (!Opts.hasOutputDirectory()) {
384 emitPrelude(OS&: *OS.get(), Opts);
385 } else {
386 std::string ViewPath = getOutputPath(Path, Extension: "html", InToplevel);
387 emitPrelude(OS&: *OS.get(), Opts, PathToStyle: getPathToStyle(ViewPath));
388 }
389
390 return std::move(OS);
391}
392
393void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
394 emitEpilog(OS&: *OS.get());
395}
396
397/// Emit column labels for the table in the index.
398static void emitColumnLabelsForIndex(raw_ostream &OS,
399 const CoverageViewOptions &Opts) {
400 SmallVector<std::string, 4> Columns;
401 Columns.emplace_back(Args: tag(Name: "td", Str: "Filename", ClassName: "column-entry-bold"));
402 Columns.emplace_back(Args: tag(Name: "td", Str: "Function Coverage", ClassName: "column-entry-bold"));
403 if (Opts.ShowInstantiationSummary)
404 Columns.emplace_back(
405 Args: tag(Name: "td", Str: "Instantiation Coverage", ClassName: "column-entry-bold"));
406 Columns.emplace_back(Args: tag(Name: "td", Str: "Line Coverage", ClassName: "column-entry-bold"));
407 if (Opts.ShowRegionSummary)
408 Columns.emplace_back(Args: tag(Name: "td", Str: "Region Coverage", ClassName: "column-entry-bold"));
409 if (Opts.ShowBranchSummary)
410 Columns.emplace_back(Args: tag(Name: "td", Str: "Branch Coverage", ClassName: "column-entry-bold"));
411 if (Opts.ShowMCDCSummary)
412 Columns.emplace_back(Args: tag(Name: "td", Str: "MC/DC", ClassName: "column-entry-bold"));
413 OS << tag(Name: "tr", Str: join(Begin: Columns.begin(), End: Columns.end(), Separator: ""));
414}
415
416std::string
417CoveragePrinterHTML::buildLinkToFile(StringRef SF,
418 const FileCoverageSummary &FCS) const {
419 SmallString<128> LinkTextStr(sys::path::relative_path(path: FCS.Name));
420 sys::path::remove_dots(path&: LinkTextStr, /*remove_dot_dot=*/true);
421 sys::path::native(path&: LinkTextStr);
422 std::string LinkText = escape(Str: LinkTextStr, Opts);
423 std::string LinkTarget =
424 escape(Str: getOutputPath(Path: SF, Extension: "html", /*InToplevel=*/false), Opts);
425 return a(Link: LinkTarget, Str: LinkText);
426}
427
428Error CoveragePrinterHTML::emitStyleSheet() {
429 auto CSSOrErr = createOutputStream(Path: "style", Extension: "css", /*InToplevel=*/true);
430 if (Error E = CSSOrErr.takeError())
431 return E;
432
433 OwnedStream CSS = std::move(CSSOrErr.get());
434 CSS->operator<<(Str: CSSForCoverage);
435
436 return Error::success();
437}
438
439void 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(Name: ProjectTitleTag, Str: escape(Str: Opts.ProjectTitle, Opts));
444 OSRef << tag(Name: ReportTitleTag, Str: Title);
445 if (Opts.hasCreatedTime())
446 OSRef << tag(Name: CreatedTimeTag, Str: escape(Str: Opts.CreatedTimeStr, Opts));
447
448 // Emit a link to some documentation.
449 OSRef << tag(Name: "p", Str: "Click " +
450 a(Link: "http://clang.llvm.org/docs/"
451 "SourceBasedCodeCoverage.html#interpreting-reports",
452 Str: "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(OS&: 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.
463void 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, FirstCol: Filename, FCS, IsTotals);
475}
476
477Error 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(Path: "index", Extension: "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(OS&: OSRef, Opts, PathToStyle: getPathToStyle(ViewPath: ""));
493
494 emitReportHeader(OSRef, Title: "Coverage Report");
495
496 FileCoverageSummary Totals("TOTALS");
497 auto FileReports = CoverageReport::prepareFileReports(
498 Coverage, Totals, Files: SourceFiles, Options: 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(OS&: OSRef, SF: SourceFiles[I], FCS: FileReports[I]);
503 else
504 EmptyFiles = true;
505 }
506 emitFileSummary(OS&: OSRef, SF: "Totals", FCS: 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(Name: "p", Str: "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(SF: SourceFiles[I], FCS: FileReports[I]);
520 OSRef << tag(Name: "tr", Str: tag(Name: "td", Str: tag(Name: "pre", Str: Link)), ClassName: "light-row") << '\n';
521 }
522 OSRef << EndTable << EndCenteredDiv;
523 }
524
525 OSRef << tag(Name: "h5", Str: escape(Str: Opts.getLLVMVersionString(), Opts));
526 emitEpilog(OS&: OSRef);
527
528 return Error::success();
529}
530
531struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
532 CoveragePrinterHTMLDirectory &Printer;
533
534 Reporter(CoveragePrinterHTMLDirectory &Printer,
535 const coverage::CoverageMapping &Coverage,
536 const CoverageFiltersMatchAll &Filters)
537 : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
538 Printer(Printer) {}
539
540 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(path&: OSPath, a: "index");
549 auto OSOrErr = Printer.createOutputStream(Path: OSPath, Extension: "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(Path: (LCPath + "index").str(), Extension: "html",
557 /*InToplevel=*/false);
558 emitPrelude(OS&: OSRef, Opts: Options, PathToStyle: getPathToStyle(ViewPath: IndexHtmlPath));
559
560 auto NavLink = buildTitleLinks(LCPath);
561 Printer.emitReportHeader(OSRef, Title: "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(x: &Report);
570 else
571 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: buildRelLinkToFile(RelPath: Report.Name), FCS: 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(x: &Report);
579 else
580 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: buildRelLinkToFile(RelPath: Report.Name), FCS: Report,
581 /*IsTotals=*/false);
582 }
583
584 // Emit the totals row.
585 emitTableRow(OS&: OSRef, Opts: Options, FirstCol: "Totals", FCS: 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(Name: "p", Str: "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(RelPath: FCS->Name);
598 OSRef << tag(Name: "tr", Str: tag(Name: "td", Str: tag(Name: "pre", Str: Link)), ClassName: "light-row") << '\n';
599 }
600 OSRef << EndTable << EndCenteredDiv;
601 }
602
603 // Emit epilog.
604 OSRef << tag(Name: "h5", Str: escape(Str: Options.getLLVMVersionString(), Opts: Options));
605 emitEpilog(OS&: 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.
612 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(Start: 0, N: *Iter);
628 sys::path::native(path&: RootPath);
629 sys::path::remove_dots(path&: RootPath, /*remove_dot_dot=*/true);
630 }
631 Components.emplace_back(Args: std::move(RootPath), Args: 0);
632
633 for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
634 SmallString<128> SubPath = LCPath.substr(Start: Last, N: *Iter - Last);
635 sys::path::native(path&: SubPath);
636 sys::path::remove_dots(path&: SubPath, /*remove_dot_dot=*/true);
637 auto Level = unsigned(SubPath.count(Str: sys::path::get_separator())) + 1;
638 Components.back().second += Level;
639 Components.emplace_back(Args: std::move(SubPath), Args&: 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(Link: "./index.html", Str: 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, Str: Name);
657 S += sys::path::get_separator();
658 }
659 return S;
660 }
661
662 std::string buildRelLinkToFile(StringRef RelPath) const {
663 SmallString<128> LinkTextStr(RelPath);
664 sys::path::native(path&: LinkTextStr);
665
666 // remove_dots will remove trailing slash, so we need to check before it.
667 auto IsDir = LinkTextStr.ends_with(Suffix: sys::path::get_separator());
668 sys::path::remove_dots(path&: LinkTextStr, /*remove_dot_dot=*/true);
669
670 SmallString<128> LinkTargetStr(LinkTextStr);
671 if (IsDir) {
672 LinkTextStr += sys::path::get_separator();
673 sys::path::append(path&: LinkTargetStr, a: "index.html");
674 } else {
675 LinkTargetStr += ".html";
676 }
677
678 auto LinkText = escape(Str: LinkTextStr, Opts: Options);
679 auto LinkTarget = escape(Str: LinkTargetStr, Opts: Options);
680 return a(Link: LinkTarget, Str: LinkText);
681 }
682};
683
684Error 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(Path: "index", Extension: "html", /*InToplevel=*/true);
706 if (auto E = OSOrErr.takeError())
707 return E;
708 auto OS = std::move(OSOrErr.get());
709 auto LCPIndexFilePath =
710 getOutputPath(Path: (LCPath + "index").str(), Extension: "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
724void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
725 OS << BeginCenteredDiv << BeginTable;
726}
727
728void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
729 OS << EndTable << EndCenteredDiv;
730}
731
732void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
733 OS << BeginSourceNameDiv << tag(Name: "pre", Str: escape(Str: getSourceName(), Opts: getOptions()))
734 << EndSourceNameDiv;
735}
736
737void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
738 OS << "<tr>";
739}
740
741void 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
749void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
750 // The table-based output makes view dividers unnecessary.
751}
752
753void 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(Elt: std::string(Line.substr(Start, N: 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(Str: Snippets[I], Opts: 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(Args&: LC, Args&: RC);
797 return tag(Name: "span", Str: Snippet, ClassName: 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(Name: "div", Str: Snippets[I + 1] + tag(Name: "span", Str: formatCount(N: CurSeg->Count),
853 ClassName: "tooltip-content"),
854 ClassName: "tooltip");
855
856 if (getOptions().Debug)
857 errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
858 << formatCount(N: 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
874void SourceCoverageViewHTML::renderLineCoverageColumn(
875 raw_ostream &OS, const LineCoverageStats &Line) {
876 std::string Count;
877 if (Line.isMapped())
878 Count = tag(Name: "pre", Str: formatCount(N: Line.getExecutionCount()));
879 std::string CoverageClass =
880 (Line.getExecutionCount() > 0) ? "covered-line" : "uncovered-line";
881 OS << tag(Name: "td", Str: Count, ClassName: CoverageClass);
882}
883
884void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
885 unsigned LineNo) {
886 std::string LineNoStr = utostr(X: uint64_t(LineNo));
887 std::string TargetName = "L" + LineNoStr;
888 OS << tag(Name: "td", Str: a(Link: "#" + TargetName, Str: tag(Name: "pre", Str: LineNoStr), TargetName),
889 ClassName: "line-number");
890}
891
892void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
893 const LineCoverageStats &Line,
894 unsigned) {
895 // Region markers are rendered in-line using tooltips.
896}
897
898void 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
906void 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: ViewDepth + 1);
912 OS << EndExpansionDiv;
913}
914
915void 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(X: uint64_t(R.LineStart));
938 std::string ColNoStr = utostr(X: uint64_t(R.ColumnStart));
939 std::string TargetName = "L" + LineNoStr;
940
941 OS << " Branch (";
942 OS << tag(Name: "span",
943 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr),
944 TargetName),
945 ClassName: "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(Name: "span", Str: "True", ClassName: TrueColor);
959 OS << ": ";
960 if (getOptions().ShowBranchCounts)
961 OS << tag(Name: "span", Str: formatCount(N: R.ExecutionCount), ClassName: TrueCovClass) << ", ";
962 else
963 OS << format(Fmt: "%0.2f", Vals: 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(Name: "span", Str: "False", ClassName: FalseColor);
971 OS << ": ";
972 if (getOptions().ShowBranchCounts)
973 OS << tag(Name: "span", Str: formatCount(N: R.FalseExecutionCount), ClassName: FalseCovClass);
974 else
975 OS << format(Fmt: "%0.2f", Vals: FalsePercent) << "%";
976
977 OS << "]\n";
978 }
979 OS << EndPre;
980 OS << EndExpansionDiv;
981}
982
983void 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(Name: "span",
996 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr)),
997 ClassName: "line-number") +
998 ") to (";
999 LineNoStr = utostr(X: uint64_t(DecisionRegion.LineEnd));
1000 ColNoStr = utostr(X: uint64_t(DecisionRegion.ColumnEnd));
1001 OS << tag(Name: "span",
1002 Str: a(Link: "#" + TargetName, Str: tag(Name: "span", Str: LineNoStr + ":" + ColNoStr)),
1003 ClassName: "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(Condition: 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(TestVectorIndex: i);
1016 OS << "\n";
1017 for (unsigned i = 0; i < Record.getNumConditions(); i++)
1018 OS << Record.getConditionCoverageString(Condition: i);
1019 OS << " MC/DC Coverage for Expression: ";
1020 OS << format(Fmt: "%0.2f", Vals: Record.getPercentCovered()) << "%\n";
1021 OS << EndPre;
1022 OS << EndExpansionDiv;
1023 }
1024 return;
1025}
1026
1027void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
1028 InstantiationView &ISV,
1029 unsigned ViewDepth) {
1030 OS << BeginExpansionDiv;
1031 if (!ISV.View)
1032 OS << BeginSourceNameDiv
1033 << tag(Name: "pre",
1034 Str: escape(Str: "Unexecuted instantiation: " + ISV.FunctionName.str(),
1035 Opts: getOptions()))
1036 << EndSourceNameDiv;
1037 else
1038 ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true,
1039 /*ShowTitle=*/false, ViewDepth);
1040 OS << EndExpansionDiv;
1041}
1042
1043void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
1044 if (getOptions().hasProjectTitle())
1045 OS << tag(Name: ProjectTitleTag, Str: escape(Str: getOptions().ProjectTitle, Opts: getOptions()));
1046 OS << tag(Name: ReportTitleTag, Str: escape(Str: Title, Opts: getOptions()));
1047 if (getOptions().hasCreatedTime())
1048 OS << tag(Name: CreatedTimeTag,
1049 Str: escape(Str: getOptions().CreatedTimeStr, Opts: getOptions()));
1050}
1051
1052void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1053 unsigned FirstUncoveredLineNo,
1054 unsigned ViewDepth) {
1055 std::string SourceLabel;
1056 if (FirstUncoveredLineNo == 0) {
1057 SourceLabel = tag(Name: "td", Str: tag(Name: "pre", Str: "Source"));
1058 } else {
1059 std::string LinkTarget = "#L" + utostr(X: uint64_t(FirstUncoveredLineNo));
1060 SourceLabel =
1061 tag(Name: "td", Str: tag(Name: "pre", Str: "Source (" +
1062 a(Link: LinkTarget, Str: "jump to first uncovered line") +
1063 ")"));
1064 }
1065
1066 renderLinePrefix(OS, ViewDepth);
1067 OS << tag(Name: "td", Str: tag(Name: "pre", Str: "Line")) << tag(Name: "td", Str: tag(Name: "pre", Str: "Count"))
1068 << SourceLabel;
1069 renderLineSuffix(OS, ViewDepth);
1070}
1071

source code of llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp