1 | //===- SourceCoverageViewText.cpp - A text-based 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 text-based coverage renderer. |
10 | /// |
11 | //===----------------------------------------------------------------------===// |
12 | |
13 | #include "SourceCoverageViewText.h" |
14 | #include "CoverageReport.h" |
15 | #include "llvm/ADT/SmallString.h" |
16 | #include "llvm/ADT/StringExtras.h" |
17 | #include "llvm/Support/FileSystem.h" |
18 | #include "llvm/Support/Format.h" |
19 | #include "llvm/Support/Path.h" |
20 | #include <optional> |
21 | |
22 | using namespace llvm; |
23 | |
24 | Expected<CoveragePrinter::OwnedStream> |
25 | CoveragePrinterText::createViewFile(StringRef Path, bool InToplevel) { |
26 | return createOutputStream(Path, Extension: "txt" , InToplevel); |
27 | } |
28 | |
29 | void CoveragePrinterText::closeViewFile(OwnedStream OS) { |
30 | OS->operator<<(C: '\n'); |
31 | } |
32 | |
33 | Error CoveragePrinterText::createIndexFile( |
34 | ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage, |
35 | const CoverageFiltersMatchAll &Filters) { |
36 | auto OSOrErr = createOutputStream(Path: "index" , Extension: "txt" , /*InToplevel=*/true); |
37 | if (Error E = OSOrErr.takeError()) |
38 | return E; |
39 | auto OS = std::move(OSOrErr.get()); |
40 | raw_ostream &OSRef = *OS.get(); |
41 | |
42 | CoverageReport Report(Opts, Coverage); |
43 | Report.renderFileReports(OS&: OSRef, Files: SourceFiles, Filters); |
44 | |
45 | Opts.colored_ostream(OS&: OSRef, Color: raw_ostream::CYAN) << "\n" |
46 | << Opts.getLLVMVersionString(); |
47 | |
48 | return Error::success(); |
49 | } |
50 | |
51 | struct CoveragePrinterTextDirectory::Reporter : public DirectoryCoverageReport { |
52 | CoveragePrinterTextDirectory &Printer; |
53 | |
54 | Reporter(CoveragePrinterTextDirectory &Printer, |
55 | const coverage::CoverageMapping &Coverage, |
56 | const CoverageFiltersMatchAll &Filters) |
57 | : DirectoryCoverageReport(Printer.Opts, Coverage, Filters), |
58 | Printer(Printer) {} |
59 | |
60 | Error generateSubDirectoryReport(SubFileReports &&SubFiles, |
61 | SubDirReports &&SubDirs, |
62 | FileCoverageSummary &&SubTotals) override { |
63 | auto &LCPath = SubTotals.Name; |
64 | assert(Options.hasOutputDirectory() && |
65 | "No output directory for index file" ); |
66 | |
67 | SmallString<128> OSPath = LCPath; |
68 | sys::path::append(path&: OSPath, a: "index" ); |
69 | auto OSOrErr = Printer.createOutputStream(Path: OSPath, Extension: "txt" , |
70 | /*InToplevel=*/false); |
71 | if (auto E = OSOrErr.takeError()) |
72 | return E; |
73 | auto OS = std::move(OSOrErr.get()); |
74 | raw_ostream &OSRef = *OS.get(); |
75 | |
76 | std::vector<FileCoverageSummary> Reports; |
77 | for (auto &&SubDir : SubDirs) |
78 | Reports.push_back(x: std::move(SubDir.second.first)); |
79 | for (auto &&SubFile : SubFiles) |
80 | Reports.push_back(x: std::move(SubFile.second)); |
81 | |
82 | CoverageReport Report(Options, Coverage); |
83 | Report.renderFileReports(OS&: OSRef, FileReports: Reports, Totals: SubTotals, ShowEmptyFiles: Filters.empty()); |
84 | |
85 | Options.colored_ostream(OS&: OSRef, Color: raw_ostream::CYAN) |
86 | << "\n" |
87 | << Options.getLLVMVersionString(); |
88 | |
89 | return Error::success(); |
90 | } |
91 | }; |
92 | |
93 | Error CoveragePrinterTextDirectory::createIndexFile( |
94 | ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage, |
95 | const CoverageFiltersMatchAll &Filters) { |
96 | if (SourceFiles.size() <= 1) |
97 | return CoveragePrinterText::createIndexFile(SourceFiles, Coverage, Filters); |
98 | |
99 | Reporter Report(*this, Coverage, Filters); |
100 | auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles); |
101 | if (auto E = TotalsOrErr.takeError()) |
102 | return E; |
103 | auto &LCPath = TotalsOrErr->Name; |
104 | |
105 | auto TopIndexFilePath = |
106 | getOutputPath(Path: "index" , Extension: "txt" , /*InToplevel=*/true, /*Relative=*/false); |
107 | auto LCPIndexFilePath = |
108 | getOutputPath(Path: (LCPath + "index" ).str(), Extension: "txt" , /*InToplevel=*/false, |
109 | /*Relative=*/false); |
110 | return errorCodeToError( |
111 | EC: sys::fs::copy_file(From: LCPIndexFilePath, To: TopIndexFilePath)); |
112 | } |
113 | |
114 | namespace { |
115 | |
116 | static const unsigned LineCoverageColumnWidth = 7; |
117 | static const unsigned LineNumberColumnWidth = 5; |
118 | |
119 | /// Get the width of the leading columns. |
120 | unsigned getCombinedColumnWidth(const CoverageViewOptions &Opts) { |
121 | return (Opts.ShowLineStats ? LineCoverageColumnWidth + 1 : 0) + |
122 | (Opts.ShowLineNumbers ? LineNumberColumnWidth + 1 : 0); |
123 | } |
124 | |
125 | /// The width of the line that is used to divide between the view and |
126 | /// the subviews. |
127 | unsigned getDividerWidth(const CoverageViewOptions &Opts) { |
128 | return getCombinedColumnWidth(Opts) + 4; |
129 | } |
130 | |
131 | } // anonymous namespace |
132 | |
133 | void SourceCoverageViewText::(raw_ostream &) {} |
134 | |
135 | void SourceCoverageViewText::(raw_ostream &) {} |
136 | |
137 | void SourceCoverageViewText::renderSourceName(raw_ostream &OS, bool WholeFile) { |
138 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) << getSourceName() |
139 | << ":\n" ; |
140 | } |
141 | |
142 | void SourceCoverageViewText::renderLinePrefix(raw_ostream &OS, |
143 | unsigned ViewDepth) { |
144 | for (unsigned I = 0; I < ViewDepth; ++I) |
145 | OS << " |" ; |
146 | } |
147 | |
148 | void SourceCoverageViewText::renderLineSuffix(raw_ostream &, unsigned) {} |
149 | |
150 | void SourceCoverageViewText::renderViewDivider(raw_ostream &OS, |
151 | unsigned ViewDepth) { |
152 | assert(ViewDepth != 0 && "Cannot render divider at top level" ); |
153 | renderLinePrefix(OS, ViewDepth: ViewDepth - 1); |
154 | OS.indent(NumSpaces: 2); |
155 | unsigned Length = getDividerWidth(Opts: getOptions()); |
156 | for (unsigned I = 0; I < Length; ++I) |
157 | OS << '-'; |
158 | OS << '\n'; |
159 | } |
160 | |
161 | void SourceCoverageViewText::renderLine(raw_ostream &OS, LineRef L, |
162 | const LineCoverageStats &LCS, |
163 | unsigned ExpansionCol, |
164 | unsigned ViewDepth) { |
165 | StringRef Line = L.Line; |
166 | unsigned LineNumber = L.LineNo; |
167 | auto *WrappedSegment = LCS.getWrappedSegment(); |
168 | CoverageSegmentArray Segments = LCS.getLineSegments(); |
169 | |
170 | std::optional<raw_ostream::Colors> Highlight; |
171 | SmallVector<std::pair<unsigned, unsigned>, 2> HighlightedRanges; |
172 | |
173 | // The first segment overlaps from a previous line, so we treat it specially. |
174 | if (WrappedSegment && !WrappedSegment->IsGapRegion && |
175 | WrappedSegment->HasCount && WrappedSegment->Count == 0) |
176 | Highlight = raw_ostream::RED; |
177 | |
178 | // Output each segment of the line, possibly highlighted. |
179 | unsigned Col = 1; |
180 | for (const auto *S : Segments) { |
181 | unsigned End = std::min(a: S->Col, b: static_cast<unsigned>(Line.size()) + 1); |
182 | colored_ostream(OS, Color: Highlight.value_or(u: raw_ostream::SAVEDCOLOR), |
183 | IsColorUsed: getOptions().Colors && Highlight, /*Bold=*/false, |
184 | /*BG=*/true) |
185 | << Line.substr(Start: Col - 1, N: End - Col); |
186 | if (getOptions().Debug && Highlight) |
187 | HighlightedRanges.push_back(Elt: std::make_pair(x&: Col, y&: End)); |
188 | Col = End; |
189 | if ((!S->IsGapRegion || Highlight == raw_ostream::RED) && S->HasCount && |
190 | S->Count == 0) |
191 | Highlight = raw_ostream::RED; |
192 | else if (Col == ExpansionCol) |
193 | Highlight = raw_ostream::CYAN; |
194 | else |
195 | Highlight = std::nullopt; |
196 | } |
197 | |
198 | // Show the rest of the line. |
199 | colored_ostream(OS, Color: Highlight.value_or(u: raw_ostream::SAVEDCOLOR), |
200 | IsColorUsed: getOptions().Colors && Highlight, /*Bold=*/false, /*BG=*/true) |
201 | << Line.substr(Start: Col - 1, N: Line.size() - Col + 1); |
202 | OS << '\n'; |
203 | |
204 | if (getOptions().Debug) { |
205 | for (const auto &Range : HighlightedRanges) |
206 | errs() << "Highlighted line " << LineNumber << ", " << Range.first |
207 | << " -> " << Range.second << '\n'; |
208 | if (Highlight) |
209 | errs() << "Highlighted line " << LineNumber << ", " << Col << " -> ?\n" ; |
210 | } |
211 | } |
212 | |
213 | void SourceCoverageViewText::renderLineCoverageColumn( |
214 | raw_ostream &OS, const LineCoverageStats &Line) { |
215 | if (!Line.isMapped()) { |
216 | OS.indent(NumSpaces: LineCoverageColumnWidth) << '|'; |
217 | return; |
218 | } |
219 | std::string C = formatBinaryCount(N: Line.getExecutionCount()); |
220 | OS.indent(NumSpaces: LineCoverageColumnWidth - C.size()); |
221 | colored_ostream(OS, Color: raw_ostream::MAGENTA, |
222 | IsColorUsed: Line.hasMultipleRegions() && getOptions().Colors) |
223 | << C; |
224 | OS << '|'; |
225 | } |
226 | |
227 | void SourceCoverageViewText::renderLineNumberColumn(raw_ostream &OS, |
228 | unsigned LineNo) { |
229 | SmallString<32> Buffer; |
230 | raw_svector_ostream BufferOS(Buffer); |
231 | BufferOS << LineNo; |
232 | auto Str = BufferOS.str(); |
233 | // Trim and align to the right. |
234 | Str = Str.substr(Start: 0, N: std::min(a: Str.size(), b: (size_t)LineNumberColumnWidth)); |
235 | OS.indent(NumSpaces: LineNumberColumnWidth - Str.size()) << Str << '|'; |
236 | } |
237 | |
238 | void SourceCoverageViewText::renderRegionMarkers(raw_ostream &OS, |
239 | const LineCoverageStats &Line, |
240 | unsigned ViewDepth) { |
241 | renderLinePrefix(OS, ViewDepth); |
242 | OS.indent(NumSpaces: getCombinedColumnWidth(Opts: getOptions())); |
243 | |
244 | CoverageSegmentArray Segments = Line.getLineSegments(); |
245 | |
246 | // Just consider the segments which start *and* end on this line. |
247 | if (Segments.size() > 1) |
248 | Segments = Segments.drop_back(); |
249 | |
250 | unsigned PrevColumn = 1; |
251 | for (const auto *S : Segments) { |
252 | if (!S->IsRegionEntry) |
253 | continue; |
254 | if (S->Count == Line.getExecutionCount()) |
255 | continue; |
256 | // Skip to the new region. |
257 | if (S->Col > PrevColumn) |
258 | OS.indent(NumSpaces: S->Col - PrevColumn); |
259 | PrevColumn = S->Col + 1; |
260 | std::string C = formatCount(N: S->Count); |
261 | PrevColumn += C.size(); |
262 | OS << '^' << C; |
263 | |
264 | if (getOptions().Debug) |
265 | errs() << "Marker at " << S->Line << ":" << S->Col << " = " |
266 | << formatBinaryCount(N: S->Count) << "\n" ; |
267 | } |
268 | OS << '\n'; |
269 | } |
270 | |
271 | void SourceCoverageViewText::renderExpansionSite(raw_ostream &OS, LineRef L, |
272 | const LineCoverageStats &LCS, |
273 | unsigned ExpansionCol, |
274 | unsigned ViewDepth) { |
275 | renderLinePrefix(OS, ViewDepth); |
276 | OS.indent(NumSpaces: getCombinedColumnWidth(Opts: getOptions()) + (ViewDepth == 0 ? 0 : 1)); |
277 | renderLine(OS, L, LCS, ExpansionCol, ViewDepth); |
278 | } |
279 | |
280 | void SourceCoverageViewText::renderExpansionView(raw_ostream &OS, |
281 | ExpansionView &ESV, |
282 | unsigned ViewDepth) { |
283 | // Render the child subview. |
284 | if (getOptions().Debug) |
285 | errs() << "Expansion at line " << ESV.getLine() << ", " << ESV.getStartCol() |
286 | << " -> " << ESV.getEndCol() << '\n'; |
287 | ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false, |
288 | /*ShowTitle=*/false, ViewDepth: ViewDepth + 1); |
289 | } |
290 | |
291 | void SourceCoverageViewText::renderBranchView(raw_ostream &OS, BranchView &BRV, |
292 | unsigned ViewDepth) { |
293 | // Render the child subview. |
294 | if (getOptions().Debug) |
295 | errs() << "Branch at line " << BRV.getLine() << '\n'; |
296 | |
297 | auto BranchCount = [&](StringRef Label, uint64_t Count, bool Folded, |
298 | double Total) { |
299 | if (Folded) |
300 | return std::string{"Folded" }; |
301 | |
302 | std::string Str; |
303 | raw_string_ostream OS(Str); |
304 | |
305 | colored_ostream(OS, Color: raw_ostream::RED, IsColorUsed: getOptions().Colors && !Count, |
306 | /*Bold=*/false, /*BG=*/true) |
307 | << Label; |
308 | |
309 | if (getOptions().ShowBranchCounts) |
310 | OS << ": " << formatBinaryCount(N: Count); |
311 | else |
312 | OS << ": " << format(Fmt: "%0.2f" , Vals: (Total != 0 ? 100.0 * Count / Total : 0.0)) |
313 | << "%" ; |
314 | |
315 | return Str; |
316 | }; |
317 | |
318 | for (const auto &R : BRV.Regions) { |
319 | // This can be `double` since it is only used as a denominator. |
320 | // FIXME: It is still inaccurate if Count is greater than (1LL << 53). |
321 | double Total = |
322 | static_cast<double>(R.ExecutionCount) + R.FalseExecutionCount; |
323 | |
324 | renderLinePrefix(OS, ViewDepth); |
325 | OS << " Branch (" << R.LineStart << ":" << R.ColumnStart << "): [" ; |
326 | |
327 | if (R.TrueFolded && R.FalseFolded) { |
328 | OS << "Folded - Ignored]\n" ; |
329 | continue; |
330 | } |
331 | |
332 | OS << BranchCount("True" , R.ExecutionCount, R.TrueFolded, Total) << ", " |
333 | << BranchCount("False" , R.FalseExecutionCount, R.FalseFolded, Total) |
334 | << "]\n" ; |
335 | } |
336 | } |
337 | |
338 | void SourceCoverageViewText::renderMCDCView(raw_ostream &OS, MCDCView &MRV, |
339 | unsigned ViewDepth) { |
340 | for (auto &Record : MRV.Records) { |
341 | renderLinePrefix(OS, ViewDepth); |
342 | OS << "---> MC/DC Decision Region (" ; |
343 | // Display Line + Column information. |
344 | const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion(); |
345 | OS << DecisionRegion.LineStart << ":" ; |
346 | OS << DecisionRegion.ColumnStart << ") to (" ; |
347 | OS << DecisionRegion.LineEnd << ":" ; |
348 | OS << DecisionRegion.ColumnEnd << ")\n" ; |
349 | renderLinePrefix(OS, ViewDepth); |
350 | OS << "\n" ; |
351 | |
352 | // Display MC/DC Information. |
353 | renderLinePrefix(OS, ViewDepth); |
354 | OS << " Number of Conditions: " << Record.getNumConditions() << "\n" ; |
355 | for (unsigned i = 0; i < Record.getNumConditions(); i++) { |
356 | renderLinePrefix(OS, ViewDepth); |
357 | OS << " " << Record.getConditionHeaderString(Condition: i); |
358 | } |
359 | renderLinePrefix(OS, ViewDepth); |
360 | OS << "\n" ; |
361 | renderLinePrefix(OS, ViewDepth); |
362 | OS << " Executed MC/DC Test Vectors:\n" ; |
363 | renderLinePrefix(OS, ViewDepth); |
364 | OS << "\n" ; |
365 | renderLinePrefix(OS, ViewDepth); |
366 | OS << " " ; |
367 | OS << Record.getTestVectorHeaderString(); |
368 | for (unsigned i = 0; i < Record.getNumTestVectors(); i++) { |
369 | renderLinePrefix(OS, ViewDepth); |
370 | OS << Record.getTestVectorString(TestVectorIndex: i); |
371 | } |
372 | renderLinePrefix(OS, ViewDepth); |
373 | OS << "\n" ; |
374 | for (unsigned i = 0; i < Record.getNumConditions(); i++) { |
375 | renderLinePrefix(OS, ViewDepth); |
376 | OS << Record.getConditionCoverageString(Condition: i); |
377 | } |
378 | renderLinePrefix(OS, ViewDepth); |
379 | OS << " MC/DC Coverage for Decision: " ; |
380 | colored_ostream(OS, Color: raw_ostream::RED, |
381 | IsColorUsed: getOptions().Colors && Record.getPercentCovered() < 100.0, |
382 | /*Bold=*/false, /*BG=*/true) |
383 | << format(Fmt: "%0.2f" , Vals: Record.getPercentCovered()) << "%" ; |
384 | OS << "\n" ; |
385 | renderLinePrefix(OS, ViewDepth); |
386 | OS << "\n" ; |
387 | } |
388 | } |
389 | |
390 | void SourceCoverageViewText::renderInstantiationView(raw_ostream &OS, |
391 | InstantiationView &ISV, |
392 | unsigned ViewDepth) { |
393 | renderLinePrefix(OS, ViewDepth); |
394 | OS << ' '; |
395 | if (!ISV.View) |
396 | getOptions().colored_ostream(OS, Color: raw_ostream::RED) |
397 | << "Unexecuted instantiation: " << ISV.FunctionName << "\n" ; |
398 | else |
399 | ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, |
400 | /*ShowTitle=*/false, ViewDepth); |
401 | } |
402 | |
403 | void SourceCoverageViewText::renderTitle(raw_ostream &OS, StringRef Title) { |
404 | if (getOptions().hasProjectTitle()) |
405 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) |
406 | << getOptions().ProjectTitle << "\n" ; |
407 | |
408 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) << Title << "\n" ; |
409 | |
410 | if (getOptions().hasCreatedTime()) |
411 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) |
412 | << getOptions().CreatedTimeStr << "\n" ; |
413 | } |
414 | |
415 | void SourceCoverageViewText::(raw_ostream &, unsigned) {} |
416 | |