1//===- CoverageReport.cpp - Code coverage report -------------------------===//
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// This class implements rendering of a code coverage report.
10//
11//===----------------------------------------------------------------------===//
12
13#include "CoverageReport.h"
14#include "RenderingSupport.h"
15#include "llvm/ADT/DenseMap.h"
16#include "llvm/Support/Format.h"
17#include "llvm/Support/Path.h"
18#include "llvm/Support/ThreadPool.h"
19#include "llvm/Support/Threading.h"
20#include <numeric>
21
22using namespace llvm;
23
24namespace {
25
26/// Helper struct which prints trimmed and aligned columns.
27struct Column {
28  enum TrimKind { NoTrim, WidthTrim, RightTrim };
29
30  enum AlignmentKind { LeftAlignment, RightAlignment };
31
32  StringRef Str;
33  unsigned Width;
34  TrimKind Trim;
35  AlignmentKind Alignment;
36
37  Column(StringRef Str, unsigned Width)
38      : Str(Str), Width(Width), Trim(WidthTrim), Alignment(LeftAlignment) {}
39
40  Column &set(TrimKind Value) {
41    Trim = Value;
42    return *this;
43  }
44
45  Column &set(AlignmentKind Value) {
46    Alignment = Value;
47    return *this;
48  }
49
50  void render(raw_ostream &OS) const {
51    if (Str.size() <= Width) {
52      if (Alignment == RightAlignment) {
53        OS.indent(Width - Str.size());
54        OS << Str;
55        return;
56      }
57      OS << Str;
58      OS.indent(Width - Str.size());
59      return;
60    }
61
62    switch (Trim) {
63    case NoTrim:
64      OS << Str;
65      break;
66    case WidthTrim:
67      OS << Str.substr(0, Width);
68      break;
69    case RightTrim:
70      OS << Str.substr(0, Width - 3) << "...";
71      break;
72    }
73  }
74};
75
76raw_ostream &operator<<(raw_ostream &OS, const Column &Value) {
77  Value.render(OS);
78  return OS;
79}
80
81Column column(StringRef Str, unsigned Width) { return Column(Str, Width); }
82
83template <typename T>
84Column column(StringRef Str, unsigned Width, const T &Value) {
85  return Column(Str, Width).set(Value);
86}
87
88// Specify the default column widths.
89size_t FileReportColumns[] = {25, 12, 18, 10, 12, 18, 10,
90                              16, 16, 10, 12, 18, 10};
91size_t FunctionReportColumns[] = {25, 10, 8, 8, 10, 8, 8};
92
93/// Adjust column widths to fit long file paths and function names.
94void adjustColumnWidths(ArrayRef<StringRef> Files,
95                        ArrayRef<StringRef> Functions) {
96  for (StringRef Filename : Files)
97    FileReportColumns[0] = std::max(FileReportColumns[0], Filename.size());
98  for (StringRef Funcname : Functions)
99    FunctionReportColumns[0] =
100        std::max(FunctionReportColumns[0], Funcname.size());
101}
102
103/// Prints a horizontal divider long enough to cover the given column
104/// widths.
105void renderDivider(ArrayRef<size_t> ColumnWidths, raw_ostream &OS) {
106  size_t Length = std::accumulate(ColumnWidths.begin(), ColumnWidths.end(), 0);
107  for (size_t I = 0; I < Length; ++I)
108    OS << '-';
109}
110
111/// Return the color which correponds to the coverage percentage of a
112/// certain metric.
113template <typename T>
114raw_ostream::Colors determineCoveragePercentageColor(const T &Info) {
115  if (Info.isFullyCovered())
116    return raw_ostream::GREEN;
117  return Info.getPercentCovered() >= 80.0 ? raw_ostream::YELLOW
118                                          : raw_ostream::RED;
119}
120
121/// Get the number of redundant path components in each path in \p Paths.
122unsigned getNumRedundantPathComponents(ArrayRef<std::string> Paths) {
123  // To start, set the number of redundant path components to the maximum
124  // possible value.
125  SmallVector<StringRef, 8> FirstPathComponents{sys::path::begin(Paths[0]),
126                                                sys::path::end(Paths[0])};
127  unsigned NumRedundant = FirstPathComponents.size();
128
129  for (unsigned I = 1, E = Paths.size(); NumRedundant > 0 && I < E; ++I) {
130    StringRef Path = Paths[I];
131    for (const auto &Component :
132         enumerate(make_range(sys::path::begin(Path), sys::path::end(Path)))) {
133      // Do not increase the number of redundant components: that would remove
134      // useful parts of already-visited paths.
135      if (Component.index() >= NumRedundant)
136        break;
137
138      // Lower the number of redundant components when there's a mismatch
139      // between the first path, and the path under consideration.
140      if (FirstPathComponents[Component.index()] != Component.value()) {
141        NumRedundant = Component.index();
142        break;
143      }
144    }
145  }
146
147  return NumRedundant;
148}
149
150/// Determine the length of the longest redundant prefix of the paths in
151/// \p Paths.
152unsigned getRedundantPrefixLen(ArrayRef<std::string> Paths) {
153  // If there's at most one path, no path components are redundant.
154  if (Paths.size() <= 1)
155    return 0;
156
157  unsigned PrefixLen = 0;
158  unsigned NumRedundant = getNumRedundantPathComponents(Paths);
159  auto Component = sys::path::begin(Paths[0]);
160  for (unsigned I = 0; I < NumRedundant; ++I) {
161    auto LastComponent = Component;
162    ++Component;
163    PrefixLen += Component - LastComponent;
164  }
165  return PrefixLen;
166}
167
168} // end anonymous namespace
169
170namespace llvm {
171
172void CoverageReport::render(const FileCoverageSummary &File,
173                            raw_ostream &OS) const {
174  auto FileCoverageColor =
175      determineCoveragePercentageColor(File.RegionCoverage);
176  auto FuncCoverageColor =
177      determineCoveragePercentageColor(File.FunctionCoverage);
178  auto InstantiationCoverageColor =
179      determineCoveragePercentageColor(File.InstantiationCoverage);
180  auto LineCoverageColor = determineCoveragePercentageColor(File.LineCoverage);
181  SmallString<256> FileName = File.Name;
182  sys::path::remove_dots(FileName, /*remove_dot_dots=*/true);
183  sys::path::native(FileName);
184  OS << column(FileName, FileReportColumns[0], Column::NoTrim);
185
186  if (Options.ShowRegionSummary) {
187    OS << format("%*u", FileReportColumns[1],
188                 (unsigned)File.RegionCoverage.getNumRegions());
189    Options.colored_ostream(OS, FileCoverageColor)
190        << format("%*u", FileReportColumns[2],
191                  (unsigned)(File.RegionCoverage.getNumRegions() -
192                             File.RegionCoverage.getCovered()));
193    if (File.RegionCoverage.getNumRegions())
194      Options.colored_ostream(OS, FileCoverageColor)
195          << format("%*.2f", FileReportColumns[3] - 1,
196                    File.RegionCoverage.getPercentCovered())
197          << '%';
198    else
199      OS << column("-", FileReportColumns[3], Column::RightAlignment);
200  }
201
202  OS << format("%*u", FileReportColumns[4],
203               (unsigned)File.FunctionCoverage.getNumFunctions());
204  OS << format("%*u", FileReportColumns[5],
205               (unsigned)(File.FunctionCoverage.getNumFunctions() -
206                          File.FunctionCoverage.getExecuted()));
207  if (File.FunctionCoverage.getNumFunctions())
208    Options.colored_ostream(OS, FuncCoverageColor)
209        << format("%*.2f", FileReportColumns[6] - 1,
210                  File.FunctionCoverage.getPercentCovered())
211        << '%';
212  else
213    OS << column("-", FileReportColumns[6], Column::RightAlignment);
214
215  if (Options.ShowInstantiationSummary) {
216    OS << format("%*u", FileReportColumns[7],
217                 (unsigned)File.InstantiationCoverage.getNumFunctions());
218    OS << format("%*u", FileReportColumns[8],
219                 (unsigned)(File.InstantiationCoverage.getNumFunctions() -
220                            File.InstantiationCoverage.getExecuted()));
221    if (File.InstantiationCoverage.getNumFunctions())
222      Options.colored_ostream(OS, InstantiationCoverageColor)
223          << format("%*.2f", FileReportColumns[9] - 1,
224                    File.InstantiationCoverage.getPercentCovered())
225          << '%';
226    else
227      OS << column("-", FileReportColumns[9], Column::RightAlignment);
228  }
229
230  OS << format("%*u", FileReportColumns[10],
231               (unsigned)File.LineCoverage.getNumLines());
232  Options.colored_ostream(OS, LineCoverageColor) << format(
233      "%*u", FileReportColumns[11], (unsigned)(File.LineCoverage.getNumLines() -
234                                               File.LineCoverage.getCovered()));
235  if (File.LineCoverage.getNumLines())
236    Options.colored_ostream(OS, LineCoverageColor)
237        << format("%*.2f", FileReportColumns[12] - 1,
238                  File.LineCoverage.getPercentCovered())
239        << '%';
240  else
241    OS << column("-", FileReportColumns[12], Column::RightAlignment);
242  OS << "\n";
243}
244
245void CoverageReport::render(const FunctionCoverageSummary &Function,
246                            const DemangleCache &DC,
247                            raw_ostream &OS) const {
248  auto FuncCoverageColor =
249      determineCoveragePercentageColor(Function.RegionCoverage);
250  auto LineCoverageColor =
251      determineCoveragePercentageColor(Function.LineCoverage);
252  OS << column(DC.demangle(Function.Name), FunctionReportColumns[0],
253               Column::RightTrim)
254     << format("%*u", FunctionReportColumns[1],
255               (unsigned)Function.RegionCoverage.getNumRegions());
256  Options.colored_ostream(OS, FuncCoverageColor)
257      << format("%*u", FunctionReportColumns[2],
258                (unsigned)(Function.RegionCoverage.getNumRegions() -
259                           Function.RegionCoverage.getCovered()));
260  Options.colored_ostream(
261      OS, determineCoveragePercentageColor(Function.RegionCoverage))
262      << format("%*.2f", FunctionReportColumns[3] - 1,
263                Function.RegionCoverage.getPercentCovered())
264      << '%';
265  OS << format("%*u", FunctionReportColumns[4],
266               (unsigned)Function.LineCoverage.getNumLines());
267  Options.colored_ostream(OS, LineCoverageColor)
268      << format("%*u", FunctionReportColumns[5],
269                (unsigned)(Function.LineCoverage.getNumLines() -
270                           Function.LineCoverage.getCovered()));
271  Options.colored_ostream(
272      OS, determineCoveragePercentageColor(Function.LineCoverage))
273      << format("%*.2f", FunctionReportColumns[6] - 1,
274                Function.LineCoverage.getPercentCovered())
275      << '%';
276  OS << "\n";
277}
278
279void CoverageReport::renderFunctionReports(ArrayRef<std::string> Files,
280                                           const DemangleCache &DC,
281                                           raw_ostream &OS) {
282  bool isFirst = true;
283  for (StringRef Filename : Files) {
284    auto Functions = Coverage.getCoveredFunctions(Filename);
285
286    if (isFirst)
287      isFirst = false;
288    else
289      OS << "\n";
290
291    std::vector<StringRef> Funcnames;
292    for (const auto &F : Functions)
293      Funcnames.emplace_back(DC.demangle(F.Name));
294    adjustColumnWidths({}, Funcnames);
295
296    OS << "File '" << Filename << "':\n";
297    OS << column("Name", FunctionReportColumns[0])
298       << column("Regions", FunctionReportColumns[1], Column::RightAlignment)
299       << column("Miss", FunctionReportColumns[2], Column::RightAlignment)
300       << column("Cover", FunctionReportColumns[3], Column::RightAlignment)
301       << column("Lines", FunctionReportColumns[4], Column::RightAlignment)
302       << column("Miss", FunctionReportColumns[5], Column::RightAlignment)
303       << column("Cover", FunctionReportColumns[6], Column::RightAlignment);
304    OS << "\n";
305    renderDivider(FunctionReportColumns, OS);
306    OS << "\n";
307    FunctionCoverageSummary Totals("TOTAL");
308    for (const auto &F : Functions) {
309      auto Function = FunctionCoverageSummary::get(Coverage, F);
310      ++Totals.ExecutionCount;
311      Totals.RegionCoverage += Function.RegionCoverage;
312      Totals.LineCoverage += Function.LineCoverage;
313      render(Function, DC, OS);
314    }
315    if (Totals.ExecutionCount) {
316      renderDivider(FunctionReportColumns, OS);
317      OS << "\n";
318      render(Totals, DC, OS);
319    }
320  }
321}
322
323void CoverageReport::prepareSingleFileReport(const StringRef Filename,
324    const coverage::CoverageMapping *Coverage,
325    const CoverageViewOptions &Options, const unsigned LCP,
326    FileCoverageSummary *FileReport, const CoverageFilter *Filters) {
327  for (const auto &Group : Coverage->getInstantiationGroups(Filename)) {
328    std::vector<FunctionCoverageSummary> InstantiationSummaries;
329    for (const coverage::FunctionRecord *F : Group.getInstantiations()) {
330      if (!Filters->matches(*Coverage, *F))
331        continue;
332      auto InstantiationSummary = FunctionCoverageSummary::get(*Coverage, *F);
333      FileReport->addInstantiation(InstantiationSummary);
334      InstantiationSummaries.push_back(InstantiationSummary);
335    }
336    if (InstantiationSummaries.empty())
337      continue;
338
339    auto GroupSummary =
340        FunctionCoverageSummary::get(Group, InstantiationSummaries);
341
342    if (Options.Debug)
343      outs() << "InstantiationGroup: " << GroupSummary.Name << " with "
344             << "size = " << Group.size() << "\n";
345
346    FileReport->addFunction(GroupSummary);
347  }
348}
349
350std::vector<FileCoverageSummary> CoverageReport::prepareFileReports(
351    const coverage::CoverageMapping &Coverage, FileCoverageSummary &Totals,
352    ArrayRef<std::string> Files, const CoverageViewOptions &Options,
353    const CoverageFilter &Filters) {
354  unsigned LCP = getRedundantPrefixLen(Files);
355  auto NumThreads = Options.NumThreads;
356
357  // If NumThreads is not specified, auto-detect a good default.
358  if (NumThreads == 0)
359    NumThreads =
360        std::max(1U, std::min(llvm::heavyweight_hardware_concurrency(),
361                              unsigned(Files.size())));
362
363  ThreadPool Pool(NumThreads);
364
365  std::vector<FileCoverageSummary> FileReports;
366  FileReports.reserve(Files.size());
367
368  for (StringRef Filename : Files) {
369    FileReports.emplace_back(Filename.drop_front(LCP));
370    Pool.async(&CoverageReport::prepareSingleFileReport, Filename,
371               &Coverage, Options, LCP, &FileReports.back(), &Filters);
372  }
373  Pool.wait();
374
375  for (const auto &FileReport : FileReports)
376    Totals += FileReport;
377
378  return FileReports;
379}
380
381void CoverageReport::renderFileReports(
382    raw_ostream &OS, const CoverageFilters &IgnoreFilenameFilters) const {
383  std::vector<std::string> UniqueSourceFiles;
384  for (StringRef SF : Coverage.getUniqueSourceFiles()) {
385    // Apply ignore source files filters.
386    if (!IgnoreFilenameFilters.matchesFilename(SF))
387      UniqueSourceFiles.emplace_back(SF.str());
388  }
389  renderFileReports(OS, UniqueSourceFiles);
390}
391
392void CoverageReport::renderFileReports(
393    raw_ostream &OS, ArrayRef<std::string> Files) const {
394  renderFileReports(OS, Files, CoverageFiltersMatchAll());
395}
396
397void CoverageReport::renderFileReports(
398    raw_ostream &OS, ArrayRef<std::string> Files,
399    const CoverageFiltersMatchAll &Filters) const {
400  FileCoverageSummary Totals("TOTAL");
401  auto FileReports =
402      prepareFileReports(Coverage, Totals, Files, Options, Filters);
403
404  std::vector<StringRef> Filenames;
405  for (const FileCoverageSummary &FCS : FileReports)
406    Filenames.emplace_back(FCS.Name);
407  adjustColumnWidths(Filenames, {});
408
409  OS << column("Filename", FileReportColumns[0]);
410  if (Options.ShowRegionSummary)
411    OS << column("Regions", FileReportColumns[1], Column::RightAlignment)
412       << column("Missed Regions", FileReportColumns[2], Column::RightAlignment)
413       << column("Cover", FileReportColumns[3], Column::RightAlignment);
414  OS << column("Functions", FileReportColumns[4], Column::RightAlignment)
415     << column("Missed Functions", FileReportColumns[5], Column::RightAlignment)
416     << column("Executed", FileReportColumns[6], Column::RightAlignment);
417  if (Options.ShowInstantiationSummary)
418    OS << column("Instantiations", FileReportColumns[7], Column::RightAlignment)
419       << column("Missed Insts.", FileReportColumns[8], Column::RightAlignment)
420       << column("Executed", FileReportColumns[9], Column::RightAlignment);
421  OS << column("Lines", FileReportColumns[10], Column::RightAlignment)
422     << column("Missed Lines", FileReportColumns[11], Column::RightAlignment)
423     << column("Cover", FileReportColumns[12], Column::RightAlignment) << "\n";
424  renderDivider(FileReportColumns, OS);
425  OS << "\n";
426
427  bool EmptyFiles = false;
428  for (const FileCoverageSummary &FCS : FileReports) {
429    if (FCS.FunctionCoverage.getNumFunctions())
430      render(FCS, OS);
431    else
432      EmptyFiles = true;
433  }
434
435  if (EmptyFiles && Filters.empty()) {
436    OS << "\n"
437       << "Files which contain no functions:\n";
438
439    for (const FileCoverageSummary &FCS : FileReports)
440      if (!FCS.FunctionCoverage.getNumFunctions())
441        render(FCS, OS);
442  }
443
444  renderDivider(FileReportColumns, OS);
445  OS << "\n";
446  render(Totals, OS);
447}
448
449} // end namespace llvm
450