chromium/tools/mb/lib/validation.py

# 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.
"""Validation functions for the Meta-Build config file"""

import ast
import collections
import difflib
import json
import os
import re


def GetAllConfigs(builder_groups):
  """Build a list of all of the configs referenced by builders.
  """
  all_configs = {}
  for builder_group in builder_groups:
    for config in builder_groups[builder_group].values():
      if isinstance(config, dict):
        for c in config.values():
          all_configs[c] = builder_group
      else:
        all_configs[config] = builder_group
  return all_configs


def CheckAllConfigsAndMixinsReferenced(errs, all_configs, configs, mixins):
  """Check that every actual config is actually referenced."""
  for config in configs:
    if not config in all_configs:
      errs.append('Unused config "%s".' % config)

  # Figure out the whole list of mixins, and check that every mixin
  # listed by a config or another mixin actually exists.
  referenced_mixins = set()
  for config, mixin_names in configs.items():
    for mixin in mixin_names:
      if not mixin in mixins:
        errs.append(
            'Unknown mixin "%s" referenced by config "%s".' % (mixin, config))
      referenced_mixins.add(mixin)

  for mixin in mixins:
    for sub_mixin in mixins[mixin].get('mixins', []):
      if not sub_mixin in mixins:
        errs.append(
            'Unknown mixin "%s" referenced by mixin "%s".' % (sub_mixin, mixin))
      referenced_mixins.add(sub_mixin)

  # Check that every mixin defined is actually referenced somewhere.
  for mixin in mixins:
    if not mixin in referenced_mixins:
      errs.append('Unreferenced mixin "%s".' % mixin)

  return errs


def _GetConfigsByBuilder(builder_groups):
  """Builds a mapping from buildername -> [config]

    Args
      builder_groups: the builder_group's dict from mb_config.pyl
    """

  result = collections.defaultdict(list)
  for builder_group in builder_groups.values():
    for buildername, builder in builder_group.items():
      result[buildername].append(builder)

  return result


def CheckDuplicateConfigs(errs, config_pool, mixin_pool, grouping,
                          flatten_config):
  """Check for duplicate configs.

  Evaluate all configs, and see if, when
  evaluated, differently named configs are the same.
  """
  evaled_to_source = collections.defaultdict(set)
  for group, builders in grouping.items():
    for builder in builders:
      config = grouping[group][builder]
      if not config:
        continue

      if isinstance(config, dict):
        # Ignore for now
        continue

      if config.startswith('//'):
        args = config
      else:
        flattened_config = flatten_config(config_pool, mixin_pool, config)
        args = flattened_config['gn_args']
        if 'error' in args:
          continue
        # Force the args_file into consideration when testing for duplicate
        # configs.
        args_file = flattened_config['args_file']
        if args_file:
          args += ' args_file=%s' % args_file

      evaled_to_source[args].add(config)

  for v in evaled_to_source.values():
    if len(v) != 1:
      errs.append(
          'Duplicate configs detected. When evaluated fully, the '
          'following configs are all equivalent: %s. Please '
          'consolidate these configs into only one unique name per '
          'configuration value.' % (', '.join(sorted('%r' % val for val in v))))


def CheckDebugDCheckOrOfficial(errs, gn_args, builder_group, builder, phase):
  # TODO(crbug.com/40189120): Figure out how to check this properly
  # for simplechrome-based bots.
  if gn_args.get('is_chromeos_device'):
    return

  if ((gn_args.get('is_debug') == True)
      or (gn_args.get('is_official_build') == True)
      or ('dcheck_always_on' in gn_args)):
    return

  if phase:
    errs.append('Phase "%s" of builder "%s" on %s did not specify '
                'one of is_debug=true, is_official_build=true, or '
                'dcheck_always_on=(true|false).' %
                (phase, builder, builder_group))
  else:
    errs.append('Builder "%s" on %s did not specify '
                'one of is_debug=true, is_official_build=true, or '
                'dcheck_always_on=(true|false).' % (builder, builder_group))


def CheckExpectations(mbw, jsonish_blob, expectations_dir):
  """Checks that the expectation files match the config file.

  Returns: True if expectations are up-to-date. False otherwise.
  """
  # Assert number of builder_groups == number of expectation files.
  if len(mbw.ListDir(expectations_dir)) != len(jsonish_blob):
    return False
  for builder_group, builders in jsonish_blob.items():
    if not mbw.Exists(os.path.join(expectations_dir, builder_group + '.json')):
      return False  # No expecation file for the builder_group.
    expectation = mbw.ReadFile(os.path.join(expectations_dir,
                                            builder_group + '.json'))
    builders_json = json.dumps(builders,
                               indent=2,
                               sort_keys=True,
                               separators=(',', ': '))
    if builders_json != expectation:
      return False  # Builders' expectation out of sync.
  return True


def CheckKeyOrdering(errs, groups, configs, mixins):
  # Check ordering of groups within "builder_groups".
  group_names = list(groups.keys())
  sorted_group_names = sorted(group_names)
  if group_names != sorted_group_names:
    errs.append('\nThe keys in "builder_groups" are not sorted:')
    errs.extend(difflib.context_diff(group_names, sorted_group_names))

  # Check ordering of builders within each group.
  for group, builders in groups.items():
    builder_names = list(builders.keys())
    sorted_builder_names = sorted(builder_names)
    if builder_names != sorted_builder_names:
      errs.append('\nThe builders in group "%s" are not sorted:' % group)
      errs.extend(difflib.context_diff(builder_names, sorted_builder_names))

  # Check ordering of configs names, but don't bother checking the ordering
  # of mixins within a config.
  config_names = list(configs.keys())
  sorted_config_names = sorted(config_names)
  if config_names != sorted_config_names:
    errs.append('\nThe config names are not sorted:')
    errs.extend(difflib.context_diff(config_names, sorted_config_names))

  # Check ordering of mixin names.
  mixin_names = list(mixins.keys())
  sorted_mixin_names = sorted(mixin_names)
  if mixin_names != sorted_mixin_names:
    errs.append('\nThe mixin names are not sorted:')
    errs.extend(difflib.context_diff(mixin_names, sorted_mixin_names))