[Code Coverage] Add support for component build + update documentation.

Bug: 831939
Change-Id: I2804796894045dc69aa8309aa3ab34e6c80b38d7
Reviewed-on: https://chromium-review.googlesource.com/1010464
Commit-Queue: Max Moroz <[email protected]>
Reviewed-by: Yuke Liao <[email protected]>
Reviewed-by: Abhishek Arya <[email protected]>
Cr-Commit-Position: refs/heads/master@{#553211}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index a6a1c65..6d8b6e4 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -313,6 +313,50 @@
       html_file.write(html_header + html_table + html_footer)
 
 
+def _GetSharedLibraries(binary_paths):
+  """Returns set of shared libraries used by specified binaries."""
+  libraries = set()
+  cmd = []
+  shared_library_re = None
+
+  if sys.platform.startswith('linux'):
+    cmd.extend(['ldd'])
+    shared_library_re = re.compile(
+        r'.*\.so\s=>\s(.*' + BUILD_DIR + '.*\.so)\s.*')
+  elif sys.platform.startswith('darwin'):
+    cmd.extend(['otool', '-L'])
+    shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
+  else:
+    assert False, ('Cannot detect shared libraries used by the given targets.')
+
+  assert shared_library_re is not None
+
+  cmd.extend(binary_paths)
+  output = subprocess.check_output(cmd)
+
+  for line in output.splitlines():
+    m = shared_library_re.match(line)
+    if not m:
+      continue
+
+    shared_library_path = m.group(1)
+    if sys.platform.startswith('darwin'):
+      # otool outputs "@rpath" macro instead of the dirname of the given binary.
+      shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
+
+    assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
+                                                 'the given target(s) does not '
+                                                 'exist.' % shared_library_path)
+    with open(shared_library_path) as f:
+      data = f.read()
+
+    # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
+    if '__llvm_cov' in data:
+      libraries.add(shared_library_path)
+
+  return list(libraries)
+
+
 def _GetHostPlatform():
   """Returns the host platform.
 
@@ -823,23 +867,25 @@
   """Runs a single command and generates a profraw data file."""
   # Per Clang "Source-based Code Coverage" doc:
   #
-  # "%p" expands out to the process ID.
+  # "%p" expands out to the process ID. It's not used by this scripts due to:
+  # 1) If a target program spawns too many processess, it may exhaust all disk
+  #    space available. For example, unit_tests writes thousands of .profraw
+  #    files each of size 1GB+.
+  # 2) If a target binary uses shared libraries, coverage profile data for them
+  #    will be missing, resulting in incomplete coverage reports.
   #
   # "%Nm" expands out to the instrumented binary's signature. When this pattern
   # is specified, the runtime creates a pool of N raw profiles which are used
   # for on-line profile merging. The runtime takes care of selecting a raw
   # profile from the pool, locking it, and updating it before the program exits.
-  # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
   # N must be between 1 and 9. The merge pool specifier can only occur once per
   # filename pattern.
   #
-  # "%p" is used when tests run in single process, however, it can't be used for
-  # multi-process because each process produces an intermediate dump, which may
-  # consume hundreds of gigabytes of disk space.
+  # "%1m" is used when tests run in single process, such as fuzz targets.
   #
-  # For "%Nm", 4 is chosen because it creates some level of parallelism, but
-  # it's not too big to consume too much computing resource or disk space.
-  profile_pattern_string = '%p' if _IsFuzzerTarget(target) else '%4m'
+  # For other cases, "%4m" is chosen as it creates some level of parallelism,
+  # but it's not too big to consume too much computing resource or disk space.
+  profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
   expected_profraw_file_name = os.extsep.join(
       [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
   expected_profraw_file_path = os.path.join(OUTPUT_DIR,
@@ -1262,6 +1308,7 @@
 
   logging.info('Generating code coverage report in html (this can take a while '
                'depending on size of target!)')
+  binary_paths.extend(_GetSharedLibraries(binary_paths))
   per_file_coverage_summary = _GeneratePerFileCoverageSummary(
       binary_paths, profdata_file_path, absolute_filter_paths,
       args.ignore_filename_regex)