def test_nr_issues_multiple_sources(self, query_sum_mock): """ Test that the field is summed correctly and that involved issues are in the issue list. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') query_sum_mock.return_value = (0, ([])) jira_filter.nr_issues('12345', '78910') self.assertTrue(query_sum_mock.has_calls([call('12345'), call('78910')]))
def test_nr_issues_with_empty_jira_answer(self, get_query_url_mock): """ Test that the sum is -1 and the issue list is empty when jira returns empty json. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = [] result, issue_list = jira_filter.nr_issues('12345') get_query_url_mock.assert_called_once() self.assertEqual(-1, result) self.assertEqual([], issue_list)
def test_issues_with_field_without_issues(self, get_query_url_mock): """ Test that the issue list is empty when there are no issues. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": []} issue_list = jira_filter.issues_with_field('12345') get_query_url_mock.assert_called_once() self.assertEqual([], issue_list)
def test_nr_issues_when_no_issues(self, get_query_url_mock): """ Test that the number of issues is zero and the issue list is empty when there is no issues. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "0", "issues": []} result, issue_list = jira_filter.nr_issues('12345') get_query_url_mock.assert_called_once() self.assertEqual(0, result) # pay attention, it reads 'total' and does not count the issues self.assertEqual([], issue_list)
def test_issues_with_field_with_empty_jira_answer(self, get_query_url_mock, mock_error): """ Test that the issue list is empty when jira returns empty json. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = [] issue_list = jira_filter.issues_with_field('12345') get_query_url_mock.assert_called_once() self.assertEqual([], issue_list) self.assertEqual("Couldn't get issues from Jira filter %s: %s.", mock_error.call_args_list[0][0][0]) self.assertEqual("12345", mock_error.call_args_list[0][0][1]) self.assertIsInstance(mock_error.call_args_list[0][0][2], TypeError)
def test_nr_issues_with_field_omitted(self, get_query_url_mock): """ Test that the number of items equals those that do not have specified field. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}]} result, issue_list = jira_filter.nr_issues_with_field_empty('12345') get_query_url_mock.assert_called_once() self.assertEqual(1, result) self.assertEqual([{"href": "http://jira/browse/ISS-1", "text": "First Issue"}], issue_list)
def __init__(self, url: str, username: str, password: str, field_name: str = 'duedate', *args, **kwargs) -> None: self._fields_to_ignore = kwargs.pop('fields_to_ignore', []) self.__url = url self.__field_name = field_name self.__jira_filter = JiraFilter(self.__url, username, password, self.__field_name) super().__init__(*args, **kwargs)
def test_issues_with_field_exceeding_value_invalid2(self, get_query_url_mock, mock_error): """ Test that the errors with issues having invalid data are logged. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "no-fields": ''}]} issue_list = jira_filter.issues_with_field_exceeding_value('12345', limit_value=25) get_query_url_mock.assert_called_once() self.assertEqual([], issue_list) self.assertEqual("Error processing jira issues: %s.", mock_error.call_args_list[0][0][0]) self.assertIsInstance(mock_error.call_args_list[0][0][1], KeyError)
def test_nr_issues_multiple_sources(self, query_sum_mock): """ Test that the field is summed correctly and that involved issues are in the issue list. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') query_sum_mock.return_value = (0, ([])) jira_filter.nr_issues('12345', '78910') self.assertTrue( query_sum_mock.has_calls([call('12345'), call('78910')]))
def test_issues_with_field_exceeding_value(self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": 20.3}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}, {"key": "ISS-3", "fields": {"summary": "The Last Issue", "customfield_11700": None}}]} issue_list = jira_filter.issues_with_field_exceeding_value('12345', limit_value=25) get_query_url_mock.assert_called_once() self.assertEqual([ ("http://jira/browse/ISS-1", "First Issue", 20.3) ], issue_list)
def test_nr_issues_with_field_empty_when_no_issues(self, get_query_url_mock): """ Test that the number of issues is zero and the issue list is empty when there is no issues. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": []} result, issue_list = jira_filter.nr_issues_with_field_empty('12345') get_query_url_mock.assert_called_once() self.assertEqual(0, result) self.assertEqual([], issue_list)
def test_issues_with_field_exceeding_value_2_sources(self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.side_effect = [ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": 20.3}}]}, {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-11", "fields": {"summary": "Issue 11", "customfield_11700": 11}}]}] issue_list = jira_filter.issues_with_field_exceeding_value('12345', '22345', limit_value=25) self.assertTrue(get_query_url_mock.has_calls([call('12345'), call('22345')])) self.assertEqual([ ("http://jira/browse/ISS-1", "First Issue", 20.3), ("http://jira/browse/ISS-11", "Issue 11", 11) ], issue_list)
def test_nr_issues(self, get_query_url_mock): """ Test that the number of items equals the sum of totals per metric source returned by Jira. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": "20.3"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}]} result, issue_list = jira_filter.nr_issues('12345') get_query_url_mock.assert_called_once() self.assertEqual(5, result) # pay attention, it reads 'total' and does not count the issues self.assertEqual([ {"href": "http://jira/browse/ISS-1", "text": "First Issue"}, {"href": "http://jira/browse/ISS-2", "text": "2nd Issue"} ], issue_list)
def test_issues_with_field_exceeding_value_2_sources( self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.side_effect = [{ "searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [{ "key": "ISS-1", "fields": { "summary": "First Issue", "customfield_11700": 20.3 } }] }, { "searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [{ "key": "ISS-11", "fields": { "summary": "Issue 11", "customfield_11700": 11 } }] }] issue_list = jira_filter.issues_with_field_exceeding_value( '12345', '22345', limit_value=25) self.assertTrue( get_query_url_mock.has_calls([call('12345'), call('22345')])) self.assertEqual([("http://jira/browse/ISS-1", "First Issue", 20.3), ("http://jira/browse/ISS-11", "Issue 11", 11)], issue_list)
def test_issues_with_field_exceeding_value(self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": 20.3}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}, {"key": "ISS-3", "fields": {"summary": "The Last Issue", "customfield_11700": None}}]} issue_list = jira_filter.issues_with_field_exceeding_value( '12345', limit_value=25) get_query_url_mock.assert_called_once() self.assertEqual([("http://jira/browse/ISS-1", "First Issue", 20.3)], issue_list)
def test_issues_with_field_exceeding_value_date(self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='duedate') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "custom": 20.3, "duedate": "2018-10-22T19:12:36.000+0100"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "custom": 100, "duedate": "2017-12-28T13:10:46.000+0100"}}, {"key": "ISS-3", "fields": {"summary": "The Last Issue", "custom": 55, "duedate": None}}]} issue_list = jira_filter.issues_with_field_exceeding_value( '12345', limit_value="2018-03-22T19:12", extra_fields=['custom', 'non-existent-field']) get_query_url_mock.assert_called_once() self.assertEqual([ ("http://jira/browse/ISS-2", "2nd Issue", "2017-12-28T13:10:46.000+0100", 100, None) ], issue_list)
def test_nr_issues_with_field_omitted(self, get_query_url_mock): """ Test that the number of items equals those that do not have specified field. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}]} result, issue_list = jira_filter.nr_issues_with_field_empty('12345') get_query_url_mock.assert_called_once() self.assertEqual(1, result) self.assertEqual([{ "href": "http://jira/browse/ISS-1", "text": "First Issue" }], issue_list)
def test_issues_with_field_exceeding_value_invalid2( self, get_query_url_mock, mock_error): """ Test that the errors with issues having invalid data are logged. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "no-fields": ''}]} issue_list = jira_filter.issues_with_field_exceeding_value( '12345', limit_value=25) get_query_url_mock.assert_called_once() self.assertEqual([], issue_list) self.assertEqual("Error processing jira issues: %s.", mock_error.call_args_list[0][0][0]) self.assertIsInstance(mock_error.call_args_list[0][0][1], KeyError)
def test_url(self, get_query_url_mock): """ Test that the Jira filter returns the correct urls for the filters. """ jira1 = 'http://jira1/view' jira2 = 'http://jira2/view' get_query_url_mock.side_effect = [jira1, jira2] result = JiraFilter('', '', '').metric_source_urls(123, 567) get_query_url_mock.assert_has_calls( [call(123, search=False), call(567, search=False)]) self.assertEqual([jira1, jira2], result)
def test_issues_with_field_exceeding_value_date(self, get_query_url_mock): """ Test that the issues are returned correctly with their field values. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='duedate') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "custom": 20.3, "duedate": "2018-10-22T19:12:36.000+0100"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "custom": 100, "duedate": "2017-12-28T13:10:46.000+0100"}}, {"key": "ISS-3", "fields": {"summary": "The Last Issue", "custom": 55, "duedate": None}}]} issue_list = jira_filter.issues_with_field_exceeding_value( '12345', limit_value="2018-03-22T19:12", extra_fields=['custom', 'non-existent-field']) get_query_url_mock.assert_called_once() self.assertEqual([("http://jira/browse/ISS-2", "2nd Issue", "2017-12-28T13:10:46.000+0100", 100, None)], issue_list)
def test_sum_field(self, get_query_url_mock): """ Test that the field is summed correctly and that involved issues are in the issue list. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": "20.3"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}, {"key": "ISS-3", "fields": {"summary": "The Last Issue", "customfield_11700": None}}]} result, issue_list = jira_filter.sum_field('12345') get_query_url_mock.assert_called_once() self.assertEqual(120.3, result) self.assertEqual([{ "href": "http://jira/browse/ISS-1", "text": "First Issue" }, { "href": "http://jira/browse/ISS-2", "text": "2nd Issue" }], issue_list)
def test_nr_issues(self, get_query_url_mock): """ Test that the number of items equals the sum of totals per metric source returned by Jira. """ jira_filter = JiraFilter('http://jira/', 'username', 'password', field_name='customfield_11700') get_query_url_mock.return_value = \ {"searchUrl": "http://jira/search", "viewUrl": "http://jira/view", "total": "5", "issues": [ {"key": "ISS-1", "fields": {"summary": "First Issue", "customfield_11700": "20.3"}}, {"key": "ISS-2", "fields": {"summary": "2nd Issue", "customfield_11700": 100}}]} result, issue_list = jira_filter.nr_issues('12345') get_query_url_mock.assert_called_once() self.assertEqual( 5, result ) # pay attention, it reads 'total' and does not count the issues self.assertEqual([{ "href": "http://jira/browse/ISS-1", "text": "First Issue" }, { "href": "http://jira/browse/ISS-2", "text": "2nd Issue" }], issue_list)
def test_get_issue_url(self): """ Test Jira issue url formatting. """ jira_filter = JiraFilter('url/', '', '') self.assertEqual('url/browse/key', jira_filter.get_issue_url('key'))
class JiraActionList(ActionLog): """ Jira used as an action list """ metric_source_name = 'Jira Action List' def __init__(self, url: str, username: str, password: str, field_name: str = 'duedate', *args, **kwargs) -> None: self._fields_to_ignore = kwargs.pop('fields_to_ignore', []) self.__url = url self.__field_name = field_name self.__jira_filter = JiraFilter(self.__url, username, password, self.__field_name) super().__init__(*args, **kwargs) @classmethod def _is_str_date_before(cls, str_date: str, limit_date: datetime.datetime) -> bool: return utils.parse_iso_datetime_local_naive(str_date) < limit_date def _get_issues_older_than(self, *metric_source_ids: str, limit_date: datetime.datetime) -> List[Tuple[str, str, str]]: try: extra_fields = ['updated', 'created'] + [list(field.keys())[0] for field in self._fields_to_ignore] issues = self.__jira_filter.issues_with_field_exceeding_value( *metric_source_ids, extra_fields=extra_fields, compare=self._is_str_date_before, limit_value=limit_date) return [i for i in issues if not self.__should_issue_be_ignored(i)] except IndexError as reason: logging.error("Jira filter result for overdue issues inadequate. Reason: %s.", reason) return None def __should_issue_be_ignored(self, issue) -> bool: for index, ignore in enumerate(self._fields_to_ignore): if issue[index + 5] == list(ignore.values())[0]: return True return False def ignored_lists(self) -> List[str]: """ Return the ignored lists. """ return self._fields_to_ignore def nr_of_over_due_actions(self, *metric_source_ids: str) -> int: """ Return the number of over due actions. """ issue_list = self._get_issues_older_than(*metric_source_ids, limit_date=datetime.datetime.now()) return len(issue_list) if issue_list is not None else -1 def over_due_actions_url(self, *metric_source_ids: str) -> List[Tuple[str, str, str]]: """ Return the urls to the over due actions. """ issue_list = self._get_issues_older_than(*metric_source_ids, limit_date=datetime.datetime.now()) return [(issue[0], issue[1], self.__get_formatted_time_delta(issue[2])) for issue in issue_list] \ if issue_list is not None else [] def nr_of_inactive_actions(self, *metric_source_ids: str) -> int: """ Return the number of inactive actions. """ issue_list = self._get_issues_inactive_for(*metric_source_ids) return len(issue_list) if issue_list is not None else -1 def inactive_actions_url(self, *metric_source_ids: str) -> List[Tuple[str, str, str]]: """ Return the urls for the inactive actions. """ issue_list = self._get_issues_inactive_for(*metric_source_ids) return [(issue[0], issue[1], self.__get_formatted_time_delta(issue[3])) for issue in issue_list] \ if issue_list is not None else [] def _get_issues_inactive_for(self, *metric_source_ids, delta: relativedelta = relativedelta(days=14)): try: overdue_issue_list = self._get_issues_older_than(*metric_source_ids, limit_date=datetime.datetime.now()) return [issue for issue in overdue_issue_list if issue[3] is not None and utils.parse_iso_datetime_local_naive(issue[3]) <= (datetime.datetime.now() - delta)] except IndexError as reason: logging.error("Jira filter result for inactive issues inadequate. Reason: %s.", reason) return None @classmethod def __get_formatted_time_delta(cls, date_to_parse) -> str: return utils.format_timedelta(datetime.datetime.now().astimezone() - dateutil.parser.parse(date_to_parse)) 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_filter.metric_source_urls(*metric_source_ids) def datetime(self, *metric_source_ids: str) -> datetime.datetime: # pylint: disable=unused-argument,no-self-use """ Return the date and time of the last measurement. """ issue_list = self._get_issues_older_than(*metric_source_ids, limit_date=datetime.datetime.now()) return max([max(utils.parse_iso_datetime_local_naive(issue[4]), utils.parse_iso_datetime_local_naive(issue[3]) if issue[3] else datetime.datetime.min) for issue in issue_list])
class JiraActionList(ActionLog): """ Jira used as an action list """ metric_source_name = 'Jira Action List' def __init__(self, url: str, username: str, password: str, field_name: str = 'duedate', *args, **kwargs) -> None: self._fields_to_ignore = kwargs.pop('fields_to_ignore', []) self.__url = url self.__field_name = field_name self.__jira_filter = JiraFilter(self.__url, username, password, self.__field_name) super().__init__(*args, **kwargs) @classmethod def _is_str_date_before(cls, str_date: str, limit_date: datetime.datetime) -> bool: return utils.parse_iso_datetime_local_naive(str_date) < limit_date def _get_issues_older_than( self, *metric_source_ids: str, limit_date: datetime.datetime) -> List[Tuple[str, str, str]]: try: extra_fields = ['updated', 'created'] + [ list(field.keys())[0] for field in self._fields_to_ignore ] issues = self.__jira_filter.issues_with_field_exceeding_value( *metric_source_ids, extra_fields=extra_fields, compare=self._is_str_date_before, limit_value=limit_date) return [i for i in issues if not self.__should_issue_be_ignored(i)] except IndexError as reason: logging.error( "Jira filter result for overdue issues inadequate. Reason: %s.", reason) return None def __should_issue_be_ignored(self, issue) -> bool: for index, ignore in enumerate(self._fields_to_ignore): if issue[index + 5] == list(ignore.values())[0]: return True return False def ignored_lists(self) -> List[str]: """ Return the ignored lists. """ return self._fields_to_ignore def nr_of_over_due_actions(self, *metric_source_ids: str) -> int: """ Return the number of over due actions. """ issue_list = self._get_issues_older_than( *metric_source_ids, limit_date=datetime.datetime.now()) return len(issue_list) if issue_list is not None else -1 def over_due_actions_url( self, *metric_source_ids: str) -> List[Tuple[str, str, str]]: """ Return the urls to the over due actions. """ issue_list = self._get_issues_older_than( *metric_source_ids, limit_date=datetime.datetime.now()) return [(issue[0], issue[1], self.__get_formatted_time_delta(issue[2])) for issue in issue_list] \ if issue_list is not None else [] def nr_of_inactive_actions(self, *metric_source_ids: str) -> int: """ Return the number of inactive actions. """ issue_list = self._get_issues_inactive_for(*metric_source_ids) return len(issue_list) if issue_list is not None else -1 def inactive_actions_url( self, *metric_source_ids: str) -> List[Tuple[str, str, str]]: """ Return the urls for the inactive actions. """ issue_list = self._get_issues_inactive_for(*metric_source_ids) return [(issue[0], issue[1], self.__get_formatted_time_delta(issue[3])) for issue in issue_list] \ if issue_list is not None else [] def _get_issues_inactive_for( self, *metric_source_ids, delta: relativedelta = relativedelta(days=14)): try: overdue_issue_list = self._get_issues_older_than( *metric_source_ids, limit_date=datetime.datetime.now()) return [ issue for issue in overdue_issue_list if issue[3] is not None and utils.parse_iso_datetime_local_naive(issue[3]) <= (datetime.datetime.now() - delta) ] except IndexError as reason: logging.error( "Jira filter result for inactive issues inadequate. Reason: %s.", reason) return None @classmethod def __get_formatted_time_delta(cls, date_to_parse) -> str: return utils.format_timedelta(datetime.datetime.now().astimezone() - dateutil.parser.parse(date_to_parse)) 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_filter.metric_source_urls(*metric_source_ids) def datetime(self, *metric_source_ids: str) -> datetime.datetime: # pylint: disable=unused-argument,no-self-use """ Return the date and time of the last measurement. """ issue_list = self._get_issues_older_than( *metric_source_ids, limit_date=datetime.datetime.now()) return max([ max( utils.parse_iso_datetime_local_naive(issue[4]), utils.parse_iso_datetime_local_naive(issue[3]) if issue[3] else datetime.datetime.min) for issue in issue_list ])