Skip to content

Commit

Permalink
[6.13.z] Add support for Jira Issue handler (#14820)
Browse files Browse the repository at this point in the history
Add support for Jira Issue handler (#14728)

* Add support for Jira issue handler

* Address jira issue handler pr comments

(cherry picked from commit 95f7884)

Co-authored-by: Jameer Pathan <[email protected]>
  • Loading branch information
Satellite-QE and jameerpathan111 authored Apr 19, 2024
1 parent 292ba8d commit 9e6f59b
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
env:
PYCURL_SSL_LIBRARY: openssl
ROBOTTELO_BUGZILLA__API_KEY: ${{ secrets.BUGZILLA_KEY }}
ROBOTTELO_JIRA__API_KEY: ${{ secrets.JIRA_KEY }}

jobs:
codechecks:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
id: cscheck
env:
ROBOTTELO_BUGZILLA__API_KEY: ${{ secrets.BUGZILLA_KEY }}
ROBOTTELO_JIRA__API_KEY: ${{ secrets.JIRA_KEY }}

- name: Customer scenario status
run: |
Expand Down
5 changes: 5 additions & 0 deletions conf/jira.yaml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
JIRA:
# url default value is set to 'https://issues.redhat.com' even if not provided.
URL: https://issues.redhat.com
# Provide api_key to access Jira REST API
API_KEY: replace-with-jira-api-key
115 changes: 111 additions & 4 deletions pytest_plugins/metadata_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,22 @@
from robottelo.config import settings
from robottelo.hosts import get_sat_rhel_version
from robottelo.logging import collection_logger as logger
from robottelo.utils.issue_handlers.jira import are_any_jira_open

FMT_XUNIT_TIME = '%Y-%m-%dT%H:%M:%S'
IMPORTANCE_LEVELS = []
selected = []
deselected = []


def parse_comma_separated_list(option_value):
if isinstance(option_value, str):
if option_value.lower() == 'true':
return True
if option_value.lower() == 'false':
return False
return [item.strip() for item in option_value.split(',')]
return None


def pytest_addoption(parser):
Expand All @@ -26,6 +39,25 @@ def pytest_addoption(parser):
'--team',
help='Comma separated list of teams to include in test collection',
)
parser.addoption(
'--blocked-by',
type=parse_comma_separated_list,
nargs='?',
const=True,
default=True,
help='Comma separated list of Jiras to collect tests matching BlockedBy testimony marker. '
'If no issue is provided all the tests with BlockedBy testimony marker will be processed '
'and deselected if any issue is open.',
)
parser.addoption(
'--verifies-issues',
type=parse_comma_separated_list,
nargs='?',
const=True,
default=False,
help='Comma separated list of Jiras to collect tests matching Verifies testimony marker. '
'If no issue is provided all the tests with Verifies testimony marker will be selected.',
)


def pytest_configure(config):
Expand All @@ -34,6 +66,8 @@ def pytest_configure(config):
'importance: CaseImportance testimony token, use --importance to filter',
'component: Component testimony token, use --component to filter',
'team: Team testimony token, use --team to filter',
'blocked_by: BlockedBy testimony token, use --blocked-by to filter',
'verifies_issues: Verifies testimony token, use --verifies_issues to filter',
]:
config.addinivalue_line("markers", marker)

Expand All @@ -56,6 +90,57 @@ def pytest_configure(config):
re.IGNORECASE,
)

blocked_by_regex = re.compile(
# To match :BlockedBy: SAT-32932
r'\s*:BlockedBy:\s*(?P<blocked_by>.*\S*)',
re.IGNORECASE,
)

verifies_regex = re.compile(
# To match :Verifies: SAT-32932
r'\s*:Verifies:\s*(?P<verifies>.*\S*)',
re.IGNORECASE,
)


def handle_verification_issues(item, verifies_marker, verifies_issues):
"""Handles the logic for deselecting tests based on Verifies testimony token
and --verifies-issues pytest option.
"""
if verifies_issues:
if not verifies_marker:
log_and_deselect(item, '--verifies-issues')
return False
if isinstance(verifies_issues, list):
verifies_args = verifies_marker.args[0]
if all(issue not in verifies_issues for issue in verifies_args):
log_and_deselect(item, '--verifies-issues')
return False
return True


def handle_blocked_by(item, blocked_by_marker, blocked_by):
"""Handles the logic for deselecting tests based on BlockedBy testimony token
and --blocked-by pytest option.
"""
if isinstance(blocked_by, list):
if not blocked_by_marker:
log_and_deselect(item, '--blocked-by')
return False
if all(issue not in blocked_by for issue in blocked_by_marker.args[0]):
log_and_deselect(item, '--blocked-by')
return False
elif isinstance(blocked_by, bool) and blocked_by_marker:
if blocked_by and are_any_jira_open(blocked_by_marker.args[0]):
log_and_deselect(item, '--blocked-by')
return False
return True


def log_and_deselect(item, option):
logger.debug(f'Deselected test {item.nodeid} due to "{option}" pytest option.')
deselected.append(item)


@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items, config):
Expand All @@ -81,9 +166,8 @@ def pytest_collection_modifyitems(items, config):
importance = [i for i in (config.getoption('importance') or '').split(',') if i != '']
component = [c for c in (config.getoption('component') or '').split(',') if c != '']
team = [a.lower() for a in (config.getoption('team') or '').split(',') if a != '']

selected = []
deselected = []
verifies_issues = config.getoption('verifies_issues')
blocked_by = config.getoption('blocked_by')
logger.info('Processing test items to add testimony token markers')
for item in items:
item.user_properties.append(
Expand All @@ -100,6 +184,8 @@ def pytest_collection_modifyitems(items, config):
for d in map(inspect.getdoc, (item.function, getattr(item, 'cls', None), item.module))
if d is not None
]
blocked_by_marks_to_add = []
verifies_marks_to_add = []
for docstring in item_docstrings:
item_mark_names = [m.name for m in item.iter_markers()]
# Add marker starting at smallest docstring scope
Expand All @@ -113,6 +199,18 @@ def pytest_collection_modifyitems(items, config):
doc_team = team_regex.findall(docstring)
if doc_team and 'team' not in item_mark_names:
item.add_marker(pytest.mark.team(doc_team[0].lower()))
doc_verifies = verifies_regex.findall(docstring)
if doc_verifies and 'verifies_issues' not in item_mark_names:
verifies_marks_to_add.extend(str(b.strip()) for b in doc_verifies[-1].split(','))
doc_blocked_by = blocked_by_regex.findall(docstring)
if doc_blocked_by and 'blocked_by' not in item_mark_names:
blocked_by_marks_to_add.extend(
str(b.strip()) for b in doc_blocked_by[-1].split(',')
)
if blocked_by_marks_to_add:
item.add_marker(pytest.mark.blocked_by(blocked_by_marks_to_add))
if verifies_marks_to_add:
item.add_marker(pytest.mark.verifies_issues(verifies_marks_to_add))

# add markers as user_properties so they are recorded in XML properties of the report
# pytest-ibutsu will include user_properties dict in testresult metadata
Expand Down Expand Up @@ -169,7 +267,16 @@ def pytest_collection_modifyitems(items, config):
deselected.append(item)
continue

selected.append(item)
if verifies_issues or blocked_by:
# Filter tests based on --verifies-issues and --blocked-by pytest options
# and Verifies and BlockedBy testimony tokens.
verifies_marker = item.get_closest_marker('verifies_issues', False)
blocked_by_marker = item.get_closest_marker('blocked_by', False)
if not handle_verification_issues(item, verifies_marker, verifies_issues):
continue
if not handle_blocked_by(item, blocked_by_marker, blocked_by):
continue
selected.append(item)

# selected will be empty if no filter option was passed, defaulting to full items list
items[:] = selected if deselected else items
Expand Down
4 changes: 4 additions & 0 deletions robottelo/config/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@
must_exist=True,
),
],
jira=[
Validator('jira.url', default='https://issues.redhat.com'),
Validator('jira.api_key', must_exist=True),
],
ldap=[
Validator(
'ldap.basedn',
Expand Down
7 changes: 6 additions & 1 deletion robottelo/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,10 +1725,15 @@ class Colored(Box):
),
}


# Bugzilla statuses used by Robottelo issue handler.
OPEN_STATUSES = ("NEW", "ASSIGNED", "POST", "MODIFIED")
CLOSED_STATUSES = ("ON_QA", "VERIFIED", "RELEASE_PENDING", "CLOSED")
WONTFIX_RESOLUTIONS = ("WONTFIX", "CANTFIX", "DEFERRED")
# Jira statuses used by Robottelo issue handler.
JIRA_OPEN_STATUSES = ("New", "Backlog", "Refinement", "To Do", "In Progress")
JIRA_ONQA_STATUS = "Review"
JIRA_CLOSED_STATUSES = ("Release Pending", "Closed")
JIRA_WONTFIX_RESOLUTIONS = "Obsolete"

GROUP_MEMBERSHIP_MAPPER = {
"config": {
Expand Down
4 changes: 2 additions & 2 deletions robottelo/utils/issue_handlers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Issue handler should expose 3 functions.

### `is_open_<handler_code>(issue, data=None)`

e.g: `is_open_bz, is_open_gh, is_open_jr` for Bugzilla, Github and Jira.
e.g: `is_open_bz, is_open_gh, is_open_jira` for Bugzilla, Github and Jira.

This function is dispatched from `robottelo.helpers.is_open` that is also used
to check for status in the `pytest.mark.skip_if_open` marker.
Expand Down Expand Up @@ -78,10 +78,10 @@ Example of `collected_data`:
## Issue handlers implemented

- `.bugzilla.py`: BZ:123456
- `.jira.py`: SAT-22761

## Issue handlers to be implemented

- `.github.py`: GH:satelliteqe/robottelo#123
- `.gitlab.py`: GL:path/to/repo#123
- `.jira.py`: JR:SATQE-4561
- `.redmine.py`: RM:pulp.plan.io#5580
22 changes: 13 additions & 9 deletions robottelo/utils/issue_handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re

# Methods related to issue handlers in general
from robottelo.utils.issue_handlers import bugzilla
from robottelo.utils.issue_handlers import bugzilla, jira

handler_methods = {'BZ': bugzilla.is_open_bz}
SUPPORTED_HANDLERS = tuple(f"{handler}:" for handler in handler_methods)
handler_methods = {'BZ': bugzilla.is_open_bz, 'SAT': jira.is_open_jira}
SUPPORTED_HANDLERS = tuple(f"{handler}" for handler in handler_methods)


def add_workaround(data, matches, usage, validation=(lambda *a, **k: True), **kwargs):
Expand All @@ -16,10 +18,11 @@ def add_workaround(data, matches, usage, validation=(lambda *a, **k: True), **kw
def should_deselect(issue, data=None):
"""Check if test should be deselected based on marked issue."""
# Handlers can be extended to support different issue trackers.
handlers = {'BZ': bugzilla.should_deselect_bz}
supported_handlers = tuple(f"{handler}:" for handler in handlers)
handlers = {'BZ': bugzilla.should_deselect_bz, 'SAT': jira.should_deselect_jira}
supported_handlers = tuple(f"{handler}" for handler in handlers)
if str(issue).startswith(supported_handlers):
handler_code = str(issue).partition(":")[0]
res = re.split(':|-', issue)
handler_code = res[0]
return handlers[handler_code.strip()](issue.strip(), data)
return None

Expand All @@ -29,19 +32,20 @@ def is_open(issue, data=None):
Issue must be prefixed by its handler e.g:
Bugzilla: BZ:123456
Bugzilla: BZ:123456, Jira: SAT-12345
Arguments:
issue {str} -- A string containing handler + number e.g: BZ:123465
data {dict} -- Issue data indexed by <handler>:<number> or None
"""
# Handlers can be extended to support different issue trackers.
if str(issue).startswith(SUPPORTED_HANDLERS):
handler_code = str(issue).partition(":")[0]
res = re.split(':|-', issue)
handler_code = res[0]
else: # EAFP
raise AttributeError(
"is_open argument must be a string starting with a handler code "
"e.g: 'BZ:123456'"
"e.g: 'BZ:123456' for Bugzilla and 'SAT-12345' for Jira."
f"supported handlers are: {SUPPORTED_HANDLERS}"
)
return handler_methods[handler_code.strip()](issue.strip(), data)
8 changes: 4 additions & 4 deletions robottelo/utils/issue_handlers/bugzilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def collect_data_bz(collected_data, cached_data): # pragma: no cover


def collect_dupes(bz, collected_data, cached_data=None): # pragma: no cover
"""Recursivelly find for duplicates"""
"""Recursively find for duplicates"""
cached_data = cached_data or {}
if bz.get('resolution') == 'DUPLICATE':
# Collect duplicates
Expand Down Expand Up @@ -180,15 +180,15 @@ def collect_clones(bz, collected_data, cached_data=None): # pragma: no cover


@retry(
stop=stop_after_attempt(4), # Retry 3 times before raising
wait=wait_fixed(20), # Wait seconds between retries
stop=stop_after_attempt(4),
wait=wait_fixed(20),
)
def get_data_bz(bz_numbers, cached_data=None): # pragma: no cover
"""Get a list of marked BZ data and query Bugzilla REST API.
Arguments:
bz_numbers {list of str} -- ['123456', ...]
cached_data
cached_data {dict} -- Cached data previous loaded from API
Returns:
[list of dicts] -- [{'id':..., 'status':..., 'resolution': ...}]
Expand Down
Loading

0 comments on commit 9e6f59b

Please sign in to comment.