chromium/tools/metrics/actions/action_utils.py

# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""A utility module for parsing and applying action suffixes in actions.xml.

Note: There is a copy of this file used internally by the UMA processing
infrastructure. Any changes to this file should also be done (manually) to the
internal copy. Please contact tools/metrics/OWNERS for more details.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function


class Error(Exception):
  pass


class UndefinedActionItemError(Error):
  pass


class InvalidOrderingAttributeError(Error):
  pass


class SuffixNameEmptyError(Error):
  pass


class InvalidAffecteddActionNameError(Error):
  pass


class Action(object):
  """Represents Chrome user action.

  Attributes:
    name: name of the action.
    description: description of the action.
    owners: list of action owners
    not_user_triggered: if action is not user triggered
    obsolete: explanation on why user action is not being used anymore
    from_suffix: If True, this action was computed via a suffix.
  """

  def __init__(self,
               name,
               description,
               owners,
               not_user_triggered=False,
               obsolete=None,
               from_suffix=False):
    self.name = name
    self.description = description
    self.owners = owners
    self.not_user_triggered = not_user_triggered
    self.obsolete = obsolete
    self.from_suffix = from_suffix


class Suffix(object):
  """Action suffix in actions.xml.

  Attributes:
    name: name of the suffix.
    description: description of the suffix.
    separator: the separator between affected action name and suffix name.
    ordering: 'suffix' or 'prefix'. if set to prefix, suffix name will be
              inserted after the first dot separator of affected action name.
  """

  def __init__(self, name, description, separator, ordering):
    if not name:
      raise SuffixNameEmptyError('Suffix name cannot be empty.')

    if ordering != 'suffix' and ordering != 'prefix':
      raise InvalidOrderingAttributeError("Ordering has to be either 'prefix' "
                                          "or 'suffix'.")

    self.name = name
    self.description = description
    self.separator = separator
    self.ordering = ordering

  def __repr__(self):
    return '<%s, %s, %s, %s>' % (self.name, self.description, self.separator,
                                 self.ordering)


def CreateActionsFromSuffixes(actions_dict, action_suffix_nodes):
  """Creates new actions from suffixes and adds them to actions_dict.

  Args:
    actions_dict: dict of existing action name to Action object.
    action_suffix_nodes: a list of action-suffix nodes

  Returns:
    A dictionary of action name to list of Suffix objects for that action.

  Raises:
    UndefinedActionItemError: if an affected action name can't be found
  """
  action_to_suffixes_dict = _CreateActionToSuffixesDict(action_suffix_nodes)

  # Some actions in action_to_suffixes_dict keys may yet to be created.
  # Therefore, while new actions can be created and added to the existing
  # actions keep calling _CreateActionsFromSuffixes.
  while _CreateActionsFromSuffixes(actions_dict, action_to_suffixes_dict):
    pass

  # If action_to_suffixes_dict is not empty by the end, we have missing actions.
  if action_to_suffixes_dict:
    raise UndefinedActionItemError('Following actions are missing: %s.' %
                                   (list(action_to_suffixes_dict.keys())))


def _CreateActionToSuffixesDict(action_suffix_nodes):
  """Creates a dict of action name to list of Suffix objects for that action.

  Args:
    action_suffix_nodes: a list of action-suffix nodes

  Returns:
    A dictionary of action name to list of Suffix objects for that action.
  """
  action_to_suffixes_dict = {}
  for action_suffix_node in action_suffix_nodes:
    separator = _GetAttribute(action_suffix_node, 'separator', '_')
    ordering = _GetAttribute(action_suffix_node, 'ordering', 'suffix')
    suffixes = [Suffix(suffix_node.getAttribute('name'),
                       suffix_node.getAttribute('label'),
                       separator, ordering) for suffix_node in
                action_suffix_node.getElementsByTagName('suffix')]

    action_nodes = action_suffix_node.getElementsByTagName('affected-action')
    for action_node in action_nodes:
      action_name = action_node.getAttribute('name')
      # If <affected-action> has <with-suffix> child nodes, only those suffixes
      # should be used with that action. filter the list of suffix names if so.
      action_suffix_names = [suffix_node.getAttribute('name') for suffix_node in
                             action_node.getElementsByTagName('with-suffix')]
      if action_suffix_names:
        action_suffixes = [suffix for suffix in suffixes if suffix.name in
                           action_suffix_names]
      else:
        action_suffixes = list(suffixes)

      if action_name in action_to_suffixes_dict:
        action_to_suffixes_dict[action_name] += action_suffixes
      else:
        action_to_suffixes_dict[action_name] = action_suffixes

  return action_to_suffixes_dict


def _GetAttribute(node, attribute_name, default_value):
  """Returns the attribute's value or default_value if attribute doesn't exist.

  Args:
    node: an XML dom element.
    attribute_name: name of the attribute.
    default_value: default value to return if attribute doesn't exist.

  Returns:
    The value of the attribute or default_value if attribute doesn't exist.
  """
  if node.hasAttribute(attribute_name):
    return node.getAttribute(attribute_name)
  else:
    return default_value


def _CreateActionsFromSuffixes(actions_dict, action_to_suffixes_dict):
  """Creates new actions with action-suffix pairs and adds them to actions_dict.

  For every key (action name) in action_to_suffixes_dict, This function looks
  to see whether it exists in actions_dict. If so it combines the Action object
  from actions_dict with all the Suffix objects from action_to_suffixes_dict to
  create new Action objects. New Action objects are added to actions_dict and
  the action name is removed from action_to_suffixes_dict.

  Args:
    actions_dict: dict of existing action name to Action object.
    action_to_suffixes_dict: dict of action name to list of Suffix objects it
                             will combine with.

  Returns:
    True if any new action was added, False otherwise.
  """
  expanded_actions = set()
  for action_name, suffixes in action_to_suffixes_dict.items():
    if action_name in actions_dict:
      existing_action = actions_dict[action_name]
      for suffix in suffixes:
        _CreateActionFromSuffix(actions_dict, existing_action, suffix)

      expanded_actions.add(action_name)

  for action_name in expanded_actions:
    del action_to_suffixes_dict[action_name]

  return bool(expanded_actions)


def _CreateActionFromSuffix(actions_dict, action, suffix):
  """Creates a new action with action and suffix and adds it to actions_dict.

  Args:
    actions_dict: dict of existing action name to Action object.
    action: an Action object to combine with suffix.
    suffix: a suffix object to combine with action.

  Returns:
    None.

  Raises:
    InvalidAffecteddActionNameError: if the action name does not contain a dot
  """
  if suffix.ordering == 'suffix':
    new_action_name = action.name + suffix.separator + suffix.name
  else:
    (before, dot, after) = action.name.partition('.')
    if not after:
      raise InvalidAffecteddActionNameError(
          "Action name '%s' must contain a '.'." % action.name)
    new_action_name = before + dot + suffix.name + suffix.separator + after

  new_action_description = action.description + ' ' + suffix.description

  actions_dict[new_action_name] = Action(
      new_action_name,
      new_action_description,
      list(action.owners),
      action.not_user_triggered,
      action.obsolete,
      from_suffix=True)