| # Copyright 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Utility functions for interacting with a CL's action history.""" |
| |
| from __future__ import print_function |
| |
| import collections |
| from chromite.cbuildbot import constants |
| |
| # Bidirectional mapping between pre-cq status strings and CL action strings. |
| _PRECQ_STATUS_TO_ACTION = { |
| constants.CL_STATUS_INFLIGHT: constants.CL_ACTION_PRE_CQ_INFLIGHT, |
| constants.CL_STATUS_PASSED: constants.CL_ACTION_PRE_CQ_PASSED, |
| constants.CL_STATUS_FAILED: constants.CL_ACTION_PRE_CQ_FAILED, |
| constants.CL_STATUS_LAUNCHING: constants.CL_ACTION_PRE_CQ_LAUNCHING, |
| constants.CL_STATUS_WAITING: constants.CL_ACTION_PRE_CQ_WAITING, |
| constants.CL_STATUS_READY_TO_SUBMIT: |
| constants.CL_ACTION_PRE_CQ_READY_TO_SUBMIT |
| } |
| |
| _PRECQ_ACTION_TO_STATUS = dict( |
| (v, k) for k, v in _PRECQ_STATUS_TO_ACTION.items()) |
| |
| assert len(_PRECQ_STATUS_TO_ACTION) == len(_PRECQ_ACTION_TO_STATUS), \ |
| '_PRECQ_STATUS_TO_ACTION values are not unique.' |
| |
| CL_ACTION_COLUMNS = ['id', 'build_id', 'action', 'reason', |
| 'build_config', 'build_number', 'change_number', |
| 'patch_number', 'change_source', 'timestamp'] |
| |
| _CLActionTuple = collections.namedtuple('_CLActionTuple', CL_ACTION_COLUMNS) |
| |
| GerritPatchTuple = collections.namedtuple('GerritPatchTuple', |
| ['gerrit_number', 'patch_number', |
| 'internal']) |
| |
| |
| # pylint: disable=E1101,W0232 |
| class CLAction(_CLActionTuple): |
| """An action or history log entry for a particular CL.""" |
| |
| @classmethod |
| def FromGerritPatchAndAction(cls, change, action, reason=None, |
| timestamp=None): |
| """Creates a CLAction instance from a change and action. |
| |
| Args: |
| change: A GerritPatch instance. |
| action: An action string. |
| reason: Optional reason string. |
| timestamp: Optional datetime.datetime timestamp. |
| """ |
| return CLAction(None, None, action, reason, None, None, |
| int(change.gerrit_number), int(change.patch_number), |
| BoolToChangeSource(change.internal), timestamp) |
| |
| @classmethod |
| def FromMetadataEntry(cls, entry): |
| """Creates a CLAction instance from a metadata.json-style action tuple. |
| |
| Args: |
| entry: An action tuple as retrieved from metadata.json (previously known |
| as a CLActionTuple). |
| build_metadata: The full build metadata.json entry. |
| """ |
| change_dict = entry[0] |
| return CLAction(None, None, entry[1], entry[3], None, None, |
| int(change_dict['gerrit_number']), |
| int(change_dict['patch_number']), |
| BoolToChangeSource(change_dict['internal']), |
| entry[2]) |
| |
| |
| def AsMetadataEntry(self): |
| """Get a tuple representation, suitable for metadata.json.""" |
| # pylint: disable=W0212 |
| return (self.patch._asdict(), self.action, self.timestamp, self.reason) |
| |
| @property |
| def patch(self): |
| """The GerritPatch this action affects.""" |
| return GerritPatchTuple( |
| gerrit_number=self.change_number, |
| patch_number=self.patch_number, |
| internal=self.change_source == constants.CHANGE_SOURCE_INTERNAL |
| ) |
| |
| |
| def TranslatePreCQStatusToAction(status): |
| """Translate a pre-cq |status| into a cl action. |
| |
| Returns: |
| An action string suitable for use in cidb, for the given pre-cq status. |
| |
| Raises: |
| KeyError if |status| is not a known pre-cq status. |
| """ |
| return _PRECQ_STATUS_TO_ACTION[status] |
| |
| |
| def TranslatePreCQActionToStatus(action): |
| """Translate a cl |action| into a pre-cq status. |
| |
| Returns: |
| A pre-cq status string corresponding to the given |action|. |
| |
| Raises: |
| KeyError if |action| is not a known pre-cq status-transition-action. |
| """ |
| return _PRECQ_ACTION_TO_STATUS[action] |
| |
| |
| def BoolToChangeSource(internal): |
| """Translate a change.internal bool into a change_source string. |
| |
| Returns: |
| 'internal' if internal, else 'external'. |
| """ |
| return (constants.CHANGE_SOURCE_INTERNAL if internal |
| else constants.CHANGE_SOURCE_EXTERNAL) |
| |
| |
| def GetCLPreCQStatus(change, action_history): |
| """Get the pre-cq status for |change| based on |action_history|. |
| |
| Args: |
| change: GerritPatch instance to get the pre-CQ status for. |
| action_history: A list of CLAction instances. This may include |
| actions for changes other than |change|. |
| |
| Returns: |
| The status, as a string, or None if there is no recorded pre-cq status. |
| """ |
| raw_actions_for_patch = ActionsForPatch(change, action_history) |
| actions_for_patch = [a for a in raw_actions_for_patch |
| if a.action in _PRECQ_ACTION_TO_STATUS] |
| |
| if not actions_for_patch: |
| return None |
| |
| action = actions_for_patch[-1].action |
| |
| # Workaround an old bug in the Pre-CQ where it forgot to mark patches as |
| # failed. See http://crbug.com/429777 . TODO(davidjames): Remove this. |
| if action == constants.CL_ACTION_PRE_CQ_INFLIGHT: |
| for a in reversed(raw_actions_for_patch): |
| if a.action == action: |
| break |
| elif a.action == constants.CL_ACTION_KICKED_OUT: |
| action = constants.CL_ACTION_PRE_CQ_FAILED |
| |
| return TranslatePreCQActionToStatus(action) |
| |
| |
| def ActionsForPatch(change, action_history): |
| """Filters a CL action list to only those for a given patch. |
| |
| Args: |
| change: GerritPatch instance to filter for. |
| action_history: List of CLAction objects. |
| """ |
| patch_number = int(change.patch_number) |
| change_number = int(change.gerrit_number) |
| change_source = BoolToChangeSource(change.internal) |
| |
| actions_for_patch = [a for a in action_history |
| if a.change_source == change_source and |
| a.change_number == change_number and |
| a.patch_number == patch_number] |
| |
| return actions_for_patch |
| |
| |
| def GetRequeuedOrSpeculative(change, action_history, is_speculative): |
| """For a |change| get either a requeued or speculative action if necessary. |
| |
| This method returns an action string for an action that should be recorded |
| on |change|, or None if no action needs to be recorded. |
| |
| Args: |
| change: GerritPatch instance to operate upon. |
| action_history: List of CL actions (may include actions on changes other |
| than |change|). |
| is_speculative: Boolean indicating if |change| is speculative, i.e. it does |
| not have CQ approval. |
| |
| Returns: |
| CL_ACTION_REQUEUED, CL_ACTION_SPECULATIVE, or None. |
| """ |
| actions_for_patch = ActionsForPatch(change, action_history) |
| |
| if is_speculative: |
| # Speculative changes should have 1 CL_ACTION_SPECULATIVE action that is |
| # newer than the newest REQUEUED or KICKED_OUT action, and at least 1 |
| # action if there is no REQUEUED or KICKED_OUT action. |
| for a in reversed(actions_for_patch): |
| if a.action == constants.CL_ACTION_SPECULATIVE: |
| return None |
| elif (a.action == constants.CL_ACTION_REQUEUED or |
| a.action == constants.CL_ACTION_KICKED_OUT): |
| return constants.CL_ACTION_SPECULATIVE |
| return constants.CL_ACTION_SPECULATIVE |
| else: |
| # Non speculative changes should have 1 CL_ACTION_REQUEUED action that is |
| # newer than the newest SPECULATIVE or KICKED_OUT action, but no action if |
| # there are no SPECULATIVE or REQUEUED actions. |
| for a in reversed(actions_for_patch): |
| if (a.action == constants.CL_ACTION_KICKED_OUT or |
| a.action == constants.CL_ACTION_SPECULATIVE): |
| return constants.CL_ACTION_REQUEUED |
| if a.action == constants.CL_ACTION_REQUEUED: |
| return None |
| |
| return None |
| |
| def GetCLActionCount(change, configs, action, action_history, |
| latest_patchset_only=True): |
| """Return how many times |action| has occured on |change|. |
| |
| Args: |
| change: GerritPatch instance to operate upon. |
| configs: List or set of config names to consider. |
| action: The action string to look for. |
| action_history: List of CLAction instances to count through. |
| latest_patchset_only: If True, only count actions that occured to the |
| latest patch number. Note, this may be different than the patch |
| number specified in |change|. Default: True. |
| |
| Returns: |
| The count of how many times |action| occured on |change| by the given |
| |config|. |
| """ |
| change_number = int(change.gerrit_number) |
| change_source = BoolToChangeSource(change.internal) |
| actions_for_change = [a for a in action_history |
| if a.change_source == change_source and |
| a.change_number == change_number] |
| |
| if actions_for_change and latest_patchset_only: |
| latest_patch_number = max(a.patch_number for a in actions_for_change) |
| actions_for_change = [a for a in actions_for_change |
| if a.patch_number == latest_patch_number] |
| |
| actions_for_change = [a for a in actions_for_change |
| if (a.build_config in configs and |
| a.action == action)] |
| |
| return len(actions_for_change) |
| |
| |
| def GetCLPreCQProgress(change, action_history): |
| """Gets a CL's per-config PreCQ statuses. |
| |
| Args: |
| change: GerritPatch instance to get statuses for. |
| action_history: List of CLAction instances. |
| |
| Returns: |
| A dict of the form {config_name: (status, timestamp)} specifying |
| all the per-config pre-cq statuses, where status is one of |
| constants.CL_PRECQ_CONFIG_STATUSES and timestampe is a datetime.datetime of |
| when this status was most recently achieved. |
| """ |
| actions_for_patch = ActionsForPatch(change, action_history) |
| config_status_dict = {} |
| |
| # Only configs for which the pre-cq-launcher has requested verification |
| # should be included in the per-config status. |
| for a in actions_for_patch: |
| if a.action == constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ: |
| assert a.reason, 'Validation was requested without a specified config.' |
| config_status_dict[a.reason] = (constants.CL_PRECQ_CONFIG_STATUS_PENDING, |
| a.timestamp) |
| |
| # Loop through actions_for_patch several times, in order of status priority. |
| # CL_PRECQ_CONFIG_STATUS_LAUNCHED, |
| # CL_PRECQ_CONFIG_STATUS_FAILED, |
| # CL_PRECQ_CONFIG_STATUS_INFLIGHT |
| # All have the same priority |
| for a in actions_for_patch: |
| if (a.action == constants.CL_ACTION_TRYBOT_LAUNCHING and |
| a.reason in config_status_dict): |
| config_status_dict[a.reason] = (constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED, |
| a.timestamp) |
| elif (a.action == constants.CL_ACTION_PICKED_UP and |
| a.build_config in config_status_dict): |
| config_status_dict[a.build_config] = ( |
| constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT, a.timestamp) |
| elif (a.action == constants.CL_ACTION_KICKED_OUT and |
| (a.build_config in config_status_dict or |
| a.reason in config_status_dict)): |
| config = (a.build_config if a.build_config in config_status_dict else |
| a.reason) |
| config_status_dict[config] = (constants.CL_PRECQ_CONFIG_STATUS_FAILED, |
| a.timestamp) |
| |
| for a in actions_for_patch: |
| if (a.action == constants.CL_ACTION_VERIFIED and |
| a.build_config in config_status_dict): |
| config_status_dict[a.build_config] = ( |
| constants.CL_PRECQ_CONFIG_STATUS_VERIFIED, a.timestamp) |
| |
| return config_status_dict |
| |
| |
| def GetPreCQProgressMap(changes, action_history): |
| """Gets the per-config pre-cq status for all changes. |
| |
| Args: |
| changes: Set of GerritPatch changes to consider. |
| action_history: List of CLAction instances. |
| |
| Returns: |
| A dict of the form {change: config_status_dict} where config_status_dict |
| is as returned by GetCLPreCQProgress. Any change that has not yet been |
| screened will be absent from the returned dict. |
| """ |
| progress_map = {} |
| for change in changes: |
| config_status_dict = GetCLPreCQProgress(change, action_history) |
| if config_status_dict: |
| progress_map[change] = config_status_dict |
| |
| return progress_map |
| |
| |
| def GetPreCQCategories(progress_map): |
| """Gets the set of busy and verified CLs in the pre-cq. |
| |
| Args: |
| progress_map: See return type of GetPreCQProgressMap. |
| |
| Returns: |
| A (busy, verified) tuple where busy and verified are each a set of changes. |
| A change is verified if all its pending configs have verified it. A change |
| is busy if it is not verified, but all pending configs are either launched |
| or inflight or verified. |
| """ |
| busy = set() |
| verified = set() |
| busy_states = (constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED, |
| constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT, |
| constants.CL_PRECQ_CONFIG_STATUS_VERIFIED) |
| |
| for change, config_status_dict in progress_map.iteritems(): |
| if all(s == constants.CL_PRECQ_CONFIG_STATUS_VERIFIED for |
| (s, t) in config_status_dict.values()): |
| verified.add(change) |
| elif all(s in busy_states for |
| (s, t) in config_status_dict.values()): |
| busy.add(change) |
| |
| return busy, verified |
| |
| |
| def GetPreCQConfigsToTest(changes, progress_map): |
| """Gets the set of configs to be tested for any change in |changes|. |
| |
| Note: All |changes| must already be screened, i.e. must appear in |
| progress_map. |
| |
| Args: |
| changes: A list or set of changes (GerritPatch). |
| progress_map: See return type of GetPreCQProgressMap. |
| |
| Returns: |
| A set of configs that must be launched in order to make each change in |
| |changes| be considered 'busy' by the pre-cq. |
| |
| Raises: |
| KeyError if any change in |changes| is not yet screened, and hence |
| does not appear in progress_map. |
| """ |
| configs_to_test = set() |
| # Failed is considered a to-test state so that if a CL fails a given config |
| # and gets rejected, it will be re-tested by that config when it is re-queued. |
| to_test_states = (constants.CL_PRECQ_CONFIG_STATUS_PENDING, |
| constants.CL_PRECQ_CONFIG_STATUS_FAILED) |
| for change in changes: |
| for config, (status, _) in progress_map[change].iteritems(): |
| if status in to_test_states: |
| configs_to_test.add(config) |
| return configs_to_test |
| |
| |
| def GetRelevantChangesForBuilds(changes, action_history, build_ids): |
| """Get relevant changes for |build_ids| by examing CL actions. |
| |
| Args: |
| changes: A list of GerritPatch instances to examine. |
| action_history: A list of CLAction instances. |
| build_ids: A list of build id to examine. |
| |
| Returns: |
| A dictionary mapping a build id to a set of changes. |
| """ |
| changes_map = dict() |
| relevant_actions = [x for x in action_history if x.build_id in build_ids] |
| for change in changes: |
| actions = ActionsForPatch(change, relevant_actions) |
| pickups = set([x.build_id for x in actions if |
| x.action == constants.CL_ACTION_PICKED_UP]) |
| discards = set([x.build_id for x in actions if |
| x.action == constants.CL_ACTION_IRRELEVANT_TO_SLAVE]) |
| relevant_build_ids = pickups - discards |
| for build_id in relevant_build_ids: |
| changes_map.setdefault(build_id, set()).add(change) |
| |
| return changes_map |