def test_get_query_url_jql(self, url_read_mock): """ Test that the jql filter url is correct. """ jira = Jira('http://jira/', 'username', 'password') result = jira.get_query_url('jql query - not a number', search=True) url_read_mock.assert_not_called() self.assertEqual("http://jira/rest/api/2/search?maxResults=1000&jql=jql%20query%20-%20not%20a%20number", result)
def test_get_query_url_empty_id(self, url_read_mock): """ Test that the url is None when empty metric source id is given. """ jira = Jira('http://jira/', '', '') result = jira.get_query_url('') url_read_mock.assert_not_called() self.assertEqual(None, result)
def test_get_query_url_jql_display(self, url_read_mock): """ Test that the display url of jql query is correct. """ jira = Jira('http://jira/', 'username', 'password') result = jira.get_query_url('jql query - not a number', search=False) url_read_mock.assert_not_called() self.assertEqual("http://jira/issues/?jql=jql%20query%20-%20not%20a%20number", result)
def test_get_field_id_http_error(self, url_read_mock): """ Test that the url is None when http error occurs. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.side_effect = urllib.error.HTTPError(None, None, None, None, None) result = jira.get_field_id("First Name") url_read_mock.assert_called_once_with('http://jira/rest/api/2/field') self.assertEqual(None, result)
def test_get_field_id(self, url_read_mock): """ Test that the url is None when http error occurs. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.return_value = '[{"id": "fn22", "name": "First Name"}]' result = jira.get_field_id("First Name") url_read_mock.assert_called_once_with('http://jira/rest/api/2/field') self.assertEqual("fn22", result)
def test_get_query_url_view(self, url_read_mock): """ Test that the view url is correctly retrieved. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.return_value = '{"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5"}' result = jira.get_query_url('333', search=False) url_read_mock.assert_called_once_with('http://jira/rest/api/2/filter/333') self.assertEqual("http://jira/view", result)
def test_get_query_url_http_error(self, url_read_mock): """ Test that the url is None when http error occurs. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.side_effect = urllib.error.HTTPError(None, None, None, None, None) result = jira.get_query_url('333') url_read_mock.assert_called_once_with('http://jira/rest/api/2/filter/333') self.assertEqual(None, result)
def test_get_query_query_url_empty(self, get_query_url_mock, url_read_mock): """ Test that the query result is None when query url returns empty result. """ jira = Jira('http://jira/', '', '') get_query_url_mock.return_value = "" result = jira.get_query('333') get_query_url_mock.assert_called_once() url_read_mock.assert_not_called() self.assertEqual(None, result)
def test_get_field_id_not_exist(self, error_mock, url_read_mock): """ Test that the url is None when http error occurs. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.return_value = '[{"id": "fn22", "name": "First Name"}]' result = jira.get_field_id("Some Other Name") url_read_mock.assert_called_once_with('http://jira/rest/api/2/field') error_mock.assert_called_once_with("Error retrieving id for the field with name %s.", "Some Other Name") self.assertEqual(None, result)
def __init__(self, url: str, username: str, password: str, field_name: str = '') -> None: from hqlib.metric_source import Jira # Import here to prevent circular import self.__url = url self.__jira = Jira(url, username, password) self.__field_name = field_name super().__init__()
def test_get_query_http_error(self, get_query_url_mock, url_read_mock): """ Test that the query result is None when http error occurs. """ jira = Jira('http://jira/', '', '') get_query_url_mock.return_value = "http://other/what?that=1" url_read_mock.side_effect = urllib.error.HTTPError(None, None, None, None, None) result = jira.get_query('333') get_query_url_mock.assert_called_once() url_read_mock.assert_called_once_with('http://jira/what?maxResults=1000&that=1') self.assertEqual(None, result)
def test_get_issue_details_json_error(self, url_read_mock): """ Test that the issue details are None when json is incorrect . """ jira = Jira('http://jira/', '', '') url_read_mock.return_value = 'not json' result = jira.get_issue_details('ISS-ID') url_read_mock.assert_called_once_with( 'http://jira/rest/api/2/issue/ISS-ID?maxResults=1000&expand=changelog&fields="*all,-comment"' ) self.assertEqual(None, result)
def test_get_query(self, get_query_url_mock, url_read_mock): """ Test that the query is correctly retrieved. """ jira = Jira('http://jira/', '', '') get_query_url_mock.return_value = "http://other/what?that=1" url_read_mock.return_value = '{"x": "y"}' result = jira.get_query('333') get_query_url_mock.assert_called_once() url_read_mock.assert_called_once_with('http://jira/what?maxResults=1000&that=1') self.assertEqual({"x": "y"}, result)
def test_get_issue_details(self, url_read_mock): """ Test that the issue details are correctly retrieved. """ jira = Jira('http://jira/', '', '') url_read_mock.return_value = '{"x": "1"}' result = jira.get_issue_details('ISS-ID') url_read_mock.assert_called_once_with( 'http://jira/rest/api/2/issue/ISS-ID?maxResults=1000&expand=changelog&fields="*all,-comment"' ) self.assertEqual({"x": "1"}, result)
def test_get_issue_details_http_error(self, url_read_mock): """ Test that the issue details are None when hrrp error occurs. """ jira = Jira('http://jira/', '', '') url_read_mock.side_effect = urllib.error.HTTPError(None, None, None, None, None) result = jira.get_issue_details('ISS-ID') url_read_mock.assert_called_once_with( 'http://jira/rest/api/2/issue/ISS-ID?maxResults=1000&expand=changelog&fields="*all,-comment"' ) self.assertEqual(None, result)
def test_get_field_id_not_exist(self, error_mock, url_read_mock): """ Test that the url is None when http error occurs. """ jira = Jira('http://jira/', 'username', 'password') url_read_mock.return_value = '[{"id": "fn22", "name": "First Name"}]' result = jira.get_field_id("Some Other Name") url_read_mock.assert_called_once_with('http://jira/rest/api/2/field') error_mock.assert_called_once_with( "Error retrieving id for the field with name %s.", "Some Other Name") self.assertEqual(None, result)
def test_url_padding(self, init_mock): """ Tests that jira url is not padded if not needed.""" init_mock.return_value = None jira = Jira('X/', '', '') self.assertEqual('X/', jira._Jira__url)
def test_url_opener_constructor(self, init_mock): """ Test that by Jira initialisation, UrlOpener is initialised with user name and password as parameters. """ init_mock.return_value = None Jira('http://jira/', 'jira_username', 'jira_password') init_mock.assert_called_with(username='******', password='******')
class JiraFilter(BugTracker): """ Metric source for Jira filters. The metric source id is the filter id. """ metric_source_name = 'Jira filter' def __init__(self, url: str, username: str, password: str, field_name: str = '') -> None: from hqlib.metric_source import Jira # Import here to prevent circular import self.__url = url self.__jira = Jira(url, username, password) self.__field_name = field_name super().__init__() def _query_field_empty(self, query_id: QueryId, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ return self.__query_field(query_id, self._increment_if_field_empty, field) def __query_field(self, query_id: QueryId, func: Callable, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ result = self.sum_for_all_issues(query_id, func, tuple(), field) return (-1, []) if result is None else (sum(result[::2]), self._get_just_links(result[1::2])) @classmethod def _increment_if_field_empty(cls, issue: Dict, field: str) -> Tuple: """ Return 1 if the field is empty, otherwise 0. """ try: int(issue['fields'][field]) return 0, None except (TypeError, KeyError): return 1, issue def _query_total(self, query_id: QueryId) -> Tuple[int, List[str]]: """ Return the number of results of the specified query. """ results = self.__jira.get_query(query_id) return (int(results['total']), self._get_just_links(results['issues'])) if results else (-1, []) def _get_just_links(self, issues: List): return [ utils.format_link_object( self.get_issue_url(issue['key']), issue['fields']['summary']) for issue in issues if issue] def nr_issues(self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues in the filter. """ count, issues = zip(*[self._query_total(metric_source_id) for metric_source_id in metric_source_ids]) return (-1, []) if -1 in count else (sum(count), issues[0]) @classmethod def _get_create_date_from_json(cls, json: Dict, to_str: bool): to_from = "toString" if to_str else "fromString" def is_progress_event(history_item): """ Return whether the history item is a start of progress or end of progress event. """ return history_item["field"] == "status" and history_item["fieldtype"] == "jira" and \ history_item[to_from] == "In Progress" dates = [] for history in json['changelog']['histories']: if any(filter(is_progress_event, history['items'])): dates.append(dateutil.parser.parse(history["created"])) return dates def get_start_and_end_progress_date(self, issue: Dict) -> Tuple[Optional[DateTime], Optional[DateTime]]: """ Fetch the changelog of the given issue and get number of days between it is moved for the first time to the status "In Progress", till the last time it is moved out of it. """ json = self.__jira.get_issue_details(issue['key']) try: to_in_progress_date = min(self._get_create_date_from_json(json, True)) except ValueError: logging.info("Invalid date, or issue %s never moved to status 'In Progress'", issue['key']) return None, None except TypeError: logging.error("Received invalid json from %s: %s", self.__url, json) return None, None try: from_in_progress_date = max(self._get_create_date_from_json(json, False)) except ValueError: logging.info("Invalid date, or issue %s still in status 'In Progress'", issue['key']) return to_in_progress_date, None return to_in_progress_date, from_in_progress_date def sum_for_all_issues(self, query_id: QueryId, func: Callable, total: object, *args, **kwargs): """ Perform the func calculation over jira issues returned by the query specified by query_id. """ results = self.__jira.get_query(query_id) if not results: return None for issue in results['issues']: total += func(issue, *args, **kwargs) return total def get_issue_url(self, issue_key: str) -> str: """ Format Jira issue url for given issue id. """ return utils.url_join(self.__url, 'browse/{key}'.format(key=issue_key)) def nr_issues_with_field_empty(self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues whose field has not been filled in. """ count, issues = zip(*[ self._query_field_empty(metric_source_id, self.__field_name) for metric_source_id in metric_source_ids]) return -1 if -1 in count else sum(count), issues[0] def issues_with_field(self, *metric_source_ids: str) -> List[Tuple[str, float]]: """ Return a list of issues links and values from the specified field. """ return self._get_issues_for_criterion(*metric_source_ids, append_function=self.__append_links) def issues_with_field_exceeding_value(self, *metric_source_ids: str, compare: callable = lambda x, y: x < y, limit_value, extra_fields: [str] = None) -> List[Tuple]: """ Return a list of issues links and values where the value of the field exceeds given margin. """ return self._get_issues_for_criterion(*metric_source_ids, append_function=self.__append_links_exceeding, compare=compare, limit_value=limit_value, extra_fields=extra_fields) def _get_issues_for_criterion(self, *metric_source_ids: str, append_function: callable, compare: callable = None, limit_value=None, extra_fields=None) -> List: result_list = [] for query_id in metric_source_ids: query_result = self.__jira.get_query(query_id) try: issues = query_result["issues"] except (ValueError, KeyError, TypeError) as reason: logging.error("Couldn't get issues from Jira filter %s: %s.", query_id, reason) return [] try: self.__get_links_and_values(issues, append_function, compare, limit_value, extra_fields, result_list) except (KeyError, AttributeError) as reason: logging.error("Error processing jira issues: %s.", reason) return result_list # pylint: disable=too-many-arguments def __get_links_and_values(self, issues, append_function, compare: callable, limit_value, extra_fields, result_list): for issue in issues: fields = issue["fields"] if not fields.get(self.__field_name): continue # Skip issues that don't have a value for the field append_function(fields, issue, result_list, compare, limit_value, extra_fields if extra_fields else []) # pylint: disable=too-many-arguments def __append_links_exceeding(self, fields, issue, result_list: List[Tuple], compare: callable, limit_value, extra_fields: [str]): if compare(fields[self.__field_name], limit_value): result_list.append(( self.get_issue_url(issue["key"]), fields["summary"], fields[self.__field_name], *[fields[field_name] if field_name in fields else None for field_name in extra_fields])) # pylint: disable=too-many-arguments # pylint: disable=unused-argument def __append_links(self, fields, issue, result_list: List[Tuple], compare=None, limit_value=None, extra_fields=None): link = utils.format_link_object(self.get_issue_url(issue["key"]), fields["summary"]) result_list.append((link, float(fields[self.__field_name]))) def metric_source_urls(self, *metric_source_ids: str) -> List[str]: """ Return the url(s) to the metric source for the metric source id. """ return [self.__jira.get_query_url(metric_source_id, search=False) for metric_source_id in metric_source_ids]
class JiraFilter(BugTracker): """ Metric source for Jira filters. The metric source id is the filter id. """ metric_source_name = 'Jira filter' def __init__(self, url: str, username: str, password: str, field_name: str = '') -> None: from hqlib.metric_source import Jira # Import here to prevent circular import self.__url = url self.__jira = Jira(url, username, password) self.__field_name = field_name super().__init__() def _query_sum(self, query_id: QueryId, field: str) -> Tuple[float, List[str]]: """ Return the sum of the fields as returned by the query. """ return self.__query_field(query_id, self._get_field_float_value, field) @classmethod def _get_field_float_value(cls, issue: Dict, field: str) -> Tuple: """ Get the float value from issue's field, or 0, in the case of error. """ try: return float(issue['fields'][field]), issue except (TypeError, KeyError): return 0, None def _query_field_empty(self, query_id: QueryId, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ return self.__query_field(query_id, self._increment_if_field_empty, field) def __query_field(self, query_id: QueryId, func: Callable, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ result = self.sum_for_all_issues(query_id, func, tuple(), field) return (-1, []) if result is None else (sum(result[::2]), self._get_just_links(result[1::2])) @classmethod def _increment_if_field_empty(cls, issue: Dict, field: str) -> Tuple: """ Return 1 if the field is empty, otherwise 0. """ try: int(issue['fields'][field]) return 0, None except (TypeError, KeyError): return 1, issue def _query_total(self, query_id: QueryId) -> Tuple[int, List[str]]: """ Return the number of results of the specified query. """ results = self.__jira.get_query(query_id) return (int(results['total']), self._get_just_links(results['issues'])) if results else (-1, []) def _get_just_links(self, issues: List): return [ ExtraInfo.format_extra_info_link(self.get_issue_url(issue['key']), issue['fields']['summary']) for issue in issues if issue ] def nr_issues(self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues in the filter. """ count, issues = zip(*[ self._query_total(int(metric_source_id)) for metric_source_id in metric_source_ids ]) return -1 if -1 in count else sum(count), issues[0] @classmethod def _get_create_date_from_json(cls, json: Dict, to_str: bool): to_from = "toString" if to_str else "fromString" def is_progress_event(history_item): """ Return whether the history item is a start of progress or end of progress event. """ return history_item["field"] == "status" and history_item["fieldtype"] == "jira" and \ history_item[to_from] == "In Progress" dates = [] for history in json['changelog']['histories']: if any(filter(is_progress_event, history['items'])): dates.append(dateutil.parser.parse(history["created"])) return dates def get_start_and_end_progress_date( self, issue: Dict) -> Tuple[Optional[DateTime], Optional[DateTime]]: """ Fetch the changelog of the given issue and get number of days between it is moved for the first time to the status "In Progress", till the last time it is moved out of it. """ json = self.__jira.get_issue_details(issue['key']) try: to_in_progress_date = min( self._get_create_date_from_json(json, True)) except ValueError: logging.info( "Invalid date, or issue %s never moved to status 'In Progress'", issue['key']) return None, None except TypeError: logging.error("Received invalid json from %s: %s", self.__url, json) return None, None try: from_in_progress_date = max( self._get_create_date_from_json(json, False)) except ValueError: logging.info( "Invalid date, or issue %s still in status 'In Progress'", issue['key']) return to_in_progress_date, None return to_in_progress_date, from_in_progress_date def sum_for_all_issues(self, query_id: QueryId, func: Callable, total: object, *args, **kwargs): """ Perform the func calculation over jira issues returned by the query specified by query_id. """ results = self.__jira.get_query(query_id) if not results: return None for issue in results['issues']: total += func(issue, *args, **kwargs) return total def get_issue_url(self, issue_key: str) -> str: """ Format Jira issue url for given issue id. """ return self.__url + 'browse/{key}'.format(key=issue_key) def nr_issues_with_field_empty( self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues whose field has not been filled in. """ count, issues = zip(*[ self._query_field_empty(int(metric_source_id), self.__field_name) for metric_source_id in metric_source_ids ]) return -1 if -1 in count else sum(count), issues[0] def sum_field(self, *metric_source_ids: str) -> Tuple[float, List[str]]: """ Return the sum of the values in the specified field. """ results, issues = zip(*[ self._query_sum(int(metric_source_id), self.__field_name) for metric_source_id in metric_source_ids ]) return -1 if -1 in results else sum(results), issues[0] def metric_source_urls(self, *metric_source_ids: str) -> List[str]: """ Return the url(s) to the metric source for the metric source id. """ return [ self.__jira.get_query_url(int(metric_source_id), search=False) for metric_source_id in metric_source_ids ]
class JiraFilter(BugTracker): """ Metric source for Jira filters. The metric source id is the filter id. """ metric_source_name = 'Jira filter' def __init__(self, url: str, username: str, password: str, field_name: str = '') -> None: from hqlib.metric_source import Jira # Import here to prevent circular import self.__url = url self.__jira = Jira(url, username, password) self.__field_name = field_name super().__init__() def _query_field_empty(self, query_id: QueryId, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ return self.__query_field(query_id, self._increment_if_field_empty, field) def __query_field(self, query_id: QueryId, func: Callable, field: str) -> Tuple[int, List[str]]: """ Return the number of empty fields, returned by the query. """ result = self.sum_for_all_issues(query_id, func, tuple(), field) return (-1, []) if result is None else (sum(result[::2]), self._get_just_links(result[1::2])) @classmethod def _increment_if_field_empty(cls, issue: Dict, field: str) -> Tuple: """ Return 1 if the field is empty, otherwise 0. """ try: int(issue['fields'][field]) return 0, None except (TypeError, KeyError): return 1, issue def _query_total(self, query_id: QueryId) -> Tuple[int, List[str]]: """ Return the number of results of the specified query. """ results = self.__jira.get_query(query_id) return (int(results['total']), self._get_just_links(results['issues'])) if results else (-1, []) def _get_just_links(self, issues: List): return [ utils.format_link_object(self.get_issue_url(issue['key']), issue['fields']['summary']) for issue in issues if issue ] def nr_issues(self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues in the filter. """ count, issues = zip(*[ self._query_total(metric_source_id) for metric_source_id in metric_source_ids ]) return -1 if -1 in count else sum(count), issues[0] @classmethod def _get_create_date_from_json(cls, json: Dict, to_str: bool): to_from = "toString" if to_str else "fromString" def is_progress_event(history_item): """ Return whether the history item is a start of progress or end of progress event. """ return history_item["field"] == "status" and history_item["fieldtype"] == "jira" and \ history_item[to_from] == "In Progress" dates = [] for history in json['changelog']['histories']: if any(filter(is_progress_event, history['items'])): dates.append(dateutil.parser.parse(history["created"])) return dates def get_start_and_end_progress_date( self, issue: Dict) -> Tuple[Optional[DateTime], Optional[DateTime]]: """ Fetch the changelog of the given issue and get number of days between it is moved for the first time to the status "In Progress", till the last time it is moved out of it. """ json = self.__jira.get_issue_details(issue['key']) try: to_in_progress_date = min( self._get_create_date_from_json(json, True)) except ValueError: logging.info( "Invalid date, or issue %s never moved to status 'In Progress'", issue['key']) return None, None except TypeError: logging.error("Received invalid json from %s: %s", self.__url, json) return None, None try: from_in_progress_date = max( self._get_create_date_from_json(json, False)) except ValueError: logging.info( "Invalid date, or issue %s still in status 'In Progress'", issue['key']) return to_in_progress_date, None return to_in_progress_date, from_in_progress_date def sum_for_all_issues(self, query_id: QueryId, func: Callable, total: object, *args, **kwargs): """ Perform the func calculation over jira issues returned by the query specified by query_id. """ results = self.__jira.get_query(query_id) if not results: return None for issue in results['issues']: total += func(issue, *args, **kwargs) return total def get_issue_url(self, issue_key: str) -> str: """ Format Jira issue url for given issue id. """ return utils.url_join(self.__url, 'browse/{key}'.format(key=issue_key)) def nr_issues_with_field_empty( self, *metric_source_ids: str) -> Tuple[int, List[str]]: """ Return the number of issues whose field has not been filled in. """ count, issues = zip(*[ self._query_field_empty(metric_source_id, self.__field_name) for metric_source_id in metric_source_ids ]) return -1 if -1 in count else sum(count), issues[0] def issues_with_field(self, *metric_source_ids: str) -> List[Tuple[str, float]]: """ Return a list of issues links and values from the specified field. """ return self._get_issues_for_criterion( *metric_source_ids, append_function=self.__append_links) def issues_with_field_exceeding_value( self, *metric_source_ids: str, compare: callable = lambda x, y: x < y, limit_value, extra_fields: [str] = None) -> List[Tuple]: """ Return a list of issues links and values where the value of the field exceeds given margin. """ return self._get_issues_for_criterion( *metric_source_ids, append_function=self.__append_links_exceeding, compare=compare, limit_value=limit_value, extra_fields=extra_fields) def _get_issues_for_criterion(self, *metric_source_ids: str, append_function: callable, compare: callable = None, limit_value=None, extra_fields=None) -> List: result_list = [] for query_id in metric_source_ids: query_result = self.__jira.get_query(query_id) try: issues = query_result["issues"] except (ValueError, KeyError, TypeError) as reason: logging.error("Couldn't get issues from Jira filter %s: %s.", query_id, reason) return [] try: self.__get_links_and_values(issues, append_function, compare, limit_value, extra_fields, result_list) except (KeyError, AttributeError) as reason: logging.error("Error processing jira issues: %s.", reason) return result_list # pylint: disable=too-many-arguments def __get_links_and_values(self, issues, append_function, compare: callable, limit_value, extra_fields, result_list): for issue in issues: fields = issue["fields"] if not fields.get(self.__field_name): continue # Skip issues that don't have a value for the field append_function(fields, issue, result_list, compare, limit_value, extra_fields if extra_fields else []) # pylint: disable=too-many-arguments def __append_links_exceeding(self, fields, issue, result_list: List[Tuple], compare: callable, limit_value, extra_fields: [str]): if compare(fields[self.__field_name], limit_value): result_list.append( (self.get_issue_url(issue["key"]), fields["summary"], fields[self.__field_name], *[ fields[field_name] if field_name in fields else None for field_name in extra_fields ])) # pylint: disable=too-many-arguments # pylint: disable=unused-argument def __append_links(self, fields, issue, result_list: List[Tuple], compare=None, limit_value=None, extra_fields=None): link = utils.format_link_object(self.get_issue_url(issue["key"]), fields["summary"]) result_list.append((link, float(fields[self.__field_name]))) def metric_source_urls(self, *metric_source_ids: str) -> List[str]: """ Return the url(s) to the metric source for the metric source id. """ return [ self.__jira.get_query_url(metric_source_id, search=False) for metric_source_id in metric_source_ids ]