blob: 2ec5f0d2cfe38b71337c11acefa4d455176e1109 [file] [log] [blame]
Avi Drissmandfd880852022-09-15 20:11:091# Copyright 2018 The Chromium Authors
Max Moroz1de68d72018-08-21 13:38:182# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
5# The script intentionally does not have a shebang, as it is Py2/Py3 compatible.
6
7import argparse
8from collections import defaultdict
9import functools
Max Moroz1de68d72018-08-21 13:38:1810import json
11import logging
12import os
13import re
14import shutil
15import subprocess
16import sys
17
Julia Hansbrough5e3ae9a22023-06-20 19:17:0318# Appends third_party/ so that coverage_utils can import jinja2 from
19# third_party/.
20sys.path.append(
21 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir,
22 'third_party'))
23import jinja2
24
Max Moroz1de68d72018-08-21 13:38:1825# The default name of the html coverage report for a directory.
26DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
27
28# Name of the html index files for different views.
29COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
30DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
31FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
32INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
Zequan Wu3ccc2e72022-08-26 15:58:2233REPORT_DIR = 'coverage'
Max Moroz1de68d72018-08-21 13:38:1834
35
36class CoverageSummary(object):
37 """Encapsulates coverage summary representation."""
38
39 def __init__(self,
40 regions_total=0,
41 regions_covered=0,
42 functions_total=0,
43 functions_covered=0,
44 lines_total=0,
45 lines_covered=0):
46 """Initializes CoverageSummary object."""
47 self._summary = {
48 'regions': {
49 'total': regions_total,
50 'covered': regions_covered
51 },
52 'functions': {
53 'total': functions_total,
54 'covered': functions_covered
55 },
56 'lines': {
57 'total': lines_total,
58 'covered': lines_covered
59 }
60 }
61
62 def Get(self):
63 """Returns summary as a dictionary."""
64 return self._summary
65
66 def AddSummary(self, other_summary):
67 """Adds another summary to this one element-wise."""
68 for feature in self._summary:
69 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
70 self._summary[feature]['covered'] += other_summary.Get()[feature][
71 'covered']
72
73
74class CoverageReportHtmlGenerator(object):
75 """Encapsulates coverage html report generation.
76
77 The generated html has a table that contains links to other coverage reports.
78 """
79
80 def __init__(self, output_dir, output_path, table_entry_type):
81 """Initializes _CoverageReportHtmlGenerator object.
82
83 Args:
84 output_dir: Path to the dir for writing coverage report to.
85 output_path: Path to the html report that will be generated.
86 table_entry_type: Type of the table entries to be displayed in the table
87 header. For example: 'Path', 'Component'.
88 """
89 css_file_name = os.extsep.join(['style', 'css'])
90 css_absolute_path = os.path.join(output_dir, css_file_name)
91 assert os.path.exists(css_absolute_path), (
92 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
93 'is called first, and the css file is generated at: "%s".' %
94 css_absolute_path)
95
96 self._css_absolute_path = css_absolute_path
97 self._output_dir = output_dir
98 self._output_path = output_path
99 self._table_entry_type = table_entry_type
100
101 self._table_entries = []
102 self._total_entry = {}
103
104 source_dir = os.path.dirname(os.path.realpath(__file__))
105 template_dir = os.path.join(source_dir, 'html_templates')
106
107 jinja_env = jinja2.Environment(
108 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
109 self._header_template = jinja_env.get_template('header.html')
110 self._table_template = jinja_env.get_template('table.html')
111 self._footer_template = jinja_env.get_template('footer.html')
112
113 self._style_overrides = open(
114 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
115
116 def AddLinkToAnotherReport(self, html_report_path, name, summary):
117 """Adds a link to another html report in this report.
118
119 The link to be added is assumed to be an entry in this directory.
120 """
121 # Use relative paths instead of absolute paths to make the generated reports
122 # portable.
123 html_report_relative_path = GetRelativePathToDirectoryOfFile(
124 html_report_path, self._output_path)
125
126 table_entry = self._CreateTableEntryFromCoverageSummary(
127 summary, html_report_relative_path, name,
128 os.path.basename(html_report_path) ==
129 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
130 self._table_entries.append(table_entry)
131
132 def CreateTotalsEntry(self, summary):
133 """Creates an entry corresponds to the 'Totals' row in the html report."""
134 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
135
136 def _CreateTableEntryFromCoverageSummary(self,
137 summary,
138 href=None,
139 name=None,
140 is_dir=None):
141 """Creates an entry to display in the html report."""
142 assert (href is None and name is None and is_dir is None) or (
143 href is not None and name is not None and is_dir is not None), (
144 'The only scenario when href or name or is_dir can be None is when '
145 'creating an entry for the Totals row, and in that case, all three '
146 'attributes must be None.')
147
148 entry = {}
149 if href is not None:
150 entry['href'] = href
151 if name is not None:
152 entry['name'] = name
153 if is_dir is not None:
154 entry['is_dir'] = is_dir
155
156 summary_dict = summary.Get()
157 for feature in summary_dict:
158 if summary_dict[feature]['total'] == 0:
159 percentage = 0.0
160 else:
161 percentage = float(summary_dict[feature]
162 ['covered']) / summary_dict[feature]['total'] * 100
163
164 color_class = self._GetColorClass(percentage)
165 entry[feature] = {
166 'total': summary_dict[feature]['total'],
167 'covered': summary_dict[feature]['covered'],
168 'percentage': '{:6.2f}'.format(percentage),
169 'color_class': color_class
170 }
171
172 return entry
173
174 def _GetColorClass(self, percentage):
175 """Returns the css color class based on coverage percentage."""
176 if percentage >= 0 and percentage < 80:
177 return 'red'
178 if percentage >= 80 and percentage < 100:
179 return 'yellow'
180 if percentage == 100:
181 return 'green'
182
183 assert False, 'Invalid coverage percentage: "%d".' % percentage
184
185 def WriteHtmlCoverageReport(self, no_component_view, no_file_view):
186 """Writes html coverage report.
187
188 In the report, sub-directories are displayed before files and within each
189 category, entries are sorted alphabetically.
190 """
191
192 def EntryCmp(left, right):
193 """Compare function for table entries."""
194 if left['is_dir'] != right['is_dir']:
195 return -1 if left['is_dir'] == True else 1
196
197 return -1 if left['name'] < right['name'] else 1
198
199 self._table_entries = sorted(
200 self._table_entries, key=functools.cmp_to_key(EntryCmp))
201
202 css_path = os.path.join(self._output_dir, os.extsep.join(['style', 'css']))
203
204 directory_view_path = GetDirectoryViewPath(self._output_dir)
205 directory_view_href = GetRelativePathToDirectoryOfFile(
206 directory_view_path, self._output_path)
207
208 component_view_href = None
209 if not no_component_view:
210 component_view_path = GetComponentViewPath(self._output_dir)
211 component_view_href = GetRelativePathToDirectoryOfFile(
212 component_view_path, self._output_path)
213
214 # File view is optional in the report.
215 file_view_href = None
216 if not no_file_view:
217 file_view_path = GetFileViewPath(self._output_dir)
218 file_view_href = GetRelativePathToDirectoryOfFile(file_view_path,
219 self._output_path)
220
221 html_header = self._header_template.render(
222 css_path=GetRelativePathToDirectoryOfFile(css_path, self._output_path),
223 directory_view_href=directory_view_href,
224 component_view_href=component_view_href,
225 file_view_href=file_view_href,
226 style_overrides=self._style_overrides)
227
228 html_table = self._table_template.render(
229 entries=self._table_entries,
230 total_entry=self._total_entry,
231 table_entry_type=self._table_entry_type)
232 html_footer = self._footer_template.render()
233
Prakhar8dd69902021-06-16 22:47:10234 if not os.path.exists(os.path.dirname(self._output_path)):
235 os.makedirs(os.path.dirname(self._output_path))
Max Moroz1de68d72018-08-21 13:38:18236 with open(self._output_path, 'w') as html_file:
237 html_file.write(html_header + html_table + html_footer)
238
239
240class CoverageReportPostProcessor(object):
241 """Post processing of code coverage reports produced by llvm-cov."""
242
243 def __init__(self,
244 output_dir,
245 src_root_dir,
246 summary_data,
247 no_component_view,
248 no_file_view,
Max Moroz70263d62018-08-21 17:19:31249 component_mappings={},
250 path_equivalence=None):
Max Moroz1de68d72018-08-21 13:38:18251 """Initializes CoverageReportPostProcessor object."""
252 # Caller provided parameters.
253 self.output_dir = output_dir
Max Moroz5e2058f2018-08-23 15:02:03254 self.src_root_dir = os.path.normpath(GetFullPath(src_root_dir))
255 if not self.src_root_dir.endswith(os.sep):
256 self.src_root_dir += os.sep
Max Moroz1de68d72018-08-21 13:38:18257 self.summary_data = json.loads(summary_data)
258 assert len(self.summary_data['data']) == 1
259 self.no_component_view = no_component_view
260 self.no_file_view = no_file_view
261
262 # Mapping from components to directories
263 self.component_to_directories = None
264 if component_mappings:
265 self._ExtractComponentToDirectoriesMapping(component_mappings)
266
267 # The root directory that contains all generated coverage html reports.
268 self.report_root_dir = GetCoverageReportRootDirPath(self.output_dir)
269
Zequan Wu3ccc2e72022-08-26 15:58:22270 # The root directory that all coverage html files generated by llvm-cov.
271 self.html_file_root_dir = GetHtmlFileRootDirPath(self.output_dir)
272
Max Moroz1de68d72018-08-21 13:38:18273 # Path to the HTML file for the component view.
274 self.component_view_path = GetComponentViewPath(self.output_dir)
275
276 # Path to the HTML file for the directory view.
277 self.directory_view_path = GetDirectoryViewPath(self.output_dir)
278
279 # Path to the HTML file for the file view.
280 self.file_view_path = GetFileViewPath(self.output_dir)
281
282 # Path to the main HTML index file.
283 self.html_index_path = GetHtmlIndexPath(self.output_dir)
284
Max Moroz70263d62018-08-21 17:19:31285 self.path_map = None
286 if path_equivalence:
287
288 def _PreparePath(path):
289 path = os.path.normpath(path)
290 if not path.endswith(os.sep):
291 # A normalized path does not end with '/', unless it is a root dir.
292 path += os.sep
293 return path
294
295 self.path_map = [_PreparePath(p) for p in path_equivalence.split(',')]
296 assert len(self.path_map) == 2, 'Path equivalence argument is incorrect.'
297
Max Moroz1de68d72018-08-21 13:38:18298 def _ExtractComponentToDirectoriesMapping(self, component_mappings):
299 """Initializes a mapping from components to directories."""
300 directory_to_component = component_mappings['dir-to-component']
301
302 self.component_to_directories = defaultdict(list)
303 for directory in sorted(directory_to_component):
304 component = directory_to_component[directory]
305
306 # Check if we already added the parent directory of this directory. If
307 # yes,skip this sub-directory to avoid double-counting.
308 found_parent_directory = False
309 for component_directory in self.component_to_directories[component]:
310 if directory.startswith(component_directory + '/'):
311 found_parent_directory = True
312 break
313
314 if not found_parent_directory:
315 self.component_to_directories[component].append(directory)
316
Max Moroz70263d62018-08-21 17:19:31317 def _MapToLocal(self, path):
318 """Maps a path from the coverage data to a local path."""
319 if not self.path_map:
320 return path
321 return path.replace(self.path_map[0], self.path_map[1], 1)
322
Max Moroz1de68d72018-08-21 13:38:18323 def CalculatePerDirectoryCoverageSummary(self, per_file_coverage_summary):
324 """Calculates per directory coverage summary."""
325 logging.debug('Calculating per-directory coverage summary.')
326 per_directory_coverage_summary = defaultdict(lambda: CoverageSummary())
327
328 for file_path in per_file_coverage_summary:
329 summary = per_file_coverage_summary[file_path]
330 parent_dir = os.path.dirname(file_path)
331
332 while True:
333 per_directory_coverage_summary[parent_dir].AddSummary(summary)
334
Max Moroz5e2058f2018-08-23 15:02:03335 if os.path.normpath(parent_dir) == os.path.normpath(self.src_root_dir):
Max Moroz1de68d72018-08-21 13:38:18336 break
337 parent_dir = os.path.dirname(parent_dir)
338
339 logging.debug('Finished calculating per-directory coverage summary.')
340 return per_directory_coverage_summary
341
342 def CalculatePerComponentCoverageSummary(self,
343 per_directory_coverage_summary):
344 """Calculates per component coverage summary."""
345 logging.debug('Calculating per-component coverage summary.')
346 per_component_coverage_summary = defaultdict(lambda: CoverageSummary())
347
348 for component in self.component_to_directories:
349 for directory in self.component_to_directories[component]:
350 absolute_directory_path = GetFullPath(directory)
351 if absolute_directory_path in per_directory_coverage_summary:
352 per_component_coverage_summary[component].AddSummary(
353 per_directory_coverage_summary[absolute_directory_path])
354
355 logging.debug('Finished calculating per-component coverage summary.')
356 return per_component_coverage_summary
357
358 def GeneratePerComponentCoverageInHtml(self, per_component_coverage_summary,
359 per_directory_coverage_summary):
360 """Generates per-component coverage reports in html."""
361 logging.debug('Writing per-component coverage html reports.')
362 for component in per_component_coverage_summary:
363 self.GenerateCoverageInHtmlForComponent(component,
364 per_component_coverage_summary,
365 per_directory_coverage_summary)
366 logging.debug('Finished writing per-component coverage html reports.')
367
368 def GenerateComponentViewHtmlIndexFile(self, per_component_coverage_summary):
369 """Generates the html index file for component view."""
370 component_view_index_file_path = self.component_view_path
371 logging.debug('Generating component view html index file as: "%s".',
372 component_view_index_file_path)
373 html_generator = CoverageReportHtmlGenerator(
374 self.output_dir, component_view_index_file_path, 'Component')
375 for component in per_component_coverage_summary:
376 html_generator.AddLinkToAnotherReport(
377 self.GetCoverageHtmlReportPathForComponent(component), component,
378 per_component_coverage_summary[component])
379
380 # Do not create a totals row for the component view as the value is
381 # incorrect due to failure to account for UNKNOWN component and some paths
382 # belonging to multiple components.
383 html_generator.WriteHtmlCoverageReport(self.no_component_view,
384 self.no_file_view)
385 logging.debug('Finished generating component view html index file.')
386
387 def GenerateCoverageInHtmlForComponent(self, component_name,
388 per_component_coverage_summary,
389 per_directory_coverage_summary):
390 """Generates coverage html report for a component."""
391 component_html_report_path = self.GetCoverageHtmlReportPathForComponent(
392 component_name)
393 component_html_report_dir = os.path.dirname(component_html_report_path)
394 if not os.path.exists(component_html_report_dir):
395 os.makedirs(component_html_report_dir)
396
397 html_generator = CoverageReportHtmlGenerator(
398 self.output_dir, component_html_report_path, 'Path')
399
400 for dir_path in self.component_to_directories[component_name]:
401 dir_absolute_path = GetFullPath(dir_path)
402 if dir_absolute_path not in per_directory_coverage_summary:
403 # Any directory without an exercised file shouldn't be included into
404 # the report.
405 continue
406
407 html_generator.AddLinkToAnotherReport(
408 self.GetCoverageHtmlReportPathForDirectory(dir_path),
409 os.path.relpath(dir_path, self.src_root_dir),
410 per_directory_coverage_summary[dir_absolute_path])
411
412 html_generator.CreateTotalsEntry(
413 per_component_coverage_summary[component_name])
414 html_generator.WriteHtmlCoverageReport(self.no_component_view,
415 self.no_file_view)
416
417 def GetCoverageHtmlReportPathForComponent(self, component_name):
418 """Given a component, returns the corresponding html report path."""
419 component_file_name = component_name.lower().replace('>', '-')
420 html_report_name = os.extsep.join([component_file_name, 'html'])
421 return os.path.join(self.report_root_dir, 'components', html_report_name)
422
423 def GetCoverageHtmlReportPathForDirectory(self, dir_path):
424 """Given a directory path, returns the corresponding html report path."""
Max Moroz70263d62018-08-21 17:19:31425 assert os.path.isdir(
426 self._MapToLocal(dir_path)), '"%s" is not a directory.' % dir_path
Max Moroz1de68d72018-08-21 13:38:18427 html_report_path = os.path.join(
428 GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
429
Brent McBrideb25b177a42020-05-11 18:13:06430 return self.CombineAbsolutePaths(self.report_root_dir, html_report_path)
Max Moroz1de68d72018-08-21 13:38:18431
432 def GetCoverageHtmlReportPathForFile(self, file_path):
433 """Given a file path, returns the corresponding html report path."""
Max Moroz70263d62018-08-21 17:19:31434 assert os.path.isfile(
435 self._MapToLocal(file_path)), '"%s" is not a file.' % file_path
Max Moroz1de68d72018-08-21 13:38:18436 html_report_path = os.extsep.join([GetFullPath(file_path), 'html'])
437
Zequan Wu3ccc2e72022-08-26 15:58:22438 return self.CombineAbsolutePaths(self.html_file_root_dir, html_report_path)
Brent McBrideb25b177a42020-05-11 18:13:06439
440 def CombineAbsolutePaths(self, path1, path2):
441 if GetHostPlatform() == 'win':
442 # Absolute paths in Windows may start with a drive letter and colon.
443 # Remove them from the second path before appending to the first.
444 _, path2 = os.path.splitdrive(path2)
445
Max Moroz1de68d72018-08-21 13:38:18446 # '+' is used instead of os.path.join because both of them are absolute
447 # paths and os.path.join ignores the first path.
Brent McBrideb25b177a42020-05-11 18:13:06448 return path1 + path2
Max Moroz1de68d72018-08-21 13:38:18449
450 def GenerateFileViewHtmlIndexFile(self, per_file_coverage_summary,
451 file_view_index_file_path):
452 """Generates html index file for file view."""
453 logging.debug('Generating file view html index file as: "%s".',
454 file_view_index_file_path)
455 html_generator = CoverageReportHtmlGenerator(
456 self.output_dir, file_view_index_file_path, 'Path')
457 totals_coverage_summary = CoverageSummary()
458
459 for file_path in per_file_coverage_summary:
Prakharba1aa2e42023-05-24 00:21:00460 if not os.path.isfile(self._MapToLocal(file_path)):
461 logging.warning('%s is not a file.', file_path)
462 continue
Max Moroz1de68d72018-08-21 13:38:18463 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
Max Moroz1de68d72018-08-21 13:38:18464 html_generator.AddLinkToAnotherReport(
465 self.GetCoverageHtmlReportPathForFile(file_path),
466 os.path.relpath(file_path, self.src_root_dir),
467 per_file_coverage_summary[file_path])
468
469 html_generator.CreateTotalsEntry(totals_coverage_summary)
470 html_generator.WriteHtmlCoverageReport(self.no_component_view,
471 self.no_file_view)
472 logging.debug('Finished generating file view html index file.')
473
474 def GeneratePerFileCoverageSummary(self):
475 """Generate per file coverage summary using coverage data in JSON format."""
476 files_coverage_data = self.summary_data['data'][0]['files']
477
478 per_file_coverage_summary = {}
479 for file_coverage_data in files_coverage_data:
Choongwoo Han56752522021-06-10 17:38:34480 file_path = os.path.normpath(file_coverage_data['filename'])
Max Moroz5e2058f2018-08-23 15:02:03481 assert file_path.startswith(self.src_root_dir), (
Max Moroz1de68d72018-08-21 13:38:18482 'File path "%s" in coverage summary is outside source checkout.' %
483 file_path)
484
485 summary = file_coverage_data['summary']
486 if summary['lines']['count'] == 0:
487 continue
488
489 per_file_coverage_summary[file_path] = CoverageSummary(
490 regions_total=summary['regions']['count'],
491 regions_covered=summary['regions']['covered'],
492 functions_total=summary['functions']['count'],
493 functions_covered=summary['functions']['covered'],
494 lines_total=summary['lines']['count'],
495 lines_covered=summary['lines']['covered'])
496
497 logging.debug('Finished generating per-file code coverage summary.')
498 return per_file_coverage_summary
499
500 def GeneratePerDirectoryCoverageInHtml(self, per_directory_coverage_summary,
501 per_file_coverage_summary):
502 """Generates per directory coverage breakdown in html."""
503 logging.debug('Writing per-directory coverage html reports.')
504 for dir_path in per_directory_coverage_summary:
505 self.GenerateCoverageInHtmlForDirectory(
506 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
507
508 logging.debug('Finished writing per-directory coverage html reports.')
509
510 def GenerateCoverageInHtmlForDirectory(self, dir_path,
511 per_directory_coverage_summary,
512 per_file_coverage_summary):
513 """Generates coverage html report for a single directory."""
514 html_generator = CoverageReportHtmlGenerator(
515 self.output_dir, self.GetCoverageHtmlReportPathForDirectory(dir_path),
516 'Path')
517
Max Moroz70263d62018-08-21 17:19:31518 for entry_name in os.listdir(self._MapToLocal(dir_path)):
Max Moroz1de68d72018-08-21 13:38:18519 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
520
521 if entry_path in per_file_coverage_summary:
522 entry_html_report_path = self.GetCoverageHtmlReportPathForFile(
523 entry_path)
524 entry_coverage_summary = per_file_coverage_summary[entry_path]
525 elif entry_path in per_directory_coverage_summary:
526 entry_html_report_path = self.GetCoverageHtmlReportPathForDirectory(
527 entry_path)
528 entry_coverage_summary = per_directory_coverage_summary[entry_path]
529 else:
530 # Any file without executable lines shouldn't be included into the
531 # report. For example, OWNER and README.md files.
532 continue
533
534 html_generator.AddLinkToAnotherReport(entry_html_report_path,
535 os.path.basename(entry_path),
536 entry_coverage_summary)
537
538 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
539 html_generator.WriteHtmlCoverageReport(self.no_component_view,
540 self.no_file_view)
541
542 def GenerateDirectoryViewHtmlIndexFile(self):
543 """Generates the html index file for directory view.
544
545 Note that the index file is already generated under src_root_dir, so this
546 file simply redirects to it, and the reason of this extra layer is for
547 structural consistency with other views.
548 """
549 directory_view_index_file_path = self.directory_view_path
550 logging.debug('Generating directory view html index file as: "%s".',
551 directory_view_index_file_path)
552 src_root_html_report_path = self.GetCoverageHtmlReportPathForDirectory(
553 self.src_root_dir)
554 WriteRedirectHtmlFile(directory_view_index_file_path,
555 src_root_html_report_path)
556 logging.debug('Finished generating directory view html index file.')
557
Max Moroz1de68d72018-08-21 13:38:18558 def OverwriteHtmlReportsIndexFile(self):
559 """Overwrites the root index file to redirect to the default view."""
560 html_index_file_path = self.html_index_path
561 directory_view_index_file_path = self.directory_view_path
562 WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
563
564 def CleanUpOutputDir(self):
565 """Perform a cleanup of the output dir."""
566 # Remove the default index.html file produced by llvm-cov.
567 index_path = os.path.join(self.output_dir, INDEX_HTML_FILE)
568 if os.path.exists(index_path):
569 os.remove(index_path)
570
571 def PrepareHtmlReport(self):
Max Moroz1de68d72018-08-21 13:38:18572 per_file_coverage_summary = self.GeneratePerFileCoverageSummary()
573
574 if not self.no_file_view:
575 self.GenerateFileViewHtmlIndexFile(per_file_coverage_summary,
576 self.file_view_path)
577
578 per_directory_coverage_summary = self.CalculatePerDirectoryCoverageSummary(
579 per_file_coverage_summary)
580
581 self.GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
582 per_file_coverage_summary)
583
584 self.GenerateDirectoryViewHtmlIndexFile()
585
586 if not self.no_component_view:
587 per_component_coverage_summary = (
588 self.CalculatePerComponentCoverageSummary(
589 per_directory_coverage_summary))
590 self.GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
591 per_directory_coverage_summary)
592 self.GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
593
594 # The default index file is generated only for the list of source files,
595 # needs to overwrite it to display per directory coverage view by default.
596 self.OverwriteHtmlReportsIndexFile()
597 self.CleanUpOutputDir()
598
599 html_index_file_path = 'file://' + GetFullPath(self.html_index_path)
600 logging.info('Index file for html report is generated as: "%s".',
601 html_index_file_path)
602
603
604def ConfigureLogging(verbose=False, log_file=None):
605 """Configures logging settings for later use."""
606 log_level = logging.DEBUG if verbose else logging.INFO
607 log_format = '[%(asctime)s %(levelname)s] %(message)s'
608 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
609
610
611def GetComponentViewPath(output_dir):
612 """Path to the HTML file for the component view."""
613 return os.path.join(
614 GetCoverageReportRootDirPath(output_dir), COMPONENT_VIEW_INDEX_FILE)
615
616
617def GetCoverageReportRootDirPath(output_dir):
618 """The root directory that contains all generated coverage html reports."""
619 return os.path.join(output_dir, GetHostPlatform())
620
621
Zequan Wu3ccc2e72022-08-26 15:58:22622def GetHtmlFileRootDirPath(output_dir):
623 """The directory that contains llvm-cov generated coverage html reports. """
624 return os.path.join(output_dir, REPORT_DIR)
625
626
Max Moroz1de68d72018-08-21 13:38:18627def GetDirectoryViewPath(output_dir):
628 """Path to the HTML file for the directory view."""
629 return os.path.join(
630 GetCoverageReportRootDirPath(output_dir), DIRECTORY_VIEW_INDEX_FILE)
631
632
633def GetFileViewPath(output_dir):
634 """Path to the HTML file for the file view."""
635 return os.path.join(
636 GetCoverageReportRootDirPath(output_dir), FILE_VIEW_INDEX_FILE)
637
638
639def GetHtmlIndexPath(output_dir):
640 """Path to the main HTML index file."""
641 return os.path.join(GetCoverageReportRootDirPath(output_dir), INDEX_HTML_FILE)
642
643
644def GetFullPath(path):
645 """Return full absolute path."""
Max Moroz00b94112018-09-05 04:08:05646 return os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
Max Moroz1de68d72018-08-21 13:38:18647
648
649def GetHostPlatform():
650 """Returns the host platform.
651
652 This is separate from the target platform/os that coverage is running for.
653 """
654 if sys.platform == 'win32' or sys.platform == 'cygwin':
655 return 'win'
656 if sys.platform.startswith('linux'):
657 return 'linux'
658 else:
659 assert sys.platform == 'darwin'
660 return 'mac'
661
662
663def GetRelativePathToDirectoryOfFile(target_path, base_path):
664 """Returns a target path relative to the directory of base_path.
665
666 This method requires base_path to be a file, otherwise, one should call
667 os.path.relpath directly.
668 """
669 assert os.path.dirname(base_path) != base_path, (
670 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
671 base_path)
672 base_dir = os.path.dirname(base_path)
673 return os.path.relpath(target_path, base_dir)
674
675
Erik Chen283b92c72019-07-22 16:37:39676def GetSharedLibraries(binary_paths, build_dir, otool_path):
Max Moroz1de68d72018-08-21 13:38:18677 """Returns list of shared libraries used by specified binaries."""
678 logging.info('Finding shared libraries for targets (if any).')
679 shared_libraries = []
680 cmd = []
681 shared_library_re = None
682
683 if sys.platform.startswith('linux'):
684 cmd.extend(['ldd'])
685 shared_library_re = re.compile(r'.*\.so[.0-9]*\s=>\s(.*' + build_dir +
686 r'.*\.so[.0-9]*)\s.*')
687 elif sys.platform.startswith('darwin'):
Erik Chen283b92c72019-07-22 16:37:39688 otool = otool_path if otool_path else 'otool'
689 cmd.extend([otool, '-L'])
Max Moroz1de68d72018-08-21 13:38:18690 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
691 else:
692 assert False, 'Cannot detect shared libraries used by the given targets.'
693
694 assert shared_library_re is not None
695
696 cmd.extend(binary_paths)
697 output = subprocess.check_output(cmd).decode('utf-8', 'ignore')
698
699 for line in output.splitlines():
700 m = shared_library_re.match(line)
701 if not m:
702 continue
703
704 shared_library_path = m.group(1)
705 if sys.platform.startswith('darwin'):
706 # otool outputs "@rpath" macro instead of the dirname of the given binary.
707 shared_library_path = shared_library_path.replace('@rpath', build_dir)
708
709 if shared_library_path in shared_libraries:
710 continue
711
712 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
713 'the given target(s) does not '
714 'exist.' % shared_library_path)
715 with open(shared_library_path, 'rb') as f:
716 data = f.read()
717
718 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
719 if b'__llvm_cov' in data:
720 shared_libraries.append(shared_library_path)
721
722 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
723 shared_libraries)
724 logging.info('Finished finding shared libraries for targets.')
725 return shared_libraries
726
727
Max Moroz1de68d72018-08-21 13:38:18728def WriteRedirectHtmlFile(from_html_path, to_html_path):
729 """Writes a html file that redirects to another html file."""
730 to_html_relative_path = GetRelativePathToDirectoryOfFile(
731 to_html_path, from_html_path)
732 content = ("""
733 <!DOCTYPE html>
734 <html>
735 <head>
736 <!-- HTML meta refresh URL redirection -->
737 <meta http-equiv="refresh" content="0; url=%s">
738 </head>
739 </html>""" % to_html_relative_path)
740 with open(from_html_path, 'w') as f:
741 f.write(content)
742
743
744def _CmdSharedLibraries(args):
745 """Handles 'shared_libs' command."""
746 if not args.object:
747 logging.error('No binaries are specified.')
748 return 1
749
Erik Chen283b92c72019-07-22 16:37:39750 library_paths = GetSharedLibraries(args.object, args.build_dir, None)
Max Moroza19fd492018-10-22 17:07:11751 if not library_paths:
Max Moroz1de68d72018-08-21 13:38:18752 return 0
753
754 # Print output in the format that can be passed to llvm-cov tool.
Max Moroza19fd492018-10-22 17:07:11755 output = ' '.join(
756 '-object=%s' % os.path.normpath(path) for path in library_paths)
Max Moroz1de68d72018-08-21 13:38:18757 print(output)
758 return 0
759
760
761def _CmdPostProcess(args):
762 """Handles 'post_process' command."""
763 with open(args.summary_file) as f:
764 summary_data = f.read()
765
766 processor = CoverageReportPostProcessor(
767 args.output_dir,
768 args.src_root_dir,
769 summary_data,
770 no_component_view=True,
Max Moroz70263d62018-08-21 17:19:31771 no_file_view=False,
772 path_equivalence=args.path_equivalence)
Max Moroz1de68d72018-08-21 13:38:18773 processor.PrepareHtmlReport()
774
775
776def Main():
777 parser = argparse.ArgumentParser(
778 'coverage_utils', description='Code coverage utils.')
779 parser.add_argument(
780 '-v',
781 '--verbose',
782 action='store_true',
783 help='Prints additional debug output.')
784
785 subparsers = parser.add_subparsers(dest='command')
786
787 shared_libs_parser = subparsers.add_parser(
788 'shared_libs', help='Detect shared libraries.')
Max Moroz1de68d72018-08-21 13:38:18789 shared_libs_parser.add_argument(
Max Moroz70263d62018-08-21 17:19:31790 '-build-dir', help='Path to the build dir.', required=True)
791 shared_libs_parser.add_argument(
792 '-object',
793 action='append',
794 help='Path to the binary using shared libs.',
795 required=True)
Max Moroz1de68d72018-08-21 13:38:18796
797 post_processing_parser = subparsers.add_parser(
798 'post_process', help='Post process a report.')
799 post_processing_parser.add_argument(
Max Moroz70263d62018-08-21 17:19:31800 '-output-dir', help='Path to the report dir.', required=True)
Max Moroz1de68d72018-08-21 13:38:18801 post_processing_parser.add_argument(
Max Moroz70263d62018-08-21 17:19:31802 '-src-root-dir', help='Path to the src root dir.', required=True)
Max Moroz1de68d72018-08-21 13:38:18803 post_processing_parser.add_argument(
Max Moroz70263d62018-08-21 17:19:31804 '-summary-file', help='Path to the summary file.', required=True)
805 post_processing_parser.add_argument(
806 '-path-equivalence',
807 help='Map the paths in the coverage data to local '
808 'source files path (=<from>,<to>)')
Max Moroz1de68d72018-08-21 13:38:18809
810 args = parser.parse_args()
811 ConfigureLogging(args.verbose)
812
813 if args.command == 'shared_libs':
814 return _CmdSharedLibraries(args)
815 elif args.command == 'post_process':
816 return _CmdPostProcess(args)
Max Moroz70263d62018-08-21 17:19:31817 else:
818 parser.print_help(sys.stderr)
Max Moroz1de68d72018-08-21 13:38:18819
820
821if __name__ == '__main__':
822 sys.exit(Main())