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 ? *Highlight : 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 && *Highlight == raw_ostream::RED)) && |
190 | S->HasCount && 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 ? *Highlight : 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 = formatCount(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 | << formatCount(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 | for (const auto &R : BRV.Regions) { |
298 | double TruePercent = 0.0; |
299 | double FalsePercent = 0.0; |
300 | // FIXME: It may overflow when the data is too large, but I have not |
301 | // encountered it in actual use, and not sure whether to use __uint128_t. |
302 | uint64_t Total = R.ExecutionCount + R.FalseExecutionCount; |
303 | |
304 | if (!getOptions().ShowBranchCounts && Total != 0) { |
305 | TruePercent = ((double)(R.ExecutionCount) / (double)Total) * 100.0; |
306 | FalsePercent = ((double)(R.FalseExecutionCount) / (double)Total) * 100.0; |
307 | } |
308 | |
309 | renderLinePrefix(OS, ViewDepth); |
310 | OS << " Branch (" << R.LineStart << ":" << R.ColumnStart << "): [" ; |
311 | |
312 | if (R.Folded) { |
313 | OS << "Folded - Ignored]\n" ; |
314 | continue; |
315 | } |
316 | |
317 | colored_ostream(OS, Color: raw_ostream::RED, |
318 | IsColorUsed: getOptions().Colors && !R.ExecutionCount, |
319 | /*Bold=*/false, /*BG=*/true) |
320 | << "True" ; |
321 | |
322 | if (getOptions().ShowBranchCounts) |
323 | OS << ": " << formatCount(N: R.ExecutionCount) << ", " ; |
324 | else |
325 | OS << ": " << format(Fmt: "%0.2f" , Vals: TruePercent) << "%, " ; |
326 | |
327 | colored_ostream(OS, Color: raw_ostream::RED, |
328 | IsColorUsed: getOptions().Colors && !R.FalseExecutionCount, |
329 | /*Bold=*/false, /*BG=*/true) |
330 | << "False" ; |
331 | |
332 | if (getOptions().ShowBranchCounts) |
333 | OS << ": " << formatCount(N: R.FalseExecutionCount); |
334 | else |
335 | OS << ": " << format(Fmt: "%0.2f" , Vals: FalsePercent) << "%" ; |
336 | OS << "]\n" ; |
337 | } |
338 | } |
339 | |
340 | void SourceCoverageViewText::renderMCDCView(raw_ostream &OS, MCDCView &MRV, |
341 | unsigned ViewDepth) { |
342 | for (auto &Record : MRV.Records) { |
343 | renderLinePrefix(OS, ViewDepth); |
344 | OS << "---> MC/DC Decision Region (" ; |
345 | // Display Line + Column information. |
346 | const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion(); |
347 | OS << DecisionRegion.LineStart << ":" ; |
348 | OS << DecisionRegion.ColumnStart << ") to (" ; |
349 | OS << DecisionRegion.LineEnd << ":" ; |
350 | OS << DecisionRegion.ColumnEnd << ")\n" ; |
351 | renderLinePrefix(OS, ViewDepth); |
352 | OS << "\n" ; |
353 | |
354 | // Display MC/DC Information. |
355 | renderLinePrefix(OS, ViewDepth); |
356 | OS << " Number of Conditions: " << Record.getNumConditions() << "\n" ; |
357 | for (unsigned i = 0; i < Record.getNumConditions(); i++) { |
358 | renderLinePrefix(OS, ViewDepth); |
359 | OS << " " << Record.getConditionHeaderString(Condition: i); |
360 | } |
361 | renderLinePrefix(OS, ViewDepth); |
362 | OS << "\n" ; |
363 | renderLinePrefix(OS, ViewDepth); |
364 | OS << " Executed MC/DC Test Vectors:\n" ; |
365 | renderLinePrefix(OS, ViewDepth); |
366 | OS << "\n" ; |
367 | renderLinePrefix(OS, ViewDepth); |
368 | OS << " " ; |
369 | OS << Record.getTestVectorHeaderString(); |
370 | for (unsigned i = 0; i < Record.getNumTestVectors(); i++) { |
371 | renderLinePrefix(OS, ViewDepth); |
372 | OS << Record.getTestVectorString(TestVectorIndex: i); |
373 | } |
374 | renderLinePrefix(OS, ViewDepth); |
375 | OS << "\n" ; |
376 | for (unsigned i = 0; i < Record.getNumConditions(); i++) { |
377 | renderLinePrefix(OS, ViewDepth); |
378 | OS << Record.getConditionCoverageString(Condition: i); |
379 | } |
380 | renderLinePrefix(OS, ViewDepth); |
381 | OS << " MC/DC Coverage for Decision: " ; |
382 | colored_ostream(OS, Color: raw_ostream::RED, |
383 | IsColorUsed: getOptions().Colors && Record.getPercentCovered() < 100.0, |
384 | /*Bold=*/false, /*BG=*/true) |
385 | << format(Fmt: "%0.2f" , Vals: Record.getPercentCovered()) << "%" ; |
386 | OS << "\n" ; |
387 | renderLinePrefix(OS, ViewDepth); |
388 | OS << "\n" ; |
389 | } |
390 | } |
391 | |
392 | void SourceCoverageViewText::renderInstantiationView(raw_ostream &OS, |
393 | InstantiationView &ISV, |
394 | unsigned ViewDepth) { |
395 | renderLinePrefix(OS, ViewDepth); |
396 | OS << ' '; |
397 | if (!ISV.View) |
398 | getOptions().colored_ostream(OS, Color: raw_ostream::RED) |
399 | << "Unexecuted instantiation: " << ISV.FunctionName << "\n" ; |
400 | else |
401 | ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, |
402 | /*ShowTitle=*/false, ViewDepth); |
403 | } |
404 | |
405 | void SourceCoverageViewText::renderTitle(raw_ostream &OS, StringRef Title) { |
406 | if (getOptions().hasProjectTitle()) |
407 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) |
408 | << getOptions().ProjectTitle << "\n" ; |
409 | |
410 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) << Title << "\n" ; |
411 | |
412 | if (getOptions().hasCreatedTime()) |
413 | getOptions().colored_ostream(OS, Color: raw_ostream::CYAN) |
414 | << getOptions().CreatedTimeStr << "\n" ; |
415 | } |
416 | |
417 | void SourceCoverageViewText::(raw_ostream &, unsigned) {} |
418 | |