Make profraw merge error resilient in code coverage script.

[email protected]

Bug: 826305
Change-Id: I3e3c196ad0f792a3aa94b3cb31fbe8d8df8fb5e1
Reviewed-on: https://chromium-review.googlesource.com/1044952
Commit-Queue: Abhishek Arya <[email protected]>
Reviewed-by: Max Moroz <[email protected]>
Cr-Commit-Position: refs/heads/master@{#556217}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index 843bacbc..314df30 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -130,6 +130,9 @@
 # directly, call _GetBuildArgs instead.
 _BUILD_ARGS = None
 
+# Retry failed merges.
+MERGE_RETRIES = 3
+
 
 class _CoverageSummary(object):
   """Encapsulates coverage summary representation."""
@@ -778,15 +781,16 @@
     A relative path to the generated profdata file.
   """
   _BuildTargets(targets, jobs_count)
-  profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
+  target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
       targets, commands)
-  profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
-      profraw_file_paths)
+  coverage_profdata_file_path = (
+      _CreateCoverageProfileDataFromTargetProfDataFiles(
+          target_profdata_file_paths))
 
-  for profraw_file_path in profraw_file_paths:
-    os.remove(profraw_file_path)
+  for target_profdata_file_path in target_profdata_file_paths:
+    os.remove(target_profdata_file_path)
 
-  return profdata_file_path
+  return coverage_profdata_file_path
 
 
 def _BuildTargets(targets, jobs_count):
@@ -822,7 +826,7 @@
   logging.debug('Finished building %s', str(targets))
 
 
-def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
+def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
   """Runs commands and returns the relative paths to the profraw data files.
 
   Args:
@@ -839,47 +843,65 @@
     if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
       os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
 
-  profraw_file_paths = []
+  profdata_file_paths = []
 
   # Run all test targets to generate profraw data files.
   for target, command in zip(targets, commands):
     output_file_name = os.extsep.join([target + '_output', 'txt'])
     output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
-    logging.info('Running command: "%s", the output is redirected to "%s"',
-                 command, output_file_path)
 
-    if _IsIOSCommand(command):
-      # On iOS platform, due to lack of write permissions, profraw files are
-      # generated outside of the OUTPUT_DIR, and the exact paths are contained
-      # in the output of the command execution.
-      output = _ExecuteIOSCommand(target, command)
-      profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
-    else:
-      # On other platforms, profraw files are generated inside the OUTPUT_DIR.
-      output = _ExecuteCommand(target, command)
+    profdata_file_path = None
+    for _ in xrange(MERGE_RETRIES):
+      logging.info('Running command: "%s", the output is redirected to "%s"',
+                   command, output_file_path)
 
-    with open(output_file_path, 'w') as output_file:
-      output_file.write(output)
+      if _IsIOSCommand(command):
+        # On iOS platform, due to lack of write permissions, profraw files are
+        # generated outside of the OUTPUT_DIR, and the exact paths are contained
+        # in the output of the command execution.
+        output = _ExecuteIOSCommand(target, command)
+      else:
+        # On other platforms, profraw files are generated inside the OUTPUT_DIR.
+        output = _ExecuteCommand(target, command)
+
+      with open(output_file_path, 'w') as output_file:
+        output_file.write(output)
+
+      profraw_file_paths = []
+      if _IsIOS():
+        profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
+      else:
+        for file_or_dir in os.listdir(OUTPUT_DIR):
+          if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
+            profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
+
+      assert profraw_file_paths, (
+          'Running target %s failed to generate any profraw data file, '
+          'please make sure the binary exists and is properly '
+          'instrumented.' % target)
+
+      try:
+        profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
+            target, profraw_file_paths)
+        break
+      except Exception:
+        print('Retrying...')
+      finally:
+        # Remove profraw files now so that they are not used in next iteration.
+        for profraw_file_path in profraw_file_paths:
+          os.remove(profraw_file_path)
+
+    assert profdata_file_path, (
+        'Failed to merge target %s profraw files after %d retries. '
+        'Please file a bug with command you used, commit position and args.gn '
+        'config here: '
+        'https://bugs.chromium.org/p/chromium/issues/entry?'
+        'components=Tools%%3ECodeCoverage'% (target, MERGE_RETRIES))
+    profdata_file_paths.append(profdata_file_path)
 
   logging.debug('Finished executing the test commands')
 
-  if _IsIOS():
-    return profraw_file_paths
-
-  for file_or_dir in os.listdir(OUTPUT_DIR):
-    if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
-      profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
-
-  # Assert one target/command generates at least one profraw data file.
-  for target in targets:
-    assert any(
-        os.path.basename(profraw_file).startswith(target)
-        for profraw_file in profraw_file_paths), (
-            'Running target: %s failed to generate any profraw data file, '
-            'please make sure the binary exists and is properly instrumented.' %
-            target)
-
-  return profraw_file_paths
+  return profdata_file_paths
 
 
 def _ExecuteCommand(target, command):
@@ -980,34 +1002,71 @@
                  'Please refer to base/test/test_support_ios.mm for example.')
 
 
-def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
-  """Returns a relative path to the profdata file by merging profraw data files.
+def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
+  """Returns a relative path to coverage profdata file by merging target
+  profdata files.
 
   Args:
-    profraw_file_paths: A list of relative paths to the profraw data files that
-                        are to be merged.
+    profdata_file_paths: A list of relative paths to the profdata data files
+                         that are to be merged.
 
   Returns:
-    A relative path to the generated profdata file.
+    A relative path to the merged coverage profdata file.
 
   Raises:
-    CalledProcessError: An error occurred merging profraw data files.
+    CalledProcessError: An error occurred merging profdata files.
   """
   logging.info('Creating the coverage profile data file')
-  logging.debug('Merging profraw files to create profdata file')
+  logging.debug(
+      'Merging target profdata files to create coverage profdata file')
   profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
   try:
     subprocess_cmd = [
         LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
     ]
+    subprocess_cmd.extend(profdata_file_paths)
+    subprocess.check_call(subprocess_cmd)
+  except subprocess.CalledProcessError as error:
+    print('Failed to merge target profdata files to create coverage profdata. '
+          'Try again.')
+    raise error
+
+  logging.debug('Finished merging target profdata files')
+  logging.info('Code coverage profile data is created as: %s',
+               profdata_file_path)
+  return profdata_file_path
+
+
+def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
+  """Returns a relative path to target profdata file by merging target
+  profraw files.
+
+  Args:
+    profraw_file_paths: A list of relative paths to the profdata data files
+                         that are to be merged.
+
+  Returns:
+    A relative path to the merged coverage profdata file.
+
+  Raises:
+    CalledProcessError: An error occurred merging profdata files.
+  """
+  logging.info('Creating target profile data file')
+  logging.debug('Merging target profraw files to create target profdata file')
+  profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
+
+  try:
+    subprocess_cmd = [
+        LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
+    ]
     subprocess_cmd.extend(profraw_file_paths)
     subprocess.check_call(subprocess_cmd)
   except subprocess.CalledProcessError as error:
-    print('Failed to merge profraw files to create profdata file')
+    print('Failed to merge target profraw files to create target profdata.')
     raise error
 
-  logging.debug('Finished merging profraw files')
-  logging.info('Code coverage profile data is created as: %s',
+  logging.debug('Finished merging target profraw files')
+  logging.info('Target %s profile data is created as: %s', target,
                profdata_file_path)
   return profdata_file_path
 
@@ -1309,7 +1368,11 @@
       '-l', '--log_file', type=str, help='Redirects logs to a file.')
 
   arg_parser.add_argument(
-      'targets', nargs='+', help='The names of the test targets to run.')
+      'targets',
+      nargs='+',
+      help='The names of the test targets to run. If multiple run commands are '
+      'specified using the -c/--command option, then the order of targets and '
+      'commands must match, otherwise coverage generation will fail.')
 
   args = arg_parser.parse_args()
   return args