Support exporting coverage data in lcov format

TEST=coverage.py --format lcov works and coverage_test.py passes
BUG=b:196885721

Change-Id: Ia37165fb20b9a7e0976f1ec51f1e1d9b5e2d8fde
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3098794
Reviewed-by: Yuke Liao <[email protected]>
Commit-Queue: Akekawit Jitprasert <[email protected]>
Cr-Commit-Position: refs/heads/main@{#914805}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index dda605f..9c519a9 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -124,6 +124,9 @@
 # Name of the file with summary information generated by llvm-cov export.
 SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
 
+# Name of the coverage file in lcov format generated by llvm-cov export.
+LCOV_FILE_NAME = os.extsep.join(['coverage', 'lcov'])
+
 # Build arg required for generating code coverage data.
 CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
 
@@ -248,6 +251,40 @@
   logging.debug('Finished running "llvm-cov show" command.')
 
 
+def _GeneratePerFileLineByLineCoverageInLcov(binary_paths, profdata_file_path, filters,
+                                             ignore_filename_regex):
+  """Generates per file line-by-line coverage using "llvm-cov export".
+
+  Args:
+    binary_paths: A list of paths to the instrumented binaries.
+    profdata_file_path: A path to the profdata file.
+    filters: A list of directories and files to get coverage for.
+    ignore_filename_regex: A regular expression for skipping source code files
+                           with certain file paths.
+  """
+  logging.debug('Generating per file line by line coverage reports using '
+                '"llvm-cov export" command.')
+  for path in binary_paths:
+    if not os.path.exists(path):
+      logging.error("Binary %s does not exist", path)
+  subprocess_cmd = [
+      LLVM_COV_PATH, 'export', '-format=lcov',
+      '-instr-profile=' + profdata_file_path, binary_paths[0]
+  ]
+  subprocess_cmd.extend(
+      ['-object=' + binary_path for binary_path in binary_paths[1:]])
+  _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
+  subprocess_cmd.extend(filters)
+  if ignore_filename_regex:
+    subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
+
+  # Write output on the disk to be used by code coverage bot.
+  with open(_GetLcovFilePath(), 'w') as f:
+    subprocess.check_call(subprocess_cmd, stdout=f)
+
+  logging.debug('Finished running "llvm-cov export" command.')
+
+
 def _GetLogsDirectoryPath():
   """Path to the logs directory."""
   return os.path.join(
@@ -268,6 +305,13 @@
       SUMMARY_FILE_NAME)
 
 
+def _GetLcovFilePath():
+  """The LCOV file that contains coverage data written by llvm-cov export."""
+  return os.path.join(
+      coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
+      LCOV_FILE_NAME)
+
+
 def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
   """Builds and runs target to generate the coverage profile data.
 
@@ -973,8 +1017,8 @@
       '--format',
       type=str,
       default='html',
-      help='Output format of the "llvm-cov show" command. The supported '
-      'formats are "text" and "html".')
+      help='Output format of the "llvm-cov show/export" command. The '
+      'supported formats are "text", "html" and "lcov".')
 
   arg_parser.add_argument(
       '-v',
@@ -1083,14 +1127,21 @@
     binary_paths.extend(
         coverage_utils.GetSharedLibraries(binary_paths, BUILD_DIR, otool_path))
 
-  assert args.format == 'html' or args.format == 'text', (
-      '%s is not a valid output format for "llvm-cov show". Only "text" and '
-      '"html" formats are supported.' % (args.format))
+  assert args.format in ['html', 'lcov', 'text'], (
+      '%s is not a valid output format for "llvm-cov show/export". Only '
+      '"text", "html" and "lcov" formats are supported.' % (args.format))
   logging.info('Generating code coverage report in %s (this can take a while '
                'depending on size of target!).' % (args.format))
   per_file_summary_data = _GeneratePerFileCoverageSummary(
       binary_paths, profdata_file_path, absolute_filter_paths,
       args.ignore_filename_regex)
+
+  if args.format == 'lcov':
+    _GeneratePerFileLineByLineCoverageInLcov(
+      binary_paths, profdata_file_path, absolute_filter_paths,
+      args.ignore_filename_regex)
+    return
+
   _GeneratePerFileLineByLineCoverageInFormat(
       binary_paths, profdata_file_path, absolute_filter_paths,
       args.ignore_filename_regex, args.format)