1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205

build / android / gyp / nocompile_test.py [blame]

#!/usr/bin/env python3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Checks that compiling targets in BUILD.gn file fails."""

import argparse
import json
import os
import subprocess
import re
import sys
from util import build_utils

_CHROMIUM_SRC = os.path.normpath(os.path.join(__file__, '..', '..', '..', '..'))
_NINJA_PATH = os.path.join(_CHROMIUM_SRC, 'third_party', 'ninja', 'ninja')

# Relative to _CHROMIUM_SRC
_GN_SRC_REL_PATH = os.path.join('buildtools', 'linux64', 'gn')

# Regex for determining whether compile failed because 'gn gen' needs to be run.
_GN_GEN_REGEX = re.compile(r'ninja: (error|fatal):')


def _raise_command_exception(args, returncode, output):
  """Raises an exception whose message describes a command failure.

    Args:
      args: shell command-line (as passed to subprocess.Popen())
      returncode: status code.
      output: command output.
    Raises:
      a new Exception.
    """
  message = 'Command failed with status {}: {}\n' \
      'Output:-----------------------------------------\n{}\n' \
      '------------------------------------------------\n'.format(
          returncode, args, output)
  raise Exception(message)


def _run_command(args, cwd=None):
  """Runs shell command. Raises exception if command fails."""
  p = subprocess.Popen(args,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.STDOUT,
                       cwd=cwd)
  pout, _ = p.communicate()
  if p.returncode != 0:
    _raise_command_exception(args, p.returncode, pout)


def _run_command_get_failure_output(args):
  """Runs shell command.

  Returns:
      Command output if command fails, None if command succeeds.
  """
  p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  pout, _ = p.communicate()

  if p.returncode == 0:
    return None

  # For Python3 only:
  if isinstance(pout, bytes) and sys.version_info >= (3, ):
    pout = pout.decode('utf-8')
  return '' if pout is None else pout


def _copy_and_append_gn_args(src_args_path, dest_args_path, extra_args):
  """Copies args.gn.

    Args:
      src_args_path: args.gn file to copy.
      dest_args_path: Copy file destination.
      extra_args: Text to append to args.gn after copy.
    """
  with open(src_args_path) as f_in, open(dest_args_path, 'w') as f_out:
    f_out.write(f_in.read())
    f_out.write('\n')
    f_out.write('\n'.join(extra_args))


def _find_regex_in_test_failure_output(test_output, regex):
  """Searches for regex in test output.

    Args:
      test_output: test output.
      regex: regular expression to search for.
    Returns:
      Whether the regular expression was found in the part of the test output
      after the 'FAILED' message.
  """
  if test_output is None:
    return False

  failed_index = test_output.find('FAILED')
  if failed_index < 0:
    return False

  failure_message = test_output[failed_index:]
  if regex.find('\n') >= 0:
    return re.search(regex, failure_message)
  return _search_regex_in_list(failure_message.split('\n'), regex)


def _search_regex_in_list(value, regex):
  for line in value:
    if re.search(regex, line):
      return True
  return False


def _do_build_get_failure_output(gn_path, gn_cmd, options):
  # Extract directory from test target. As all of the test targets are declared
  # in the same BUILD.gn file, it does not matter which test target is used.
  target_dir = gn_path.rsplit(':', 1)[0]

  if gn_cmd is not None:
    gn_args = [
        _GN_SRC_REL_PATH, '--root-target=' + target_dir, gn_cmd,
        os.path.relpath(options.out_dir, _CHROMIUM_SRC)
    ]
    _run_command(gn_args, cwd=_CHROMIUM_SRC)

  ninja_args = [_NINJA_PATH, '-C', options.out_dir, gn_path]
  return _run_command_get_failure_output(ninja_args)


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('--gn-args-path',
                      required=True,
                      help='Path to args.gn file.')
  parser.add_argument('--test-configs-path',
                      required=True,
                      help='Path to file with test configurations')
  parser.add_argument('--out-dir',
                      required=True,
                      help='Path to output directory to use for compilation.')
  parser.add_argument('--stamp', help='Path to touch.')
  options = parser.parse_args()

  with open(options.test_configs_path) as f:
    # Escape '\' in '\.' now. This avoids having to do the escaping in the test
    # specification.
    config_text = f.read().replace(r'\.', r'\\.')
    test_configs = json.loads(config_text)

  if not os.path.exists(options.out_dir):
    os.makedirs(options.out_dir)

  out_gn_args_path = os.path.join(options.out_dir, 'args.gn')
  extra_gn_args = [
      'enable_android_nocompile_tests = true',
      'treat_warnings_as_errors = true',
      # RBE does not work with non-standard output directories.
      'use_remoteexec = false',
  ]
  _copy_and_append_gn_args(options.gn_args_path, out_gn_args_path,
                           extra_gn_args)

  ran_gn_gen = False
  did_clean_build = False
  error_messages = []
  for config in test_configs:
    # Strip leading '//'
    gn_path = config['target'][2:]
    expect_regex = config['expect_regex']

    test_output = _do_build_get_failure_output(gn_path, None, options)

    # 'gn gen' takes > 1s to run. Only run 'gn gen' if it is needed for compile.
    if (test_output
        and _search_regex_in_list(test_output.split('\n'), _GN_GEN_REGEX)):
      assert not ran_gn_gen
      ran_gn_gen = True
      test_output = _do_build_get_failure_output(gn_path, 'gen', options)

    if (not _find_regex_in_test_failure_output(test_output, expect_regex)
        and not did_clean_build):
      # Ensure the failure is not due to incremental build.
      did_clean_build = True
      test_output = _do_build_get_failure_output(gn_path, 'clean', options)

    if not _find_regex_in_test_failure_output(test_output, expect_regex):
      if test_output is None:
        # Purpose of quotes at beginning of message is to make it clear that
        # "Compile successful." is not a compiler log message.
        test_output = '""\nCompile successful.'
      error_message = '//{} failed.\nExpected compile output pattern:\n'\
          '{}\nActual compile output:\n{}'.format(
              gn_path, expect_regex, test_output)
      error_messages.append(error_message)

  if error_messages:
    raise Exception('\n'.join(error_messages))

  if options.stamp:
    build_utils.Touch(options.stamp)


if __name__ == '__main__':
  main()