tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

nocompile_test.py (6769B)


      1 #!/usr/bin/env python3
      2 # Copyright 2020 The Chromium Authors
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 """Checks that compiling targets in BUILD.gn file fails."""
      6 
      7 import argparse
      8 import json
      9 import os
     10 import subprocess
     11 import re
     12 import sys
     13 from util import build_utils
     14 
     15 _CHROMIUM_SRC = os.path.normpath(os.path.join(__file__, '..', '..', '..', '..'))
     16 _NINJA_PATH = os.path.join(_CHROMIUM_SRC, 'third_party', 'ninja', 'ninja')
     17 
     18 # Relative to _CHROMIUM_SRC
     19 _GN_SRC_REL_PATH = os.path.join('buildtools', 'linux64', 'gn')
     20 
     21 # Regex for determining whether compile failed because 'gn gen' needs to be run.
     22 _GN_GEN_REGEX = re.compile(r'ninja: (error|fatal):')
     23 
     24 
     25 def _raise_command_exception(args, returncode, output):
     26  """Raises an exception whose message describes a command failure.
     27 
     28    Args:
     29      args: shell command-line (as passed to subprocess.Popen())
     30      returncode: status code.
     31      output: command output.
     32    Raises:
     33      a new Exception.
     34    """
     35  message = 'Command failed with status {}: {}\n' \
     36      'Output:-----------------------------------------\n{}\n' \
     37      '------------------------------------------------\n'.format(
     38          returncode, args, output)
     39  raise Exception(message)
     40 
     41 
     42 def _run_command(args, cwd=None):
     43  """Runs shell command. Raises exception if command fails."""
     44  p = subprocess.Popen(args,
     45                       stdout=subprocess.PIPE,
     46                       stderr=subprocess.STDOUT,
     47                       cwd=cwd)
     48  pout, _ = p.communicate()
     49  if p.returncode != 0:
     50    _raise_command_exception(args, p.returncode, pout)
     51 
     52 
     53 def _run_command_get_failure_output(args):
     54  """Runs shell command.
     55 
     56  Returns:
     57      Command output if command fails, None if command succeeds.
     58  """
     59  p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     60  pout, _ = p.communicate()
     61 
     62  if p.returncode == 0:
     63    return None
     64 
     65  # For Python3 only:
     66  if isinstance(pout, bytes) and sys.version_info >= (3, ):
     67    pout = pout.decode('utf-8')
     68  return '' if pout is None else pout
     69 
     70 
     71 def _copy_and_append_gn_args(src_args_path, dest_args_path, extra_args):
     72  """Copies args.gn.
     73 
     74    Args:
     75      src_args_path: args.gn file to copy.
     76      dest_args_path: Copy file destination.
     77      extra_args: Text to append to args.gn after copy.
     78    """
     79  with open(src_args_path) as f_in, open(dest_args_path, 'w') as f_out:
     80    f_out.write(f_in.read())
     81    f_out.write('\n')
     82    f_out.write('\n'.join(extra_args))
     83 
     84 
     85 def _find_regex_in_test_failure_output(test_output, regex):
     86  """Searches for regex in test output.
     87 
     88    Args:
     89      test_output: test output.
     90      regex: regular expression to search for.
     91    Returns:
     92      Whether the regular expression was found in the part of the test output
     93      after the 'FAILED' message.
     94  """
     95  if test_output is None:
     96    return False
     97 
     98  failed_index = test_output.find('FAILED')
     99  if failed_index < 0:
    100    return False
    101 
    102  failure_message = test_output[failed_index:]
    103  if regex.find('\n') >= 0:
    104    return re.search(regex, failure_message)
    105  return _search_regex_in_list(failure_message.split('\n'), regex)
    106 
    107 
    108 def _search_regex_in_list(value, regex):
    109  for line in value:
    110    if re.search(regex, line):
    111      return True
    112  return False
    113 
    114 
    115 def _do_build_get_failure_output(gn_path, gn_cmd, options):
    116  # Extract directory from test target. As all of the test targets are declared
    117  # in the same BUILD.gn file, it does not matter which test target is used.
    118  target_dir = gn_path.rsplit(':', 1)[0]
    119 
    120  if gn_cmd is not None:
    121    gn_args = [
    122        _GN_SRC_REL_PATH, '--root-target=' + target_dir, gn_cmd,
    123        os.path.relpath(options.out_dir, _CHROMIUM_SRC)
    124    ]
    125    _run_command(gn_args, cwd=_CHROMIUM_SRC)
    126 
    127  ninja_args = [_NINJA_PATH, '-C', options.out_dir, gn_path]
    128  return _run_command_get_failure_output(ninja_args)
    129 
    130 
    131 def main():
    132  parser = argparse.ArgumentParser()
    133  parser.add_argument('--gn-args-path',
    134                      required=True,
    135                      help='Path to args.gn file.')
    136  parser.add_argument('--test-configs-path',
    137                      required=True,
    138                      help='Path to file with test configurations')
    139  parser.add_argument('--out-dir',
    140                      required=True,
    141                      help='Path to output directory to use for compilation.')
    142  parser.add_argument('--stamp', help='Path to touch.')
    143  options = parser.parse_args()
    144 
    145  with open(options.test_configs_path) as f:
    146    # Escape '\' in '\.' now. This avoids having to do the escaping in the test
    147    # specification.
    148    config_text = f.read().replace(r'\.', r'\\.')
    149    test_configs = json.loads(config_text)
    150 
    151  if not os.path.exists(options.out_dir):
    152    os.makedirs(options.out_dir)
    153 
    154  out_gn_args_path = os.path.join(options.out_dir, 'args.gn')
    155  extra_gn_args = [
    156      'enable_android_nocompile_tests = true',
    157      'treat_warnings_as_errors = true',
    158      # RBE does not work with non-standard output directories.
    159      'use_remoteexec = false',
    160      'use_reclient = false',
    161      # Do not use fast_local_dev_server.py.
    162      'android_static_analysis = "on"',
    163  ]
    164  _copy_and_append_gn_args(options.gn_args_path, out_gn_args_path,
    165                           extra_gn_args)
    166 
    167  ran_gn_gen = False
    168  did_clean_build = False
    169  error_messages = []
    170  for config in test_configs:
    171    # Strip leading '//'
    172    gn_path = config['target'][2:]
    173    expect_regex = config['expect_regex']
    174 
    175    test_output = _do_build_get_failure_output(gn_path, None, options)
    176 
    177    # 'gn gen' takes > 1s to run. Only run 'gn gen' if it is needed for compile.
    178    if (test_output
    179        and _search_regex_in_list(test_output.split('\n'), _GN_GEN_REGEX)):
    180      assert not ran_gn_gen
    181      ran_gn_gen = True
    182      test_output = _do_build_get_failure_output(gn_path, 'gen', options)
    183 
    184    if (not _find_regex_in_test_failure_output(test_output, expect_regex)
    185        and not did_clean_build):
    186      # Ensure the failure is not due to incremental build.
    187      did_clean_build = True
    188      test_output = _do_build_get_failure_output(gn_path, 'clean', options)
    189 
    190    if not _find_regex_in_test_failure_output(test_output, expect_regex):
    191      if test_output is None:
    192        # Purpose of quotes at beginning of message is to make it clear that
    193        # "Compile successful." is not a compiler log message.
    194        test_output = '""\nCompile successful.'
    195      error_message = '//{} failed.\nExpected compile output pattern:\n'\
    196          '{}\nActual compile output:\n{}'.format(
    197              gn_path, expect_regex, test_output)
    198      error_messages.append(error_message)
    199 
    200  if error_messages:
    201    raise Exception('\n'.join(error_messages))
    202 
    203  if options.stamp:
    204    build_utils.Touch(options.stamp)
    205 
    206 
    207 if __name__ == '__main__':
    208  main()